<?xml version="1.0" encoding="UTF-8"?>
  <?xml-stylesheet href="/css/rss-style.xsl" type="text/xsl"?>
  <rss version="2.0"
  xmlns:atom="https://www.w3.org/2005/Atom"
  xmlns:content="https://purl.org/rss/1.0/modules/content/">
  <channel>
  <title>Salma Alam-Naylor's RSS Feed</title>
  <atom:link href="https://whitep4nth3r.com/feed.xml" rel="self" type="application/rss+xml" />
  <link>https://whitep4nth3r.com</link>
  <description>I'm Salma. Web developer. International speaker. Tech educator. Entertainer. Has a weird newsletter. Makes stuff on The Internet.</description>
    
        <item>
          <title>I am in an abusive relationship with the technology industry</title>
          <description>I am writing to you in a moment of intense grief-induced burnout.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/i-am-in-an-abusive-relationship-with-the-technology-industry/</link>
          <guid>https://whitep4nth3r.com/blog/i-am-in-an-abusive-relationship-with-the-technology-industry/</guid>
          <pubDate>Fri, 06 Mar 2026 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Yesterday morning, I clicked on a link to the <a href="https://www.youtube.com/watch?v=uDAS0AER8vY" target="_blank">Official Folk Albums Chart Show</a>. I love folk music, especially the modern fusion stuff. Listening to this very specific genre of music reminds me of my early twenties when I, too, played in a modern fusion folk band. I flicked through the show a little. I think I was only searching for information, but what I found was something more profound.</p><p class="post__p">I landed on a live studio recording of <a href="https://youtu.be/uDAS0AER8vY?si=qdBokwxQ0PYTkG3g&t=426" target="_blank">Seth Lakeman performing Rollback the Years from the 2025 album The Granite Way</a>. I’m a sucker for raw vocal harmonies, and when they kicked in toward the first chorus, I felt tears in my eyes. When I heard the line “Roll the years back to a life we once knew”, I felt something more.</p><p class="post__p">Coming from an organic oral tradition literally meaning “of the people”, folk music is one of the most raw and human-centred genres of music. Folk music has always served as an expressive way to tell stories, share ideas, or protest. When people couldn’t read or write, folk music was a way to communicate. Folk music is about community, hardship, adventure, love, heartbreak, the human condition.</p><p class="post__p">Skipping ahead in the video, I found an excerpt of a track <a href="https://www.youtube.com/watch?v=JUvbTzMqa2k" target="_blank">Lonesome Woods by Hannah James and Toby Kuhn</a>. The opening of the recording features voice, cello and body percussion. There was something very profound that struck me about this track. The arrangement was so simple, yet Hannah’s bare feet on the wooden board and the sound of her hands on her own skin coupled with her silky voice and a single cello made the whole experience feel incredibly raw and real. I felt like I was there with them in the forest clearing. And then the opening lyrics of this track started to make a lot of my feelings make sense.</p><blockquote class="post__blockquote"><p class="post__p">Through lonesome woods I took my way. So dark so dark as dark could be. The leaves were shivering on every tree. Oh don’t you think that griefs me.</p></blockquote><p class="post__p">And then, nearing the <a href="https://en.wikipedia.org/wiki/Golden_ratio" target="_blank">Golden Ratio</a> of the track, Hannah and Toby are on the top of a hill. Hannah has an accordion now and she&#39;s playing her wooden board with shoes on, and they’re playing together, facing each other, and smiling infectiously, fully enjoying this moment of human-centred expression in the sun and the wind. And I wished I were there with them to experience it. And then I cried.</p><p class="post__p">And then I thought for a while. I took myself on a walk. And I thought some more. And then I was able to put into words what I was feeling.</p><p class="post__p">After clicking on this random link shared on the internet on a normal Thursday, I had uncovered a deep grief. A deep grief about how the technology industry has become so abhorrently hostile to the human experience that it has inadvertently distracted me from real and true humanity held deep within art and music; a deep grief borne from realising I thought I’d lost one of the founding concepts of my identity since I first touched a piano at the age of five. I felt all of this even though I am actively engaging in playing music every day.</p><p class="post__p">I think I am in an abusive relationship with the technology industry. Yes, this is probably about AI. <b class="post__p--bold">Stay with me</b>.</p><p class="post__p">Last week I spoke to a very good friend of mine who works as a senior software engineer. They told me that they don’t really write code anymore as part of their job; they only talk out loud to Claude. This surprised me. They also said that now they feel like an administrator rather than a software engineer — someone who merely assigns and reviews tasks — and, despite <b class="post__p--italic">feeling</b> “more productive”, they also don’t feel that good about the whole thing.</p><p class="post__p">But I felt something weird during this conversation. Given I was surprised that this friend had seemingly embraced using AI day-to-day, and given how much I respect them, I suddenly felt like I should be doing the same. I started questioning my worth, my value, my <b class="post__p--italic">productivity</b>. Don’t get me wrong, I’ve tried using AI in my day-to-day activities, albeit <b class="post__p--italic">very lightly</b>. Given the type of work I do, it would be remiss of me not to explore the options. But there’s a deep discomfort that comes with me reaching for generative AI, not least because I have never been able to generate very good results.</p><p class="post__p">Anyway, here’s where the abusive part comes in.</p><p class="post__p">You simply cannot <b class="post__p--italic">breathe</b> without seeing, hearing, or engaging in any kind of technical conversation about AI. AI has dominated the Zeitgeist so catastrophically that the only way to escape is to turn off the WiFi and delete all the apps. Every single piece of fucking software has some kind of shitty AI add-on, forced into your face at regular intervals whilst you’re trying to go about your life or do your job or check your email or write an email or read an email or talk to a human support agent or read a recipe or open an issue on an open-source project or watch a YouTube video or open your IDE or do a fucking internet search. The cognitive overload of AI trying to Make You More Productive™️ whilst you’re actually trying to <b class="post__p--italic">be</b> productive is so shockingly absurd. And yet, we are being made to feel like we are stagnating, being left behind, not good enough, that we are luddites should we not adopt this imposing technology. We are being told we’re missing out, even though we’re probably doing just fine. The technology is gaslighting us.</p><p class="post__p">This week, I spoke to another friend about how their company is now mandating AI usage for all employees. There is no reason for this mandate, only that someone who knows someone said that at another company where everyone adopted AI they were able to fire all the software engineers and make bank. So if I’m reading this correctly, the message is: “You must adopt this tool, or you will be fired. But we’re going to fire you soon anyway. Good luck.”</p><p class="post__p">I do not enjoy using generative AI. Not only because have I seen few benefits for me personally. Not only because has it ruined the experience of trying to search for accurate and reliable information. Not only because of the way the chat-bot-style LLMs are built to project some kind of sickening quasi-human personality. Not only because mandated generative AI use is burning out my friends. Not only because <a href="https://www.google.com/search?q=AI+suicide&sca_esv=3407319d660045c2&biw=1844&bih=1472&tbm=nws&sxsrf=ANbL-n4lXT74-7ZSpfpYANzYOdvONYL8tw:1772808416636&ei=4Oiqac7GJra7hbIPkue14Ac&ved=0ahUKEwjOtrb_wYuTAxW2XUEAHZJzDXwQ4dUDCBA&uact=5&oq=AI+suicide&gs_lp=Egxnd3Mtd2l6LW5ld3MiCkFJIHN1aWNpZGUyCxAAGIAEGJECGIoFMgUQABiABDIIEAAYgAQYxwNI5BZQtQhYyhRwAHgAkAEAmAHjAaAB-wqqAQU4LjQuMbgBA8gBAPgBAZgCB6ACigXCAhAQABiABBixAxhDGIMBGIoFwgIIEAAYgAQYsQPCAgsQABiABBixAxiDAcICDRAAGIAEGLEDGEMYigXCAhEQABiABBiRAhixAxiDARiKBcICDhAAGIAEGJECGLEDGIoFwgIKEAAYgAQYQxiKBZgDAIgGAZIHAzQuM6AHvieyBwM0LjO4B4oFwgcFMS41LjHIBxGACAA&sclient=gws-wiz-news" target="_blank">this technology has caused numerous people to die</a>. Not only because <a href="https://www.bloomberg.com/graphics/2025-ai-impacts-data-centers-water-data/" target="_blank">AI is draining water from areas that need it most</a>. Not only because <a href="https://hai.stanford.edu/ai-index/2025-ai-index-report/economy" target="_blank">billions of dollars are going directly to AI organisations</a> whilst millions of humans exist in poverty. Not only because using AI is an incredibly lonely experience where human collaboration and conversation has been replaced by weird conversations with computer algorithms. Not only because of the implicit bias generative AI displays as a result of the data it was trained on. Not only because people are doing some really weird things with AI like trying to sell apps that keep your dead relatives “alive”. Not only because apparently <a href="https://lifehacker.com/tech/googles-co-founder-says-ai-performs-best-when-you-threaten-it" target="_blank">AI performs better when you threaten it</a>. Not only because the pivot to AI has wrongly centred the worker value conversation around how much an individual can do rather than <b class="post__p--italic">what they can contribute</b> (not much has changed since I wrote <a href="https://whitep4nth3r.com/blog/the-promise-that-wasnt-kept/" target="_blank">this article</a> almost a year ago).</p><p class="post__p">I just enjoyed my work better when I wasn’t bombarded by a single piece of technology 24 hours a day seven days a week, that seems to have very, very bad implications for humans and the planet, actually. I&#39;d rather go back to the era of the JavaScript framework debates.</p><p class="post__p">What’s even more personally terrifying: what if I need to find a new job in the near future? There are seemingly no non-generative-AI-centred options left for someone like me. I’m afraid that every opportunity will either be for a company building some kind of generative AI experience, or one that mandates the use of generative AI in your daily responsibilities, or one that refuses to use AI at the expense of their financial success and the stability of my employment. At this point I cannot escape. I am at the mercy of the profession I chose. I have a family to feed and a mortgage to pay. Retraining is not an option right now. I must force myself to adapt.</p><p class="post__p">And what <b class="post__p--italic">will</b> the future look like when this time comes? Will the industry’s <a href="https://www.media.mit.edu/publications/your-brain-on-chatgpt/" target="_blank">mass cognitive decline</a> be so far gone that none of this will matter anyway?</p><p class="post__p"><b class="post__p--italic">Through lonesome woods I took my way. So dark so dark as dark could be. The leaves were shivering on every tree. Oh don’t you think that griefs me.</b></p><p class="post__p">Anyway. All of this is to say that I would like you to go and listen to some folk music today.</p><hr class="post__hr" /><h2 class="post__h2">Video essay version</h2>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/mD1ecA94kdY?si=8JFTCtKPTq44pBNS"
        title="I am in an abusive relationship with the technology industry (yes this is about AI)"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to make your first contribution to an open source project</title>
          <description>Getting involved in open source doesn't have to be scary! Understand how to find a great project and make your first contribution in this guide.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-make-your-first-open-source-contribution/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-make-your-first-open-source-contribution/</guid>
          <pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I recently (and accidentally) became a maintainer of a new open source project, <a href="https://npmx.dev" target="_blank">npmx.dev</a>. The project’s vision is to provide a new way to browse the npm package registry, with some exciting bonus features.</p><p class="post__p">I’ve always been on the fringes of open source; I’ve contributed to a few projects here and there, such as the <a href="https://github.com/withastro/docs/pull/8476" target="_blank">Astro docs</a>, <a href="https://github.com/GoogleChrome/lighthouse/pull/13412" target="_blank">Google Lighthouse</a>, and some others, and most of my code on GitHub is open source for the wider community. But, contributing to open source projects can be scary: big codebases, unfamiliar tech, lots of new people, and a sea of open issues can make it hard to know where to start.</p><p class="post__p">However, open source isn’t really all about <b class="post__p--italic">code</b>, it’s about <b class="post__p--italic">community.</b> Since my involvement in npmx, I’ve experienced first-hand what a welcoming and inclusive approach to open source can do to empower anyone to make a variety of contributions to help make The Web ecosystem a great place to be and work.</p><p class="post__p">And so, with that experience and some great examples of what good looks like in open source, and because npmx just announced their <a href="https://npmx.dev/blog/alpha-release" target="_blank">alpha release</a>, I thought I’d put together a guide on how to get involved in open source to help you get started.</p><p class="post__p">Throughout this guide, I’ll be using some specific open source terms. Here&#39;s what they mean:</p><ul><li><p class="post__p"><b class="post__p--bold">Open source</b>: a software development model that encourages open collaboration, with source code, documentation, and discussion around the development of the project freely available to the public.</p></li><li><p class="post__p"><b class="post__p--bold">Issue</b>: A ticket used to track bugs, features, questions, or tasks.</p></li><li><p class="post__p"><b class="post__p--bold">Pull Request (PR)</b>: a request to merge any changes into the main project, and to review and discuss those changes.</p></li><li><p class="post__p"><b class="post__p--bold">Maintainer</b>: a key contributor to the project who leads, manages, and steers the direction of the project. Maintainers are responsible for reviewing and merging pull requests, managing issues, and taking care of the community, usually acting as the final decision-makers. Some maintainers may also be called <b class="post__p--bold">stewards</b>.</p></li><li><p class="post__p"><b class="post__p--bold">Contributor</b>: anyone who has contributed anything to an open source project, whether it is code, documentation, testing and reviewing pull requests, design, finding issues, community management, or anything else related to the project.</p></li></ul><p class="post__p">Where this guide provides specific examples on things such as pull requests and issues, the examples will be specific to <a href="https://github.com" target="_blank">GitHub</a>, as that&#39;s where all the action is happening for npmx.</p><h2 class="post__h2">1. Find a cool project</h2><p class="post__p">The first step is to find a project that you are excited about. Maybe it’s a library or tool you already use that you want to help improve, or perhaps its a project that you really want to see succeed, or it could be that some of your friends are working on something fun.</p><p class="post__p">Your interests matter more than the popularity of the project, and your time is valuable. If you care about the problems that the project is solving, you’ll be more motivated to contribute and invest more of your time in the project. </p><blockquote class="post__blockquote"><p class="post__p">Making ten contributions to a single project is much more valuable for <u>you</u> than making ten contributions to ten different projects.</p></blockquote><p class="post__p">And don’t worry if you don’t feel like you’re unqualified to contribute: you are.</p><h2 class="post__h2">2. Read the community guidelines</h2><p class="post__p">Before making your first contribution, look for the project’s community guidelines or code of conduct. You can usually find this type of document in the root of a project repository, named <code>CODE_OF_CONDUCT.md</code> or similar.</p><p class="post__p">A code of conduct should help you understand what you can expect from the community: how people treat each other, how conflict is handled, and what behaviour is encouraged or discouraged. A great example is the <a href="https://github.com/npmx-dev/npmx.dev/blob/main/CODE_OF_CONDUCT.md" target="_blank">npmx code of conduct</a>, outlining the community’s standards and the responsibilities of the project maintainers.</p><p class="post__p">If your chosen project doesn’t have a code of conduct or community standards, you might consider skipping it. That being said, your first contribution <b class="post__p--italic">could</b> be to create these community standards from one of the <a href="https://opensource.guide/code-of-conduct/" target="_blank">many templates available</a>. If you’re comfortable agreeing to the code of conduct for your chosen project, move on to the next step!</p><h2 class="post__h2">3. Browse closed pull requests</h2><p class="post__p">Take a look at some closed pull requests to help you understand more about the project culture and direction, and how maintainers and contributors work together. It will also give you an idea of how the community standards are being upheld. Some questions you can ask yourself:</p><ul><li><p class="post__p">What kind of work is being contributed?</p></li><li><p class="post__p">Are reviews kind and constructive?</p></li><li><p class="post__p">How are mistakes or incomplete work handled?</p></li><li><p class="post__p">If the project is busy, how are multiple pull requests addressing the same issue handled?</p></li><li><p class="post__p">What’s missing from contributions?</p></li></ul><p class="post__p">If you’re browsing a project on GitHub, you can find the closed PRs by navigating to the pull requests page, and filtering by closed.</p><img src="https://images.ctfassets.net/56dzm01z6lln/D1Wgk9qbrndiwTKKqzgXE/625120061a76dc7e402fe555906508b0/npmx_closed_prs.png" alt="A list of closed PRS on GitHub from the npmx.dev project. This filter is achieved by clicking on the closed filter at the top of the list." height="3944" width="6944" /><h2 class="post__h2">4. Check the contributors list</h2><p class="post__p">Next, take a look at the contributors list. A healthy and active project often has:</p><ul><li><p class="post__p">Many contributors (not just one or two people doing everything)</p></li><li><p class="post__p">Recent activity</p></li><li><p class="post__p">A visible breadth of contributors</p></li></ul><p class="post__p">While you can’t conclude everything from avatars and usernames, immediate visual diversity can be a strong signal that the project is welcoming to all kinds of people. This, in my opinion, always makes for a more exciting project to get involved with. It&#39;s been really wholesome to see <a href="https://npmx.dev/about" target="_blank">how many contributors have been working on npmx so far</a>.</p><h2 class="post__h2">5. Explore open issues</h2><p class="post__p">Exploring open issues will help you get an idea of what needs to be worked on. Issues may be bugs, tasks, larger feature requests, or discussions about ideas that are not yet set in stone. Project maintainers will often use helpful issue labels to categorise issues based on skills or areas of the project. When you’re just starting out, it’s a good idea to look for issues with the label <code>good first issue</code>, or you could filter issues by your area of expertise.</p><p class="post__p">For example, when joining a new project, I often use the issue filter to surface good first issues, or issues labelled <code>front</code> or <code>front end</code>, <code>a11y</code> or <code>accessibility</code>, or <code>docs</code>, as those are my areas of expertise. Another good label to look for is <code>help wanted</code>, as this type of issue is usually more of a discussion around planning an approach to solving a problem.</p><img src="https://images.ctfassets.net/56dzm01z6lln/67uPErL1ujy1F6ktrlHNv3/af7f2adc0e4f7a47fffe3d9375b8aa0e/npmx_goodfirstissues.png" alt="A list of issues labelled with good first issue on the npmx.dev project on GitHub. This view is achieved by using the filter options in the UI, and their configuration is represented as a text query in the search box at the top of the list." height="3944" width="6944" /><p class="post__p">For your first contribution, I recommend choosing a small, low-lift issue. A lot of the time investment when making your first contribution is taking time to understand the code or the documentation, learning the process of contributing, and ultimately, building relationships with other contributors and maintainers as you move through the process. Choosing a smaller issue means less cognitive overheads for you, and more opportunity to make a meaningful impact. For example, my first contribution to npmx was <a href="https://github.com/npmx-dev/npmx.dev/pull/514" target="_blank">a pull request to fix the alignment of the logo in the header</a>. This pull request fixed <a href="https://github.com/npmx-dev/npmx.dev/issues/498" target="_blank">a very small issue</a> that was reported by another contributor that allowed me to open my first pull request with minimal effort.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3jua10z3MkxkibitZ3LBJi/073fa0921ba818357a3020ac6e0d59d7/npmx_firstpr.png" alt="Screenshot of my a PR in GitHub with the title fix: center align icon in nav, which shows I added two CSS classes, flex and items-center to a div inside the header." height="3944" width="6944" /><p class="post__p">In a very active open source project, there may be multiple open pull requests that address the same issue. This could feel a little daunting at first (and it’s sometimes tricky for maintainers to manage), but it’s usually not a big deal and can be managed effectively. If you’re worried about this and don’t want to attempt something that is already in progress, GitHub helpfully identifies which issues have open PRs on the issues list with an icon, which I’ve highlighted on the image below.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2W6utCQZtGPZQMvkUw5dor/a28a961db4a1629d0c1f865f5474827d/npmx_openissues.png" alt="A list of open issues on the npmx.dev repository." height="3944" width="6944" /><h2 class="post__h2">6. Read the contributing guide</h2><p class="post__p">When you’ve found an issue you’d like to make a contribution for, it’s time to understand how the process works. Well-maintained open source projects will have a contribution guide in the project, which you can usually find in the root of a project repository, named <code>CONTRIBUTING.md</code>. A contributing guide is usually geared towards contributing to code or documentation, and will include instructions on how to set up the project, maintaining a consistent code style, testing, and how to format your pull requests.</p><p class="post__p">If you get stuck or notice something confusing or outdated whilst you’re trying to set up the project, you may have found your first opportunity for a contribution! In fact, when setting up the npmx project, I wasn’t sure how the code formatting worked because it wasn’t clear in the contributing guide. And so, after discussing with other maintainers, I opened <a href="https://github.com/npmx-dev/npmx.dev/pull/523" target="_blank">a pull request to clarify how code is formatted</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6QDhbG6GccnVhjj7P4wB6r/390847a8d99e75877861c412362fb2cf/npmx_contributingpr.png" alt="A merged PR on GitHub showing I added the following text to the contributing.md: When committing changes, try to keep an eye out for unintended formatting updates. These can make a pull request look noisier than it really is and slow down the review process. Sometimes IDEs automatically reformat files on save, which can unintentionally introduce extra changes. To help with this, the project uses `oxfmt` to handle formatting via a pre-commit hook. The hook will automatically reformat files when needed. If something can’t be fixed automatically, it will let you know what needs to be updated before you can commit. If you want to get ahead of any formatting issues, you can also run `pnpm lint:fix` before committing to fix formatting across the whole project." height="3944" width="6944" /><h2 class="post__h2">7. Chat with the community</h2><p class="post__p">Many projects have a Discord, Slack, or other space for chatting with the community, which will usually be linked somewhere in the project repository or in the project itself. If you want help or just want to chat about the project and what issues would be good to tackle, it’s a great idea to join one of those spaces. It’s also a good opportunity to check that the community standards are being upheld and that you feel welcomed by the maintainers and contributors when you join.</p><p class="post__p">Also, don’t feel like you need to jump in and get involved straight away. It can be daunting to jump into an active chat stream of people who are actively working on a project. Take your time, and do a little lurking until you feel comfortable enough to take part.</p><h2 class="post__h2">8. Open a pull request</h2><p class="post__p">If you’ve found an open issue and you know how to tackle it, you can usually do the work and open a pull request without asking for it to be assigned to you. That being said, in a very active project it is always helpful to leave a comment on an issue stating that you intend to work on it.</p><p class="post__p">Communication in open source is key, especially with many people working asynchronously on a project across multiple time zones, day and night. But on the other hand, a very active project can feel very noisy and overwhelming, and more often than not, maintainers will appreciate your initiative when it comes to direct contributions, rather than the extra admin of assigning people to issues, and the extra time it takes for contributors to wait for permission.</p><p class="post__p">To open a code-based pull request, <a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo" target="_blank">fork the repository</a> to your own GitHub account using the fork button on the main page of the repository, and clone your fork of the repository to your local machine. </p><img src="https://images.ctfassets.net/56dzm01z6lln/2CKxiSx1hsggxcDwW5SaDX/b7370054e66cb1dbc6ea799bbd68db15/npmx_fork.png" alt="The fork button at the top of the npmx repository showing that I have one existing fork." height="3944" width="6944" /><p class="post__p">When you&#39;ve cloned the repository to your local machine, create a new branch off of main to make your changes. When you push those changes to your own GitHub fork, you&#39;ll see a banner at the top of your repository page with a &quot;Compare &amp; pull request&quot; button.</p><img src="https://images.ctfassets.net/56dzm01z6lln/FeQpEal12xViXIHwkjDkJ/cf4b13a12df31c6f064dd97b49d4da92/npmx_prcta.png" alt="A banner at the top of my fork of the npmx repository showing that fix-123 had recent pushes 11 seconds ago, with the compare and pull request button to the right." height="3944" width="6944" /><p class="post__p">Click this button to write a title and description for your PR, making sure to follow the contributing guidelines. Then, click &quot;Create pull request&quot;. In an active project, you probably won&#39;t need to message the maintainers to notify them that you&#39;ve submitted a PR, as they&#39;ll be watching for pull requests coming in. I know you&#39;re probably really excited to make your first contribution, but be patient in waiting for a review and approval!</p><img src="https://images.ctfassets.net/56dzm01z6lln/4jBrErMcMCwGWYqHBb9ffI/1cca0821bc34a07ed97949698a175e32/npmx_openpr.png" alt="A PR title fix: update readme with the description: this PR adds clarity to the README about submitting pull requests, fixes #123." height="3944" width="6944" /><h2 class="post__h2">9. Not every PR needs an issue</h2><p class="post__p">If you spot a typo, a small refactor, or an improvement worth making, you can propose it directly with a PR rather than creating an issue. Make sure to include a clear description of the change and why it’s useful, especially if it’s your first contribution. Even if your pull request is not accepted, you’ve practised the contribution workflow, so you’re ready for the next issue.</p><p class="post__p">Even if someone else submits a similar PR before you, your work won’t be wasted. Maintainers may still merge your pull request, borrow ideas from it, or use it to improve the final solution. This happened to me on an early PR for npmx, where I made <a href="https://github.com/npmx-dev/npmx.dev/pull/545" target="_blank">a small change to the Open Graph images for the package pages</a>, which will eventually be superseded by a full redesign of the Open Graph images across the project.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2xuSgZP0kZlOBKQVNW7GqH/ca7e21f9abc3ead4949a54a311512d7f/npmx_starspr.png" alt="A merged PR on GitHub to the npmx.dev repository showing I added an inline star SVG to the open graph image, with the number of stars to the right of it." height="3944" width="6944" /><h2 class="post__h2">10. Not every contribution has to be code</h2><p class="post__p"><b class="post__p--bold">In open source, all contributions are valuable,</b> whether or not they make it into the project codebase or documentation. Finding and reporting issues, requesting or suggesting features, reviewing and testing pull requests, opening PRs that don’t get merged, updating documentation, and welcoming people into the community are all valuable ways to get involved. Participating in open source projects gives you a wealth of opportunities to use your existing strengths, or learn new skills to add to your experience.</p><h2 class="post__h2">Get involved</h2><p class="post__p">In my experience, open source really is about humans, rather than code. Open source projects have huge potential to bring people together and solve problems for the people that work on The Web, making the ecosystem (and, optimistically, the world) better for everyone to benefit.</p><p class="post__p">If you’re looking for a great open source experience to get involved in, I would highly recommend the npmx project, who welcomed me with open arms from the outset. Find an open issue, give it a try, and join us in the <a href="https://build.npmx.dev/" target="_blank">npmx Discord</a> to experience a truly wholesome community approach to building great tools for The Web. Start small, and have fun!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>I built a website for drummers</title>
          <description>How am I meant to enjoy this, without any SAUCE?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/website-for-drummers-free-pdf-downloads-transcriptions/</link>
          <guid>https://whitep4nth3r.com/blog/website-for-drummers-free-pdf-downloads-transcriptions/</guid>
          <pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I&#39;ve been a musician for 35 years. Growing up I played the piano, flute, piccolo, guitar, bass guitar and saxophone, my degree was in music composition, and in 2023 I started learning how to play the drums. Since starting drum lessons, I&#39;ve been eager to learn to play a variety of different songs in eclectic styles. Given my musical history and training, it&#39;s been fairly straightforward for me to pick up reading drum notation, and I&#39;d often go in search of &quot;free drum transcriptions&quot; online to print out and play.</p><p class="post__p">However, many of the songs I&#39;ve wanted to learn over the years have been impossible to find transcriptions for; most drum transcription sites tend to hold catalogues of the rock classics in bulk, rather than niche jazz performances by emerging artists, for example. There&#39;s also an increasingly frightening amount of &quot;AI generated drum transcription tools&quot; surfacing on The Internet, and we all know how useful that&#39;s going to be (not useful).  And so, I took to transcribing the songs I wanted to learn myself. </p><p class="post__p">It turns out (according to my drum teacher), that I&#39;m pretty darn good at transcribing drum tracks, and I <b class="post__p--italic">really</b> enjoy it. Slowing the tracks down using the <a href="https://apps.apple.com/us/app/amazing-slow-downer/id910469258" target="_blank">Amazing Slow Downer</a>, carefully listening to the intricacies of the part, and translating it to a written score has given me a deeper insight into a variety of playing styles and techniques, and as a result it&#39;s really helped improve my playing. At some point I thought to myself: <b class="post__p--italic">I can&#39;t be the only one searching for these weird and obscure songs to play drums to, right?</b></p><p class="post__p">And so, I decided to put my drum transcriptions on The Internet, on a brand new Website of their own, for everyone in the world to enjoy.</p><p class="post__p"><b class="post__p--bold">Today, I&#39;m releasing </b><a href="https://drumsauce.net" target="_blank"><b class="post__p--bold">DRUMSAUCE</b></a><b class="post__p--bold">, a catalogue of free PDF drum transcriptions, created by my own human ears and hands.</b></p><img src="https://images.ctfassets.net/56dzm01z6lln/3IIPcH6Mhseis4CMFV7SHT/ad1276ea0899149dd8cddb380e72ed90/drumsauce_ss.png" alt="Screenshot of drumsauce.net, showing the drumsauce logo in the top left with reddish pink dripping sauce, the menu of links below it, and on the right, a large h1 that says free drum transcriptions, and you can see two latest transcriptions below with album art and some information about each one." height="2572" width="4112" /><h2 class="post__h2">About the website</h2><p class="post__p">The website consists of five main areas. The <a href="https://drumsauce.net/" target="_blank">home page</a> shows the three latest transcriptions, there&#39;s a <a href="https://drumsauce.net/" target="_blank">transcriptions</a> area, which currently shows all transcriptions (there are only three so far!) but will need some categorising, searching and filtering at some point, but I didn&#39;t want to over-engineer it too soon. There&#39;s the single transcription page, such as <a href="https://drumsauce.net/transcriptions/salin-painted-lady/" target="_blank">Painted Lady by Salin</a> which contains the album cover art, a preview of the first page of the PDF, a download link, and an embedded YouTube video. There&#39;s a little <a href="https://drumsauce.net/about/" target="_blank">about page</a>, and finally, a <a href="https://drumsauce.net/request/" target="_blank">request form</a> where you can make a transcription request. Additionally, if you&#39;re into RSS feeds, you can also subscribe to the <a href="https://drumsauce.net/rss.xml" target="_blank">DRUMSAUCE RSS feed</a>!</p><img src="https://images.ctfassets.net/56dzm01z6lln/hlFZck8kvCNZjvPS0SPoX/c5b9e595a25c3904f1e4beea1a5c21d1/salin_pl_ss.png" alt="Screenshot of a single transcription page, showing the logo and navigation on the left, and on the right there's a header section with the album cover art, large title, some meta data including genre and description, and below that you can see the first page of the PDF." height="2572" width="4112" /><h2 class="post__h2">The tech</h2><p class="post__p">Honestly, I don&#39;t want to talk too much about the tech behind the website, because ultimately it doesn&#39;t really matter. I used a framework to create a static site (and the astute among you will inspect the DOM to see what framework that is, or you know, stalk me on GitHub), and I deployed it to a well-known deployment platform that has support for forms.</p><p class="post__p">However, I will mention that <a href="https://www.npmjs.com/package/pdfjs-dist" target="_blank">pdfjs-dist </a>is a great little library for working with PDFs on the web, and it&#39;s what powers the PDF preview on the transcription pages. I thought about using an image as the preview instead, but I didn&#39;t want to create too much extra effort for myself when adding new transcriptions to the site. All I need to do when adding a new transcription is:</p><ul><li><p class="post__p">add a new object to a <code>data.json</code> file that contains an array of transcriptions</p></li><li><p class="post__p">find the <a href="https://musicbrainz.org/" target="_blank">MusicBrainz</a> release ID for the track</p></li><li><p class="post__p">upload the new pdf to the repo</p></li></ul><p class="post__p">Regarding the MusicBrainz release ID, I use this to show the album cover art for the transcription. Ideally, I wouldn&#39;t be requesting resources from random third-party remote servers. However, I did this to minimise effort on my part. When the list of transcriptions fills up, this may have to be revisited.</p><h2 class="post__h2">You can just make stuff</h2><p class="post__p">It&#39;s bewildering to think that in the 19 days since I wrote my <a href="https://whitep4nth3r.com/blog/this-is-not-a-2025-wrap-up-post/" target="_blank">(not) a 2025 wrap up post</a>, where I bemoaned ungraciously that I had zero time for creative projects, <a href="https://whitep4nth3r.com/blog/website-redesign-2026/" target="_blank">I completely redesigned my website</a> and launched a whole new project. I guess this is my rebellion era.</p><p class="post__p">p.s. I also made <a href="https://www.fretonator.com/" target="_blank">a website for guitarists</a> in 2020.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>I tracked everything I wore in 2025. Was it worth it?</title>
          <description>Habit-tracking can feel useful and insightful. But how does it help us, really?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/i-tracked-everything-i-wore-in-2025/</link>
          <guid>https://whitep4nth3r.com/blog/i-tracked-everything-i-wore-in-2025/</guid>
          <pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Since 2023, I’ve been using the <a href="https://www.myindyx.com/" target="_blank">Indyx</a> mobile app to catalogue items in my wardrobe. Over the years I’ve tried quite a few different wardrobe cataloguing apps, but Indyx is the one that stuck, and for one big reason: it is the only wardrobe cataloguing app I’ve tried that doesn’t try to encourage you to <b class="post__p--italic">buy more stuff</b> at every opportunity. This is echoed in Indyx’s core principle, taken from their website home page:</p><blockquote class="post__blockquote"><p class="post__p">The fashion industry has trained us to think that the only way to feel stylish is to buy something new. Indyx breaks that fast-fashion cycle by celebrating the closet you have and teaching you how to shop smarter for things you love and will actually wear.</p></blockquote><p class="post__p">Prior to 2025, I had only used Indyx to catalog my wardrobe and keep track of everything I <b class="post__p--bold">owned</b>. But I didn’t really understand how I <b class="post__p--italic">utilised</b> my wardrobe, and whether everything I had was serving me. And so, on 1st January 2025 I decided to start tracking every single item and outfit I wore for the entire year to see what secrets it would unfold.</p><h2 class="post__h2">My tracking goals</h2><p class="post__p">I love my clothes, and I love fashion as a vehicle for self-expression and exploration. But, since my teenage years I have had the tendency to impulse-buy individual items on a whim without any thought to how wearable those items are with the rest of my wardrobe. I also have a track record of impulsively reinventing myself through the vehicle of clothing over and over again, which manifests in selling most of my current wardrobe and purchasing new or second-hand things to replace them in a completely different style. I’ve experienced this cycle a number of times in my adult life. More sensible people will ensure that new purchases can be worn with existing items before they hand over their hard-earned money, but I was never really a sensible shopper. I wanted beautiful things, regardless, and I wanted them now.</p><p class="post__p">With all that said, my goals with regards to tracking everything I wore in 2025 were an attempt to curb this cycle of behaviour, and address three main goals:</p><ul><li><p class="post__p">I wanted to buy less</p></li><li><p class="post__p">I wanted to wear more of what I had</p></li><li><p class="post__p">I wanted to understand how I utilised my wardrobe</p></li></ul><h2 class="post__h2">Adding items and oufits to the app</h2><p class="post__p">Adding items to the Indyx app is pretty straightforward. Either upload an image or take a photo of an item laying flat, and the app will remove the background to make it look as good as possible. I prefer to add official product images to the app, because they look better all lined up together, but where I couldn’t find a flat-lay official product image, I took a photo (I still don’t like how these look).</p><img src="https://images.ctfassets.net/56dzm01z6lln/2NrD5AZdW9cPI2FiWwnQsN/42743bb7ec16d886f081ad1898fbecfc/indyx_items_screen.png" alt="Screenshot from the Indyx app showing 9 of my items. A leather jacket, pair of boots, a sweatshirt, black top, a jumper, a dark leopard print tote bag and three pairs of jeans in blue, black and grey." height="2586" width="1279" /><p class="post__p">When you add an item, there are a bunch of other optional details you can fill in, such as whether the item was second hand, the date you acquired it, colours, tags, and the price you paid. When you add a price, Indyx calculates the “cost per wear” based on how many times you wore an item. When using the app for the first time in 2023 and cataloguing all items, I wasn’t often sure of the purchase dates, so I opted not to add any dates for any of the items, which is why the image below states that I wore these boots 44 times in 0 days. An improvement I would like to see here is for the acquired date to fall back to the date the item was created in the app. </p><p class="post__p">All of this cataloguing is quite a lot of admin, and I the only thing I have found useful is how many times I wore each item. I never need to filter items by colour, for example, because most of my items are black.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7nmr8LK5mpXHAl9WTYY7CG/70ce93995499b7c189857c6a26ff78c1/boots_montage.png" alt="A mash up of four screens from the app showing a pair of Doc Martens boots. Screens include the item photo, cost info, product info, and outfits the boots are included in." height="1301" width="2580" /><p class="post__p">All items are available to view on the main screen, where you can search and filter items or customise the view (which is behind an <b class="post__p--italic">Insider</b> subscription, for some reason). You can also create collections of items, which have been really useful when deciding what to pack without looking through your wardrobe for all the work travel I did in 2025.</p><p class="post__p">You add outfits in a separate area of the app, and you can arrange and resize items to your liking on the canvas.</p><img src="https://images.ctfassets.net/56dzm01z6lln/c1FlTZ6EzXRIkGEMWHw8T/c37c52f0974588894e424e10560c7fc3/indyx_edit_outfit.png" alt="The edit outfit in Indyx, showing black jeans and black long sleeved top arranged on the left, and a black and white patterned cardigan and black belt on the right." height="2557" width="1290" /><h2 class="post__h2">How tracking works</h2><p class="post__p">You can track items and/or outfits you wear by clicking on the calendar icon at the top right of the main screen, clicking on the day, and selecting items and outfits. You can also add a selfie of your outfit each day, but I didn’t decide it was useful for me; it was also a little annoying to have to skip the mandatory selfie screen each day. Given you may add a number of outfits or items per day, you can also choose a specific item or outfit to act as the cover image for the calendar view.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6XauinDlX67drSFK3LSutE/397417fe5d4c6034dfa6f06aa0fa2739/indyx_october_2025.png" alt="Screenshot from the calendar part of the Indyx app, showing all the base outfits I wore in October 2025." height="2287" width="1290" /><h2 class="post__h2">What I tracked</h2><p class="post__p">Given I work from home and sometimes don’t leave the house in a day, I added a base outfit first thing in the morning, without any accessories such as shoes or outerwear. I always wear a necklace, but I didn’t add jewellery to the outfits to avoid having to create a whole separate outfit if I decided to wear a different necklace with the same base outfit one day.</p><p class="post__p">Alongside the base outfit, I added other single items such as shoes, coats and jewellery as I wore them on any particular day. Most days I would select an existing outfit or create a new outfit after I’d got dressed, and as I got ready to leave the house, I would make sure I added the coat, shoes, bag or any other accessory that I decided to wear. When I went <b class="post__p--italic">out-out,</b> such as for a special occasion or travel for a work trip, I tracked the full outfit including all accessories, shoes and outerwear. I’m not sure of this logic or why I chose to do it this way.</p><p class="post__p">All of this admin was inconvenient at first, but it quickly became muscle memory to keep track of everything as I wore it. The only thing I didn’t keep track of were gym/yoga outfits, but I did keep track of when I got changed during the day, such as when I changed into more comfortable clothes such as hoodies and tracksuit bottoms in the evening after work.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3vR19U3FS8wt1KMPups7Vs/6c943243c335e4f7f9caffd3babfc5dc/indyx_nov_7_2025.png" alt="Screenshot from the tracked items from November 7th 2025, showing a base outfit of black jeans, black long sleeved top, black cardigan and black belt. Other items also added are a pair of black boots, a khaki green coat, and a silver skull necklace." height="2575" width="1290" /><h2 class="post__h2">What I learned</h2><p class="post__p">Honestly, the only new thing I learned was the total estimated cost of my wardrobe, which I had been ignoring for a very long time. Given this number accumulated over many years, however, I’m not even sure it’s that important.</p><h2 class="post__h2">Was it worth it?</h2><p class="post__p">We only have so much time in a day. If I added up all the time I spent clicking around in Indyx to catalogue clothing, finding a good official flat lay product image on The Internet and adding all the required meta information to each item, putting together outfits every morning that didn’t already exist, and cataloguing items each time I put on a jacket and shoes to leave the house or when I put my lounge clothes on after work, maybe I could have read a whole book, or, you know, just had a little bit more time to relax in a day.</p><h3 class="post__h3">Did I buy less?</h3><p class="post__p">Qualitatively, I feel like I curbed my impulsive buying somewhat. One of the main catalysts for this was that when I found something I liked, I could look through the app to see if I had something similar, and I usually did, so I didn’t buy the new thing. Also, at the end of December, I did my six-monthly wardrobe spring clean, where I take everything out, decide if items are still serving me, give the wardrobe a vacuum to get rid of the accumulated dust, and put it all back in again. Usually, my bi-annual clear outs involve a lot of selling and donating, but this time, I only wanted to get rid of one oversized sweatshirt (because it really wasn’t the right colour for me, and it was really very too big), which I actually just gave to my husband.</p><p class="post__p">Quantitively, the data also shows good results in the area. Given I’ve been tracking the cost of each item in the app since 2023, the data shows in 2025, I spent <b class="post__p--bold">45% less</b> than in 2024, and <b class="post__p--bold">20% less</b> than in 2023. Whether this is a result of tracking in the app, a result of wanting to be more intentional in how I curate my wardrobe, or a bit of both, is unclear.</p><h3 class="post__h3">Did I wear more?</h3><p class="post__p">I didn’t realise this in 2025, but I actually feel like I have <b class="post__p--italic">more</b> flexibility in what I wear since I <b class="post__p--italic">stopped</b> tracking on 1st January 2026. Looking back, I feel like I wore less of my wardrobe over the year, or at least <b class="post__p--italic">fewer combinations of items</b>. If I was in a rush and I didn’t have the time to create a new outfit in the app, I would instead reach for an outfit that already existed in the app so I could assign it to the day in a couple of clicks, rather than wear what I actually wanted to wear. In hindsight, tracking everything constrained my creativity and expression a little.</p><h3 class="post__h3">Do I understand how I utilised my wardrobe?</h3><p class="post__p">As is seemingly tradition for many tech companies these days, Indyx provided a “wrapped” at the end of 2025, which consisted of a few slides of how you used your wardrobe. At the time of writing I cannot access my wrapped again, but all I can remember is that I wore around 2% of all possible 11k+ outfit combinations (I’m not sure how this is calculated but <b class="post__p--italic">surely</b> I don’t possess 11k+ <b class="post__p--italic">sensible</b> outfit combinations), and that my most worn item is my black belt, which I wore 118 times since I bought it in on sale in summer 2025, and it came in at 31p per wear.</p><p class="post__p">With regards to usage tracking, which you only have access to with an Insider subscription (which is currently £60 a year), the data hasn’t proved that useful. This is mainly because it doesn’t provide any actionable insights, and I’m not sure that it can. The composition data is just data and I’m sure is pretty representative of any standard wardrobe.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6PGMCdyDuZD1gfJHj0lnfB/ae76bec603f98d29adfe736f134e391c/indyx_composition.png" alt="Screenshot of the composition screen in Indyx, showing the breakdown of my wardrobe. 35% top, 17% jewellery, 13% bottom, 10% shoes, 8% accessory, 6% outerware, 6% one piece, 4% bag, 1% swim, 0% other." height="2660" width="1290" /><p class="post__p">The utilisation statistics are encouraging, though. Indyx describes (in the question mark tooltip) how utilisation is calculated: “Items that were logged to your calendar at least once in the defined period divided by the total number of items in your wardrobe. A higher % means that you’re actively wearing more of your wardrobe.” According to this data, I wore 90% of my wardrobe in 2025, which, despite not wearing things because I couldn’t be bothered to log them to the app, and <b class="post__p--italic">feeling like I didn’t wear as much of my wardrobe</b>, is actually pretty good going.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1NLs9nkCdRgXo9JINbmf2s/48ea9df5d57db9b3806de509e1f85edc/indyx_usage.png" alt="Usage screen from Indyx, showing that in the last one month I used 31%, in three months I used 49%, in 6 months I used 75%, and in the last year I used 90%." height="1519" width="1290" /><h2 class="post__h2">Final thoughts</h2><p class="post__p">I feel pretty accomplished in that I achieved my goal of tracking everything for a whole year, that I spent less on clothes in 2025, and that I wore 90% of my wardrobe during the year. Again, I’m not sure than the latter two achievements are entirely related to tracking everything in Indyx, but it probably was contributing a factor.</p><p class="post__p">Would I do something like this again? Probably not. For the last 11 days, I’ve felt so free not having to track everything in the app, but I also can&#39;t help feeling like I’ve forgotten to do something throughout the day, given tracking items became such a big part of my daily routine in 2025.</p><p class="post__p">All in all, I think I can say that I achieved my wardrobe-related goals of 2025. I bought less, I wore more (even though it didn’t feel like it), and I understood how I used my wardrobe, even if the data didn’t give me any actionable insights.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>I redesigned my website (again)</title>
          <description>The Shirt is at the heart of what this website redesign became.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/website-redesign-2026/</link>
          <guid>https://whitep4nth3r.com/blog/website-redesign-2026/</guid>
          <pubDate>Fri, 09 Jan 2026 19:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">It’s been almost <b class="post__p--bold">three years</b> since I redesigned my website. The last <code>major-redesign</code> branch was merged into main on April 26 2023. I spent a good few months on the last redesign, and had lots of excellent design help from an Internet friend, <a href="https://bsky.app/profile/aaoa.design" target="_blank">Anatole</a>. If you read my <a href="https://whitep4nth3r.com/blog/this-is-not-a-2025-wrap-up-post/" target="_blank">(not a) 2025 wrap up post</a>, you’ll know that I’ve been trying to <b class="post__p--italic">rebuild</b> my website since April 2025. I’ve had this vision in my head for so long now that I don’t even know if it is good anymore, but it has been weighing me down and crowding my brain for almost a year because I just couldn’t make any progress.</p><p class="post__p">I say <b class="post__p--italic">rebuild</b> and not redesign because I started to <b class="post__p--italic">actually rebuild</b> my website in <a href="https://nordcraft.com" target="_blank">Nordcraft</a>, given it’s always beneficial to dogfood the products you are working on. However, my current website repo received its first commit in Jan 21 2022 and has received 986 commits since. It is home to a huge variety of content, it has been a personal labour of love of mine, and rebuilding it from the ground-up amidst all my other priorities felt extremely overwhelming.</p><h2 class="post__h2">Personal websites and the evolution of mine</h2><p class="post__p">At the start of the year, my friend Henry posted an incredible essay. In <a href="https://henry.codes/writing/a-website-to-destroy-all-websites/" target="_blank">A Website To End All Websites: How to win the war for the soul of the internet and build the Web We Want</a>, Henry urges us to build <b class="post__p--italic">personal websites</b>. More on this in a moment. But it was reading this article that inspired me to finally make my website what I wanted it to be in the way I wanted to.</p><p class="post__p">Before the redesign, my website was extremely over-engineered and also messy in places, mostly due to how it’s been a playground for experimentation at work over the years. Whilst working at Contentful, Netlify and Sentry I frequently used my website to showcase features of those platforms, which often led to me building some very unnecessary things that no one used. Some of these features were cool, such as little banners based on the referring page powered by Netlify Edge Functions and the HTTP referrer header, but when I was laser-focussed on web performance at Sentry, I ripped all of those cool features out in the quest to get my Time to First Byte (TTFB) as low as possible. I even stopped using custom fonts.</p><p class="post__p">After reading Henry’s essay, it dawned on me that my website was no longer as personal as it could be, given I had used it to do <b class="post__p--italic">job stuff</b> for so long. It had become a mish-mash of performance-hacking to the detriment of design and personality, a bloated wasteland of unvisited URLS such as the “blog posts filtered by topics” pages thanks to a GraphQL trick I wrote about at Contentful, a useless events page that merged my Twitch schedule and conference event entries from a CMS and localised the time for visitors using a Netlify Edge Function that got no traffic whatsoever, and it contained a very corporate article search powered by a post-build script that sent JSON to Algolia that no one ever used except me. And when I talk about the website being over-engineered, boy was it over-engineered. The only instances of hard-coded strings existed as page titles; every other bit of text came from the CMS. This constrained my creativity, especially when it came to the home page.</p><p class="post__p">All this is to say that my website didn’t feel <b class="post__p--italic">personal</b> anymore. And so, this was the main goal of the redesign: to reclaim my personality on my own domain; to free my online home from the shackles of employment-based responsibility; to make something that was mine. Plus, the previous design was intended to be a marketing funnel for my Twitch channel (that’s literally the brief I gave Anatole) given that is what I was focussed on growing at the time. It sounds ridiculous now I type it out loud, but it made sense at the time. A lot of people discovered my website organically through links to my blog posts, and I wanted to make sure they knew I did live coding on Twitch. And now that <a href="https://whitep4nth3r.com/blog/goodbye-twitch/" target="_blank">I’ve retired from Twitch</a>, it seemed fitting to evolve my website into the next thing.</p><p class="post__p">In a true moment of clarity, thanks to Henry, I chose not to <b class="post__p--italic">rebuild</b> my website, but to <b class="post__p--italic">redesign</b> it; the tech stack is still <a href="https://www.11ty.dev/" target="_blank">Eleventy</a>, Contentful and Netlify. <b class="post__p--bold">I did it in a week</b>. I lost sleep, I dreamt about coding, I bugged my friends for their thoughts and opinions and <b class="post__p--italic">I couldn’t think about anything else</b>. But I did it. (I also did it without a designer, which feels like a big deal for me. Maybe my years of experience are starting to pay off.)</p><p class="post__p">As promised, I’d like to share some of the fun things I created in the redesign, and links to all the good articles that helped.</p><h2 class="post__h2">I started with a vibe</h2><p class="post__p">I started this redesign with a <b class="post__p--italic">vibe</b> based around my current Internet profile picture, which is a photo of me taken whilst speaking on stage at the What The Stack conference in Macedonia in September 2025. The talk I gave was set in 1995, and I was wearing a very excellent vintage shirt from the ‘90s to match the vibe. (<a href="https://whitep4nth3r.com/talks/an-introduction-to-the-world-wide-web-for-very-senior-programmers/" target="_blank">Click here to watch the recording.</a>)</p><p class="post__p"><b class="post__p--italic">The Shirt is at the heart of what this website redesign became.</b></p><p class="post__p">The only solid plans I had for the design was to simplify the colour palette and use only <code>#000</code> and <code>#fff</code>, the pink used on my previous design, and a complementary green. I was going for a kind of early 2000s punk/emo gig or music journalism vibe, with big bold lines, contrasting and seemingly chaotic fonts, and grunge-inspired textures.</p><p class="post__p">For the grunge-inspired stuff, I used three little design details.</p><h3 class="post__h3">Distorted text using SVG filters</h3><p class="post__p">You’ll notice that some of the text on the page you’re reading is slightly distorted. Check out the active “Articles” link in the header, for example. This little text distortion effect is created using an SVG filter, which uses <code>feTurbulence</code> and <code>feDisplacementMap</code>. I’m using just one filter across the site for “design consistency” (whatever that means), which lives in the footer, so I can access it on every page.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mPwxsOtRWp"
      aria-describedby="mPwxsOtRWp">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mPwxsOtRWp">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="mPwxsOtRWp" itemprop="text" content="%3Csvg%20width%3D%220%22%20height%3D%220%22%20class%3D%22svg__filter%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3Cfilter%20id%3D%22distort%22%3E%0A%20%20%20%20%20%20%3CfeTurbulence%20%0A%09%20%20%20%20%20%20type%3D%22turbulence%22%20%0A%09%20%20%20%20%20%20result%3D%22noise%22%20%0A%09%20%20%20%20%20%20numOctaves%3D%221%22%20%0A%09%20%20%20%20%20%20baseFrequency%3D%220.01%200.01%22%3E%3C%2FfeTurbulence%3E%0A%20%20%20%20%20%20%3CfeDisplacementMap%0A%20%20%20%20%20%20%20%20in%3D%22SourceGraphic%22%0A%20%20%20%20%20%20%20%20in2%3D%22noise%22%0A%20%20%20%20%20%20%20%20scale%3D%226%22%0A%20%20%20%20%20%20%20%20xChannelSelector%3D%22R%22%0A%20%20%20%20%20%20%20%20yChannelSelector%3D%22R%22%3E%3C%2FfeDisplacementMap%3E%0A%20%20%20%20%3C%2Ffilter%3E%0A%20%20%3C%2Fdefs%3E%0A%3C%2Fsvg%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>svg__filter<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>defs</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>filter</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>distort<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>feTurbulence</span> <br>	      <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>turbulence<span class="token punctuation">"</span></span> <br>	      <span class="token attr-name">result</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>noise<span class="token punctuation">"</span></span> <br>	      <span class="token attr-name">numOctaves</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1<span class="token punctuation">"</span></span> <br>	      <span class="token attr-name">baseFrequency</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0.01 0.01<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>feTurbulence</span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>feDisplacementMap</span><br>        <span class="token attr-name">in</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>SourceGraphic<span class="token punctuation">"</span></span><br>        <span class="token attr-name">in2</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>noise<span class="token punctuation">"</span></span><br>        <span class="token attr-name">scale</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>6<span class="token punctuation">"</span></span><br>        <span class="token attr-name">xChannelSelector</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>R<span class="token punctuation">"</span></span><br>        <span class="token attr-name">yChannelSelector</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>R<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>feDisplacementMap</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>filter</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>defs</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">To apply the SVG filter to an element using CSS, you can use the <code>filter</code> property. For example, here’s how I add the filter to the active link in the header. Notice how the <code>id</code> attribute given to the SVG matches the value of the <code>url</code> value.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="klMwwFqFXs"
      aria-describedby="klMwwFqFXs">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="klMwwFqFXs">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="klMwwFqFXs" itemprop="text" content="%5Bdata-active%3D%22blog%22%5D%20%7B%0A%20%20filter%3A%20url(%23distort)%3B%0A%7D">
      <pre class="language-markup"><code class="language-markup">[data-active="blog"] {<br>  filter: url(#distort);<br>}</code></pre>
    </div>
  </div>

  <p class="post__p">For a more in-depth read about distorting text with SVG, I recommend reading <a href="https://henry.codes/writing/how-to-distort-text-with-svg/" target="_blank">this article by Henry Desroches</a>.</p><h3 class="post__h3">High-contrast and halftone images</h3><p class="post__p">In order to maintain the grunge-inspired feel across the site, I wanted to stylise the images. I thought about getting rid of most of the images, for example on the cards on the list of articles, but the site needed some real-life colour, so I kept them in. An interesting Internet search led me to a very detailed article by Lean Rada on <a href="https://leanrada.com/notes/pure-css-halftone/" target="_blank">Creating a halftone effect with CSS</a>.</p><p class="post__p">Using this article as inspiration, I created my own halftone effect by creating an oversized pseudo element, and paired this with a CSS contrast filter and <code>mix-blend-mode: hard-light</code> to <b class="post__p--italic">make the images really pop</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4DMh69cNXvwSf0itGHd7Xt/c1dcc5caf586174ba1b2bd7d91ce1995/redesign_cards.png" alt="Screenshot of four cards on the blog list page, showing four large images with the halftone and high contrast effect." height="1012" width="1000" /><p class="post__p">I built this as a mixin (yes, I’m still using Sass), and included it anywhere I needed it.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="jmPAQwSpgP"
      aria-describedby="jmPAQwSpgP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="jmPAQwSpgP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="scss">
      <meta data-code-id="jmPAQwSpgP" itemprop="text" content="%40mixin%20half_tone%20%7B%0A%20%20overflow%3A%20hidden%3B%0A%20%20filter%3A%20contrast(150%25)%3B%0A%0A%20%20%26%3A%3Abefore%20%7B%0A%20%20%20%20content%3A%20%22%22%3B%0A%20%20%20%20top%3A%20-50%25%3B%0A%20%20%20%20left%3A%20-50%25%3B%0A%20%20%20%20right%3A%20-50%25%3B%0A%20%20%20%20bottom%3A%20-50%25%3B%0A%20%20%20%20position%3A%20absolute%3B%0A%20%20%20%20transform%3A%20rotate(20deg)%3B%0A%20%20%20%20background%3A%20radial-gradient(circle%20at%20center%2C%20%23000%2C%20%23fff)%3B%0A%20%20%20%20background-size%3A%200.15em%200.15em%3B%0A%20%20%7D%0A%0A%20%20*%20%7B%0A%20%20%20%20mix-blend-mode%3A%20hard-light%3B%0A%20%20%20%20object-fit%3A%20cover%3B%0A%20%20%7D%0A%7D">
      <pre class="language-scss"><code class="language-scss"><span class="token keyword">@mixin</span> <span class="token selector">half_tone </span><span class="token punctuation">{</span><br>  <span class="token property">overflow</span><span class="token punctuation">:</span> hidden<span class="token punctuation">;</span><br>  <span class="token property">filter</span><span class="token punctuation">:</span> <span class="token function">contrast</span><span class="token punctuation">(</span>150%<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token selector"><span class="token parent important">&amp;</span>::before </span><span class="token punctuation">{</span><br>    <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">""</span><span class="token punctuation">;</span><br>    <span class="token property">top</span><span class="token punctuation">:</span> -50%<span class="token punctuation">;</span><br>    <span class="token property">left</span><span class="token punctuation">:</span> -50%<span class="token punctuation">;</span><br>    <span class="token property">right</span><span class="token punctuation">:</span> -50%<span class="token punctuation">;</span><br>    <span class="token property">bottom</span><span class="token punctuation">:</span> -50%<span class="token punctuation">;</span><br>    <span class="token property">position</span><span class="token punctuation">:</span> absolute<span class="token punctuation">;</span><br>    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">rotate</span><span class="token punctuation">(</span>20deg<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">radial-gradient</span><span class="token punctuation">(</span>circle at center<span class="token punctuation">,</span> #000<span class="token punctuation">,</span> #fff<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">background-size</span><span class="token punctuation">:</span> 0.15em 0.15em<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token selector">* </span><span class="token punctuation">{</span><br>    <span class="token property">mix-blend-mode</span><span class="token punctuation">:</span> hard-light<span class="token punctuation">;</span><br>    <span class="token property">object-fit</span><span class="token punctuation">:</span> cover<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Where I didn’t use the halftone effect on images such as the home page and about page, I used a CSS contrast filter set to 200%.</p><h3 class="post__h3">Textured backgrounds</h3><p class="post__p">Some of the solid colour backgrounds needed a bit of texture to either make things stand out a bit more in the foreground, like the hero section at the top of the home page, or blend more into the background, such as where you can see my photo and short bio on the blog article page. To generate the textures for these backgrounds, I used a <a href="https://texture.alanwsmith.com/" target="_blank">Background Texture Maker</a> by Alan Smith that generates CSS that uses a base64 encoded SVG to create a repeating background image. It also, coincidentally, uses SVG filters. This is the CSS I&#39;m using for the background texture on the hero on the home page.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="UHXmJXfemq"
      aria-describedby="UHXmJXfemq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="UHXmJXfemq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="scss">
      <meta data-code-id="UHXmJXfemq" itemprop="text" content=".hero__cloud__underlay%20%7B%0A%20%20background-size%3A%20150px%20150px%3B%0A%20%20background-repeat%3A%20repeat%3B%0A%20%20background-image%3A%20url(%22data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20viewBox%3D'0%200%20150%20150'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%253E%20%253Cfilter%20id%3D'noiseFilter'%253E%20%253CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'3'%20numOctaves%3D'2'%20result%3D'noise'%20%2F%253E%20%253CfeColorMatrix%20type%3D'saturate'%20values%3D'0'%20result%3D'grayscale'%20%2F%253E%20%253CfeComponentTransfer%253E%20%253CfeFuncA%20in%3D'grayscale'%20type%3D'linear'%20slope%3D'0.4'%20result%3D'updated'%20%2F%253E%20%253C%2FfeComponentTransfer%253E%20%253CfeMerge%253E%20%253CfeMergeNode%20in%3D'noise'%20%2F%253E%20%253CfeMergeNode%20in%3D'updated'%20%2F%253E%20%253C%2FfeMerge%253E%20%253C%2Ffilter%253E%20%253Crect%20width%3D'100%2525'%20height%3D'100%2525'%20opacity%3D'0.46'%20filter%3D'url(%2523noiseFilter)'%2F%253E%20%253C%2Fsvg%253E%22)%3B%0A%7D">
      <pre class="language-scss"><code class="language-scss"><span class="token selector">.hero__cloud__underlay </span><span class="token punctuation">{</span><br>  <span class="token property">background-size</span><span class="token punctuation">:</span> 150px 150px<span class="token punctuation">;</span><br>  <span class="token property">background-repeat</span><span class="token punctuation">:</span> repeat<span class="token punctuation">;</span><br>  <span class="token property">background-image</span><span class="token punctuation">:</span> <span class="token url">url</span><span class="token punctuation">(</span><span class="token string">"data:image/svg+xml,%3Csvg viewBox='0 0 150 150' xmlns='http://www.w3.org/2000/svg'%3E %3Cfilter id='noiseFilter'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='3' numOctaves='2' result='noise' /%3E %3CfeColorMatrix type='saturate' values='0' result='grayscale' /%3E %3CfeComponentTransfer%3E %3CfeFuncA in='grayscale' type='linear' slope='0.4' result='updated' /%3E %3C/feComponentTransfer%3E %3CfeMerge%3E %3CfeMergeNode in='noise' /%3E %3CfeMergeNode in='updated' /%3E %3C/feMerge%3E %3C/filter%3E %3Crect width='100%25' height='100%25' opacity='0.46' filter='url(%23noiseFilter)'/%3E %3C/svg%3E"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">What I also love about this background texture is that it works in both light and dark mode without needing to provide different background-image values based on the theme.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5lQNdKwyBGOW73KMd4ZDDa/23934598f9e9d30be2dbdad68b5b5bc9/redesign_light_dark_texture.png" alt="A side by side comparison of the home page in dark mode on the left and light mode on the right, showing that the textured background adapts to both modes without needing to be modified." height="844" width="1500" /><h2 class="post__h2">The vibes were received</h2><p class="post__p">When I showed a deploy preview to my friends, they understood the vibe; they got the memo. And the fact that I was able to communicate the vibe so clearly made me feel very accomplished indeed.</p><blockquote class="post__blockquote"><p class="post__p">“I get mid-late 2000s punk flyer”</p></blockquote><blockquote class="post__blockquote"><p class="post__p">“Feels like a piece of London rock venue promotional material from ~2010 (complimentary)”</p></blockquote><blockquote class="post__blockquote"><p class="post__p">“This site makes me think of cheap rock punk gigs I used to go to when I was younger and I&#39;m absolutely all for it”</p></blockquote><h2 class="post__h2">Scroll trigger animations</h2><p class="post__p">On the home page I needed a bit of <b class="post__p--italic">something something</b> on scroll, and so I thought on larger screens it would be fun to horizontally scroll my name as you scroll down the page. I thought I could use CSS <code>scroll-timeline</code> for this, but I couldn’t get it to work very well. I either gave up too soon or this is not what <code>scroll-timeline</code> is for, so I opted to use the <a href="https://gsap.com/docs/v3/Plugins/ScrollTrigger/" target="_blank">ScrollTrigger plugin from GSAP</a>. I’ve never used GSAP before, but it’s been on my list to try since around 2015; it was about time.</p><p class="post__p">The GSAP API is really intuitive, and it gives really smooth results. This is the code for the scrolling name that triggers if the <code>prefers-reduced-motion</code> preference is not set to <code>reduce</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="nBfxKfERLJ"
      aria-describedby="nBfxKfERLJ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="nBfxKfERLJ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="nBfxKfERLJ" itemprop="text" content="gsap.to(%22.hero__name%22%2C%20%7B%0A%20%20x%3A%20%22100%25%22%2C%0A%20%20ease%3A%20%22none%22%2C%0A%20%20scrollTrigger%3A%20%7B%0A%20%20%20%20trigger%3A%20%22html%22%2C%0A%20%20%20%20start%3A%20%22top%20top%22%2C%0A%20%20%20%20end%3A%20%22bottom%20top%22%2C%0A%20%20%20%20scrub%3A%20true%2C%0A%20%20%7D%2C%0A%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript">gsap<span class="token punctuation">.</span><span class="token function">to</span><span class="token punctuation">(</span><span class="token string">".hero__name"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">x</span><span class="token operator">:</span> <span class="token string">"100%"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">ease</span><span class="token operator">:</span> <span class="token string">"none"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">scrollTrigger</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">trigger</span><span class="token operator">:</span> <span class="token string">"html"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">start</span><span class="token operator">:</span> <span class="token string">"top top"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">end</span><span class="token operator">:</span> <span class="token string">"bottom top"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">scrub</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">I also had a little fun with moving and scaling the image of me at the time same, which also uses GSAP.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="XRIdoXsvjq"
      aria-describedby="XRIdoXsvjq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="XRIdoXsvjq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="XRIdoXsvjq" itemprop="text" content="gsap.to(%22.hero__img%22%2C%20%7B%0A%20%20scale%3A%201.2%2C%0A%20%20y%3A%20%22-20%25%22%2C%0A%20%20ease%3A%20%22none%22%2C%0A%20%20scrollTrigger%3A%20%7B%0A%20%20%20%20trigger%3A%20%22html%22%2C%0A%20%20%20%20start%3A%20%22top%20top%22%2C%0A%20%20%20%20end%3A%20%22bottom%20top%22%2C%0A%20%20%20%20scrub%3A%20true%2C%0A%20%20%7D%2C%0A%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript">gsap<span class="token punctuation">.</span><span class="token function">to</span><span class="token punctuation">(</span><span class="token string">".hero__img"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">scale</span><span class="token operator">:</span> <span class="token number">1.2</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">y</span><span class="token operator">:</span> <span class="token string">"-20%"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">ease</span><span class="token operator">:</span> <span class="token string">"none"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">scrollTrigger</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">trigger</span><span class="token operator">:</span> <span class="token string">"html"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">start</span><span class="token operator">:</span> <span class="token string">"top top"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">end</span><span class="token operator">:</span> <span class="token string">"bottom top"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">scrub</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">There are a couple of other little scroll trigger animations powered by GSAP dotted around the site as well.</p><h2 class="post__h2">View transitions</h2><p class="post__p">I started writing an article about cross-document <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API" target="_blank">view transitions</a> a few months ago, and haven’t had time to finish it. Whilst researching for the article, I went extremely overboard with the page transitions on the previous design (regular readers might have noticed), so I toned it down in the redesign, whilst adding some stand-out details.</p><h3 class="post__h3">Default page transitions</h3><p class="post__p">This block of CSS adds a fade transition when navigating between pages. I tried providing a different <code>animation-duration</code> for <code>prefers-reduced-motion</code> but I really didn’t know whether the fade should be quicker or slower, so I only apply the view transitions when there is <code>no-preference</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mXnalqUyQy"
      aria-describedby="mXnalqUyQy">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mXnalqUyQy">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="scss">
      <meta data-code-id="mXnalqUyQy" itemprop="text" content="%40media%20(prefers-reduced-motion%3A%20no-preference)%20%7B%0A%20%20%40view-transition%20%7B%0A%20%20%20%20navigation%3A%20auto%3B%0A%20%20%7D%0A%0A%20%20%3A%3Aview-transition-group(root)%20%7B%0A%20%20%20%20animation-duration%3A%200.4s%3B%0A%20%20%7D%0A%7D">
      <pre class="language-scss"><code class="language-scss"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-reduced-motion</span><span class="token punctuation">:</span> no-preference<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>  <span class="token atrule"><span class="token rule">@view-transition</span></span> <span class="token punctuation">{</span><br>    <span class="token property">navigation</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token function">view-transition-group</span><span class="token punctuation">(</span>root<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token property">animation-duration</span><span class="token punctuation">:</span> 0.4s<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Hero image and name transitions</h3><p class="post__p">What’s nice about view transitions is that when they’re enabled, you can add a <code>view-transition-name</code> property to elements via CSS, and if the browser can match elements with the same property names across different pages, it will transition them automatically without any further configuration. I use this technique to move my name and image between the home and about pages.</p><p class="post__p">Rather than messing around with any custom animations (which you can do, but I’ve found it to be a very tiresome process that doesn&#39;t always produce the same results every time), I used the default view transitions behaviour by just adding a <code>view-transition-name</code> to both the image and name banner to transition them between the pages.</p><p class="post__p">Try navigating between the home page and about page to see how powerful this little block of code is. This is all I’m using in the way of view transitions for the hero name and hero image, coupled with the view transition activation code above.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="jWaWvTsnWT"
      aria-describedby="jWaWvTsnWT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="jWaWvTsnWT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="scss">
      <meta data-code-id="jWaWvTsnWT" itemprop="text" content="%2F*%20home%20page%20*%2F%0A.home__hero__name%20%7B%0A%20%20view-transition-name%3A%20hero-name%3B%0A%7D%0A%0A.home__hero__img%20%7B%0A%20%20view-transition-name%3A%20hero-img%3B%0A%7D%0A%0A%2F*%20about%20page%20*%2F%0A.about__hero__name%20%7B%0A%20%20view-transition-name%3A%20hero-name%3B%0A%7D%0A%0A.about__hero__img%20%7B%0A%20%20view-transition-name%3A%20hero-img%3B%0A%7D">
      <pre class="language-scss"><code class="language-scss"><span class="token comment">/* home page */</span><br><span class="token selector">.home__hero__name </span><span class="token punctuation">{</span><br>  <span class="token property">view-transition-name</span><span class="token punctuation">:</span> hero-name<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.home__hero__img </span><span class="token punctuation">{</span><br>  <span class="token property">view-transition-name</span><span class="token punctuation">:</span> hero-img<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">/* about page */</span><br><span class="token selector">.about__hero__name </span><span class="token punctuation">{</span><br>  <span class="token property">view-transition-name</span><span class="token punctuation">:</span> hero-name<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.about__hero__img </span><span class="token punctuation">{</span><br>  <span class="token property">view-transition-name</span><span class="token punctuation">:</span> hero-img<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Article title transitions</h3><p class="post__p">I also use the default view transition behaviour on article headings, so when you click on an article card, the article title transitions to the top of the page on the article layout. Given each view transition needs a unique name, this needed to be done inline on the element itself. Luckily, each of my articles coming from the CMS has a unique ID, so I used that to create a unique <code>view-transition-name</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="MQQCjJZGVl"
      aria-describedby="MQQCjJZGVl">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="MQQCjJZGVl">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="MQQCjJZGVl" itemprop="text" content="%3C!--%20on%20the%20card%20component%20--%3E%0A%3Ch2%20class%3D%22card__title%22%20%0A%20%20style%3D%22view-transition-name%3A%20heading-%24%7Bitem.sys.id%7D%22%3E%24%7Bheading%7D%3C%2Fh2%3E%0A%0A%3C!--%20on%20the%20article%20layout%20--%3E%0A%3Ch1%20class%3D%22post__h1%22%20%0A%20%20style%3D%22view-transition-name%3A%20heading-%24%7Bpost.sys.id%7D%22%3E%24%7Bpost.title%7D%3C%2Fh1%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token comment">&lt;!-- on the card component --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h2</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card__title<span class="token punctuation">"</span></span> <br>  <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token selector">view-transition-name: heading-$</span><span class="token punctuation">{</span>item.sys.id<span class="token punctuation">}</span></span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>${heading}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h2</span><span class="token punctuation">></span></span><br><br><span class="token comment">&lt;!-- on the article layout --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>post__h1<span class="token punctuation">"</span></span> <br>  <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token selector">view-transition-name: heading-$</span><span class="token punctuation">{</span>post.sys.id<span class="token punctuation">}</span></span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>${post.title}<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">The article layout</h2><p class="post__p">When I spoke at Front End North in July 2025, I was very inspired by a talk given by the wonderful <a href="https://bsky.app/profile/ichimnetz.com" target="_blank">Nils Binder</a> titled <a href="https://ichimnetz.com/slides/unwrapping-web-design.pdf" target="_blank">Unwrapping Web Design</a>, in which he challenged the audience to stop making the same old boring website designs we’ve been making since 2010 and think a little more creatively about page layouts. In the talk, Nils showcased some interesting layouts that used irregular grid column widths, and generally just got a little bit creative with the whole concept of a layout.</p><p class="post__p">I have attempted to channel Nils’ advice in the new article layout on larger screens especially, which uses the following grid layout to try and break out of the two or three column standard.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="gRpcNCBQio"
      aria-describedby="gRpcNCBQio">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="gRpcNCBQio">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="scss">
      <meta data-code-id="gRpcNCBQio" itemprop="text" content=".article%20%7B%0A%20%20gap%3A%202rem%3B%0A%20%20display%3A%20grid%3B%0A%20%20grid-template-columns%3A%201fr%3B%0A%20%20grid-template-areas%3A%0A%20%20%20%20%22title%22%0A%20%20%20%20%22aside%22%0A%20%20%20%20%22content%22%0A%20%20%20%20%22author%22%0A%20%20%20%20%22bsky%22%3B%0A%20%20align-items%3A%20flex-start%3B%0A%20%20flex-direction%3A%20column%3B%0A%0A%20%20%40media%20screen%20and%20(min-width%3A%20utils.%24post_breakpoint_md)%20%7B%0A%20%20%20%20gap%3A%201rem%3B%0A%20%20%20%20display%3A%20grid%3B%0A%20%20%20%20grid-auto-columns%3A%20auto%3B%0A%20%20%20%20grid-template-rows%3A%20auto%201fr%20auto%3B%0A%20%20%20%20grid-template-areas%3A%0A%20%20%20%20%20%20%22title%20title%20author%22%0A%20%20%20%20%20%20%22aside%20content%20author%22%0A%20%20%20%20%20%20%22bsky%20bsky%20bsky%22%3B%0A%20%20%20%20grid-template-columns%3A%202fr%206fr%202fr%3B%0A%20%20%7D%0A%0A%20%20%40media%20screen%20and%20(min-width%3A%20utils.%24post_breakpoint_lg)%20%7B%0A%20%20%20%20gap%3A%203rem%3B%0A%20%20%20%20grid-template-areas%3A%0A%20%20%20%20%20%20%22bsky%20title%20title%20author%22%0A%20%20%20%20%20%20%22bsky%20aside%20content%20author%22%3B%0A%20%20%20%20grid-template-columns%3A%201fr%202fr%206fr%202fr%3B%0A%20%20%7D%0A%0A%20%20%40media%20screen%20and%20(min-width%3A%20utils.%24post_breakpoint_xl)%20%7B%0A%20%20%20%20grid-template-columns%3A%201fr%201fr%207fr%202fr%3B%0A%20%20%7D%0A%7D">
      <pre class="language-scss"><code class="language-scss"><span class="token selector">.article </span><span class="token punctuation">{</span><br>  <span class="token property">gap</span><span class="token punctuation">:</span> 2rem<span class="token punctuation">;</span><br>  <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span><br>  <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> 1fr<span class="token punctuation">;</span><br>  <span class="token property">grid-template-areas</span><span class="token punctuation">:</span><br>    <span class="token string">"title"</span><br>    <span class="token string">"aside"</span><br>    <span class="token string">"content"</span><br>    <span class="token string">"author"</span><br>    <span class="token string">"bsky"</span><span class="token punctuation">;</span><br>  <span class="token property">align-items</span><span class="token punctuation">:</span> flex-start<span class="token punctuation">;</span><br>  <span class="token property">flex-direction</span><span class="token punctuation">:</span> column<span class="token punctuation">;</span><br><br>  <span class="token atrule"><span class="token rule">@media</span> screen <span class="token operator">and</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> utils.<span class="token variable">$post_breakpoint_md</span><span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>    <span class="token property">gap</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><br>    <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span><br>    <span class="token property">grid-auto-columns</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span><br>    <span class="token property">grid-template-rows</span><span class="token punctuation">:</span> auto 1fr auto<span class="token punctuation">;</span><br>    <span class="token property">grid-template-areas</span><span class="token punctuation">:</span><br>      <span class="token string">"title title author"</span><br>      <span class="token string">"aside content author"</span><br>      <span class="token string">"bsky bsky bsky"</span><span class="token punctuation">;</span><br>    <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> 2fr 6fr 2fr<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token atrule"><span class="token rule">@media</span> screen <span class="token operator">and</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> utils.<span class="token variable">$post_breakpoint_lg</span><span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>    <span class="token property">gap</span><span class="token punctuation">:</span> 3rem<span class="token punctuation">;</span><br>    <span class="token property">grid-template-areas</span><span class="token punctuation">:</span><br>      <span class="token string">"bsky title title author"</span><br>      <span class="token string">"bsky aside content author"</span><span class="token punctuation">;</span><br>    <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> 1fr 2fr 6fr 2fr<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token atrule"><span class="token rule">@media</span> screen <span class="token operator">and</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> utils.<span class="token variable">$post_breakpoint_xl</span><span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>    <span class="token property">grid-template-columns</span><span class="token punctuation">:</span> 1fr 1fr 7fr 2fr<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Showing Bluesky replies on blog posts</h3><p class="post__p">As well as showing Bluesky likes on articles, which <a href="https://whitep4nth3r.com/blog/show-bluesky-likes-on-blog-posts/" target="_blank">I wrote about in 2024</a>, I now show <b class="post__p--italic">replies</b> to that post as well. This was a bigger undertaking than I anticipated and involved much recursion to render embeds within embeds within embeds, and replies to replies to replies.</p><img src="https://images.ctfassets.net/56dzm01z6lln/e3wAzPZVTyda774MhwaqE/eae62d7db6fb9014b3809ab013155a8f/redesign_replies.png" alt="A little screenshot of some bluesky replies on an article, showing nested replies, and embeds containing embeds." height="1137" width="1000" /><p class="post__p">The code is a mess, but it was fun. I will do a full write-up of how I achieved it in due course, but the main takeaways were this:</p><ul><li><p class="post__p">To have some level of moderation over the replies shown, I’m using an authenticated endpoint to fetch the replies as myself. This means I can hide replies on Bluesky that I don’t want to show on my articles.</p></li><li><p class="post__p">I’m fetching the replies client-side via a serverless function so they are always up to date. However, I am not sure how scaleable this will be. I managed to rate limit myself once during development when I had too many localhost tabs open, so I have a feeling this might happen on high-traffic articles. If this is noticeable, I’ll probably end up writing a daily cron job to save the replies to JSON in my website repo, which I’ll fetch from the client. Alternatively I could fetch the replies at build time, but that also means I’ll have to rebuild my website regularly (probably daily) in order for the replies to stay as up to date as possible. This also means that if I ever need to hide replies, I’ll need to run the cron job immediately. <b class="post__p--italic">I don’t think I want this level of administration in my life.</b></p></li><li><p class="post__p">As a result of using a serverless function to fetch the replies data, there is no way to reliably cache an authentication token unless I use a separate service to store it. This means I am requesting an authentication token on every request for the replies. I don’t like this and this is why I am considering using a cron job to fetch the replies and store them at regular intervals.</p></li><li><p class="post__p">The above two bullet points are why I am delaying the full write-up of how I show the replies on my articles, because I want to find the best solution.</p></li></ul><p class="post__p">Overall, I love seeing the Bluesky replies under an article. There&#39;s something really wholesome about it.</p><h2 class="post__h2">Spotlighting my weird newsletter</h2><p class="post__p">In the previous design, I included a link to my newsletter on the home page and next to blog posts. However, this was just a link, and didn’t look very <b class="post__p--italic">official</b>. Yes, it’s supposed to be purposefully weird, but I guess that only goes so far.</p><p class="post__p">My newsletter is powered by <a href="https://buttondown.com/refer/weirdwidewebhole" target="_blank">Buttondown</a>, and they provide you with a block of HTML with a simple sign-up form that you can drop in anywhere on your website and style to your heart’s content. Now, interested readers can subscribe to my newsletter from any page directly. I also tell you a little bit more about what you’ll get with the newsletter. Scroll down and try it out, I dare you.</p><h2 class="post__h2">It’s done</h2><p class="post__p">Now that I’ve released the redesign, I’m looking forward to more sleep and more relaxed shoulders. Fixing up this website in just a week whilst doing a job and being a parent and going to yoga and practising drums and doing other things has been a wild ride, and I’m actually really happy with it. There are also some other things I’d like to add to the website, such as newsletter archives and some other little details, but that can wait.</p><p class="post__p">If you’re now feeling motivated to redesign your website or make your first website(!), check out this list of human-made and human-curated <a href="https://personalsit.es/" target="_blank">personal sites</a> for inspiration.</p><p class="post__p">The world is your oyster. And so is <b class="post__p--bold">The Web</b>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>This is not a 2025 wrap up post</title>
          <description>I won't be talking about anything that happened.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/this-is-not-a-2025-wrap-up-post/</link>
          <guid>https://whitep4nth3r.com/blog/this-is-not-a-2025-wrap-up-post/</guid>
          <pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I decided to start the year with a hot Vinyasa yoga class at 9:30am on January 1st 2026. Despite suffering from a week of pre-menstrual full-body pain and desperate fatigue, for some wild reason I felt compelled to see in the new year this way. “I’ll start the year as I mean to go on,” I affirmed valiantly. It was horrible. I cried.</p><p class="post__p">I’ve been regularly attending yoga classes since April 2025, and I’m very proud of attending a total of 58 classes in 2025. Yoga started as a way to explore a different way of self-care and recovery after a <a href="https://whitep4nth3r.com/blog/work-is-meaningless/" target="_blank">very difficult 2024</a> and start to 2025, but it quickly became something I gravitated towards, and made part of my regular routine. Slow flow or restorative flow sessions are my favourite. This type of session, comprising meditative sequences of asanas (postures) and reflective thinking has given me a new appreciation for the uniqueness and abilities of my body, has greatly improved my strength and flexibility (I can touch my toes for the first time in my adult life!), and has gifted me with the space to try and be truly present, to quieten my raging mind (to an extent), and to look inward to understand myself more.</p><p class="post__p">Vinyasa yoga is one of the more dynamic styles of yoga, comprising an energetic flow of different movements, rather than more static yoga asanas. I took a few of these sessions in 2025, but every time I found myself fighting against it rather than embracing the practice. Today, I figured out why. As the teacher urged the class to keep pushing ourselves, to find more strength, to keep going, for just a few more breaths, all I could hear was my inner monologue screaming <b class="post__p--italic">I’m fucking tired.</b> Now, this could probably be attributed to the pre-menstrual full-body pain and desperate fatigue, for when I got home, the period goddesses descended upon me, but I also think it surfaced something else.</p><p class="post__p">Vinyasa yoga reminds me of the relentlessness of life. Find more strength: just a few more breaths/minutes/hours/days/weeks. <b class="post__p--italic">Keep going</b>. Keep grinding, bro.</p><p class="post__p">I don’t want to keep going. I want to rest. I want to recover. <b class="post__p--italic">I’m fucking tired</b>.</p><hr class="post__hr" /><p class="post__p">For a few weeks now, a 2025 wrap up post has been on my mind. It’s the thing you do when you’re a person on The Internet Who Has a Blog, after all. But I don’t want to write a wrap up post. <b class="post__p--italic">I’m fucking tired</b>.</p><p class="post__p">I could talk about how at the start of 2025 I was in debilitating pain and thought I would never type again. <a href="https://whitep4nth3r.com/blog/how-i-learned-to-code-with-my-voice/" target="_blank">I could talk about how I learned how to code with my voice</a> as a result of this. I could talk about how I changed jobs and moved from Sentry to <a href="https://nordcraft.com" target="_blank">Nordcraft</a>, and about all the educational videos I made and the 23 blog posts I wrote. I could talk about the seven conferences I spoke at, or the one I emceed at, and that I went to Denmark to visit the Nordcraft office three times, and about how all of the travelling <b class="post__p--italic">completely burned me out</b> and how I never want to leave my house again, at least in 2026.</p><p class="post__p">I could also talk about how <a href="https://whitep4nth3r.com/blog/goodbye-twitch/" target="_blank">I decided to retire from streaming on Twitch</a> to make room in my life for other creative things, and how my studio now feels horribly surreal and empty without my streaming setup, and how that makes me feel a deep sadness that I can’t quite articulate. I could also talk about how I have the most beautiful group of Internet Friends who I am truly grateful for every single day, and <a href="https://whitep4nth3r.com/blog/the-experience-is-enough/" target="_blank">how our friendship blossomed after attending a conference together</a>, and how we ate pie and mash on one of the hottest days of the year in the UK, and how they are so smart and so creative, and how privileged I am to be accepted, welcomed, and loved by them.</p><p class="post__p">I could also talk about how <a href="https://whitep4nth3r.com/blog/i-am-40/" target="_blank">I turned 40</a>, and how I received some wonderful letters from people on The Internet to mark the occasion. I could also talk about how I sent the 100th issue of <a href="https://buttondown.com/weirdwidewebhole" target="_blank">my newsletter</a>, and how I surpassed 500 subscribers, and how much of an achievement that feels like.</p><p class="post__p">I could also talk about my wonderful child, who challenges me to be more patient and compassionate and loving every single day. I could talk about how he is eight years old now, and going through some kind of separation anxiety phase, where he knows he doesn’t need me as much anymore, but he still <b class="post__p--italic">wants to need me</b>, and as a result I have no time in the evenings to truly relax or indulge in the right headspace to be creative, because he won’t go to sleep until I do. I could also talk about how his school schedule is so demanding I wonder if the school leadership ever consider that parents actually have jobs and other conflicting responsibilities when they request my attendance at all manner of afternoon soirees. I could talk about how I have to turn up, how I have to be there for him, because the look on his face when he finally picks me out in a crowd of people is something I cherish.</p><p class="post__p">I could talk about all the creative things I have done this year, except I haven’t had the time, energy, or space to do many. I could talk about how my second very large cross stitch is almost complete after 18 months of working on it. I could talk about how I resumed my drum lessons in September after taking a break for a year, and how that has truly reenergised me. I could talk about how I find time to play the piano here and there, and how I recorded a couple of performances for the group chat that were enjoyed and well received, and I could talk about how I started to create so many new pieces of music by noodling on the piano and writing a cool idea on a scrap of paper that were subsequently abandoned because I didn’t feel like anything was <b class="post__p--italic">fucking good enough</b>. I could talk about how I’m <b class="post__p--italic">aching</b> to create music again, how I have so many ideas in my head I can’t sort through them in any meaningful way, and how I am just yearning for the space and time and energy to just <b class="post__p--italic">fucking make something</b>.</p><p class="post__p">I could also talk about how I’ve been trying to rebuild and redesign my website, and have been doing so since April 2025, and how I have absolutely zero time to sink into this because work is so incredibly busy and also <b class="post__p--italic">I’m fucking tired</b>. I could also talk about all the other ideas I’ve had for websites and side projects, and how I feel a constant lingering sense of guilt and failure that I have manifested absolutely nothing I truly wanted to. I could talk about how maybe 2026 could be different, but how it’s very hard to visualise right now, because I have a very busy first quarter of 2026 coming up at work where I am creating a new video course. I could talk about how I’ll be making around 50 videos, and how it’s all rather stressful and awkward timing-wise because in February I am having major mouth surgery, and I won’t be able to speak properly for a few weeks, and you definitely don’t want to see me on camera with a fat swollen face.</p><p class="post__p">I could also talk about how 2025 has been a disgusting year for the world, in particular the technology industry, and how <b class="post__p--italic">fucking AI</b> has invaded every fibre of our online experience, and how no one whatsoever wants this, but we just have to accept those <b class="post__p--italic">fucking AI sparkles</b> littering every corner of apps and browsers and search results with no easy way to turn them off or opt out or just make it go away, and how this has contributed to how <b class="post__p--italic">fucking tired</b> I am, and how I really do not want to be on The Internet for one more day, if I can help it.</p><p class="post__p">But this is not a wrap up post. So I won’t talk about any of it.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>All good things must come to an end</title>
          <description>I guess this time the game really does stop, when the stream ends.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/goodbye-twitch/</link>
          <guid>https://whitep4nth3r.com/blog/goodbye-twitch/</guid>
          <pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate>
          <category>Streaming</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">A lot has happened since Wednesday 24th June 2020: the day I went live on <a href="https://twitch.tv/whitep4nth3r" target="_blank">Twitch</a> for the first time. That was the day I thrust myself upon The Internet and seemingly changed the trajectory of my career, which has taken me to places and introduced me to people I could never even have imagined.</p><p class="post__p">I remember my first stream like it was yesterday. After I pressed the go live button, I remember going back and forth between my makeshift pandemic office upstairs and the lounge downstairs to make sure I was, indeed, broadcasting to the world. (I hadn&#39;t yet discovered the Twitch dashboard that lets you monitor your stream in real time.) That first stream had just an average of three viewers, and mainly consisted of counselling a Lithuanian 17-year old who was having trouble with his parents whilst I was building my first open source app: <a href="https://fretonator.com" target="_blank">fretonator.com</a>. I often wonder if he&#39;s doing OK now.</p><img src="https://images.ctfassets.net/56dzm01z6lln/bb0BjRGjW5ebTjENKYOKd/e28b5ed20d67726fa23f639982464c1e/original-stream-setup.png" alt="A screenshot of whitep4nth3r's simple stream overlays from the early days of streaming in 2020." height="561" width="1000" /><p class="post__p">Despite the strange start to this streaming journey, I was hooked instantly. During the height of the early Covid pandemic panic, before the vaccine was invented, whilst millions of people were dying, as we were all adjusting to remote working in our homes, and whilst my child was just two and a half years old, somehow I found the time and inclination to go live on Twitch <b class="post__p--bold">four evenings a week consistently</b>. Honestly, I don&#39;t know how I did it. But at the time, everyone was on Twitch; it was addictive. Being on The Internet felt like a necessity in the early days of the pandemic, and being part of a live stream hosted by a real person on Twitch was uplifting in an otherwise dire set of circumstances. It was how we stayed connected when we figured we all could just die at any time. And, through streaming and watching others on Twitch I had discovered an incredible community with shared interests who all just simply enjoyed hanging out with each other. It was a truly, truly wonderful time in my life in an otherwise dark, dark time for the planet.</p><p class="post__p">Before Twitch, I was living in a tiny world, working at a local tech agency in Manchester, UK. I had not yet heard of niche sub-categories of the technology industry such as Developer Relations, Developer Advocacy, Developer Experience, or Developer Education. At the time, given that people in these job roles were no longer travelling to events, they had moved their activities to Twitch. By immersing myself in what was then the Science and Technology category on Twitch, and the wonderful mechanics of Twitch raids (where you send all your viewers to another stream when you&#39;re ready to go offline), I came to know many people who were working in DevRel, and was entirely enamoured with the whole thing.</p><p class="post__p">I got my first Developer Advocacy job after I got to know a streamer I raided who was a Developer Advocate at the time, <a href="https://bsky.app/profile/laylacodesit.bsky.social" target="_blank">LaylaCodesIt</a>. I remember choosing to raid her because she was the only other British woman I witnessed streaming in the Science and Technology category since I discovered Twitch. We became fast friends, we streamed together in a little series we called CodeCake, and she referred me to an ex-colleague of hers who was hiring a front-end specialist Developer Advocate at Contentful. I remember thinking that this type of job was too good to be true. How could it be possible that I could use my expertise in teaching <b class="post__p--italic">and</b> skills in technology in the same job? (For those of you that don&#39;t know my history, I was a music teacher before I started working in tech.) And so in January 2021, the next chapter of my journey began.</p><p class="post__p">Streaming was a big part of my DevRel job at Contentful; I would go live three times a week during work hours consistently. In fact, I got invited to the Twitch Partner program during my time at Contentful, which I attribute to being able to have so much freedom to stream and talk about Contentful in public as part of my job responsibilities, thus being fortunate to build a thriving community. Being a talented and prolific streamer was seen as a valuable asset to any DevRel team at a time when we couldn&#39;t really travel safely, and I was proud of the viewership I achieved and sustained.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2QSpPlnkmEeahKCGClzGqZ/ac826fa30aebb31eb1e5decb412c719f/stream_thumb_fallback.jpg" alt="Twitch stream, me on the right, with the Netlify dashboard behind me." height="720" width="1280" /><p class="post__p">My next move was to the Netlify Developer Experience team in January 2022. One of the specific reasons they hired me was for the success of my Twitch streams. Netlify was, and still is, a fully remote company. However, as more and more companies started to force people to return to the office in 2023, viewership across the entirety of Twitch started to decline: no longer were people able to tune in to a Software and Game Dev Twitch stream in the background whilst they powered through their JIRA tickets. This, coupled with with the post-pandemic economic decline of the technology industry in general, caused the business value of DevRel teams to be questioned constantly. DevRel teams were now at the top of the elimination list, and this resulted in an alarming shift in how companies valued live streaming in DevRel. </p><blockquote class="post__blockquote"><p class="post__p">We pay your salary to work for this company. We don&#39;t pay you to have fun on Twitch.</p></blockquote><p class="post__p">I remember very vividly when words to that effect were said to me in a team meeting. It was crushing. The endeavour that had catapulted me into this career, the thing I loved to do so much, the brand of fun and entertainment I had poured my whole heart and soul into, the activity that had got me these first two jobs in the Developer Relations industry: was being condemned as valueless, trivial, worthless. Shortly after that conversation, I was made redundant in a company restructure.</p><p class="post__p">My next move was to Sentry as a Senior Developer Relations Advocate. My manager was very supportive of my Twitch streaming activities and encouraged it, but I couldn&#39;t help shake the feeling that I was providing zero business value in return for my efforts. And it is a <b class="post__p--bold">huge </b>effort: being on camera for four hours at a time, writing code, problem-solving, building something real whilst maintaining an entertaining persona and conversing with viewers really takes it out of you. Plus, viewership across Twitch was declining further, given more and more people were returning to office, save for a few outliers. It also became quite obvious that I was in a sub-optimal time zone for sustained viewership. Twitch remains a very US-centric platform, especially in the Software and Game Dev category. There just weren&#39;t that many people browsing Twitch when I went live. Often, I would be at the top of the category when I was live, yet still with only around 100-130 concurrent viewers.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2X5LcxHFM54jtbt2uJTlod/5ec344ef8be58978d9c9294bf5cdb69a/stream_thumb_fallback.png" alt="I'm live streaming on Twitch, to the right of the screen, smiling and with my hand over my mouth. To the left of me are two terminal windows with lots of code on. Panther emojis are raining down on the screen. We're having a good time." height="1080" width="2160" /><p class="post__p">Now, I have written about how <a href="https://whitep4nth3r.com/blog/your-live-coding-stream-does-not-need-a-bigger-audience/" target="_blank">your live coding stream does not need a bigger audience</a>, and I stand by that. But, as someone who has a job where the value of your activities are constantly being questioned, that big audience is a real indicator of potential value. If you&#39;re showcasing great software to 500 people, the probability that you generate a good amount of interest which results in sign ups and eventual paying customers is much higher than if you&#39;re streaming to ~60 viewers. I&#39;d been laid off once before in this industry. And so, naturally, I started to prioritise other activities at work that would be seen as being more valuable in order to keep my job, such as delivering more formal workshops and engaging in other more prominent marketing activities.</p><p class="post__p">And then in the autumn of 2024, <a href="https://whitep4nth3r.com/blog/work-is-meaningless/" target="_blank">my husband got really sick</a>, and I didn&#39;t think he&#39;d recover, and all of my priorities in life shifted. I stopped streaming entirely for around three months. And when I returned to Twitch, something didn&#39;t feel quite right. With all of the above considered around the conversation of business value, I didn&#39;t look forward to going live like I used to; whilst live streaming I felt like I was failing at my job — failing at delivering value. And then, the mental and physical exhaustion of the events that unfolded surrounding my husband&#39;s illness hit me hard. My body gave up. I could no longer use my hands without debilitating pain. I thought this would be a great opportunity for &quot;content&quot;, however (like a fool). I live streamed <a href="https://whitep4nth3r.com/blog/how-i-learned-to-code-with-my-voice/" target="_blank">learning to code with my voice</a>, and it was, without exaggeration, <b class="post__p--bold">a living hell</b>. Talking simultaneously to a captive audience whilst learning how to talk to a machine whilst being in excruciating pain was so incredibly abhorrently vile I wouldn&#39;t recommend it to my worst enemy. (Not that I have enemies, mind you.) And so I took another break: mainly because I handed in my notice at Sentry to take some time to recover from all of this pain and exhaustion, to try and practise self-care, and to try and get my life together and my health back. </p><img src="https://images.ctfassets.net/56dzm01z6lln/3pye3JTSDtVdlEe2Ryl2wY/fc6e3b73cf46a5288636a26f714d5f5f/whitep4nth3r-2025-2-4-151350.579-1080p-streamshot.png" alt="A screenshot of my streaming showing my coding in VSCode with cursorless annotations, with my fingers either side of my head because clearly it was making my head hurt." height="1080" width="2160" /><p class="post__p">A month later, I started a new role as Head of Developer Education at <a href="https://nordcraft.com" target="_blank">Nordcraft</a>. Whilst I went live a few times at the start of my job to share this new and exciting developer tool with the world, <b class="post__p--italic">something still didn&#39;t feel right</b>. I couldn&#39;t stop hearing those words in my head: <b class="post__p--italic">&quot;We pay your salary to work for this company. We don&#39;t pay you to have fun on Twitch.&quot;</b> Now, obviously these words have not been uttered at Nordcraft and the people I work with are truly very wonderful, but I have more responsibility in this role than I have had in previous roles, and this company is a small start-up of just nine people, and therefore time in the workday is precious. Additionally, I am now a one-person department with many other responsibilities and priorities that just haven&#39;t afforded me the time to put everything aside and hang out on Twitch for four hours at a time to <b class="post__p--italic">maintain that elusive viewership.</b></p><p class="post__p">I am so incredibly grateful for everything that streaming on Twitch has given me. I have built a network of talented friends in technology; I have become an international speaker; I built a whole ridiculous game played via my Twitch chat; I built some truly silly and fun projects; I pivoted my entire career; and I made friends for life. But, alas, it is time to move on to the next chapter. </p><img src="https://images.ctfassets.net/56dzm01z6lln/6jPDe1xXNIwYjlFvypu6fs/1687cb2dbefc9b96a04c405f9e983632/hack_week_thumb.png" alt="the game doesn't stop when the stream ends in white text on top of a very close up of my face" height="1080" width="1920" /><p class="post__p"><b class="post__p--bold">On Thursday 18th December 2025, </b><a href="https://www.twitch.tv/whitep4nth3r/schedule?segmentID=3bc13979-547b-4d77-a90c-945dd6b72fff" target="_blank"><b class="post__p--bold">I will be going live on Twitch for the last time</b></a><b class="post__p--bold">. I will also be ceremoniously turning off my Twitch bot (which will save me £84 a year) and will be making the code open source.</b></p><p class="post__p">Thank you, everyone, for coming on this journey with me.</p><p class="post__p">Let&#39;s play <a href="https://pantherworld.netlify.app/" target="_blank">p4nth3rworld </a>one last time.</p><p class="post__p">I guess this time the game really does stop, when the stream ends.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Is the BenQ coding monitor any good?</title>
          <description>The BenQ coding monitor does come with one major downside, but it (surprisingly) became my primary monitor as soon as I unboxed it.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/is-the-benq-coding-monitor-any-good-review/</link>
          <guid>https://whitep4nth3r.com/blog/is-the-benq-coding-monitor-any-good-review/</guid>
          <pubDate>Tue, 25 Nov 2025 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">After appearing in an episode of <a href="https://www.youtube.com/watch?v=3xG2y7Bs0ws&list=PLz8Iz-Fnk_eTkZvSNWXW_TKZ2UwVirT2M&index=8&t=1s" target="_blank">Code TV’s Web Dev Challenge</a> in early 2025, I was very lucky to be sent a brand new <a href="https://www.benq.eu/en-uk/monitor/programming/rd280ua/buy.html" target="_blank">BenQ RD280UA monitor specifically for coding</a> to try out at home. (Thank you, BenQ!) This monitor is an interesting one. It’s a 28 inch 4k monitor in an unconventional aspect ratio of 3:2 that comes with different colour modes for different activities, including modes optimised for working in light mode and dark mode, and claims to be easy on the eyes for long stretches of work.</p><p class="post__p">At the time of writing, I’ve been using the BenQ RD280UA for around three months as my primary monitor connected to my MacBook Pro M4, and I thought it would be useful to document my experience with using it for anyone considering investing in this almost novelty piece of kit. For me, the BenQ monitor does come with one major downside, but the pros heavily outweighed the one con, meaning this monitor surprisingly became my primary monitor on day one.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1O2mzBp8nT6bFYGy5T4dBH/98ea529d46cd02de8bd9392c7c16d59f/benq_desk.png" alt="A photo of my desk. My Dygma Defy keyboard is set up right in front of the BenQ monitor, which is directly in front of where I sit. To the right of that is my camera with an Elgato Prompter attached to it. To the right of that is the LG DualUp monitor. My microphone is placed in front of the camera. There's also various other things on the desk including a turquoise Yeti cup, a note book, and an Elgat stream deck." height="675" width="1200" /><h2 class="post__h2">Pro: it’s really comfortable to look at</h2><p class="post__p">I wasn’t expecting to replace my LG DualUp as my primary monitor so quickly, but as soon as I turned on the BenQ, it was very clear how much more pleasant the monitor was to look at. BenQ describe this monitor as using “Eye-Care Technology”; I don’t know the precise underlying technicalities of this, but instantly my eyes felt much more relaxed looking at this monitor compared to the LG DualUp.</p><p class="post__p">The display has near-zero glare, everything looks really smooth on the screen, and the eye care options such as Night Hours Protection, Low Blue Light Plus, and Brightness Intelligence Technology can be fine-tuned to your personal preferences. This isn’t an eye care feature per se, but I also <b class="post__p--italic">really</b> like the MoonHalo, which is a ring light at the back of the monitor that I use at a brightness level of 3 and a colour temperature of 1. Unfortunately I don’t have my monitor against a wall to take full advantage of the reflection of ambient light that the MoonHalo creates, but when it’s darker in my office, it’s a welcoming addition to using the <a href="https://www.benq.eu/en-uk/lighting/monitor-light/screenbar-halo-2.html" target="_blank">BenQ Screenbar Halo</a> that sits at the top of the monitor (which BenQ also sent me a couple of years ago!).</p><img src="https://images.ctfassets.net/56dzm01z6lln/1qmgH9PysnjHae1Aauw4Nt/b8e0265a6f047d13a7bb1d2a02ce5ba7/benq_moonhalo.png" alt="A photo of the BenQ monitor from the back with the Moon Halo lit up. The BenQ screenbar halo is also placed on the top of the monitor, with the back portion lit up." height="675" width="1200" /><h2 class="post__h2">Pro: switching between the colour modes makes different tasks feel easier</h2><p class="post__p">I like to work in both light and dark modes depending on the task I’m doing. For coding in an IDE I prefer using dark mode; for reading and writing I prefer using light mode. The middle button at the bottom of the monitor allows you to switch between optimised colour profiles quickly when switching between tasks.</p><p class="post__p">I’ve found the dark theme colour profile works great for working in a dark theme in VS Code, but when working in <a href="https://nordcraft.com" target="_blank">Nordcraft</a> and editing videos using DaVinci Resolve (which both have a dark UI) I’ve found the M-book profile to be better for me, as it increases the overall brightness of the screen and, more importantly, the contrast between colours. This is really helpful for someone like me who has quite a severe astigmatism, which often causes my eyes to feel strained when looking at screens for long stretches of time, especially when working in dark UIs. I also really like the ePaper mode which puts the monitor in a kind of dimmed monochrome mode. It’s like looking at an e-reader, and it’s really kind on the eyes.</p><p class="post__p">One other thing I will mention on the colour profiles is that if you enable High Dynamic Range via the MacOS display settings, you won’t be able to use the different colour profiles, as the OS settings seemingly override any other monitor-specific colour profile capabilities.</p><img src="https://images.ctfassets.net/56dzm01z6lln/43UQDGAqmItL04iGHPF1Fq/a76ee0aa81a409426b1c6557cd2c0ef9/benq_colour_profiles.png" alt="A photo of the Nordcraft editor with the colour profile options open on the monitor. The currently selected profile is M-Book." height="774" width="1200" /><h2 class="post__h2">Pro: the monitor is a really comfortable size</h2><p class="post__p">The unconventional 3:2 aspect ratio is really comfortable to work in. The larger screen real-estate provides more vertical space than the standard 16:9 aspect ratio, which I had become accustomed to with the LG DualUp, and also makes viewing two applications or browser tabs side by side very comfortable. Plus, the flexible arm makes it easy to adjust the horizontal and vertical positioning should your preferences change throughout the day when you switch between tasks, or whether you’re sitting or standing up at your desk.</p><p class="post__p">Whilst the aspect ratio of this monitor is really comfortable to <b class="post__p--italic">work</b> in, it’s entirely inconvenient for one of my regular activities.</p><h2 class="post__h2">Con: the 3:2 aspect ratio is awkward for making screen-recorded video content</h2><p class="post__p">I make a lot of video content as part of my work at Nordcraft. A 16:9 resolution is the standard people have come to expect when watching videos on YouTube, and so using a 3:2 screen to record these videos is not ideal. Some internet searches propose that you can choose to use a 16:9 resolution via operating system settings and adjust the output in the BenQ Display Mode Aspect settings, but on a MacBook Pro M4 there isn’t the option to make this work. It is for this reason that I have continued to record my screen for videos using the split dual display on the LG DualUp, which provides me with the required 16:9 aspect ratio.</p><p class="post__p">I have also experimented with trying to capture only a 16:9 portion of a browser in my recording software (OBS), but it’s not that reliable and I probably gave up too early. What’s more, due to the monitor’s high resolution and the less-than-ideal capabilities of my MacBook Pro, when capturing the BenQ screen via OBS I often run into overloaded encoding errors (when I forget to downscale the recording resolution), meaning I’ve frequently attempted to make recordings three or four times before I give up and resort back to using the LG DualUp screen. It’s a real shame, because looking at the LG DualUp is really not as comfortable as looking at the BenQ, and I want the best for my eyes!</p><p class="post__p">With that being said, the newer <a href="https://www.benq.eu/en-uk/monitor/programming/rd320ua/buy.html" target="_blank">BenQ RD320UA</a> coding monitor <b class="post__p--italic">is</b> in 16:9, and so I would go for this one in a heartbeat if I could afford to splash out on another monitor because it would give me everything I could possibly ever need. And in which case, I’d probably ditch the second monitor entirely for my daily work. I’d still need another monitor for live streaming, though.</p><h2 class="post__h2">Overall: it’s a solid monitor</h2><p class="post__p">The technology behind the BenQ series of programming monitors cannot be disputed. Working on this monitor for long periods of time is a very comfortable experience, so much so that it almost makes <b class="post__p--italic">working</b> a joyful experience (I said almost). If you struggle with eye strain or you’ve never managed to tweak your existing monitor settings to your preferences, this could be a worthy investment. But I would urge you to skip the 3:2 version and get the 16:9 version if you’re in the business of making screen-recorded video content.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>We all have a choice</title>
          <description>There is hope if we do it together.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/we-all-have-a-choice/</link>
          <guid>https://whitep4nth3r.com/blog/we-all-have-a-choice/</guid>
          <pubDate>Mon, 29 Sep 2025 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you&#39;re on The Internet and you work in tech, you&#39;re probably feeling painfully burned out at the <b class="post__p--bold">State of Things</b> right now. Your feelings are very valid. And you are not alone.</p><p class="post__p">Over the last decade, technology companies that started out as honest and independent hubs for innovation have gradually and very slowly been swallowed whole by something that, for those of us who have built our lives around being ethical, being nice, being good, and Doing The Right Thing, we just cannot comprehend. It has been a slow but sure bait and switch.</p><p class="post__p">The media and public discourse would have us believe that This Is The Way Things Are; we&#39;re locked in now and there is no other way. But that is what these companies rely on: <b class="post__p--bold">you&#39;re too burned out to take action right now</b>. And so as a collective we continue to pay for services that these companies provide and, by proxy, support these organisations in moving further and further away from what is right and good. </p><p class="post__p">I often speak about how <a href="https://whitep4nth3r.com/blog/you-are-more-than-the-tools-you-use/" target="_blank">your tools don&#39;t matter</a> and how telling your story about what you&#39;re creating is the most important part of building for the web. But in 2025, the tools we choose to use, support, build community around and pay for send an important message to our peers about what we believe in, what we&#39;re willing to tolerate, and ultimately, who we choose to be.</p><p class="post__p">Yesterday the CEO of Vercel posted a proud selfie to The Internet, posing with the Prime Minister of Israel, Benjamin Netanyahu. Regardless of how utterly absurd this actually is, this whole circus act sent a very clear and frankly, harrowing memo to the entirety of the technology industry.</p><p class="post__p">Those of you are are ethical and nice and good but also invested in the Vercel ecosystem are probably a little shaken up right now. And maybe you hope this is a one-off because you really don&#39;t have time to migrate all of your projects to another deployment platform right now, let alone migrate away from Next.js to a new web framework. </p><p class="post__p">But we all have a choice.</p><p class="post__p">Taking action and Doing The Right Thing is often difficult, always exhausting, but it is what we must do, together. We all deserve better. The world deserves better. It&#39;ll take a little work to get there, but there is hope. </p><p class="post__p"><b class="post__p--bold">There is hope if we do it together. The Internet is ours.</b></p><p class="post__p"><b class="post__p--italic">We all have a choice.</b></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>I am 40</title>
          <description>Letters from The Internet.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/i-am-40/</link>
          <guid>https://whitep4nth3r.com/blog/i-am-40/</guid>
          <pubDate>Thu, 25 Sep 2025 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Two weeks ago I celebrated my 40th birthday. The day was perfect; I had a lazy morning, set up a new drum kit, and went for a delicious Portuguese dinner with my husband and son.</p><p class="post__p">To complement my birthday celebrations, I requested that people on The Internet send me a letter via email that I would open on my birthday. The Internet did not disappoint: I received 20 letters, some from strangers, some from newsletter subscribers, some from friends, and each one was delightful to read. I replied to every single one.</p><hr class="post__hr" /><p class="post__p">Some letters were written in the style of my newsletter, <a href="https://buttondown.com/weirdwidewebhole" target="_blank">Weird Wide Web Hole</a>. This was incredible to experience:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="kEDJYyTLqa"
      aria-describedby="kEDJYyTLqa">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="kEDJYyTLqa">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="kEDJYyTLqa" itemprop="text" content="I%20don't%20know%20you%0AYou're%20a%20total%20stranger%0AYou%20are%20a%20face%20and%20some%20words%0AA%20logo%20and%20sometimes%20a%20voice%0A%0AYou%20enlighten%20my%20Thursdays%0AYour%20name%20is%20a%20breath%20of%20air%20in%20my%20mails%0AI'm%20not%20sure%20if%20I%20like%20IT%20anymore%20(don't%20tell%20my%20bosses)%0ABut%20I%20like%20your%20sharings%20for%20sure%0A%0AIt%20has%20a%20feeling%0Amelancholic%2C%20hopeful%2C%20magical%2C%20weird%20(in%20a%20good%20way)%0AIt%20inspires%20me%0AThis%20feeling%20is%20way%20more%20important%20than%20the%20topic%0A">
      <pre class="language-markdown"><code class="language-markdown">I don't know you<br>You're a total stranger<br>You are a face and some words<br>A logo and sometimes a voice<br><br>You enlighten my Thursdays<br>Your name is a breath of air in my mails<br>I'm not sure if I like IT anymore (don't tell my bosses)<br>But I like your sharings for sure<br><br>It has a feeling<br>melancholic, hopeful, magical, weird (in a good way)<br>It inspires me<br>This feeling is way more important than the topic</code></pre>
    </div>
  </div>

  <hr class="post__hr" /><p class="post__p">The letters also said some wonderful things:</p><blockquote class="post__blockquote"><p class="post__p">Your presence on the web, your posts, your videos, your live streaming, your you-ness is so very refreshing and enjoyable. You inspire so many of us to be our best at whatever it is we&#39;re doing. </p></blockquote><blockquote class="post__blockquote"><p class="post__p">I don&#39;t know you that well, but I see you on Bluesky and it looks like you create fun things. The world needs more fun people creating fun things.</p></blockquote><hr class="post__hr" /><p class="post__p">An Internet Friend who became an IRL friend this year (despite living very far away) gifted me with these beautiful words:</p><p class="post__p"><b class="post__p--italic">I&#39;m so absurdly grateful that you&#39;ve come into my life in a genuine way this past year. I still don&#39;t know that I can express to you how crazy it is that I have looked up to someone on the internet for years, admired your work, the incredible things you find for your newsletter, the way you teach and present yourself in public regard with such genuine-ness - never trying to hide or dampen your personality, and then one day I was just able to text you about life and hear about what yoga class you&#39;re going to this week. </b></p><p class="post__p"><b class="post__p--italic">It&#39;s a fucking delight and the world is a magical place AND what an honor it is to have a friend that I look up to and admire and want to become more like as I develop in my own career and as a human. </b></p><p class="post__p">They also shared one of their favourite poems with me:</p><blockquote class="post__blockquote"><p class="post__p">As usual, we&#39;re drunk with Love today.
Evict your thoughts and find a song to play.
Prayers and devotions come in countless shapes and sizes.
Pick the ones the beauty in your soul recognizes.</p></blockquote><p class="post__p">I want to share an excerpt of my reply to this one:</p><p class="post__p">Thank you for your kind, sweet words. I have a secret for you, though. Well perhaps it&#39;s not a secret. But I&#39;m not special, really. Not on my own. It&#39;s the people around us that make us special, the connections we form, the communities we create, and the relationships we nurture and cultivate. Without you in my life I would be less good. So read all of that stuff you wrote and direct it inward towards your heart and your light. Your soul will recognise the beauty and love in those words; they are yours, so they are yours.</p><p class="post__p">Life is a journey towards discovering one&#39;s true strength; not being &quot;better&quot; or &quot;more this&quot; or &quot;more that&quot;. <b class="post__p--italic">We are all the universe</b>. We all have infinite love and wisdom inside of ourselves. We just need to work a little to find it, to experience it, and to share it with others. And that &quot;work&quot; is just living another day, seeing other things, connecting with other souls, breathing and basking in gratitude for what we have been blessed with by the particles in the sky.</p><hr class="post__hr" /><p class="post__p">I also received a letter from an old school friend who, coincidentally, also ended up working as a developer and has been secretly watching me on The Internet for some time:</p><blockquote class="post__blockquote"><p class="post__p">I just wanted to let you know I&#39;m really glad to see you kicking ass in the tech world. I always knew you&#39;d go on to do big things.</p></blockquote><hr class="post__hr" /><p class="post__p">Internet Friends who have lived through their 40s were gracious to gift me their wisdom:</p><p class="post__p"><b class="post__p--italic">A warm congrats on reaching this epic milestone! I&#39;m speaking to you from 10 years in the future, where I recently turned 50. For some, turning 40 can fill one with existential dread. But for most of my 40s I felt it was the best time of my life, despite everything going on in the world. I felt like I finally knew what I was doing professionally. I felt the joy of being a parent.</b></p><p class="post__p"><b class="post__p--italic">If I had one regret it would be that I didn&#39;t take better care of my health. When you&#39;re young it&#39;s easy to think you&#39;re going to live forever. But by the time you&#39;re my age, it all starts slowly (or sometimes quickly) falling apart. The aches and pains start to compound, parts of your body are just like &quot;fuck this&quot;. At least, that&#39;s been my experience. My birthday wish for you is that you are happy and healthy and can avoid being on a first name basis with multiple medical specialists for as long as possible.</b></p><p class="post__p"><b class="post__p--italic">Your 40s are awesome. And you are awesome. So I have no doubt that you will thrive. I can&#39;t speak for your 50s though. I&#39;ll let you know in 10 years.</b></p><hr class="post__hr" /><p class="post__p">And another:</p><p class="post__p"><b class="post__p--italic">We only met recently and briefly but as someone who is nearing 50, I wanted to take a minute to tell you how rad your 40s are. I dreaded being 40 but it&#39;s been the BEST decade. Here are 3 reasons:</b></p><ul><li><p class="post__p"><b class="post__p--italic">You are kinder to yourself and start to like yourself a bit more. 40s me is far superior to 30s me in every aspect. This might just be down to perception but even if it is, it&#39;s working.</b></p></li><li><p class="post__p"><b class="post__p--italic">You become a bit invisible but this is not always a bad thing. I&#39;ve done a lot of solo travelling in every decade, and in my 40s, it stopped being an issue that I was a woman on my own walking through souks or being out at night on my own. Take a book with you to dinner and own your space.</b></p></li><li><p class="post__p"><b class="post__p--italic">You genuinely stop giving a fuck about whether anyone likes you or if you&#39;re wearing the right clothes or living the right life. I know you&#39;ve been saying you don&#39;t give a fuck for years now but you secretly have up until this point.</b></p></li></ul><p class="post__p"><b class="post__p--italic">Wishing you a lovely day and a glorious decade ahead.</b></p><p class="post__p">I replied:</p><p class="post__p">This part really stuck with me:

<b class="post__p--italic">I know you&#39;ve been saying you don&#39;t give a fuck for years now but you secretly have up until this point.</b></p><p class="post__p">Honestly, I can feel the secret fucks that were so embedded within me starting to fall away. You&#39;re right, they were there during my 30s. I was looking towards this date for a whole year, thinking that perhaps it was the end of something. Perhaps I thought it was the end of me as I knew me. But through changing ingrained thought patterns around so many things over the last year in preparation for this milestone, it is feeling more and more like a beginning every second. <b class="post__p--bold">I think I know what they mean when they say life begins at 40 now. </b></p><p class="post__p">And I&#39;m looking forward to becoming a bit invisible. I can&#39;t wait for the day I can &quot;leave The Internet&quot; and just live. I took a day off work on Friday for my birthday, as I always like to spend some time alone. And I felt <b class="post__p--bold">so human</b> not sitting at a desk and just being out in the world. I want more of that. I want all of that all of the time. </p><p class="post__p">I have definitely started to like myself more over this last year. I am giving myself more grace, being less hard on myself, living more in the present and I&#39;ve stopped chasing things that are not worth chasing. </p><p class="post__p">I can&#39;t wait to see what the next 10 years brings; and I can&#39;t wait to pass down your advice coloured with my own experience.</p><hr class="post__hr" /><p class="post__p">And finally, a short and simple reminder from an Internet Friend:</p><blockquote class="post__blockquote"><p class="post__p">At 40 it all begins to continue.</p></blockquote><p class="post__p">Thank you, Internet. I love you.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to delete all squash-merged local git branches with one terminal command</title>
          <description>I wrote a new bash script. And you probably shouldn't use it.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-delete-all-squash-merged-local-git-branches-with-one-terminal-command/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-delete-all-squash-merged-local-git-branches-with-one-terminal-command/</guid>
          <pubDate>Thu, 04 Sep 2025 23:00:00 GMT</pubDate>
          <category>Git</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In 2022 I wrote about how I use a bash function to <a href="https://whitep4nth3r.com/blog/delete-all-merged-git-branches-one-terminal-command/" target="_blank">delete all merged git branches with a single terminal command</a>. This works great for branches that have been <b class="post__p--italic">merged</b>, but not <b class="post__p--italic">squashed</b>. Given the team I&#39;m working on right now like to squash and merge pull requests on GitHub, and also how I like to keep my dev environment clean, it was time to update the clean up function to take squashed branches into account.</p><p class="post__p">Now, this was a total rabbit hole I went down, so bear with me.</p><h2 class="post__h2">The difference between &quot;merge&quot; and &quot;squash and merge&quot;</h2><p class="post__p">The default option when merging pull requests on GitHub is <b class="post__p--bold">merge. </b>When you <b class="post__p--bold">merge</b> a pull request on GitHub, all commits from a feature branch are added to the base branch in a <b class="post__p--bold">merge commit</b>. A merge commit preserves the full history of the changes being merged, allowing you to see an intertwining history of events that happened on both branches after a feature branch is merged into the base branch.</p><img src="https://images.ctfassets.net/56dzm01z6lln/siIaZSpjIqHSPwJufRQZM/957431ba3fbb14aebe7c10f8a459bd61/merge-commit.png" alt="All checks have passed. 1 neutral, 3 successful checks. No conflicts with base branch. Merging can be performed automatically. Merge pull request call to action green button." height="496" width="1980" /><p class="post__p">On GitHub, a default merge uses the <a href="https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---no-ff" target="_blank">--no-ff option in git</a>, which means <b class="post__p--bold">no fast-forward</b>. This option is what creates the <b class="post__p--bold">merge commit</b> and allows you to inspect the complete history of both the feature and base branches. </p><p class="post__p">When you <b class="post__p--bold">squash and merge</b> a pull request, all commits on the feature branch are &quot;squashed&quot; into a single commit, using the <b class="post__p--bold">fast-forward</b> option. A <b class="post__p--bold">fast-forward</b> commit merges all file changes after the most recent change on the base branch. This loses the distinct feature branch timeline of changes. This can be useful when:</p><ol><li><p class="post__p">You want to keep your commit history tidy on the base branch</p></li><li><p class="post__p">You&#39;re merging small changes on short-lived branches</p></li><li><p class="post__p">You don&#39;t need to preserve the feature branch history</p></li></ol><img src="https://images.ctfassets.net/56dzm01z6lln/4iBPRwnxVrCNR7mipkwn3l/329a6c45f86189bfb6b0c2841b6bdd87/squash-and-merge.png" alt="All checks have passed. 1 neutral, 3 successful checks. No conflicts with base branch. Merging can be performed automatically. Squash and merge pull request call to action green button." height="496" width="1980" /><h2 class="post__h2">Why the original bash script doesn&#39;t work for squashed branches</h2><p class="post__p">Here&#39;s the original bash script I wrote to delete all merged branches. The first line, <code>git checkout main &amp;&amp; git branch --merged</code>, checks out the main branch, and lists all branches that can be detected as being merged into the main branch.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mvwHijmevh"
      aria-describedby="mvwHijmevh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mvwHijmevh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="mvwHijmevh" itemprop="text" content="%23%20delete%20all%20branches%20merged%20into%20main%0Adam()%20%7B%0A%20%20echo%20%22%3D%3D%3D%20Deleting%20all%20merged%20branches%20%3D%3D%3D%22%0A%20%20git%20checkout%20main%20%26%26%20git%20branch%20--merged%20%7C%20grep%20-vE%20%22(%5E%5C*%7Cmain)%22%20%7C%20xargs%20git%20branch%20-d%0A%20%20echo%20%22%E2%98%91%EF%B8%8F%20Done!%22%0A%7D">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># delete all branches merged into main</span><br><span class="token function-name function">dam</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"=== Deleting all merged branches ==="</span><br>  <span class="token function">git</span> checkout main <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> branch <span class="token parameter variable">--merged</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-vE</span> <span class="token string">"(^\*|main)"</span> <span class="token operator">|</span> <span class="token function">xargs</span> <span class="token function">git</span> branch <span class="token parameter variable">-d</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"☑️ Done!"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">When you do a <b class="post__p--bold">squash merge</b>, Git takes all the changes from the feature branch, compresses them into a <b class="post__p--italic">new commit</b>, and applies it to <code>main</code> as the latest change. The original branch’s commits are <b class="post__p--bold">not in the history</b>, so Git still sees that branch as “not merged&quot;. The <code>git branch --merged</code> command won&#39;t match any branches, and so won&#39;t find any branches to delete.</p><h2 class="post__h2">The script that finds and deletes squashed branches</h2><p class="post__p">To delete all squashed branches that merged using the fast-forward option we need to take a different approach. This script is named <code>dams()</code>, which stands for &quot;delete all merged squashed&quot;. To use the script:</p><ul><li><p class="post__p">add the following code to your <code>.bashrc</code> or <code>.zshrc</code> file (switch out <code>main</code> for what you usually call your main branches if necessary)</p></li><li><p class="post__p">save the file</p></li><li><p class="post__p">source the new changes (or reload your terminal)</p></li><li><p class="post__p">run <code>dams</code> in a git project directory (without the brackets)</p></li><li><p class="post__p">enjoy the cleanup!</p></li><li><p class="post__p">(This may need to be adjusted according to your shell, please don&#39;t come for me.)</p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="beKxTsTTEh"
      aria-describedby="beKxTsTTEh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="beKxTsTTEh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="beKxTsTTEh" itemprop="text" content="dams()%20%7B%0A%20%20echo%20%22%3D%3D%3D%20Deleting%20merged%20and%20squash-merged%20branches%20%3D%3D%3D%22%0A%0A%20%20%23%20Ensure%20main%20is%20checked%20out%20and%20up%20to%20date%0A%20%20git%20checkout%20main%20%26%26%0A%20%20git%20pull%20%26%26%0A%0A%20%20%23%20Loop%20over%20all%20local%20branches%20except%20main%0A%20%20for%20branch%20in%20%24(git%20for-each-ref%20--format%3D'%25(refname%3Ashort)'%20--exclude%3Drefs%2Fheads%2Fmain%20refs%2Fheads%2F)%3B%20do%0A%20%20%20%20%23%20Skip%20if%20branch%20does%20not%20exist%20on%20remote%0A%20%20%20%20if%20!%20git%20ls-remote%20--exit-code%20--heads%20origin%20%22%24branch%22%20%3E%2Fdev%2Fnull%202%3E%261%3B%20then%0A%20%20%20%20%20%20echo%20%22Skipping%20%24branch%20(not%20on%20remote)%22%0A%20%20%20%20%20%20continue%0A%20%20%20%20fi%0A%0A%20%20%20%20%23%20Delete%20only%20if%20content%20is%20fully%20in%20main%0A%20%20%20%20if%20git%20diff%20--quiet%20%22%24branch%22%20main%3B%20then%0A%20%20%20%20%20%20git%20branch%20-D%20%22%24branch%22%0A%20%20%20%20%20%20echo%20%22Deleted%20%24branch%22%0A%20%20%20%20else%0A%20%20%20%20%20%20echo%20%22Keeping%20%24branch%20(has%20unmerged%20changes)%22%0A%20%20%20%20fi%0A%20%20done%0A%0A%20%20echo%20%22%E2%98%91%EF%B8%8F%20Done!%22%0A%7D">
      <pre class="language-bash"><code class="language-bash"><span class="token function-name function">dams</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"=== Deleting merged and squash-merged branches ==="</span><br><br>  <span class="token comment"># Ensure main is checked out and up to date</span><br>  <span class="token function">git</span> checkout main <span class="token operator">&amp;&amp;</span><br>  <span class="token function">git</span> pull <span class="token operator">&amp;&amp;</span><br><br>  <span class="token comment"># Loop over all local branches except main</span><br>  <span class="token keyword">for</span> <span class="token for-or-select variable">branch</span> <span class="token keyword">in</span> <span class="token variable"><span class="token variable">$(</span><span class="token function">git</span> for-each-ref <span class="token parameter variable">--format</span><span class="token operator">=</span><span class="token string">'%(refname:short)'</span> <span class="token parameter variable">--exclude</span><span class="token operator">=</span>refs/heads/main refs/heads/<span class="token variable">)</span></span><span class="token punctuation">;</span> <span class="token keyword">do</span><br>    <span class="token comment"># Skip if branch does not exist on remote</span><br>    <span class="token keyword">if</span> <span class="token operator">!</span> <span class="token function">git</span> ls-remote --exit-code <span class="token parameter variable">--heads</span> origin <span class="token string">"<span class="token variable">$branch</span>"</span> <span class="token operator">></span>/dev/null <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&amp;1</span><span class="token punctuation">;</span> <span class="token keyword">then</span><br>      <span class="token builtin class-name">echo</span> <span class="token string">"Skipping <span class="token variable">$branch</span> (not on remote)"</span><br>      <span class="token builtin class-name">continue</span><br>    <span class="token keyword">fi</span><br><br>    <span class="token comment"># Delete only if content is fully in main</span><br>    <span class="token keyword">if</span> <span class="token function">git</span> <span class="token function">diff</span> <span class="token parameter variable">--quiet</span> <span class="token string">"<span class="token variable">$branch</span>"</span> main<span class="token punctuation">;</span> <span class="token keyword">then</span><br>      <span class="token function">git</span> branch <span class="token parameter variable">-D</span> <span class="token string">"<span class="token variable">$branch</span>"</span><br>      <span class="token builtin class-name">echo</span> <span class="token string">"Deleted <span class="token variable">$branch</span>"</span><br>    <span class="token keyword">else</span><br>      <span class="token builtin class-name">echo</span> <span class="token string">"Keeping <span class="token variable">$branch</span> (has unmerged changes)"</span><br>    <span class="token keyword">fi</span><br>  <span class="token keyword">done</span><br><br>  <span class="token builtin class-name">echo</span> <span class="token string">"☑️ Done!"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Let&#39;s break down what the script does, and learn some things about Git!</p><h3 class="post__h3">Check out the main branch and update it</h3><p class="post__p">First we want to make sure we checkout main and pull in the latest changes to compare the other branches against.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZmroAExEdJ"
      aria-describedby="ZmroAExEdJ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZmroAExEdJ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="git">
      <meta data-code-id="ZmroAExEdJ" itemprop="text" content="git%20checkout%20main%20%26%26%20git%20pull%20%26%26">
      <pre class="language-git"><code class="language-git">git checkout main &amp;&amp; git pull &amp;&amp;</code></pre>
    </div>
  </div>

  <h3 class="post__h3">Loop over local branches and check for changes</h3><p class="post__p">Here, the script loops over the branch names returned from the command <a href="https://git-scm.com/docs/git-for-each-ref" target="_blank"><code>git for-each-ref</code></a>, which iterates over all branches (or refs) that match a <b class="post__p--bold">pattern</b> and shows them according to the given <b class="post__p--bold">format</b>. The <b class="post__p--bold">pattern</b> excludes the main branch (<code>--exclude</code>), and the <code>--format</code> requests the short branch name. <code>refs/heads/</code> at the end of the line instructs the script to look only at local branches.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="RLRZOqWGIw"
      aria-describedby="RLRZOqWGIw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="RLRZOqWGIw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="RLRZOqWGIw" itemprop="text" content="for%20branch%20in%20%24(git%20for-each-ref%20--format%3D'%25(refname%3Ashort)'%20--exclude%3Drefs%2Fheads%2Fmain%20refs%2Fheads%2F)%3B%20do%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token keyword">for</span> <span class="token for-or-select variable">branch</span> <span class="token keyword">in</span> <span class="token variable"><span class="token variable">$(</span><span class="token function">git</span> for-each-ref <span class="token parameter variable">--format</span><span class="token operator">=</span><span class="token string">'%(refname:short)'</span> <span class="token parameter variable">--exclude</span><span class="token operator">=</span>refs/heads/main refs/heads/<span class="token variable">)</span></span><span class="token punctuation">;</span> <span class="token keyword">do</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Check if a local branch has been pushed to the remote</h3><p class="post__p">This following code runs <a href="https://git-scm.com/docs/git-ls-remote" target="_blank"><code>git ls-remote</code></a> to check if the branch in the loop has been pushed to the remote to avoid deleting branches that haven&#39;t been pushed. The <code>--exit-code</code> flag instructs the command to exit with status &quot;2&quot; when no matching refs are found in the remote repository. In Bash, <code>if</code> considers <b class="post__p--bold">exit code 0 as true</b> and non-zero as false. Exit code 2 will evaluate to false, so with the <code>!</code> at the start of the <code>if</code>, we&#39;re checking if <code>git ls-remote</code> returns false. If it does, we skip deleting that branch.</p><p class="post__p">The <code>&gt;/dev/null 2&gt;&amp;1</code> part instructs the script to throw away both stdout and stderr output so that it produces no output at all, to keep the terminal free of noise. The script still produces a success or failure via the exit code, but suppresses all output.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="tEfYhYfQMd"
      aria-describedby="tEfYhYfQMd">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="tEfYhYfQMd">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="tEfYhYfQMd" itemprop="text" content="%23%20Skip%20if%20branch%20does%20not%20exist%20on%20remote%0Aif%20!%20git%20ls-remote%20--exit-code%20--heads%20origin%20%22%24branch%22%20%3E%2Fdev%2Fnull%202%3E%261%3B%20then%0A%20%20echo%20%22Skipping%20%24branch%20(not%20on%20remote)%22%0A%20%20continue%0Afi">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># Skip if branch does not exist on remote</span><br><span class="token keyword">if</span> <span class="token operator">!</span> <span class="token function">git</span> ls-remote --exit-code <span class="token parameter variable">--heads</span> origin <span class="token string">"<span class="token variable">$branch</span>"</span> <span class="token operator">></span>/dev/null <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&amp;1</span><span class="token punctuation">;</span> <span class="token keyword">then</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"Skipping <span class="token variable">$branch</span> (not on remote)"</span><br>  <span class="token builtin class-name">continue</span><br><span class="token keyword">fi</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Check for diffs and delete the local branch</h3><p class="post__p">This part of the script checks for diffs between the branch in the loop and the main branch. Again, in Bash, <code>if</code> considers <b class="post__p--bold">exit code 0 as true</b> and non-zero as false, so whilst this may look backwards compared traditional programming languages, if there is no diff found between the branches, then the branch will be deleted using <a href="https://git-scm.com/docs/git-branch#Documentation/git-branch.txt--D" target="_blank"><code>git branch -D</code></a>. </p><p class="post__p">I decided to use the <code>-D</code> flag to force the delete in this script, seeing as I&#39;m operating on my local machine and it&#39;s safe to force delete local branches in my case, especially if we have checked at the start of the script if the local branch has been pushed to the remote.<b class="post__p--bold"> </b></p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mFGHZUEaez"
      aria-describedby="mFGHZUEaez">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mFGHZUEaez">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="mFGHZUEaez" itemprop="text" content="%23%20Delete%20only%20if%20content%20is%20fully%20in%20main%0Aif%20git%20diff%20--quiet%20%22%24branch%22%20main%3B%20then%0A%20%20git%20branch%20-D%20%22%24branch%22%0A%20%20echo%20%22Deleted%20%24branch%22%0Aelse%0A%20%20echo%20%22Keeping%20%24branch%20(has%20unmerged%20changes)%22%0Afi">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># Delete only if content is fully in main</span><br><span class="token keyword">if</span> <span class="token function">git</span> <span class="token function">diff</span> <span class="token parameter variable">--quiet</span> <span class="token string">"<span class="token variable">$branch</span>"</span> main<span class="token punctuation">;</span> <span class="token keyword">then</span><br>  <span class="token function">git</span> branch <span class="token parameter variable">-D</span> <span class="token string">"<span class="token variable">$branch</span>"</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"Deleted <span class="token variable">$branch</span>"</span><br><span class="token keyword">else</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"Keeping <span class="token variable">$branch</span> (has unmerged changes)"</span><br><span class="token keyword">fi</span></code></pre>
    </div>
  </div>

  <p class="post__p">It uses the <code>--quiet</code> flag to suppress all output, as we don&#39;t need to view that whilst the script runs. Without the <code>--quiet</code> flag, here&#39;s what the command outputs in isolation when there is a diff:</p><img src="https://images.ctfassets.net/56dzm01z6lln/5NsHfI04BKrd8BFmcpsWbE/4a3eba900b10b467165d15d7bd48077b/git-diff.png" alt="Running git diff branch-6 main in a terminal showing a diff in the output." height="1058" width="1840" /><p class="post__p">And here&#39;s how the output looks when the full script runs and finds some branches to delete that have been squashed and merged on GitHub:</p><img src="https://images.ctfassets.net/56dzm01z6lln/3mNDs3yiNP6mCHdVD8DVni/effa454324eb6455123a1e125f00e318/running-dams.png" alt="Running the delete all merged and squashed branches script showing main has been checked out and pulled. Then branch 5 was deleted and branch 6 was skipped because it is not on the remote." height="1634" width="1840" /><h2 class="post__h2">Probably don&#39;t use this script</h2><p class="post__p">You probably shouldn&#39;t trust random bash scripts written by random people found on the internet, especially where your precious version-controlled work is concerned, so you probably shouldn&#39;t use this. This is my disclaimer. <b class="post__p--bold">Use this script with caution!</b></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Why is CSS ::first-letter not working?</title>
          <description>I had some misconceptions about this sneaky pseudo element.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/why-is-css-first-letter-not-working/</link>
          <guid>https://whitep4nth3r.com/blog/why-is-css-first-letter-not-working/</guid>
          <pubDate>Wed, 20 Aug 2025 01:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I recently had a use for the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::first-letter" target="_blank">CSS ::first-letter pseudo element</a>. I was dealing with some inconsistently formatted strings from the back end, and wanted to make sure that on the front end, all strings were shown with lowercase letters except for the first, which needed to be uppercase. I thought it would be trivial to use the following CSS:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ohmrYHkazo"
      aria-describedby="ohmrYHkazo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ohmrYHkazo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="ohmrYHkazo" itemprop="text" content=".parentElement%20%7B%0A%20%20text-transform%3A%20lowercase%3B%0A%7D%0A%0A.parentElement%3A%3Afirst-letter%20%7B%0A%20%20text-transform%3A%20uppercase%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.parentElement</span> <span class="token punctuation">{</span><br>  <span class="token property">text-transform</span><span class="token punctuation">:</span> lowercase<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.parentElement::first-letter</span> <span class="token punctuation">{</span><br>  <span class="token property">text-transform</span><span class="token punctuation">:</span> uppercase<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">In my case, however, the first letter was <b class="post__p--italic">not</b> being uppercased, and I was confused about why. Here is a modified version of the HTML I was attempting to style:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ArsibczpOI"
      aria-describedby="ArsibczpOI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ArsibczpOI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="ArsibczpOI" itemprop="text" content="%3Cdiv%20class%3D%22parentElement%22%3E%0A%20%20%3Cdiv%20class%3D%22child1%22%3E%0A%20%20%20%20%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%3Cpath%3E...%3C%2Fpath%3E%0A%20%20%20%20%3C%2Fsvg%3E%0A%20%20%3C%2Fdiv%3E%0A%20%20%3Cspan%20class%3D%22child2%22%3E%0A%20%20%20%20inconsistently%20FormattED%20string%0A%20%20%3C%2Fspan%3E%0A%3C%2Fdiv%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>parentElement<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>child1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>16<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>16<span class="token punctuation">"</span></span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>path</span><span class="token punctuation">></span></span>...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>path</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>child2<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    inconsistently FormattED string<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Naïvely, I thought I would be able to apply the styles on the div with the class <code>parentElement</code>, and the styles would cascade down to the span with the class <code>child2</code>. However, it turns out that the <code>::first-letter </code>pseudo element selector can only target text under a certain set of conditions.</p><h2 class="post__h2">The ::first-letter spec</h2><blockquote class="post__blockquote"><p class="post__p">The <b class="post__p--bold">::first-letter</b> <a href="https://developer.mozilla.org/en-US/docs/Web/CSS" target="_blank">CSS</a> pseudo-element applies styles to the first letter of the first line of a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Visual_formatting_model#block_containers" target="_blank">block container</a>, but only when not preceded by other content (such as images or inline tables).</p></blockquote><p class="post__p">When drilling down into what exactly defines a “<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Visual_formatting_model#block_boxes" target="_blank">block container</a>”, there is definitely some ambiguity across the CSS spec: “In specifications, block boxes, block-level boxes, and block containers are all referred to as <b class="post__p--bold">block boxes</b> in certain places. These things are somewhat different and the term block box should only be used if there is no ambiguity.” I won’t go deep into the specifics of block boxes and block containers in this post, but I will attempt to provide a summary:</p><p class="post__p">An element is a block container only if it contains block-level or inline-level boxes. The following values for the CSS <code>display</code> property produce block containers:</p><ul><li><p class="post__p"><code>block</code></p></li><li><p class="post__p"><code>inline-block</code></p></li><li><p class="post__p"><code>list-item</code></p></li><li><p class="post__p"><code>table-cell</code></p></li><li><p class="post__p"><code>table-caption</code></p></li><li><p class="post__p"><code>flow-root</code></p></li></ul><p class="post__p">The following values for the CSS <code>display</code> property produce <b class="post__p--italic">block-level</b> <b class="post__p--italic">boxes</b>, but not <b class="post__p--italic">block containers</b>, given how their child elements are formatted:</p><ul><li><p class="post__p"><code>flex</code></p></li><li><p class="post__p"><code>grid</code></p></li></ul><p class="post__p">The key issue in my case was that I was attempting to target a non-block container; the <code>parentElement</code> class specified a display property of <code>flex</code> and attempting to cascade the styles for <code>::first-letter</code> down to a child element.</p><h2 class="post__h2">The rules of ::first-letter</h2><p class="post__p">To summarise, using CSS <code>::first-letter</code> will <b class="post__p--bold">not</b> work in the following conditions:</p><ul><li><p class="post__p">The element you are targeting is set to <code>display: flex </code>or <code>display: grid</code></p></li><li><p class="post__p">The element you are targeting is preceded by other content, such as a <code>::before </code>pseudo element, images or inline tables</p></li><li><p class="post__p">You are attempting to cascade <code>::first-letter </code>styles from a parent element, even if the elements are block containers</p></li></ul><p class="post__p">To make sure CSS <code>::first-letter</code> works for you:</p><ul><li><p class="post__p">Apply the <code>::first-letter </code>styles directly to a block container (and those block containers are specified above)</p></li><li><p class="post__p">Ensure the content you are attempting to target is not preceded by other content, such as a <code>::before </code>pseudo element, images or inline tables</p></li></ul><h2 class="post__h2">A note on browser support</h2><p class="post__p">CSS <code>::first-letter</code> may have limited browser support: <a href="https://caniuse.com/?search=initial-letter" target="_blank">caniuse.com</a> reports this pseudo element selector is not supported in Firefox, but Baseline reports that &quot;This feature is well established and works across many devices and browser versions. It’s been available across browsers since ⁨July 2015⁩.&quot;</p><h2 class="post__h2">Using ::first-letter in action</h2><p class="post__p">To better understand how <code>::first-letter</code> works, I threw together some examples of when this pseudo element can be correctly targeted, and when it fails to work. You can view the examples at <a href="https://css_first_letter.toddle.site/" target="_blank">CSS first-letter demo</a>, and you can <a href="https://editor.nordcraft.com/projects/css_first_letter/branches/main/components/HomePage" target="_blank">view the code here</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Your Twitch live stream graphics don’t really matter, but they can help</title>
          <description>Create a professional-looking stream brand with just a few key graphics.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/create-free-twitch-live-stream-graphics/</link>
          <guid>https://whitep4nth3r.com/blog/create-free-twitch-live-stream-graphics/</guid>
          <pubDate>Tue, 19 Aug 2025 23:00:00 GMT</pubDate>
          <category>Streaming</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you’re thinking about live streaming on Twitch, my advice has always been to <b class="post__p--italic">not think too much about it</b> and Just Go Live™️. You’re probably stressing about your stream setup and whether it’s “good enough”, or whether your graphics and visuals are “professional enough”. <b class="post__p--bold">None of that really matters</b>; your stream is about <b class="post__p--italic">you</b> and the authentic connections and relationships you build with your viewers. </p><p class="post__p">However, good-looking graphics and a more polished approach to production in a sea of similar-looking stream thumbnails can certainly entice people to click on your stream to see what you’re all about. And then it’s down to you to get those curious viewers to stick around, press that follow button, and return to your next stream.</p><p class="post__p">I have received many compliments on my stream setup over the years, but it took years to get it to where it is today. I started out with nothing but a view of the chat and some follower stats powered by Streamlab’s fork of OBS. I had no branding, no graphics, and zero flair. If you’re curious about my stream’s evolution and how I’ve approached it, you can watch <a href="https://whitep4nth3r.com/talks/entertainment-as-code-finale/" target="_blank">a talk I gave at Web Unleashed in 2024</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/bb0BjRGjW5ebTjENKYOKd/e28b5ed20d67726fa23f639982464c1e/original-stream-setup.png" alt="A screenshot of whitep4nth3r's simple stream overlays from the early days of streaming in 2020." height="561" width="1000" /><p class="post__p">Since first pressing the go live button in 2020, I gradually developed more of a “personal brand”. This evolved over time, mirroring the evolution of my website design. On the latest branding iteration I was fortunate to work with a designer, <a href="https://bsky.app/profile/aaoa.design" target="_blank">aaoa</a> (who is also a streamer), but I am very conscious that not every streamer has access to, or the budget to employ a designer, especially when starting out. I’ve used this branding for over two years now, and already feel like I need a bit of a refresh.</p><h2 class="post__h2">What kind of stream graphics do I need to get started?</h2><p class="post__p">You can create a professional-looking stream brand with just a few key graphics, considering how your Twitch channel will look when you are both online <b class="post__p--italic">and offline</b>.</p><h3 class="post__h3">Panel graphics</h3><p class="post__p">Potential stream viewers often browse the Twitch directories looking for streamers to follow regardless of whether they are currently online. When your stream is offline and someone views your page, they will land on the “Home” tab, which shows your featured clips and suggested streamers, which you can configure in your featured content settings in your Twitch dashboard.</p><p class="post__p">Curious viewers will often click on the “About” tab, which is where you have the chance to shine. On the “About” tab you can add a number of different panels: you can tell people a little about yourself, lay down your stream rules, detail your stream tech specs, and more. Adding graphics to each of these panels can give a great first impression to potential followers and viewers, and helps to communicate your personality and what your stream is about. You can use graphics on panels as header images, or use taller images to fill a full panel.</p><img src="https://images.ctfassets.net/56dzm01z6lln/w20XiQCLbVxNctBGoC8w1/63c1e90f85578278099a6ced262fef56/twitch_about_panels.png" alt="Screenshot of my Twitch page, viewing the about panel. Showing panels for about me, The Claw stream team, my streaming specs and stream rules. Each panel consists of an image and text. The images are the panel titles, showing pink to orange gradient text for About, Stream Team, Specs and Rules. Each image also has another bit of text in a different font in black, either repeating the title or on the stream team graphic, it reads The Claw." height="3588" width="5344" /><h3 class="post__h3">Offline banner</h3><p class="post__p">If someone is exploring your Twitch page and they click on the “Chat” tab, they’ll see the live layout, with a space for the live video stream on the left, and the chat feed on the right. Here, Twitch gives you another opportunity to hint at your personality and the look and feel of your stream with an offline banner, which you can upload via the channel brand settings in your Twitch dashboard. I got a bit lazy with my latest offline banner, and it’s just a photo of me reaching to turn my camera off.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2m2Ovziyz1l5xd5fFmraDy/227a988969488aa77ff491ae2e11828a/twitch_offline_page.png" alt="A screenshot of my Twitch channel on the chat tab when I am offline. The video player is replaced with an image which is currently just my face looking at the camera, reaching to turn it off." height="671" width="1000" /><h3 class="post__h3">Starting soon screen</h3><p class="post__p">Most streamers like to show a “Starting soon” screen when they go live, which gives viewers a chance to join the chat before the real fun begins. This screen can contain some moving parts for some visual interest, which you’ll need to set up in your live streaming software (such as <a href="https://obsproject.com/" target="_blank">OBS</a>), but the easiest way to get a “Starting soon” screen up and running is by creating a static image that matches the branding of your offline banner and Twitch panel graphics. Here’s an example.</p><img src="https://images.ctfassets.net/56dzm01z6lln/CVj1Xo5BoXkDFxfW7rA9w/163b9a844a95935d6a21d5144231f988/stream_starting_example.png" alt="A black and pink pixellated image of various pixellated game style graphics including a cat, a computer, a mobile phone etc. A solid pink banner spans the width of the image with white text that says whitep4nth3r. Below in white uppercase text it says stream starting." height="675" width="1200" /><h3 class="post__h3">On a break screen</h3><p class="post__p">Sensible streamers take regular breaks throughout their streams, and for this you’ll probably want to switch to another scene to let viewers know you’re taking a break. Similar to the “Starting soon” screen, this can contain some moving parts, such as a feed of the chat or some recent stream clips, or for simplicity it can be a static image. Here’s an example.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1NTxbB4X76IyLByt4TemQN/87b336c7d21fb814aa576d8070876251/stream_break_example.png" alt="STREAM ON HOLD in large white uppercase letters to the left, with the subtitled grabbing a coffee BRB in a medium grey. To the right is an image of an old desktop computer from the 90s, haloed in neon green. The rest of the background is black with some grey random circle lines to add some design flair." height="1080" width="1920" /><h2 class="post__h2">How to create Twitch channel banners (for free!)</h2><p class="post__p">There are a number of free tools available to help you create graphics for your Twitch streams quickly and easily. I recently explored the capabilities of <a href="https://www.adobe.com/express/create/banner/twitch" target="_blank">Adobe Express</a> which claims you can “Make Twitch banners for free in minutes”. So how does it measure up?</p><img src="https://images.ctfassets.net/56dzm01z6lln/1yZtTlSmZnPzhdlpmryl0Y/c614f21830c7a4720a4ad3cdee960c2f/adobe_express_twitch_banner_homepage.png" alt="The headline aake Twitch banners for free in minutes on the Adobe Express landing page, showing a screenshot of the banner editing UI below." height="859" width="1280" /><p class="post__p">After signing up or logging in to Adobe Express from the <a href="https://www.adobe.com/express/create/banner/twitch" target="_blank">Twitch banner landing page</a>, you’re taken to a canvas where you can start creating your Twitch graphics. When you land, the templates search box comes with a pre-populated search term in the templates area — “twitch live stream banner” — so you can start browsing templates for inspiration straight away. You can also scroll the search box area horizontally for a list of other pre-configured search terms, such as “twitch panel banner” or “twitch starting soon banner”.</p><p class="post__p">These search terms largely return similar results, so you’ll probably want to use the results filter to narrow down your search, such as filtering by size. The recommended image sizes for panel banners varies depending on whether you want to show a header image or a full panel image, but you’ll probably want to use a 16:9 image for an offline banner. The size filters are extensive given the wide array of use cases for Adobe Express, so to filter accurately you’ll need to scroll through a lot of options.</p><p class="post__p">To browse free options only, make sure to check the “Show only: free” checkbox at the top of the left panel.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7vqQ9fSHOx63yr3bKLFg5y/e216e304b45a6524ace6012be0b91193/twitch_banner_creation_landing.png" alt="A blank canvas in Adobe Express, showing the pre-populated search term in the search box on the left, with the show only free checkbox checked. The search results are a grid of images in varying designs." height="1343" width="2000" /><p class="post__p">To save templates you like for later, you can hover the image and click the heart button. You can view your favourites in the “Your stuff” tab on the left of the UI.</p><img src="https://images.ctfassets.net/56dzm01z6lln/8IVcOK1BzyxYz0cELw5s9/e804a29997f9d725725e065593350dc2/adobe_express_favourites.png" alt="The your stuff tab is selected in Adobe Express at the left hand side of the UI, showing I have saved one template to favourites." height="644" width="718" /><p class="post__p">Stream branding is a very personal thing, and I often think that using a free tool to create personalised graphics means that I will end up with the same branding and graphics as other streamers, which isn’t ideal if you want to stand out in a saturated market. But, at the time of writing, there are over 1000 free templates to browse in Adobe Express, reducing the chance that the graphics you choose will be identical to other streamers using the same tool.</p><p class="post__p">Once you’ve scrolled the list of templates and you’ve found a design you like, click it to open it on the canvas and start editing.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6Ya4mIKw9xpCN3JBeNDkhD/ffe4c8ab4e738eee03f594449803a8dd/adobe_express_open_template.png" alt="A new template has been opened in Adobe Express, featuring an image of a man stylised in black and white against a pink, black and neon green brush background, with text on the right that says live mobile gaming channel with John Doe, subscribe now." height="1343" width="2000" /><p class="post__p">To replace the default images with your own, click the image and choose “Replace”, and you’ll have the opportunity to upload your own image. What’s great about this part is that the image you upload will then match the style of the template image.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5nJjJ8mtTKBi0eaot0o8q7/2a5831a6c8b2440be8f4807396484e0d/adobe_express_replace_image.png" alt="The Adobe Express canvas, cropped to zoom in on the replace button that appears after you click an image." height="1709" width="2581" /><p class="post__p">You can then click each text element and edit it to suit your needs, so you end up with something like this:</p><img src="https://images.ctfassets.net/56dzm01z6lln/798nmgQBd4993gCAAgHMeW/0f094fdca6e372cf769d98f95737e3bd/adobe_express_edited_template.png" alt="I've switched out the image of a man with an image of me pointing to the text on the right. My photo is stylised in black and white the same as the template image. On the right I have changed the text to live on twitch, writing code for your entertainment, with whitep4nth3r." height="1343" width="2000" /><p class="post__p">You can further customise the fonts, font sizes and colours, and the different layers that make up the design. Once you’re happy with your design, click “Download” in the top right. If you’ve used a free template, you can download the image straight away and use it on your Twitch channel.</p><p class="post__p">All in all, editing this template took me less than one minute. That’s not bad.</p><img src="https://images.ctfassets.net/56dzm01z6lln/660hngyhEUc9TpvQJR90Gt/d35f4073f7a8b81526bfe5ffd7aeade7/adobe_express_download.png" alt="The download dialog is open at the top right of the Adobe Express canvas, where you can choose the file type of the image, and the size, with a large purple download button." height="890" width="1200" /><h2 class="post__h2">What’s missing from Adobe Express Twitch banner maker?</h2><p class="post__p">Overall, this is a useful tool to help you create graphics in a few minutes for your Twitch channel for free. That being said, there are a couple of things that could be improved, so keep this in mind when you’re creating.</p><h3 class="post__h3">Template collections</h3><p class="post__p">While you can create single images from templates really quickly, what’s missing are collections of templates in the same style, so that you can easily create <b class="post__p--bold">matching</b> Twitch panel graphics, offline banners and stream graphics. At the time of writing, if you want to create a coherent look across your different key graphics, you’ll have to choose one template you like, and adjust the size of the canvas and move things around to account for the different image dimensions required for the different areas of your Twitch channel.</p><h3 class="post__h3">A good search experience</h3><p class="post__p">The search terms don’t always return good matches. The template I chose to edit above was named “Green Pink Live Mobile Gaming Video YouTube Banner”. Searching for this term didn’t return that exact template in the results, and using the size filter to attempt to find it in the list of 800+ results didn’t return any matches at all, despite filtering by the exact size of the template. If you like the look of any templates, save them to your favourites so you can find them again.</p><h2 class="post__h2">Try it for yourself</h2><p class="post__p">Minor issues aside, the Twitch Banner Maker from Adobe Express does what it claims: you really can “Make Twitch banners for free in minutes”. If you need a quick fix for your stream graphics whilst you’re getting started on Twitch, head over to <a href="https://www.adobe.com/express/create/banner/twitch" target="_blank">Adobe Express</a>, explore the templates, and personalise them to your heart’s content. And good luck on the live streaming, you got this.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I set up my Dygma Defy keyboard layers</title>
          <description>Gotta love those Superkeys.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/dygma-defy-keyboard-layers/</link>
          <guid>https://whitep4nth3r.com/blog/dygma-defy-keyboard-layers/</guid>
          <pubDate>Tue, 29 Jul 2025 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">After discovering the joy of using mechanical keyboards in 2020, I had been using a traditional “staggered” layout split keyboard for a number of years (a <a href="https://mistelkeyboard.com/products/d11cf7a73da49468e2a530b4cf18e76c" target="_blank">Mistel Barocco MD770</a> with red, then brown switches), and I was keen to move into the world of columnar layouts given the ergonomic benefits. But, with so much choice on the market, and not wanting to get too deep into custom keyboard builds, I never quite made the leap. However, ergonomic keyboard shop Dygma were kind enough to reach out to me in 2024 to send me a <a href="https://whitep4nth3r.com/keyboard" target="_blank">Dygma Defy keyboard</a> to try.</p><p class="post__p">I chose the wireless silver version with an extra set of English UK key caps, Kailh silent pink switches, tenting, and the RGBW Underglow.</p><img src="https://images.ctfassets.net/56dzm01z6lln/HExyQ7N90REMpWsJOeVla/3f135b037e4689ca681634d0286d254e/dygma.png" alt="The left half of a white and silver dygma defy keyboard, in the dark on a black desk matt, lit up with rainbow RGB. There’s another split keyboard behind it, lit purple and a white stream deck in the background. " height="1498" width="1498" /><p class="post__p">It looks <b class="post__p--italic">beautiful</b> on my desk. But does a beautiful keyboard like this deliver what it looks like it promises? And is it really worth the investment? And by investment I don’t just mean the money. Moving from a more traditional keyboard layout to a customised columnar layout is certainly a challenge if you’ve been typing “the normal way” for most of your life, and requires quite a bit of <b class="post__p--italic">time</b> investment to relearn how to type and to develop muscle memory for key customisations and layers. What’s more, I use multiple devices on a daily basis, and I only use a columnar keyboard for work. Switching back and forth between a columnar layout and staggered layout took quite a bit of getting used to at first, but now it’s second nature, like switching between playing a flute and a piccolo, if you’re familiar with that type of thing.</p><p class="post__p">Honestly though, I am really enjoying using my Dygma Defy and I can’t see myself switching to a different keyboard any time soon. With that being said, I probably don’t use this keyboard to its full potential, and so I thought I’d take the time to talk through my five customised layers for anyone who’s curious, and also to remind myself what customisations I don’t use enough.</p><p class="post__p">The Dygma Defy is customisable using Bazecor (software built by Dygma), and it’s pretty intuitive. It allows for some incredible customisations, including macros (combinations of keys), Superkeys, and full LED customisation. Plus, I periodically save a backup of my configuration to GitHub, which has been useful when switching machines.</p><h2 class="post__h2">Base layer</h2><p class="post__p">The base layer is the default layer, and is used for general typing activities with a few modifications.</p><img src="https://images.ctfassets.net/56dzm01z6lln/07NapZrMNFy8dWqfPpPIM/1a9779fbd1d266df8ad68c7def9081cc/dygma_layer_1.png" alt="The base layer I configured for my Dygma defy. Details are described below." height="1440" width="2560" /><h3 class="post__h3">The thumb clusters</h3><ul><li><p class="post__p">The yellow keys numbered 2, 3, 4 switch to the relevant layers by holding the key. You can choose whether the layers are accessed via a key press or a key hold. I prefer to use the key hold so that I can be sure that when I am not touching the keyboard, the base layer is active.</p></li><li><p class="post__p">I configured two Enter keys; I usually use the right Enter key when typing, but the left Enter key is useful when I am using my trackpad with my right hand to move the cursor around text.</p></li><li><p class="post__p">The same is true for the extra backspace key on the left thumb cluster, coloured red to match the other backspace key at the top right, which is where it usually lives on more traditional keyboards.</p></li><li><p class="post__p">I also configured two command keys (⌘) on the left and right, but I only really use the left.</p></li><li><p class="post__p">Two space keys on either side are also useful, but I only really use the right.</p></li><li><p class="post__p">On the left there is a Superkey, coloured purple. Superkeys are a really neat concept by Dygma, which allow you to use different actions on one key to do different things. On this Superkey, a single press copies, a long press pastes, and a double tap cuts.</p></li><li><p class="post__p">On the right there is a macro key labelled “Emoji”, which is configured to pop up the emoji picker on Mac, which is the equivalent of pressing CTRL + Left ⌘ + Space.</p></li><li><p class="post__p">There are also some keys to check the battery level or pair with bluetooth, which I set up to remind me those things existed. I don’t really use these keys, given I keep my keyboard plugged in to give me quicker access to configuring things in Bazecor on the fly (customisation is more stable when the keyboard is plugged in). I am a little annoyed with the wires on my desk all the time, so maybe I’ll switch to wireless at some point.</p></li></ul><h3 class="post__h3">The main keyboard</h3><p class="post__p">This is pretty much a standard setup, with a few personalisations:</p><ul><li><p class="post__p">I don’t use caps lock, so I switched that key for a backtick (above left shift).</p></li><li><p class="post__p">Backspace and delete are useful next to each other at the top right.</p></li><li><p class="post__p">I added an LED configuration cycle button for my left hand (next to B), which I press by mistake far too often. I should probably remove this given I prefer to use the keyboard with my own LED colour-codings, but I always feel like <b class="post__p--italic">sometimes </b>I might want to change the LEDs to one of the other settings, like rainbow, or pure white. But colour is such as important part of how I configured my keyboard, so I just don’t.</p></li><li><p class="post__p">I configured some commonly used symbols in the two centre columns: -, =, [, ], \.</p></li><li><p class="post__p">The SUPER H key when held switches to layer 5, which I’ll describe below. It functions as a standard H key when tapped.</p></li></ul><h2 class="post__h2">Layer 2</h2><p class="post__p">This layer is a pretty powerful layer, with a number of macros that power window management. It is accessed by holding the layer 2 button on the right thumb cluster, which appears as TRANS for transparent on this layer, meaning it always defaults to the base layer function.</p><img src="https://images.ctfassets.net/56dzm01z6lln/33oNyEqGd5hOz9g8OKrmSn/9d231106af9bf51e368305608712443f/dygma_layer_2.png" alt="The layer 2 I configured for my Dygma defy. Details are described below." height="1440" width="2560" /><h3 class="post__h3">Window management</h3><p class="post__p">I’m using Raycast for window management. Setting up the window management keys involved configuring the commands in Raycast first, and mapping those commands to macros in Bazecor. I’ve found this to be a really efficient way of using window management without having to memorise a bunch of key commands, which are often convoluted. That being said, I have no idea what key commands I set up in Raycast now, so I am lost without the Dygma with regards to window management.</p><ul><li><p class="post__p">The pink keys around the middle power a number of window management macros, such as left half/right half/top half/bottom half and centre to screen.</p></li><li><p class="post__p">The two MACRO Move keys on the left side move windows between screens.</p></li><li><p class="post__p">The MACRO Maxim key in purple on the left side maximises a window.</p></li><li><p class="post__p">The arrow keys are self explanatory.</p></li><li><p class="post__p">I don’t use HOME, END, PAGE UP or PAGE DOWN, but I didn’t really use them on a traditional keyboard either. I probably should.</p></li><li><p class="post__p">I also put caps lock on this layer, but I forgot about this, so don’t use it.</p></li><li><p class="post__p">I set up the number pad on the right side, but I hardly use this because I seldom press the correct keys. I coloured the 5 key on the number pad differently to try and position my hand correctly, but I just haven’t used it enough for the muscle memory to kick in yet.</p></li><li><p class="post__p">Upon inspecting this layer whilst writing this post, I had forgotten that I had set up a Macro to open the clipboard in Raycast on the right thumb clubster. To this day I have been using the long traditional shortcut on the base layer. I think I don’t use it because it’s awkward to press the key combination of thumb and fourth finger on that right cluster unless I move my hand a lot.</p></li></ul><h2 class="post__h2">Layer 3</h2><p class="post__p">This layer serves to recreate most of the F row keys and their functionality on a Mac, with some bonus macros.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7dhUIARaHYbb8OcV8ykNCa/4d5c234fa66dc7c3122b2d59c7c577df/dygma_layer_3.png" alt="The layer 3 I configured for my Dygma defy. Details are described below." height="1440" width="2560" /><ul><li><p class="post__p">The F key row is at the top. I seldom have a use for this.</p></li><li><p class="post__p">Media keys and volume control are on the left.</p></li><li><p class="post__p">The pink macro on the left is a Mac screen capture macro, which I use a lot. It’s the equivalent of pressing CTRL + Shift + Left ⌘ + 4.</p></li><li><p class="post__p">On the right, I set up two macros to zoom in and out, as it was cumbersome on the base layer to use ⌘ + +/- to zoom in and out, given that I placed the + and - on opposite sides of the keyboard.</p></li><li><p class="post__p">On the right I also configured keys for increasing/decreasing my laptop screen brightness, and sleeping or shutting down my machine. I always use the sleep button. These functions are built into Bazecore, which I think is a really nice touch. There’s even the option to bring up a calculator! But I tend to use Raycast for quick calculations.</p></li></ul><img src="https://images.ctfassets.net/56dzm01z6lln/3WXwYHkRscNlJ8wcGYbnr2/736d9973e4f0fd810060fcaa7e3fc014/dygma_media_config.png" alt="A screenshot of the Bazecor interface showing that you can assign keyboard keys to media keys, LED functions, and machine functions such as sleep or shut down using the interface directly." height="319" width="1161" /><h2 class="post__h2">Layer 4</h2><p class="post__p">I set up a mouse layer in the hope that I would train myself to use my trackpad less, and stop moving my hands back and forth from the keyboard. This has, so far, been unsuccessful.</p><p class="post__p">To hold the layer 4 access button and use the mouse wheel up/down keys is quite comfortable but I think I could find a better configuration. Maybe I should just unplug my trackpad and force myself down this path.</p><img src="https://images.ctfassets.net/56dzm01z6lln/79wRtj4qQNQYG7ZKUjuAio/4238f4add26cf7729793061bb3e5e395/dygma_layer_4.png" alt="The layer 4 I configured for my Dygma defy. Details are described above." height="1440" width="2560" /><h2 class="post__h2">Layer 5</h2><p class="post__p">Whilst I have become very comfortable typing text on this keyboard, I still make quite a few mistakes when coding. It’s still ingrained in me to use the traditional keyboard configurations on the base layer for symbols like [] and {} with the shift key.</p><p class="post__p">I set up a symbols layer to try and improve my efficiency with these symbols, which is accessed by holding H on the right. I just haven’t developed the muscle memory to access this layer enough, yet.</p><p class="post__p">I colour-coded similar opening and closing brackets to try and train me on which was which. I even set up a macro for ⇒ for all you JavaScript fans out there.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5pWLyh2LZmuol8lVVkPaGk/a7d95f97d06ae622205a725988e28569/dygma_layer_5.png" alt="The layer 5 I configured for my Dygma defy. Details are described above." height="1440" width="2560" /><h2 class="post__h2">Notes on ergonomics</h2><p class="post__p">I realised whilst writing this post that I have not been making use of the keyboard tenting, which I have heard is good for reducing strain on your wrists. I stopped using tenting when I found it difficult to type earlier this year when I was having trouble with my hands (read <a href="https://whitep4nth3r.com/blog/how-i-learned-to-code-with-my-voice/" target="_blank">How I learned to code with my voice</a> if you’re curious), and I forgot to go back.</p><p class="post__p">With regards to ergonomics, I have found that using a columnar keyboard is definitely better for my hands. It feels odd at first, switching from a traditional staggered layout to a columnar layout, but when you get used to it, you really are moving your hands a lot less, which is better, apparently.</p><p class="post__p">Additionally, the way you set up your keyboard layers is the key to your own personal ergonomics, which this is something that traditional non-customisable keyboards just can’t provide. When setting up your layers, try out the configurations and key combinations to make sure that you’re not putting strain on your hands. If you find them difficult to access, or if they cause pain, switch them up.</p><p class="post__p">Customisable keyboards geared towards ergonomics can also get you out of bad habits. One of the things I noticed is that on a traditional keyboard, I used to press Right Shift plus {}, which gave me a kind of claw hand many times a day. The way I have set this keyboard up forces me to press Left Shift + } and Right Shift +{, which means I don’t code with claw hands. With that being said, this is still very awkward when coding, so I should <b class="post__p--italic">really use my symbol layer</b>.</p><p class="post__p">This is a great keyboard. Check it out on the official Dygma website: <a href="https://whitep4nth3r.com/keyboard" target="_blank">Dygma Defy keyboard</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Put your phone down</title>
          <description>Is this how we're living our lives now? </description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/put-your-phone-down/</link>
          <guid>https://whitep4nth3r.com/blog/put-your-phone-down/</guid>
          <pubDate>Thu, 17 Jul 2025 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Yesterday, my husband and I treated ourselves to an evening at the spa. It was a nice spa, clean and spacious, not too busy. But what was supposed to be a relaxing and rejuvenating experience soon turned into an incredibly stressful and surreal one, because most of the other spa attendees spent the majority of their time with their phones in their hands: filming.</p><p class="post__p">What started off as a few selfies next to the opulent spa decor quickly descended into these eager content creators filming almost every single moment they were experiencing, editing the images and videos as they were taken, <b class="post__p--italic">right from in the pool</b>, comparing shots and angles. My husband and I spent most of our time in the spa either moving to different spaces to put ourselves out of the way, or having to dodge the line of sight of the tens of active phone cameras, lest we be thrust onto the internet in our swimsuits. When the other guests weren&#39;t filming, they were scrolling their phones from the pool as if they were on their couches at home, most probably watching for the likes to roll in on the TikToks and Instagram reels they posted just minutes earlier.</p><p class="post__p">In the past, I&#39;ve been to spas that operate a strict no-phone policy to protect the privacy and experience of its paying guests. So why was it different at this spa? My only guess is that they&#39;re profiting from <b class="post__p--italic">free internet promotion</b> by its patrons at the expense of the overall experience. We did the British thing and sent a strongly worded email when we got home.</p><p class="post__p">Looking around at the sea of phones in what was supposed to be an escape away from the hustle and bustle of real life, work, bills, and responsibilities, I felt sad. Not just because the risk of appearing in someone&#39;s internet &quot;content&quot; in my swimsuit without consent is an invasion of privacy, but that these people weren&#39;t truly <b class="post__p--italic">experiencing</b> what was intended to be experienced. They weren&#39;t truly present; they were somewhere on the internet, <b class="post__p--bold">doing labour for likes</b>.</p><p class="post__p">Is this how we&#39;re living our lives now? </p><p class="post__p">Put your phone down.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Why Women in Tech isn't enough</title>
          <description>Real progress needs systemic change and active involvement from men in positions of power.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/why-women-in-tech-isnt-enough/</link>
          <guid>https://whitep4nth3r.com/blog/why-women-in-tech-isnt-enough/</guid>
          <pubDate>Tue, 15 Jul 2025 23:00:00 GMT</pubDate>
          <category>Career</category>
          
  <content:encoded><![CDATA[ 
    <div class="post__callout">
  <h3 class="post__callout__title">Disclaimer</h3>
    <div class="post__callout__content">
      <p>This article is based entirely on my personal experience as a woman in the technology industry. I have no doubt that there are organisations and initiatives that are putting in incredible work to improve experiences and opportunities for &quot;non-men&quot; in the industry, and genuinely helping to improve equity in hiring pipelines in tech. If you’ve found safety and opportunity in such spaces as described below, I am truly happy for you.</p>
<p>This article does not intend to belittle the efforts of such initiatives, but highlight that they may not be working for everyone as intended. I am most certainly not advocating for funding cuts or erasure around DEI initiatives, but only that we rethink what is not working.</p>

    </div>
  </div><p class="post__p">The technology industry (and beyond) is performative, often offering shallow solutions to deeper problems, shifting the responsibility of change onto those that bear the brunt of the dire consequences the world has created for itself. <b class="post__p--italic">Want to prevent climate change?</b> Use paper straws instead of holding corporations accountable for their increasing carbon emissions. <b class="post__p--italic">Want to protect your mental health online?</b> Limit your screen time using app-blocking tools instead of questioning why social media platforms have been deliberately engineered to be addictive. <b class="post__p--italic">Want to create equal opportunities in the technology industry?</b> Segregate all the women, non-binary, intersex, LGBTQIA+ identifying and trans folk under the banner “Women in Tech” and hope they figure it all out.</p><p class="post__p">At most of the tech companies I have worked, there have been dedicated spaces for “non-men” to get together, including Slack channels, informal meet-ups, and formal discussion panels. “Non-men” in this context has been used to discount “non-heterosexual-cis-men”, and to include women, non-binary, intersex, trans women, trans men and those identifying as LGBTQIA+ all at once. The people spanning the very wide spectrum of this group have probably, and unfortunately, all experienced some kind of discrimination in the workplace, and so it seems sensible for a company to put us all together to share our experiences in a safe and enclosed space, right? It helps, but I&#39;m not so sure it offers a full solution.</p><h2 class="post__h2">The problem with “non-men” spaces</h2><p class="post__p">Reports on gender statistics in the technology industry have consistently published figures of around 17-25% of women and non-binary people working in this industry since the early 2000s (this varies by geographical location), and not much has changed over the last 25 years. A recent <a href="https://spacelift.io/blog/women-in-tech-statistics" target="_blank">statistics dump by Spacelift</a> reports that “half of all women who work in tech have left the industry by age 35”, and “none of the biggest US tech companies report women occupying more than a quarter of all technical roles”. Furthermore, “the proportion of undergraduate computer science degrees awarded to women has fallen from 37% in 1985 to about 20% today.”</p><p class="post__p">Why are the statistics not showing much improvement despite countless initiatives surrounding diversity being created, and in particular events, awards, and “safe” spaces for “Women in Tech”? I have a hypothesis: all of these initiatives are operating in closed and exclusive spaces, which is sending a strong signal to the male majority with the power to affect real change that they are not welcome here, and it <b class="post__p--italic">is not their problem to solve</b>.</p><p class="post__p">A few years ago I was nominated for a “Women in Tech” award by an anonymous party. Not only did I have to confirm my nomination by writing a number of time-consuming essays about my achievements and eligibility for the award, I also couldn’t shake the weird feeling that I was nominated only on the basis that I am a woman. I understand the need to celebrate and lift up “non-men” in tech, and to provide a platform for representation to young and less experienced people entering the industry, but I wasn’t comfortable with demonstrating that if you want to succeed in the tech industry you need special awards and events under the banner of your gender. I did not accept the nomination; I want to win awards that the men are eligible for, too. Because I can, and I have.</p><p class="post__p">These enclosed “non-men” spaces are also not without their specific problems. At one company I worked for, I was asked to participate in a “Senior Women in Tech Panel”, where I was invited to share my experiences and career progression in tech to an audience of women, non-binary, and trans men and women. This event was not moderated well, and descended into a chaotic discussion about how many children each of the panel members had, and negative remarks about the ethnicity and representation of the panel speakers. This may have been a one-off, but at that point I decided to leave the closed Slack channel for “non-men”, and chose not to participate in events of this type for the rest of my career.</p><p class="post__p">What&#39;s more, the hard work of ensuring that workplaces are equitable often falls upon the shoulders of underrepresented groups, who are already carrying the burden of discrimination, being overlooked for promotions, and other related issues. A friend reports:</p><blockquote class="post__blockquote"><p class="post__p">They&#39;ve started a women&#39;s network at work and I am not a member because I think these initiatives are basically just making female members of staff do extra work to fix organisational problems.</p></blockquote><h2 class="post__h2">The problem with women-coded labels</h2><p class="post__p">Imagine if men described themselves as a “male founder” or “HeTO” (a male CTO), or slapped the #MenInTech label on their social media posts when sharing their achievements? The industry’s obsession with women-coded job titles such as “female founder” (which has descended painfully into “girl boss”) that were conjured up to challenge the male default and demonstrate representation in the industry, does not sit comfortably with me.</p><p class="post__p">Feminising job titles, in my opinion, belittles success and achievement in the context of a patriarchal default. Just as “Women in Tech” awards and closed spaces for “non-men” segregate underrepresented groups of people from the spaces where people with power make decisions, women-coded labels inherently classify people on a separate, non-default scale of success and achievement. </p><h2 class="post__h2">It may work for early career folks, but what&#39;s next?</h2><p class="post__p">After speaking with many women during writing this article, it looks like &quot;Women in Tech&quot; initiatives work well in the context of those entering the industry, but may not be enough to support getting enough underrepresented groups into leadership positions. </p><p class="post__p">A friend of mine shared this with me about her experiences:</p><blockquote class="post__blockquote"><p class="post__p">I joined tech through Rails Girls. It was a really nice space, it made me feel very safe. It also played into this thing that I knew was happening a lot, which is women thinking that they are not capable of doing something because the world is telling them that they cannot. I really loved getting into tech through these female-focused initiatives. It made me feel like there was a group of people who cared about me joining tech, which is such an important thing if you are looking at an industry that is overloaded with a specific default type of guy.</p></blockquote><p class="post__p">Another friend had a similar experience, but once she had established her career, things changed:</p><blockquote class="post__blockquote"><p class="post__p">My perspective also changed with age and expertise gathered. I found a lot of “Women in Tech” programs and initiatives incredibly useful as a newbie. I wouldn’t be where I am without codebar and some early speaking opportunities. But as I gained my voice and my knowledge I found it increasingly patronising.</p></blockquote><p class="post__p">Another <a href="https://www.aiprm.com/women-in-tech-statistics/" target="_blank">Women in Tech statistics report from AIPRM </a>reports that in 2025, &quot;around 14% of global tech leaders were female in 2023 – up from 8% in 2015&quot;, which is promising. Yet, the Spacelift report also finds that “The so-called Big Five tech companies (Alphabet/Google, Apple, Meta/Facebook, Amazon, Microsoft) have <b class="post__p--italic">never</b> had a female CEO.” And to couple that with another finding from the Spacelift report mentioned above, that “half of all women who work in tech have left the industry by age 35”, it seems that once we get to a certain level of seniority, age, or experience, something isn&#39;t working.</p><p class="post__p">Rachel-Lee Nabors wrote about this in 2018 in an article titled <a href="https://medium.com/@rachelnabors/a-counterintuitive-way-to-increase-diversity-in-tech-31aea2ce6a50" target="_blank">A Counterintuitive Way to Increase Diversity in Tech</a>:</p><blockquote class="post__blockquote"><p class="post__p">I’m mid-career in the web development and design industry. And right now, I’m hearing a lot of the same stories from my fellow mid-career friends from underrepresented groups (women, minorities, LGBTQ, and more): you reach a certain point in your career and you can’t win.</p></blockquote><h2 class="post__h2">Responses to how you may be feeling right now</h2><blockquote class="post__blockquote"><p class="post__p">I have benefitted from &quot;Women in Tech&quot; initiatives or other &quot;non-men&quot; initiatives in tech. Are you saying these programmes shouldn&#39;t exist?</p></blockquote><blockquote class="post__blockquote"><p class="post__p">I&#39;m a man and I think DEI is terrible. I&#39;m glad to see a woman agreeing with me at last!</p></blockquote><p class="post__p">As stated in the disclaimer, this is absolutely not the point of this article. Diversity, equity and inclusion initiatives are absolutely needed in this industry. However, the current landscape has seemingly affected little change in statistics on the whole, especially at leadership levels. I don&#39;t think we can continue to segregate ourselves if we want to make progress. Personally, I have worked with some excellent men. And men who have worked with me have said I am also excellent; we have all benefitted from working with each other. To that end, we absolutely need these initiatives: but the men must be invested as well. They must not be excluded from the conversation about how they can help affect change, especially in the context of leadership, given it&#39;s mostly the men who hold those positions today.</p><blockquote class="post__blockquote"><p class="post__p">I am a man and consider myself an ally. Do I need to do something?</p></blockquote><p class="post__p">I remember speaking on a panel of all women, which was organised by a man. I requested that he not promote the event as an &quot;all-women&quot; event to avoid focussing on gender. He did it anyway. Celebrate underrepresented groups in tech, but do not focus on their gender identity. Celebrate their achievements, use your privilege to lift them up, share their work, make their voice heard: not because they are not men, but because their work is worth celebrating. If you are a man in a leadership position, you can absolutely do more to bring more underrepresented groups into leadership with you. To take another quote from Rachel-Lee&#39;s article:</p><blockquote class="post__blockquote"><p class="post__p">You should be a mouthpiece when someone isn’t heard, an advocate when people are unable to be represented, a shield bearer when folks are under attack, an elevator for the stuck, a signal booster for the unnoticed. Wherever possible, let them do the work, and be heard and understood, and represent themselves. You’re the back up, the path carver, the taker of the brunt of the negative impact, always asking, “How can I help?” Always pushing others up, even over yourself.</p></blockquote><h2 class="post__h2">We’re focussing on the wrong thing</h2><p class="post__p">I think that purely gender-based initiatives are shallow solutions to the deeper problems of male-dominated industries and society as a whole, that focus solely on surface-level optics of inclusion rather than addressing the root cause of inequality and imbalance on a larger scale. So, how do we address the root cause of inequality and imbalance? </p><p class="post__p"><b class="post__p--bold">We invite those with power to have a seat at the women’s table (and the underrepresented table as a whole): men </b><b class="post__p--bold"><b class="post__p--italic">must</b></b><b class="post__p--bold"> be part of the conversation to help affect change, especially at higher seniority levels.</b></p><p class="post__p">Women, non-binary, intersex and trans folk are in tech, have always been in tech, and will (hopefully) always be in tech. There <b class="post__p--italic">is</b> a problem, in that these underrepresented groups are often discriminated against and overlooked. The solution? Men in tech can lead the change in how men represent themselves and their gender as a whole in the industry. A bandaid? Groups where &quot;non-men&quot; can feel safe. Both are important, but only one will affect real change.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>The promise that wasn’t kept</title>
          <description>This isn't progress.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/the-promise-that-wasnt-kept/</link>
          <guid>https://whitep4nth3r.com/blog/the-promise-that-wasnt-kept/</guid>
          <pubDate>Wed, 28 May 2025 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I recently wrote about <a href="https://whitep4nth3r.com/blog/does-ai-really-make-you-more-productive/" target="_blank">AI and productivity</a>, and how data from the <a href="https://cloud.google.com/devops/state-of-devops" target="_blank">2024 Accelerate State of DevOps Report</a> from DORA shows that widespread AI adoption in the software industry is contributing to a real and meaningful decline in software delivery performance. Approximately 76% of developers use AI tools in daily tasks such as coding, debugging, and documentation.</p><p class="post__p">This week I posted <a href="https://bsky.app/profile/whitep4nth3r.com/post/3lq7ls2rgz22p" target="_blank">a silly and whimsical post on Bluesky</a>, which seemed to resonate with a lot of you out there, and it reminded me of a section in the DORA report about <b class="post__p--italic">valuable work</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1anQ22fRzvjnTYgdcXguCj/69823bccdf406ced4d62290b221bf4e1/bsky_post_value.png" alt="i was born to make websites, fun websites, silly websites, curious websites, websites that bend, delight, amuse and entertain, websites for people on planet earth, and there will be no shareholder value, but the websites will be built, and they will be enjoyed" height="512" width="900" /><p class="post__p">AI has always promised to “help people spend more time doing valuable work” by “automating the manual, repetitive, toilsome tasks” so that software developers can be “free to use their time on ‘something better.’” Despite this, the report states that “individuals are reporting a decrease in the amount of time they spend doing valuable work as AI adoption increases”. The maths isn’t mathsing.</p><h2 class="post__h2">What is valuable work, actually?</h2><p class="post__p">I have observed a growing trend of developers focussing solely on the tools used to make software, rather than what the software actually <b class="post__p--italic">does</b>. Many people are sharing their new apps on social media, attempting to provide context for their creations by listing the databases, runtimes, frameworks, UI libraries and AI code generation tools they used. But what does your app actually <b class="post__p--italic">do</b>? What problems does it solve? <b class="post__p--bold">Tell me about the value you just created!</b></p><p class="post__p">Valuable work and meaning is not derived from what AI makes us (apparently) faster at: generating code. Meaning and value in software development is actually created through the impact of building things that makes human lives <b class="post__p--italic">better</b>, or <b class="post__p--italic">easier</b>, or <b class="post__p--italic">slightly less bad</b>. Now, it can be argued that much of the work in the technology industry in 2025 is not centred on making things better for humans whatsoever, but that’s a discussion for another day. </p><p class="post__p">What’s becoming clear is that the mass adoption of AI is shifting the focus away from human-centred software solutions that provide meaningful value, and is reducing the entire industry to just the tools at its disposal. <b class="post__p--bold">Just generate the code, bro. Just ship one more app, bro.</b></p><h2 class="post__h2">The new kitchen metaphor</h2><p class="post__p">If I employ someone to build a new kitchen for me, I really don’t care what drills, hammers, nails, or sandpaper they use to get the job done. I just want a valuable end result: a fancy-looking and functional kitchen that makes good use of the available space, enabling me to cook delicious food in a delightful and comfortable environment. Ultimately, a great kitchen is created with vision, creativity, and by solving existing problems the old kitchen presented. The same is true for software.</p><p class="post__p">Value in software development cannot be determined by how many lines of code you can bash out in any working day, and especially whether or not you are using AI to do so. Real value is delivered through vision, creativity, experimentation, and using human brains to solve human-centred problems. The report backs this up:</p><blockquote class="post__blockquote"><p class="post__p">[T]here is also an art and empathy underlying a great product. This might be difficult to believe for people who think everything is a problem to be resolved through computation, but certain elements of product development, such as creativity or user experience design, may still (or forever) heavily rely on human intuition and expertise.</p></blockquote><p class="post__p">And what’s even more interesting, is that while seemingly “high-performing teams and organizations use AI”, the report finds that “products don’t seem to benefit”. We’re all just churning out AI generated code, moving those tickets, making meaningless graphs go up, but to what end? Real software is actually not moving forward. We&#39;re all just cranking out the same broken software with the same stupid bugs. In fact, as I was editing this post whilst sitting with a fresh batch of black hair dye on my head at the hairdressers, one wrong tap of a button on my phone deleted half of the article. I attempted to use the three-finger gesture to bring up the undo button on my iPhone, which, not surprisingly, did not restore what was deleted. I ended up having to find a deploy preview of this post (that I fortunately deployed before I left the house), copy and paste half of the article back into the CMS, and reformat the headings, being very careful to not make a single wrong move.</p><p class="post__p">All the new kitchens and silly add-ons are shit. There’s no value in that. </p><h2 class="post__h2">Tools do not determine value created</h2><p class="post__p">The tools someone uses to build a kitchen are only as good as the skills of the person using them. A skilled craftsperson can probably use any old tool and produce a great result that holds up for years to come. A less experienced craftsperson who doesn’t understand the fundamental concepts of space, structure and value-based utility might be able to make a single kitchen cabinet <b class="post__p--italic">look good</b> to the untrained eye, only for the shelves to be at the wrong height so I can&#39;t store my stuff. The same can be said for software.</p><p class="post__p">There’s nothing wrong with being inexperienced; we all have to start from somewhere. But we can’t rely on tools as a shortcut to <b class="post__p--italic">gain</b> valuable experience. Experience takes time to develop, and your tools are only as good as your fundamental knowledge and skills. If you skip the knowledge and skills part, and if you fail to <b class="post__p--italic">learn</b> about what you’re doing and the implications of <b class="post__p--italic">how you’re doing it</b> and the human value you have the potential to deliver, then you have little hope of building human value into your software. Because for the most part, <b class="post__p--bold"><b class="post__p--italic">humans use software</b></b>. Andreas Møller said it better in <a href="https://blog.nordcraft.com/they-lied-to-you-building-software-is-really-hard" target="_blank">They lied to you. Building software is really hard</a>:</p><blockquote class="post__blockquote"><p class="post__p">The true value of a software engineer is in our ability to analyze problems as well as design and implement creative solutions. To get good at these skills you need to understand not just the tools at your disposal but also the technologies you are building on top of. If you don’t understand how an application works then you have no chance of fixing its bugs and issues.</p></blockquote><p class="post__p">In the report, “respondents also reported expectations that AI will have net-negative impacts on their careers, the environment, and society, as a whole”. Whilst AI has empowered anyone to build and ship web and mobile apps, the tangible negative impact of the soulless, valueless software released on a daily basis across the industry cannot be underestimated.</p><p class="post__p">And no, I do not want to use your new AI tool to summarise an email (?) or summarise a document (?) or summarise a meeting (?) or anything else. I want to use my brain so I can comprehend and learn and connect with what is in front of me.</p><h2 class="post__h2">Productivity <b class="post__p--bold">≠ </b>value</h2><p class="post__p">Returning to the topic of the previous article, I want to touch again on the topic of productivity. Productivity as a measure of value is extremely misleading. The very notion of productivity has been conjured up by Big Capitalism™️ to keep us busy, and to misdirect our attention so that we forget to question the broken system in which we have no choice but to participate. All of this prevents us from real growth, and moves us further and further away from the pursuit of real value.</p><p class="post__p">You could argue there is space for both approaches in software development. Build the hard, compelling stuff using your human brain, and use AI to generate the code for some of the more boring parts of your apps. Take form validation, for example, scaffolding out a new project, or setting up all the boilerplate to make some API calls. But those boring parts are only boring <b class="post__p--bold"><b class="post__p--italic">because you wrote the code to do the same thing before!</b></b> You already learned how to do it without AI.</p><p class="post__p">But unfortunately, too many of us are getting sucked into the productivity hype cycle and engaging in daily conversations with energy-sapping computer machines that vomit out thousands of lines of code based on probability and existing mistakes that the Large Language Models themselves are trained on. It&#39;s absurd. We have a whole new cohort of inexperienced kitchen builders who have invested in the latest must-have drills, hammers, nails and sandpaper, but have no idea how to build real value into what they are using those tools to create. And so we&#39;re seeing an influx of infinite inferior kitchens that offer no human value. They may look good when you walk into the room, but will inevitably fall to pieces as soon as you put the kettle on to make a cup of tea. </p><p class="post__p">The data speaks for itself. Vibe coders are reporting <a href="https://www.techrepublic.com/article/ai-generated-code-outages/" target="_blank">outages and critical security vulnerabilities in their apps</a>, <a href="https://www.reddit.com/r/cursor/comments/1inoryp/cursor_fck_up_my_4_months_of_works/" target="_blank">losing months of work that didn’t use version control</a>, and the <a href="https://varunraghu.com/why-i-wont-be-vibe-coding-anymore/" target="_blank">inability to truly learn new things</a>.</p><h2 class="post__h2">The world is already cooked, and yet, we’re cooking it more</h2><p class="post__p">What’s more, there’s the environmental impacts of AI, which are only just beginning to emerge. An MIT article titled <a href="https://news.mit.edu/2025/explained-generative-ai-environmental-impact-0117" target="_blank">Explained: Generative AI’s environmental impact</a> outlines how the rapid expansion of generative AI presents significant sustainability challenges, including electricity and water overuse, hardware-related emissions, and increasing pressure on power grids.</p><p class="post__p">The world is already cooked. And yet, we’re cooking it more — literally, and figuratively — by shipping insecure software into the void that we have no idea how to debug, scale, or extend. In the not-so-distant future, LLMs will be trained purely on LLM-generated software, and the world will eat itself.</p><p class="post__p">I challenge you to find the value in that.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>The experience is enough</title>
          <description>I found my tribe at a conference.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/the-experience-is-enough/</link>
          <guid>https://whitep4nth3r.com/blog/the-experience-is-enough/</guid>
          <pubDate>Mon, 12 May 2025 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">A few weeks ago I attended <a href="https://heypresents.com/" target="_blank">All Day Hey</a>, a friendly one-day single-track conference in Leeds, UK. I attended with a group of very solid Internet Friends™️, most of whom I was meeting In Real Life for the first time.</p><p class="post__p">I have attended many conferences over the last few years, but usually I am attending because I am a speaker. All Day Hey is the only conference I can remember deliberately attending as a non-speaker back in 2023, and now 2025. It can be difficult to justify attending a conference purely for the sake of attending, on both personal and professional grounds. We all know there isn&#39;t enough time in the day to do what we need to do: exercise, eat well, get enough sleep, do the food shopping, spend quality time with your loved ones; and work deadlines are always looming. But taking time away from your desk and your Internet Machine can make a profound impact on your professional and personal spirits.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7qufGPAf6AYrM9N1AOanFg/aa862c1be5d22320eb29ad7ab673facc/IMG_2755.JPG" alt="A group of people are photographed from an elevated angle against a blurred backdrop of a bar. The group are holding coffees, back packs, and laughing. The sun beats down from the glass in the ceiling." height="1068" width="1600" /><p class="post__p">In this doom-riddled capitalist world, we are too preoccupied with metrics. This is through no fault of our own. If we are constantly tasked with making the needle go up in our professional endeavours, it seems only natural that we would apply this default thinking to other areas of life. In considering whether to attend a conference, you might ask yourself:</p><ul><li><p class="post__p">What if I don&#39;t make any new professional connections?</p></li><li><p class="post__p">What if I don&#39;t learn anything new from the talks to take back to work?</p></li><li><p class="post__p">What if my boss thinks it&#39;s a waste of time?</p></li></ul><p class="post__p">Truthfully, you might not make any new professional connections, although you may meet people with whom you have existing relationships, and you might not learn anything <b class="post__p--italic">new</b> from any of the talks. But attending a conference is certainly not a waste of time, especially considering the wider impact it may have on you as a full and complete human being. </p><img src="https://images.ctfassets.net/56dzm01z6lln/1LmnYXQ3LXFZlHbwyIcPke/dc905bdaba2dece2af9a4cf0fc9bd306/adh25-46.JPEG" alt="A row of conference attendees are photographed whilst watching a speaker on the stage. Some are looking at the camera, some at the stage intently, and others are talking about what they are seeing in hushed voices." height="1336" width="2000" /><p class="post__p">We should be treating attending a conference as an <b class="post__p--italic">experience</b> first and foremost. Experiences shape us; they shape our attitudes, our opinions, and our approaches to <b class="post__p--bold">all things</b>, whether personal or professional. The experience of being in a room of like-minded people for a day can be energising. Hallway conversations centred on shared interests can be refreshing. The raw buzz of being in the same place as lots of people you know from On The Internet™️ can be very exciting. You just cannot replicate this from behind your Internet Machine in your office, especially if it&#39;s at your home. Meeting people In Real Life is an experience that should be cherished for the experience itself, regardless of the circumstances or outcomes.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1cOVaIaQQN8qdNvW8kBO3f/fa00d601ef4887ba3505694ab6f2bee5/adh25-220.JPEG" alt="The conference attendees are photographed from the stage with the house lights on. They are sitting on red velvet, very comfortable chairs, as this is a cinema. Everyone looks energised yet calm after a very good conference day." height="1336" width="2000" /><p class="post__p">I must admit that for a few years I was reluctant to attend conferences purely for the sake of the experience, and I was guilty of using all of the excuses laid out in the bullet points above. I would whisper naïvely to myself: &quot;Surely, I can do all this networking and learning from the comfort of my own home. I don&#39;t need to waste time travelling to a venue.&quot; But something about this experience I shared on a beautifully sunny day in Leeds with my Internet Friends has changed something within me. </p><blockquote class="post__blockquote"><p class="post__p"><b class="post__p--italic">Surround yourself with people who add value to your life. Who challenges you to be greater than you were yesterday? Who sprinkles magic into your existence, just like you do to theirs? Life isn’t meant to be done alone. Find your tribe, and journey freely and loyally together. </b><b class="post__p--bold">– Alexandra Elle, author</b></p></blockquote><p class="post__p">I have always been a bit of a self-categorised &quot;outsider&quot;. I feel that I don&#39;t often <b class="post__p--italic">fit in</b> at social events in the real world that aren&#39;t centred on technology or music (or anywhere else, really). But what I realise now is that I spent so long hiding myself away from the Real World in my home office that I forgot that there is a community of people in the world where I <b class="post__p--italic">do</b> fit in; one that has welcomed me with open arms, laughs at my jokes, appreciates and shares my work, and wants me to succeed. And all of this has occurred in the Real World, not behind a screen.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2ttb5oXo2SKO49IYoF8FWg/d08f8d891199565fc49c5f25d3df8866/IMG_2729.JPG" alt="Three aging emos post grandiose yet humbly against the urban backdrop of Leeds, looking either wistfully off into the distance or straight into the camera, they embody everything that can be right with the world. Hope, confidence, quietness, serenity, camaraderie. The day was good, and so are these people. " height="1600" width="1067" /><p class="post__p">I&#39;ve always felt I belonged to a tribe on The Internet, which has been facilitated somewhat by the strange phenomenon of the Social Media Networks™️ that I have used to share knowledge and education as part of my job. But sharing a day in the Real World with the tribe, speaking and hearing real words with each other, and sharing food together, has promoted this blessed group to a collection of real friends and <b class="post__p--italic">professional allies</b>. </p><p class="post__p">Did I make any new professional connections at All Day Hey? Yes. Meeting smart people who I <b class="post__p--italic">knew of</b> but <b class="post__p--italic">didn&#39;t know, really,</b> has strengthened my professional network and capacity to reach more people in my work as a developer educator. Did I learn anything new from the talks at All Day Hey? Not quite, but I was inspired to think more creatively about alt text by <a href="https://heypresents.com/talks/what-do-you-see" target="_blank">Lola Odelola</a>, and the tribe bonded deeply over her ideas. Did my boss think it was a waste of time? I haven&#39;t asked him. </p><p class="post__p">And did I enjoy the experience? Yes, absolutely. The experience was enough: and more.</p><p class="post__p"><a href="https://heypresents.com/conferences/2025/photos" target="_blank">View all official 2025 photos by All Day Hey</a> | last photo by <a href="https://bsky.app/profile/robbowen.digital" target="_blank">Robb Owen</a></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>It wasn’t the idea that failed: it was the execution</title>
          <description>And we still can't get it right in 2025.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.nordcraft.com/it-wasnt-the-idea-that-failed-it-was-the-execution</link>
          <guid>https://blog.nordcraft.com/it-wasnt-the-idea-that-failed-it-was-the-execution</guid>
          <pubDate>Thu, 08 May 2025 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Since writing my new talk, <b class="post__p--italic">An Introduction to the World Wide Web for Very Senior Programmers</b>, in which I transport the audience back to the year 1995, I’ve become somewhat of a proud and nerdy Internet historian. So much happened on the web in 1995. HTML 2.0 was released in September and saw the introduction of the HTML <code>&lt;img&gt;</code> tag, transitioning the online experience from something that resembled reading books and formal documents, to flicking through colorful magazines or photo albums. JavaScript was released in December 1995, which moved the web from a read-only medium to an interactive experience. Multimedia tools like <a href="https://macromedia.fandom.com/wiki/Macromedia_Shockwave_Player" target="_blank">Macromedia Shockwave Player</a> and <a href="https://www.webdesignmuseum.org/software/futuresplash-animator-in-1996" target="_blank">FutureWave FutureSplash Animator</a> were created in 1995 out of a desire to bring exciting and interactive CD-ROM-like experiences to the browser.</p><p class="post__p">While the end-user experience was evolving, new tools to support web developers who were building websites were also being invented. The World Wide Web Consortium (W3C) was formed in 1995. And, whilst the first version of CSS was not released until 1996, the <a href="https://www.w3.org/People/howcome/p/cascade.html" target="_blank">1994 proposal for Cascading HTML Style Sheets</a> was being developed by the W3C. This would give web developers <b class="post__p--bold">full control</b> over the design of their websites, instead of allowing end-users to enhance the visuals of their internet-surfing experience using proprietary browser configurations. And, whilst the web was becoming more visual, so were the tools used to <b class="post__p--italic">build</b> websites.</p><h2 class="post__h2">It all started with Visual Basic</h2><p class="post__p">In 1991, Visual Basic was launched for Windows software application development. Visual Basic was a graphical user interface (GUI) that provided a visual abstraction layer on top of C/C++ and the Win32 API. Ryan Lucas describes this historical release in detail in <a href="https://retool.com/visual-basic" target="_blank">The history and legacy of Visual Basic</a>, and pays particular attention to how it saved the careers of “millions of mainframe COBOL programmers who were looking with terror at the microcomputer invasion,” as recalled by the creator of Visual Basic, Alan Cooper.</p><blockquote class="post__blockquote"><p class="post__p">To design their UI, developers could drag and drop out components onto a WYSIWYG canvas. To add behavior to a UI element, they could simply select it and choose a click event handler from a dropdown. Mainframe programmers were suddenly empowered to quickly get up to speed writing Windows apps.</p></blockquote><img src="https://images.ctfassets.net/56dzm01z6lln/7EqKJruJjRPwBragMuQX43/ab1e7081f5baebec692290ed80a048c5/visual_basic.png" alt="The Visual Basic user interface in 1995." height="600" width="800" /><p class="post__p">Whilst application developers were embracing a more visual way of working in the early 90s, developers on the web were still confined to the command line, Notepad, and MS-DOS Editor on Windows. That is, until FrontPage 1.0 was released in November 1995.</p><p class="post__p">FrontPage was developed by Vermeer Technologies, and was categorised as a “World Wide Web publishing and site management tool”. Like Visual Basic, FrontPage was a WYSIWYG (what you see is what you get) visual interface, and this tool built HTML. It also provided a personal web server, which allowed you to preview your website on your local machine as you built it. It also included templates, automated scripts, and more. FrontPage was acquired by Microsoft just two months later in January 1996, and was rebranded to Microsoft FrontPage in June 1996 to coincide with the release of version 1.1.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5dM1UJjgfWYEc2KOm5nSXS/9618d8fd5be1ca3e544c1a677b8847db/frontpage-1-0.png" alt="Microsoft FrontPage 1.0, showing a home page made for the web as text and a list of links, all within the FrontPage GUI." height="525" width="600" /><p class="post__p">Around the same time, a company called iBand worked on a similar visual tool to FrontPage called Backstage Designer. And, as is seemingly standard practice in this industry, iBand was acquired by Macromedia in 1996, and soon rebranded the Backstage Designer product as <a href="https://macromedia.fandom.com/wiki/Macromedia_Backstage_Desktop_Studio" target="_blank">Macromedia Backstage Desktop Studio</a>, which in 1998 became a web development GUI you may be more familiar with: <a href="https://macromedia.fandom.com/wiki/Macromedia_Dreamweaver" target="_blank">DreamWeaver</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4tjOIE9bgEjMPPChs1q0SZ/dccd05ca66e012ff143bd9fb4c36534d/iband-backstage.webp" alt="Screenshot of iBand backstage designer in 1996. It resembles Microsoft FrontPage." height="300" width="400" /><p class="post__p">Development tools for building games were also evolving in the same way at the same time. <a href="https://en.wikipedia.org/wiki/Unreal_Engine_1" target="_blank">Unreal Engine 1</a>, a GUI built in Visual Basic, was also released in 1995.</p><img src="https://images.ctfassets.net/56dzm01z6lln/11E4YcEpVV28uTMnnITQ6Y/ed48cc816f83547c5e59ff007b09a8aa/unreal_engine.jpg" alt="A screenshot released by Epic of the first version of UnrealEd, displaying a graphical user interface written in Visual Basic," height="273" width="364" /><p class="post__p">And what’s very interesting to note at this point, is that while game development has largely adopted visual development tools across the industry, web development seemingly abandoned the visual tools of the mid-late 90s. Thirty years later, most of us are still building websites and web applications using text editors. Why is that?</p><h2 class="post__h2">Why didn’t we adopt visual website tools in the 90s?</h2><p class="post__p">The bottom line is that visual website tools in the 90s (and beyond) produced terrible HTML markup, and therefore, pretty terrible websites. I remember fondly using the Apple website builder <a href="https://www.apple.com/welcomescreen/ilife/iweb-3/" target="_blank">iWeb</a> in the early 2000s. In fact, it wasn’t very long ago that a website I built for a client in 2008 using iWeb was still up and running. The text-based CMS I built for them, which displayed a text file in an iframe that they uploaded via FTP, really stood the test of time. But now I have professional knowledge and experience of building things for the web, I know that the HTML markup that iWeb produced was <b class="post__p--italic">absolute filthy garbage</b>. And if the HTML markup is garbage, then your site is not accessible to those who use assistive technology, such as screen readers.</p><p class="post__p">In 1986, <a href="https://www.afb.org/aw/5/2/14760#:~:text=The%20PC%20was%20in%20its,DOS%2C%20called%20IBM%20Screen%20Reader" target="_blank">IBM announced the first screen reader</a>, but it wasn’t until the mid 90s that screen readers started to become a little more accessible themselves, when <a href="https://abilitynet.org.uk/factsheets/introduction-screen-readers#simple-table-of-contents-6" target="_blank">JAWS was released for Windows 1.0</a> in 1995. The first web accessibility guidelines, later branded as the <a href="https://en.wikipedia.org/wiki/Web_Content_Accessibility_Guidelines" target="_blank"><b class="post__p--bold">Web Content Accessibility Guidelines</b></a> or WCAG, were published in January 1995. Yet, despite this, tools that were created to build websites were not ever seemingly built with these official standards in mind. This goes for tools built in 1995, and astonishingly, 2025.</p><p class="post__p">This week on May 7th 2025, Figma announced <a href="https://www.figma.com/sites/" target="_blank">Figma Sites</a>, a tool to publish your designs built in Figma directly to the web. But this new product has not been well received. Adrian Roselli warns us: <a href="https://adrianroselli.com/2025/05/do-not-publish-your-designs-on-the-web-with-figma-sites.html" target="_blank">Do not publish your designs on the web with Figma Sites</a>.</p><blockquote class="post__blockquote"><p class="post__p">“…Unless you want to fail all the WCAGs, create litigation risk, close off opportunities in Europe, engage in reputational harm, and oh yeah, <b class="post__p--italic">throw up barriers to your customers and users</b>.”</p></blockquote><p class="post__p">It’s no surprise we weren’t getting it right in 1995, if we <b class="post__p--italic">still</b> can’t get it right 30 years later with all of this knowledge, experience, and empathy under our belts. And I’m not even going to mention at this point how AI can’t get this right, either. Of course it can’t; it doesn’t possess the capacity for empathy.</p><h2 class="post__h2">Developers want control</h2><p class="post__p">Developers want complete control over their HTML, CSS and JavaScript: and rightly so. If visual web development tools can’t generate clean and semantic HTML, organized and debuggable CSS, or performant JavaScript, it really is a no-brainer to continue to build websites and web applications using text-based web frameworks, or rolling your own.</p><p class="post__p">It is no secret that I am writing all of these things on the <a href="https://nordcraft.com" target="_blank">Nordcraft</a> blog, and that Nordcraft is an open-source Web Development Engine which combines a web framework with a suite of advanced visual tools that let designers and developers collaborate to build high performance web applications, and that I work at Nordcraft. And you might think that Nordcraft is just another visual website builder that produces the same old garbage that all the other visual tools do. But speaking as someone who has been building websites and writing code for over 20 years, and as someone who likes complete control over said websites and code, I can say for a fact that Nordcraft is very, very, different.</p><h2 class="post__h2">Nordcraft actually gives you control</h2><p class="post__p">In Nordcraft, your HTML markup is yours to control. Using the Nordcraft editor, you build your HTML structure in the same way you build HTML with text, using semantic element tags of your choosing, with full access to adding attributes and other web platform-based functionality. Nordcraft does not prescribe how to build your HTML markup: you do. The only difference (and advantage) is that you are building your HTML on a WYSIWYG canvas, where designers can collaborate with you and add design flair <b class="post__p--italic">as part of the process</b>, not as some preliminary task that sees designs in Figma thrown over the fence in a developer’s general direction (which is, all too often, the case). CSS and JavaScript work in the same way: you have complete control over what you ship to the browser, without any unexpected nonsense.</p><p class="post__p">It’s 2025, and we can do better than what has come before us, and what is, unfortunately, currently unfolding in this visual web development landscape. By giving you, as developers and designers, full control over the end result you envision, I am confident that Nordcraft won’t suffer the same fate of the early visual tools of the 90s, or Figma Sites in 2025.</p><p class="post__p">Your fate is in your own hands, as well as the fate of your websites.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Thrive</title>
          <description>p.s. coffee is not breakfast</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/thrive/</link>
          <guid>https://whitep4nth3r.com/blog/thrive/</guid>
          <pubDate>Mon, 28 Apr 2025 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In December 2024, I was convinced something was gravely wrong with my body, and I was completely sure I was peri-menopausal at the age of 39. I spoke to doctors; I had my blood tested for absolutely everything. Yet, nothing was obviously wrong with me medically. And then in January, I developed issues with my severe pain in my hands which made it difficult to work, and so <a href="https://whitep4nth3r.com/blog/how-i-learned-to-code-with-my-voice/" target="_blank">I learned how to code with my voice</a> and wrote about it.

In that article, I wrote:</p><blockquote class="post__blockquote"><p class="post__p">My physical pain is real, but given the personal events I have endured over the last two years, I also believe that this manifestation of pain has been exacerbated by a combination of a series of emotionally traumatic events.</p></blockquote><p class="post__p">My hands recovered towards the end of February after leaving a very stressful work environment, but I still felt like something was deeply wrong with the rest of my body. I have been engaging in talking therapy on and off for 20 years and at this point I decided to go back. It is helping <b class="post__p--italic">mentally</b>, but I still felt like there was still something wrong <b class="post__p--italic">physically</b>.</p><p class="post__p">In March 2025 I decided to see a nutritionist. And this is where it all became clear. The root cause of all of this (the pain, my messed up menstrual cycle, the constant tiredness, the constant fear of something being wrong with me) is that amidst the whirlwind of events that took place over the last two years, I neglected to take care of myself <b class="post__p--italic">physically</b>. And I don&#39;t mean through exercise. I wasn&#39;t eating enough, and I wasn&#39;t eating <b class="post__p--italic">right</b>. </p><p class="post__p">My nutritionist recommended I read <a href="https://drchatterjee.com/books/the-stress-solution/" target="_blank">The Stress Solution</a> by Dr Rangan Chatterjee and the concept of how we (unintentionally) put our bodies into a stress state really resonated with me. Our bodies are designed as such that when we are in a stress state, we are primed to survive dangerous or difficult situations, such as running from a predator or completing a difficult exam. </p><p class="post__p">In times of stress, our bodies change physically in order for us to become a peak version of ourselves to maximise our chance at survival. Our pupils become dilated, sugars are released into our blood so that we can endure prolonged physical exertion. Our organs become resistant to insulin so that more sugar can remain in our bloodstream, rather than being available for our organs to store, so our brain can stay more alert. Our heart beats faster and our blood pressure rises to make sure the sugary blood can move around our body effectively. Also, our brains and immune systems become hyper-vigilant to threat, sending stress signals throughout the body. At the time same our libido plummets and our digestion system switches off, in order to maximise the energy reserves needed to survive running away from those dangerous predators.</p><p class="post__p">I wasn&#39;t eating enough, and I wasn&#39;t eating right. I &quot;didn&#39;t have the time to think about eating&quot; given everything else going on, and I was inadvertently putting my body into a stress state 24 hours a day, seven days a week. </p><p class="post__p">For the last four weeks, I have been eating bountifully. I have been eating three incredibly varied and nutritious meals every day to send the right signals to my body that I am safe, and this has moved my body into a thrive state. Apparently I&#39;m also dairy intolerant, so I don&#39;t eat that anymore. But the results of these efforts are already so obvious. I am less tired, I can move easier, my brain is less foggy, my bloody is less fizzy (seriously, I felt like my blood was fizzy), my periods aren&#39;t messed up anymore, and I finally have the energy for proper exercise (I have taken up yoga). And more importantly, I don&#39;t <b class="post__p--italic">feel</b> like anything is wrong with me anymore.</p><p class="post__p">The world is difficult in 2025; stressors are everywhere, especially when you spend time on the Internet. But by being intentional in how we nourish our bodies, we can send the right signals to our body that we are thriving, so that we can not only <b class="post__p--italic">survive</b>, but <b class="post__p--italic">live</b>.</p><p class="post__p">p.s. coffee is not breakfast</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Does AI really make you more productive?</title>
          <description>Oh no! My metrics!</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.nordcraft.com/does-ai-really-make-you-more-productive</link>
          <guid>https://blog.nordcraft.com/does-ai-really-make-you-more-productive</guid>
          <pubDate>Fri, 28 Mar 2025 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In October 2024, the DORA research programme published the <a href="https://cloud.google.com/devops/state-of-devops" target="_blank">2024 Accelerate State of DevOps Report</a>, which for the first time, included a section on how AI adoption is affecting software development at an individual, team and product level. With the recent emergence of the new “<a href="https://en.wikipedia.org/wiki/Vibe_coding" target="_blank">vibe coding</a>” meta, a term introduced by <a href="https://karpathy.ai/" target="_blank">Andrej Karpathy</a> in February 2025, where AI is is enabling people to ship apps from idea to production in record time, I wanted to take the time to reflect on the report and discuss whether AI is <b class="post__p--italic">really</b> making us more productive as software developers.</p><h2 class="post__h2">Most developers are relying on AI</h2><p class="post__p">The report found that almost 76% of participants are “relying on” some form of AI tooling in their daily responsibilities as a software developer. This can include writing, optimising, documenting and debugging code, explaining unfamiliar code, writing tests, data analysis, and summarising information. “[D]evelopers who trust gen AI use it more”, but almost 40% of participants “reported having little or no trust in AI”.</p><p class="post__p"><b class="post__p--bold">I do not trust AI.</b> I have over 11 years of professional industry experience, and have been making websites in some form or another for almost 30 years. With all this considered, AI code generation has only ever made me feel <b class="post__p--italic">less</b> productive. I prefer to understand every single line of code I ship in my applications: it makes everything easier to debug, fix, and extend. I have found that AI-generated code is often sloppy, unnecessarily complex, and a lot of the time, just plain wrong. For me, AI code generation is akin to mindlessly copy-pasting code snippets from Stack Overflow, and we all know how that goes. It usually takes me longer to understand AI generated code than write my own.</p><h2 class="post__h2">You may be sacrificing software delivery metrics by relying on AI</h2><p class="post__p"><a href="https://dora.dev/guides/dora-metrics-four-keys/" target="_blank">DORA’s software delivery metrics</a> provide an effective way of measuring outcomes of software delivery processes. They are split into two categories: throughput and stability. The report found that “AI adoption is negatively impacting software delivery performance”, and the “negative impact on delivery stability is larger”.</p><h3 class="post__h3">Sacrificing throughput with AI</h3><p class="post__p">Throughput measures the velocity of changes in software that are being made, that is, how quickly and how frequently software teams can ship changes to production. Throughput is all about how efficient and responsive a team can be.</p><p class="post__p">The report hypothesises that “the fundamental paradigm shift that AI has produced in terms of respondent productivity and code generation speed may have caused the field to forget one of DORA’s most basic principles — the importance of small batch sizes.” Since AI code generation will often spit out huge batches of code in one fell swoop, pull requests are getting larger. Larger pull requests and changes are much more difficult and time-consuming to review thoroughly for edge-cases and potential issues.</p><p class="post__p">Speaking from experience, code reviewers are more likely to skim over large changes and miss important details; walls of impenetrable code are much more difficult to process and interpret by a real human brain than smaller changes, amidst an already busy work-day. Whilst combing through that <code>+11456 -7892</code> code review, you’re probably thinking about all those lines of code <b class="post__p--italic">you need to write yourself</b> in order to stay “productive”.</p><p class="post__p">Of course, there are tools that provide AI-assisted code reviews. But if we, as developers, do not trust AI to produce maintainable and reviewable code, why should we trust AI to review it? If you find yourself constantly reviewing large pull requests with a lot of AI generated code, you’re probably sacrificing throughput. You might not be shipping as fast.</p><h3 class="post__h3">Sacrificing stability with AI</h3><p class="post__p">Stability measures the quality of software changes delivered, and the team’s ability to fix bugs. Stability is measured using a combination of change fail percentage, which is the percentage of deployments to production that require hot-fixes or rollbacks, and failed deployment recovery time. A lower change fail percentage means the team has a reliable delivery process, and a lower recovery time indicates more resilient and responsive systems.</p><p class="post__p">The report suggests that it’s “possible that we’re gaining speed through an over-reliance on AI for assisting in the process or trusting code generated by AI a bit too much.” I would argue that <b class="post__p--italic">speed</b> here is the <b class="post__p--italic">illusion of speed</b>, and the concept of “over-reliance” is key here, especially in the context of the new vibe coding meta. Vibe coding is about being dependent on AI code generation. Describe the app or code you want, and let an LLM take care of it for you. Ask it for a few edits. Ship it.</p><p class="post__p">The report states that a developer’s productivity “is likely to increase by approximately 2.1% when an individual’s AI adoption is increased by 25%”. Vibe coding and any sort of AI adoption is attractive because it <b class="post__p--italic">feels</b> fast; it <b class="post__p--italic">feels</b> more productive. Now, I’m not proposing that professional software developers are going all-in on vibe coding, but increased adoption of AI and reported productivity increases poses a risk to software stability. We think we’re going faster, but we may be shipping poorer quality software and more broken changes, because we’re putting our trust in AI generated code too much.</p><p class="post__p">Data from the industry is proving that vibe coders are shipping unstable applications, reporting <a href="https://www.techrepublic.com/article/ai-generated-code-outages/" target="_blank">outages and critical security vulnerabilities in their apps</a>, <a href="https://www.reddit.com/r/cursor/comments/1inoryp/cursor_fck_up_my_4_months_of_works/" target="_blank">losing months of work that didn’t use version control</a>, and the anecdotal <a href="https://discord.com/channels/972416966683926538/972416966683926541/1349174629268197387" target="_blank">struggle to finish vibe-coded apps</a>. If professional software development teams do not stay vigilant with their use of AI, they run the risk of sacrificing stability. </p><p class="post__p">Plus, if you’re moving fast, but always having to pick up the pieces caused by over-zealous AI code generation, are you really being more productive?</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Is this the next step in the evolution of front end dev?</title>
          <description>Tired of endless admin in web dev?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/the-next-step-in-the-evolution-of-front-end-dev/</link>
          <guid>https://whitep4nth3r.com/blog/the-next-step-in-the-evolution-of-front-end-dev/</guid>
          <pubDate>Mon, 03 Mar 2025 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In 2025, setting up new web development projects is still really, really annoying. And honestly, it’s one of the reasons I either prototype a new project in a single HTML file, or don’t manage to find the energy to prototype a new fun idea at all. Starting a new front end project involves a never-ending list of administrative tasks and decisions to be made, when all you want to do is get building. Your list of setup tasks might look something like this:</p><ul><li><p class="post__p">Set up a new development environment (install e.g. correct version of node, npm, yarn, pnpm, bun)</p></li><li><p class="post__p">Set up a new project (create directory, choose framework, choose starter project or boilerplate, install dependencies, ensure they are compatible with each other)</p></li><li><p class="post__p">Define a file structure and component library approach</p></li><li><p class="post__p">Define a base style guide and CSS resets</p></li><li><p class="post__p">Write boilerplate code to connect to APIs and databases</p></li><li><p class="post__p">Configure a build and deploy pipeline</p></li><li><p class="post__p">Configure Webpack???</p></li></ul><p class="post__p">And, if you’re working with a designer, you’ve also got the back and forth handover and sign-off processes to go through, which in a busy team or multi-project agency, can really hold things up. <b class="post__p--bold">You just want to get building!</b></p><p class="post__p">There are some alternatives to the time-consuming, decision-heavy setup process. You could use virtual machines or environments that enable faster time-to-setup through scripts and automated processes. But still, even this requires some effort and “onboarding&quot;. I remember at one job, I went through a “tried and tested” vagrant virtual machine setup process that didn’t actually work correctly with my version of MacOS at the time. Now, granted this wasn’t a brand new project, but it took me a full working week to configure my development environment so that I could be productive. So what are the alternative approaches? Recently, I’ve become curious about <b class="post__p--italic">visual development frameworks</b>.</p><h2 class="post__h2">What is a visual development framework?</h2><p class="post__p">When you use a visual development framework, you&#39;re working in an environment where you don&#39;t need to think about — or do — any of the relentless admin and project configuration tasks described above. Everything is taken care of from the beginning, and you&#39;re ready to start making components, fetching data from APIs, adding custom code, and watching your ideas come to life straight away. Visual development also removes the need to translate designs from design software to code: the design of the user interface is baked in to how you build it. No more handovers between teams! </p><p class="post__p">In summary, <b class="post__p--bold">visual development solves two key problems in front end development</b>: it abstracts away the repetitive administrative setup work, and removes the inconvenience of separate design and dev environments.</p><p class="post__p">Here&#39;s how working with HTML, CSS and data from an API looks inside visual development framework, Nordcraft. You’ve got a document tree of HTML elements on the left, CSS styling and API connections on the right, and the visual result in the middle. In traditional text-based development, this weather widget might be powered by a collection of files dotted around a codebase. In visual development, it’s all right there.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3H0mMRPpxlRr4V9zyPMto/14820bead90035efca1698487959105d/toddle_weather.png" alt="A weather widget in the centre of the screen showing the current temperature, location and weather icon. But what's most interesting is that on the left side there is a tree of HTML elements that make up this widget that kind of looks like an accessibility tree, and on the right, there is an API panel showing that a GET request is being made to a weather API url, and the data is formatted in JSON, which is the data we are using in the HTML generated. We don't see any code here, just visual building blocks and data." height="3078" width="5344" /><h2 class="post__h2">Is this the next big step for front end development?</h2><p class="post__p">A common sentiment among the terminally-online community is that if you’re not writing actual lines of code, then you’re not a <b class="post__p--italic">real developer</b>; often the industry treats the process of development as the outcome, rather than the actual website or app produced. But when you think about how every evolutionary step of computer science has centred on abstracting away things that no longer make sense for a human to do, then visual development frameworks are the logical next step in the evolution of front end development.</p><p class="post__p">For example, hardly anyone is coding in <a href="https://en.wikipedia.org/wiki/Assembly_language" target="_blank">Assembly</a> in 2025, and we don’t shun people for not being <b class="post__p--italic">real programmers</b> just because they don’t write instructions to add memory data to a register in a x86-family processor in original Intel syntax, or know the equivalent in AT&amp;T syntax used by the GNU Assembler. Most of us don’t need to worry about this anymore.</p><p class="post__p">Whilst everything is a set of binary instructions at its core, we also don’t need to worry about writing code in binary, thanks to <a href="https://en.wikipedia.org/wiki/Grace_Hopper" target="_blank">Grace Hopper</a> who released the A-O compiler in 1949, which used symbolic mathematical code to represent binary code. And we don’t even need to worry about writing code in mathematical symbols, because soon after A-O, Hopper released B-O, or “Flow-Matic”, which is considered the first English language data-processing compiler. Each advancement removes another layer of machine-to-human obscurity.</p><p class="post__p">What’s important to note, however, is that the evolution of programming languages was based on computation itself, not human-computer interaction (HCI). In the mid-latter part of the 20th century, there weren’t that many people using computers, and so HCI wasn’t a priority to be optimised for. But in 2025, we live our whole lives through HCI (for better or worse), and what has constantly been overlooked and under-prioritised is the <b class="post__p--italic">actual experience</b> of this human-computer interaction, both from the point of view as an end-user experiencing something on a device, and as someone who is crafting that end-user experience on a device.</p><p class="post__p">I speak from experience that as a web developer, it can be challenging to create really great experiences and human-computer interactions when you’re constantly moving between different layers of abstraction and relative obscurity (e.g. design → code as text → user interfaces). Visual development frameworks are a tool to help us get the same outcomes as writing traditional text-based code, without the slow up-front time-costs.</p><h2 class="post__h2">My next step</h2><div class="post__callout">
  <h3 class="post__callout__title">Big news!</h3>
    <div class="post__callout__content">
      <p>I'm excited to announce that I have joined <a href="https://nordcraft.com">Nordcraft</a> as Head of Developer Education.</p>

    </div>
  </div><p class="post__p"><a href="https://nordcraft.com" target="_blank">Nordcraft</a> is a Web Development Engine. Using Nordcraft, you can create fully-interactive front end websites and apps with any APIs, all via an in-browser visual interface. Nordcraft puts human-computer interaction at the front and centre of what you’re building, for both you as a web developer and your end users. And I&#39;d like to go so far as to say that programming in a <b class="post__p--italic">different</b> way might actually help you understand some programming concepts in a <b class="post__p--italic">deeper</b> way. Plus, you’ll never have to configure Webpack again. </p><p class="post__p">The team at Nordcraft is also working hard to make Nordcraft fully open source in 2025 and you can <a href="https://github.com/nordcraftengine/nordcraft" target="_blank">check out the progress towards this on GitHub</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2zLK5SinDSXZaPyFscnoXO/ce167cb8efb7d24021a9f9c4d414640d/nordcraft.png" alt="The Nordcraft home page. The main headline is: Craft AAA web apps that delight your users, with a green CTA button that says start building. There is a video to the left of this main section that shows the Nordcraft editor in action." height="2474" width="3824" /><p class="post__p">It’s early days, but I’m incredibly excited to help shape the future of Nordcraft, and to help you build the best human-computer interactions on the web you possibly can. If you want to keep up with what we’re working on at Nordcraft, subscribe to the <a href="https://www.youtube.com/@nordcraftengine" target="_blank">Nordcraft Engine channel on YouTube</a> and <a href="https://discord.com/invite/svBKYZf3UR" target="_blank">join the Discord</a> (it’s very active!), and I can&#39;t wait to build more silly websites using Nordcraft, for your entertainment.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>You are more than the tools you use: tell your story</title>
          <description>And your people will find you.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/you-are-more-than-the-tools-you-use/</link>
          <guid>https://whitep4nth3r.com/blog/you-are-more-than-the-tools-you-use/</guid>
          <pubDate>Wed, 12 Feb 2025 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">How many times have you seen someone introduce themselves via the tools they use? Maybe this looks familiar:</p><blockquote class="post__blockquote"><p class="post__p">Hi, my name is Salma! I’m currently building an app with Express, Nuxt, and the Twitch API. Let’s connect! 🚀👩🏻‍💻🌐</p></blockquote><p class="post__p">I understand the need to find shared interests with the online community to make genuine connections, I really do, but statements like this don’t tell people anything about what <b class="post__p--italic">you actually do</b>. Last year I did exactly what I described above. But what does it even mean? Take the following non-tech examples:</p><blockquote class="post__blockquote"><p class="post__p">Hi, my name is Salma! I’m currently making music with a piano, guitar, and synthesisers. Let’s connect! 🎹🎸🎵</p></blockquote><blockquote class="post__blockquote"><p class="post__p">Hi, my name is Salma! I’m currently making food with pasta, onions, and seasonings. Let’s connect! 🍝🧅🌶️</p></blockquote><blockquote class="post__blockquote"><p class="post__p">Hi, my name is Salma! I’m currently making clothes with fabric, thread, and needles. Let’s connect! 👗🧵🪡</p></blockquote><p class="post__p">You get the idea. The above statements tell people about what you’re using, but not about what you are <b class="post__p--italic">doing</b>. Let’s reframe the above non-tech examples.</p><blockquote class="post__blockquote"><p class="post__p">Hi, my name is Salma! I’m currently working on an experimental EP that pushes the boundaries of the human perception of sound through the blending of acoustic instruments and digital noise to produce ethereal and meditative soundscapes. 🎹🎸🎵</p></blockquote><blockquote class="post__blockquote"><p class="post__p">Hi, my name is Salma! I really love making unique pasta sauces that combine culinary influences from around the world. This week I’m trying to perfect a Japanese-inspired pasta sauce that takes inspiration from ramen and blends it with the traditional Italian concept of bolognese. 🍝🧅🍜</p></blockquote><blockquote class="post__blockquote"><p class="post__p">Hi, my name is Salma! I’m currently making all my clothes from old furniture fabric and thrifted offcuts to prove that fashion doesn’t have to be fast to be exciting and innovative. I recently made a dress from my grandma’s old bedspread and I can’t wait to wear it to a party this week! 👗🧵🪡</p></blockquote><p class="post__p">The above examples may be a little contrived, to say the least, but they tell a story; they reveal a little more about your personality; and they communicate more about who you are. Now, what about that technical example?</p><blockquote class="post__blockquote"><p class="post__p">Hi, my name is Salma, and I write code for your entertainment. Last year I built a text-based game for my Twitch stream that blurred the lines between me being online and offline, allowing viewers to interact with my stream 24/7. I love to find ways to help people truly connect through unexpected and fun applications of technology. 💖🔥📺</p></blockquote><p class="post__p">You are more than the tools you use: tell your story. And your people will find you.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>3 reasons you should tour a conference talk</title>
          <description>After putting in the work, you owe it to yourself.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/3-reasons-you-should-tour-a-conference-talk/</link>
          <guid>https://whitep4nth3r.com/blog/3-reasons-you-should-tour-a-conference-talk/</guid>
          <pubDate>Tue, 11 Feb 2025 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In 2023-2024, I gave my conference talk <b class="post__p--italic">Entertainment as Code</b> six times. It should have been eight times, but you know, life happens. Writing a full-length conference is no trivial matter; it can take weeks of full-time work to craft a captivating story around your chosen topic, and even longer to build technical and/or representative real-world examples to support your narrative. All of this hard work should not go to waste. I want to share my experience of touring a conference talk, and why <b class="post__p--bold">you</b> should be touring a conference talk, too.</p><h2 class="post__h2">Each talk is the same, but different</h2><p class="post__p">The <a href="https://whitep4nth3r.com/talks/entertainment-as-code-premier/" target="_blank">first performance</a> of Entertainment as Code was very different to the <a href="https://whitep4nth3r.com/talks/entertainment-as-code-finale/" target="_blank">last</a>. This was mainly because this talk centred on a new and evolving project I was working on throughout 2024, but also because the very notion of this conference talk inspired the project itself. Entertainment as Code became kind of an ouroboros; the more I gave the talk and spoke to attendees about how they enjoyed it, the more inspiration I received to evolve the project and the talk into its final form.</p><p class="post__p">By giving a conference talk multiple times, you also learn what works and what doesn’t work about your talk. As a (former) comedian, I learned a lot of hard lessons about what works and what doesn’t work for particular audiences in particular venues. I will never forget the time I referred to a fictional seven year old as the c-word to rapturous applause from an audience of twenty-somethings in Camden (trust me, it was funny), whilst repeating the same to a room full of middle-aged people in Blackburn almost ended up with me getting booed off the stage.</p><p class="post__p">I like to weave in jokes to my conference talks, and telling these jokes multiple times gives me the opportunity to iterate and land them as well as possible. The same goes for the content of the talk. In my opinion, no talk should be given in the same way any two times. On the one hand, as the speaker you will get bored. And on the other hand, conference attendees may attend multiple events and end up seeing you give the talk more than once (this definitely happened to me) and so you owe it to them to give them just a little bit more fun, insight, and technical depth than before. They’ll remember you for that.</p><h2 class="post__h2">You get better at giving the talk</h2><p class="post__p">Speaking from experience, musicians, and comedians especially, never give the same performance twice. They also never give a performance just once. Each performance is a practice to make the next one better. As a performer, you have a duty to adapt your material to the room in which you’re in and the audience that is before you. A conference talk is also a performance, and it should be no different. And in intentionally adapting and iterating on your conference talk as you tour it, you learn more about public speaking, how to tell a really compelling story, and the content of the talk itself.</p><p class="post__p">My conference talks usually start with a strict script. As time goes on and I give the talk more times, I know the material more, my confidence grows, and as a result I’m able to improvise and go off-script, adapting the material to the audience and the room with little effort. It is incredibly satisfying for both the speaker and the audience to deliver the material in a way that resonates particularly with that room of people on a particular day.</p><h2 class="post__h2">Your talk reaches more people</h2><p class="post__p">According to a quick internet search for “average tech conference attendee numbers”, <a href="https://www.imagovenues.co.uk/go-perform/50-stats-every-uk-conference-organiser-should-know/" target="_blank">the average number of attendees at a UK conference is 100</a>. Giving your talk to just 100 people and calling it a day is not a good return on investment for potentially weeks of work. After putting in that work, you owe it to yourself to submit those CFPs, and get yourself an audience. The more people you reach, the more opportunities you may get in the future. And the cycle continues. You become the ouroboros. And the cycle is complete. And if you enjoyed the process, write a new talk, and do it all again.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>A case for getting dressed every day</title>
          <description>On rediscovering the importance of getting dressed daily after finding the perfect pair of jeans.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/get-dressed-every-day/</link>
          <guid>https://whitep4nth3r.com/blog/get-dressed-every-day/</guid>
          <pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Since the first Covid lockdown of 2020 until the end of 2024, I lived in leggings, baggy t shirts and hoodies. The “new normal” of working from home allowed me to joyously part with my collection of skinny jeans that had never, ever, been comfortable, and I convinced myself that I was now “dressing for comfort”. I proudly declared to myself that I had reached the age where it didn’t really matter what I looked like anymore; I didn’t have anyone to particularly impress. That being said, I also convinced myself that because I was investing in high-quality slow fashion t shirts and hoodies and leggings, I was dressing well.</p><p class="post__p">In December 2024, when remembering how I used to scold my mother for living in leggings and baggy t shirts from her mid-30s onward, and how I am consciously striving to be everything she is not and never was, I decided it was probably time for a change. But where to start? It was time to find a new pair of jeans.</p><p class="post__p">Jeans are a tricky business; I have never felt comfortable in jeans, especially when sat working at my desk (I prefer to sit cross-legged). I just figured I didn’t have the right body type for jeans. However, what I discovered is that the variety of jeans available on the market has expanded considerably since I departed with my skinny jeans in 2020, and so I embarked upon this journey with an open mind. But, I had no idea what style was right for me given my long slim legs are often at odds with my slightly chonkier waistline where standard sizing is concerned (thanks, <b class="post__p--italic">genes</b>) . Or so I thought.</p><p class="post__p">Shopping online for jeans would have resulted in an endless cycle of buying and returning, so I made a personal shopping appointment to be able to sit in a changing room and have someone bring me an endless supply of jeans to try on in desperate hope of perfect pair. Serendipitously, the first pair of mid-blue acid wash mid-rise relaxed 90s style straight jeans I picked from the pile were <b class="post__p--italic">absolutely perfect</b>. What’s interesting to note is that I always figured I needed high-rise jeans to hide my chonkiness, and that mid-rise jeans would cut me in two, given this was always the case with <b class="post__p--italic">skinny jeans</b>. I never would have event considered clicking a pair of mid-rise jeans when online shopping. Boy, was I wrong. These jeans made me feel amazing. So much so that I also picked up a second pair in a similar style in black from the same brand. And the icing on the cake was that these jeans fit me wonderfully in my regular size. I did not expect that.</p><p class="post__p">I don’t think I can really get away with wearing baggy t shirts and relaxed jeans like the kids, so I stocked up on a few slim-fitting t-shirts to pair with them, and my new daily uniform was born. <a href="https://bsky.app/profile/whitep4nth3r.com/post/3lc6oi3q3kk2q" target="_blank">I’ve talked about Indyx on Bluesky before</a>, which is an app I’ve been using to track items in my wardrobe, and at the beginning of 2025 I decided to start using Indyx to track my daily outfits to make sure I was making the most of the new items in my wardrobe. Instead of just throwing on the same old pairs of leggings and baggy t shirts, I really enjoyed getting dressed intentionally again. I felt put-together, and more ready for the day ahead. So much so, in fact, that whenever I wore leggings for a chiropractor appointment, for example, I felt <b class="post__p--italic">extremely underdressed</b> and sloppy.</p><p class="post__p">Last night I must have slept weirdly, because I woke up with neck pain and as a result, I really didn’t feel like “getting dressed” today. So I’m sitting here writing this wearing leggings and a baggy t shirt and a zip-up hoody and <b class="post__p--italic">I feel wrong</b>. I don’t feel ready for the day. I don’t feel ready to work. I feel like an old version of myself that has lost progress in some sort of journey of self-discovery. But I think what this really means is that I have actually made good progress.</p><p class="post__p">And all this is to say that it’s worth getting dressed every day, whatever that means for you.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I created a newsletter I actually enjoy writing</title>
          <description>Send what you like to receive, make it easy for yourself, and do it for the love.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-create-a-newsletter/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-create-a-newsletter/</guid>
          <pubDate>Thu, 06 Feb 2025 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Lots of developers send newsletters, and as I thrust myself into the online technology space in 2020, it was an item I added to my to do list straight away. Given the incessant algorithmic curation of content on the web, a newsletter is a crucial tool in being able to reach an audience consistently; someone giving you their email address is a powerful indicator of trust. But it took me three more years to actually start sending a newsletter because it felt like such a huge effort amidst everything else I had to do for work and for life.</p><p class="post__p">It turns out I was over-engineering the concept of sending a newsletter in my head. And it really can be as simple as you want to make it. Since January 2024, I’ve sent 56 weekly issues of my newsletter, and I’ve enjoyed writing and sending every single one. Here’s how I approached creating and sending my newsletter, <a href="https://buttondown.com/weirdwidewebhole" target="_blank">weird wide web hole</a>, to make each issue easy to write, and delightful to send.</p><h2 class="post__h2">Send what you would like to receive</h2><p class="post__p">I am a huge fan of weird things and obscure things, intriguing experiences, and things that don’t always appear to be what they seem. Since the early days of the internet, I have always been fascinated with strange websites, curious applications of technology, the vastness of the universe, and unexpected collaborative online experiences. I am always thrilled to discover a non-standard, and perhaps unknown website written by a stranger, because I get to peek inside their head and explore the inner qualities of their soul and expression, and the human condition itself.</p><p class="post__p">I created weird wide web hole to satisfy my own desires to explore the weird side of the web. I send what I would be excited to receive: obscure descriptions of strange and intriguing websites and incantations of wonder and curiosity. Every word I send and every link I collect is intended to make you pause, appreciate, and explore things differently to how we’ve become accustomed on this vast world wide web.</p><h2 class="post__h2">Make it easy for yourself</h2><p class="post__p">Lots of newsletters contain lots of words. As a consumer of newsletters, I often find I can’t make the time to read so many words. Newsletters often sit unread in my inbox for days, until I have the time and space to enjoy them. The attention span of the world is disintegrating before our very eyes, and so I purposefully choose to write just a few words. Yet every word I write is written intentionally, and for you to consume deeply and deliberately. I write few words because I mindful of your time and of my own time. And writing fewer words makes approaching each newsletter cognitively easy. Each issue takes around five minutes to put together.</p><p class="post__p">Additionally, each newsletter contains just four links, which I collect throughout the week. Four is a very achievable number, and often I have a collection of links that are lined up for one or two future issues. I write the foreword to each issue on the day I send it, purely inspired by how the links in the newsletter made me feel, or what’s going on in my head that day. I don’t think too hard about what I write, I just let it happen.</p><h2 class="post__h2">Do it for the love, not the numbers</h2><p class="post__p">At the time of writing, I have 346 active subscribers to my newsletter. That may seem like a small number compared to the number of people who know me on Bluesky or Twitch or even Youtube, but if you visualise that number of people in a room, it’s actually quite wonderful. I have the attention of almost 350 people, every Thursday.</p><p class="post__p">I know weird wide web hole may not appeal to a vast audience, and I actually kind of like that. I’m not going to sign off this blog post with a call to action to subscribe, because if you wanted to enter the weird wide web hole, you’d already be in there.</p><p class="post__p">goodbye</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I learned to code with my voice</title>
          <description>Struggling with severe hand pain, I learned to code by voice.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-i-learned-to-code-with-my-voice/</link>
          <guid>https://whitep4nth3r.com/blog/how-i-learned-to-code-with-my-voice/</guid>
          <pubDate>Tue, 04 Feb 2025 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In January 2025, I developed excruciating pain and pins and needles in my hands, which made it very difficult to type and use my trackpad. I panicked. I couldn’t work. And if I couldn’t work, I couldn’t earn money. And if I couldn’t earn money, I couldn’t afford to eat or pay my mortgage. I was actually really, really, scared.</p><p class="post__p">After ruling out potentially genetically inherited rheumatoid arthritis with x-rays and blood tests, I deduced that probably I had carpal tunnel syndrome (which, coincidentally, my mother also suffered with). The most recent development, after seeing a physiotherapist, is that my symptoms don’t actually align with carpal tunnel syndrome, but instead, abnormal pressure on the radial nerve, primarily in my right arm. I’m doing some prescribed daily exercises that are alleviating symptoms somewhat, but the bottom line is that rest is the best medicine in this case: physical rest <b class="post__p--italic">and mental rest</b>. My physical pain is real, but given the personal events I have endured over the last two years, I also believe that this manifestation of pain has been exacerbated by a combination of a series of emotionally traumatic events. I may write about this at a later date, but in this post I want to talk about how I learned to work and code using my voice, and offer some advice if you, too, find yourself in this extremely difficult situation.</p><h2 class="post__h2">Getting started is challenging</h2><p class="post__p">I knew I had to do something to ease the pain to enable me to work, but I had no idea where to start. And so I turned to <a href="https://bsky.app/profile/whitep4nth3r.com/post/3lfanag5si22c" target="_blank">Bluesky to ask if anyone had any advice</a>. The advice was overwhelming, actually. Ergonomics, exercises, meditation, alternative keyboard layouts, and voice control. And when I asked for advice on <a href="https://bsky.app/profile/whitep4nth3r.com/post/3lfcdlegln225" target="_blank">how to approach voice control and coding by voice</a>, the advice was even more overwhelming. Everyone was very generous with their advice, but whilst dealing with debilitating pain, I was surrounded by a seemingly endless list of tools, plugins, apps, custom setups, and confusing options that left me feeling incredibly afraid for my short term future. How was I supposed to learn all of this to work effectively <b class="post__p--italic">right now</b>?</p><p class="post__p">Initially I made the mistake of trying to use all the tools at once. The result? Complete and utter confusion. I felt like I had reached the limits of what my brain was capable of, and this felt devastating. But, I did what every good software engineer does when dealing with a difficult bug: I stripped everything down and pieced the tools and functionality back together, one by one. Next, I’ll detail the tools that worked for me, and I’ll follow with some important advice that I wish I’d found or that someone had given me when I was at the start of my journey.</p><h2 class="post__h2">My voice coding tools</h2><p class="post__p">I found success in just two weeks with just four tools.</p><h3 class="post__h3">Talon</h3><p class="post__p"><a href="https://talonvoice.com/docs/#" target="_blank">Talon</a> enables you to write code, play games, and control your computer with <b class="post__p--italic"><b class="post__p--bold">voice</b></b>, eye tracking, or noises. It is certainly a powerful piece of software — but it is a heavy lift to set up. I was fortunate that I was able to afford some light use of my hands whilst installing and configuring my Talon setup, but it probably would have been impossible if I had zero hands to work with.</p><p class="post__p">Talon is entirely geared towards software engineers. So much so, that setting up Talon requires being able to navigate your terminal, and write or install script files before you can even use any type of voice control effectively. Fortunately, the Talon community gives you a much-needed head start by providing a selection of useful scripts in the <a href="https://github.com/talonhub/community" target="_blank">community repository on GitHub</a>, which you can clone directly inside your <code>.talon</code> directory created on installation. This repository of Talon scripts allows you to get started relatively quickly in using VSCode and other IDEs, web browsers, email programs, work-based chat programs such as Slack, and even kubectl. Without this community repository, I would have been completely lost.</p><p class="post__p">You can also fork the community repository, make some changes according to your needs, and clone that to your <code>.talon</code> directory. The current recommendation is not to modify too many files in the community repository, and to add your own files to make modifications where possible, to avoid any merge conflicts when pulling in new changes to the community maintained repository.</p><p class="post__p">I also wrote a few of my own Talon scripts to make running commands in my terminal a lot easier, such as <code>npm run dev</code> (rather than having to spell them out), and you can view these in my <a href="https://github.com/whitep4nth3r/talon-custom" target="_blank">talon-custom</a> repository on GitHub. I also modified many of the default spellings from American to British English versions. Talon also has a very helpful Slack community, which is a bonus.</p><h3 class="post__h3">Cursorless</h3><p class="post__p"><a href="https://www.cursorless.org/" target="_blank">Cursorless</a> is a tool to use in conjunction with Talon, to help speed up the way you navigate and interact with code in VSCode. The main superpower I have so far unlocked with Cursorless is instead of having to ask Talon to move the cursor down a few lines, or left a couple words, and then select some text and/or delete it, you can do all of that in one Cursorless command. Now, it is clearly far more powerful than that, but with Cursorless you need to start slowly, and you need to have learned the Talon alphabet before any of this is possible. More on that later.</p><h3 class="post__h3">Apple Voice Control</h3><p class="post__p">Whilst trying to get to grips with Talon, I found that Apple Voice Control was a good option for being able to use my mobile devices effectively, and for being able to complete basic tasks on my MacBook such as window switching, natural voice dictation and web browsing. I was particularly impressed with the ease of targeting clickable elements using the “show numbers” command, which assigns each clickable element on a screen a number that you can select with your voice. The way that the functionality of Apple Voice Control is mirrored across different MacOS and iOS devices is really well thought out, and I can see it working well for the most part for people who don’t need to code with their voice.</p><p class="post__p">I will mention, however, that a number of web apps do not yet respond well to the text editing commands provided by Apple Voice Control, in particular Notion, which I use on a daily basis. Bear this in mind if you need to rely on Apple Voice Control for longer periods of time; it might just ruin your day. I had much better success using Talon with Notion when I had learned more of the Talon-specific commands.</p><h3 class="post__h3">Rango</h3><p class="post__p">With Talon on its own, navigating the web is still a challenge at first. Often I found myself switching between using Talon and Apple Voice Control so that I didn’t have to use the mouse functions in Talon to click on buttons and other clickable elements, because saying tab over and over again to reach my desired click target was time consuming and frustrating (not to mention that tab orders often suck on web pages, and click targets do not always have clear focus indicators) and I found it difficult to learn the Talon mouse commands. The <a href="https://github.com/david-tejada/rango-talon#js-repo-pjax-container" target="_blank">Rango browser extension</a> in conjunction with the <a href="https://github.com/david-tejada/rango-talon#js-repo-pjax-container" target="_blank">rango-talon</a> user file set gave me the same functionality of the “show numbers” command from Apple Voice Control, but without having to use the command itself, allowing me to click on any clickable target effectively using letters from the Talon alphabet, without having to say “tab” 100 times.</p><h2 class="post__h2">Other tools I tried with less success</h2><p class="post__p">Given that learning how to use my voice to work was frustrating, I fell into the trap of trying to find a tool that would give me back the power of typing code as quickly as possible. I tried two more approaches that I currently do not recommend.</p><h3 class="post__h3">Serenade</h3><p class="post__p"><a href="https://serenade.ai/" target="_blank">Serenade</a> claims to offer you the ability to write code using natural speech, which sounds incredibly enticing, but I had limited to no success with this product. The tutorials gave promising results, but I have a feeling that Serenade is geared entirely towards React and Python apps, as when trying to write code inside a Vue/Nuxt app, Serenade didn’t understand at all how to do what it claimed.</p><h3 class="post__h3">Voice dictation to direct AI code generation</h3><p class="post__p">An interesting suggestion I received was to use voice dictation as a way to prompt AI code generation tools to write the code for me. I tried this using Claude inside Cursor and hated it. My end goal was to be able to use my voice to work in the way I was accustomed to, rather than switching to an entirely unfamiliar AI-codegen-based workflow.</p><h2 class="post__h2">Important advice for learning to code with your voice</h2><p class="post__p">Here are some of the things I wish I had known before learning to work with my voice.</p><h3 class="post__h3">Take it slowly</h3><p class="post__p">The one thing I wish that had been communicated to me from the beginning of my journey is that <b class="post__p--italic">you have to start slowly</b>. Really slowly. Using a computer and coding with voice is an entirely new, and let’s face it, alien skill. And with any new skill, you can’t be expected to go from zero to 100 in a day. The dissonance I felt with this though, is that I could already code, and I was frustrated that I couldn’t reach my typing levels of productivity faster. I had ideas I wanted to get into my IDE, I had a vision for an app that I wanted to build, and I hated the fact that my body wasn’t letting me do it.</p><p class="post__p">Initially I installed Talon and Cursorless, watched two tutorial videos, and wanted to get to work. But I couldn’t; the landscape was entirely unfamiliar. I felt stuck. And then I had to force myself to do what any teacher would tell you when you got stuck: take it slowly. And start from the start.</p><h3 class="post__h3">Start from the start</h3><p class="post__p">When learning anything, it can be tempting to try and dive right into the deep end to get a head start. But if you can’t swim, you’ll drown. When learning to use Talon, you have to start from the start: the Talon alphabet.</p><p class="post__p">Talon uses its own proprietary alphabet which allows you to dictate single letters. A common concern is why Talon doesn’t use the NATO phonetic alphabet to avoid needing to learn a whole different map of words to single letters. The answer is that the NATO phonetic alphabet uses a collection of words that are two or three syllables in length, whereas each Talon letter name uses just <b class="post__p--bold">one syllable</b>. This means that in using the Talon alphabet, you’ll be uttering less than half of the syllables needed compared to the NATO phonetic alphabet in order to be productive.</p><p class="post__p">I learned the Talon alphabet fluently with around one hour of dedicated practise time on a Sunday evening, and this website in particular, which uses a flashcard system, was incredibly helpful: <a href="https://visualjerk.github.io/talon-learn-alphabet/#/" target="_blank">LearnTalon</a>.</p><h3 class="post__h3">Ramp up gradually before trying to code</h3><p class="post__p">Before attempting to code, I would recommend learning a few more skills in using Talon to navigate and edit text (rather than jumping straight to Cursorless). This will also help you with general prose writing and navigating the web and your machine. <a href="https://chaosparrot.github.io/talon_practice/" target="_blank">Talon Practise</a> was incredibly useful for this, giving you the opportunity to practise particular skills in small, focussed exercises. My favourite exercise which I was very excited to master was the <a href="https://chaosparrot.github.io/talon_practice/lessons/selection.html" target="_blank">copying, pasting and selection exercise</a>.</p><p class="post__p">Before using Cursorless, try using just Talon to edit some code. This will make the usefulness of Cursorless much more obvious if and when you decide to use it.</p><h3 class="post__h3">Try small coding tasks before big ones</h3><p class="post__p">We’ve all got jobs to do, and you’ve probably got quite a few big tickets looming over you on that project board. But you have to start small. Completing a small coding task with your voice, and committing and pushing the code in the terminal using your new-found Talon alphabet skills (<b class="post__p--italic">gust sit trap space say commit space dash made space quote say completed small task quote slap</b>), will give you a feeling of accomplishment like no other.</p><p class="post__p">My first all-voice code change and commit was commenting out a function in my Twitch bot which I no longer needed, and committing and pushing that change in the terminal. <a href="https://www.twitch.tv/whitep4nth3r/clip/DreamyNastyTroutPMSTwin-N_UdpsyTHdmj7JlG" target="_blank">Watch the clip on Twitch</a>.</p><h3 class="post__h3">You will get better</h3><p class="post__p">After two weeks of using my voice to work and code for around 80% of the time, I was able to successfully (yet slowly) use Talon and Cursorless to add a whole new feature to my website, and I wrote about it here: <a href="https://whitep4nth3r.com/blog/how-to-build-a-copy-code-snippet-button/" target="_blank">How to build a copy code snippet button and why it matters</a>. You will get better at this, but it will take time and practice. Make sure to take regular breaks, because using your voice so much will be exhausting, not to mention the new cognitive overheads of trying to do your job in such a different way.</p><p class="post__p">Fortunately for me, my pain is also getting better and so I’ve been able to use my keyboard and trackpad more than I was able to over the last few weeks. That being said, given I have become comfortable with using my voice to code, write, and navigate the internet, I find myself switching between using my hands and voice throughout the day. Maybe it’s because I feel like I don’t want to lose this new skill that I had the necessity to learn, or maybe it serves as a cool party trick to demonstrate when live streaming. But more importantly, it hammers home the fact that one day we all might succumb to a temporary or permanent disability, and as web developers we have an important responsibility to build empathy and accessibility into our professional practice at all times.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to build a copy code snippet button and why it matters</title>
          <description>It is impossible to highlight and copy code blocks when you are unable to use your hands.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-build-a-copy-code-snippet-button/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-build-a-copy-code-snippet-button/</guid>
          <pubDate>Fri, 24 Jan 2025 00:00:00 GMT</pubDate>
          <category>Accessibility</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you’ve been watching my streams in 2025, you’ll know that I’m currently suffering from carpel tunnel syndrome. In an attempt to rest my hands and ease the pain, I have been trying to code, navigate the internet, and use my machine with my voice. This has not been without significant challenges, and until you experience this for yourself, it is very difficult to understand just how hostile technology is towards people who have limited or no use of their hands. Those of you who know me, know I am a huge advocate for an accessible web and accessibility practices, but given this is the first time I have experienced the limited use of my hands, I had no idea just how important it was to give people the ability to perform as few actions as possible on a machine to get their job done.</p><p class="post__p">It is extremely difficult to highlight blocks of code and copy them with voice commands. At this point, for me at least, it is impossible. (Either it is actually impossible, or I haven’t had the brainpower to work out how to do it yet. Learning to navigate the web and code without your hands takes a huge mental toll.) For this reason, when you’re writing about code, especially when you’re creating tutorials that involve the reader copying and pasting code, it is <b class="post__p--bold">absolutely imperative that you include a way for people with limited use of their hands to copy that code in a single action.</b></p><p class="post__p">Prior to my experience with carpal tunnel syndrome, I was of the opinion that a copy code button was superfluous. But I was wrong. And so, one of my first voice-based coding tasks was to add a copy code button on all code snippets featured on my blog posts. I am working on another blog post where I talk about my voice coding journey, but in case you’re interested, the tools I have been using are <a href="https://talonvoice.com/" target="_blank">Talon</a> and <a href="https://www.cursorless.org/" target="_blank">Cursorless</a> inside VSCode. Relearning how to do everything on a computer with your voice involves a steep learning curve, but this whole endeavour is giving me some pain relief already. And yes, I am writing this blog post with my voice as much as my tools allow.</p><p class="post__p">My copy code snippet button solution is written in plain HTML, CSS and JavaScript inside an Eleventy project using the JavaScript templating engine (hence the use of string interpolation with curly braces).</p><h2 class="post__h2">The HTML (and some preliminary JavaScript to build the HTML)</h2><p class="post__p">In a blog post about code, you’ll probably have more than one code block snippet on the page. For this reason, you need to assign each code block with a unique ID in order to target the correct content to copy. You can generate a unique ID in any way you like, such as by using an external library to do so, but here’s how I did it for each code snippet at build time.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ihQEuNUmiH"
      aria-describedby="ihQEuNUmiH">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ihQEuNUmiH">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="ihQEuNUmiH" itemprop="text" content="function%20makeId(length)%20%7B%0A%20%20let%20result%20%3D%20%22%22%3B%0A%20%20const%20characters%20%3D%20%22ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz%22%3B%0A%20%20const%20charactersLength%20%3D%20characters.length%3B%0A%20%20let%20counter%20%3D%200%3B%0A%20%20while%20(counter%20%3C%20length)%20%7B%0A%20%20%20%20result%20%2B%3D%20characters.charAt(Math.floor(Math.random()%20*%20charactersLength))%3B%0A%20%20%20%20counter%20%2B%3D%201%3B%0A%20%20%7D%0A%20%20return%20result%3B%0A%7D%0A%0Aconst%20thisCodeID%20%3D%20makeId(10)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">makeId</span><span class="token punctuation">(</span><span class="token parameter">length</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> result <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> characters <span class="token operator">=</span> <span class="token string">"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> charactersLength <span class="token operator">=</span> characters<span class="token punctuation">.</span>length<span class="token punctuation">;</span><br>  <span class="token keyword">let</span> counter <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><br>  <span class="token keyword">while</span> <span class="token punctuation">(</span>counter <span class="token operator">&lt;</span> length<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    result <span class="token operator">+=</span> characters<span class="token punctuation">.</span><span class="token function">charAt</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> charactersLength<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    counter <span class="token operator">+=</span> <span class="token number">1</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br>  <span class="token keyword">return</span> result<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">const</span> thisCodeID <span class="token operator">=</span> <span class="token function">makeId</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Given that formatted and syntax-highlighted code blocks will contain extra HTML and CSS classes to power the formatting and highlighting, we need to ensure that the copy button only copies the <b class="post__p--bold">raw code content</b> to the clipboard. My solution to this was to store the raw code string in a meta tag for easy retrieval in JavaScript (in contrast to stripping out extraneous HTML tags; and even then, we wouldn’t want to strip HTML tags from an HTML code snippet!). </p><p class="post__p">Given we are using meta tags to store information, and we like to write valid HTML in 2025, we need to add some extra markup to satisfy <a href="https://schema.org/" target="_blank">Schema.org</a> vocabulary, and describe the the type of data we are using inside the meta tag. When storing the raw code string in a meta tag, you’ll also want to use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent" target="_blank"><code>encodeURIComponent</code></a> so that the code doesn’t actually execute on the page if it is able to (such as in the case of JavaScript). This meta tag lives inside to each code block on my blog posts, with the following Schema.org properties:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BEAMzbCkiq"
      aria-describedby="BEAMzbCkiq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BEAMzbCkiq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="BEAMzbCkiq" itemprop="text" content="%3Cdiv%20data-code-block%20itemscope%20itemtype%3D%22https%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%3E%0A%20%20%3Cmeta%20itemprop%3D%22codeSampleType%22%20content%3D%22snippet%22%3E%0A%20%20%3Cmeta%20itemprop%3D%22programmingLanguage%22%20content%3D%22%24%7Blang%7D%22%3E%0A%20%20%3Cmeta%20data-code-id%3D%22%24%7BthisCodeId%7D%22%20itemprop%3D%22text%22%20content%3D%22%24%7BencodeURIComponent(code)%7D%22%3E%0A%20%20%20%20%24%7BHighlightPairedShortcode(code%2C%20%60%24%7Bprefix%7D%24%7Blang%7D%60)%7D%0A%3C%2Fdiv%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">data-code-block</span> <span class="token attr-name">itemscope</span> <span class="token attr-name">itemtype</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://schema.org/SoftwareSourceCode<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">itemprop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>codeSampleType<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>snippet<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">itemprop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>programmingLanguage<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>${lang}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">data-code-id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>${thisCodeId}<span class="token punctuation">"</span></span> <span class="token attr-name">itemprop</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>${encodeURIComponent(code)}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    ${HighlightPairedShortcode(code, `${prefix}${lang}`)}<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">It&#39;s also worth noting that I am using the <a href="https://www.11ty.dev/docs/plugins/syntaxhighlight/" target="_blank">syntax highlighting plugin from Eleventy</a>, hence the use of <code>highlightPairedShortcode</code> and why I cannot easily access the code sample with <code>innerText</code> as easily as you might expect.</p><p class="post__p">Here&#39;s how Schema.org interprets the structured data described in the markup:</p><img src="https://images.ctfassets.net/56dzm01z6lln/6SaheZOgUwuusOD5Qf0zqU/44a3454aa5bdce40536269a5186fb656/meta_softwaresourcecode.png" alt="A schema.org interpretation of the markup. Type software source code, code sample type snippet, programming language javascript, text console log." height="400" width="1160" /><p class="post__p">Next, add a button next to or inside the code block, referencing the unique identifier you created earlier. I also chose to add a little clipboard SVG icon next to the button text. If you choose to omit the button text and use an icon only, make sure to use an <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label" target="_blank">aria-label</a> to ensure the button has an accessible name (e.g. &quot;Copy code snippet&quot;) that can be understood by assistive technologies such as screen readers.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="aWfnEpnPdl"
      aria-describedby="aWfnEpnPdl">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="aWfnEpnPdl">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="aWfnEpnPdl" itemprop="text" content="%3Cbutton%20type%3D%22button%22%20data-copy%3D%22%24%7BthisCodeId%7D%22%3E%0A%20%20Copy%0A%3C%2Fbutton%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button<span class="token punctuation">"</span></span> <span class="token attr-name">data-copy</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>${thisCodeId}<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  Copy<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">The CSS</h2><p class="post__p">I’m not going to prescribe how to style this copy button on your own blog posts, but I chose to position mine absolutely at the top right of each code block. Ensure you include styles on the button for <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus" target="_blank"><code>focus</code></a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:active" target="_blank"><code>active</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible" target="_blank"><code>focus-visible</code></a> to ensure that people who are reading your blog posts without a mouse and navigating through the clickable elements using a keyboard or their voice have a clear indication of which element is currently focussed and has been clicked.</p><h2 class="post__h2">The JavaScript</h2><p class="post__p">Finally, use JavaScript to find each copy button on the page, and add the following event listener to each button. This code grabs the unique identifier for the raw code snippet from the button, finds the relevant <code>meta</code> tag, and copies the raw code stored in that <code>meta</code> tag to the clipboard. Ensure to use <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent" target="_blank"><code>decodeURIComponent</code></a> to copy the raw code snippet to its original form.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="oYLaqHbprA"
      aria-describedby="oYLaqHbprA">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="oYLaqHbprA">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="oYLaqHbprA" itemprop="text" content="const%20copyButtons%20%3D%20document.querySelectorAll(%60%5Bdata-copy%5D%60)%3B%0A%0AcopyButtons.forEach((button)%20%3D%3E%20%7B%0A%20%20button.addEventListener(%22click%22%2C%20(e)%20%3D%3E%20%7B%0A%20%20%20%20const%20codeId%20%3D%20e.target.dataset.copy%3B%0A%20%20%20%20const%20codeAsText%20%3D%20document.querySelector(%60%5Bdata-code-id%3D%24%7BcodeId%7D%5D%60).getAttribute(%22content%22)%3B%0A%20%20%20%20navigator.clipboard.writeText(decodeURIComponent(codeAsText))%3B%0A%20%20%7D)%3B%0A%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> copyButtons <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">[data-copy]</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>copyButtons<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">button</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  button<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> codeId <span class="token operator">=</span> e<span class="token punctuation">.</span>target<span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>copy<span class="token punctuation">;</span><br>    <span class="token keyword">const</span> codeAsText <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">[data-code-id=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>codeId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">]</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">"content"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    navigator<span class="token punctuation">.</span>clipboard<span class="token punctuation">.</span><span class="token function">writeText</span><span class="token punctuation">(</span><span class="token function">decodeURIComponent</span><span class="token punctuation">(</span>codeAsText<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">This solution will transfer pretty well to your favourite front end framework of choice. And remember, temporary disabilities can happen to anyone; none of us are immortal.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>A reluctant roundup of 2024</title>
          <description>In the spirit of self-love and positivity, I will now force myself to list all of the things I did on the internet (and beyond) in 2024.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/a-reluctant-roundup-of-2024/</link>
          <guid>https://whitep4nth3r.com/blog/a-reluctant-roundup-of-2024/</guid>
          <pubDate>Fri, 03 Jan 2025 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I published a <a href="https://whitep4nth3r.com/blog/2022-in-review/" target="_blank">2022 roundup</a>. I didn’t publish a 2023 roundup; it was a shit year for me. </p><p class="post__p">I’m also hesitant to publish a 2024 roundup because it has, once again, been a shit year for me. But I have already written about much of that. So in the spirit of self-love and positivity and a renewed sense of hope ignited within me by the days getting longer in the northern hemisphere, I will now force myself to list all of the things I did on the internet (and beyond) in 2024.</p><h2 class="post__h2">Writing</h2><p class="post__p">In 2024 I published 24 blog posts; 11 were written as part of my job for Sentry, and 13 were my own. I published more personal, life-based posts than usual in 2024. Whilst being vulnerable on the internet is scary, these personal posts resonated with a lot of people. I received countless emails and DMs from friends and strangers on the internet who had experienced or were experiencing similar things in life, and who found comfort in my vulnerability. One person, in particular, has become what I consider a good friend who I turn to in moments of internal crisis. (You know who you are.)</p><h2 class="post__h2">Speaking</h2><p class="post__p">In 2024 I delivered my talk <a href="https://whitep4nth3r.com/talks/entertainment-as-code-finale/" target="_blank">Entertainment as Code</a> a total of five times. I travelled to Norway, Toronto, London, Middlesbrough and gave the talk online. It was a really interesting process to give an evolving conference talk throughout the year, whilst the underlying project I was speaking about simultaneously evolved. I would strongly recommend delivering a talk multiple times. After all, musicians and comedians and theatre performers don’t just do a thing once and move on to the next thing.</p><h2 class="post__h2">Streaming</h2><p class="post__p">I continued to <a href="https://twitch.tv/whitep4nth3r" target="_blank">live stream on Twitch</a> regularly, until I took a forced break in Q3. Streams this year focussed mainly on building my silly text-based community game <a href="https://pantherworld.netlify.app" target="_blank">p4nth3rworld</a>, which is what powered this year’s conference talk. The project culminated in a hack week in August 2024, where I streamed for 25 hours across five days working on the game. I’m not sure if I will continue working on this much more into 2025, despite there still being a sizeable <a href="https://github.com/whitep4nth3r/pantherworld/issues" target="_blank">backlog of feature requests</a>. In fact, I am ruminating on a blog post about when and why to archive a project. Stay tuned.</p><h2 class="post__h2">Newsletter</h2><p class="post__p">In 2024 I launched a newsletter: <a href="https://buttondown.com/weirdwidewebhole" target="_blank">weird wide web hole</a>. Each issue contains four links and some words from my brain at the time. I sent 50 issues and gained 350 subscribers. The open rate averaged at 50%, and retention at 90%. I came up with a formula that made building and sending each issue quick and not stressful, and something I would like to receive. The most important thing I wanted to focus on is to enjoy sending this newsletter, and after a full year I still do. The weird wide web hole will continue to open up every Thursday in 2025; <a href="https://buttondown.com/weirdwidewebhole" target="_blank">subscribe here</a>.</p><h2 class="post__h2">Career</h2><p class="post__p">Towards the end of 2024 I started thinking more intentionally about how my career might evolve over the next few years. I’ve had some good conversations with some very clever people, and have started exploring Developer Relations through a more learned and strategic business lens. Two blog posts I published in 2024 were on this topic (<a href="https://whitep4nth3r.com/blog/defining-paths-to-business-value-in-developer-relations/" target="_blank">Defining paths to business value in Developer Relations</a> and <a href="https://whitep4nth3r.com/blog/stop-writing-seo-first-the-4-types-of-devrel-content-plus-leading-themes/" target="_blank">Stop writing SEO-first! These are the 4 types of DevRel content (plus their Leading Themes</a>) and have been very well received.</p><h2 class="post__h2">Hobbies</h2><p class="post__p">I’m still working through another huge cross-stitch and I’ve been reading more. I had to forego my drum lessons for the last part of 2024, but I’m starting up again this month. And in 2025 I’m determined to actually produce that new piece of music that I’ve been promising myself since March 2020.</p><img src="https://downloads.ctfassets.net/56dzm01z6lln/4G416fBeg1WZOn47EDrWKb/5213e8717fa9ff5d668907f3a8bc87bd/neptune_progress.png" alt="An angled lose up of a cross stitch of Neptune, his face is complete, but not much of the beard, the colourful shell and coral based headdress is mostly complete and some of the dark blue background has been started. There is a banana at the top right of the large canvas for scale. The canvas is around 1.5m by 1m." height="4476" width="4284" /><h2 class="post__h2">2025</h2><p class="post__p">As I approach 40 this year, I am beginning to understand why people say “life begins” at 40. As time goes on, I learn more about myself, and the world, and how to interact with it more intentionally and more calmly; I have fewer and fewer opinions about things I cannot change. Whilst my priorities are centred on health, wellbeing, family, friends, and peace, I am starting to feel pretty well-equipped to navigate this weird industry landscape without burning out, and perhaps, you could even say, <b class="post__p--italic">thrive</b>.</p><p class="post__p">As my friend <a href="https://bsky.app/profile/danielroe.dev/post/3lejxkm35xc2h" target="_blank">Daniel Roe said on Bluesky</a>:</p><blockquote class="post__blockquote"><p class="post__p">you&#39;re thoughtful and very funny and don&#39;t hide what you really think </p><p class="post__p">I think you don&#39;t think as highly of yourself as you deserve though</p></blockquote><p class="post__p">In 2025 I’ll try, Daniel. I’ll try.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>It’s OK to have a slow day</title>
          <description>Sometimes, our bodies speak louder than our minds.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/its-ok-to-have-a-slow-day/</link>
          <guid>https://whitep4nth3r.com/blog/its-ok-to-have-a-slow-day/</guid>
          <pubDate>Tue, 17 Dec 2024 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">We put so much pressure on ourselves to be continuously productive. But we all know, deep-down, that this is an unsustainable, and frankly, highly unenjoyable way to live.</p><p class="post__p">Sometimes, our bodies speak louder than our minds. Some days, we are more tired; we ache, we find it more difficult to get out of bed. This is true for me, especially in the dark winter months. And so we are <b class="post__p--italic">forced</b> to take it slow on those days. We know we need the rest, and yet we feel guilt. We feel shame. We berate ourselves for not being able to fully submit ourselves to our labour. We apologise for not getting that thing done, even though there was no arbitrary deadline. And those feelings of guilt and shame send is spiralling into exhaustion. It is a literal vicious cycle.</p><p class="post__p">It’s OK to have a slow day. We all need slow days. We need days where we can think and reflect. We need days where we can create space to make important decisions without the inevitable pressure of time and artificial emergencies. Slow days allow us to recharge our bodies and our minds, so that we have the mental and physical energy to think more creatively and more innovatively. Think about those times when you step away from the code and finally solve that bug you’ve been working on for hours. You need to step away more often.</p><p class="post__p">Productivity and progress is not only measured by deliverables, such as lines of code, features, or blog posts. You are more than what you produce. You are your ideas, your thoughts, and your actions. Ideas come to healthy, energised humans. Ideas come to those who make space and make time. And actions can only be performed by those who have the energy, when they made space and time to rest.</p><p class="post__p">It’s OK to have a slow day. I wrote this on a slow day.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Your live coding stream does not need a bigger audience</title>
          <description>Less is more.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/your-live-coding-stream-does-not-need-a-bigger-audience/</link>
          <guid>https://whitep4nth3r.com/blog/your-live-coding-stream-does-not-need-a-bigger-audience/</guid>
          <pubDate>Wed, 11 Dec 2024 12:00:00 GMT</pubDate>
          <category>Streaming</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">There&#39;s a growing and gamified pressure from live streaming platforms like Twitch and YouTube to increase your viewer count and &quot;get more engagement&quot;. But, with particular types of content like live coding, this often only benefits the platform itself 💸. I believe you don&#39;t actually <b class="post__p--italic">need</b> a large audience for a live coding stream, and smaller audiences are more preferable and beneficial to you as a streamer.</p><p class="post__p">Live coding streams are (usually) centred on building something in public and collaborating with viewers as part of the process. Imagine a live coding stream in the real world: a group of software engineers, all sat around a whiteboard in a conference room or around a laptop in an office, trying to solve a problem. You probably imagined a group of fewer than ten engineers, right? Now imagine if you tried to cram hundreds of people into that space and wanted to solve the same problem. It would be chaos, right? Collaborating is very, very difficult to do well when hundreds of people are attempting to “collaborate” at anyone time.</p><p class="post__p">What I have experienced, is that live coding streams actually benefit from fewer viewers. Fewer viewers are usually more engaged because they have more <b class="post__p--italic">space</b> to engage when chat isn’t scrolling at 100mph. As a streamer, when I have fewer than 100 concurrent viewers, I am able to read and fully engage with each member of chat and what they have to offer. I don’t miss any ideas; everyone in the virtual room gets input. I am also able to physically reply to each viewer, making them feel seen, and as a result, they will hopefully become more engaged in what I have to offer, and return to the stream in the future. And the cycle continues. Yes, there are chat slow mode options you can use to avoid chat-overwhelm, but as a live stream <b class="post__p--italic">viewer</b> myself, I tend to view this as a barrier to entry to a stream (”Oh no, it’s busy in here, I won’t be <b class="post__p--italic">seen</b>”).</p><p class="post__p">Being able to respond to and engage with all of your viewers as fully as possible helps build a sense of community around what you&#39;re building and why you&#39;re building it. That’s why I believe your live coding stream does not need a bigger audience. You just need an audience.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>The 4 types of DevRel content</title>
          <description>Discover how to classify DevRel content to understand what works for your team. Learn about 4 content types, their goals, and leading themes.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/stop-writing-seo-first-the-4-types-of-devrel-content-plus-leading-themes/</link>
          <guid>https://whitep4nth3r.com/blog/stop-writing-seo-first-the-4-types-of-devrel-content-plus-leading-themes/</guid>
          <pubDate>Wed, 11 Dec 2024 00:00:00 GMT</pubDate>
          <category>DevRel Meta</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In working toward a way to define <b class="post__p--italic"><b class="post__p--bold">attributable</b></b> paths to real quantitative business value in Developer Relations (DevRel), which I discussed at the end of <a href="https://whitep4nth3r.com/blog/defining-paths-to-business-value-in-developer-relations/" target="_blank">this previous article</a>, I recently conducted a content classification exercise to understand the different types of DevRel output at Sentry (where I am currently am employed) and their underlying themes, using over 230 real content pieces. The exercise involved:</p><ul><li><p class="post__p">labelling each content piece with a “type” (Academic, Guide, Tutorial), and</p></li><li><p class="post__p">identifying the “Leading Theme” of the piece (Product/SEO, Contrived Example, Real Experience).</p></li></ul><p class="post__p">Here’s a small snapshot of the data using some of my own content as examples:</p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header">Title</th><th class="post__table__header">URL</th><th class="post__table__header">Type</th><th class="post__table__header">Leading Theme</th></tr><tr class="post__table__row"><td class="post__table__cell">How to deal with API rate limits</td><td class="post__table__cell"><a href="https://blog.sentry.io/how-to-deal-with-api-rate-limits/" target="_blank">https://blog.sentry.io/how-to-deal-with-api-rate-limits/</a></td><td class="post__table__cell">Academic</td><td class="post__table__cell">Contrived Example</td></tr><tr class="post__table__row"><td class="post__table__cell">How I fixed my brutal TTFB</td><td class="post__table__cell"><a href="https://blog.sentry.io/how-i-fixed-my-brutal-ttfb/" target="_blank">https://blog.sentry.io/how-i-fixed-my-brutal-ttfb/</a></td><td class="post__table__cell">Guide</td><td class="post__table__cell">Real Experience</td></tr><tr class="post__table__row"><td class="post__table__cell">Sentry can’t fix React hydration errors, but it can really help you debug them</td><td class="post__table__cell"><a href="https://blog.sentry.io/sentry-cant-fix-react-hydration-errors-but-it-can-really-help-you-debug-them" target="_blank">https://blog.sentry.io/sentry-cant-fix-react-hydration-errors-but-it-can-really-help-you-debug-them</a></td><td class="post__table__cell">Tutorial</td><td class="post__table__cell">Contrived Example</td></tr><tr class="post__table__row"><td class="post__table__cell">What is INP and why you should care</td><td class="post__table__cell"><a href="https://blog.sentry.io/what-is-inp/" target="_blank">https://blog.sentry.io/what-is-inp/</a></td><td class="post__table__cell">Academic</td><td class="post__table__cell">Product/SEO</td></tr><tr class="post__table__row"><td class="post__table__cell">How to make your web page faster before it even loads</td><td class="post__table__cell"><a href="https://blog.sentry.io/how-to-make-your-web-page-faster-before-it-even-loads/" target="_blank">https://blog.sentry.io/how-to-make-your-web-page-faster-before-it-even-loads/</a></td><td class="post__table__cell">Guide</td><td class="post__table__cell">Contrived Example</td></tr></tbody>
      </table></div><p class="post__p">I discussed this data with <a href="https://bsky.app/profile/guthals.com" target="_blank">Sarah Guthals</a>, and compared it with her own independent analysis of the same content. We also collected some quantitative data on some of the highest and lowest performing content pieces based on “views” and “visits to the sign up form”. This led us to formally define <b class="post__p--bold">four types of DevRel content</b>, where each should communicate a particular <b class="post__p--bold">Leading Theme</b> as standard.</p><h2 class="post__h2">The 4 types of DevRel content</h2><p class="post__p">Whereas we initially defined <b class="post__p--italic">three</b> types of content and used these to categorise existing pieces, we agreed to expand the types to <b class="post__p--italic">four</b>, which were more representative of what we wanted to aim for: <b class="post__p--bold">Academic</b>, <b class="post__p--bold">Guide</b>, <b class="post__p--bold">Dev Tips</b>, and <b class="post__p--bold">Tutorial</b>. The <b class="post__p--bold">Leading Themes</b> are described as follows (in the context of DevRel at Sentry):</p><ul><li><p class="post__p"><b class="post__p--bold">Product</b>: <b class="post__p--italic">How Sentry helps</b></p></li><li><p class="post__p"><b class="post__p--bold">Contrived Example</b>: <b class="post__p--italic">How Sentry can help in a particular (potentially abstract) challenge</b></p></li><li><p class="post__p"><b class="post__p--bold">Real Experience</b>: <b class="post__p--italic">How Sentry helped in a real product with real users</b></p></li></ul><p class="post__p">The table below aims to concisely communicate the preferred Leading Theme and Goal of each of the four types of DevRel content. Where I have used the word “TOOL”, this would usually be substituted for the name of the product or company you are writing/recording/speaking about or for (i.e. Sentry). I have included representative examples of my own content to practically demonstrate each type.</p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header">Type</th><th class="post__table__header">Leading Theme</th><th class="post__table__header">Goal</th><th class="post__table__header">Example</th><th class="post__table__header">URL</th></tr><tr class="post__table__row"><td class="post__table__cell">Academic</td><td class="post__table__cell">Product</td><td class="post__table__cell">To demonstrate a concept that affects developers, using TOOL</td><td class="post__table__cell">What is INP and why you should care</td><td class="post__table__cell"><a href="https://blog.sentry.io/what-is-inp/" target="_blank">https://blog.sentry.io/what-is-inp/</a></td></tr><tr class="post__table__row"><td class="post__table__cell">Guide</td><td class="post__table__cell">Real Experience</td><td class="post__table__cell">To demonstrate how to solve a specific challenge using TOOL</td><td class="post__table__cell">How I reduced an API call from &gt;5 seconds to under 100ms</td><td class="post__table__cell"><a href="https://blog.sentry.io/how-i-reduced-an-api-call-from-greater-than-5-seconds-to-under-100ms/" target="_blank">https://blog.sentry.io/how-i-reduced-an-api-call-from-greater-than-5-seconds-to-under-100ms/</a></td></tr><tr class="post__table__row"><td class="post__table__cell">Dev Tips</td><td class="post__table__cell">Contrived Example</td><td class="post__table__cell">To demonstrate how to accomplish a smaller task using TOOL</td><td class="post__table__cell">How to deal with API rate limits</td><td class="post__table__cell"><a href="https://blog.sentry.io/how-to-deal-with-api-rate-limits/" target="_blank">https://blog.sentry.io/how-to-deal-with-api-rate-limits/</a></td></tr><tr class="post__table__row"><td class="post__table__cell">Tutorial</td><td class="post__table__cell">Product</td><td class="post__table__cell">To demonstrate, step by step, how to go from nothing to a working example using TOOL</td><td class="post__table__cell">Using Hooks To Monitor &amp; Error Track With Sentry</td><td class="post__table__cell"><a href="https://docs.directus.io/blog/hooks-monitoring-error-tracking-sentry.html" target="_blank">https://docs.directus.io/blog/hooks-monitoring-error-tracking-sentry.html</a></td></tr></tbody>
      </table></div><h2 class="post__h2">Which type of content “converts” better (and therefore provides more business value)?</h2><p class="post__p">The type of content that performed best in our content database was <b class="post__p--bold">Guides</b>. Now, the content that we produce as DevRel, isn&#39;t necessarily aimed at converting readers to sign-ups as a standalone effort; each piece of content is part of a larger campaign to educate developers on topics, challenges, and solutions for which the TOOL is useful. So while our conversion rate from view &gt; visit to the sign up page was around 3% for Guides, we leverage that as an early indicator of success, not as our singular success metric.</p><p class="post__p">As a result of this, and given our discussions internally, a company with a TOOL like Sentry should focus larger chunks of DevRel time on <b class="post__p--bold">Guides</b> and <b class="post__p--bold">Dev Tips</b>. From experience, Guides and Dev Tips are:</p><ul><li><p class="post__p">less time-consuming to create, and</p></li><li><p class="post__p">easier to centre on real examples and experience than Tutorials or Academic pieces.</p></li></ul><p class="post__p">In focussing your efforts on Guides and Dev Tips, you should end up being able to deliver more value (e.g. conversion however you define it) more efficiently, whilst at the same time providing developers with clear real-world examples of challenges and successes. For example, in response to my latest Guide <a href="https://blog.sentry.io/how-i-reduced-an-api-call-from-greater-than-5-seconds-to-under-100ms/?original_referrer=https://blog.sentry.io/" target="_blank">How I reduced an API call from &gt;5 seconds to under 100ms</a>, <a href="https://bsky.app/profile/oliverturner.cloud/post/3lcn7t3um6c2d" target="_blank">Blueskyer Oliver Turner</a> said “what a fab example of good Dev Rel this was: a highly relatable real-world example, neatly solved with the help of the product.”</p><img src="https://images.ctfassets.net/56dzm01z6lln/2PX1JwZuNnC8Tsg5T8nQnP/968702da408f6c309afc75d2324ab571/olive_blog_post_feedback.png" alt="Just wanted to say that when I read the article I was struck by what a fab example of good Dev Rel this was: a highly relatable real-world example, neatly solved with the help of the product. Not wishing more perf issues on you, but looking forward to more like this 😀" height="299" width="740" /><h2 class="post__h2">Stop creating SEO-first content</h2><p class="post__p">Notice how SEO (Search Engine Optimisation) is not a leading theme in this framework, despite being used in the original classification exercise. In DevRel, we <b class="post__p--italic">know</b> content is often created because companies want to show up in search engines for certain keywords, but, interestingly (or perhaps, not-so-interestingly), SEO-first content tended to be the worst performing in terms of conversion from views &gt; visits to a sign up form in our limited dataset, with the exception of a few timely pieces such as <a href="https://blog.sentry.io/what-is-inp/?original_referrer=https://blog.sentry.io/" target="_blank">What is INP and why you should care</a>. It could also be argued that, despite the main motivation for writing this piece being heavily weighted toward SEO, it ended up being a Guide anyway — which is the better approach.</p><p class="post__p">As someone working in DevRel, you may receive requests related to using keywords so that a company can show up in search results (coincidentally my INP post was the top featured post on Google when INP was launched), but you will be doing your company a disservice by <b class="post__p--italic">leading</b> a piece of content with SEO. In DevRel, your job is to create something <b class="post__p--italic">useful</b> for developers, and depending on your usual audience, a Guide or a Dev Tip is probably preferable. In the INP post, I intentionally did not lead with SEO, but addressed a real topic facing developers today (INP), was aware of the company&#39;s desire to show up in search results for INP, and yet focused on providing actionable insights for developers.</p><p class="post__p">There is a particular piece of content I wrote (not at Sentry) that haunts me because I wrote it entirely for SEO purposes on the suggestion of a colleague in marketing, and did so begrudgingly. It is probably the only piece I did not archive on my own domain. Let me know if you ever track it down and we can laugh/cry together.</p><h2 class="post__h2">Do this exercise for yourself</h2><p class="post__p">I would highly recommend doing the content classification exercise for content that your DevRel team has created to help you identify:</p><ul><li><p class="post__p">whether the right things for your team and company product are determining your Leading Themes,</p></li><li><p class="post__p">whether your content pieces have clearly defined goals, and</p></li><li><p class="post__p">whether you have any gaps in your content types.</p></li></ul><p class="post__p">To get you started, I’ve put together a template in Google Sheets, where you can list your content pieces and start categorising by type and Leading Theme. I’ve included the Product/SEO Leading Theme despite it not being the most desirable one in DevRel, given this is a useful insight into how content is <b class="post__p--italic">often</b> created. Open the <a href="https://docs.google.com/spreadsheets/d/1QMuWnzgHXZcA9n3Lo9Gm9ZTRLotIddx99ddFvzleLUg/edit?usp=sharing" target="_blank">DevRel Content Classification spreadsheet</a>, make a copy, and give it a go. If you have data available relating to your success metrics, see if you can find any correlation between the different types of content approaches and success. I’d love to know what you discovered — <a href="https://bsky.app/profile/whitep4nth3r.com" target="_blank">let me know on Bluesky</a>!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>A story about a coat</title>
          <description>A coat cannot make you truly happy.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-making-an-impulsive-purchase-made-me-realise-i-havent-been-ok-since-my-dad-died/</link>
          <guid>https://whitep4nth3r.com/blog/how-making-an-impulsive-purchase-made-me-realise-i-havent-been-ok-since-my-dad-died/</guid>
          <pubDate>Sun, 08 Dec 2024 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">My childhood wasn’t a good childhood. A combination of a narcissistic mother, and a father who came from a culture where it was normal to hit your child, has equated to most of my childhood memories centring on emotional and physical abuse.</p><p class="post__p">Since the age of 16, when I started earning my own money, I enjoyed buying clothes. Online shopping wasn’t really a thing in the early 2000s, and so at this point it wasn’t really a “problem”. However, when I gained more independence and started driving at age 17, I would take every opportunity to travel to the local shops to buy something. It didn’t matter what it was; I just needed <b class="post__p--italic">something</b>.</p><p class="post__p">I moved out of my childhood home at age 18 and never looked back. After some talking therapy in my early twenties, I realised that I had been buying clothes to cope with the pervasive emotions that stemmed from an abusive childhood. But this type of realisation goes only some of the way to fixing the problem. I spent most of my twenties reinventing myself through a constant cycle of buying, selling and donating clothes. I went through a goth phase, a boho hippy phase, an all-black phase, and more. Each new reinvention would require a considerable initial investment to ensure my outward appearance was cohesive and put-together, which I justified through the hassle of selling everything on eBay from the previous phase. This continued throughout my thirties.</p><p class="post__p">In December 2023, I found myself in a Youtube hole on the theme of “not buying things”. I was inspired to follow suit, and <b class="post__p--italic">not buy any new and unnecessary clothes in 2024</b>. I put together an elaborate Notion page; I made rules about what I was allowed to buy and what I wasn’t allowed buy, and what I could buy if I truly needed to replace something. This prompted me, again, to sell or donate a lot of clothes that I didn’t wear regularly. My “no buy” lasted until March 2024, when I broke the seal by purchasing a new sweatshirt, because I convinced myself I needed something to pep me up for a conference trip to London. And so began another cycle of reinvention: clearing out and selling more clothes, and replacing them with sustainable, timeless, and classic pieces. These were clothes that were going to take me into my fifties. This was my “investment piece” cycle. This was the last cycle, <b class="post__p--italic">just one more reinvention bro</b>, I promised myself.</p><p class="post__p">This year was going well, actually. Whilst I joyfully abandoned the concept of not buying anything at all, each clothes-based purchase for the most part was carefully considered. I convinced myself that because I was using a clothing inventory app, (<a href="https://bsky.app/profile/whitep4nth3r.com/post/3lc6oi3q3kk2q" target="_blank">which I even talked about on Bluesky</a>) and because I made sure that each item I was adding to my wardrobe could make X number of outfits, I was shopping the right way. However, as autumn set in, and the days got darker, I found myself drawn towards the lure of buying things to <b class="post__p--italic">make me feel something</b>. Just one more item bro, then I’ll be the perfect person; I’ll be complete. And then, something stupid happened.</p><p class="post__p">I can’t even remember how this happened, but two days ago, I became obsessed with a coat. I don’t even know how I came to know about this coat, where I found this coat, or what part of the internet showed me this coat. But for 24 hours, this coat was everything to me. It was my next step to achieving the perfect version of myself. I researched <b class="post__p--italic">everything</b> about this coat. I watched videos, read blog posts, looked for good reviews, bad reviews, researched the sizing, the materials: everything. I found every single shopping website that stocked this coat. But I didn’t buy this coat. Because I couldn’t justify the cost. And plus, I already have two winter coats, and I like wearing them.</p><p class="post__p">After giving myself 24 hours to consider this purchase, (and convincing myself that this was enough time to really consider this purchase), I bought the coat. I didn’t need the coat. But there it was: the dopamine-fuelled high of a new purchased followed very quickly by a sinking feeling of guilt, shame, and disgust. I was fully aware of what I was doing and why I was doing it; but I did it anyway.</p><p class="post__p">Last week I spoke to my doctor about symptoms I had been attributing to a somewhat early menopause that I had been monitoring for around 18 months (to cut a long story short, most of my cycle involves feeling very depressed and nauseous). Given my age (39 at the time of writing), the doctor dismissed my concerns of peri-menopause given that my periods are still regular and normal, and suggested that maybe I try some anti-depressants or the combined pill to ease my symptoms of extreme PMS. I rejected this idea; I haven’t had a good experience in the past with SSRIs or SNRIs (which I used in combination with therapy to try and deal with my childhood) or any type of contraceptive pill.</p><p class="post__p">But when I clicked “confirm purchase” on the coat, it hit me. “Around 18 months ago” is when a string of crappy life events came at me, one after the other.</p><ul><li><p class="post__p">In March 2023, my dad died;</p></li><li><p class="post__p">In July 2023, I got laid off;</p></li><li><p class="post__p">In August 2023, in the midst of many interview cycles, my husband had his first seizure, which set off a string of health-related issues culminating in <a href="https://whitep4nth3r.com/blog/work-is-meaningless/" target="_blank">a long hospital stay in September 2024</a>;</p></li><li><p class="post__p">The world has been pretty fucked this past year;</p></li><li><p class="post__p">And throughout all of this, my mother has continued to reject me, despite the promises of a renewed relationship after the passing of my dad.</p></li></ul><p class="post__p">Since March 2023, I haven’t been OK, but I figured it was my age and my hormones. But I know now that I am still trapped. Trapped in an endless cycle of trying to be OK, thinking I’m OK, acting like I’m OK, but being consumed by my desire to be loved and accepted, which manifests in buying clothes to reinvent myself.</p><p class="post__p">And yet, I am determined to be OK.</p><p class="post__p">The coat hasn’t arrived yet. Maybe I’ll send it back. Or maybe I’ll keep it. Maybe I’ll wear the shit out of it to remind me that I do have insight about my emotions. I know that I still have unmet needs from my childhood, and I&#39;ll probably have to live with this forever. And deep down, I know that a stupid impulsive purchase will not fix me.</p><p class="post__p">Yet, I am determined to be OK.</p><p class="post__p"><b class="post__p--bold">Update: I returned the coat. It made me feel nothing whatsoever. </b>
</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I reduced an API call from >5 seconds to under 100ms</title>
          <description>I wrote more terrible code. Here's how I fixed it.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/how-i-reduced-an-api-call-from-greater-than-5-seconds-to-under-100ms/</link>
          <guid>https://blog.sentry.io/how-i-reduced-an-api-call-from-greater-than-5-seconds-to-under-100ms/</guid>
          <pubDate>Thu, 05 Dec 2024 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Given that 100% of the databases I have interacted with in my <b class="post__p--italic">professional</b> career have been SQL databases, my data-based mental model (please enjoy my pun) has always defaulted to a relational one. However, when spinning up a tiny side project in 2020 (a bot to provide interactivity to<a href="https://twitch.tv/whitep4nth3r" target="_blank"> <u>my Twitch stream</u></a>), my data-storing requirements didn’t call for a relational model at the time, so I chose a NoSQL solution: MongoDB.</p><p class="post__p">Four years later in 2024, this tiny side project became much larger than I ever expected, and evolved into a text-based game:<a href="https://p4nth3r.world/" target="_blank"> <u>Pantherworld</u></a>. If you’re curious about how this evolved and would like to learn more about how this project grew from a tiny Twitch bot to a game,<a href="https://www.youtube.com/watch?v=xImG7GEAAaI&ab_channel=Sentry" target="_blank"> <u>watch the latest edition of my 2024 conference talk, Entertainment as Code, on Youtube</u></a>.</p><p class="post__p">Pantherworld has been played by hundreds of people, 24 hours a day, 7 days a week via my Twitch chat interface since April 2024. Game events in Pantherworld are powered by events that happen on my stream. When something happens, such as when I get a new follower, a random world item spawns in a random world zone. The objective is for my stream audience to use text commands in the Twitch chat to move around the world and claim items to fill up their inventory. Some items are rarer than others, so will spawn less frequently.</p><p class="post__p">To enable players to keep track of gameplay activity, I built<a href="https://p4nth3r.world/" target="_blank"> <u>a front end companion app</u></a>, which fetches game data from the NoSQL database via a collection of APIs. As the game attracted more and more players, there was one API call that very obviously didn’t scale: the endpoint that fetched the leaderboard data. It was painfully slow, but I had no idea <b class="post__p--italic">why</b> it was slow. I added a skeleton loader on the front end (instead of showing just a blank screen) to try and hide how slow it was, but it was painful to watch.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3bbHbCfusCQD00eIF8LmI7/3107e4e7399ab913b915e049172dc48d/1_pworld_loading.gif" alt="A dark web page with the title leaderboard. Underneath the title there are seven horizontal grey bars, slowly fading in and out, representing a skeleton loader. The loading never ends." height="648" width="1036" /><p class="post__p">It’s important to reiterate that my data-based mental model has always defaulted to a relational one, <b class="post__p--bold">and so I figured I was just doing NoSQL wrong</b> but I didn’t know <b class="post__p--italic">how</b> I was doing it wrong. Additionally, this app had evolved so much over four years of development that the data model requirements changed to <b class="post__p--italic">actually require a relational model</b>, and so my initial gut reaction was to refactor the whole thing to use an SQL database. <b class="post__p--bold">But I couldn’t face refactoring a four-year old legacy app and migrating hundreds of thousands of NoSQL documents to SQL</b>. It was time to turn to<a href="https://docs.sentry.io/concepts/key-terms/tracing/" target="_blank"> <u>tracing</u></a> to help me understand how I could optimize this terrible code.</p><h2 class="post__h2">What is tracing?</h2><p class="post__p">Tracing is a technology that captures each function call, database query, network request, browser event — everything that happens in your app — allowing you to understand how your apps actually work and expose where you may be able to make performance improvements or squash bugs. In Sentry, singular events are named<a href="https://docs.sentry.io/concepts/search/searchable-properties/spans/" target="_blank"> <u>spans</u></a><b class="post__p--italic">,</b> and they are connected in the<a href="https://docs.sentry.io/concepts/key-terms/tracing/trace-view/" target="_blank"> <u>Trace View</u></a> via an HTTP header sent with each span. You can also capture all of these events <b class="post__p--italic">across your entire stack of apps and services,</b> which is known as<a href="https://docs.sentry.io/concepts/key-terms/tracing/distributed-tracing/" target="_blank"> <u>Distributed Tracing</u></a>.</p><h2 class="post__h2">How to add tracing support for MongoDB database queries</h2><p class="post__p">Tracing for MongoDB database queries is supported out of the box for the latest version of the Sentry JavaScript and Python SDKs; no further setup required. My backend API is an Express app, so I’m using the latest version of the<a href="https://docs.sentry.io/platforms/javascript/guides/node/" target="_blank"> <u>Sentry Node SDK</u></a>. It’s recommended to keep all Sentry SDK initialization code in a separate file; mine is named <code>instrument.ts</code>. Here’s a stripped down SDK configuration in showing the most relevant options:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="hURFZGjHcY"
      aria-describedby="hURFZGjHcY">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="hURFZGjHcY">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="hURFZGjHcY" itemprop="text" content="%2F%2F%20instrument.ts%0A%0Aimport%20*%20as%20Sentry%20from%20%22%40sentry%2Fnode%22%3B%0A%0ASentry.init(%7B%0A%C2%A0%C2%A0%2F%2F%20unique%20Sentry%20project%20DSN%20(Data%20Source%20Name)%0A%C2%A0%20dsn%3A%20process.env.SENTRY_DSN%2C%C2%A0%C2%A0%C2%A0%0A%C2%A0%C2%A0%2F%2F%20add%20environment%20to%20context%20in%20Sentry%0A%C2%A0%C2%A0environment%3A%20process.env.NODE_ENV%2C%0A%C2%A0%C2%A0%2F%2F%20send%2075%25%20of%20traces%20to%20Sentry%C2%A0%C2%A0%C2%A0%0A%C2%A0%C2%A0tracesSampleRate%3A%200.75%2C%C2%A0%0A%7D)%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// instrument.ts</span><br><br><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> Sentry <span class="token keyword">from</span> <span class="token string">"@sentry/node"</span><span class="token punctuation">;</span><br><br>Sentry<span class="token punctuation">.</span><span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token comment">// unique Sentry project DSN (Data Source Name)</span><br>  dsn<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">SENTRY_DSN</span><span class="token punctuation">,</span>   <br>  <span class="token comment">// add environment to context in Sentry</span><br>  environment<span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NODE_ENV</span><span class="token punctuation">,</span><br>  <span class="token comment">// send 75% of traces to Sentry   </span><br>  tracesSampleRate<span class="token operator">:</span> <span class="token number">0.75</span><span class="token punctuation">,</span> <br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">The <code>tracesSampleRate</code> option tells the Sentry SDK to enable tracing. It takes a value between 0 and 1, and configures the percentage of traces your app will send to Sentry. It’s recommended to adjust this number based on your Sentry account plan and how many users your apps have.</p><p class="post__p">To enable Distributed Tracing (i.e. trace from a front end app to a back end app), make sure to add the<a href="https://docs.sentry.io/platforms/javascript/configuration/integrations/browsertracing/" target="_blank"> <u>browserTracingIntegration</u></a> to your front end Sentry SDK configuration in addition to setting the <code>tracesSampleRate</code>. Exceptions to this rule are full-stack front end frameworks such as<a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/" target="_blank"> <u>Next.js</u></a> and<a href="https://docs.sentry.io/platforms/javascript/guides/nuxt/" target="_blank"> <u>Nuxt</u></a>, which add the <code>browserTracingIntegration</code> by default.</p><p class="post__p">In the back end, to ensure that Sentry can automatically instrument all modules in your application, including MongoDB, make sure to require or import the <code>instrument.js</code> file <b class="post__p--bold">before requiring any other modules in your application.</b> Here are some of the imports at top of my app entry-point file, <code>app.ts</code> as an example:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="nMerTeTCoT"
      aria-describedby="nMerTeTCoT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="nMerTeTCoT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="nMerTeTCoT" itemprop="text" content="%2F%2F%20app.ts%0A%0Aimport%20%22.%2Fenv%22%3B%0Aimport%20*%20as%20Sentry%20from%20%22%40sentry%2Fnode%22%3B%20%2F%2F%20%3C----%20first%20imported%20module%0Aimport%20%7B%20webServer%20%7D%20from%20%22.%2Fwebserver%22%3B%0Aimport%20Database%20from%20%22.%2Fdata%2Fdatabase%22%3B%0Aimport%20WebSocketServer%20from%20%22.%2FWebSocketServer%22%3B%0A%0A%2F%2F...%20all%20other%20code">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// app.ts</span><br><br><span class="token keyword">import</span> <span class="token string">"./env"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> Sentry <span class="token keyword">from</span> <span class="token string">"@sentry/node"</span><span class="token punctuation">;</span> <span class="token comment">// &lt;---- first imported module</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> webServer <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"./webserver"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> Database <span class="token keyword">from</span> <span class="token string">"./data/database"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> WebSocketServer <span class="token keyword">from</span> <span class="token string">"./WebSocketServer"</span><span class="token punctuation">;</span><br><br><span class="token comment">//... all other code</span></code></pre>
    </div>
  </div>

  <p class="post__p">Now we’ve got tracing set up to capture MongoDB queries in my Express app, let’s investigate why my code for the leaderboard was so slow.</p><h2 class="post__h2">Why a single API call was taking &gt;5 seconds</h2><p class="post__p">Here’s a snapshot of traces before I made the code optimizations, with the durations of the API calls highlighted. HTTP GET requests to the /world/leaderboard API were taking anywhere from 4 to almost 8 seconds.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1SLJnoWv8eZMvW5OE3qvuz/05e665c05083420284876845b5df36fb/2_trace_list_before.png" alt="A table showing links to traces in Sentry. Each table row shows a trade ID, the root of the trace, which is the leaderboard GET endpoint, matching spans count, a horizontal bar representing the timeline, the duration, timestamp and a small issues menu. The durations column is annotated with a pink rectangle, showing durations of around 4 to 8 seconds." height="815" width="1280" /><p class="post__p">The (zoomed out) Trace View for a single API call exposes a series of request waterfalls and duplicate database queries. Something had to be done.</p><img src="https://images.ctfassets.net/56dzm01z6lln/16MFikNy65yPccXj4MFs4e/d74fe92501b25f99684e7e646f6dc760/3_before_full.png" alt="A maximum zoomed out web page showing a single trace view in Sentry of purple bars representing spans, or events. There are literally hundreds of requests made to a database, all cascading in a waterfall shape. This is a bad request waterfall." height="731" width="1280" /><p class="post__p">Before we explore how I reduced this API call from &gt;5 seconds to under 100ms, let’s look at what happens when a call to /world/leaderboard is made by the front end app.</p><p class="post__p">The leaderboard API call returns an array of players. Each <code>Player</code> object contains a <code>username</code>, an <code>items count</code>, and a <code>wealth_index</code> number (where the wealth index is a sum of all inventory items multiplied by their rarity).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="RdAgqihRjm"
      aria-describedby="RdAgqihRjm">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="RdAgqihRjm">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="RdAgqihRjm" itemprop="text" content="%2F%2F%20leaderboard.ts%0A%0Atype%20Player%20%3D%20%7B%0A%C2%A0%20username%3A%20string%3B%0A%C2%A0%20items%3A%20number%3B%0A%C2%A0%20wealth_index%3A%20number%3B%0A%7D%3B%0A%0Aexport%20async%20function%20getFullLeaderboard()%3A%20Promise%3C%7Bplayers%3A%20Player%5B%5D%7D%3E%20%7B%0A%09%2F%2F%20...%0A%7D">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// leaderboard.ts</span><br><br><span class="token keyword">type</span> <span class="token class-name">Player</span> <span class="token operator">=</span> <span class="token punctuation">{</span><br>  username<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br>  items<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span><br>  wealth_index<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getFullLeaderboard</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span><span class="token punctuation">{</span>players<span class="token operator">:</span> Player<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token operator">></span> <span class="token punctuation">{</span><br>	<span class="token comment">// ...</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Here’s the original code, slightly simplified. It does the following:</p><ol><li><p class="post__p">Queries the <code>Items</code> collection to fetch all items that are assigned to a user, groups the items by <code>userId</code>, and returns a count of those items</p></li><li><p class="post__p">For each player, calls <code>getAllItemsForPlayer()</code> (which queries the <code>Items</code> collection <b class="post__p--italic">again</b>)</p></li><li><p class="post__p">For each player, queries the <code>Player</code> collection (and the only purpose of this was to retrieve the <code>userDisplayName</code> which I didn’t want to store alongside the item data; a SQL join would have been nice here)</p></li><li><p class="post__p">Constructs and calculates each <code>Player</code> object, pushes it into an array, sorts the array, and returns it.</p></li></ol>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KijFVwVfdR"
      aria-describedby="KijFVwVfdR">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KijFVwVfdR">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="KijFVwVfdR" itemprop="text" content="%2F%2F%20leaderboard.ts%0A%0A%2F%2F%20imports...%0A%0Afunction%20sortByWealthIndexDesc(a%3A%20Player%2C%20b%3A%20Player)%20%7B%0A%C2%A0%20%2F%2F%20...%20standard%20sort%20function%0A%7D%0A%0Aexport%20async%20function%20getFullLeaderboard()%3A%20Promise%3C%7Bplayers%3A%20Player%5B%5D%7D%3E%20%7B%0A%C2%A0%20const%20players%20%3D%20await%20PantherworldItemModel.aggregate(%5B%0A%C2%A0%20%C2%A0%20%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20%24match%3A%20%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20%C2%A0%20userId%3A%20%7B%20%24ne%3A%20null%20%7D%2C%0A%C2%A0%20%C2%A0%20%C2%A0%20%7D%2C%0A%C2%A0%20%C2%A0%20%7D%2C%0A%0A%C2%A0%20%C2%A0%20%2F%2F%20Group%20documents%20by%20userId%2C%20count%20the%20number%20of%20items%20in%20each%20group%0A%C2%A0%20%C2%A0%20%7B%20%24group%3A%20%7B%20_id%3A%20%22%24userId%22%2C%20itemCount%3A%20%7B%20%24sum%3A%201%20%7D%20%7D%20%7D%2C%0A%C2%A0%20%5D)%3B%0A%0A%C2%A0%20const%20leaderboardPlayers%3A%20Player%5B%5D%20%3D%20%5B%5D%3B%0A%0A%C2%A0%20const%20promises%20%3D%20players.map(async%20(player)%20%3D%3E%20%7B%0A%C2%A0%20%C2%A0%20const%20allItems%3A%20ItemForWealthIndex%5B%5D%20%3D%20await%20getAllItemsForPlayer(%0A%C2%A0%20%C2%A0%20%C2%A0%20player._id%2C%0A%C2%A0%20%C2%A0%20)%3B%0A%0A%C2%A0%20%C2%A0%20const%20dbPlayer%20%3D%20(await%20PantherworldPlayerModel.findOne(%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20userId%3A%20player._id%2C%0A%C2%A0%20%C2%A0%20%7D))%20as%20PantherworldPlayerData%3B%0A%0A%C2%A0%20%C2%A0%20leaderboardPlayers.push(%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20username%3A%20dbPlayer.userDisplayName%2C%0A%C2%A0%20%C2%A0%20%C2%A0%20items%3A%20player.itemCount%2C%0A%C2%A0%20%C2%A0%20%C2%A0%20wealth_index%3A%20getWealthIndex(allItems)%2C%0A%C2%A0%20%C2%A0%20%7D%20as%20Player)%3B%0A%C2%A0%20%7D)%3B%0A%0A%C2%A0%20await%20Promise.all(promises)%0A%C2%A0%20%C2%A0%20.then()%0A%C2%A0%20%C2%A0%20.catch((err)%20%3D%3E%20%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20Sentry.captureException(err)%3B%0A%C2%A0%20%C2%A0%20%7D)%3B%0A%0A%C2%A0%20return%20%7B%0A%C2%A0%20%C2%A0%20players%3A%20leaderboardPlayers.sort(sortByWealthIndexDesc)%2C%0A%C2%A0%20%7D%3B%0A%7D">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// leaderboard.ts</span><br><br><span class="token comment">// imports...</span><br><br><span class="token keyword">function</span> <span class="token function">sortByWealthIndexDesc</span><span class="token punctuation">(</span>a<span class="token operator">:</span> Player<span class="token punctuation">,</span> b<span class="token operator">:</span> Player<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// ... standard sort function</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getFullLeaderboard</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span><span class="token punctuation">{</span>players<span class="token operator">:</span> Player<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token operator">></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> players <span class="token operator">=</span> <span class="token keyword">await</span> PantherworldItemModel<span class="token punctuation">.</span><span class="token function">aggregate</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      $match<span class="token operator">:</span> <span class="token punctuation">{</span><br>        userId<span class="token operator">:</span> <span class="token punctuation">{</span> $ne<span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>    <span class="token comment">// Group documents by userId, count the number of items in each group</span><br>    <span class="token punctuation">{</span> $group<span class="token operator">:</span> <span class="token punctuation">{</span> _id<span class="token operator">:</span> <span class="token string">"$userId"</span><span class="token punctuation">,</span> itemCount<span class="token operator">:</span> <span class="token punctuation">{</span> $sum<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> leaderboardPlayers<span class="token operator">:</span> Player<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> promises <span class="token operator">=</span> players<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span>player<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> allItems<span class="token operator">:</span> ItemForWealthIndex<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getAllItemsForPlayer</span><span class="token punctuation">(</span><br>      player<span class="token punctuation">.</span>_id<span class="token punctuation">,</span><br>    <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> dbPlayer <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword">await</span> PantherworldPlayerModel<span class="token punctuation">.</span><span class="token function">findOne</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      userId<span class="token operator">:</span> player<span class="token punctuation">.</span>_id<span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">as</span> PantherworldPlayerData<span class="token punctuation">;</span><br><br>    leaderboardPlayers<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      username<span class="token operator">:</span> dbPlayer<span class="token punctuation">.</span>userDisplayName<span class="token punctuation">,</span><br>      items<span class="token operator">:</span> player<span class="token punctuation">.</span>itemCount<span class="token punctuation">,</span><br>      wealth_index<span class="token operator">:</span> <span class="token function">getWealthIndex</span><span class="token punctuation">(</span>allItems<span class="token punctuation">)</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span> <span class="token keyword">as</span> Player<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">await</span> <span class="token builtin">Promise</span><span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span>promises<span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      Sentry<span class="token punctuation">.</span><span class="token function">captureException</span><span class="token punctuation">(</span>err<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    players<span class="token operator">:</span> leaderboardPlayers<span class="token punctuation">.</span><span class="token function">sort</span><span class="token punctuation">(</span>sortByWealthIndexDesc<span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">You can probably already make some guesses about the improvements that can be made. Let’s talk through the mistakes.</p><h3 class="post__h3">Mistake #1: I didn’t test with production-like data</h3><p class="post__p">When I wrote the leaderboard API code and tested it in <b class="post__p--bold">development</b>, it didn’t feel slow. It was only when I pushed the feature to production that I noticed how slow it really was. My development database contained less than 20% of the game data compared to production — and it wasn’t growing — whereas my production database was growing hour by hour, adding around 30 items and 10 players to the world per hour.</p><h3 class="post__h3">Mistake #2: “helper” functions aren’t always helpful</h3><p class="post__p">In trying to be a diligent and DRY (Don’t Repeat Yourself) developer, I reused an existing “helper function” — <code>getAllItemsForPlayer()</code> — which calculates a player’s wealth index. The problem is <code>getAllItemsForPlayer()</code> made a call to the <code>Items</code> collection to get inventory items for every... single ...player. If this code were written <b class="post__p--bold">inline</b> (and not abstracted out to another function), it would have been self-documenting that it wasn’t the right call to make <code>n</code> calls to a database (where <code>n</code> is the number of active players) because I’d already queried the <code>Items</code> table at the top of the function. <b class="post__p--italic">I already had access to that data in the first database query.</b></p><h3 class="post__h3">Mistake #3: I copied and pasted code not fit-for-purpose</h3><p class="post__p">Before the full leaderboard API, I had written a small function to find the top three players in the game by item count, which was originally displayed on the home page. It made the following query on the <code>Items</code> collection:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="rMFJCEoZkU"
      aria-describedby="rMFJCEoZkU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="rMFJCEoZkU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="rMFJCEoZkU" itemprop="text" content="const%20top3%20%3D%20await%20PantherworldItemModel.aggregate(%5B%0A%C2%A0%20%7B%20%24match%3A%20%7B%20userId%3A%20%7B%20%24ne%3A%20null%20%7D%20%7D%20%7D%2C%0A%0A%C2%A0%20%2F%2F%20Group%20documents%20by%20userId%2C%20count%20the%20number%20of%20items%20in%20each%20group%0A%C2%A0%20%7B%20%24group%3A%20%7B%20_id%3A%20%22%24userId%22%2C%20itemCount%3A%20%7B%20%24sum%3A%201%20%7D%20%7D%20%7D%2C%0A%0A%C2%A0%20%2F%2F%20Sort%20the%20groups%20by%20itemCount%20in%20descending%20order%0A%C2%A0%20%7B%20%24sort%3A%20%7B%20itemCount%3A%20-1%20%7D%20%7D%2C%0A%0A%C2%A0%20%2F%2F%20Select%20top%203%0A%C2%A0%20%7B%20%24limit%3A%203%20%7D%2C%0A%5D)%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token keyword">const</span> top3 <span class="token operator">=</span> <span class="token keyword">await</span> PantherworldItemModel<span class="token punctuation">.</span><span class="token function">aggregate</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br>  <span class="token punctuation">{</span> $match<span class="token operator">:</span> <span class="token punctuation">{</span> userId<span class="token operator">:</span> <span class="token punctuation">{</span> $ne<span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>  <span class="token comment">// Group documents by userId, count the number of items in each group</span><br>  <span class="token punctuation">{</span> $group<span class="token operator">:</span> <span class="token punctuation">{</span> _id<span class="token operator">:</span> <span class="token string">"$userId"</span><span class="token punctuation">,</span> itemCount<span class="token operator">:</span> <span class="token punctuation">{</span> $sum<span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>  <span class="token comment">// Sort the groups by itemCount in descending order</span><br>  <span class="token punctuation">{</span> $sort<span class="token operator">:</span> <span class="token punctuation">{</span> itemCount<span class="token operator">:</span> <span class="token operator">-</span><span class="token number">1</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>  <span class="token comment">// Select top 3</span><br>  <span class="token punctuation">{</span> $limit<span class="token operator">:</span> <span class="token number">3</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">When you compare this code to the first query in the original <code>getFullLeaderboard()</code> function above, it’s the same code, just with the <code>$sort</code> and the <code>$limit</code>. Big mistake. In querying the <code>Items</code> collection in the full leaderboard API, I already had access to <b class="post__p--italic">all item data</b>, meaning I didn’t need to loop through the grouped array and use the “helper” function to <code>getAllItemsForPlayer()</code>. Instead, by using the <code>$push</code> operator in the <code>Items</code> collection query, I could create an array of objects mapping user IDs to an array of items owned by that user, like so:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="tbhdCbBJzw"
      aria-describedby="tbhdCbBJzw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="tbhdCbBJzw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="tbhdCbBJzw" itemprop="text" content="const%20itemsGroupedByPlayer%20%3D%20await%20PantherworldItemModel.aggregate(%5B%0A%C2%A0%20%C2%A0%20%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20%24match%3A%20%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20%C2%A0%20userId%3A%20%7B%20%24ne%3A%20null%20%7D%2C%0A%C2%A0%20%C2%A0%20%C2%A0%20%7D%2C%0A%C2%A0%20%C2%A0%20%7D%2C%0A%0A%C2%A0%20%C2%A0%20%2F%2F%20Group%20documents%20by%20userId%20and%20push%20all%20items%20into%20an%20array%0A%C2%A0%20%C2%A0%20%7B%20%24group%3A%20%7B%20_id%3A%20%22%24userId%22%2C%20items%3A%20%7B%20%24push%3A%20%22%24%24ROOT%22%20%7D%20%7D%20%7D%2C%0A%C2%A0%20%5D)%3B%0A%0A%C2%A0%20%2F%2F%20Returns%20an%20array%20of%20objects%20shaped%20like%20this%3A%0A%C2%A0%20%2F%2F%20%7B%20_id%3A%20'12345'%2C%20items%3A%20%5B...%5D%20%7D%2C">
      <pre class="language-typescript"><code class="language-typescript"><span class="token keyword">const</span> itemsGroupedByPlayer <span class="token operator">=</span> <span class="token keyword">await</span> PantherworldItemModel<span class="token punctuation">.</span><span class="token function">aggregate</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      $match<span class="token operator">:</span> <span class="token punctuation">{</span><br>        userId<span class="token operator">:</span> <span class="token punctuation">{</span> $ne<span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>    <span class="token comment">// Group documents by userId and push all items into an array</span><br>    <span class="token punctuation">{</span> $group<span class="token operator">:</span> <span class="token punctuation">{</span> _id<span class="token operator">:</span> <span class="token string">"$userId"</span><span class="token punctuation">,</span> items<span class="token operator">:</span> <span class="token punctuation">{</span> $push<span class="token operator">:</span> <span class="token string">"$$ROOT"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Returns an array of objects shaped like this:</span><br>  <span class="token comment">// { _id: '12345', items: [...] },</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">How I reduced the API call from &gt;5 seconds to under 100ms</h2><p class="post__p">The main culprit in this slow API call was the “helper” function that called the database n times, where <code>n</code> is the number of players. I removed these extra <code>n</code> database queries by fetching all data from the <code>Items</code> collection once and using <code>$push</code> to group the data I needed (rather than discarding it).</p><p class="post__p">There was, however, some more work I had to do to build up an array of items in a particular format to be able to reuse <b class="post__p--italic">another helper function</b> which calculates a player’s wealth index. But it was worth it. The leaderboard function now only makes two calls to the database: one to the <code>Items</code> collection, and one to the <code>Players</code> collection. Here’s the refactored code, simplified for brevity:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="roJuhmCJwV"
      aria-describedby="roJuhmCJwV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="roJuhmCJwV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="roJuhmCJwV" itemprop="text" content="%2F%2F%20leaderboard.ts%0A%0A%2F%2F%20imports...%0A%0Afunction%20constructItemsForWealthIndexCalculation(%0A%C2%A0%20items%2C%0A)%3A%20ItemForWealthIndex%5B%5D%20%7B%0A%09%2F%2F%20...%20%0A%09%2F%2F%20this%20function%20sorts%20the%20raw%20items%20data%20into%20something%20usable%0A%09%2F%2F%20for%20the%20helper%20function%20getWealthIndex()%0A%0A%C2%A0%20return%20itemsForWealthIndex%3B%0A%7D%0A%0Afunction%20sortByWealthIndexDesc(a%3A%20Player%2C%20b%3A%20Player)%20%7B%0A%C2%A0%20%2F%2F%20...%20standard%20sort%20function%0A%7D%0A%0Aexport%20async%20function%20getFullLeaderboard()%3A%20Promise%3C%7Bplayers%3A%20Player%5B%5D%7D%3E%20%7B%0A%C2%A0%20const%20itemsGroupedByPlayer%20%3D%20await%20PantherworldItemModel.aggregate(%5B%0A%C2%A0%20%C2%A0%20%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20%24match%3A%20%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20%C2%A0%20userId%3A%20%7B%20%24ne%3A%20null%20%7D%2C%0A%C2%A0%20%C2%A0%20%C2%A0%20%7D%2C%0A%C2%A0%20%C2%A0%20%7D%2C%0A%0A%C2%A0%20%C2%A0%20%2F%2F%20Group%20documents%20by%20userId%20and%C2%A0%0A%C2%A0%C2%A0%C2%A0%C2%A0%2F%2F%20push%20all%20items%20with%20that%20userId%20into%20an%20array%0A%C2%A0%20%C2%A0%20%7B%20%24group%3A%20%7B%20_id%3A%20%22%24userId%22%2C%20items%3A%20%7B%20%24push%3A%20%22%24%24ROOT%22%20%7D%20%7D%20%7D%2C%0A%C2%A0%20%5D)%3B%0A%0A%C2%A0%20const%20leaderboardPlayers%3A%20Player%5B%5D%20%3D%20%5B%5D%3B%0A%C2%A0%C2%A0%2F%2F%20get%20all%20player%20data%20in%20one%20query%20using%20the%20%24in%20operator%0A%C2%A0%20const%20allPlayerRecords%20%3D%20await%20PantherworldPlayerModel.find(%7B%0A%C2%A0%20%C2%A0%20userId%3A%20%7B%20%24in%3A%20itemsGroupedByPlayer.map((player)%20%3D%3E%20player._id)%20%7D%2C%0A%C2%A0%20%7D)%3B%0A%0A%C2%A0%20for%20(const%20player%20of%20itemsGroupedByPlayer)%20%7B%0A%C2%A0%20%C2%A0%20const%20itemsForWealthIndex%20%3D%20constructItemsForWealthIndexCalculation(%0A%C2%A0%20%C2%A0%20%C2%A0%20player.items%2C%0A%C2%A0%20%C2%A0%20)%3B%0A%0A%C2%A0%20%C2%A0%20leaderboardPlayers.push(%7B%0A%C2%A0%20%C2%A0%20%C2%A0%20username%3A%20allPlayerRecords.find(%0A%C2%A0%20%C2%A0%20%C2%A0%20%C2%A0%20(playerRecord)%20%3D%3E%20playerRecord.userId%20%3D%3D%3D%20player._id%2C%0A%C2%A0%20%C2%A0%20%C2%A0%20).userDisplayName%2C%0A%C2%A0%20%C2%A0%20%C2%A0%20items%3A%20player.items.length%2C%0A%C2%A0%20%C2%A0%20%C2%A0%20wealth_index%3A%20getWealthIndex(itemsForWealthIndex)%2C%0A%C2%A0%20%C2%A0%20%7D%20as%20Player)%3B%0A%C2%A0%20%7D%0A%0A%C2%A0%20return%20%7B%0A%C2%A0%20%C2%A0%20players%3A%20leaderboardPlayers.sort(sortByWealthIndexDesc)%2C%0A%C2%A0%20%7D%3B%0A%7D%0A">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// leaderboard.ts</span><br><br><span class="token comment">// imports...</span><br><br><span class="token keyword">function</span> <span class="token function">constructItemsForWealthIndexCalculation</span><span class="token punctuation">(</span><br>  items<span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token operator">:</span> ItemForWealthIndex<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><br>	<span class="token comment">// ... </span><br>	<span class="token comment">// this function sorts the raw items data into something usable</span><br>	<span class="token comment">// for the helper function getWealthIndex()</span><br><br>  <span class="token keyword">return</span> itemsForWealthIndex<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">sortByWealthIndexDesc</span><span class="token punctuation">(</span>a<span class="token operator">:</span> Player<span class="token punctuation">,</span> b<span class="token operator">:</span> Player<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// ... standard sort function</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getFullLeaderboard</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator">&lt;</span><span class="token punctuation">{</span>players<span class="token operator">:</span> Player<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token operator">></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> itemsGroupedByPlayer <span class="token operator">=</span> <span class="token keyword">await</span> PantherworldItemModel<span class="token punctuation">.</span><span class="token function">aggregate</span><span class="token punctuation">(</span><span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      $match<span class="token operator">:</span> <span class="token punctuation">{</span><br>        userId<span class="token operator">:</span> <span class="token punctuation">{</span> $ne<span class="token operator">:</span> <span class="token keyword">null</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>    <span class="token comment">// Group documents by userId and </span><br>    <span class="token comment">// push all items with that userId into an array</span><br>    <span class="token punctuation">{</span> $group<span class="token operator">:</span> <span class="token punctuation">{</span> _id<span class="token operator">:</span> <span class="token string">"$userId"</span><span class="token punctuation">,</span> items<span class="token operator">:</span> <span class="token punctuation">{</span> $push<span class="token operator">:</span> <span class="token string">"$$ROOT"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> leaderboardPlayers<span class="token operator">:</span> Player<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>  <span class="token comment">// get all player data in one query using the $in operator</span><br>  <span class="token keyword">const</span> allPlayerRecords <span class="token operator">=</span> <span class="token keyword">await</span> PantherworldPlayerModel<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    userId<span class="token operator">:</span> <span class="token punctuation">{</span> $<span class="token keyword">in</span><span class="token operator">:</span> itemsGroupedByPlayer<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span>player<span class="token punctuation">)</span> <span class="token operator">=></span> player<span class="token punctuation">.</span>_id<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> player <span class="token keyword">of</span> itemsGroupedByPlayer<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> itemsForWealthIndex <span class="token operator">=</span> <span class="token function">constructItemsForWealthIndexCalculation</span><span class="token punctuation">(</span><br>      player<span class="token punctuation">.</span>items<span class="token punctuation">,</span><br>    <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    leaderboardPlayers<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      username<span class="token operator">:</span> allPlayerRecords<span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><br>        <span class="token punctuation">(</span>playerRecord<span class="token punctuation">)</span> <span class="token operator">=></span> playerRecord<span class="token punctuation">.</span>userId <span class="token operator">===</span> player<span class="token punctuation">.</span>_id<span class="token punctuation">,</span><br>      <span class="token punctuation">)</span><span class="token punctuation">.</span>userDisplayName<span class="token punctuation">,</span><br>      items<span class="token operator">:</span> player<span class="token punctuation">.</span>items<span class="token punctuation">.</span>length<span class="token punctuation">,</span><br>      wealth_index<span class="token operator">:</span> <span class="token function">getWealthIndex</span><span class="token punctuation">(</span>itemsForWealthIndex<span class="token punctuation">)</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span> <span class="token keyword">as</span> Player<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    players<span class="token operator">:</span> leaderboardPlayers<span class="token punctuation">.</span><span class="token function">sort</span><span class="token punctuation">(</span>sortByWealthIndexDesc<span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s how the trace view looks after the optimizations: now under 100ms! I call that a success.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5VKeb51xCjMa269nYLnNwZ/8c9883b7237e22d7d47575816bf06791/4_after.png" alt="A single trace view in Sentry, now showing only four spans that touch the database. The total length of the trace is now only 76 milliseconds," height="825" width="1280" /><h2 class="post__h2">The bottom line: tracing takes out the guesswork when debugging performance issues</h2><p class="post__p">Without tracing, I probably could have messed around in my code for a while to find the root cause of the performance bottleneck. However, <b class="post__p--italic">with</b> tracing, what was causing the slowdown was obvious; Sentry showed me a clear visual representation of what was going wrong. As developers, we’ve all got jobs to do, and we’ve all (probably) got deadlines as well. Tracing is one of those things that helps us get our jobs done. And by taking out the guesswork, we can get our jobs done well and efficiently, giving us more time to go and touch grass.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Paths to business value in Developer Relations</title>
          <description>This post is not about measuring success in DevRel, but about making an impact in a way *only* DevRel can do, and *should* be doing. </description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/defining-paths-to-business-value-in-developer-relations/</link>
          <guid>https://whitep4nth3r.com/blog/defining-paths-to-business-value-in-developer-relations/</guid>
          <pubDate>Tue, 26 Nov 2024 00:00:00 GMT</pubDate>
          <category>DevRel Meta</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">After over six years as a front end developer and tech lead in agencies, startups and e-commerce, I side-stepped to Developer Relations (DevRel) in 2021. At the time of writing, I have been employed as an Individual Contributor (IC) in DevRel for four years; I’ve worked at Contentful (2021), Netlify (2022-2023), and Sentry (2023 - present).</p><h2 class="post__h2">What is DevRel, exactly?</h2><p class="post__p">DevRel is often a tricky sub-industry to define within the technology industry itself because its function looks different (and should be different) depending on the business in which it operates. To further complicate the definition, DevRel is the umbrella under which three further distinct functions can be defined: Developer Marketing, Developer Education, and Developer Experience. Due to my own experience, this article leans more heavily to discussing Developer Education and Developer Experience.</p><p class="post__p">The activities and output of a member of a DevRel team (in any of the three categories) can include, but is not limited to:</p><ul><li><p class="post__p">technical writing (e.g. documentation)</p></li><li><p class="post__p">one-off technical tutorials (written or recorded videos)</p></li><li><p class="post__p">multi-part technical courses (written or recorded videos)</p></li><li><p class="post__p">technical workshops</p></li><li><p class="post__p">example code and projects</p></li><li><p class="post__p">contributing to Open Source projects</p></li><li><p class="post__p">QA/testing for (new) product features</p></li><li><p class="post__p">hosting, speaking at, or attending tech conferences and meet-ups</p></li><li><p class="post__p">hosting hackathons</p></li><li><p class="post__p">social, advertising, event copy, and other forms of short-form content geared toward marketing a product</p></li><li><p class="post__p">community support and troubleshooting</p></li><li><p class="post__p">supporting paying customers with technical solutions</p></li><li><p class="post__p">supporting a sales team</p></li></ul><p class="post__p">A DevRel audience consumes only the output of <b class="post__p--italic"><b class="post__p--bold">what</b></b> a DevRel team creates, often without the context of the <b class="post__p--italic"><b class="post__p--bold">how</b></b> or the <b class="post__p--italic"><b class="post__p--bold">why</b></b><b class="post__p--italic">.</b> As a result, DevRel activities are often reduced to “content creation” within a marketing context, which positions DevRel as a task-based function — both internally and externally to a business — without holistically considering the impact and value of said tasks.</p><h2 class="post__h2">DevRel is not content creation</h2><p class="post__p">Throughout my DevRel career, the focus of a DevRel team has rarely centred on “content creation” for the sake of itself. In my lived experience, core DevRel activities have always emerged from a desire to <b class="post__p--bold"><b class="post__p--italic">help developers solve problems and build better software</b></b> — regardless of whether or not they are actually using a particular product or piece of tech. The output of these core activities are often <b class="post__p--italic">content</b> (written, recorded, code samples, live video), but that content is nearly always a side-effect of <b class="post__p--italic"><b class="post__p--bold">building real apps with real users</b></b>.<b class="post__p--italic"><b class="post__p--bold"> </b></b>To summarise, at the core of DevRel is:</p><ul><li><p class="post__p">helping developers solve problems and build better software</p></li><li><p class="post__p">building real apps with real users</p></li></ul><p class="post__p">However, it is incredibly difficult to <b class="post__p--bold">quantitatively</b> measure <b class="post__p--italic">how much you have helped a developer solve problems and build better software</b>. Sure, you could collect qualitative testimonials and excited replies on social media; trust in you will grow over time and people will choose to actively seek out your resources, opinions and expertise. Your earned reputation as a technical representative of a company will contribute to how much developers trust said company, and its products. This is great for the ✨vibes✨, but this doesn’t translate clearly and effectively to <b class="post__p--italic"><b class="post__p--bold">quantitative value</b></b>. </p><p class="post__p">And what is quantitative value to a business? Money. Sign ups. Paying customers. Line graphs go up. Return on investment for shareholders. Stonks. 📈</p><h2 class="post__h2">How can DevRel provide business value?</h2><p class="post__p">Providing business value in a DevRel team does not mean you must dedicate your activities and output to gaining sign ups and paying customers <b class="post__p--italic">only</b>. But what it does mean, is that:</p><blockquote class="post__blockquote"><p class="post__p">Most DevRel activities must define a path by which they contribute towards generating monetary value for a business.</p></blockquote><p class="post__p">We cannot reduce this to simply adding a “sign up” link to every piece of content or activity and hoping for the best. And it also doesn’t mean that you can’t be creative and have fun; because what we <b class="post__p--italic">can</b> group all activities under is <b class="post__p--italic">building real apps with real users.</b></p><h2 class="post__h2">Defining paths to monetary value by building real apps with real users</h2><p class="post__p">Given DevRel teams are often small in size, building real projects is an activity often sacrificed for churning out content that provides an immediate quantitative measure of “success”, i.e. <b class="post__p--italic">we shipped 5 blog posts this month</b>. But building real projects that have real users is really the only way you can experience a product just as the customer does, so you can find the same pain-points they do. And because you work for a software company, you have a direct line to the teams and engineers who build the product, and you can advocate for making it better for users, so they don’t stop paying for the product (churn). When I worked with <a href="https://bsky.app/profile/philhawksworth.dev" target="_blank">Phil Hawksworth</a> at Netlify, he referred to a DevRel team as “the bridge between the customers and the engineers”; and I agree.</p><p class="post__p">We can split a DevRel audience into three broad categories of developers or customers: paying users, non-paying users, and non-users. These three categories define three broad business outcomes:</p><ol><li><p class="post__p">Reduce customer churn (lost customers and revenue),</p></li><li><p class="post__p">Increase customer conversion to paid features, and</p></li><li><p class="post__p">Increase new customer sign ups.</p></li></ol><p class="post__p">DevRel activities and deliverables should target multiple categories and serve multiple purposes to demonstrate maximum impact. Let’s take a look at a few examples at how activities can create paths to the broad outcomes defined above. I will use examples that I have experienced personally through being intentional about making an impact in those areas, through <b class="post__p--italic">helping developers solve problems and build better software by building real apps with real users</b>.</p><h2 class="post__h2">1. Reduce customer churn</h2><p class="post__p">To reduce the risk of losing customer accounts, and therefore revenue, DevRel has a role in helping make a product as good as it can possibly be to use. This might often be referred to as the practice of Developer Experience. And in order to understand how the product can be better, you have to <b class="post__p--italic">experience using</b> it.</p><p class="post__p">For a company like Sentry (an application error and performance monitoring SaaS tool), it is particularly important to build real apps that have real users. Unless you want to spend time automating fake user visits and fake user journeys to your app, you need real users to generate real data in a Sentry account. If you don’t collect a broad range of data from how users use your apps on different devices, in different browsers, and in different locations for example, you might not find an edge-case bug that, when fixed, saves a large customer account from churning.</p><p class="post__p">This is one of the reasons I’ve been building <a href="https://p4nth3r.world" target="_blank">Pantherworld</a> at work. Pantherworld is a text-based game, and has been played by hundreds of people, 24 hours a day, 7 days a week via my Twitch chat interface since April 2024. It’s a distributed app (separate back end and front end) built with Express, MongoDB and Nuxt. At first, this project may appear out of scope of the working hours of a Senior Developer Relations Advocate, but the key thing here is that <b class="post__p--bold">Pantherworld uses Sentry</b>. I am constantly dogfooding the product whilst building a real app with real users.</p><h2 class="post__h2">2. Increase customer conversion to paid features</h2><p class="post__p">This path is definitely more about <b class="post__p--italic">marketing to existing customers</b>, but it can still be done through building real apps with real users. In building Pantherworld, I have given many live demonstrations of Sentry in a real-world debugging context, which can encourage viewers who are existing customers to upgrade to more paying features. The same can be said for <b class="post__p--bold">workshops</b>, which are usually delivered by one or more DevRel team members as part of a more formal existing customer outreach programme. When creating and delivering the workshops, you will likely also be building a real app with real users to demonstrate the product effectively, and undertaking another variety of activities detailed below. It&#39;s all about giving a relevant, polished, yet authentic product demo.</p><h2 class="post__h2">3. Increase new customer sign ups</h2><p class="post__p">Increasing new customer sign ups is about widening the net of your influence; and this is where you can get really creative in <b class="post__p--italic">helping developers solve problems and build better software</b> through making the apps you want to make with the tech you want to use. Granted, this path will usually involve a series of broad approaches and experiments, but here are some generic tips that help you widen your net:</p><ul><li><p class="post__p">Learn how to optimise for SEO: if developers can’t find your content, they can’t read it;</p></li><li><p class="post__p">Learn how to tailor your content for different platforms (e.g. social platforms and their purpose);</p></li><li><p class="post__p">Experiment with different media formats and approaches, gather data, iterate, repeat;</p></li><li><p class="post__p">Aim to inspire developers with innovative solutions and attention-grabbing concepts (make it fun); and</p></li><li><p class="post__p">Build trust with developers through building real apps with real users, just like they are.</p></li></ul><h2 class="post__h2">Activities, paths, and overlap</h2><p class="post__p">Here is a non-exhaustive list of some of the DevRel activities I regularly undertake as part of building real apps with real users, the goals each activity have contributed toward, and the defined path to those goals. </p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header">Activity</th><th class="post__table__header">Primary Goal(s)</th><th class="post__table__header">Path</th></tr><tr class="post__table__row"><td class="post__table__cell">Test out and help improve the latest versions of the product and SDKs</td><td class="post__table__cell">Reduce churn • 
Increase conversion • 
Increase sign ups
</td><td class="post__table__cell">Testing and providing feedback on the product and SDKs based on real-world usage identifies opportunities to improve the end-user experience for all, particularly around new and experimental features.</td></tr><tr class="post__table__row"><td class="post__table__cell">Identify areas for improvement on documentation, and contribute where appropriate</td><td class="post__table__cell">Reduce churn • 
Increase conversion • 
Increase sign ups</td><td class="post__table__cell">Improving documentation improves the end-user experience, reducing frustration and pain points. Great documentation is also a huge selling point for new customers.</td></tr><tr class="post__table__row"><td class="post__table__cell">Create written content based on authentic experiences of building a real app with real users that<b class="post__p--italic"> helps developers solve problems and build better software</b></td><td class="post__table__cell">Reduce churn • 
Increase conversion • 
Increase sign ups</td><td class="post__table__cell">Authentic &quot;engineer-to-engineer&quot; content around real-world problems and experiences builds trust with existing and prospective customers.</td></tr><tr class="post__table__row"><td class="post__table__cell">Speak at conferences as a company representative</td><td class="post__table__cell">Reduce churn • 
Increase sign ups</td><td class="post__table__cell">Conferences are a good opportunity to meet with existing customers. I have met with existing and prospective customers at conferences. Through IRL demos or hallway track conversations, you can help customers understand how your company’s software can solve their problems in ways they didn’t think of before.</td></tr><tr class="post__table__row"><td class="post__table__cell">Live streaming and building in public</td><td class="post__table__cell">Increase sign ups • </td><td class="post__table__cell">Demonstrating how to use a product in real-time when building software is a proven way to engage a community in the capabilities of a product, especially in the context of building real apps with real users. </td></tr><tr class="post__table__row"><td class="post__table__cell">Deliver a workshop</td><td class="post__table__cell">Reduce churn • 
Increase conversion</td><td class="post__table__cell">Showcasing the product and/or new paid features to an already captive audience increases the likelihood of conversion to those features.</td></tr><tr class="post__table__row"><td class="post__table__cell">Create social, advertising, event copy, and other forms of short-form content</td><td class="post__table__cell">Increase sign ups</td><td class="post__table__cell">By giving traditional advertising an authentic developer voice, you create opportunities to build excitement around new features and showcase new capabilities.</td></tr><tr class="post__table__row"><td class="post__table__cell">Support paying customers with technical solutions</td><td class="post__table__cell">Reduce churn • 
Increase conversion</td><td class="post__table__cell">During workshops, you can create opportunities to provide 1:1 support to help solve customer problems where appropriate.</td></tr><tr class="post__table__row"><td class="post__table__cell">Write case studies/blog posts/creating videos to highlight particular new features and capabilities</td><td class="post__table__cell">Increase sign ups</td><td class="post__table__cell">Where existing customers don’t attend workshops, authentic content based on real world apps with real users can help shine a light on why it’s worth upgrading to a paid feature.</td></tr><tr class="post__table__row"><td class="post__table__cell">Create content on solving generic technical problems that stem from real apps with real users (i.e. don’t just create content about your product)</td><td class="post__table__cell">Increase sign ups</td><td class="post__table__cell">A developer searches for a solution to a generic problem; they find your content. If it’s helpful, they are likely to put trust in the company you work for and more likely to choose the product when they need it.</td></tr><tr class="post__table__row"><td class="post__table__cell">Repurpose existing content intentionally in different media formats to reach a wider audience</td><td class="post__table__cell">Increase sign ups</td><td class="post__table__cell">Intentional content in different formats (written, audio, video, long content, short content) is more likely to reach a wider net of developers who may not have previously considered or discovered your product.</td></tr><tr class="post__table__row"><td class="post__table__cell">Collaborate with colleagues in more traditional marketing roles (e.g. demand generation) to help more traditional advertising content speak to developers</td><td class="post__table__cell">Increase sign ups</td><td class="post__table__cell">Developers trust, listen to, and buy from developers. If your company’s intentional advertising speaks to developers when you lend it your voice, you are more likely to increase new customer sign ups.</td></tr></tbody>
      </table></div><p class="post__p">Each activity is intentionally designed to <b class="post__p--italic">help developers solve problems and build better software through building real apps with real users</b>. In doing this you are able to:</p><ul><li><p class="post__p">Engage with developers (potential paying customers) authentically and effectively by speaking their language and sharing experiences,</p></li><li><p class="post__p">Use your technical expertise and experience gained through building real apps to help developers solve problems and build better software with innovative solutions in new contexts, and</p></li><li><p class="post__p">Build trust in the community as a credible technologist who builds real apps with real users.</p></li></ul><h2 class="post__h2">Closing thoughts</h2><p class="post__p">DevRel is not content creation. DevRel is not about views, engagement, or followers; although if those metrics are higher, you may increase your probability of reaching more developers. But that has little to no business value if your offering doesn’t define a path to reducing customer churn, increasing conversion to paid features, or increasing new customer sign ups, at the same time as building trust with the developer community. When all is said and done, developers want to use and pay for good tech that <b class="post__p--italic">helps them solve problems and build better software</b>. DevRel has an important part to play in ensuring that what a company is selling is good, and that product features are communicated effectively and authentically, borne of real-world technical experiences.</p><p class="post__p">I believe that if you maintain a primary focus on <b class="post__p--italic">helping developers solve problems and build better software through building real apps for real users</b>, you <b class="post__p--italic">will</b> create paths to monetary value. And crucially, if you can <b class="post__p--italic">attribute</b> those paths to monetary value to something you did or made or said, you’ll be able to demonstrate quantitatively how DevRel is adding value to a business. However, attribution is company-specific: specific to how the <b class="post__p--italic">company</b> measures success, and specific to how the DevRel team at the company measures success. Attribution is also specific to the tools available to the team or company in being able to measure success. <b class="post__p--bold">This post isn’t about measuring success, it’s about making an </b><b class="post__p--bold"><b class="post__p--italic">impact</b></b><b class="post__p--bold"> in a way </b><b class="post__p--bold"><b class="post__p--italic">only</b></b><b class="post__p--bold"> DevRel can do, and </b><b class="post__p--bold"><b class="post__p--italic">should</b></b><b class="post__p--bold"> be doing.</b></p><p class="post__p">The approach to “doing DevRel” in this article works to create paths to business value; I know it does — it has worked for me and my excellent colleagues, past and present. And in 2025, I’ll be working on a framework with <a href="https://bsky.app/profile/guthals.com" target="_blank">Sarah Guthals</a> in which DevRel can define specific <b class="post__p--italic"><b class="post__p--bold">attributable</b></b> paths to real quantitative business value.</p><p class="post__p">Stay tuned.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I show Bluesky likes on my blog posts</title>
          <description>Learn how to use the Bluesky API to show likes and Bluesky user avatars on your blog posts when you share them on Bluesky.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/show-bluesky-likes-on-blog-posts/</link>
          <guid>https://whitep4nth3r.com/blog/show-bluesky-likes-on-blog-posts/</guid>
          <pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I’m really enjoying <a href="https://bsky.app/profile/whitep4nth3r.com" target="_blank">spending time on Bluesky</a> right now. One of the things I really enjoy about the whole experience is that the project is pretty much Open Source, people are building some really cool things with the platform, and there are some nice APIs to have fun with.</p><p class="post__p">I’m familiar with the <a href="https://indieweb.org/Webmention" target="_blank">Webmentions standard</a> and how it can be used to facilitate cross-site conversations by showing data such as likes and comments/replies to links on the internet. I worked with Webmentions a few years ago to display Webmention data from other social media platforms on my site. However, it often felt like a lot of hoops to jump through, when you can just get some data from an API.</p><p class="post__p">In this post, we’re going to use the Bluesky API to fetch a collection of avatars of users who have liked a Bluesky post that you have associated with a public blog post, so you can display something that looks like this on your website.</p><img src="https://images.ctfassets.net/56dzm01z6lln/awnWpYcQandKgPrHTrxJD/bb016146c012b1651cc62c171d46cc74/bsky_likes.png" alt="🦋 203 likes on Bluesky. Underlined link: like this post on bluesky to see your face on this page. Followed by four rows of 15 circular avatars, overlapping each other. The final avatar is a blue circle which white text that says +144." height="539" width="984" /><h2 class="post__h2">The workflow</h2><p class="post__p">Given this website is a static site built with Eleventy, it requires a few steps to associate a published blog post with a Bluesky post.</p><ol><li><p class="post__p">Publish a blog post, which triggers a static site build</p></li><li><p class="post__p">Publish a Bluesky post which links to the published blog post</p></li><li><p class="post__p">Associate the ID of the Bluesky post with that published blog post (in a CMS, for example)</p></li><li><p class="post__p">Re-build the site</p></li><li><p class="post__p">Profit</p></li></ol><h2 class="post__h2">Technical choices</h2><p class="post__p">When building this component, I made some very deliberate technical choices based on the desired user-experience, and some important performance considerations.</p><h3 class="post__h3">I used client-side JavaScript</h3><p class="post__p">This website is a static site that uses client-side JavaScript sparingly. The JavaScript code for this functionality runs on my blog page templates conditionally if a blog post has a Bluesky Post ID associated with it.</p><p class="post__p">Alternatives to this approach would be to (in my case) use an Edge Function to modify the static HTML response at request time, but in the past I have had performance issues with calling third-party APIs in this way, such as a slower Time to First Byte (TTFB) than desired. Read <a href="https://whitep4nth3r.com/blog/how-i-fixed-my-brutal-ttfb/" target="_blank">How I Fixed my Brutal TTFB</a> for more context.</p><p class="post__p">Additionally, this feature on my website is a <a href="https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement" target="_blank">progressive enhancement</a>, and the function of the page is not dependent on showing Bluesky likes. Therefore, if calls to the Bluesky API fail on the client, it doesn’t matter, and we can clean up the DOM appropriately. If we were running this same code on a server, it could block the rendering of the page (without proper error handling, at least), and the post wouldn’t get read. Big shame.</p><p class="post__p">With my site being a static site, technically I could fetch the Bluesky data at <b class="post__p--italic">build time</b> and render the data statically on each blog post. However, I wanted this feature to bring joy by being a near real-time interactive experience. And plus, it wouldn’t be ideal to re-build my website every minute or so, to keep the data in sync.</p><h3 class="post__h3">Optimising for performance</h3><p class="post__p">Given we are loading <code>n</code> third-party images (user avatars), the size of the images is important. Fortunately, the Bluesky API provides at least two image sizes for each user, and we want to use the smallest one.</p><p class="post__p">Additionally, given we are loading <code>n</code> images and we don’t know how long they will take to load or how much of an effect they will have on the page layout, some considerations have been made to avoid Cumulative Layout Shift (CLS) as much as possible. These will be outlined alongside the code examples below.</p><h2 class="post__h2">Prerequisites to show Bluesky likes on your blog posts</h2><ol><li><p class="post__p">A Bluesky account</p></li><li><p class="post__p">A website</p></li><li><p class="post__p">Some blog posts</p></li><li><p class="post__p">A way to store a Bluesky post ID with your blog post data (e.g. if you write your blogs in markdown, store the post ID in your front matter; if you’re using a CMS, add a field to your blog post content model, etc)</p></li></ol><h2 class="post__h2">The code</h2><p class="post__p">Let’s take a look at the HTML, CSS and JavaScript that makes the magic happen.</p><h3 class="post__h3">The HTML</h3><p class="post__p">The HTML is contained within a <code>section</code> element. This component contains:</p><ul><li><p class="post__p">an <code>h3</code> element, which will be populated with the total number of likes (your heading level element may vary),</p></li><li><p class="post__p">a link to the Bluesky post to encourage people to like it, and</p></li><li><p class="post__p">an empty <code>ul</code> element, ready to be filled with Bluesky avatars.</p></li></ul><p class="post__p">For the CSS classes I’m using <a href="https://whitep4nth3r.com/blog/what-is-bem-in-css/" target="_blank">BEM syntax</a>, but you can use whatever CSS system you prefer. To target the DOM elements in JavaScript I’m using data-attributes prefixed with <code>data-bsky</code>; you can target DOM elements using CSS classes in JavaScript, but I prefer to use data-attributes to separate concerns. You could even use IDs on the elements and target those with JavaScript if you wish.</p><p class="post__p">The <code>bskyPostId</code> associated with a blog post is added into a data-attribute on a <code>meta</code> tag next to this component. This is purely unique to my set-up, given that I’m building a static site, and need access to a build-time variable on the client-side in a separate JavaScript file. You may have access to your <code>bskyPostId</code> in your app state, for example, if you’re using a different framework. Edit as you see fit.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DgunUPzIOT"
      aria-describedby="DgunUPzIOT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DgunUPzIOT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="DgunUPzIOT" itemprop="text" content="%3Cmeta%20data-bsky-post-id%3D%22%24%7Bpost.bskyPostId%7D%22%20%2F%3E%0A%0A%3Csection%20class%3D%22post__likes%22%20data-bsky-container%3E%0A%20%20%3Ch3%20class%3D%22post__likesTitle%22%3E%0A%20%20%20%20%F0%9F%A6%8B%20%3Cspan%20data-bsky-likes-count%3E%3C%2Fspan%3E%20likes%20on%20Bluesky%0A%20%20%3C%2Fh3%3E%0A%20%20%3Ca%20class%3D%22post__likesCta%22%20href%3D%22https%3A%2F%2Fbsky.app%2Fprofile%2F%7Bhandle%7D%2Fpost%2F%24%7BpostId%7D%22%20target%3D%22_blank%22%3E%0A%09%20%20Like%20this%20post%20on%20Bluesky%20to%20see%20your%20face%20on%20this%20page%0A%20%20%3C%2Fa%3E%0A%20%20%3Cul%20data-bsky-likes%20class%3D%22post__likesList%22%3E%3C%2Ful%3E%0A%3C%2Fsection%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">data-bsky-post-id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>${post.bskyPostId}<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>post__likes<span class="token punctuation">"</span></span> <span class="token attr-name">data-bsky-container</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h3</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>post__likesTitle<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    🦋 <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">data-bsky-likes-count</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span> likes on Bluesky<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h3</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>post__likesCta<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://bsky.app/profile/{handle}/post/${postId}<span class="token punctuation">"</span></span> <span class="token attr-name">target</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>_blank<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>	  Like this post on Bluesky to see your face on this page<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>ul</span> <span class="token attr-name">data-bsky-likes</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>post__likesList<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>ul</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">The CSS</h3><p class="post__p">The CSS you see here has been slightly modified from my implementation to avoid you having to use my custom properties and personal spacing preferences. Please add what you need to make your implementation right for you.</p><p class="post__p">I’d like to call out the magic number <code>min-height: 400px</code> on the parent container class, <code>.post__likes</code>; this is to maintain a fixed height of at least 400px for the element on page load, so that the avatars don&#39;t shift the page content around as they gradually load in (the container will expand vertically on mobile). This is to prevent a bad CLS score. In the JavaScript code below, you’ll notice that I’ve specified a limit on the number of avatars fetched, based on how many avatars will fit comfortably inside this fixed-height container.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YfWfPLPRfq"
      aria-describedby="YfWfPLPRfq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YfWfPLPRfq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="YfWfPLPRfq" itemprop="text" content=".post__likes%20%7B%0A%20%20%2F*%20to%20avoid%20CLS%20as%20much%20as%20possible!%20*%2F%0A%20%20min-height%3A%20400px%3B%20%0A%20%20%2F*%20to%20prevent%20negative%20margin%20from%20allowing%20avatars%20to%20spill%20out%20of%20container%20*%2F%0A%20%20padding-right%3A%201rem%3B%0A%7D%0A%0A.post__likesTitle%20%7B%0A%20%20font-size%3A%202rem%3B%0A%20%20color%3A%20%23000%3B%0A%7D%0A%0A.post__likesCta%20%7B%0A%20%20color%3A%20%20%23000%3B%0A%20%20font-size%3A%201.25rem%3B%0A%20%20font-style%3A%20italic%3B%0A%20%20display%3A%20block%3B%0A%7D%0A%0A.post__likesList%20%7B%0A%20%20list-style%3A%20none%3B%0A%20%20padding%3A%200%3B%0A%20%20display%3A%20flex%3B%0A%20%20flex-direction%3A%20row%3B%0A%20%20flex-wrap%3A%20wrap%3B%0A%7D%0A%0A.post__like%20%7B%0A%20%20width%3A%204rem%3B%0A%20%20aspect-ratio%3A%201%2F1%3B%0A%20%20margin-right%3A%20-1rem%3B%0A%20%20border-radius%3A%20100%25%3B%0A%20%20filter%3A%20drop-shadow(0px%200.125rem%200.125rem%20rgba(0%2C%200%2C%200%2C%200.25))%3B%0A%7D%0A%0A.post__like__avatar%20%7B%0A%20%20border-radius%3A%20100%25%3B%0A%7D%0A%0A.post__like--placeholder%2C%0A.post__like--howManyMore%20%7B%0A%20%20width%3A%204rem%3B%0A%20%20aspect-ratio%3A%201%2F1%3B%0A%20%20display%3A%20flex%3B%0A%20%20justify-content%3A%20center%3B%0A%20%20align-items%3A%20center%3B%0A%20%20font-size%3A%201rem%3B%0A%20%20font-weight%3A%20bold%3B%0A%20%20font-style%3A%20italic%3B%0A%20%20background-color%3A%20%23208bfe%3B%20%2F*%20Bluesky%20blue%20*%2F%0A%20%20color%3A%20%23fff%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.post__likes</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* to avoid CLS as much as possible! */</span><br>  <span class="token property">min-height</span><span class="token punctuation">:</span> 400px<span class="token punctuation">;</span> <br>  <span class="token comment">/* to prevent negative margin from allowing avatars to spill out of container */</span><br>  <span class="token property">padding-right</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.post__likesTitle</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 2rem<span class="token punctuation">;</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> #000<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.post__likesCta</span> <span class="token punctuation">{</span><br>  <span class="token property">color</span><span class="token punctuation">:</span>  #000<span class="token punctuation">;</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 1.25rem<span class="token punctuation">;</span><br>  <span class="token property">font-style</span><span class="token punctuation">:</span> italic<span class="token punctuation">;</span><br>  <span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.post__likesList</span> <span class="token punctuation">{</span><br>  <span class="token property">list-style</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span><br>  <span class="token property">padding</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br>  <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span><br>  <span class="token property">flex-direction</span><span class="token punctuation">:</span> row<span class="token punctuation">;</span><br>  <span class="token property">flex-wrap</span><span class="token punctuation">:</span> wrap<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.post__like</span> <span class="token punctuation">{</span><br>  <span class="token property">width</span><span class="token punctuation">:</span> 4rem<span class="token punctuation">;</span><br>  <span class="token property">aspect-ratio</span><span class="token punctuation">:</span> 1/1<span class="token punctuation">;</span><br>  <span class="token property">margin-right</span><span class="token punctuation">:</span> -1rem<span class="token punctuation">;</span><br>  <span class="token property">border-radius</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br>  <span class="token property">filter</span><span class="token punctuation">:</span> <span class="token function">drop-shadow</span><span class="token punctuation">(</span>0px 0.125rem 0.125rem <span class="token function">rgba</span><span class="token punctuation">(</span>0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0<span class="token punctuation">,</span> 0.25<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.post__like__avatar</span> <span class="token punctuation">{</span><br>  <span class="token property">border-radius</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.post__like--placeholder,<br>.post__like--howManyMore</span> <span class="token punctuation">{</span><br>  <span class="token property">width</span><span class="token punctuation">:</span> 4rem<span class="token punctuation">;</span><br>  <span class="token property">aspect-ratio</span><span class="token punctuation">:</span> 1/1<span class="token punctuation">;</span><br>  <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span><br>  <span class="token property">justify-content</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span><br>  <span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><br>  <span class="token property">font-weight</span><span class="token punctuation">:</span> bold<span class="token punctuation">;</span><br>  <span class="token property">font-style</span><span class="token punctuation">:</span> italic<span class="token punctuation">;</span><br>  <span class="token property">background-color</span><span class="token punctuation">:</span> #208bfe<span class="token punctuation">;</span> <span class="token comment">/* Bluesky blue */</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> #fff<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">The JavaScript</h3><p class="post__p">Disclaimer: this code is provided in plain JavaScript; you may adapt this code to your own website framework should you wish, but the beauty of writing this in plain JavaScript is that you can use it on any front end as it is.</p><p class="post__p">First, you’ll need to define a few variables. The <code>LIMIT</code> specifies the maximum number of avatars you want to display on your page depending on how you want to display them. My limit is set to <code>55</code> because that’s how many avatars fit nicely on four rows (with extra space to display how many more likes there are). The maximum number of avatars you can fetch with this API endpoint is 100.</p><p class="post__p">The <code>bskyPostId</code> is grabbed from the <code>meta</code> tag as described in the HTML section above (you may need to do this differently depending on your framework and existing code).</p><p class="post__p">In order to modify the DOM after fetching data, we need to access the <code>container</code>, <code>likesContainer</code> and <code>likesCount</code> elements using <code>document.querySelector()</code>.</p><p class="post__p">Replace the value of <code>myDid</code> with your own <a href="https://docs.bsky.app/docs/advanced-guides/resolving-identities" target="_blank">Bluesky DID</a>. And everything else is good to go.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="qtxNgrkejf"
      aria-describedby="qtxNgrkejf">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="qtxNgrkejf">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="qtxNgrkejf" itemprop="text" content="const%20LIMIT%20%3D%2055%3B%0A%0Aconst%20bskyPostId%20%3D%20document.querySelector(%22%5Bdata-bsky-post-id%5D%22).dataset.bskyPostId%3B%0A%0Aconst%20container%20%3D%20document.querySelector(%22%5Bdata-bsky-container%5D%22)%3B%0Aconst%20likesContainer%20%3D%20document.querySelector(%22%5Bdata-bsky-likes%5D%22)%3B%0Aconst%20likesCount%20%3D%20document.querySelector(%22%5Bdata-bsky-likes-count%5D%22)%3B%0A%0Aconst%20myDid%20%3D%20%22add_your_did%22%3B%0Aconst%20bskyAPI%20%3D%20%22https%3A%2F%2Fpublic.api.bsky.app%2Fxrpc%2F%22%3B%0Aconst%20getLikesURL%20%3D%20%60%24%7BbskyAPI%7Dapp.bsky.feed.getLikes%3Flimit%3D%24%7BLIMIT%7D%26uri%3D%60%3B%0Aconst%20getPostURL%20%3D%20%60%24%7BbskyAPI%7Dapp.bsky.feed.getPosts%3Furis%3D%60%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token constant">LIMIT</span> <span class="token operator">=</span> <span class="token number">55</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> bskyPostId <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-bsky-post-id]"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>dataset<span class="token punctuation">.</span>bskyPostId<span class="token punctuation">;</span><br><br><span class="token keyword">const</span> container <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-bsky-container]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> likesContainer <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-bsky-likes]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> likesCount <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-bsky-likes-count]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> myDid <span class="token operator">=</span> <span class="token string">"add_your_did"</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> bskyAPI <span class="token operator">=</span> <span class="token string">"https://public.api.bsky.app/xrpc/"</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> getLikesURL <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bskyAPI<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">app.bsky.feed.getLikes?limit=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">LIMIT</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;uri=</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><span class="token keyword">const</span> getPostURL <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bskyAPI<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">app.bsky.feed.getPosts?uris=</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, we’re going to define two functions that modify the DOM using the data from the Bluesky APIs.</p><p class="post__p">The <code>drawHowManyMore</code> function only runs if there are more likes on the post than what has been fetched by the <code>getLikes</code> API. Again, I’m using BEM syntax for my CSS; if you’re using something different you will need to update which classes are added to the <code>likesMore</code> element.</p><p class="post__p">The <code>drawLikes</code> function loops through the <code>likes</code> data from the <code>getLikes</code> API and creates an <code>img</code> element for each <code>actor</code>, adding a placeholder fallback if there is no avatar provided. Note that we replace <code>avatar</code> with <code>avatar_thumbnail</code> in the <code>like.actor.avatar</code> string. This is to display an image that is <code>128x128px</code>, instead of the default <code>1000x1000px</code>. Don’t forget the <code>alt</code> text attribute on the <code>img</code> element or the <code>aria-label</code> on the placeholder <code>span</code> element.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="exdHCJRCht"
      aria-describedby="exdHCJRCht">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="exdHCJRCht">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="exdHCJRCht" itemprop="text" content="function%20drawHowManyMore(postLikesCount%2C%20likesActorLength)%20%7B%0A%20%20if%20(postLikesCount%20%3E%20LIMIT)%20%7B%0A%20%20%20%20const%20likesMore%20%3D%20document.createElement(%22li%22)%3B%0A%20%20%20%20likesMore.classList.add(%22post__like%22)%3B%0A%20%20%20%20likesMore.classList.add(%22post__like--howManyMore%22)%3B%0A%20%20%20%20likesMore.innerText%20%3D%20%60%2B%24%7BpostLikesCount%20-%20likesActorLength%7D%60%3B%0A%20%20%20%20likesContainer.appendChild(likesMore)%3B%0A%20%20%7D%0A%7D%0A%0Afunction%20drawLikes(likesActors%2C%20postLikesCount)%20%7B%0A%20%20for%20(const%20like%20of%20likesActors)%20%7B%0A%20%20%20%20const%20likeEl%20%3D%20document.createElement(%22li%22)%3B%0A%20%20%20%20likeEl.classList.add(%22post__like%22)%3B%0A%0A%20%20%20%20if%20(like.actor.avatar%20!%3D%3D%20undefined)%20%7B%0A%20%20%20%20%20%20likeEl.innerHTML%20%3D%20%60%0A%20%20%20%20%20%20%3Cimg%20class%3D%22post__like__avatar%22%20%0A%20%20%20%20%20%20%20%20src%3D%22%24%7Blike.actor.avatar.replace(%22avatar%22%2C%20%22avatar_thumbnail%22)%7D%22%20%0A%20%20%20%20%20%20%20%20alt%3D%22%24%7Blike.actor.displayName%7D%22%0A%20%20%20%20%20%20%2F%3E%60%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20likeEl.classList.add(%22post__like--placeholder%22)%3B%0A%20%20%20%20%20%20likeEl.innerHTML%20%3D%20%60%0A%20%20%20%20%20%20%20%20%3Cspan%20aria-label%3D%22%24%7Blike.actor.displayName%7D%22%3E%40%3C%2Fspan%3E%0A%20%20%20%20%20%20%60%3B%0A%20%20%20%20%7D%0A%20%20%20%20likesContainer.appendChild(likeEl)%3B%0A%20%20%7D%0A%0A%20%20drawHowManyMore(postLikesCount%2C%20likesActors.length)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">drawHowManyMore</span><span class="token punctuation">(</span><span class="token parameter">postLikesCount<span class="token punctuation">,</span> likesActorLength</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>postLikesCount <span class="token operator">></span> <span class="token constant">LIMIT</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> likesMore <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"li"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    likesMore<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">"post__like"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    likesMore<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">"post__like--howManyMore"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    likesMore<span class="token punctuation">.</span>innerText <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">+</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>postLikesCount <span class="token operator">-</span> likesActorLength<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>    likesContainer<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>likesMore<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">drawLikes</span><span class="token punctuation">(</span><span class="token parameter">likesActors<span class="token punctuation">,</span> postLikesCount</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> like <span class="token keyword">of</span> likesActors<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> likeEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"li"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    likeEl<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">"post__like"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>like<span class="token punctuation">.</span>actor<span class="token punctuation">.</span>avatar <span class="token operator">!==</span> <span class="token keyword">undefined</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      likeEl<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br>      &lt;img class="post__like__avatar" <br>        src="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>like<span class="token punctuation">.</span>actor<span class="token punctuation">.</span>avatar<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">"avatar"</span><span class="token punctuation">,</span> <span class="token string">"avatar_thumbnail"</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" <br>        alt="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>like<span class="token punctuation">.</span>actor<span class="token punctuation">.</span>displayName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"<br>      /></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>      likeEl<span class="token punctuation">.</span>classList<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">"post__like--placeholder"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      likeEl<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br>        &lt;span aria-label="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>like<span class="token punctuation">.</span>actor<span class="token punctuation">.</span>displayName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">">@&lt;/span><br>      </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>    likesContainer<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>likeEl<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token function">drawHowManyMore</span><span class="token punctuation">(</span>postLikesCount<span class="token punctuation">,</span> likesActors<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s where we get the data from the Bluesky APIs, which is fetched from two API endpoints: <a href="https://docs.bsky.app/docs/api/app-bsky-feed-get-posts" target="_blank">app.bsky.feed.getPosts</a> and <a href="https://docs.bsky.app/docs/api/app-bsky-feed-get-likes" target="_blank">app.bsky.feed.getLikes</a>. After checking if a <code>bskyPostId</code> is available and not null, construct a <code>postUri</code> (<a href="https://atproto.com/specs/at-uri-scheme" target="_blank">at-uri</a>) using the <code>myDid</code> and <code>bskyPostId</code> variables.</p><p class="post__p">Next, fetch the <code>bskyPost</code> data to get the total number of likes on that post: <code>postData.posts[0].likeCount</code>. Then, fetch the <code>bskyPostLikes</code> data, which will return a <code>likes</code> array of user objects (or <code>actors</code>) that contain the avatar URLs we want to display.</p><p class="post__p">If the <code>bskyPostLikes</code> response contains any <code>likes</code>, we update the <code>likesCount</code> text content in the HTML, and run the <code>drawLikes</code> function. If there are any errors in these API calls, we simply remove the whole container from the page. And that’s it!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="JIGLaFnLUc"
      aria-describedby="JIGLaFnLUc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="JIGLaFnLUc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="JIGLaFnLUc" itemprop="text" content="if%20(bskyPostId%20!%3D%3D%20%22null%22)%20%7B%0A%20%20const%20postUri%20%3D%20%60at%3A%2F%2F%24%7BmyDid%7D%2Fapp.bsky.feed.post%2F%24%7BbskyPostId%7D%60%3B%0A%0A%20%20try%20%7B%0A%20%20%20%20const%20bskyPost%20%3D%20await%20fetch(getPostURL%20%2B%20postUri)%3B%0A%20%20%20%20const%20bskyPostLikes%20%3D%20await%20fetch(getLikesURL%20%2B%20postUri)%3B%0A%20%20%20%20const%20postData%20%3D%20await%20bskyPost.json()%3B%0A%20%20%20%20const%20likesData%20%3D%20await%20bskyPostLikes.json()%3B%0A%0A%20%20%20%20const%20totalLikesCount%20%3D%20postData.posts%5B0%5D.likeCount%3B%0A%0A%20%20%20%20if%20(likesData.likes.length%20%3E%200)%20%7B%0A%20%20%20%20%20%20likesCount.textContent%20%3D%20totalLikesCount%3B%0A%20%20%20%20%20%20drawLikes(likesData.likes%2C%20totalLikesCount)%3B%0A%20%20%20%20%7D%0A%20%20%7D%20catch%20(error)%20%7B%0A%20%20%20%20container.remove()%3B%0A%20%20%7D%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>bskyPostId <span class="token operator">!==</span> <span class="token string">"null"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> postUri <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">at://</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>myDid<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/app.bsky.feed.post/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>bskyPostId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> bskyPost <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>getPostURL <span class="token operator">+</span> postUri<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> bskyPostLikes <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>getLikesURL <span class="token operator">+</span> postUri<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> postData <span class="token operator">=</span> <span class="token keyword">await</span> bskyPost<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> likesData <span class="token operator">=</span> <span class="token keyword">await</span> bskyPostLikes<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> totalLikesCount <span class="token operator">=</span> postData<span class="token punctuation">.</span>posts<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>likeCount<span class="token punctuation">;</span><br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>likesData<span class="token punctuation">.</span>likes<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      likesCount<span class="token punctuation">.</span>textContent <span class="token operator">=</span> totalLikesCount<span class="token punctuation">;</span><br>      <span class="token function">drawLikes</span><span class="token punctuation">(</span>likesData<span class="token punctuation">.</span>likes<span class="token punctuation">,</span> totalLikesCount<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    container<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://github.com/whitep4nth3r/mk2-p4nth3rblog/blob/main/src/_client_scripts/bsky_post_likes.js" target="_blank">View the full JavaScript file on GitHub</a>.</p><h2 class="post__h2">Some cool observations</h2><ol><li><p class="post__p">It only takes a few seconds from a Bluesky user liking a post to their avatar showing up on a blog post.</p></li><li><p class="post__p">The <code>likes </code>actors are sorted by timestamp-of-like-descending, so when someone likes your post on Bluesky, they appear at the <b class="post__p--italic">top left </b>of the avatar list. This, I hope, creates even more joy than intended (for left-to-right reading geographies, at least).</p></li><li><p class="post__p">The Bluesky <code>getPosts</code> API updates quicker than the <code>getLikes</code> API. This means that on a page refresh, the likes number is generally up-to-date, and the avatars may take another second or two to appear on another refresh.</p></li></ol><h2 class="post__h2">Share your results with me on Bluesky</h2><p class="post__p">I hope it goes without saying that I’d love to see your implementations and how you made this code work for your on your website. When you’re ready to post about it on Bluesky, tag the handle <a href="https://bsky.app/profile/whitep4nth3r.com" target="_blank">@whitep4nth3r.com</a> in the replies, and I’ll like it to put my face on your blog post.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Sentry can’t fix React hydration errors, but it can really help you debug them</title>
          <description>Hydration failed because the initial ui does not match what was rendered on the server. Great.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/sentry-cant-fix-react-hydration-errors-but-it-can-really-help-you-debug-them/</link>
          <guid>https://blog.sentry.io/sentry-cant-fix-react-hydration-errors-but-it-can-really-help-you-debug-them/</guid>
          <pubDate>Tue, 24 Sep 2024 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p"><b class="post__p--italic">Hydration failed because the initial ui does not match what was rendered on the server.</b> Don’t you just <b class="post__p--italic">~~love~~</b> hate it when that happens?</p><p class="post__p">If you’re building server-rendered pages with Next.js or any React-based meta-framework, you know hydration errors suck and are usually difficult to debug. Hydration in React is the process that happens when a React application that was rendered as HTML on the server is made interactive in the browser. Hydration errors happen when the markup rendered by React on the client doesn’t match the initial server-rendered HTML, or when invalid HTML was sent by the server, and React couldn’t fix it. Sometimes this is unavoidable, for example, with dates and localization. You can suppress errors for these unavoidable differences in your React code, but most hydration errors would indicate that you’ve got some bugs in your app.</p><p class="post__p">This article is not about <a href="https://sentry.io/answers/hydration-error-nextjs/" target="_blank">how to fix hydration errors</a> but how to debug them using <a href="https://sentry.io/welcome/" target="_blank">Sentry</a>. When hydration errors happen in development, your JavaScript framework of choice will usually show you a large error message with some details about the code that triggered the error. However, <b class="post__p--bold">hydration errors aren’t usually visible or obvious in production</b>, and your average real-world users probably won’t be able to provide useful screenshots with error reports (and probably won’t think to look in the browser console for errors). What’s more, the <b class="post__p--italic">automatic</b> error issue created by Sentry won’t be very useful, either.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1dvE3UXMgUZs0f6rVoFizM/3edaa76e5572ff6734938fff79012560/1_useless_error.png" alt="Error issue in Sentry created by a hydration issue. The error message is not useful and there isn’t much context associated with the issue.
" height="1237" width="1920" /><h2 class="post__h2">But wait, Session Replay has a superpower</h2><p class="post__p">If you’re using Sentry’s <a href="https://docs.sentry.io/product/explore/session-replay/" target="_blank">Session Replay</a> to get reproductions of user sessions when a user triggers a hydration error:</p><ol><li><p class="post__p">Sentry will automatically create a specific <a href="https://docs.sentry.io/platforms/javascript/session-replay/issue-types/#configuring-hydration-errors" target="_blank">Hydration Error Issue</a> for you (for free!),</p></li><li><p class="post__p">group all the same errors together,</p></li><li><p class="post__p">capture both the server-rendered and client-rendered markup that existed when the error happened,</p></li><li><p class="post__p">and show you the diffs in both visual and raw HTML modes.</p></li></ol><p class="post__p"><b class="post__p--bold">Just make sure you’re using Sentry’s JavaScript SDK, version 7.87.0 or above, and you’ve got Session Replay enabled to get the good stuff.</b></p><p class="post__p">Here’s what a grouped Hydration Error looks like in Sentry:</p><ul><li><p class="post__p">It is named “Hydration Error,” so you can find it easily in the issue list view.</p></li><li><p class="post__p">Choose which event to view (recommended, latest, oldest) and navigate between events to compare contextual information.</p></li><li><p class="post__p">View a sliding diff between the server and the client (open the diff viewer to also view a side by side visual diff and HTML diff).</p></li><li><p class="post__p">Click the “Resolving Hydration Errors” link to get more help on solving hydration errors.</p></li></ul><img src="https://images.ctfassets.net/56dzm01z6lln/35ctKNM0bZzuOn70erjzPz/480d3c14e0361555a2ea4377da483cb9/2_hydration_issue.png" alt="Grouped hydration error issue view showing how to navigate between grouped events, highlighting the diff viewer, and pointing to a link to get more info about how to solve hydration errors." height="1237" width="1920" /><h2 class="post__h2">Debugging hydration errors</h2><p class="post__p">Given that the standard hydration error message states that the server-rendered HTML did not match the client-rendered HTML, the diff viewer is really helpful in finding those differences. The “Before” view is the server-rendered version, and the “After” view is what React rendered on the client.</p><img src="https://images.ctfassets.net/56dzm01z6lln/61YrYH8IUVYvDPcun1gOZd/e083aec4a1a5511c20ec69e81d4f0af5/3_side_by_side_diff.png" alt="Hydration error side by side visual diff viewer, showing that the server rendered page is the same as the client rendered page.
" height="1237" width="1920" /><p class="post__p">What you might notice in this example, however, is that the “Before” and “After” versions look the same visually. When inspecting the HTML diff, we see a blank page rendered on the server and HTML rendered on the client. This isn’t correct. HTML is definitely being rendered on the server; I checked in my browser’s network tab.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7GlImf9rXIgEmYDBccVdjg/70da3a16890df43520b5b4cb5df049fe/4_html_diff.png" alt="Hydration error HTML diff viewer, showing a blank page on the server and invalid HTM on the client." height="1237" width="1920" /><p class="post__p">The real issue in the code causing the hydration error is not that the server-rendered and client-rendered HTML don’t match, it’s that my HTML is invalid (a <code>&lt;p&gt;</code> is contained within a <code>&lt;p&gt;</code>) and React threw an error. This is why hydration errors suck: they don’t always make sense.</p><p class="post__p">So why does there appear to be no HTML rendered on the server according to the diff? Given that React threw a nonsensical hydration error for invalid HTML, Sentry can only make a best-effort guess as to what the problem is. Usually, if one event has a diff that doesn’t make sense, another event may be more helpful. The bottom line is that when React throws a hydration error, there’s a problem that needs fixing. And because React doesn’t tell us the details needed to fix the problem in production, Sentry fills in the blanks on the hydration error diff tools as much as possible so you can better debug the problem.</p><h2 class="post__h2">Does creating hydration error issues and diffs increase my Sentry bill?</h2><p class="post__p">No. If you’re already using Session Replay, you get automatic grouped hydration error issues for free. Hydration error issues in Sentry are generated from Replays and their associated data, so have no impact on your error quota, either. Additionally, you’ll also get alerted on hydration errors by default.</p><h2 class="post__h2">Bonus tip: level-up your debugging by unmasking non-sensitive data in replays</h2><p class="post__p">If you’re using Session Replay, you might have noticed that it masks all text content with * and blocks all media elements on the client by default, before it is sent to Sentry. In this article, the examples shown in the hydration errors images show text rather than asterisks, due to how the <a href="https://docs.sentry.io/product/explore/session-replay/web/" target="_blank">Session Replay SDK</a> was configured.</p><p class="post__p">If you’re working on a content-based website that’s free of personally identifiable information (PII), you can disable the default text masking and image blocking by configuring the <code>maskAllText</code> and <code>blockAllMedia</code> configuration options in the Session Replay initialization.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="dXVzcWLXBf"
      aria-describedby="dXVzcWLXBf">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="dXVzcWLXBf">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="dXVzcWLXBf" itemprop="text" content="Sentry.replayIntegration(%7B%0A%20%20maskAllText%3A%20false%2C%0A%20%20blockAllMedia%3A%20false%2C%0A%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript">Sentry<span class="token punctuation">.</span><span class="token function">replayIntegration</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">maskAllText</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">blockAllMedia</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">This can be really helpful in debugging <b class="post__p--bold">fake</b> hydration errors (that are actually invalid HTML errors) like the one demonstrated in this article. Note: Only use this if your site has no sensitive data or if you’ve already set up other options for masking or blocking actual sensitive data. <a href="https://docs.sentry.io/platforms/javascript/session-replay/privacy/#privacy-configuration" target="_blank">Read more about Session Replay privacy configuration on the Sentry docs</a>.</p><h2 class="post__h2">Debuggability is key</h2><p class="post__p">Hydration errors in production have always been a bit of a mystery, without any practical way to deal with and debug them. With the Sentry Replay SDK, you now get the HTML diffs and as much context as possible with each hydration error, helping you to debug and fix things faster. And whilst this ultimately helps you as a developer, it benefits everyone else as well: stakeholders, clients, and most importantly — your users. Now, go fix those bugs.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Work is meaningless, and it almost killed my husband</title>
          <description>Work is an exchange of time for money.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/work-is-meaningless/</link>
          <guid>https://whitep4nth3r.com/blog/work-is-meaningless/</guid>
          <pubDate>Sun, 22 Sep 2024 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">My husband’s name is Richard. He is the love of my life. At the time of writing we have been married for 13 and a half years. Richard works as Head of Internal IT at a digital agency. In 2023, he received the Employee of the Year award. This will be important, later.</p><h2 class="post__h2">20th August 2024</h2><p class="post__p">On 20th August 2024, I found Richard asleep in front of his work laptop on the sofa at 4:30pm. It was slightly concerning, but I knew he’d been working a lot lately. He was just tired. That evening, we went out for pizza with some friends. I noticed something “off” about Richard. The walk home felt like talking with a stranger after a mediocre date. That week, he started to fall asleep more and more often: at his desk, whilst dinner was cooking, or whilst playing video games.</p><h2 class="post__h2">24th August 2024</h2><p class="post__p">The following weekend, Richard had developed a “bit of a cold”. But it felt like more than a cold. He was slurring his speech; he was moving slower. I noticed it was becoming more difficult for him to play with our six year old son. The things he was trying to do and say didn’t make sense. Normal conversation would be interspersed with moments of strange nonsense. It got worse in the evenings when he was more tired. His movements became slower and weirder. At times he would fall asleep mid-sentence.</p><p class="post__p">“There’s a problem with your brain,” I said. “You should go to the doctor.” I cancelled a speaking gig in Denmark to look after him and our son, who was still enjoying the summer holidays. Time is a blur at this point. Richard blamed all of the above on his “cold” and shrugged it off. He didn&#39;t contact the doctor.</p><h2 class="post__h2">27th August 2024</h2><p class="post__p">On Tuesday 27th August, Richard was in a state of near-constant delirium. I called the NHS non-emergency service, 111, and explained his symptoms. They sent an ambulance straight away, and he was taken to hospital. I spent the evening with my son, we had Japanese take out and watched a film. I felt much less anxious knowing Richard was in the hospital and could get the treatment he needed. I went to bed.</p><p class="post__p">At around 11pm that night, Richard returned home. He told me that everything was fine, and that he just had some sort of virus that would clear up on its own. After being admitted to hospital twice in the last year for having seizures as a result of iron deficiency anaemia (even after numerous tests throughout the year, they still hadn’t found the cause), he vowed he would never go back again.</p><p class="post__p">Given that he turns 40 this year, Richard decided it was time for a lifestyle change. It was time for him to start exercising regularly, eating better, and taking care of his health. He started wearing his Apple Watch again. That week he purchased new running shoes, and whilst he was still showing symptoms of this “weird virus”, he went out for several runs in the local park. His watch reported an average walking heart rate of over 140bpm; jogging was close to 180bpm. Richard is a 39 year old male. Those numbers weren’t good. I asked him once again to go to the doctor; he wouldn’t.</p><h2 class="post__h2">3rd September 2024</h2><p class="post__p">On September 3rd, our son returned to school, and Richard and I spent the day together. We went for a walk, we ate home made soup for lunch, but Richard was struggling to do some basic tasks. That night, he coughed up blood in his sleep. He didn’t wake up when it happened.</p><h2 class="post__h2">4th September 2024</h2><p class="post__p">That morning, I found Richard at 4am and cleaned him up. I decided it was time to force him to see a doctor. I asked him to make an appointment via the online GP service whilst I was getting ready to leave for the school run. By this point, Richard couldn’t use his phone. He couldn’t type on a keyboard. Every time he tried to do it, he typed random strings of nonsense. I typed out a detailed message for him myself, submitted the form, and left to take our son to school.</p><p class="post__p">When I returned home, Richard told me that he got a call from the doctor to arrange an appointment, and he had no idea why. He was so confused as to why the doctor was ringing him, that he became convinced it was our son that was unwell, and made an appointment under his name instead. An hour later, I took Richard to the GP surgery.</p><p class="post__p">After a short assessment and a telephone call to the hospital, the GP informed us that during his hospital admission the week prior, it was discovered that Richard was critically anaemic again (as in, he will die pretty soon without new red blood cells and iron). But Richard had returned home that night and told me he was fine.</p><p class="post__p">It became clear that Richard had discharged himself from the hospital that night whilst waiting for his blood test results. I blamed myself for this for a short while; <b class="post__p--italic">I should have gone with him to the hospital</b>. But I know it’s not my fault. Richard didn’t think anything was wrong with him. He didn’t want to believe anything was wrong with him. He wanted to get back to work. <b class="post__p--bold">Employee of the Year 2023 didn’t want to let anyone down.</b></p><p class="post__p">I took Richard to A&amp;E (the ER) that day, with an urgent letter from the GP. I organised my mother in law to help with childcare so I could stay with him for as long as possible so he wouldn’t escape again. At the hospital, they took him into triage straight away, admitted him, and hooked him up to all the machines.</p><p class="post__p">I thought I was watching my husband die.</p><hr class="post__hr" /><h2 class="post__h2">14 days</h2><p class="post__p">Richard was in the hospital for 14 days. He was treated for a very bad chest infection, anaemia, and encephalitis: an uncommon but serious condition in which the brain becomes inflamed. According to the <a href="https://www.nhs.uk/conditions/encephalitis/" target="_blank">NHS article on encephalitis</a>, “Some people eventually make a full recovery from encephalitis, although this can be a long and frustrating process. Many people never make a full recovery and are left with long-term problems caused by damage to their brain.”</p><p class="post__p">Whilst being treated for the anaemia and chest infection, Richard was subject to a lot of tests on his brain. All of the easily discoverable bad things were ruled out; he hadn’t had a stroke, there was no bleeding, there was no sign of cancer: his brain was physically normal. Yet my Richard still wasn’t “normal”.</p><p class="post__p">I visited Richard for eight hours every day. I took him his favourite foods, clean clothes, helped him shower, and listened to him talk a lot of nonsense. Throughout all this, I fully prepared myself to be his full-time carer and a single parent when he was “well enough” to return home. He started to experience a complete and utter devastating depression. He felt like he’d let everyone down. I stayed with him, reassured him, and promised to love him, no matter what happened.</p><h2 class="post__h2">13th September 2024</h2><p class="post__p">On Richard’s 10th day in hospital, it was my 39th birthday. I prepared myself to be in a similar situation on my 40th birthday.</p><h2 class="post__h2">16th September 2024</h2><p class="post__p">On day 13, I went to visit Richard at 11am as usual. I said hello, asked how he was feeling, and placed a bag of clean clothes on the floor. And when he spoke to me, <b class="post__p--bold">his voice was his own</b>. His words were no longer slurred, his movements were his own: sharper and more refined. He was my Richard. He was back. His brain was better.</p><p class="post__p">We’re still waiting for the results of some tests; but the doctors still don’t know what happened to Richard’s brain, or why he made such a sudden overnight recovery. We don’t know if the anaemia and brain swelling are linked, or whether it was just a huge coincidence. We’re not entirely sure whether the antibiotics used to treat the chest infection helped treat whatever was happening in his brain. We still don’t know the cause of the anaemia but I’m sure in time we will — and he is undergoing more tests.</p><h2 class="post__h2">Our jobs are meaningless</h2><p class="post__p">We may not know the cause of Richard’s anaemia, or what happened with his brain, but what we do know after all of this, is that our jobs in tech are largely meaningless, and they’re <b class="post__p--italic">definitely</b> not worth almost dying for.</p><p class="post__p"><b class="post__p--bold">You see, it’s not the work itself that put Richard in hospital, it’s that he dedicated so much of his waking time and brain space toward work that </b><u><b class="post__p--bold">he neglected to consider his own declining health</b></u>. He knew he was feeling more tired than usual, but he pushed through. He knew he found it difficult to climb stairs, but he thought he was just getting old. He knew his heart rate was too high, but he shrugged it off as high metabolism (it’s actually a symptom of anaemia). He was extremely pale. But that’s how he always looked, right? He knew his voice and his behaviour and his coordination had changed, but he ignored it and hoped it would get better on its own.</p><p class="post__p">Richard didn’t want to be off work sick. He didn’t want to “let anyone down”. He wanted to be Employee of the Year <b class="post__p--italic">and better.</b></p><p class="post__p">Over the past couple of years, Richard has worked too much. He stayed up all night on numerous occasions to make non-emergency internal infrastructure changes on flaky legacy systems. No users would notice the changes, but it earned him employee points. He would agree to outlandish time-intensive requests from senior members of the business that had nothing to do with his scope of work. He would check for Teams messages at the dinner table in case something broken needed fixing out of hours. He decided to take on double the work rather than scale back operations when his team of three was a team of two for a few months. He missed school events for arbitrary useless meetings.</p><p class="post__p">Richard’s self-imposed approach to work — trying to be “as good as possible at all costs” — isolated him from his family and his own sense of self, and it took him almost dying in front of me in A&amp;E to realise that <b class="post__p--italic">none of this is worth dying for, actually</b>. And plus, “as good as possible” does not actually mean “do as much as you actually possibly can and maybe, like, four times more, despite not getting the support you need from your employer and never sleeping properly for weeks at a time because you take on random non-essential assignments and eventually accidentally distance yourself from your family because you are only a single human being and there are only so many hours in the day and oh wait I completely forgot to check on this life-threatening mineral deficiency that might kill me because I didn’t want to let anyone down”. I could go on.</p><p class="post__p">He still needs a lot of rest. And I&#39;m making sure he&#39;s getting it.</p><h2 class="post__h2">Work is an exchange of time for money</h2><p class="post__p">I won’t speak any more for Richard, but I will offer you my thoughts on this.</p><p class="post__p">Work is an exchange of time for money. Most work is meaningless, and is centred on finding solutions to manufactured problems that exist only in capitalism. And whilst that sounds extremely depressing, the upside is that it leaves you free to find your own meaning in life.</p><p class="post__p">And I do hope you will find meaning — and much love — in those humans who you choose to call your family. And most importantly, please look after your own health.</p><p class="post__p">No work is worth dying for.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to make your web page faster before it even loads</title>
          <description>Perhaps you could use your new-found knowledge on DNS to wow people at all the cool parties you probably attend.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/how-to-make-your-web-page-faster-before-it-even-loads/</link>
          <guid>https://blog.sentry.io/how-to-make-your-web-page-faster-before-it-even-loads/</guid>
          <pubDate>Mon, 19 Aug 2024 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">As developers (and as front end developers in particular), we usually talk about web performance in the context of measuring what happens when we start to see things appear in a browser window, and when we can consume content or interact with the page. For example, the following Core Web Vitals guide the discussion on what we can see, use and experience:</p><ul><li><p class="post__p">First Contentful Paint (FCP), which measures the time from when the user first navigated to the page to when any part of the page’s content is rendered,</p></li><li><p class="post__p">Largest Contentful Paint (LCP), which is the point in in the page load timeline when the main content of the page has likely loaded, and</p></li><li><p class="post__p">Interaction to Next Paint (INP), which measures how quickly a web page responds to user input.</p></li></ul><p class="post__p">This makes sense given that without anything to consume and/or interact with we don’t really have an experience to measure at all, but what about the events that happen before the first byte of a web page is received by the browser? Can we measure those events, and subsequently optimize them to make our web pages and applications load even faster?</p><h2 class="post__h2">How pre-TTFB events are visualized in the Sentry Trace View</h2><p class="post__p">The above questions came to mind when looking at the <a href="https://docs.sentry.io/concepts/key-terms/tracing/trace-view/" target="_blank">Trace View</a> in Sentry, where the events that happen before anything is rendered in a browser window are captured and labeled as <code>browser</code> spans. Six spans are registered in chronological order: <b class="post__p--bold">cache, DNS, connect, TLS/SSL, request, and response</b>. All events before <code>response</code> precede the Time to First Byte (TTFB), which measures the time between the request for a web page/resource and when the first byte of a response begins to arrive.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5zM2T7HKVhi2HmC0Ew1te5/72675e4aaa6a91dfc3a3c4413084ffaf/1_trace_view_browser_events_highlighted.png" alt="Sentry trace view with a rectangle surrounding the following events in order: browser cache, browser DNS, browser connect, browser TLS/SSL, browser request, browser response.
" height="1106" width="1920" /><p class="post__p">You may be wondering how these events are captured by <a href="https://sentry.io/welcome/" target="_blank">Sentry</a>, given Sentry won’t have been initialized in the browser at this point in the page load timeline; and I wondered the same thing! The answer lies in the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance" target="_blank">Performance API</a> — a group of web standards used to measure the performance of web applications — and more specifically the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Navigation_timing" target="_blank">Navigation Timing API</a>, which provides some very useful metrics including high-precision timestamps to the browser each time an event (known as a <code>PerformanceEntry</code>) happens.</p><p class="post__p">The really cool thing is that you also have direct access to the Performance API in the browser: most of the performance entries are recorded for any web page without any setup or extra code needed to retrieve them. Try it out now by opening up the dev tools console and typing <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/performance" target="_blank"><code>window.performance</code></a>. You’ll see something like this (I’ve manually grouped related timestamps and ordered them for easier parsing):</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="SXicNwezDO"
      aria-describedby="SXicNwezDO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="SXicNwezDO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="SXicNwezDO" itemprop="text" content="%2F%2F%20captured%20from%20https%3A%2F%2Fwhitep4nth3r.com%20on%20Tues%2030%20July%20%40%2013.42%0A%7B%0A%20%20%22timing%22%3A%20%7B%0A%20%20%20%20%22navigationStart%22%3A%201722343304923%2C%0A%0A%20%20%20%20%22redirectStart%22%3A%200%2C%0A%20%20%20%20%22redirectEnd%22%3A%200%2C%0A%0A%20%20%20%20%22fetchStart%22%3A%201722343304928%2C%0A%0A%20%20%20%20%22domainLookupStart%22%3A%201722343304928%2C%0A%20%20%20%20%22domainLookupEnd%22%3A%201722343304928%2C%0A%0A%20%20%20%20%22connectStart%22%3A%201722343304928%2C%0A%20%20%20%20%22secureConnectionStart%22%3A%200%2C%0A%20%20%20%20%22connectEnd%22%3A%201722343304928%0A%0A%20%20%20%20%22requestStart%22%3A%201722343304988%2C%0A%20%20%20%20%22responseStart%22%3A%201722343304989%2C%0A%0A%20%20%20%20%22unloadEventStart%22%3A%200%2C%0A%20%20%20%20%22unloadEventEnd%22%3A%200%2C%0A%0A%20%20%20%20%22responseEnd%22%3A%201722343304998%2C%0A%0A%20%20%20%20%22domInteractive%22%3A%201722343305161%2C%0A%20%20%20%20%22domContentLoadedEventStart%22%3A%201722343305161%2C%0A%20%20%20%20%22domContentLoadedEventEnd%22%3A%201722343305161%2C%0A%20%20%20%20%22domLoading%22%3A%201722343304996%2C%0A%20%20%20%20%22domComplete%22%3A%201722343305381%2C%0A%0A%20%20%20%20%22loadEventStart%22%3A%201722343305381%2C%0A%20%20%20%20%22loadEventEnd%22%3A%201722343305381%2C%20%20%0A%20%20%7D%2C%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// captured from https://whitep4nth3r.com on Tues 30 July @ 13.42</span><br><span class="token punctuation">{</span><br>  <span class="token string-property property">"timing"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token string-property property">"navigationStart"</span><span class="token operator">:</span> <span class="token number">1722343304923</span><span class="token punctuation">,</span><br><br>    <span class="token string-property property">"redirectStart"</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"redirectEnd"</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br><br>    <span class="token string-property property">"fetchStart"</span><span class="token operator">:</span> <span class="token number">1722343304928</span><span class="token punctuation">,</span><br><br>    <span class="token string-property property">"domainLookupStart"</span><span class="token operator">:</span> <span class="token number">1722343304928</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"domainLookupEnd"</span><span class="token operator">:</span> <span class="token number">1722343304928</span><span class="token punctuation">,</span><br><br>    <span class="token string-property property">"connectStart"</span><span class="token operator">:</span> <span class="token number">1722343304928</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"secureConnectionStart"</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"connectEnd"</span><span class="token operator">:</span> <span class="token number">1722343304928</span><br><br>    <span class="token string-property property">"requestStart"</span><span class="token operator">:</span> <span class="token number">1722343304988</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"responseStart"</span><span class="token operator">:</span> <span class="token number">1722343304989</span><span class="token punctuation">,</span><br><br>    <span class="token string-property property">"unloadEventStart"</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"unloadEventEnd"</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br><br>    <span class="token string-property property">"responseEnd"</span><span class="token operator">:</span> <span class="token number">1722343304998</span><span class="token punctuation">,</span><br><br>    <span class="token string-property property">"domInteractive"</span><span class="token operator">:</span> <span class="token number">1722343305161</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"domContentLoadedEventStart"</span><span class="token operator">:</span> <span class="token number">1722343305161</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"domContentLoadedEventEnd"</span><span class="token operator">:</span> <span class="token number">1722343305161</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"domLoading"</span><span class="token operator">:</span> <span class="token number">1722343304996</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"domComplete"</span><span class="token operator">:</span> <span class="token number">1722343305381</span><span class="token punctuation">,</span><br><br>    <span class="token string-property property">"loadEventStart"</span><span class="token operator">:</span> <span class="token number">1722343305381</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"loadEventEnd"</span><span class="token operator">:</span> <span class="token number">1722343305381</span><span class="token punctuation">,</span>  <br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">So how does Sentry populate a trace with those browser spans? As a result of the Performance API recording these metrics with timestamps from the moment a URL is requested in the browser, the <a href="https://docs.sentry.io/platforms/javascript/" target="_blank">Sentry JavaScript SDK</a> is able to access these after initializing, backfill the full list of events that happened chronologically before the web page loaded, and send them as spans to the relevant full trace so they can be visualized in the Trace View.</p><h2 class="post__h2">What happens before a web page loads?</h2><p class="post__p">The <code>window.performance</code> provides a window (pun definitely intended) into the many different events that happen before we see any web page content appear in a browser. It returns a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance" target="_blank"><code>Performance</code></a> object, which contains a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance/timing" target="_blank"><code>timing property</code></a>, shown in the above code example. While this is a quick way for us to inspect the events recorded by the browser on page load without writing any code, the <code>Performance.timing</code> property is now deprecated and has been superseded by the <a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming" target="_blank"><code>PerformanceNavigationTiming API</code></a> (with only some minor changes so far).</p><p class="post__p">This diagram from the <a href="https://www.w3.org/TR/navigation-timing-2/#processing-model" target="_blank">Navigation Timing Level 2 specification</a> shows the order in which <code>PerformanceNavigationTiming</code> events are recorded from the moment a navigation request is made in the browser, to when the load event of the current document is completed. Not all events will be available for each page load, but the order matches what we observed using <code>window.performance</code> above.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3iHWHFRSmUBul0BwToakPb/f2180f9e41cbb038c85445350db662e5/2_timing_timeline.png" alt="Navigation performance timeline visualized in a series of blocks with the following descriptions: Redirect, Service Worker Init, Service Worker Fetch Event, HTTP Cache, DNS, TCP, Request, Early Hints (103), Response, Processing, Load." height="865" width="2013" /><p class="post__p">Let’s explore each relevant event metric to see what’s happening under the hood, and how it gets calculated by Sentry from particular timestamps to populate the browser spans in the Trace View. And hopefully, with this new knowledge, we’ll begin to understand how we might be able to optimize for web performance before the TTFB.</p><h3 class="post__h3">browser cache</h3><p class="post__p">If the resource is being fetched using HTTP GET (e.g. a standard request for a web page), the browser will check the HTTP cache first. <code>fetchStart</code> returns the time immediately before the browser starts checking the cache. The cache span in the Sentry Trace View is calculated as the time between the <code>fetchStart</code> timestamp and the <code>domainLookupStart</code> timestamp.</p><p class="post__p">A non-zero value for a cache span in the Trace View represents the time taken for a browser to retrieve the resource from the browser cache. Longer cache spans could point to the use of slower or older browsers, or users who don’t clear their browser caches very often, if at all.</p><h3 class="post__h3">browser DNS</h3><p class="post__p">The next span reports the DNS (Domain Name System) lookup time. When a user requests a URL, the DNS is queried to “lookup” the domain in a database and translate it to an IP address. The total time taken to complete this is calculated by subtracting the <code>domainLookupStart</code> timestamp value from the <code>domainLookupEnd</code> timestamp value.</p><h3 class="post__h3">browser connect</h3><p class="post__p">Next, it’s time to measure the time it takes for the browser to connect to a web server. This is known as “connection negotiation”, and is measured as the time between two events: <code>connectStart</code> (when the browser starts to open a connection to the web server) and <code>connectEnd</code> (when the connection to the web server has been established).</p><h3 class="post__h3">browser TLS/SSL</h3><p class="post__p">If the web server the browser is connecting to is using HTTPS, a <code>secureConnectionStart</code> event will happen in between <code>connectStart</code> and connectEnd. <code>secureConnectionStart</code> marks when the browser and web server exchange messages to acknowledge and verify a secure encrypted connection, known as TLS (Transport Layer Security) negotiation. The value of <code>secureConnectionStart</code> may be <code>0</code> if HTTPS isn’t used or <a href="https://en.wikipedia.org/wiki/HTTP_persistent_connection" target="_blank">if the connection is persistent</a>.</p><p class="post__p">In Sentry, the connect and TLS events are reported as separate spans. In this image of the Trace View, you’ll notice that the connect event begins, the TLS event begins shortly after, and the connect end event ends as soon as TLS negotiation has finished. This representation of events is useful in being able to identify whether there are any bottlenecks in either the web server connection or the TLS negotiation independently.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6UpT1hfn9DkEmwqrt3cVAo/bcb93e43af28d52fa386a694ad577d06/3_connect_tls_connect.png" alt="Sentry trace view with the browser connect span highlighted. Annotations are drawn on the image showing that connectStart happens at the start of the browser connect span, secureConnectionStart happens at the start of the browser TLS span, and connectEnd happens at the time both of the mentioned spans end.
" height="1106" width="1920" /><h3 class="post__h3">browser request</h3><p class="post__p">After the (secure) connection with the web server has been established, the browser will officially make the request for a resource, marked by the <code>requestStart</code> event.</p><h3 class="post__h3">browser response</h3><p class="post__p">Finally, the browser will receive the first byte of content. In the Sentry Trace View, here’s where the TTFB (Time to First Byte) vertical line is marked.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7MsfBCMP5LX5LxEzpM5usA/540d2673c64c0d2c84b2dccc5bb00f05/4_response_start.png" alt="Sentry trace view highlighting that responseStart happens when the TTFB event is marked, which is also when the browser response span begins" height="3078" width="5344" /><h2 class="post__h2">Can we make PerformanceNavigationTiming events faster?</h2><p class="post__p">Now we understand what happens before the first byte of a web page is delivered to a browser, let’s go a little deeper to understand if we might be able to speed up the events that happen in the navigation timeline.</p><h3 class="post__h3">Can you speed up the browser cache retrieval event?</h3><p class="post__p">As a developer who wants to improve the performance of their own applications, I’m not sure you can speed up this event for your user-base. However, you probably could speed up this event for <b class="post__p--bold">yourself</b> by being diligent with your own personal browser caches. Clear your browser caches like you commit changes to a git repository: little and often.</p><h3 class="post__h3">Can you speed up a DNS lookup?</h3><p class="post__p">The speed of a DNS lookup can be affected by a number of things, including (but not limited to):</p><ul><li><p class="post__p"><b class="post__p--bold">The size of the DNS provider’s infrastructure</b>: fewer “Points of Presence” (POP) around the globe will mean longer latency and slower lookups,</p></li><li><p class="post__p"><b class="post__p--bold">The location of the POPs</b>: if your website visitors are far away from a DNS server, DNS lookup will take longer,</p></li><li><p class="post__p"><b class="post__p--bold">The DNS cache time</b>: DNS is served from a cache until it expires. The length of the DNS cache time is determined by the Time to Live (TTL) value specified on the DNS record (which points a URL to an IP address). The higher the TTL, the less likely the browser will need to perform another DNS lookup on each subsequent request.</p></li></ul><p class="post__p">Ultimately, speeding up DNS lookups involves investing in a DNS provider that has a large and globally distributed network of POPs. If you’re a developer at a scaled-up enterprise business, this is probably taken care of for you. Additionally, setting the TTL value as high as possible for DNS records that don’t change often is probably a good tactic.</p><p class="post__p">At the time of writing, I checked my personal website DNS records out of curiosity, and I had the TTL set to <b class="post__p--bold">5 minutes</b>. This meant that the DNS cache expired every five minutes, causing browsers to do a fresh DNS lookup much more often than needed. Given that I’m not pointing my website URL to a new server, like, ever, I decided to change the TTL to 60 minutes.</p><p class="post__p">In this very limited 5-day experiment on my personal website, I saw fewer non-zero times for the browser DNS lookup spans in Sentry since switching the TTL. If your website is not mission-critical and not money-making, this could be a good solution to help speed up DNS lookup. Bear in mind, though, that if your main servers go down and you want to point your URL to backup servers, it will take up to 60 minutes for all users around the world to see the DNS changes.</p><p class="post__p">That being said, according to <a href="https://sematext.com/glossary/dns-lookup-time/#:~:text=The%20average%20DNS%20lookup%20time,is%20generally%20considered%20very%20good" target="_blank">Sematext</a>, “The average DNS lookup time is between 20 and 120 milliseconds. Anything between that and under is generally considered very good.” So perhaps this type of micro-optimisation might not be worth needing to remember that your TTL is set to 60 minutes when you need to change to a backup server during a primary server outage.</p><h3 class="post__h3">Improving DNS lookup for third-party resources with <code>rel=”dns-prefetch</code>”</h3><p class="post__p">Most front-end websites and apps are probably loading at least one or more resources from third-parties, i.e. resources/images/files/scripts from different domains. Each request to a different domain will also involve a DNS lookup event. Whilst it’s worth noting that third-party resources will be requested after the TTFB and so after the <code>PerformanceNavigationTimeline</code> events we are concerned with in this post, you can attempt to speed up the DNS lookup of these third-party resources by using the attribute <code>rel=&quot;dns-prefetch</code>” and associated <code>href</code> value on the <code>&lt;link&gt;</code> tag that requests the resource. This provides a hint to browsers that the user is likely to need to fetch things from the resource’s origin, at which point the browser can try to improve the user experience by preemptively performing DNS resolution for that origin before the resource is officially requested. This is useful when pulling in third-party fonts from Google, for example:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="MSvKlbHRRg"
      aria-describedby="MSvKlbHRRg">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="MSvKlbHRRg">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="MSvKlbHRRg" itemprop="text" content="%3Clink%20rel%3D%22dns-prefetch%22%20href%3D%22https%3A%2F%2Ffonts.googleapis.com%2F%22%20%2F%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>dns-prefetch<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://fonts.googleapis.com/<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Depending on how many third party resources are requested on page-load in parallel, this could help to speed up the in-browser events that happen after the <code>responseEnd</code> event, i.e. when the browser begins to parse the HTML and request all third-party resources (especially if they are render-blocking resources).</p><p class="post__p">Note: don’t use <code>dns-prefetch</code> on resources fetched from the top-level domain of a website (i.e. resources you host with your website). <a href="https://developer.mozilla.org/en-US/docs/Web/Performance/dns-prefetch" target="_blank">Read more about using <code>dns-prefetch</code> on MDN</a>.</p><h3 class="post__h3">Can you speed up the connect and TLS negotiation events?</h3><p class="post__p">Don’t use HTTPS? I joke. The bottom line regarding TLS negotiation time is that even back in 2010, after Google switched Gmail to use HTTPS for everything, TLS was declared “<a href="https://hpbn.co/transport-layer-security-tls/#reduce-computational-costs" target="_blank">not computationally expensive anymore</a>”. In the 2013 publication, <a href="https://hpbn.co/#author" target="_blank">High Performance Browser Networking</a>, Ilya Grigorik states that “…the early days of the Web often required additional hardware to perform ‘SSL offloading.’ The good news is, this is no longer necessary and what once required dedicated hardware can now be done directly on the CPU.”</p><p class="post__p">The one piece of advice Ilya gave in 2013 is to make full use of <a href="https://hpbn.co/transport-layer-security-tls/#tls-session-resumption" target="_blank">TLS Session Resumption</a>, which is a mechanism used to “resume or share the same negotiated secret key data between multiple connections.” In short, it’s a way for your computer and a website to remember each other, so they don’t have to go through the long process of checking the encryption keys (secret password) every time you reconnect. This makes browsing faster and uses less computational power.</p><p class="post__p">Unless you are directly responsible for implementing TLS on a server, making TLS negotiation as fast as possible is probably 99.999% taken care of for you. However, in the same way you can hint to a browser about resources you may need with <code>rel=&quot;dns-prefetch&quot;</code>, you can go one step further and use <code>rel=&quot;preconnect&quot;</code> on links to external resources, which also preemptively performs part or all of the TLS negotiation. Again, this will happen after the <code>PerformanceNavigationTiming</code> events, but it’s good intel, nonetheless.</p><h3 class="post__h3">Can you speed up the browser request and response events (TTFB)?</h3><p class="post__p">As a developer, the Time to First Byte (<code>responseStart</code>) is ultimately what you have the most control over in the page navigation timeline. Being mindful of everything that happens between the <code>requestStart</code> and <code>responseStart</code> events, and being ruthlessly efficient in optimizing these events can have a huge impact on your page speed and resulting user experience.</p><p class="post__p">Here are three things to investigate in your websites and apps:</p><h4 class="post__h4">Reduce or eliminate request waterfalls</h4><p class="post__p">A “request waterfall” is what happens when a request for a resource (code, data, image, CSS, etc.) does not start until after another request for a resource has finished. In terms of the <code>PerformanceNavigationTimeline</code>, the <code>requestStart</code> event may delay the <code>responseStart</code> event depending on the architecture of your web page or application, and how many synchronous events happen before the browser receives that first byte of data. I experienced this first-hand with my personal website; after I’d noticed that page loads had become excruciatingly slow, I investigated the situation (using Sentry) to find that each page load was making many round trips to an edge server. Choosing to remove those just-in-time requests altogether and include the required data in a static page build meant <a href="https://blog.sentry.io/how-i-fixed-my-brutal-ttfb/" target="_blank">I radically reduced the TTFB by ~80%</a>.</p><p class="post__p">Perhaps your application makes a series of database calls at the time of the <code>requestStart</code> event. Do these queries need to happen in series, or can they happen in parallel? Even better, can you grab all the data you need from the database in a single query? If React is your thing, check out this post from Lazar on <a href="https://blog.sentry.io/how-to-identify-fetch-waterfalls-in-react/" target="_blank">how to identify fetch waterfalls in React</a>.</p><p class="post__p">Better yet: do you need to make any just-in-time calls to the database at all? Or can you follow my lead and build your web pages statically, so all that needs to happen after <code>requestStart</code> is the swift delivery of a static page of HTML from a CDN (Content Delivery Network)? Note: this does not mean you can’t enhance your web page’s interactivity and fetch new data with client-side JavaScript after the page has loaded.</p><h4 class="post__h4">Cache, cache, cache</h4><p class="post__p">Speaking of CDNs, which enable content to be cached at edge servers around the world which are located physically closer to visitors, if your website (or a subset of its pages) are not serving personalized and/or dynamic content, you should take advantage of caching: where full HTML responses are stored and delivered on-demand rather than needing to be re-generated at request-time. As a front-end developer who uses modern hosting solutions to deliver my websites where I don’t even need to think about this level of configuration, I’m not going to pretend to be an expert on caching. But I will share this nugget of information from Google’s article, <a href="https://web.dev/articles/optimize-ttfb" target="_blank">Optimize Time to First Byte</a>:</p><blockquote class="post__blockquote"><p class="post__p">For sites that frequently update their content, even a short caching time can result in noticeable performance gains for busy sites, since only the first visitor during that time experiences the full latency back to the origin server, while all other visitors can reuse the cached resource from the edge server.</p></blockquote><p class="post__p">Similarly to TLS negotiation, as a front end developer in 2024, this is something we don’t often have to worry about; it’s taken care of for us thanks to the tools at our disposal. And speaking of modern tools, many front end frameworks and libraries are now bringing HTML streaming to the masses.</p><h4 class="post__h4">Harness the power of HTML streaming</h4><p class="post__p">HTML streaming is where, instead of serving the entire HTML document at once, servers send pieces of it over time. The browser receives these pieces of HTML and can start parsing them, and even rendering them, so the web page can appear to load more quickly. Instead of waiting to receive an entire HTML document in between the <code>requestStart</code> and <code>responseStart</code> events, which may also involve database calls and other logic processing, HTML streaming allows for the <code>responseStart</code> event to happen earlier, thus reducing the TTFB.</p><p class="post__p">If you’re working in the React ecosystem and want to know more, Lazar goes in-depth on HTML streaming in <a href="https://blog.sentry.io/the-forensics-of-react-server-components/" target="_blank">The Forensics of React Server Components</a>.</p><h2 class="post__h2">Knowledge is power</h2><p class="post__p">All of this data about what happens before a web browser receives the first byte of data of a web page is pretty empowering. But the real power lies in putting that data in context in the Sentry Trace View. In being able to visualize and trace <code>PerformanceNavigationTiming</code> events and issues, we open up the door to being able to effectively debug slow parts of that timeline at a granular level, and make those all-important micro-optimizations where possible.</p><p class="post__p">If your web pages and applications are as fast as they can be, however, hopefully this article has given you some useful information. Perhaps you could use your new-found knowledge on DNS to wow people at all the cool parties you probably attend.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>I conducted a community survey and here’s what I learned</title>
          <description>Oh sweet, delicious, data. Get in my mouth.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/community-survey-2024/</link>
          <guid>https://whitep4nth3r.com/blog/community-survey-2024/</guid>
          <pubDate>Mon, 22 Jul 2024 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Last month I had a crisis of confidence. And so I do what everyone does when they doubt themselves. I ran a community survey (lol) 😅. Whilst it was admittedly a rushed, unplanned and reactionary activity, the results have been quite interesting — especially in proving a few hypotheses I had about people who know me and consume my content on the internet.</p><p class="post__p">I shared this survey on LinkedIn, Twitter, Twitch and The Claw Discord. I’m not sure how many people <b class="post__p--italic">saw</b> the link to the survey, but I received 138 responses. I think that’s a great turnout! <b class="post__p--italic">(Note to self: next time ask people where they found the link to the survey in the actual survey.)</b></p><p class="post__p">Let&#39;s take a look at the findings.</p><h2 class="post__h2">There are a lot of senior technologists out there</h2><p class="post__p">… but the internet is saturated with content for beginners.</p><ul><li><p class="post__p">How to get started with <code>[LIBRARY/FRAMEWORK/INTEGRATION]</code></p></li><li><p class="post__p">How to build your first <code>[INSERT_THING_HERE]</code></p></li><li><p class="post__p">How to get your <code>[FIRST/NEXT]</code> job in tech</p></li><li><p class="post__p">A day in the life of a software engineer (OMG SO GLAMOROUS, MUCH TECH)</p></li><li><p class="post__p">What is <code>[WEB_FUNDAMENTAL_HERE]</code>?</p></li></ul><p class="post__p">Why is there so much of this stuff? Because content for beginners gets views; everyone can be a beginner in tech and and a result, this type of content can appeal to and be accessible to <b class="post__p--italic">anyone</b>. If you’re a company, “getting started” content gets people using your product quickly. And it’s a great gig for creators if you’re on a video-centric platform. Get the views, grow the channel, get paid. Beginner content is also theoretically quicker to create (but disclaimer: <b class="post__p--italic">not necessarily easier to create well</b>). Spin up a framework boilerplate project and talk through the moving parts; job done. Build a quick chatbot app that won’t scale in production; no problem. Explain a mind-blowing concept in under 60 seconds; people will bookmark and share that.</p><p class="post__p">There’s definitely a place for this type of content. It won’t provide any solutions to that obscure problem you’ve been Googling for two weeks, but it will get people interested in technology and the possibilities it unfolds. And I’m sure there are many developers who have been inspired by this type of content to get into tech and make a great career for themselves. But what about when you’ve got a steady job in tech — what do you consume on the internet to help you level up, make you think, or be entertained by?</p><h3 class="post__h3">91% of my audience are not beginners</h3><p class="post__p">96% of people who took part in my survey work in tech (not just limited to software engineers). And 91% of those participants have 3+ years experience in tech. <b class="post__p--bold">That’s 91% of my audience who are not beginners</b>. I acknowledge that there is some bias in this survey in that I surveyed <b class="post__p--italic">my audience</b>, people who already know me. In the past I hypothesised in many conversations with my peers that <b class="post__p--italic">most</b> of my audience are pretty senior in terms of technical experience given the types of discussions we often have in the Discord or live on stream. And now I have the data that proves it.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6eHmcmM3oluHBneCoo39dw/8b03cbb7aa1fd928e8f1d8c2f4ba0d04/survey_graph_seniority.png" alt="If you work in tech, how many years of experience in the industry do you have? A pie chart showing 57.9% for 10 plus years, 15.8% for 6-10 years, 17.3% for 3-5 years and 9% for 0-2 years." height="642" width="1310" /><p class="post__p">In the crisis of confidence referenced at the top of this post, I started to doubt my place on the internet. Fewer people than I’d like engage with the things I create <b class="post__p--bold">for work</b>, which is understandable given it is aimed at mid-senior level developers. And my approach to streaming and building my non-work weird projects on stream maybe doesn’t appeal to a very wide audience. In light of this I wondered, do I need to experiment with different types of content? Take a different approach, perhaps? Do I finally need to create that course for beginners I’ve been thinking about for all those years? Do I need to stop building stupid (albeit fun) projects and create things that people can take more seriously so that they can take <b class="post__p--italic">me</b> more seriously? For now I think the answer to all these questions is “no”.</p><p class="post__p">Honestly, I have no idea what “good” really looks like in terms of numbers; and I’m usually the first person to say numbers don’t matter. What matters is the value you can offer to individuals. Something that I hope I’ll be cited for when I am one day completely irrelevant is, “If anything I do, say or create helps at least one person, then I have succeeded.”</p><h2 class="post__h2">People enjoy consuming technical content on the internet outside of work hours</h2><p class="post__p">90% of survey respondents said they enjoyed consuming technical content <b class="post__p--italic">outside of work hours</b>. This number surprised me given the survey was completed by mainly mid-senior level people. As a rather senior technologist, I prefer to stay away from the internet entirely after work hours — but this could be due to my home/family situation and the different types of offline hobbies I have, so I think there’s a lot of nuance to this that probably can’t be captured in a question with a binary answer.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7DFrqScLWOpHvjX3lXI1z0/976ba7a608e9d6e6aa6abb7672deb318/survey_graph_outside_work.png" alt="Do you enjoy consuming technical content on the internet outside of work hours? Pie chart showing 89.1% yes, 10.9% no." height="642" width="1360" /><p class="post__p">Most likely, people with jobs in tech don’t have the time during work hours to consume content online, and so they save it up for later to watch/read/listen at their own leisure. It could simply be that people enjoy listening to tech podcasts on their daily commute (which is technically outside of work hours).</p><h2 class="post__h2">What very senior technologists want to consume</h2><p class="post__p">The survey included a free text field to answer the question “What are you looking for in online tech content?” Here&#39;s a summary of the answers for survey respondents who had 10+ years of experience. Senior technologists want to consume content that is:</p><ol><li><p class="post__p"><b class="post__p--bold">Educational and practical</b>: Developers want comprehensive, in-depth tutorials and guides based on real-world applications and practical examples. Two particular requests stand out: “Advanced shit,” and “Breakdowns and deep subjects. Surface level stuff is kinda saturated.”</p></li><li><p class="post__p"><b class="post__p--bold">Entertaining and fun</b>: Developers value content that is not only educational but also engaging and entertaining. (Again, this probably highlights the survey bias here, as you probably know by now I claim to write code for your entertainment.)</p></li><li><p class="post__p"><b class="post__p--bold">Relevant and timely</b>: Developers want up-to-date information on industry trends and recent advancements to help keep their skills fresh at work.</p></li><li><p class="post__p"><b class="post__p--bold">Unique and creative</b>: Developers also value unique, creative, and unconventional content, including quirky projects and surprising ideas. (Maybe I’m onto something with my stupid projects, after all.)</p></li><li><p class="post__p"><b class="post__p--bold">Concise and clear</b>: Developers prefer straightforward, concise content without unnecessary waffle or theatrics, focusing on clear instructions and explanations. Some respondents specifically asked for “No drama!”</p></li></ol><p class="post__p">All of the above doesn’t really tell me anything new, because all of the above is <b class="post__p--italic">what I want</b>, and I have always strived to create what I want to consume. That being said, it’s delightful to gather data that confirms my hypotheses, and it gives me comfort that by continuing to do what I’ve been doing, I will be satisfying at least one person somewhere, somehow.</p><h2 class="post__h2">How senior technologists like to learn</h2><p class="post__p">The final survey question asked participants how they like to learn. Surprisingly, written content came out on top, followed by video content, courses, live content, podcasts and then “other”. It was encouraging to see written content at the top of the list, given that’s the type of content I feel like I’m best at creating to help people learn.</p><h2 class="post__h2">Conclusions</h2><p class="post__p">There’s a market out there for any type of technical content that people will choose to consume. We’re in some weird kind of <a href="https://en.wikipedia.org/wiki/Attention_economy" target="_blank">Attention Economy</a>, and I’d like to think that in this economy, particularly internet-savvy connoisseurs (usually senior technologists) will be very intentional about what they consume. And if they choose to consume what I create, whether that’s through reading my latest blog post, catching the odd live stream, or bookmarking something I post on the socials, then I’ve achieved something worthy of celebrating.</p><p class="post__p">Now if only there was something to prevent me having such regular crises of confidence. Maybe I&#39;ll consume some content on that.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Your bad LCP score might be a backend issue</title>
          <description>This is the best way to debug slow web pages.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/your-bad-lcp-score-might-be-a-backend-issue/</link>
          <guid>https://blog.sentry.io/your-bad-lcp-score-might-be-a-backend-issue/</guid>
          <pubDate>Tue, 02 Jul 2024 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Largest Contentful Paint (LCP) is a Core Web Vital (CWV) metric that marks the point in the page load timeline where the main page content has likely finished loading. To get a good LCP score, the main content on a web page must finish loading in under 2.5 seconds.</p><h2 class="post__h2">How to check an LCP score</h2><p class="post__p">You can conduct a one-off page speed audit to check an LCP score using tools like <a href="https://pagespeed.web.dev/" target="_blank">Page Speed Insights</a> from Google, or <a href="https://developer.chrome.com/docs/lighthouse/overview" target="_blank">Lighthouse</a> in any Chromium browser dev tools, which will produce a report that looks something like this:</p><img src="https://images.ctfassets.net/56dzm01z6lln/30bOvZOwgsRNqP2PGsMPk9/eb933cd4495d1cdeba2336733b689e30/1_page_speed_report_empower_plant.png" alt="A Page Speed Insights report showing a performance score of 59. FCP is 2.6s. LCP is 4.4s. TBT is 250,s. CLS is 0.193. Speed Index is 9.3s.
" height="2221" width="2000" /><p class="post__p">The overall <a href="https://docs.sentry.io/product/insights/web-vitals/#performance-score" target="_blank">performance score</a> of 59 has been calculated from a weighted combination of five different metrics, shown in the metrics grid. If you’d like a more in-depth read about this score calculation, check out <a href="https://blog.sentry.io/how-to-hack-your-google-lighthouse-scores-in-2024/" target="_blank">How To Hack Your Google Lighthouse Scores In 2024</a>.</p><p class="post__p">Metric scores are graded as “poor,” “needs improvement,” or “good,” and are identified by a color and shape key. The LCP score for this page on this test is 4.4 seconds, which is poor. Additionally, notice at the bottom of the image the frames that were captured during the page load timeline, which shows what a user would experience as the main content loads. We know this is bad, but how do we debug the root cause?</p><h2 class="post__h2">What causes a bad LCP score?</h2><p class="post__p">To begin to debug why an LCP score is slow, you can select to show audits relevant to LCP under the visual page load timeline.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1aKxFeS95wYw6PUBR3Piu9/1539a2a04d278e16117f6305e989e7ab/2_lcp_fixes_from_page_speed_insights.png" alt="Audits relating to LCP under the heading diagnostics. Largest contentful paint element 4420ms. Reduce unused Javascript, potential savings of 63KiB. Serve images in next-gen formats, potential savings of 124KiB. Properly size images, potential savings of 131KiB. Eliminate render-blocking resources, potential savings of 1830ms. Efficiently encode images, potential savings of 15KiB. " height="2093" width="2000" /><p class="post__p">Page Speed Insights is advising that our slow LCP could be improved if we:</p><ul><li><p class="post__p">Reduce unused JavaScript</p></li><li><p class="post__p"><a href="https://blog.sentry.io/low-effort-image-optimization-tips/#ditch-jpeg-for-avif--webp" target="_blank">Serve images in next-gen formats</a> (such as webp and avif formats)</p></li><li><p class="post__p"><a href="https://blog.sentry.io/low-effort-image-optimization-tips/#use-a-picture-element-instead-of-just-img" target="_blank">Properly size images</a></p></li><li><p class="post__p"><a href="https://blog.sentry.io/5-easy-tips-to-improve-your-personal-website-performance/#4-remove-render-blocking-resources" target="_blank">Eliminate render-blocking resources</a></p></li><li><p class="post__p">Efficiently encode images (reduce file size without compromising quality)</p></li></ul><p class="post__p">Expanding the item provides more information for each recommendation. Let’s expand the top item — The Largest Contentful Paint element — to see what insights we can gain.</p><img src="https://images.ctfassets.net/56dzm01z6lln/eB4haUkqav9ilimTBuD3T/0caaa5fba14e4ecd933b0306777f65c0/3_lcp_diagnostics_no_help.png" alt="Largest Contentful Paint element 4420ms. This is the largest contentful element within the viewport. TTFB 14% of LCP, 600ms. Load delay 81%, 3560ms. Load time 5%, 240ms. Render delay 0%, 20ms." height="1278" width="2000" /><p class="post__p">Unfortunately, this panel isn’t telling us anything we didn’t already know. The table shows, we can see that the “Load Delay” phase accounts for 81% of the LCP, which we observed in the visual page load timeline above. <b class="post__p--bold">But this report can’t tell us why there’s a load delay</b> — and this is what we need to debug!</p><p class="post__p">Tools like Page Speed Insights and Google Lighthouse are great for diagnosing issues and providing actionable advice for front-end performance based on data from a single isolated lab test. What these tools can’t do, however, is evaluate performance across your entire stack of distributed services and applications for a sample of your real users. How do you investigate if the poor LCP score is actually due to an issue in the backend?</p><p class="post__p">Here’s where you need <a href="https://blog.sentry.io/dont-observe-debug/" target="_blank">tracing</a>.</p><h2 class="post__h2">How to use tracing to debug a bad LCP score</h2><p class="post__p">By tracking the complete end-to-end user journey from the moment a page request is made in a browser across all downstream systems and services to the database and back, <a href="https://docs.sentry.io/concepts/key-terms/tracing/?original_referrer=https://sentry.io/" target="_blank">tracing</a> helps you identify specific operations causing any performance issues in your full application stack. Each trace comprises a collection of spans. A span is an atomic event full of metadata and contextual information, which helps you further understand the interconnected parts of your distributed systems. Spans are connected and associated with a trace as a result of a unique identifier sent via an HTTP header across those systems. Tracing is also useful in standalone applications that have both a server-side and client-side runtime, such as modern JavaScript meta-frameworks.</p><p class="post__p">We can use tracing to find the root cause of a poor LCP score by tracing the waterfall of events from the moment a user lands on the products page to when the main content finally loads. Let’s go through it step by step.</p><p class="post__p">Or you could skip all of this and scroll right down to <a href="https://blog.sentry.io/your-bad-lcp-score-might-be-a-backend-issue/#but-wait-theres-more" target="_blank">this part and just use the Trace Explorer</a> if you’re already using Sentry.</p><h3 class="post__h3">Step 0: Suspect you have a real LCP problem</h3><p class="post__p">Most of us can probably tell when a page is slow to load so you might end up skipping this step. That being said, if you did a CWV test using your high-spec dev machine using high-speed internet and the scores were <b class="post__p--italic">bad</b>, then you know you have a real problem to solve.</p><h3 class="post__h3">Step 1: Install an Application Performance Monitoring tool SDK (like Sentry) across your entire suite of apps and services</h3><p class="post__p">Sentry provides an <a href="https://sentry.io/platforms/" target="_blank">abundance of SDKs</a> for a variety of programming languages and frameworks. First, <a href="https://sentry.io/signup/" target="_blank">sign up to Sentry</a> and create a new project for each of your applications. Each project will have a unique DSN (Data Source Name), which is what you’ll use to point your app to your Sentry project in your code.</p><p class="post__p">Let’s say you’re using React for your front end. Install the <a href="https://sentry.io/for/react/" target="_blank">Sentry React SDK</a> via your package manager of choice.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="najRzGeoWa"
      aria-describedby="najRzGeoWa">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="najRzGeoWa">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="najRzGeoWa" itemprop="text" content="npm%20install%20%40sentry%2Freact%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> @sentry/react</code></pre>
    </div>
  </div>

  <p class="post__p">Initialize Sentry in just a few lines of code and configure the <a href="https://docs.sentry.io/platforms/javascript/configuration/integrations/browsertracing/" target="_blank">browser tracing integration</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="SElSqrcSwq"
      aria-describedby="SElSqrcSwq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="SElSqrcSwq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="jsx">
      <meta data-code-id="SElSqrcSwq" itemprop="text" content="import%20React%20from%20%22react%22%3B%0Aimport%20ReactDOM%20from%20%22react-dom%22%3B%0Aimport%20*%20as%20Sentry%20from%20%22%40sentry%2Freact%22%3B%0Aimport%20App%20from%20%22.%2FApp%22%3B%0A%0ASentry.init(%7B%0A%20%20dsn%3A%20%22YOUR_PROJECT_DSN%22%2C%0A%20%20%20integrations%3A%20%5B%0A%20%20%20%20%2F%2F%20Enable%20tracing%0A%20%20%20%20Sentry.browserTracingIntegration()%2C%0A%20%20%5D%2C%0A%20%20%2F%2F%20Configure%20tracing%0A%20%20tracesSampleRate%3A%201.0%2C%20%2F%2F%20%20Capture%20100%25%20of%20the%20transactions%0A%7D)%3B%0A%0AReactDOM.render(%3CApp%20%2F%3E%2C%20document.getElementById(%22root%22))%3B%0A">
      <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> React <span class="token keyword">from</span> <span class="token string">"react"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> ReactDOM <span class="token keyword">from</span> <span class="token string">"react-dom"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> Sentry <span class="token keyword">from</span> <span class="token string">"@sentry/react"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> App <span class="token keyword">from</span> <span class="token string">"./App"</span><span class="token punctuation">;</span><br><br>Sentry<span class="token punctuation">.</span><span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">dsn</span><span class="token operator">:</span> <span class="token string">"YOUR_PROJECT_DSN"</span><span class="token punctuation">,</span><br>   <span class="token literal-property property">integrations</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token comment">// Enable tracing</span><br>    Sentry<span class="token punctuation">.</span><span class="token function">browserTracingIntegration</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token comment">// Configure tracing</span><br>  <span class="token literal-property property">tracesSampleRate</span><span class="token operator">:</span> <span class="token number">1.0</span><span class="token punctuation">,</span> <span class="token comment">//  Capture 100% of the transactions</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>ReactDOM<span class="token punctuation">.</span><span class="token function">render</span><span class="token punctuation">(</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">App</span></span> <span class="token punctuation">/></span></span><span class="token punctuation">,</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">"root"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">For your backend, let’s say you’re running a <a href="https://sentry.io/for/ruby/?platform=sentry.ruby.rails" target="_blank">Ruby on Rails app</a>. Add <code>sentry-ruby</code> and <code>sentry-rails</code> to your Gemfile:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="uwMwNWuwCc"
      aria-describedby="uwMwNWuwCc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="uwMwNWuwCc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="uwMwNWuwCc" itemprop="text" content="gem%20%22sentry-ruby%22%0Agem%20%22sentry-rails%22">
      <pre class="language-bash"><code class="language-bash">gem <span class="token string">"sentry-ruby"</span><br>gem <span class="token string">"sentry-rails"</span></code></pre>
    </div>
  </div>

  <p class="post__p">And initialize the SDK within your <code>config/initializers/sentry.rb</code>:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZKmFydQmwF"
      aria-describedby="ZKmFydQmwF">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZKmFydQmwF">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="ruby">
      <meta data-code-id="ZKmFydQmwF" itemprop="text" content="Sentry.init%20do%20%7Cconfig%7C%0A%20%20config.dsn%20%3D%20'YOUR_PROJECT_DSN'%0A%20%20config.breadcrumbs_logger%20%3D%20%5B%3Aactive_support_logger%5D%0A%0A%20%20%23%20To%20activate%20Tracing%2C%20set%20one%20of%20these%20options.%0A%20%20%23%20We%20recommend%20adjusting%20the%20value%20in%20production%3A%0A%20%20config.traces_sample_rate%20%3D%200.5%0A%20%20%23%20or%0A%20%20config.traces_sampler%20%3D%20lambda%20do%20%7Ccontext%7C%0A%20%20%20%20true%0A%20%20end%0Aend">
      <pre class="language-ruby"><code class="language-ruby">Sentry<span class="token punctuation">.</span>init <span class="token keyword">do</span> <span class="token operator">|</span>config<span class="token operator">|</span><br>  config<span class="token punctuation">.</span>dsn <span class="token operator">=</span> <span class="token string-literal"><span class="token string">'YOUR_PROJECT_DSN'</span></span><br>  config<span class="token punctuation">.</span>breadcrumbs_logger <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token symbol">:active_support_logger</span><span class="token punctuation">]</span><br><br>  <span class="token comment"># To activate Tracing, set one of these options.</span><br>  <span class="token comment"># We recommend adjusting the value in production:</span><br>  config<span class="token punctuation">.</span>traces_sample_rate <span class="token operator">=</span> <span class="token number">0.5</span><br>  <span class="token comment"># or</span><br>  config<span class="token punctuation">.</span>traces_sampler <span class="token operator">=</span> lambda <span class="token keyword">do</span> <span class="token operator">|</span>context<span class="token operator">|</span><br>    <span class="token boolean">true</span><br>  <span class="token keyword">end</span><br><span class="token keyword">end</span></code></pre>
    </div>
  </div>

  <p class="post__p">Check out the <a href="https://docs.sentry.io/" target="_blank">Sentry docs</a> for your preferred SDK, or create a new project in Sentry where you’ll be guided through the setup process.</p><h3 class="post__h3">Step 2: Sit back and relax whilst your APM tool (Sentry) collects data from real users</h3><p class="post__p">Well, you probably won’t be able to relax entirely. Your app is crap and slow to load. Instead, use this time to read angry <a href="https://docs.sentry.io/product/user-feedback/" target="_blank">User Feedback</a> requests about how much of a terrible developer you are. Don’t worry; it’ll all be worth it when you finally find the real source of the LCP performance bottleneck.</p><p class="post__p">Depending on how much traffic your website gets, you might need to wait a few days to get enough data.</p><h3 class="post__h3">Step 3: Explore your Core Web Vitals scores to find aggregated field data that supports your lab data</h3><p class="post__p">In Sentry, navigate to Insights &gt; Web Vitals for a quick overview of your project’s overall performance score and CWV. Click the table column headings to sort the data by “LCP” descending to confirm the field data lines up with your lab data. It does? Great.</p><img src="https://images.ctfassets.net/56dzm01z6lln/61VyYOPq5IZmICDmGhdKZa/7daacdc2bd2cb58c535702c3934a0308/4_web_vitals_sentry.png" alt="Web vitals view in Sentry. Performance score is 84 and the following p75 scores are visible: LCP 2.67s, FCP 73ms, INP none, CLS 0.25, TTFB 0ms.
" height="1152" width="2000" /><h3 class="post__h3">Step 4: Find sampled page load events with a poor LCP score</h3><p class="post__p">On the Web Vitals insights page, we’re going to click into the page route with the worst performance to view sampled data for that page route only. In our case, it’s the “/products” page. The Core Web Vitals will look different here (i.e. worse), as they’re calculated using data from just the products page rather than the full application.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5pE0ZIlYK6KMFdvcQgtmsG/01dde7da03130d4b04127ec928d119dd/5_products_page_web_vitals_summary.png" alt="Sentry page summary view for the products page. Performance score is 68 and the following p75 scores are visible: LCP 6.55s, FCP 63ms, INP none, CLS 0.25, TTFB 0ms." height="1152" width="2000" /><p class="post__p">The next step is to explore the full trace of events. There are many ways into the trace view in Sentry, but from this CWV summary, you can click into the LCP score block (which in this case is 6.55s) to view a set of sampled data with a variety of scores across the spectrum.</p><img src="https://images.ctfassets.net/56dzm01z6lln/78pq0xOtfdIzG5rwOhWaSP/07a546e53223700646dea4ad59980a45/6_sampled_data_products_page_web_vitals.png" alt="The LCP panel is open showing a list of sampled events that show a range of different performance scores, ranging from 0 to 90." height="1152" width="2000" /><p class="post__p">Click on the transaction at the top of the table with the worst LCP score to view the trace. Here’s where all of your debugging dreams will come true.</p><h3 class="post__h3">Step 5: Explore the full trace of those page load events to discover where the performance bottlenecks are happening</h3><p class="post__p">The trace view shows when the LCP happened in the timeline. Every span that was sent to Sentry before the LCP happened is now guilty until proven innocent. Thankfully, we don’t need to interrogate every single span given Sentry has already highlighted the likely causes of any slowdowns with a warning symbol. Expand the spans to investigate deeper until you find the root cause of any bottlenecks. In the case of this application, two slow database queries that happen when a request is made to the products page on the front end are causing the poor LCP score.</p><img src="https://images.ctfassets.net/56dzm01z6lln/X8lLKlG68ORGmkkq50oBP/faf7d39ae720d4fa9e773deccce30be2/7_trace_view.png" alt="The trace view in Sentry and the following areas are highlighted. The LCP in the timeline is labeled and happens at around 10 seconds. When the products page is requested, it immediately makes a call to a backend API. This subsequently makes two database queries that are slow and not optimized. This is causing the poor LCP score." height="1152" width="2000" /><p class="post__p">We successfully found the root cause of a front end performance issue that didn’t have an obvious fix. We identified the slowdown happened as a result of two specific database queries. It was almost too easy! Now, it’s time to optimize those database queries, which might not be as easy.</p><h2 class="post__h2">But wait, there’s more!</h2><p class="post__p">You can skip steps 3-5 and go directly to the <a href="https://docs.sentry.io/product/explore/traces/" target="_blank">Trace Explorer</a> (currently in beta) by navigating to Explore &gt; Trace. On this view, you can filter all spans across your projects by using the search field (which provides hints on what criteria you can filter by).</p><p class="post__p">After discovering the slow LCP issue on the products page, I can use the Trace Explorer to filter all spans by <code>span.description:/products</code> to get a quick overview of the span durations associated with those spans. If I notice anything awry, I can click directly into the trace to investigate.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1Mh20dX7cif2cVNdNiof6y/1972200f2ae1c36320dc1592467a8907/8_trace_explorer.png" alt="The trace explorer in Sentry which currently has a beta label shown next to the title of the page. This is a list of traces captured by Sentry where you can filter by span information. There’s a graph visualising matching spans for the query under the search box, and al list of traces below which you can click on to view in the trace view." height="1152" width="2000" /><h2 class="post__h2">Try the full debugging experience in real-time</h2><p class="post__p">To get an idea of how this Core Web Vitals-based debugging experience might look and feel in the Sentry product, click through this interactive demo.</p>
    <div class="post__arcadeEmbed">
      <div style="position: relative; padding-bottom: calc(54.13385826771654% + 41px); height: 0; width: 100%;"><iframe src="https://demo.arcade.software/vDGzzlUAUFN3aKD526xy?embed&show_copy_link=true" title="Web Vitals &gt; Trace View &gt; Diagnosis" frameborder="0" loading="lazy" webkitallowfullscreen mozallowfullscreen allowfullscreen allow="clipboard-write" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;color-scheme: light;"></iframe></div>
    </div><h2 class="post__h2">But wait, there’s even more!</h2><p class="post__p">If you’re still not convinced, check out this live Sentry workshop that I recently hosted with Lazar. You’ll get even more insight into how to use Sentry to debug your front-end issues that might have backend solutions, you’ll hear from me about a terrible debugging story that still haunts me to this day, and there may be some jokes. Leave a fun comment if you stop by!</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/-I872moWyB0?si=ZYomCGR2VRB2Zuaj"
        title="Tracing: Frontend issues with backend solutions (Sentry workshop)"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How To Hack Your Google Lighthouse Scores In 2024</title>
          <description>I put on my lab coat and science googles to investigate.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.smashingmagazine.com/2024/06/how-hack-google-lighthouse-scores-2024/</link>
          <guid>https://www.smashingmagazine.com/2024/06/how-hack-google-lighthouse-scores-2024/</guid>
          <pubDate>Tue, 11 Jun 2024 11:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Google Lighthouse has been one of the most effective ways to gamify and promote web page performance among developers. Using Lighthouse, we can assess web pages based on overall performance, accessibility, SEO, and what Google considers “best practices”, all with the click of a button.</p><p class="post__p">We might use these tests to evaluate out-of-the-box performance for front-end frameworks or to celebrate performance improvements gained by some diligent refactoring. And you know you love sharing screenshots of your perfect Lighthouse scores on social media. It’s a well-deserved badge of honor worthy of a confetti celebration.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3So4B3518kS2ccCAExzRXQ/2c742525f0199fe3d708624376b93ab3/lh_confetti.gif" alt="Animated gif of four perfect Google Lighthouse scores with confetti popping in all over the place" height="298" width="556" /><p class="post__p">Just the fact that Lighthouse gets developers like us talking about performance is a win. But, whilst I don’t want to be a party pooper, the truth is that web performance is far more nuanced than this. In this article, we’ll examine how Google Lighthouse calculates its performance scores, and, using this information, we will attempt to “hack” those scores in our favor, <b class="post__p--bold">all in the name of fun and science</b> — because in the end, Lighthouse is simply a good, but rough guide for debugging performance. We’ll have some fun with it and see to what extent we can “trick” Lighthouse into handing out better scores than we may deserve.</p><p class="post__p">But first, let’s talk about data.</p><h2 class="post__h2">Field data is important</h2><p class="post__p">ocal performance testing is a great way to understand if your website performance is trending in the right direction, but it won’t paint a full picture of reality. The World Wide Web is the Wild West, and collectively, we’ve almost certainly lost track of the variety of device types, internet connection speeds, screen sizes, browsers, and browser versions that people are using to access websites — all of which can have an impact on page performance and user experience.</p><p class="post__p">Field data — and lots of it — collected by an <a href="http://sentry.io/for/performance/" >application performance monitoring</a> tool like Sentry from real people using your website on their devices will give you a far more accurate report of your website performance than your lab data collected from a small sample size using a high-spec super-powered dev machine under a set of controlled conditions. Philip Walton reported in 2021 that “<a href="https://philipwalton.com/articles/my-challenge-to-the-web-performance-community/" target="_blank">almost half of all pages that scored 100 on Lighthouse didn’t meet the recommended Core Web Vitals thresholds</a>” based on data from the HTTP Archive.</p><p class="post__p">Web performance is more than a single <a href="https://sentry.io/for/web-vitals/" target="_blank">core web vital metric</a> or Lighthouse performance score. What we’re talking about goes way beyond the type of raw data we’re working with.</p><h2 class="post__h2">Web performance is more than numbers</h2><p class="post__p"><b class="post__p--italic">Speed</b> is often the first thing that comes up when talking about web performance — just how long does a page take to load? This isn’t the worst thing to measure, but we must bear in mind that speed is probably influenced heavily by business KPIs and sales targets. Google <a href="https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/" target="_blank">released a report in 2018</a> suggesting that the probability of bounces increases by 32% if the page load time reaches higher than three seconds, and soars to 123% if the page load time reaches 10 seconds. So, we must conclude that converting more sales requires reducing bounce rates. And to reduce bounce rates, we must make our pages <b class="post__p--italic">load faster</b>.</p><p class="post__p">But what does “load faster” even mean? At some point, we’re physically incapable of making a web page load any faster. Humans — and the servers that connect them — are spread around the globe, and modern internet infrastructure can only deliver so many bytes at a time.</p><p class="post__p">The bottom line is that page load is not a single moment in time. In an article titled “<a href="https://web.dev/articles/what-is-speed" target="_blank">What is speed?</a>” Google explains that a page load event is:</p><blockquote class="post__blockquote"><p class="post__p"><b class="post__p--italic">[…] “an experience that no single metric can fully capture. There are multiple moments during the load experience that can affect whether a user perceives it as ‘fast’, and if you just focus solely on one, you might miss bad experiences that happen during the rest of the time.”</b></p></blockquote><p class="post__p">The key word here is <b class="post__p--italic">experience</b>. Real web performance is less about <b class="post__p--italic">numbers</b> and <b class="post__p--italic">speed</b> than it is about <b class="post__p--italic">how we experience</b> page load and page usability as users. And this segues nicely into a discussion of how Google Lighthouse calculates performance scores. (It’s much less about pure speed than you might think.)</p><h2 class="post__h2">How Google Lighthouse performance scores are calculated</h2><p class="post__p">The Google Lighthouse performance score is calculated using a weighted combination of scores based on core web vital metrics (i.e., First Contentful Paint (FCP), Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS)) and other speed-related metrics (i.e., Speed Index (SI) and Total Blocking Time (TBT)) that are <b class="post__p--bold">observable throughout the page load timeline</b>.</p><p class="post__p">This is <a href="https://developer.chrome.com/docs/lighthouse/performance/performance-scoring/#weightings" target="_blank">how the metrics are weighted</a> in the overall score:</p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header">Metric</th><th class="post__table__header">Weighting (%)</th></tr><tr class="post__table__row"><td class="post__table__cell">Total Blocking Time</td><td class="post__table__cell">30</td></tr><tr class="post__table__row"><td class="post__table__cell">Cumulative Layout Shift</td><td class="post__table__cell">25</td></tr><tr class="post__table__row"><td class="post__table__cell">Largest Contentful Paint</td><td class="post__table__cell">25</td></tr><tr class="post__table__row"><td class="post__table__cell">First Contentful Paint</td><td class="post__table__cell">10</td></tr><tr class="post__table__row"><td class="post__table__cell">Speed Index</td><td class="post__table__cell">10</td></tr></tbody>
      </table></div><p class="post__p">The weighting assigned to each score gives us insight into how Google prioritizes the different building blocks of a good user experience:</p><h3 class="post__h3">1. A web page should respond to user input</h3><p class="post__p">The highest weighted metric is <b class="post__p--bold">Total Blocking Time (TBT),</b> a metric that looks at the total time after the <b class="post__p--bold">First Contentful Paint (FCP)</b> to help indicate where the main thread may be blocked long enough to prevent speedy responses to user input. The main thread is considered “blocked” any time there’s a JavaScript task running on the main thread for more than 50ms. Minimizing TBT ensures that a web page responds to physical user input (e.g., key presses, mouse clicks, and so on).</p><h3 class="post__h3">2. A web page should load useful content with no unexpected visual shifts</h3><p class="post__p">The next most weighted Lighthouse metrics are <b class="post__p--bold">Largest Contentful Paint (LCP</b>) and <b class="post__p--bold">Cumulative Layout Shift (CLS)</b>. LCP marks the point in the page load timeline when the page’s main content has <b class="post__p--italic">likely</b> loaded and is therefore <b class="post__p--italic">useful</b>.</p><p class="post__p">At the point where the main content has likely loaded, you also want to maintain visual stability to ensure that users can use the page and are not affected by unexpected visual shifts (CLS). A good LCP score is anything less than 2.5 seconds (which is a lot higher than we might have thought, given we are often trying to make our websites <b class="post__p--italic">as fast as possible</b>).</p><h3 class="post__h3">3. A web page should load something</h3><p class="post__p">The <b class="post__p--bold">First Contentful Paint (FCP)</b> metric marks the first point in the page load timeline where the user can see <b class="post__p--italic">something</b> on the screen, and the <b class="post__p--bold">Speed Index (SI)</b> measures how quickly content is visually displayed during page load over time until the page is “complete”.</p><p class="post__p">Your page is scored based on the speed indices of real websites using performance <a href="https://developer.chrome.com/docs/lighthouse/performance/performance-scoring#metric-scores" target="_blank">data from the HTTP Archive</a>. A good FCP score is less than 1.8 seconds and a good SI score is less than 3.4 seconds. Both of these thresholds are higher than you might expect when thinking about <b class="post__p--italic">speed</b>.</p><h2 class="post__h2">Usability is favored over raw speed</h2><p class="post__p">Google Lighthouse’s performance scoring is, without a doubt, less about speed and more about <b class="post__p--bold">usability</b>. Your SI and FCP could be super quick, but if your LCP takes too long to paint, and if CLS is caused by large images or external content taking some time to load and shifting things visually, then your overall performance score will be lower than if your page was a little slower to render the FCP but didn’t cause any CLS. Ultimately, if the page is unresponsive due to JavaScript blocking the main thread for more than 50ms, your performance score will suffer more than if the page was a little slow to paint the FCP.</p><p class="post__p">To understand more about how the weightings of each metric contribute to the final performance score, you can play about with the sliders on the <a href="https://googlechrome.github.io/lighthouse/scorecalc/" target="_blank">Lighthouse Scoring Calculator</a>, and here’s a rudimentary table demonstrating the effect of skewed individual metric weightings on the overall performance score, proving that page usability and responsiveness is favored over raw speed.</p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header">Description</th><th class="post__table__header">FCP (ms)</th><th class="post__table__header">SI (ms)</th><th class="post__table__header">LCP (ms)</th><th class="post__table__header">TBT (ms)</th><th class="post__table__header">CLS</th><th class="post__table__header">Overall Score</th></tr><tr class="post__table__row"><td class="post__table__cell">Slow to show something on screen</td><td class="post__table__cell">6000</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">90</td></tr><tr class="post__table__row"><td class="post__table__cell">Slow to load content over time</td><td class="post__table__cell">0</td><td class="post__table__cell">5000</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">90</td></tr><tr class="post__table__row"><td class="post__table__cell">Slow to load the largest part of the page</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">6000</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">76</td></tr><tr class="post__table__row"><td class="post__table__cell">Visual shifts occurring during page load</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">0.82</td><td class="post__table__cell">76</td></tr><tr class="post__table__row"><td class="post__table__cell">Page is unresponsive to user input</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">0</td><td class="post__table__cell">2000</td><td class="post__table__cell">0</td><td class="post__table__cell">70</td></tr></tbody>
      </table></div><p class="post__p">The overall Google Lighthouse performance score is calculated by converting each raw metric value into a score from 0 to 100 according to where it falls on its Lighthouse scoring distribution, which is a <b class="post__p--bold">log-normal</b> distribution derived from the performance metrics of real website performance data from the HTTP Archive. There are two main takeaways from this mathematically overloaded information:</p><ol><li><p class="post__p">Your Lighthouse performance score is plotted against real website performance data, not in isolation.</p></li><li><p class="post__p">Given that the scoring uses log-normal distribution, the relationship between the individual metric values and the overall score is non-linear, meaning you can make substantial improvements to low-performance scores quite easily, but it becomes more difficult to improve an already high score.</p></li></ol><img src="https://images.ctfassets.net/56dzm01z6lln/5gZ9A0O0w3xJyM2g35wvDU/064a36c97b8ab4bf2443146eed590e97/dist_curve.png" alt="Log-normal distribution curve visualization, high on the left, low on the right." height="3078" width="5344" /><p class="post__p">Read more about <a href="https://developer.chrome.com/docs/lighthouse/performance/performance-scoring#metric-scores" target="_blank">how metric scores are determined</a>, including a visualization of the log-normal distribution curve on <a href="http://developer.chrome.com/" >developer.chrome.com</a>.</p><h2 class="post__h2">Can we &quot;trick&quot; Google Lighthouse?</h2><p class="post__p">I appreciate Google’s focus on usability over pure speed in the web performance conversation. It urges developers to think less about aiming for raw numbers and more about the real experiences we build. That being said, I’ve wondered whether today in 2024, it’s possible to fool Google Lighthouse into believing that a bad page in terms of <b class="post__p--italic">usability and usefulness</b> is actually a great one.</p><p class="post__p">I put on my lab coat and science goggles to investigate. All tests were conducted:</p><ul><li><p class="post__p">Using the Chromium Lighthouse plugin,</p></li><li><p class="post__p">In an incognito window in the Arc browser,</p></li><li><p class="post__p">Using the “navigation” and “mobile” settings (apart from where described differently),</p></li><li><p class="post__p">By me, in a lab (i.e., no field data).</p></li></ul><p class="post__p">That all being said, I fully acknowledge that my controlled test environment contradicts my advice at the top of this post, but the experiment is an interesting ride nonetheless. What I hope you’ll take away from this is that Lighthouse scores are only one piece — and a tiny one at that — of a very large and complex web performance puzzle. And, without field data, I’m not sure any of this matters anyway.</p><h2 class="post__h2">How to hack FCP and LCP scores</h2><p class="post__p"><b class="post__p--bold">TL;DR: Show the smallest amount of LCP-qualifying content on load to boost the FCP and LCP scores until the Lighthouse test has likely finished.</b></p><p class="post__p">FCP marks the first point in the page load timeline where the user can see <b class="post__p--italic">anything</b> at all on the screen, while LCP marks the point in the page load timeline when the main page content (i.e., the largest text or image element) has <b class="post__p--italic">likely</b> loaded. A fast LCP helps reassure the user that the page is <b class="post__p--italic">useful</b>. “Likely” and “useful” are the important words to bear in mind here.</p><h3 class="post__h3">What counts as an LCP element?</h3><p class="post__p">The types of elements on a web page considered by Lighthouse for LCP are:</p><ul><li><p class="post__p"><code>&lt;img&gt;</code></p><p class="post__p"> </p><p class="post__p">elements,</p></li><li><p class="post__p"><code>&lt;image&gt;</code></p><p class="post__p"> </p><p class="post__p">elements inside an</p><p class="post__p"> </p><p class="post__p"><code>&lt;svg&gt;</code></p><p class="post__p"> </p><p class="post__p">element,</p></li><li><p class="post__p"><code>&lt;video&gt;</code></p><p class="post__p"> </p><p class="post__p">elements,</p></li><li><p class="post__p">An element with a background image loaded using the</p><p class="post__p"> </p><p class="post__p"><code>url()</code></p><p class="post__p"> </p><p class="post__p">function, (and not a CSS gradient), and</p></li><li><p class="post__p">Block-level elements containing text nodes or other inline-level text elements.</p></li></ul><p class="post__p">The following elements are <b class="post__p--italic">excluded</b> from LCP consideration due to the likelihood they do not contain useful content:</p><ul><li><p class="post__p">Elements with zero opacity (invisible to the user),</p></li><li><p class="post__p">Elements that cover the full viewport (likely to be background elements), and</p></li><li><p class="post__p">Placeholder images or other images with low entropy (i.e., low informational content, such as a solid-colored image).</p></li></ul><p class="post__p">However, the notion of an image or text element being useful is completely subjective in this case and generally out of the realm of what machine code can reliably determine. For example, <a href="https://hacking-lighthouse.netlify.app/lcp/" target="_blank">I built a page</a> containing nothing but a <code>&lt;h1&gt;</code> element where, after 10 seconds, JavaScript inserts more descriptive text into the DOM and hides the <code>&lt;h1&gt;</code> element.</p><p class="post__p">Lighthouse considers the heading element to be the LCP element in this experiment. At this point, the page load timeline has finished, but the page’s main content has <b class="post__p--italic">not</b> loaded, even though Lighthouse thinks it is <b class="post__p--italic">likely</b> to have loaded within those 10 seconds. Lighthouse still awards us with a perfect score of 100 even if the heading is replaced by a single punctuation mark, such as a full stop, which is even <b class="post__p--italic">less useful</b>.</p><p class="post__p">This test suggests that if you need to load page content via client-side JavaScript, we‘ll want to avoid displaying a skeleton loader screen since that requires loading more elements on the page. And since we know the process will take some time — and that we can offload the network request from the main thread to a web worker so it won’t affect the TBT — we can use some arbitrary “splash screen” that contains a minimal viable LCP element (for better FCP scoring). This way, we’re giving Lighthouse the <b class="post__p--italic">impression</b> that the page is useful to users quicker than it actually is.</p><p class="post__p">All we need to do is include a valid LCP element that contains something that counts as the FCP. While I would never recommend loading your main page content via client-side JavaScript in 2024 (serve static HTML from a CDN instead or build as much of the page as you can on a server), I would definitely not recommend this “hack” for a good user experience, regardless of what the Lighthouse performance score tells you. This approach also won’t earn you any favors with search engines indexing your site, as the robots are unable to discover the main content while it is absent from the DOM.</p><p class="post__p">I also tried this experiment with a variety of random images representing the LCP to make the page even less useful. But given that I used small file sizes — made smaller and converted into “next-gen” image formats using a third-party image API to help with page load speed — it seemed that Lighthouse interpreted the elements as “placeholder images” or images with “low entropy”. As a result, those images were disqualified as LCP elements, which is a good thing and makes the LCP slightly less hackable.</p><p class="post__p">View <a href="https://hacking-lighthouse.netlify.app/lcp/" target="_blank">the demo page</a> and use Chromium DevTools in an incognito window to see the results yourself.</p><img src="https://images.ctfassets.net/56dzm01z6lln/109EcEu7086FG2PSjbE8Xb/a7c326d1ce82ea79949d3ba5f971ae19/hack_lcp.png" alt="In-browser proof that the non-useful page scored 100 on Lighthouse performance" height="3078" width="5344" /><p class="post__p">This hack, however, probably won’t hold up in many other use cases. Discord, for example, uses the “splash screen” approach when you hard-refresh the app in the browser, and it receives a sad 29 performance score.</p><p class="post__p">Compared to my DOM-injected demo, the LCP element was calculated as some content behind the splash screen rather than elements contained within the splash screen content itself, given there were one or more large images in the focussed text channel I tested on. One could argue that Lighthouse scores are less important for apps that are behind authentication anyway: they don’t need to be indexed by search engines.</p><img src="https://images.ctfassets.net/56dzm01z6lln/OpjnJFkyspxypMOnYjrTL/2240c17986014bbe6dee7d283f737970/discord_perf.png" alt="Lighthouse screenshot of a score of 29 next to a blurred-out Discord server channel." height="3078" width="5344" /><p class="post__p">There are likely many other situations where apps serve user-generated content and you might be unable to control the LCP element entirely, particularly regarding images.</p><p class="post__p">For example, if you can control the sizes of all the images on your web pages, you might be able to take advantage of an interesting hack or “optimization” (in <b class="post__p--italic">very</b> large quotes) to arbitrarily game the system, as was the case of RentPath. In 2021, developers at RentPath managed to <a href="https://blog.rentpathcode.com/we-increased-our-lighthouse-score-by-17-points-by-making-our-images-larger-83f60b33a942" target="_blank">improve their Lighthouse performance score by 17 points</a> when <b class="post__p--italic">increasing</b> the size of image thumbnails on a web page. They convinced Lighthouse to calculate the LCP element as one of the larger thumbnails instead of a Google Map tile on the page, which takes considerably longer to load via JavaScript.</p><p class="post__p">The bottom line is that you can gain higher Lighthouse performance scores if you are aware of your LCP element and in control of it, whether that’s through a hack like RentPath’s or mine or a real-deal improvement. That being said, whilst I’ve described the splash screen approach as a hack in this post, that doesn’t mean this type of experience couldn’t offer a purposeful and joyful experience. Performance and user experience are about understanding what’s happening during page load, and it’s also about intent.</p><h2 class="post__h2">How to hack CLS scores</h2><p class="post__p"><b class="post__p--bold">TL;DR: Defer loading content that causes layout shifts until the Lighthouse test has</b> <b class="post__p--bold"><b class="post__p--italic">likely</b></b> <b class="post__p--bold">finished to make the test think it has enough data. CSS transforms do not negatively impact CLS, except if used in conjunction with new elements added to the DOM.</b></p><p class="post__p">CLS is measured on a decimal scale; a good score is less than 0.1, and a poor score is greater than 0.25. Lighthouse calculates CLS from the largest burst of unexpected layout shifts that occur during a user’s time on the page based on a combination of the viewport size and the movement of unstable elements in the viewport between two rendered frames. Smaller one-off instances of layout shift may be inconsequential, but a bunch of layout shifts happening one after the other will negatively impact your score.</p><p class="post__p">If you know your page contains annoying layout shifts on load, you can defer them until after the page load event has been completed, thus fooling Lighthouse into thinking there is no CLS. <a href="https://hacking-lighthouse.netlify.app/cls-bad/" target="_blank">This demo page I created</a>, for example, earns a CLS score of 0.143 even though JavaScript immediately starts adding new text elements to the page, shifting the original content up. By pausing the JavaScript that adds new nodes to the DOM by an arbitrary five seconds with a <code>setTimeout()</code>, Lighthouse doesn’t capture the CLS that takes place.</p><p class="post__p"><a href="https://hacking-lighthouse.netlify.app/cls-hacked/" target="_blank">This other demo page</a> earns a performance score of 100, even though it is arguably less useful and useable than the last page given that the added elements pop in <b class="post__p--italic">seemingly</b> at random without any user interaction.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7GFeW5uS1d8N22ufH3y4Jh/16f1a22bd8519ae4173544b76d0d3915/hack_cls.png" alt="Lighthouse performance score of 100 following the second test." height="2794" width="4336" /><p class="post__p">Whilst it is possible to defer layout shift events for a page load test, this hack definitely won’t work for field data and user experience over time (which is a more important focal point, as we discussed earlier). If we perform a “time span” test in Lighthouse on the page with deferred layout shifts, Lighthouse will correctly report a non-green CLS score of around 0.186.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2JLYcJOvxcwSgVIReCs8bR/649719838db1d527372a6e03c2b2b232/cls_timespan.png" alt="Screenshot of a timespan test performed on the same page with layout shifts." height="2794" width="4336" /><p class="post__p">If you do want to intentionally create a chaotic experience similar to the demo, you can use CSS animations and transforms to more purposefully pop the content into view on the page. In <a href="https://web.dev/articles/cls" target="_blank">Google’s guide to CLS</a>, they state that “content that moves gradually and naturally from one position to another can often help the user better understand what’s going on and guide them between state changes” — again, highlighting the importance of user experience in context.</p><p class="post__p">On <a href="https://hacking-lighthouse.netlify.app/cls-animated/" target="_blank">this next demo page</a>, I’m using CSS <code>transform</code> to <code>scale()</code> the text elements from <code>0</code> to <code>1</code> and move them around the page. The transforms fail to trigger CLS because the text nodes are already in the DOM when the page loads. That said, I did observe in my testing that if the text nodes are added to the DOM programmatically after the page loads via JavaScript and <b class="post__p--italic">then</b> animated, Lighthouse will indeed detect CLS and score things accordingly.</p><h2 class="post__h2">You can&#39;t hack a Speed Index score</h2><p class="post__p">The Speed Index score is based on the visual progress of the page as it loads. The quicker your content loads nearer the beginning of the page load timeline, the better.</p><p class="post__p">It is possible to do some hack to trick the Speed Index into thinking a page load timeline is <b class="post__p--italic">slower</b> than it is. Conversely, there’s no real way to “fake” loading content faster than it does. The only way to make your Speed Index score better is to optimize your web page for loading as much of the page as possible, as soon as possible. Whilst not entirely realistic in the web landscape of 2024 (mainly because it would put designers out of a job), you could go all-in to lower your Speed Index as much as possible by:</p><ul><li><p class="post__p">Delivering static HTML web pages only (no server-side rendering) straight from a CDN,</p></li><li><p class="post__p">Avoiding images on the page,</p></li><li><p class="post__p">Minimizing or eliminating CSS, and</p></li><li><p class="post__p">Preventing JavaScript or any external dependencies from loading.</p></li></ul><h2 class="post__h2">You also can&#39;t (really) hack a TBT score</h2><p class="post__p">TBT measures the total time after the FCP where the main thread was blocked by JavaScript tasks for long enough to prevent responses to user input. A good TBT score is anything lower than 200ms.</p><p class="post__p">JavaScript-heavy web applications (such as single-page applications) that perform complex state calculations and DOM manipulation on the client on page load (rather than on the server before sending rendered HTML) are prone to suffering poor TBT scores. In this case, you could probably hack your TBT score by deferring all JavaScript until after the Lighthouse test has finished. That said, you’d need to provide some kind of placeholder content or loading screen to satisfy the FCP and LCP and to inform users that something will happen <b class="post__p--italic">at some point</b>. Plus, you’d have to go to extra lengths to hack around the front-end framework you’re using. (You don’t want to load a placeholder page that, at some point in the page load timeline, loads a separate React app after an arbitrary amount of time!)</p><p class="post__p">What’s interesting is that while we’re still doing all sorts of fancy things with JavaScript in the client, advances in the modern web ecosystem are helping us all reduce the probability of a less-than-stellar TBT score. Many front-end frameworks, in partnership with modern hosting providers, are capable of rendering pages and processing complex logic on demand without any client-side JavaScript. While eliminating JavaScript on the client is not the goal, we certainly have a lot of options to use a lot <b class="post__p--italic">less</b> of it, thus minimizing the risk of doing too much computation on the main thread on page load.</p><h2 class="post__h2">Bottom line: Lighthouse is still just a rough guide</h2><p class="post__p">Google Lighthouse can’t detect everything that’s wrong with a particular website. Whilst Lighthouse performance scores prioritize page usability in terms of responding to user input, it still can’t detect every terrible usability or accessibility issue in 2024.</p><p class="post__p">In 2019, Manuel Matuzović <a href="https://www.matuzo.at/blog/building-the-most-inaccessible-site-possible-with-a-perfect-lighthouse-score/" target="_blank">published an experiment</a> where he intentionally created a terrible page that Lighthouse thought was pretty great. I hypothesized that five years later, Lighthouse might do better; but it doesn’t.</p><p class="post__p">On this final <a href="https://hacking-lighthouse.netlify.app/unusable/" target="_blank">demo page</a> I put together, input events are disabled by CSS and JavaScript, making the page technically unresponsive to user input. After five seconds, JavaScript flips a switch and allows you to click the button. The page still scores 100 for both performance <b class="post__p--italic">and</b> accessibility.</p><img src="https://images.ctfassets.net/56dzm01z6lln/DOKpjpAjTAYYZaQLKSesg/9fdc7212edba88145a4f6028c49c3172/unusable_page.png" alt="Lighthouse showing perfect performance and accessibility scores for a useless, inaccessible page." height="3078" width="5344" /><p class="post__p">You really can’t rely on Lighthouse as a substitute for usability testing and common sense.</p><h2 class="post__h2">Some more silly hacks</h2><p class="post__p">As with everything in life, there’s always a way to game the system. Here are some more tried and tested guaranteed hacks to make sure your Lighthouse performance score artificially knocks everyone else’s out of the park:</p><ul><li><p class="post__p">Only run Lighthouse tests using the fastest and highest-spec hardware.</p></li><li><p class="post__p">Make sure your internet connection is the fastest it can be; relocate if you need to.</p></li><li><p class="post__p">Never use field data, only lab data, collected using the aforementioned fastest and highest-spec hardware and super-speed internet connection.</p></li><li><p class="post__p">Rerun the tests in the lab using different conditions and all the special code hacks I described in this post until you get the result(s) you want to impress your friends, colleagues, and random people on the internet.</p></li></ul><p class="post__p"><b class="post__p--bold">Note</b>: <b class="post__p--italic">The best way to learn about web performance and how to optimize your websites is to do the complete opposite of everything we’ve covered in this article all of the time. And finally, to seriously level up your performance skills, </b><a href="https://sentry.io/for/performance/?utm_source=smashingmag&utm_medium=paid-community&utm_campaign=perf-fy25q2-evergreen&utm_content=blog-lighthouseblog-signup" target="_blank"><b class="post__p--italic">use an application monitoring tool like Sentry</b></a><b class="post__p--italic">. Think of Lighthouse as the canary and Sentry as the real-deal production-data-capturing, lean, mean, </b><a href="https://docs.sentry.io/product/performance/web-vitals/?utm_source=smashingmag&utm_medium=paid-community&utm_campaign=perf-fy25q2-evergreen&utm_content=blog-lighthouseblog-signup" target="_blank"><b class="post__p--italic">web vitals</b></a><b class="post__p--italic"> machine.</b></p><p class="post__p">And finally-finally, <a href="https://hacking-lighthouse.netlify.app/" target="_blank">here’s the link to the full demo site</a> for educational purposes.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>One does not simply delete cookies</title>
          <description>Sometimes, even the most "intuitive" framework APIs can create misunderstandings in the minds of seasoned web developers (ahem, me). </description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/cookies-not-deleted/</link>
          <guid>https://whitep4nth3r.com/blog/cookies-not-deleted/</guid>
          <pubDate>Sun, 09 Jun 2024 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Naming is hard. Modern developer tools often provide intuitive APIs acting as wrappers around web platform APIs to make our lives easier and developer experience smoother. But sometimes, these &quot;intuitive&quot; APIs can create misunderstandings in the minds of even the most seasoned web developers (ahem, me). </p><p class="post__p">I&#39;m currently building a project using <a href="https://astro.build/" target="_blank">Astro</a>, to which I&#39;ve added basic authentication via Twitch so users can <a href="https://p4nth3rworld.netlify.app" target="_blank">log in to view their inventory for my new stream game</a> by calling an API on the back end (my Twitch bot). I&#39;m using Astro in SSR mode, and authentication is provided by Auth.js via <a href="https://github.com/nowaythatworked/auth-astro" target="_blank">auth-astro</a>. When using Auth.js to authenticate, it saves three cookies in the browser to remember that you&#39;ve logged in to this website via Twitch.</p><p class="post__p">At the time of writing, auth-astro doesn&#39;t provide sign out functionality for Astro on the server. However, I needed some sign out functionality on the server to provide a better front end experience when the Twitch <code>accessToken</code> was identified as invalid by my back end. I created a logout route to &quot;delete&quot; the authentication cookies provided by Auth.js using Astro&#39;s <a href="https://docs.astro.build/en/reference/api-reference/#astrocookies" target="_blank">Astro.cookies API</a>.</p><p class="post__p">This code worked great in development, but it <b class="post__p--italic">didn&#39;t work in production</b>. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="TsgPAQUiMX"
      aria-describedby="TsgPAQUiMX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="TsgPAQUiMX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="TsgPAQUiMX" itemprop="text" content="%2F%2F%20src%2Fpages%2Flogout.astro%0A%0A%2F%2F%20DEV%0AAstro.cookies.delete(%22authjs.csrf-token%22)%3B%0AAstro.cookies.delete(%22authjs.callback-url%22)%3B%0AAstro.cookies.delete(%22authjs.session-token%22)%3B%0A%0A%2F%2F%20PROD%0AAstro.cookies.delete(%22__Host-authjs.csrf-token%22)%3B%0AAstro.cookies.delete(%22__Secure-authjs.callback-url%22)%3B%0AAstro.cookies.delete(%22__Secure-authjs.session-token%22)%3B%0A%0Areturn%20Astro.redirect(%22%2F%22)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// src/pages/logout.astro</span><br><br><span class="token comment">// DEV</span><br>Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"authjs.csrf-token"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"authjs.callback-url"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"authjs.session-token"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token comment">// PROD</span><br>Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"__Host-authjs.csrf-token"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"__Secure-authjs.callback-url"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"__Secure-authjs.session-token"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">return</span> Astro<span class="token punctuation">.</span><span class="token function">redirect</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">The cookie-savvy among you can probably already tell what I was doing wrong. But after deploying my project to multiple hosting platforms to check it wasn&#39;t a platform-specific problem with Astro, I learned a few things about how cookies are marked as &quot;deleted&quot;, and how servers send instructions to browsers to create and modify cookies.</p><h2 class="post__h2">Cookies are not deleted: they&#39;re modified</h2><p class="post__p">As a front end developer, I am accustomed to managing cookies via client-side JavaScript using the <a href="https://www.w3schools.com/js/js_cookies.asp" target="_blank">document.cookie</a> API. There is no way to &quot;delete&quot; a cookie using client-side JavaScript, you modify it: &quot;deleting&quot; a cookie is a misnomer. Whilst the word &quot;delete&quot; feels intuitive in the <code>Astro.cookies</code> API, it hides the fact that to &quot;delete&quot; a cookie, you need to <b class="post__p--italic">invalidate</b> it by setting an expiry date in the past. </p><p class="post__p">Additionally, you&#39;re not technically modifying a browser cookie directly via server-side code. So what&#39;s actually happening?</p><h2 class="post__h2">Cookies are modified via HTTP response headers</h2><p class="post__p">After receiving an HTTP request, a server can send cookie modification requests to a browser via one or more <code>Set-Cookie</code> HTTP response headers. For example, when you call <code>Astro.cookies.delete(&quot;my_cookie&quot;)</code>, you&#39;ll see the following response header in the browser network tab.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="IPJUrddxXt"
      aria-describedby="IPJUrddxXt">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="IPJUrddxXt">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="csv">
      <meta data-code-id="IPJUrddxXt" itemprop="text" content="Set-Cookie%3A%20my_cookie%3Ddeleted%3B%20Expires%3DThu%2C%2001%20Jan%201970%2000%3A00%3A00%20GMT">
      <pre class="language-csv"><code class="language-csv"><span class="token value">Set-Cookie: my_cookie=deleted; Expires=Thu</span><span class="token punctuation">,</span><span class="token value"> 01 Jan 1970 00:00:00 GMT</span></code></pre>
    </div>
  </div>

  <p class="post__p">This instructs the browser to store the <code>my_cookie</code> value as <code>deleted</code>, and sets the expiration date to a date in the past: 0 in <a href="https://en.wikipedia.org/wiki/Unix_time" target="_blank">Unix time</a>. It&#39;s worth bearing in mind that expiring a cookie doesn&#39;t necessarily remove it from the browser storage in all browsers, but browsers will not send expired cookies to the server in subsequent HTTP requests. (Expired cookies also won&#39;t show up in the Application tab in browser dev tools.)</p><h2 class="post__h2">Why weren&#39;t my cookies being removed in production?</h2><p class="post__p">In the code example above, notice that the cookie names for development and production are different. The production cookies are prefixed by <code>__Secure- </code>and <code>__Host-</code>. When inspecting the HTTP headers sent on the logout page in production, I noticed this warning.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4ZFR3lka08TQuZ1tYJxU5l/36860af86f173923742d7dae98b8b504/cookie_warning.png" alt="This attempt to set a cookie via a Set-Cookie header was blocked because it used the "_Secure-" or "
_Host-" prefix in its name and broke the additional rules applied to cookies with these prefixes as defined in https://tools.ietf.org/html/draft-west-cookie-prefixes-05." height="225" width="493" /><p class="post__p"><b class="post__p--bold"><b class="post__p--italic">This attempt to set a cookie via a Set-Cookie header was blocked because it used the &quot;_Secure-&quot; or &quot;_Host-&quot; prefix in its name and broke the additional rules applied to cookies with these prefixes as defined in </b></b><a href="https://tools.ietf.org/html/draft-west-cookie-prefixes-05" target="_blank"><b class="post__p--bold"><b class="post__p--italic">https://tools.ietf.org/html/draft-west-cookie-prefixes-05</b></b></a><b class="post__p--bold"><b class="post__p--italic">.</b></b></p><p class="post__p">If you click the link to this document, you&#39;ll see that it&#39;s a draft which expired in 2016. For this reason I didn&#39;t feel inclined to read it. Why was my browser showing me this in 2024? Regardless, I did a search on the page for <code>__Secure-</code> and found <a href="https://datatracker.ietf.org/doc/html/draft-west-cookie-prefixes-05#section-3" target="_blank">this section on cookie prefixes</a>. </p><p class="post__p">The short version of this story is that the browser was <b class="post__p--italic">rejecting</b> my <code>Set-Cookie</code> HTTP response headers that were requesting to modify cookies. This was because the <b class="post__p--bold">cookie options</b> were not specified correctly: they didn&#39;t match the cookie options that were specified on the existing cookies. By checking the cookie options in the Application tab in browser dev tools, and updating the code accordingly, I was successfully able to <b class="post__p--italic">modify the expiration date</b> of the specified cookies, and effectively log the user out of my application.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BFbuliwgib"
      aria-describedby="BFbuliwgib">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BFbuliwgib">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="BFbuliwgib" itemprop="text" content="Astro.cookies.delete(%22__Host-authjs.csrf-token%22%2C%20%7BhttpOnly%3A%20true%2C%20secure%3A%20true%2C%20path%3A%20%22%2F%22%7D)%3B%0AAstro.cookies.delete(%22__Secure-authjs.callback-url%22%2C%20%7BhttpOnly%3A%20true%2C%20secure%3A%20true%7D)%3B%0AAstro.cookies.delete(%22__Secure-authjs.session-token%22%2C%20%7BhttpOnly%3A%20true%2C%20secure%3A%20true%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript">Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"__Host-authjs.csrf-token"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token literal-property property">httpOnly</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token literal-property property">secure</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">"/"</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"__Secure-authjs.callback-url"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token literal-property property">httpOnly</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token literal-property property">secure</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>Astro<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">delete</span><span class="token punctuation">(</span><span class="token string">"__Secure-authjs.session-token"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token literal-property property">httpOnly</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token literal-property property">secure</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">What do the options mean?</h3><p class="post__p"><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#httponly" target="_blank"><code>httpOnly: true</code></a> prevents client-side JavaScript from accessing this cookie, for example via <code>document.cookie</code> to mitigate attacks against cross-site scripting.</p><p class="post__p"><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#secure" target="_blank"><code>secure: true</code></a><code> i</code>ndicates that the cookie is sent to the server only when a request is made with <code>https:</code>  (except on localhost), making it more resistant to man-in-the-middle attacks. </p><p class="post__p"><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value" target="_blank"><code>path: &quot;/&quot;</code></a><code> </code>indicates the path that <b class="post__p--italic">must</b> exist in the requested URL for the browser to send the <code>Set-Cookie</code> header. This was required given I was redirecting to &quot;/&quot; straight after setting the cookies.</p><p class="post__p">The reason that the initial code successfully modified the cookies in development was because the cookie names were not prefixed with <code>__Host-</code> or <code>__Secure-</code>, and because I was running the site on <code>localhost</code>. </p><h2 class="post__h2">Naming is hard</h2><p class="post__p">I&#39;m a big fan of naming things after their specific function. Should the <code>Astro.cookies</code> API rename <code>delete</code> to <code>modify</code> in order to be more explicit? Probably not. In any case, I decided to <a href="https://github.com/withastro/docs/pull/8476" target="_blank">open a PR to the Astro docs</a> to clarify what was actually happening.</p><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>5 easy tips to improve your personal website performance</title>
          <description>TL;DR: Add less.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/5-easy-tips-to-improve-your-personal-website-performance/</link>
          <guid>https://blog.sentry.io/5-easy-tips-to-improve-your-personal-website-performance/</guid>
          <pubDate>Wed, 15 May 2024 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you’re a developer, you need a personal website. While billionaire-owned, algorithm-based social media platforms arbitrarily decide what people should and should not see on their timelines, there’s no better time for you to carve out your own cozy corner on the web and <b class="post__p--italic">own your</b> content.</p><h2 class="post__h2">Why you need a personal website</h2><p class="post__p">Your personal website is your personal showcase: a one-stop-shop to show off your work, skills, personality, and ✨aesthetic✨. You could use your personal website as a place to collect what you’ve learned and what you’ve built over the years, whether that’s through traditional blogging or creating interactive demo pages. Or you could use it to build community through shared interests, to land that exciting new job, as a place to improve your refactoring skills, as an archive for your weekly newsletter, to test-drive Test Driven Development, or to build a proof-of-concept of something that really excites you.</p><p class="post__p">Your personal website is also a great place to use as a playground for new and experimental features on the web, to try out new UI component frameworks or CSS methodologies, or to stretch the boundaries of what’s possible in a browser.</p><h2 class="post__h2">Personal websites are low-risk, high reward</h2><p class="post__p">Think of your personal website as a super low-risk test environment where you can build anything you ever dreamed of without having to worry about deadlines, stakeholder management, or client feedback.</p><p class="post__p">And because a personal website is usually low-risk (i.e. there’s no P0 panic if you find a bug or it goes down) it’s also a great tool to help you learn about web performance. Through using a variety of tools such as Google Lighthouse, built-in browser dev tools, or an <a href="https://sentry.io/for/performance/" target="_blank">Application Performance Monitoring</a> tool like Sentry, you can use your personal website to identify common performance issues and experiment with different fixes to see what gets the best results. What’s more, you can transfer all that learning to the things you build in your day job. And if you are using your personal website as an online resume, you’ll make a better first impression if it loads quickly and feels fast to use: win-win. (As a hiring manager in the past, I definitely inspected the DOM and the network tab of candidate websites.)</p><p class="post__p">If you don’t yet have a personal website, go make one! It could be as simple as a single page of HTML deployed to a CDN. And then come back to check out these five easy tips for speeding up your personal website performance that you can apply to <b class="post__p--italic">any</b> website, project, or product.</p><h2 class="post__h2">1. Serve static HTML as much as possible</h2><p class="post__p">It’s too easy to render pages on the server in 2024. Most modern JavaScript-based frameworks come with pre-built adaptors to be able to deploy and server-side-render (SSR) your websites on demand using your preferred hosting platform. We also have edge-rendering and edge functions that allow us to intercept HTTP requests <b class="post__p--italic">at the edge</b> (the closest server location to the request) to add even more dynamic content to our pages based on geolocation, cookies, and more. But all of this comes at a cost — most notably to performance and your Time to First Byte (TTFB): the time it takes for the browser to receive the first byte of content from the server. (And let’s not forget the ongoing, and sometimes unexpected, monetary costs of running SSR.)</p><h3 class="post__h3">A brief history of the web</h3><p class="post__p">The web moves in cycles. In the 1990s the web was mainly static, delivering plain HTML documents that were stored on servers somewhere in the world; this was the Read Things era. As the web became a place where you could now <b class="post__p--italic">do things</b> (such as sign up and log in and CRUD), SSR — building pages on the server before sending them back to the browser — was necessary. In the early 2010s when JavaScript was more widely adopted, client-side-rendering (CSR) — sending a blank or skeleton HTML document and then building the page content in the browser — became the norm. The issue was that people still needed to <b class="post__p--italic">read things</b> on the internet as well as <b class="post__p--italic">do things</b>. And with the rise in SSR and CSR, <b class="post__p--italic">reading things</b> started to feel slow. In the mid-2010s, the modern web offered up a modern alternative: static site generators.</p><h3 class="post__h3">Static site generators make fast sites</h3><p class="post__p">Static site generators (or early “front end frameworks”) provided developers with a way to build a folder full of HTML files, each populated with the required page data at the time of build. Developers could now create tens of thousands of static HTML pages, stick them on a Content Delivery Network (CDN), and not have to worry about maintaining servers, scaling for traffic, or security policies. This was the beginning of Jamstack.</p><p class="post__p">Fast-forward ten years, and “Jamstack” may be considered dead as a concept, but <a href="https://thefutureofjamstack.org/" target="_blank">the principles of serving cached static content from a CDN for secure and performant websites still hold true</a>. Serving static HTML files is much more performant than generating web pages on demand on a server for every request. This is because the server doesn’t need to do anything apart from retrieve your HTML document from the cache and send it back. No business logic, no database queries, no delays. Of course, there are times when you may need some level of dynamic page content. But on a personal website, you probably don’t need SSR.</p><h3 class="post__h3">You probably don’t need SSR</h3><p class="post__p">What has been interesting to observe is how the rise in popularity of “full stack front end frameworks” such as Next.js has started to promote using SSR methods and even edge-rendering over serving static content. What I also observed in the mid 2010s is that Next.js was originally marketed as <b class="post__p--italic">yet another static site generator</b>, encouraging developers to use dynamic page generation sparingly. But now, many blog or personal website templates built on frameworks like Next.js often come with SSR as the default page behavior. But content-based websites — which most personal websites are — don’t need SSR. Personal websites usually involve <b class="post__p--italic">reading things</b>, not <b class="post__p--italic">doing things</b>.</p><p class="post__p">If you’re currently using SSR on your personal website, ask yourself if you really need to. By pre-building and serving pages statically I guarantee you’ll see performance gains. Take for example a case study on my personal website which should convince you. Over the years I experimented so hard with Edge Functions to add some dynamic elements to my static personal website that I pushed my p75 TTFB up to almost 4 seconds. I was devastated to learn this. <a href="https://blog.sentry.io/how-i-fixed-my-brutal-ttfb/" target="_blank">Read how I improved the TTFB by 80%</a> and if you want a spoiler: I just built the pages statically again.</p><h2 class="post__h2">2. Optimize images</h2><p class="post__p">Even if it’s just a headshot somewhere on your “about” page or a meme on your homepage, you’re probably using images on your personal website. Images are everywhere on the web. And they need to be optimized. But what do we mean by optimizing images?</p><h3 class="post__h3">Serve images in next-gen formats</h3><p class="post__p">If you’ve analyzed your personal website performance using Google Lighthouse, you might have been given the advice to “serve images in next-gen formats”. Next-gen (next generation) image formats are file types that use modern image compression algorithms, resulting in smaller image file sizes. This, in turn, results in better web page performance given the web browser has fewer bytes to download per image. Instead of serving <code>PNG</code> or <code>JPEG</code> image file formats, it is recommended to convert those to <code>webp</code> or <code>avif</code> file formats. Additionally, if you want to serve an animated <code>gif</code>, convert it to a <code>webp</code> file, which will preserve the animation while reducing the file size by almost 90%. <a href="https://x.com/whitep4nth3r/status/1460244790059188226" target="_blank">Here’s a throwback to when I discovered this</a> in 2021.</p><img src="https://images.ctfassets.net/56dzm01z6lln/40PgfHmczeYsCzcrzVjwiw/a8b80fff880a7f77833b3a2cdb31441a/gif_vs_webp-ezgif.com-optimize.gif" alt="WebP 167kb, still animated, original GIF is 1.2mb." height="338" width="600" /><h3 class="post__h3">How to convert your images to next-gen formats</h3><p class="post__p">Depending on your preferences and how many images you need to optimize, you can use offline tools, build-time tools, third-party services, and image manipulation APIs to convert your images to modern formats on your personal website. Also, bear in mind that not all browsers support <code>avif</code> and <code>webp</code> image formats just yet, so it’s important to use the HTML <code>&lt;picture&gt;</code> element to allow the browser to choose the most appropriate image file format to download and display.</p><p class="post__p">Lazar guides you through all the <a href="https://blog.sentry.io/low-effort-image-optimization-tips/" target="_blank">low-effort image optimization tips</a> you need to make sure your personal website stays in good shape, with some bonus advice on how you can monitor your website performance using <a href="https://sentry.io/welcome/" target="_blank">Sentry</a> in terms of image resources.</p><h2 class="post__h2">3. Use system fonts</h2><p class="post__p">It’s too easy to use Google fonts. Choose a font and slap a link to the font file in the head of your pages. Done. The hardest part is deciding which fonts to use. And I get it. Fancy fonts look great. But do you really need custom fonts? Like really, <b class="post__p--italic">really</b>? You could get a huge performance boost by using system fonts. No downloading, no layout shifts, no flashes of unstyled content.</p><p class="post__p">If you <b class="post__p--bold">do</b> want to use non-system fonts, it’s recommended to serve font files from the same server as your website, rather than ask the browser to fetch them from a third-party service at runtime such as Google. While you can download font files from the Google Fonts website and serve them yourself instead of linking to them, you’ll still have to convert the provided <code>ttf</code> font files to file formats required for the web (<code>woff</code> and <code>woff2</code>). In the past, I’ve always used <a href="https://www.fontsquirrel.com/tools/webfont-generator" target="_blank">Font Squirrel Webfont Generator</a> to do this, but in 2024 this feels hacky. In my experience, Font Squirrel tends to do weird things with <a href="https://fonts.google.com/knowledge/introducing_type/introducing_variable_fonts" target="_blank">variable font files</a> (which you should also use to improve your website performance).</p><p class="post__p">I’m not sure why we haven’t solved the font file format problem yet. And because the best network request is the one that’s never made, why not scrap the custom fonts altogether and just use system fonts? You may fret that your website might look different on MacOS, Linux, and Windows, but maybe that’s the fun of it. And system fonts are no longer limited to just Times New Roman and Arial. <a href="https://modernfontstacks.com/" target="_blank">Modern Font Stacks</a> shows us just how good system fonts can look in context.</p><p class="post__p">Lazar goes deeper into the web font conversation in <a href="https://blog.sentry.io/web-fonts-and-the-dreaded-cumulative-layout-shift/" target="_blank">Web Fonts and the Dreaded Cumulative Layout Shift</a>.</p><h2 class="post__h2">4. Remove render-blocking resources</h2><p class="post__p">In 2022, I refactored my personal website from Next.js to Eleventy. This decision was centered around being able to know exactly what files were being delivered to the browser so that I could keep tabs on performance. With Next.js I noticed my website was making over 30 requests for separate chunked JavaScript files for a static page with no interactivity. It didn’t make sense. That same page created with Eleventy served no JavaScript files, which is correct — because the page didn’t need any JavaScript. All this is to say that as a responsible developer, you should be aware of what you’re sending down the wire to your users — especially if it ends up being a render-blocking resource. <a href="https://youtube.com/shorts/CXDUYx4HGlQ?feature=share" target="_blank">I made a YouTube short about this</a> in 2022 to show the difference in what the two frameworks delivered to the browser.</p><p class="post__p">A render-blocking resource is as it’s described: a downloadable resource that blocks the render (or “first paint”) of a web page, delaying the time that a user sees something in the browser. The more render-blocking resources that stack up on top of each other, the longer your page takes to load, and the worse your First Contentful Paint (FCP) web vital score will be. Google stipulates that to provide a good user experience, web pages must have an FCP of 1.8 seconds or less.</p><p class="post__p">There are render-blocking resources we can’t escape, such as CSS (both inline CSS and CSS contained in stylesheets), but your website may have fallen victim to other render-blocking resources loaded via <code>&lt;script&gt;</code> tags in the <code>&lt;head&gt;</code> of your website pages. Take for example when the <a href="https://blog.sentry.io/fix-your-actual-slow-loading-assets-with-resource-monitoring/" target="_blank">SyntaxFM team at Sentry discovered a 4kB render-blocking script that added around 500ms to each page load on average</a>.</p><p class="post__p">It may be that some of your dependencies need to be render-blocking, and that’s fine. But by considering the impact of render-blocking resources and being mindful of how and when you load JavaScript every step of the way, you’ll most likely be able to keep your <a href="https://docs.sentry.io/product/performance/web-vitals/" target="_blank">Core Web Vitals</a> scores looking good. <a href="https://developer.chrome.com/docs/lighthouse/performance/render-blocking-resources" target="_blank">Google offers more advice in this article on how to eliminate render-blocking resources</a>.</p><h2 class="post__h2">5. Use less JavaScript</h2><p class="post__p">I am once again going to reference this evergreen piece of advice from Cassidy Williams in <a href="https://css-tricks.com/add-less/" target="_blank">an article on CSS Tricks</a>: “Your websites start fast until you add too much to make them slow.” When reaching to build that next feature on your personal website, ask yourself, can you do it <b class="post__p--italic">without JavaScript</b>?</p><p class="post__p">Perhaps you want to build a contact form. You don’t need JavaScript for a form. How about using <a href="https://developer.mozilla.org/en-US/docs/Learn/Forms/Basic_native_form_controls" target="_blank">native HTML form functionality</a> which includes HTML5 field validation? Why not go wild and just include a <code>mailto</code> link?</p><p class="post__p">You probably don’t need JavaScript to achieve many modern UI animations. CSS is super-powerful these days, and I’m always looking for opportunities to use some of the newer features, such as <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type" target="_blank">scroll-snap-type</a>.</p><p class="post__p">You can even <b class="post__p--italic">almost</b> create a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Masonry_layout" target="_blank">CSS-only masonry layout</a>. The technology is still experimental, and in Safari Technical Preview, but let’s hope it’s coming soon.</p><p class="post__p">And if you’re thinking about rendering some UI components using JavaScript on the client — can you build those components using static HTML, instead?</p><p class="post__p">Challenge yourself to <b class="post__p--italic">add less</b>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to prevent Prettier putting a full stop on a new line after a link</title>
          <description>Ugh, what a minefield.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/prevent-prettier-putting-a-full-stop-on-a-new-line/</link>
          <guid>https://whitep4nth3r.com/blog/prevent-prettier-putting-a-full-stop-on-a-new-line/</guid>
          <pubDate>Sun, 12 May 2024 23:00:00 GMT</pubDate>
          <category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Do you use <a href="https://prettier.io/" target="_blank">Prettier</a>? Have your configuration settings caused weird HTML rendering issues by adding extra whitespace where you didn&#39;t want it? Perhaps after an anchor link at the end of a paragraph? Me, too. Here&#39;s what&#39;s happening and how you <b class="post__p--italic">might</b> be able to fix it.</p><h2 class="post__h2">The HTML whitespace problem</h2><p class="post__p">Formatting code using Prettier is often a frustrating battle. Standard configuration options that make sense for the majority of code formatting could cause unexpected problems in the rendering of whitespace in HTML. </p><p class="post__p">Say I want to render a link followed by a full stop at the end of a paragraph, like this:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="IPOdECJkiU"
      aria-describedby="IPOdECJkiU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="IPOdECJkiU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="IPOdECJkiU" itemprop="text" content="%3Cp%3Ep4nth3rworld%20is%20a%20multiplayer%20text-based%20game%20powered%20by%20a%20live%20coding%20stream%20at%20%3Ca%20href%3D%22https%3A%2F%2Ftwitch.tv%2Fwhitep4nth3r%2Fchat%22%3Etwitch.tv%2Fwhitep4nth3r%3C%2Fa%3E.%3C%2Fp%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>p4nth3rworld is a multiplayer text-based game powered by a live coding stream at <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://twitch.tv/whitep4nth3r/chat<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>twitch.tv/whitep4nth3r<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span>.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">A certain Prettier setting might auto-format the code to look like this. You&#39;ll notice that the full stop has been placed on a new line after the closing anchor tag.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="gfFGftIlrT"
      aria-describedby="gfFGftIlrT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="gfFGftIlrT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="gfFGftIlrT" itemprop="text" content="%3Cp%3E%0A%20%20p4nth3rworld%20is%20a%20multiplayer%20text-based%20game%20powered%20by%20a%20live%20coding%20stream%20at%20%0A%20%20%3Ca%20href%3D%22https%3A%2F%2Ftwitch.tv%2Fwhitep4nth3r%2Fchat%22%3E%0A%20%20%20%20twitch.tv%2Fwhitep4nth3r%0A%20%20%3C%2Fa%3E%0A%20%20.%0A%3C%2Fp%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><br>  p4nth3rworld is a multiplayer text-based game powered by a live coding stream at <br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://twitch.tv/whitep4nth3r/chat<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    twitch.tv/whitep4nth3r<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><br>  .<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">This renders undesirable whitespace in the HTML in the browser, like this:</p><img src="https://images.ctfassets.net/56dzm01z6lln/21vF1V3fBDSgMZvfXVJiPL/38533f31fca59139da16f980688d9cf7/weird_whitespace.png" alt="A render of the previously described HTML code, showing a space added at the end of the anchor link text, creating a gap between the end of the link and the full stop." height="1456" width="2114" /><p class="post__p">This happens because whitespace is treated literally in inline HTML elements: <code>1&lt;b&gt; 2 &lt;/b&gt;3</code> will output a different result to <code>1&lt;b&gt;2&lt;/b&gt;3</code>. Given anchor tags are <code>display: inline</code> by default, any whitespace in and around these elements will <b class="post__p--bold">not</b> be ignored and will be rendered literally.</p><p class="post__p">To fix this, you might need to configure Prettier&#39;s <a href="https://prettier.io/docs/en/options.html#html-whitespace-sensitivity" target="_blank">HTML Whitespace Sensitivity</a> option — and you might need to change some other options, too.</p><h2 class="post__h2">Configure Prettier HTML Whitespace Sensitivity</h2><p class="post__p">Prettier allows you to specify the global whitespace sensitivity for HTML using the following options:</p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header">htmlWhitespaceSensitivity setting</th><th class="post__table__header">Description</th></tr><tr class="post__table__row"><td class="post__table__cell">&quot;css&quot;</td><td class="post__table__cell">Respect the default value of CSS display property.</td></tr><tr class="post__table__row"><td class="post__table__cell">&quot;strict&quot;</td><td class="post__table__cell">Whitespace (or the lack of it) around all tags is considered <b class="post__p--bold">significant</b>.</td></tr><tr class="post__table__row"><td class="post__table__cell">&quot;ignore&quot;</td><td class="post__table__cell">Whitespace (or the lack of it) around all tags is considered <b class="post__p--bold">insignificant</b>.</td></tr></tbody>
      </table></div><p class="post__p">The default option for Prettier&#39;s whitespace sensitivity setting is <code>&quot;css&quot;</code>, which <b class="post__p--bold">should</b> mean that full stops after a closing anchor tag don&#39;t wrap onto a new line and cause whitespace issues (given <code>&lt;a&gt;</code> elements are inline elements). However, this wasn&#39;t the case with my code. It <b class="post__p--bold">seemed</b> like I needed to use the <code>strict</code> option, which formatted the code like this.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="SGMBIbjYAo"
      aria-describedby="SGMBIbjYAo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="SGMBIbjYAo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="SGMBIbjYAo" itemprop="text" content="%3Cp%3E%0A%20%20p4nth3rworld%20is%20a%20multiplayer%20text-based%20game%20powered%20by%20a%20live%20coding%20stream%20at%0A%3Ca%20href%3D%22https%3A%2F%2Ftwitch.tv%2Fwhitep4nth3r%2Fchat%22%0A%20%20%20%3Etwitch.tv%2Fwhitep4nth3r%3C%2Fa%0A%20%20%3E.%0A%3C%2Fp%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span><br>  p4nth3rworld is a multiplayer text-based game powered by a live coding stream at<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://twitch.tv/whitep4nth3r/chat<span class="token punctuation">"</span></span><br>   <span class="token punctuation">></span></span>twitch.tv/whitep4nth3r<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><br>  <span class="token punctuation">></span></span>.<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Now, despite the whitespace problem being solved in the resulting HTML in the browser, this presentation still wasn&#39;t ideal for me given the the closing angled brackets were put onto a new line. This was the case <b class="post__p--italic">even though I specified that brackets should stay on the same line </b>using the <a href="https://prettier.io/docs/en/options.html#bracket-line" target="_blank">Prettier Bracket Line configuration</a><b class="post__p--italic">.</b> As it turns out, the length of my paragraph and anchor link created a bit of an edge case, and I needed to increase the <code>printWidth</code> setting.</p><h2 class="post__h2">Configure Prettier Print Width</h2><p class="post__p"><a href="https://prettier.io/docs/en/options.html#print-width" target="_blank">Prettier Print Width</a> specifies the line length at which your IDE will wrap text. The Prettier default is 80 characters. By arbitrarily changing <code>prettier.printWidth</code> to <b class="post__p--bold">90</b> in my configuration settings, the HTML was more readable in combination with the whitespace sensitivity setting.</p><p class="post__p">It&#39;s worth mentioning at this point that by increasing the <code>printWidth</code>, the default <code>htmlWhitespaceSensitivity: &quot;css&quot;</code> produced the desired formatting results. I decided to leave the whitespace setting as <code>strict</code> to remind me of this pain in the future.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1lRcdx3mTFUX6hScaP674z/17f4e7aee74091b6fbd75559a10cad52/wrapped_text.png" alt="Side by side view of the new prettier settings and the correctly formatted HTML in VS Code." height="2794" width="4336" /><p class="post__p">Here are the two settings I&#39;m currently using:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="INXrMSRiSM"
      aria-describedby="INXrMSRiSM">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="INXrMSRiSM">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="INXrMSRiSM" itemprop="text" content="%7B%0A%20%20%2F*%20in%20pretter%20config%20file%20*%2F%0A%20%20htmlWhitespaceSensitivity%3A%20%22strict%22%2C%0A%20%20printWidth%3A%2090%0A%0A%20%20%2F*%20in%20VS%20Code%20settings%20*%2F%0A%20%20%22prettier.htmlWhitespaceSensitivity%22%3A%20%22strict%22%2C%0A%20%20%22prettier.printWidth%22%3A%2090%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token punctuation">{</span><br>  <span class="token comment">/* in pretter config file */</span><br>  <span class="token literal-property property">htmlWhitespaceSensitivity</span><span class="token operator">:</span> <span class="token string">"strict"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">printWidth</span><span class="token operator">:</span> <span class="token number">90</span><br><br>  <span class="token comment">/* in VS Code settings */</span><br>  <span class="token string-property property">"prettier.htmlWhitespaceSensitivity"</span><span class="token operator">:</span> <span class="token string">"strict"</span><span class="token punctuation">,</span><br>  <span class="token string-property property">"prettier.printWidth"</span><span class="token operator">:</span> <span class="token number">90</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Ugh, what a minefield</h2><p class="post__p">Should I have needed to change the <code>printWidth</code> in order to respect the <code>bracketSameLine</code> setting for this particular block of code? Probably not. Alas, Prettier states that it&#39;s an <b class="post__p--italic">opinionated</b> code formatting tool. Make of that what you will.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Why don’t we talk about minifying CSS anymore?</title>
          <description>Remember Grunt files? </description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/why-dont-we-talk-about-minifying-css/</link>
          <guid>https://blog.sentry.io/why-dont-we-talk-about-minifying-css/</guid>
          <pubDate>Wed, 24 Apr 2024 04:00:00 GMT</pubDate>
          <category>CSS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Minifying your CSS helps improve your website performance. <b class="post__p--bold">But as developers, we don’t really talk about minifying CSS anymore. </b><b class="post__p--bold"><b class="post__p--italic">Why</b></b><b class="post__p--bold">?</b></p><p class="post__p">The TL;DR is that the delivery and optimization of CSS have both been improved with modern tech stacks, making it practically a non-issue. The efficient and performant delivery of CSS is largely solved by HTTP/2 and modern compression algorithms, whilst modern front end frameworks take care of the boring optimization jobs such as code-splitting and minification. Gone are the days when setting up a new front end project involved hours of frustrating manual configuration of Grunt files or Gulp files to compile, minify and post-process CSS files.</p><h2 class="post__h2">Why you should minify your CSS files</h2><p class="post__p"><b class="post__p--bold">Minification in CSS is the process of removing all unnecessary whitespace so that files are smaller in size, but can still be understood by the browser.</b> In short, minifying your CSS helps to optimize your First Contentful Paint (FCP).</p><p class="post__p">FCP is a Core Web Vital that measures the perceived load speed of a web page, and it marks the first point during page load where a user can see <b class="post__p--italic">anything</b> on the screen. A good FCP score is measured as 1.8 seconds or less and reassures the user that <b class="post__p--italic">something is loading</b> so that they don’t bounce away. To ensure a good FCP score, your stylesheets or inline CSS should be <b class="post__p--italic">as small as possible</b> (i.e. minified and compressed) so that they take less time to download and can be processed faster, allowing the browser to paint something on the page sooner. All of the above also contributes to optimizing the <a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path" target="_blank">critical render path</a> (the steps the browser goes through to convert the HTML, CSS, and JavaScript into pixels on the screen). This is especially important for users who have slower internet speeds.</p><p class="post__p">Additionally, CSS is a <a href="https://www.notion.so/https-blog-sentry-io-sentrys-open-source-values-94795d61d0dd4695bb8fcbe5ff8dfdc7?pvs=21" target="_blank">render blocking resource,</a> meaning the browser won’t show any content until it has downloaded the CSS and constructed the CSSOM (<a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Object_Model" target="_blank">CSS Object Model</a>) in conjunction with the DOM (<a href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model" target="_blank">Document Object Model</a>). This makes sense; most modern web pages won’t function correctly without CSS. Plus, if CSS is loaded and applied <b class="post__p--italic">after</b> HTML renders on the page, users will experience a Flash of Unstyled Content (FOUC) before the CSS rules are applied. Styling and arranging content after it has loaded will also likely negatively impact your CLS score (we’ve already got enough CLS to worry about with <a href="https://blog.sentry.io/web-fonts-and-the-dreaded-cumulative-layout-shift/" target="_blank">fonts</a>, <a href="https://blog.sentry.io/from-lcp-to-cls-improve-your-core-web-vitals-with-image-loading-best/" target="_blank">images</a> and <a href="https://blog.sentry.io/your-background-images-might-be-causing-cls/" target="_blank">other unexpected issues</a>!). CSS delivered in stylesheets and as inline CSS in the <code>&lt;head&gt;</code> tag of your pages are <b class="post__p--bold">both</b> render blocking.</p><h2 class="post__h2">CSS minification tools</h2><p class="post__p">There are a number of tools available to minify CSS in development or as part of your build pipeline including, but not limited to, <a href="https://cssnano.github.io/cssnano/" target="_blank">cssnano</a> and <a href="https://vitejs.dev/config/build-options#build-cssminify" target="_blank">Vite</a>. If you’re using <a href="https://www.npmjs.com/package/sass" target="_blank">sass</a>, it’s as straightforward as adding <code>--style=compressed</code> to your <code>sass</code> command. The results you achieve may vary depending on the complexity of your CSS, but you may see numbers similar to this (processed with sass):</p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header">Raw (kB)</th><th class="post__table__header">Minified (kB)</th><th class="post__table__header">Saving (kB)</th></tr><tr class="post__table__row"><td class="post__table__cell">78</td><td class="post__table__cell">65</td><td class="post__table__cell">13</td></tr></tbody>
      </table></div><p class="post__p">What’s interesting, though, is that these numbers pertain to files store on disk. Files delivered from a server will actually be much smaller. And this is where compression comes in.</p><h2 class="post__h2">Modern HTTP compression algorithms</h2><p class="post__p">HTTP compression is used by web servers and clients (browsers) to improve the transfer speed of resources, and it also helps to save on bandwidth costs and overheads. There are a number of compression algorithms available, but not all are supported by modern browsers.</p><p class="post__p">Servers may support multiple compression types, and the most common compression types currently used on the web are <a href="https://en.wikipedia.org/wiki/Gzip" target="_blank">gzip</a> and <a href="https://en.wikipedia.org/wiki/Brotli" target="_blank">Brotli</a>. When making an HTTP request, browsers tell the server which compression methods they are able to decode, at which point the server sends the appropriately compressed file version. Browsers that do <b class="post__p--italic">not</b> support the available compression methods will receive uncompressed data. While sending and receiving uncompressed data probably isn’t common in 2024, it’s worth bearing in mind for people using older or more niche devices that don’t receive automatic software updates.</p><p class="post__p">You can inspect HTTP request and response headers for network requests in browser dev tools to understand more about what’s described above. Here’s an example:</p><img src="https://images.ctfassets.net/56dzm01z6lln/ha4YDTRdBkMrAPCIaQiAz/4e57e8f66bc47f0144fdf0f06873a6c6/network_tab_1.png" alt="Dev tools showing the network tab in Chrome, with the content-encoding and content-length response headers highlighted using a yellow rectangle, and the accept-encoding request header also highlighted." height="1152" width="2000" /><p class="post__p">Request headers (sent to the server):</p><ul><li><p class="post__p"><code>Accept-Encoding </code>lists the compression methods the client/browser supports</p></li></ul><p class="post__p">Response headers (received from the server):</p><ul><li><p class="post__p"><code>Content-Encoding </code>describes the compression method used to decode the transferred data</p></li><li><p class="post__p"><code>Content-Length </code>provides the size of the compressed file in bytes</p></li></ul><p class="post__p">If you’re using Sentry performance to monitor your applications, you can also inspect the response headers for each network request in the associated <a href="https://docs.sentry.io/product/sentry-basics/concepts/tracing/event-detail/#span-view" target="_blank">span</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5CmX6KlHnwe9k79iJUrqhP/b807058ee801fc87ea70e9003f22cd35/span_view_1.png" alt="A Sentry trace view showing the content length and decoded content length response headers for the main css file highlighted with a yellow rectangle." height="1152" width="2000" /><p class="post__p">What’s interesting is that the difference in size between a non-minified compressed file and a minified compressed file isn’t that significant. In the case of the CSS file for my personal website, there was a saving of only 1kB.</p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header"></th><th class="post__table__header">Raw (kB)</th><th class="post__table__header">Minified (kB)</th><th class="post__table__header">Saving (kB)</th></tr><tr class="post__table__row"><td class="post__table__cell">On disk</td><td class="post__table__cell">78</td><td class="post__table__cell">65</td><td class="post__table__cell">13</td></tr><tr class="post__table__row"><td class="post__table__cell">Compressed</td><td class="post__table__cell">11</td><td class="post__table__cell">10</td><td class="post__table__cell">1</td></tr></tbody>
      </table></div><p class="post__p">Whilst 1kB may not seem much, the savings will most definitely benefit both you and your users on a larger scale. As of March 2024 the global median internet speed on mobile is 52.98Mbps. While that equates to being able to download 6625kB per second (<a href="https://www.gbmb.org/mbps-to-kbs" target="_blank">calculated here</a> and rounded up to 53Mbps), if you can shave off just 1kB from a total of 100 resources on each page load, you could be making a 15ms improvement on your page speed for those users:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LCblLZIjAJ"
      aria-describedby="LCblLZIjAJ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LCblLZIjAJ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="LCblLZIjAJ" itemprop="text" content="How%20long%20does%20it%20take%20to%20transfer%20100kB%20at%206.625%20kB%2Fms%3F%0A%0A6625kB%2Fs%20transferred%0A6625%20%C3%B7%201000%20%3D%206.625%20kB%2Fms%20transferred%0A%0A100%20kB%20%C3%B7%206.625%20kB%2Fms%20%3D%2015ms*">
      <pre class="language-markdown"><code class="language-markdown">How long does it take to transfer 100kB at 6.625 kB/ms?<br><br>6625kB/s transferred<br>6625 ÷ 1000 = 6.625 kB/ms transferred<br><br>100 kB ÷ 6.625 kB/ms = 15ms*</code></pre>
    </div>
  </div>

  <p class="post__p">*<b class="post__p--italic">This is an extremely basic calculation not taking into consideration TCP slow starts or any other network effects that affect data transfer, but it serves as an example nonetheless.</b></p><p class="post__p">Simultaneously you’ll be saving bandwidth costs as a developer or organisation. While 1kB isn’t much of a saving for a single user, <b class="post__p--bold">for 1000 users this equates to transferring 1MB less data</b>.</p><h2 class="post__h2">HTTP/2 multiplexing</h2><p class="post__p">Another advancement that may mean CSS minification is not-that-important-anymore is HTTP/2. Released in 2015 and adopted over time, HTTP/2 allows the server to use a <b class="post__p--bold">single connection to deliver multiple requests and responses in parallel</b> (multiplexing). To take advantage of this, front end bundling tools and frameworks provide the ability to “code split” or “chunk” larger CSS files and JavaScript packages in order to ship smaller files simultaneously, whilst the underlying technology pieces those files together in the client for the web page to function correctly.</p><p class="post__p">If a non-minified compressed CSS file is only 10kB, and then split into smaller chunks of, say, 2kB, then minification becomes kind of a non-issue. Additionally, many modern front end frameworks take care of this for us anyway.</p><h2 class="post__h2">CSS minification and code-splitting in front end frameworks</h2><p class="post__p">If you’re using a modern front end framework, you’ll usually get CSS minification and/or code splitting out of the box. Frameworks provide their own opinionated, yet sensible default configurations for CSS post-processing, and you can choose to optimize those options further if you wish. Additionally, by serving your websites and applications from modern hosting platforms and CDNs, you’ll benefit from HTTP/2, caching and compression without even having to think about it.</p><p class="post__p">Here’s how just some of the modern JavaScript-based frameworks handle CSS today.</p><h3 class="post__h3">Astro</h3><p class="post__p"><a href="https://docs.astro.build/en/guides/styling/#bundle-control" target="_blank">Astro automatically minifies and combines your CSS into chunks</a>, and further optimizes CSS shared between pages for reuse. Another optimization from Astro is that it “inlines” any CSS chunks that are smaller than 4kB in size (i.e. puts the CSS in a <code>&lt;style&gt;</code> tag in the <code>&lt;head&gt;</code> of the page) to prevent too many network requests for small stylesheets. This provides a “balance between the number of additional requests and the volume of CSS that can be cached between pages.”</p><h3 class="post__h3">Next.js</h3><p class="post__p">Next.js comes with a number of <a href="https://nextjs.org/docs/app/building-your-application/styling" target="_blank">pre-configured optimizations</a> for using CSS Modules, Tailwind, Sass and and CSS-in-JS. For example, when using CSS Modules, many smaller files will be automatically bundled into fewer minified code-split CSS files to reduce the number of network requests.</p><h3 class="post__h3">Nuxt</h3><p class="post__p">Nuxt comes with <code>postcss</code> and a number of <a href="https://nuxt.com/docs/getting-started/styling#using-postcss" target="_blank">pre-configured CSS minification and optimization plugins</a> including <code>postcss-import</code>, <code>postcss-url</code>, <code>autoprefixer</code> and <code>cssnano</code>.</p><h3 class="post__h3">SvelteKit</h3><p class="post__p">SvelteKit uses <a href="https://vitejs.dev/config/build-options.html#build-csscodesplit" target="_blank">Vite</a> under the hood to build and bundle projects. This comes with configurable CSS code-splitting and minification.</p><p class="post__p">Where Astro inlines all CSS files smaller than 4kB in size, SvelteKit comes with an <a href="https://kit.svelte.dev/docs/configuration#inlinestylethreshold" target="_blank">inlineStyleThreshold configuration</a> which allows you to explicitly specify the maximum length of a CSS file in UTF-16 code units to be inlined. All CSS needed for the page and smaller than this value are merged and inlined in a <code>&lt;style&gt;</code> block.</p><h3 class="post__h3">Angular</h3><p class="post__p">Angular’s build process comes with an <code>--optimization</code> flag which enables CSS minification and inlining of critical CSS. To save even more bytes, you can also choose to use the <code>removeSpecialComments</code> flag during a build to remove comments in CSS and other extraneous characters.</p><h2 class="post__h2">Conclusion</h2><p class="post__p">With the luxury of modern web hosting platforms and CDNs caching and delivering files fast to users around the world, powerful compression algorithms and the wide adoption of HTTP/2, I guess we don’t really need to <b class="post__p--italic">think</b> about CSS minification anymore. It’s generally taken care of for us in modern front end tooling — albeit in opinionated ways. That being said, it is incredibly important for us, as front end developers, to understand how CSS is being delivered to the browser in our applications in order to monitor the impact it has on the performance of our apps in production.</p><p class="post__p">Yes, these modern frameworks are doing a lot for us. But can we do <b class="post__p--italic">more</b>?</p><hr class="post__hr" /><p class="post__p">Sarah Guthals reacts and discusses the main take-away of this post.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/dZVYp4UVCro?si=mRCJdl8BgImcD-KK"
        title="Should we worry about minifying CSS for performance gains?"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Your background images might be causing CLS</title>
          <description>I broke the first rule of preventing CLS. Shame on me.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/your-background-images-might-be-causing-cls/</link>
          <guid>https://blog.sentry.io/your-background-images-might-be-causing-cls/</guid>
          <pubDate>Thu, 11 Apr 2024 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Cumulative Layout Shift (CLS) is where the layout of a web page <b class="post__p--bold">unexpectedly</b> shifts after the initial content loads and new content pops in. At its best, it’s a little inconvenient. At its worst, it’s an accidental click of a “BUY NOW” button that suddenly appeared under your mouse cursor after an ad loaded, resulting in an unwanted purchase.</p><p class="post__p">CLS is one of the <a href="https://web.dev/explore/learn-core-web-vitals" target="_blank">Core Web Vitals</a> that your pages are scored (and ranked) on by Google. It’s often caused by client-side data-fetching after a page has first loaded, and loading large images, ads or embedded media players, all of which are made worse by slow internet connections. The purpose of using CLS as an indicator of performance is to ensure that we’re building great user experiences, and not causing users to accidentally perform actions they did not intend. Not all CLS is bad, however. Google states that “<a href="https://web.dev/articles/cls#expected-unexpected-layout-shifts" target="_blank">In fact, many dynamic web applications frequently change the start position of elements on the page. A layout shift is only bad if the user isn’t expecting it.</a>”</p><h2 class="post__h2">How is CLS measured?</h2><p class="post__p">CLS is measured on a decimal scale. A good <a href="https://web.dev/articles/cls" target="_blank">cumulative layout shift score</a> is 0.1 or less. A poor CLS score is greater than 0.25. Similar to how <a href="https://blog.sentry.io/what-is-inp/" target="_blank">Interaction to Next Paint (INP)</a> is measured, Google assesses your page for CLS based on the <b class="post__p--bold">largest burst of unexpected layout shifts</b> that occur during a user’s time on the page. Smaller one-off instances of layout shift may be inconsequential, but a bunch of layout shifts happening one after the other, i.e. <b class="post__p--italic">cumulatively</b>, is what we need to avoid.</p><h2 class="post__h2">How to avoid unexpected cumulative layout shift</h2><p class="post__p">There are tried-and-tested ways to avoid causing CLS on your web pages, including but not limited to:</p><ul><li><p class="post__p">Include <code>width</code> and <code>height</code> attributes on image elements and iframes.</p></li><li><p class="post__p">Provide placeholder elements (sometimes known as skeleton loaders) that are the <b class="post__p--bold">same dimensions as dynamic elements </b>that may be loaded via JavaScript later in the page life-cycle.</p></li><li><p class="post__p">Use the CSS at-rule <code>@font-face: size-adjust</code> to minimize any shifts caused by font files loading after the initial page render. Lazar Nikolov takes a deep dive into this topic in his article: <a href="https://blog.sentry.io/web-fonts-and-the-dreaded-cumulative-layout-shift/" target="_blank">Web Fonts and the Dreaded Cumulative Layout Shift</a>.</p></li></ul><p class="post__p">This post is not intended to be a complete guide to preventing CLS, but to highlight an edge case you might not be aware of. If you’re interested, however, Barry Pollard writes in depth about <a href="https://www.smashingmagazine.com/2021/06/how-to-fix-cumulative-layout-shift-issues/" target="_blank">how to fix CLS issues</a> in Smashing Magazine.</p><h2 class="post__h2">How to check for CLS during development</h2><p class="post__p">You can use the Performance tab in Chromium browsers (such as Edge, Brave, Chrome and Arc) to identify layout shifts and their associated scores. You may want to simulate slower internet connectivity and slower machines by enabling CPU and network throttling, depending on the target audience of your website.</p><img src="https://images.ctfassets.net/56dzm01z6lln/icolWKNZHNoZAc7Uhbwnx/882880e07c3b3d28e3b36a4a72aef050/1_chrome_perf_tab.png" alt="The performance tab dev tools open in Chrome. The Performance tab title and the CPU and network settings are highlighted with a green box." height="3078" width="5344" /><p class="post__p">Click the reload button to record the page load and wait for the profile to generate. If any layout shifts happen, you’ll see a Layout Shifts lane. Zoom in and click on a layout shift event, which will open up a summary tab below with additional details, including the cumulative score for that event.</p><img src="https://images.ctfassets.net/56dzm01z6lln/10G4ROduMORFjbXMwQNbuE/d50bcdc6373cd2c03351aaa195e22b80/2_layout_shift_lane.png" alt="A performance profile has been recorded in performance dev tools. A layout shift lane is available, highlighted with a green box. Below, the summary tab is open, and the cumulative score is also highlighted with a green box." height="3078" width="5344" /><p class="post__p">Additionally, a new and experimental tool in Chrome browsers called “Performance Insights” helps you identify layout shifts a lot quicker. To enable this tool, click on the three dot menu to the top right of dev tools, hover over “More tools” and click on “Performance insights”.</p><img src="https://images.ctfassets.net/56dzm01z6lln/592YuPqB2jEQZbR5qXSrm/e0ca5004f8041613d1f0d5ca3226329e/3_activate_performance_insights.png" alt="The more options menu is open in Chrome dev tools. An arrow is pointing to the three dot more options menu. Below that, a green arrow is pointing to the more tools item. Another menu is open to the left, and a green arrow is pointing to performance insights which has a little conical flask next to it, indicating experimental." height="3078" width="5344" /><p class="post__p">With the Performance Insights tab selected in dev tools, click the “Measure page load” button. This will refresh the page and record what happens on load. On the insights panel to the right, you’ll see any registered CLS with an associated score. Click the event to inspect more details about it, including the source of the layout shift in the HTML.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1GemtMPTCFFRDzZv7elfdk/0355bddc312a6bbf5b25924fd0462800/4_devto_cls.png" alt="A performance profile has been recorded using the new experimental performance insights tool. A green arrow is pointing to the bottom of a timeline, which shoes a cumulative layout shift event. It is coloured orange, indicating it needs improvement." height="3078" width="5344" /><h2 class="post__h2">CLS is not just for foreground elements</h2><p class="post__p">Now, let’s get to what we’re <b class="post__p--italic">really</b> here for. We usually talk about CLS disrupting the user experience by unexpectedly shifting content on a page that <b class="post__p--italic">a user might interact with</b>. As a result, I always figured CLS was calculated based on content shifting in the <b class="post__p--bold">foreground only</b>, i.e. interactive UI elements that are part of the actual user experience.</p><p class="post__p">However, I recently discovered that CLS is calculated <b class="post__p--italic">for all page elements</b>, including elements in the background that may not actually shift UI elements for the user. This makes perfect sense, actually. The CLS calculator can’t really be intelligent enough to take into account the z-index of a page element.</p><p class="post__p">How did I discover this? <b class="post__p--bold">Sentry found it for me on my website.</b></p><h2 class="post__h2">How to discover CLS in production for your real users</h2><p class="post__p">Whilst checking for CLS in development is good practice, nothing beats analyzing real data captured from real users interacting with your websites. I recently started using Sentry to <a href="https://sentry.io/for/performance/" target="_blank">monitor the performance</a> and <a href="https://sentry.io/for/web-vitals/" target="_blank">Core Web Vitals</a> for my personal website.</p><p class="post__p">I have configured <a href="https://docs.sentry.io/platforms/javascript/performance/#configure" target="_blank">Sentry Performance</a> to capture information for 50% of my traffic. For each captured event (or transaction), Sentry sends a number of associated tags which can include sources of CLS if relevant. What’s really helpful is that Sentry also includes the HTML elements that point to the source of the CLS, so you know exactly where in your code to look to fix it.</p><p class="post__p">To discover your top sources of CLS, open up Sentry and navigate to Performance &gt; Web Vitals. Below the top level performance score and score breakdown, you’ll see a table listing all of your page URLs. Click on the CLS header to sort by score descending to find the worst score. For my date range selected, the highest CLS was 0.66. This is the p75 score, which is the highest CLS value that 75% of users experienced for that page.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5oXSe8TdqpAvNqIzsr3ZWr/e415bddc86bedec91ef1622b6b177fbc/5_find_poor_CLS_1.png" alt="The Sentry Web Vitals dashboard for my website. It shows three events below an overall performance score and graph of performance recorded for different transactions over time. The CLS column for those three events is highlighted with a dark pink rectangle." height="2918" width="5256" /><p class="post__p">Click on the top item in the table. You’ll then see an overview of all sampled events for that particular page (not just p75). Sort the table of events by CLS descending again to find the highest score. I’m going to investigate an event that reported a CLS of 0.92.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1YbyJBvEWQ5aJlelt4Q70l/0bcca5c606d0a5ab76b8e28c9697c5e8/6_find_poor_CLS_2.png" alt="The performance view for a single route on my website over time. The top event in the table is highlighted using a dark pink rectangle to show this is the event we want to inspect further." height="3006" width="5344" /><p class="post__p">On the event details page, scroll down and look for the Tag Details section, which should show a tag key of <code>cls.source.1</code>. Hover over it to see the full HTML element source captured.</p><img src="https://images.ctfassets.net/56dzm01z6lln/UtjU7hcYRyhdF8qteClrE/1f9054b63e2d524a732f938b215d7869/7_cls_source_pseudo.png" alt="We've zoomed in on the CLS source for an event on the performance timeline. We can see that the after pseudo element of the main element is the first source of CLS on this page." height="1176" width="4060" /><p class="post__p">Now here’s where things got interesting. For this page on my site, the main source of CLS was coming from a CSS pseudo element attached to the <code>main</code> element, which contained an SVG that provided a little bit of design flair to the page. Here’s what I was sure of:</p><ul><li><p class="post__p">The SVG was added to the page via the CSS <code>content</code> property.</p></li><li><p class="post__p">It had a z-index of -1 with a fixed position.</p></li><li><p class="post__p">It didn’t cause any foreground content to shift visually.</p></li></ul><p class="post__p">So what was the deal, here?</p><img src="https://images.ctfassets.net/56dzm01z6lln/p3ZBxDTsn7GEMTqq1hIGL/09c3c535961f1e834c09a9ae152f7287/8_website_bg_problem.png" alt="Dev tools is open on my website. All foreground elements on the page have been hidden for demonstration, apart from the header. Green arrows are pointing to the after pseudo element of the main tag, and the content property of this element, which is pointing to a CSS variable which is a URL to an SVG." height="3078" width="5344" /><h2 class="post__h2">I broke the first rule of CLS: width and height attributes</h2><p class="post__p">Much to my disappointment, I discovered that I had not specified a width and height on the SVG that was added to the page via CSS. Shame on me. Fixing my mistake by adding a width and height on the SVG meant I had to make some changes to the CSS. And in hindsight, the changes actually led to more semantically correct CSS (more on that, later).</p><p class="post__p">Here’s the diff of the changes and let’s explore why those changes were necessary.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ruYUlhIITz"
      aria-describedby="ruYUlhIITz">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ruYUlhIITz">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="ruYUlhIITz" itemprop="text" content="%20%20main%3A%3Aafter%20%7B%20%20%20%0A-%20%20%20content%3A%20url(%22path-to.svg%22)%3B%0A%2B%20%20%20content%3A%20%22%22%3B%0A%2B%20%20%20background-image%3A%20url(%22path-to.svg%22)%3B%0A%2B%20%20%20background-repeat%3A%20no-repeat%3B%20%20%20%0A%2B%20%20%20background-size%3A%20cover%3B%0A%2B%20%20%20height%3A%200%3B%0A%2B%20%20%20padding-bottom%3A%20calc(100%25%20*%20201%20%2F%201280)%3B%20%2F*%20image%20aspect%20ratio%20*%2F%0A%20%20%20%20position%3A%20fixed%3B%0A%20%20%20%20bottom%3A%200%3B%0A%20%20%20%20left%3A%200%3B%0A%20%20%20%20width%3A%20100%25%3B%0A%20%20%20%20z-index%3A%20-1%3B%0A%20%20%7D">
      <pre class="language-diff-css"><code class="language-diff-css">main::after {   <br><span class="token deleted-sign deleted language-css"><span class="token prefix deleted">-</span>   <span class="token property">content</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">"path-to.svg"</span><span class="token punctuation">)</span></span><span class="token punctuation">;</span><br></span><span class="token inserted-sign inserted language-css"><span class="token prefix inserted">+</span>   <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">""</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token property">background-image</span><span class="token punctuation">:</span> <span class="token url"><span class="token function">url</span><span class="token punctuation">(</span><span class="token string url">"path-to.svg"</span><span class="token punctuation">)</span></span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token property">background-repeat</span><span class="token punctuation">:</span> no-repeat<span class="token punctuation">;</span>   <br><span class="token prefix inserted">+</span>   <span class="token property">background-size</span><span class="token punctuation">:</span> cover<span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token property">height</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token property">padding-bottom</span><span class="token punctuation">:</span> <span class="token function">calc</span><span class="token punctuation">(</span>100% * 201 / 1280<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">/* image aspect ratio */</span><br></span><span class="token unchanged language-css"><span class="token prefix unchanged"> </span>   <span class="token property">position</span><span class="token punctuation">:</span> fixed<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token property">bottom</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token property">left</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token property">width</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token property">z-index</span><span class="token punctuation">:</span> -1<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Why I chose a pseudo element</h3><p class="post__p">Pseudo-element selectors allow you to use CSS to style a specific part of a DOM element, such as the box that is drawn before or after an element. The initial value for the <code>content</code> property of the <code>::before</code> and <code>::after</code> pseudo elements for each DOM element is <code>normal</code>, which computes to <code>none</code>. This prevents the DOM from drawing an excess number of boxes that might never get used. You can read more in the <a href="https://www.w3.org/TR/CSS22/generate.html#content" target="_blank">official W3C spec for the content property</a>, but it’s a difficult read.</p><p class="post__p">Given this SVG was a design detail and not important in the flow of page content, I chose a pseudo element to display it to document that hierarchy in the code. Originally, the SVG was added to the page via the <code>content</code> property of the <code>::after</code> pseudo element of the <code>main</code> HTML element.</p><h3 class="post__h3">The problem with styling SVGs in pseudo elemnets</h3><p class="post__p">Adding width and height attributes to the SVG without modifying the original CSS meant that the image didn’t span the full width of the page as intended. Instead, <code>width: 100%</code> in the CSS calculated the SVG width as 100% of its given width attribute size (1280px).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="SoCAfXESgO"
      aria-describedby="SoCAfXESgO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="SoCAfXESgO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="SoCAfXESgO" itemprop="text" content="%3Csvg%20viewBox%3D%220%200%201280%20201%22%20width%3D%221280%22%20height%3D%22201%22%20role%3D%22img%22%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3C!--%20SVG%20paths%20here%20--%3E%0A%3C%2Fsvg%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>svg</span> <span class="token attr-name">viewBox</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>0 0 1280 201<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1280<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>201<span class="token punctuation">"</span></span> <span class="token attr-name">role</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>img<span class="token punctuation">"</span></span>   <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://www.w3.org/2000/svg<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token comment">&lt;!-- SVG paths here --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>svg</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">In order to stretch an SVG to 100% of a container width using the width property in CSS, you need to be able to target the SVG element in CSS directly. Given the SVG was added via the <code>content</code> property of a pseudo element, this wasn’t possible: objects inserted using the content property are known as <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Replaced_element" target="_blank"><b class="post__p--italic">replaced elements</b></a>, which are actually outside the scope of CSS. Adding a relative <code>width</code> attribute value of 100% to the SVG itself wouldn’t have solved the CLS issue either — because the browser needs an absolute pixel value in order to calculate the space in which to draw it.</p><p class="post__p">And this got me thinking, if the <code>content</code> property made this so <b class="post__p--italic">difficult to achieve</b>, there must have been an easier way to make it work. And here’s where I started thinking about the concept of <b class="post__p--bold">semantic CSS</b>.</p><h3 class="post__h3">A move to semantic CSS</h3><p class="post__p">The use of the <code>content</code> property contradicted the intention of this background image. It wasn’t content, it was a background image. And so in the final implementation, I set the <code>content</code> property to an empty string so that the pseudo element could be drawn, and used the <code>background-image</code> properties to configure the SVG as a background image, as the design intended.</p><p class="post__p">If you’re curious about the padding-bottom hack, this was necessary in order to scale the SVG proportionately when stretching it across the full width of the viewport given that auto-sizing isn’t an option for CSS background images. It wasn’t possible to set the width of the SVG as 100% and height as auto in order to achieve the desired result. <a href="https://css-tricks.com/scale-svg/#aa-option-2-use-css-background-images-and-the-padding-bottom-hack" target="_blank">Read more about scaling SVG in Amelia’s article on CSS Tricks</a>.</p><h2 class="post__h2">This might have been trivial, but it matters</h2><p class="post__p">This was perhaps a trivial issue to investigate in such depth. However, CLS is one of those Core Web Vital scores that is prone to suffering from death by <b class="post__p--italic">1000 cuts</b>. Sure, small bits of layout shift here and there are inevitable, and sometimes, acceptable. But the more you focus on solving the small problems when they arise, the less likely it is that you’ll suffer from the <b class="post__p--bold">cumulative</b> effect of performance issues. Which means you spend less time fixing, and more time building.</p><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I fixed my brutal TTFB</title>
          <description>I was EMBARRASSED.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/how-i-fixed-my-brutal-ttfb/</link>
          <guid>https://blog.sentry.io/how-i-fixed-my-brutal-ttfb/</guid>
          <pubDate>Wed, 27 Mar 2024 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Recently, I improved <b class="post__p--italic">all</b> my homepage Core Web Vitals by focusing on improving just one metric: the Time to First Byte (TTFB). All it took was two small changes to how data is fetched to reduce the p75 TTFB from 3.46s to <b class="post__p--bold">just 704ms</b>. In this post I’ll explain how I found the issues, what I did to fix them, and the important decisions I made along the way. (And don’t worry, I’ll break down “p75” and “TTFB”, too!)</p><img src="https://images.ctfassets.net/56dzm01z6lln/2AOs9UmrbYJ6VmufDHeFoW/1bc86b31111b3726e554f031a2fd542e/before_after.png" alt="Two comparison performance graphs in Sentry. Before the overall score is 25, after the overall score is 78. The score breakdowns for the before are mainly poor, the score breakdowns for the after are good or meh." height="1730" width="2013" /><h2 class="post__h2">Using Sentry performance monitoring</h2><p class="post__p">As developers, we usually focus on performance for short bursts at a time: when a new site is launching, when a new feature is in development, or when we discover our site is really, <b class="post__p--italic">really</b> slow. During 2021-22<a href="https://whitep4nth3r.com/blog/how-i-improved-website-performance/" target="_blank"> <u>I rebuilt my website from the ground up in order to improve performance</u></a> and the results were great. But, over time I added so many extra bits of experimental technology to different areas of the site that performance, once again, had become embarrassingly bad. I’d noticed this myself when loading my website over the last few months, but it was only when I added <a href="https://sentry.io/for/performance/" target="_blank"><u>Sentry performance monitoring</u></a> to my site that I was able to see the full picture.</p><p class="post__p">What’s great about using a performance monitoring tool like Sentry is that it shows you <b class="post__p--italic">real user data</b> for your websites, across all the operating systems, browsers, mobile devices, internet connections and many other factors that affect the user experience. Previously I’ve used tools such as the Google Lighthouse in development or during a website build to analyze performance for each new build — but this only gave me a snapshot of the performance scores during the build pipeline for build servers. <b class="post__p--bold">Real user data is far more valuable.</b></p><p class="post__p">Here’s what the performance looked like on my homepage before any modifications from February 14-21 2024.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2ubFOuKsvd9RoS7F69zK8G/c19e6f0914f74b9efc2e414960475605/sentry_1.0.0.png" alt="A performance graph in Sentry showing mostly poor core web vitals. The overall performance score is 35." height="865" width="2013" /><p class="post__p">The most urgent thing that stood out to me to improve was the Time to First Byte (TTFB). TTFB refers to the time it takes for a browser to receive the first byte of response after it made a request to the server. Theoretically, the lower the TTFB, the sooner the browser can start painting the page, and the sooner users will see something in the browser, making them less likely to bounce away. </p><p class="post__p">The TTFB value displayed here is for the 75th percentile (p75), meaning that 3.46s was the worst score found across 75% of all homepage views. This also means that 25% of users were waiting for <b class="post__p--italic">longer</b> than 3.46 seconds for the page to load. This poor score indicated that there was too much processing happening on the server before the response could be sent back. Here’s what was happening.</p><p class="post__p">For some time, I had been using <b class="post__p--italic">two</b> separate middleware functions (or Edge Functions) at request time: one to fetch data from my newsletter provider to get the latest number of subscribers, and one to fetch data from the Twitch API to show my latest stream video, or the latest thumbnail from a current live stream in progress. Both functions would grab the initial HTTP response in memory, fetch some data from a third-party API, and rewrite the HTML accordingly. </p><img src="https://images.ctfassets.net/56dzm01z6lln/5lJA0DiKq1YaznKngiF5Cg/f4babda92e194f1e50f7426126e5ce08/homepage_request_chain.png" alt="An HTTP request is made. A newsletter.js file intercepts the request, modifies the HTML and returns it to the chain. Next, a twitch.js file intercepts the request again, modifies the HTML and returns the HTTP response." height="1176" width="2328" /><p class="post__p">The aim of this architecture was to minimize client-side data-fetching which could block the main JavaScript thread to show some dynamic data on a statically generated homepage (and I hate skeleton loaders). This was great from a &quot;show the user the most up to date thing&quot; point of view, but the downsides of this is that it effectively duplicated the HTTP request and therefore doubled the time it took to show something in the browser. Add on top of this the varying API latency from two separate third-party services in static regions being called from anywhere in the world (<b class="post__p--italic">the edge</b>) and you start getting into a bit of a mess.</p><p class="post__p">Plus, what use did the accurate newsletter subscriber number provide to anyone except me? And why did I <b class="post__p--italic">really</b> need to show the latest randomly generated stream thumbnail, especially given that most of the time it was a very unflattering image of me trying to work out how to code? People aren’t sitting in front of my homepage and refreshing it every few minutes to get an updated Twitch thumbnail. I had gone too far.</p><h2 class="post__h2">Approximating data is fine, actually</h2><p class="post__p">My priority at this point was to focus on improving the TTFB. The first step was straightforward: remove the Edge Function that fetched the number of newsletter subscribers, and instead fetch the data at build time and generate it statically. The number won’t be up to date until I do a rebuild, but we can account for small inaccuracies by adding a plus symbol to the number, or returning a string to approximate the number if the API call errors at build time.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="sxrrsOtzcJ"
      aria-describedby="sxrrsOtzcJ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="sxrrsOtzcJ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="sxrrsOtzcJ" itemprop="text" content="function%20getNewsletterSubscribers()%20%7B%0A%20%20let%20subscribers%3B%0A%0A%20%20try%20%7B%0A%20%20%20%20const%20response%20%3D%20await%20fetch(%0A%20%20%20%20%20%20%22https%3A%2F%2Fapi.email-provider.com%2Fsubscribers%22%2C%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F...%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20)%3B%0A%0A%20%20%20%20const%20result%20%3D%20await%20response.json()%3B%0A%20%20%20%20subscribers%20%3D%20%60%24%7Bresult.count.toString()%7D%2B%60%3B%0A%20%20%7D%20catch%20()%20%7B%0A%20%20%20%20subscribers%20%3D%20%22loads%20of%22%3B%0A%20%20%7D%0A%0A%20%20return%20subscribers%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">getNewsletterSubscribers</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> subscribers<span class="token punctuation">;</span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><br>      <span class="token string">"https://api.email-provider.com/subscribers"</span><span class="token punctuation">,</span><br>      <span class="token punctuation">{</span><br>        <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token comment">//...</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    subscribers <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>result<span class="token punctuation">.</span>count<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">+</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    subscribers <span class="token operator">=</span> <span class="token string">"loads of"</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">return</span> subscribers<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Removing just one Edge Function from the middleware chain improved the p75 TTFB greatly — and the difference was actually noticeable in the browser as a user loading the page. I monitored this single change for a week and the p75 value for TTFB had decreased from 3.46 seconds to just 1.88 seconds. This was a 46% decrease in how long it took for 75% of users to see something on my homepage in the browser. With one small change, all Core Web Vital scores had also improved.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2dFVkMv50qaHkA2hSUtZL0/9fbcad38a7415bb724ce579c8a391a92/sentry_1.0.1.png" alt="A performance graph in Sentry, showing the overall performance score as 64. Core web vitals are meh or good, with the exception of time to first byte which is still poor at 1.88 seconds." height="865" width="2013" /><h2 class="post__h2">The problem with moving data fetching from the server to the client</h2><p class="post__p">The next step was to remove the Edge Function that fetched the Twitch data. My hypothesis was that moving this to the client and writing the data to the DOM when it was ready would improve the <b class="post__p--italic">perceived performance</b> of the page for users — even if the data wasn’t all there yet. Without the middleware intercepting the HTTP request, the TTFB would reduce, and users would see <b class="post__p--italic">something</b> in the browser sooner.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4qPFXb7NZqEUNd9F8SAZMZ/4cd47182bdad6b2d3013ee97eca4a2cf/server_vs_client.png" alt="Two diagrams comparing the time to first byte journey. The first diagram shows the TTFB is longer when middleware intercepts the HTTP request, modifies HTML and returns it afterwards. The second diagram shows that the TTFB is shorter when no extra requests for data are made on the server, but the downside is that the data needs to be fetched on the client, which requires updating the DOM after the page has loaded." height="2616" width="2214" /><p class="post__p">First, I moved the Twitch data fetching from the server to a client-side<a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API" target="_blank"> <u>web worker</u></a>, to avoid introducing new render-blocking behavior on the main thread. However, whilst the TTFB did indeed decrease,<a href="https://web.dev/articles/cls" target="_blank"> <u>Cumulative Layout Shift (CLS)</u></a> became a problem.  (I also could have opted to load the JavaScript as an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#async" target="_blank"><u>async</u></a> <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer" target="_blank"><u>deferred</u></a> script but the visual results would have been the same.) </p><p class="post__p">There are always tradeoffs to make when improving site performance, specifically with regards to<a href="https://web.dev/explore/learn-core-web-vitals" target="_blank"> <u>Core Web Vitals</u></a>. When you make improvements in one metric, you may end up sacrificing the score of another. Fetching the data and updating the DOM after the page had loaded meant that the Twitch stream thumbnail loaded up to a second later in my dev environment, causing the page content to shift. This could be even longer for real users. Layout shifts usually happen when the element’s size is not defined in the initial HTML or CSS. There are ways to get around this, such as by including a placeholder image of the same size (with height and width specified on the element) that is later replaced by the fetched image, but in my opinion this is also not a good experience, especially on slower internet connections. <b class="post__p--bold">At this point, I had moved one performance problem from the server and created a new performance problem in the client</b>.</p><p class="post__p">It was time to make my website <b class="post__p--italic">as static as possible </b>again<b class="post__p--italic"> —</b> but this approach still came with some tradeoffs.</p><h2 class="post__h2">Making sensible tradeoffs to reduce TTFB</h2><p class="post__p">When I rebuilt my website two years ago, I made a deliberate decision to build it as a static site using as little client-side JavaScript as possible, and the simple no-nonsense design of the site took this into account. My website has always been designed to be a marketing channel for my Twitch streams, and so I always want to include <b class="post__p--italic">something</b> about Twitch on the home page. When I first launched the website rebuild in 2022, I included a link to the next scheduled stream which was fetched and pre-generated at build time. Each time I went on or offline on Twitch, I used a webhook to rebuild the site to update it with new information. If you’re curious,<a href="https://deploy-preview-32--mk2-p4nth3rblog.netlify.app/" target="_blank"> <u>here’s a snapshot of the website when it first launched</u></a>.</p><p class="post__p">To improve the TTFB without introducing new CLS, I made the homepage static again, rebuilding it using a webhook (in my Twitch bot application) each time I go on or offline on Twitch. If I’m not live on Twitch, the page is statically generated with my latest stream thumbnail and information at build time. If I <b class="post__p--italic">am</b> live on Twitch, here’s where the performance tradeoffs come into play.</p><p class="post__p">Instead of fetching data from the Twitch API at request time to get the latest live stream information, I now use the<a href="https://dev.twitch.tv/docs/embed/video-and-clips/" target="_blank"> <u>Twitch video player embed</u></a> to show the current live stream. The downside to this is that the page loads some extra client-side JavaScript to show the player. However, given that I’m live streaming for around <b class="post__p--bold">only</b> <b class="post__p--bold">six hours per week</b>, I figured this was a good tradeoff. The rest of the time you get a super-fast static experience.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2umWprhIkK7uxxcJej0NCj/84f778db8880be52f4a0ffa20d8cc273/live_not_live.png" alt="Two screenshots of my website home page. The top screenshot shows what the page looks like when I'm not live. It contains a monochrome thumbnail with the stream title, and how long ago the stream was created. The bottom screenshot shows the twitch video player, which appears when I'm live." height="4298" width="4136" /><p class="post__p">Here’s a simplified view of the code for the Twitch component (which is a JavaScript function that builds static HTML) on my homepage for completeness. The <code>isLive</code> and <code>vodData</code> parameters are fetched at build time from the Twitch API.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="gQzhTsiFrO"
      aria-describedby="gQzhTsiFrO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="gQzhTsiFrO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="gQzhTsiFrO" itemprop="text" content="function%20TwitchInfo(%7B%20isLive%2C%20vodData%20%7D)%20%7B%0A%20%20return%20%60%0A%20%20%24%7B%0A%20%20%20%20!isLive%0A%20%20%20%20%20%20%3F%20%60%3Ca%20href%3D%22%24%7BvodData.link%7D%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%3Cp%3E%24%7BvodData.title%7D%3C%2Fp%3E%0A%20%20%20%20%20%20%20%20%20%20%20%3Cp%3E%24%7BvodData.subtitle%7D%3C%2Fp%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cimg%0A%20%20%20%20%20%20%20%20%20%20%20%20src%3D%22%24%7BvodData.thumbnail.url%7D%22%0A%20%20%20%20%20%20%20%20%20%20%20%20alt%3D%22Auto-generated%20stream%20screenshot.%22%0A%20%20%20%20%20%20%20%20%20%20%20%20height%3D%22%24%7BvodData.thumbnail.height%7D%22%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3D%22%24%7BvodData.thumbnail.width%7D%22%0A%20%20%20%20%20%20%20%20%20%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2Fa%3E%60%0A%20%20%20%20%20%20%3A%20%60%3Cdiv%20id%3D%22twitch-embed%22%3E%3C%2Fdiv%3E%0A%20%20%20%20%20%20%3Cscript%20src%3D%22https%3A%2F%2Fembed.twitch.tv%2Fembed%2Fv1.js%22%3E%3C%2Fscript%3E%0A%20%20%20%20%20%20%3Cscript%3E%0A%20%20%20%20%20%20%20%20new%20Twitch.Embed(%22twitch-embed%22%2C%20%7B%2F*%20options%20*%2F%7D)%3B%0A%20%20%20%20%3C%2Fscript%3E%60%0A%20%20%7D%0A%20%20%60%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">TwitchInfo</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> isLive<span class="token punctuation">,</span> vodData <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br>  </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><br>    <span class="token operator">!</span>isLive<br>      <span class="token operator">?</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;a href="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>vodData<span class="token punctuation">.</span>link<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"><br>           &lt;p></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>vodData<span class="token punctuation">.</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/p><br>           &lt;p></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>vodData<span class="token punctuation">.</span>subtitle<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/p><br>          &lt;img<br>            src="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>vodData<span class="token punctuation">.</span>thumbnail<span class="token punctuation">.</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"<br>            alt="Auto-generated stream screenshot."<br>            height="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>vodData<span class="token punctuation">.</span>thumbnail<span class="token punctuation">.</span>height<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"<br>            width="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>vodData<span class="token punctuation">.</span>thumbnail<span class="token punctuation">.</span>width<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"<br>          /><br>        &lt;/a></span><span class="token template-punctuation string">`</span></span><br>      <span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;div id="twitch-embed">&lt;/div><br>      &lt;script src="https://embed.twitch.tv/embed/v1.js">&lt;/script><br>      &lt;script><br>        new Twitch.Embed("twitch-embed", {/* options */});<br>    &lt;/script></span><span class="token template-punctuation string">`</span></span><br>  <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br>  </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Improving performance is a balancing act</h2><p class="post__p">Ultimately, you may have to make some compromises in order to make performance gains. By deciding that it’s acceptable to show inaccurate data and load some extra JavaScript for a few hours per week, I’ve improved the Core Web Vitals scores massively on my homepage, which is the most visited page on my site. Inaccurate data might not be appropriate for every website and app — but it’s something to consider when balancing performance improvements.</p><p class="post__p">After removing both Edge Functions that ran on my homepage, and returning to a completely static build, I reduced the p75 TTFB by 80% to just 704ms. Whilst 25% of users are still experiencing a TTFB of over 704ms, 75% of my users see a page loaded in under 704ms. I’m really happy with the progress so far. If this isn’t a glorious advertisement for static sites and in turn, static site generators, I don’t know what is.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2ezbXqJfHnB3wPwJLkupcC/14f9c1be2e116fe116cea9a4e53ff58b/sentry_1.0.2.png" alt="A performance graph in Sentry, now showing an overall performance score of 78. The reported core web vitals are either good or meh. The TTFB is now reported as 704ms." height="865" width="2013" /><p class="post__p">I’ve still got some further optimizations to make on the homepage, such as some local image optimizations (such as serving images in avif and webp formats where supported), rendering the webring component that loads in third-party JavaScript statically, optimizing font files (because there’s a huge fancy font file I load in to use only three characters as background textures), and perhaps addressing the render-blocking single CSS file. </p><p class="post__p">As my good friend Cassidy Williams says:<a href="https://css-tricks.com/add-less/" target="_blank"> <u>“</u><b class="post__p--bold"><b class="post__p--italic"><u>Your websites start fast until you add too much to make them slow.”</u></b></b></a></p><hr class="post__hr" /><h2 class="post__h2">Watch a video recap</h2>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/6F-os-YmjrQ?si=KWr1hmwjxasllpdr"
        title="Website Nightmares: How I fixed my BRUTAL TTFB"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>What is INP and why you should care</title>
          <description>Your website performance is now trash until you optimize for INP.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/what-is-inp/</link>
          <guid>https://blog.sentry.io/what-is-inp/</guid>
          <pubDate>Mon, 11 Mar 2024 12:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">On March 12th 2024, Google is launching a new Core Web Vital metric, <a href="https://web.dev/blog/inp-cwv-march-12" target="_blank">Interaction to Next Paint (INP)</a>. INP will replace First Input Delay (FID) and will change the way your sites are assessed for performance by Google, which ultimately affects how your sites rank in search engine results.</p><p class="post__p"><b class="post__p--bold">TL;DR: You need to start optimizing for INP today so your sites are not negatively impacted after March 12th.</b></p><h2 class="post__h2">Why the change?</h2><p class="post__p">The <a href="https://web.dev/articles/fid" target="_blank">FID Core Web Vital</a> was designed to measure how quickly a webpage responds to a user’s first action on that page. A good FID score is achieved at 100ms or less.</p><p class="post__p">However, <a href="https://web.dev/articles/inp" target="_blank">“Chrome usage data shows that 90% of a user’s time on a page is spent after it loads,”</a> so FID doesn’t capture the full user experience. As a result, INP has been designed to measure a page’s overall <b class="post__p--italic">perceived</b> responsiveness by observing clicks, taps and key presses across the entire user journey on the page — rather than just the first interaction.</p><p class="post__p">The final INP value is calculated as the longest time observed between the user performing an action and receiving visual feedback. Examples of user interactions include clicking a button to add a product to a cart, clicking an expandable accordion or tapping a collapsed navigation menu, and typing into an input field. INP focuses on the time it takes for visual feedback to be presented after a particular action is performed (i.e. the next paint — where painting is the process in which browsers add pixels to the screen), and it excludes eventual effects of an interaction (such as network requests or UI updates).</p><img src="https://images.ctfassets.net/56dzm01z6lln/Eu5wibk7DsbIt96BHBt7Q/e421db6667bcbc4599744d10b8069253/add_to_cart.gif" alt="Two buttons. Left button is clicked and UI provides no visual feedback for 2 seconds. Right button is clicked and shows a loading state immediately." height="218" width="600" /><h2 class="post__h2">Investigating INP in dev tools</h2><p class="post__p">Let’s look at how to investigate actions and events that contribute to the INP metric using Chromium-based browsers such as Chrome, Brave, Edge, and Arc.</p><p class="post__p">Choose a web page that accepts one of the following user interactions that INP measures (scrolling and moving the pointer, for example, is not measured):</p><ul><li><p class="post__p">Clicking with a mouse</p></li><li><p class="post__p">Tapping on a device with a touchscreen</p></li><li><p class="post__p">Pressing a key on either a physical or onscreen keyboard</p></li></ul><p class="post__p">I’ve chosen to investigate what happens when I search for “web vitals” on the Sentry Docs home page. Open up the dev tools panel and click on the “Performance” tab.</p><img src="https://images.ctfassets.net/56dzm01z6lln/Agig9cb3vb7mBsuk3KCB3/0ad81e520ebcf89853b69e8c82bf1471/click_performance_tab.png" alt="Chrome dev tools, showing the performance tab, with an arrow pointing to the performance tab title." height="3078" width="5344" /><p class="post__p">Depending on the website’s target audience, it’s often useful to throttle the CPU or network speed to analyze the user experience on older machines and/or slower internet. If you want to simulate a slower CPU or network, configure the options accordingly.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6yhFFkswX0PTge0OEmJK23/62d056fccb48ed4c757d3136d5c7f8fa/throttle.png" alt="Chrome dev tools performance tab, showing a rectangle to highlight the CPU throttle and network slow down options." height="3078" width="5344" /><p class="post__p">Click the record button, and perform some interactions on the page.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7H8EpLGPvZ0xLT0mUwIUL2/a0a99cdde49fdf3e5ae01039103111c9/record_profile.gif" alt="Short animated GIF showing a click on the record button, and typing web vitals into the search bar on the Sentry docs home page." height="375" width="600" /><p class="post__p">Stop recording, and wait for the profile to load. You’ll now see a LOT of data in the form of color-coded blocks with labels in named lanes. The most important lanes to zoom in on when analyzing contributing factors to INP are “Interactions” (any user interactions performed on the page) and “Main” (the browser’s main thread).</p><img src="https://images.ctfassets.net/56dzm01z6lln/2LPabCURDJaRwsd4gMxtQc/348521a4d2f4d77652f85a8b77d28f89/lanes.png" alt="A performance profile view, with rectangles annotating the interactions and main lanes." height="2794" width="4336" /><p class="post__p">For context, the main thread is <a href="https://developer.mozilla.org/en-US/docs/Glossary/Main_thread" target="_blank">“where a browser processes user events and paints.”</a> By default, the browser uses the main thread to run all JavaScript on a page, layout the page, recompute any changes, and allocate and free up memory (garbage collection). This means that <b class="post__p--bold">long-running JavaScript functions can block the main thread, leading to unresponsive pages and a bad user experience</b>. This is what Google is encouraging developers to solve by introducing the INP metric.</p><p class="post__p">When inspecting your recorded performance profile, you may see blocks on the main thread highlighted in red; this indicates a long running task. Hovering over a task gives you the time it took to run, and under that task block, you’ll find all of the separate events and function calls that made up that task. Zoom in/out by scrolling on your mouse or trackpad, and use the scrollbars to the right of the window to scroll up and down the stack.</p><img src="https://images.ctfassets.net/56dzm01z6lln/CoqezyPuYZDGlstvjNsu0/72938da26e0fa4205cb43f1e70aeb0a2/long_task_hover.png" alt="A zoomed in view of the performance profile, with the mouse hovering over a long task that took 90.16ms." height="944" width="3128" /><p class="post__p">The summary tab below the lanes shows how much time in each task was allocated to different processes. This will vary between tasks, but usually you’ll observe that “Scripting” (i.e. JavaScript processes) and “Loading” takes the longest time.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2Y23Cmk8hc5Jo1JVfeWWDQ/4c7fd26cae331eed184eb9e7e20a21ed/aggregate_summary.png" alt="The summary tab expanded, with a rectangle highlighting the aggregated time metrics for a single task." height="2794" width="4336" /><p class="post__p">Now, let’s cross-reference the main thread activity with the interactions lane. In this example, I typed the term “web vitals” into the search input on the Sentry Docs home page. Under the task blocks on the main thread, you’ll see a number of “Event: keypress” blocks that were captured when I typed each letter. In the interactions lane, there are blocks labeled “Keyboard”, indicating when I typed. Hovering over those blocks tells us that “Long interaction is causing poor performance.”</p><img src="https://images.ctfassets.net/56dzm01z6lln/VhyZkHhMDI00HR7OC45Z7/79887faff9ca8acdb86bba5251e38195/keypresses_long_response.png" alt="A zoomed in view of the main thread, showing three key press events with arrows pointing to them. The mouse is hovering over one of the keypresses and a popup has appeared showing that a keyboard interaction was long and is indicating poor performance." height="2794" width="4336" /><p class="post__p">Also notice the whiskers on either side of the blocks. <b class="post__p--bold">These indicate that interactions are currently blocked by activity on the main thread, meaning the page might appear unresponsive</b>. As I was typing (with CPU throttling), this unresponsiveness was obvious.</p><p class="post__p">Clicking on a keyboard interaction block allows us to inspect this interaction further. The summary shows us that this interaction had an input delay of 45ms, processing time of 83.1ms, and a presentation delay of 84.638ms. The total time of the interaction took 212.74 ms.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4CPHeG8FQSpM47xUZEjOQd/e448d042cc042c59273c1e5d8924df55/keyboard_action_summary.png" alt="The keyboard action summary tab, showing again that the long interaction is indicating poor page responsiveness, and some associated metrics." height="2794" width="4336" /><p class="post__p">Google states that <a href="https://web.dev/articles/inp?utm_source=devtools#good-score" target="_blank">a good INP score is equal to or less than 200ms</a>, a poor score is greater than 500ms, and a score that needs improvement is between 200 and 500ms. Based on these benchmarks, our scores need improvement.</p><p class="post__p">To understand which processes are taking the longest inside each task, select a task block, and click on the “Bottom-Up” tab below. This shows which activities directly took up the most time in total. Click the “Total Time” column header to sort by time ascending or descending.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2kGS9gFxteiI5Qfow8U3R8/9bcb6c38b069b06cc5557e27d051f8d0/bottom_up.png" alt="The bottom up tab view for a single task, with a rectangle highlighting the total time column which you can click to sort events." height="2794" width="4336" /><p class="post__p">Expand each event to see the full list of subsequent events and their respective execution times. Comparing this detailed list to what you thought should be happening can help you identify any anomalies.</p><h2 class="post__h2">Improving INP for your websites and apps</h2><p class="post__p">Now that you’re familiar with how to investigate blocking processes by cross-referencing interactions with main thread activity in development, how do you fix issues that cause bad INP scores? As usual, it depends. But here are some questions to ask yourself if you discover unexpected long-running tasks on your web pages that could affect your INP score:</p><ul><li><p class="post__p">Are you providing timely visual feedback? Does the next paint happen within 200ms?</p></li><li><p class="post__p">Is something running when you didn’t expect it to? Can you remove that process?</p></li><li><p class="post__p">Do you really need to process each user interaction individually, or can you restrict function calls via debouncing?</p></li><li><p class="post__p">Could you extract the resulting function calls from the main thread and move them to a <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers" target="_blank">web worker</a>?</p></li></ul><p class="post__p">There are also some more experimental ways to improve the perceived responsiveness of your pages, but proceed with caution because they may not be supported on all browsers.</p><h3 class="post__h3">navigator.scheduling.isInputPending()</h3><p class="post__p"><b class="post__p--bold">With support for Chromium browsers only</b>, isInputPending() <a href="https://developer.mozilla.org/en-US/docs/Web/API/Scheduling/isInputPending" target="_blank">allows you to check whether there are pending input events in the event queue, indicating that the user is attempting to interact with the page</a>.</p><p class="post__p">This could be useful in situations where you have a queue of tasks to run (such as calls to an API on each keypress), and you want to give control back to the main thread regularly to allow the page to respond to further user interaction. At the time of writing, there are no published plans to make this API available in any other browser engines.</p><h3 class="post__h3">scheduler.yield</h3><p class="post__p">Another experimental feature from the Chrome Team is scheduler.yield, which <a href="https://developer.chrome.com/blog/introducing-scheduler-yield-origin-trial" target="_blank">expands the scheduler API to make yielding control back to the main thread “easier and better”</a> than previously used methods such as <a href="https://web.dev/articles/optimize-long-tasks#manually_defer_code_execution" target="_blank">manually deferring code execution with setTimeout()</a>.</p><p class="post__p">Both new APIs offer some good reading opportunities to understand how JavaScript in the browser actually works, but ultimately, without support for all browsers, it’s not something you should rush to adopt just yet.</p><h2 class="post__h2">A cross-browser JavaScript task scheduler</h2><p class="post__p">During my research for this post, I discovered <a href="https://github.com/astoilkov/main-thread-scheduling" target="_blank">main-thread-scheduling</a>, a JavaScript task scheduler developed by Antonio Stoilkov that focuses on helping you improve perceived page performance, and therefore, your INP scores. It uses isInputPending() if available, but provides scheduling functionality for all browsers. Personally, I haven’t had a use case to test this just yet, but at first glance, it’s currently maintained and could be worth a try.</p><h2 class="post__h2">The bottom line</h2><p class="post__p">By introducing the new INP Core Web Vital to replace FID, developers are encouraged to prioritize minimizing delays in visual feedback across the entire user journey. This can reassure users that a page is responding to their actions, which, in theory, should reduce friction and bounce rates, improving the overall web experience.</p><p class="post__p">The bottom line? As developers building for the web, even if we don’t always have complete control of how long it takes to fetch data and make calls to API services on the front-end, in order to achieve a good INP score, we need to make our pages <b class="post__p--italic">feel fast, all the time</b>.
</p><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>My blog post workflow</title>
          <description>Tools, apps, workflow, brain stuff.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/my-blog-post-workflow/</link>
          <guid>https://whitep4nth3r.com/blog/my-blog-post-workflow/</guid>
          <pubDate>Mon, 11 Mar 2024 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Cliché opener incoming.</p><p class="post__p">A lot of people have asked me about my writing workflow. So here it is, including all the tools, apps and other things I use that help me produce written content. Contrary to what seems to be the norm right now, I do <b class="post__p--bold">not</b> use any AI tools in my workflow. I like writing directly from my brain.</p><h2 class="post__h2">0. Select topic</h2><p class="post__p">I manage my non-work and work-adjacent tasks in <a href="https://www.notion.so/" target="_blank">Notion</a>. Whenever I have an idea, regardless of how big or small or silly or achievable it is, I&#39;ll add it to Notion, and use labels to categorise it by type of output (e.g. blog, silly project, website update). Today I wanted to write a short post for my site. I clicked on the filtered blog post view, and selected this one (because I hoped it would be a quick one!).</p><img src="https://images.ctfassets.net/56dzm01z6lln/5DLK5wXrs0TxB9Yjl6OVjY/3a427b078c79ceae56c7f194cab0089a/notion_blog_posts.png" alt="A list of blog posts in Notion, in the column view. There are three visible posts in the not started column, an empty in progress column, and two posts in the done column." height="1139" width="1746" /><h2 class="post__h2">1. Draft in Notion</h2><p class="post__p">I write a full draft in Notion which includes collecting images, screenshots and code samples where required. Depending on the type of post, this could take some time; I might need to build an example project or do some further in-depth research. This post, though, was a case of just writing things I already knew.</p><p class="post__p">Compared to writing directly in my CMS, using Notion makes it easier to add and edit content when the inspiration hits using mobile devices. It&#39;s also easier to do the next step.</p><h2 class="post__h2">2. Use a text-to-speech reader to make it make sense</h2><p class="post__p">This part is really important to me as it helps me keep my writing clear and straightforward. Currently I&#39;m using the <a href="https://chromewebstore.google.com/detail/read-aloud-a-text-to-spee/hdhinadidafjejdhmfkjgnolgimiaplp" target="_blank">Read Aloud Chromium browser extension</a> to read out my drafts to assess for spelling, grammar, and comprehension. Doing this also helps me separate myself from the words from my brain, and experience the post as a reader rather than a writer. I&#39;ve tried a number of text-to-speech tools; some are better than others. Read Aloud isn&#39;t the best, but it&#39;s free, and works <b class="post__p--italic">most of the time</b>.</p><h2 class="post__h2">3. Add content to CMS</h2><p class="post__p">Next, I’ll copy and paste the draft text to my CMS. I’ve been using <a href="https://www.contentful.com/" target="_blank">Contentful</a> since working there in 2021. I use Rich Text rather than Markdown for my posts and what’s great about this is that copying and pasting from Notion preserves hyperlinks and formatting. If I’m including anything else like code samples, images and other embedded media, I add those as separate linked entries manually whilst working through the blog post.</p><h2 class="post__h2">4. Preview the post in the wild</h2><p class="post__p">In 2022 I built a custom Contentful app to build draft content to a preview branch of my static site. The app adds a button to my Contentful UI that when clicked, POSTs to a webhook that builds a specific branch of my site. Read about how I built it here: <a href="https://whitep4nth3r.com/blog/previewing-posts-best-decoupled-content-management-workflow-for-your-static-site/">Build a CMS preview workflow for your Jamstack site</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/jQ0agFvrNnG9PlDjJJT9x/aeebe3b632a6c66f9df00f1cc26691bd/app_in_situ.png" alt="A screenshot of the Contentful entry editor, showing the Netlify preview app in the sidebar which comprises two buttons. The top button is labelled Build Netlify Preview and the bottom button is labeled Open Netlify." height="633" width="1273" /><h2 class="post__h2">5. Edit if necessary</h2><p class="post__p">Previewing content “in situ” helps me find potential formatting issues such as incorrect heading levels or weird spacing in code samples, and also encourages me, again, to experience the content as a reader, rather than a writer. At this point, I may make some slight edits to the wording. I’ll usually rebuild the preview a few times until I’m completely happy.</p><h2 class="post__h2">6. Publish</h2><p class="post__p">Then I hit the publish button.</p><h2 class="post__h2">7. Cross-post manually to DEV</h2><p class="post__p">I publish each post on my website to <a href="https://dev.to/whitep4nth3r" target="_blank">DEV</a> and reference the original content with a canonical link. This step is a little painful because DEV posts are written in markdown, and as mentioned above, my posts are not. For a time I experimented with auto-importing blog posts to DEV via my RSS feed, but there are a few posts in my feed that I don’t want to post on DEV, and there was no option to hide them. This meant that I had old posts hanging around in my DEV drafts, and it felt messy.</p><p class="post__p">If I <b class="post__p--italic">really</b> wanted, I could automate part of this step using a package that converts Contentful Rich Text to markdown. But doing this final step manually allows me to check for spelling, grammar and comprehension a third time. And yes, sometimes I’ll make edits to the post at this point and republish the changes on my site. I’m more of a perfectionist than I’d like to admit.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>The future of Jamstack is anti-capitalist</title>
          <description>Down with capitalism. - Mike Neumegen</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/the-future-of-jamstack-is-anti-capitalist/</link>
          <guid>https://whitep4nth3r.com/blog/the-future-of-jamstack-is-anti-capitalist/</guid>
          <pubDate>Wed, 21 Feb 2024 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">This week I took part in a live panel discussion about what’s next for Jamstack. Hosted by <a href="https://mikeneumegen.com/" target="_blank">Mike Neumegen</a> and joined by <a href="https://cassidoo.co/" target="_blank">Cassidy Williams</a>, <a href="https://bryanlrobinson.com/" target="_blank">Bryan Robinson</a>, and <a href="https://www.zachleat.com/" target="_blank">Zach Leatherman</a>, we discussed what Jamstack means for web development today, its opportunities and challenges, and what that means for the community. Honestly, the conversation was <b class="post__p--italic">invigorating</b> for me. At last, I feel like we’re getting something back that I truly forgot we lost.</p><h2 class="post__h2">Jamstack in 2022</h2><p class="post__p">In 2022 I wrote <a href="https://whitep4nth3r.com/blog/what-is-jamstack/" target="_blank">What is Jamstack?</a>, mainly as a way to refocus a very broad landscape influenced by Big Tech™️ shifting and changing the definition of the technology to suit its capitalistic needs. If you’re unfamiliar with the term, I would urge you to read that post before continuing with this one for some historical context.</p><p class="post__p">The TL;DR of the 2022 post is this.</p><p class="post__p"><b class="post__p--bold">Jamstack projects have the following characteristics:</b></p><ul><li><p class="post__p"><b class="post__p--bold">Ready-to-serve assets</b></p></li><li><p class="post__p"><b class="post__p--bold">Any interactivity provided without a managed server</b></p></li><li><p class="post__p"><b class="post__p--bold">Any interactivity provided by some combination of JavaScript and decoupled services via APIs</b></p></li></ul><p class="post__p">Full disclaimer: I was an employee of Netlify at the time. And what’s interesting is that whilst my definition aligns mostly with the conclusions of this week’s panel discussion — the introduction of the phrase “decoupled services” was highly influenced by Netlify’s evolving business model as it started to pivot its marketing efforts towards larger enterprise businesses with more money to spend. I feel like I should apologise for this blatant insertion of business jargon, but I guess I was just doing my job. Let’s fast forward to 2023.</p><h2 class="post__h2">Jamstack as we knew it died in July 2023</h2><p class="post__p"><a href="https://www.netlify.com/blog/the-jamstack-definition-evolved/" target="_blank">Netlify published a new definition of the Jamstack in 2023</a>, introducing yet more Big Business™️ words to describe it: <b class="post__p--italic">“flexibility, scalability, performance, and maintainability”;</b> <b class="post__p--italic">“Composable architecture”.</b> Whilst Jamstack started out as a developer community centred on building pre-rendered and portable sites, it was now a marketing term pointed directly at CTOs and technical buyers re-positioned to sell Netlify services.</p><p class="post__p">This message was broadcast loud and clear when “Netlify officially killed The Jamstack Community Discord”, as described by Brian Rinaldi in <a href="https://remotesynthesis.com/blog/goodbye-jamstack/" target="_blank">Is Jamstack officially finished?</a> Not many of you will know this, but as an employee of Netlify at the time, I posted that announcement in the Jamstack Discord; after all, I was <b class="post__p--italic">still</b> just doing my job.</p><p class="post__p">In the panel, Cassidy summed all of this up perfectly:</p><blockquote class="post__blockquote"><p class="post__p">If I might just shout at capitalism. I think we pulled in a whole lot of things [into the Jamstack definition] saying, “Yes that counts, yes that counts,” for the shareholders, because if it counts then that means it can be hosted on this thing.</p></blockquote><h2 class="post__h2">Jamstack is actually, like, really really alive</h2><p class="post__p">Historically when I’ve taken part in live-streamed panels, attendance and engagement with the speakers is usually low. Surprisingly, or maybe not-so, this week’s live panel was buzzing with discussion, activity and engagement. There’s a huge group of people who believe in Jamstack and want to be part of it — again.</p><p class="post__p">On a personal level, the whole experience made me realise that for the last year I’ve been lacking something deeply: a technical community home on the internet where people like to build websites like I do; a group of people who share my excitement for building portable, static websites; somewhere I don’t feel like such a nerdy outsider because I’m not religiously promoting the latest experimental breaking Next.js change or encouraging everyone to server-side render their whole entire portfolio of purely content-based landing pages for their new AI startups.</p><p class="post__p">In a 2023 post titled <a href="https://www.zachleat.com/web/jamstack-future/" target="_blank">The Tension and Future of Jamstack</a>, Zach Leatherman lays out a bunch of evidence that “Netlify has moved on from shepherding the Jamstack […] — which is fine! (and maybe preferable?).” And I agree. Whilst it may be a little trickier to fund Jamstack community endeavours without the support of a Silicon Valley tech startup, removing the capitalistic element from Jamstack is ultimately what needs to happen in order to preserve its integrity and secure its future.</p><blockquote class="post__blockquote"><p class="post__p">Down with capitalism. - Mike Neumegen</p></blockquote><img src="https://images.ctfassets.net/56dzm01z6lln/LfhJJSSStwCtQ5l7FEPti/55e65c9e0bf6ccdf160d016c167886fe/down_with_capitalism.jpg" alt="A screenshot from the live panel showing Mike write down with capitalism under the title values in a google slides presentation." height="1441" width="2553" /><h2 class="post__h2">Watch the full panel discussion</h2><p class="post__p">Until we find a new home on the internet for the Jamstack community to come together, we’re planning some more live events, and Mike has been building up a library of excellent discussions with prominent community members over on <a href="https://thefutureofjamstack.org/" target="_blank">thefutureofjamstack.org</a>.</p><p class="post__p">I’d like to extend a huge, huge thanks to Mike for organising the first “What’s next for Jamstack” panel and for giving us a platform to revive the Jamstack values, principals and community. I’m excited for what’s to come.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/Nn0kBdwGa78?si=bScMMElWQDFbrQUR"
        title="What's next for Jamstack | Panel discussion"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Ramblings about JavaScript scope, weird errors and frameworks</title>
          <description>I did learn the thing. But I forgot to remember the thing.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/ramblings-about-javascript-scope-weird-errors-frameworks/</link>
          <guid>https://whitep4nth3r.com/blog/ramblings-about-javascript-scope-weird-errors-frameworks/</guid>
          <pubDate>Tue, 20 Feb 2024 00:00:00 GMT</pubDate>
          <category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">My website had a silly bug in production for a year.</p><p class="post__p">It was quite inconsequential (I think), and I only found it when I (finally) added <a href="https://docs.sentry.io/platforms/javascript/" target="_blank">Sentry error monitoring</a> to the stack (yay!). But it got me thinking about some of the things we’re starting to take for granted in the web framework world — and how we might get tripped up as a result.</p><h2 class="post__h2">SyntaxError: Identifier &#39;button&#39; has already been declared</h2><p class="post__p">My website is built with Eleventy. I’m using the <a href="https://www.11ty.dev/docs/languages/javascript/" target="_blank">Eleventy JavaScript template language</a> and this is mainly because I moved over from Next.js and wanted to retain much of the React-based component patterns I was used to at the time.</p><p class="post__p">In my header component, which is an HTML file added as an “include” on my base layout using <a href="https://www.11ty.dev/docs/languages/liquid/" target="_blank">Liquid syntax</a>, there’s a block of inline JavaScript that powers the dark and light theme toggle, selecting the toggle button and assigning it to the variable name <code>button</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="tPZqyhPWcO"
      aria-describedby="tPZqyhPWcO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="tPZqyhPWcO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="tPZqyhPWcO" itemprop="text" content="%20const%20button%20%3D%20document.querySelector(%22%5Bdata-theme-toggle%5D%22)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> button <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-theme-toggle]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">On my blog index page, I import a separate JavaScript file that powers search functionality powered by Algolia. In this file, I manually create the “reset search” button for the search form, and assign it to the variable name — yes, you guessed it — <code>button</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="aWWBVEIlkX"
      aria-describedby="aWWBVEIlkX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="aWWBVEIlkX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="aWWBVEIlkX" itemprop="text" content="const%20button%20%3D%20document.createElement(%22button%22)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> button <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"button"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">On the blog index page is where this error triggered, given that both <code>button</code> variables were declared with the same name. But what’s interesting is that I didn’t see this error in the browser console in development because the variables were assigned in different scopes: the theme toggle button was in global scope, and the clear search button was in function scope. (<a href="https://developer.mozilla.org/en-US/docs/Glossary/Scope" target="_blank">Get the scoop on scopes on MDN</a>.)</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="elTFBLkyKN"
      aria-describedby="elTFBLkyKN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="elTFBLkyKN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="elTFBLkyKN" itemprop="text" content="%3C!--%20theme%20toggle%20button%20defined%20in%20global%20scope%20in%20header.html%20--%3E%0A%0A%3Cscript%3E%0A%09const%20button%20%3D%20document.querySelector(%22%5Bdata-theme-toggle%5D%22)%3B%0A%3C%2Fscript%3E">
      <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- theme toggle button defined in global scope in header.html --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br>	<span class="token keyword">const</span> button <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-theme-toggle]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="gxxvdAWTZp"
      aria-describedby="gxxvdAWTZp">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="gxxvdAWTZp">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="gxxvdAWTZp" itemprop="text" content="%2F%2F%20reset%20search%20button%20defined%20in%20function%20scope%20in%20app_search.js%0A%0Aconst%20renderSearchBox%20%3D%20(renderOptions%2C%20isFirstRender)%20%3D%3E%20%7B%0A%20%2F%2F%20...%0A%20%20if%20(isFirstRender)%20%7B%0A%20%20%20%20%2F%2F%20...%0A%20%20%20%20const%20button%20%3D%20document.createElement(%22button%22)%3B%0A%20%20%20%20button.textContent%20%3D%20%22Clear%22%3B%0A%20%20%20%20%2F%2F%20...%0A%20%20%7D%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// reset search button defined in function scope in app_search.js</span><br><br><span class="token keyword">const</span> <span class="token function-variable function">renderSearchBox</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">renderOptions<span class="token punctuation">,</span> isFirstRender</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br> <span class="token comment">// ...</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>isFirstRender<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// ...</span><br>    <span class="token keyword">const</span> button <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"button"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    button<span class="token punctuation">.</span>textContent <span class="token operator">=</span> <span class="token string">"Clear"</span><span class="token punctuation">;</span><br>    <span class="token comment">// ...</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Given that I saw no errors in the browser console in development or production pertaining to this issue, and given that the <code>button</code> variables were scoped differently, I inspected the issue further in Sentry to discover that what was actually picked up was a bit of a weird issue in development whilst the Eleventy dev server was reloading some changes, as demonstrated by the stack trace.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5i8fW8yWtXz33e2W3PKbyQ/af7bc6078e77d79ef15aa8660d75bb92/sentry_button_already_declared.png" alt="Sentry issue view showing Syntax Error, identifier button has already been declared. The stack trace shows minified files from the eleventy dev server." height="3902" width="5344" /><p class="post__p">But this <b class="post__p--italic">still</b> got me thinking. Whilst this definitely supports the “don’t use global scope” in JavaScript argument just in case you run into real issues like this, I think it highlights a different issue.</p><h2 class="post__h2">Framework patterns obfuscate the fundamentals</h2><p class="post__p">As developers using modern web frameworks, we’ve become accustomed to our JavaScript being scoped and encapsulated by default to the file in which we’re working. Maybe I’m just speaking for myself, but after years of working with libraries such as React, and meta-frameworks that use libraries such as React, I didn’t stop to consider at this point that the inline JavaScript I was writing in my small header component, in global scope, <b class="post__p--italic">could</b> have an impact on any other JavaScript included anywhere else.</p><p class="post__p">In 2022, I wrote a <a href="https://whitep4nth3r.com/blog/how-i-improved-website-performance/" target="_blank">blog post where I detailed my journey of refactoring my blog from Next.js to Eleventy</a>, and I said this:</p><blockquote class="post__blockquote"><p class="post__p">It took a little while to shift my thinking from Next.js to Eleventy. Next.js has a justifiably opinionated way of architecting a front end application — and that&#39;s fine — but in moving to Eleventy, it showed me I had perhaps become too reliant on the patterns of Next.js. In going back to web basics with Eleventy, and focussing on shipping plain HTML, CSS and JavaScript to the browser, I feel like I&#39;ve refreshed and reinvigorated my knowledge of how the web works natively.</p></blockquote><p class="post__p">So I did learn the thing. But I forgot to remember the thing. If you’ve read this far, why not go back in time and see what you forgot to remember you learned? I need to do it more often.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to deal with API rate limits</title>
          <description>API rate-limiting is a minefield. Why are there no standards?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/how-to-deal-with-api-rate-limits/</link>
          <guid>https://blog.sentry.io/how-to-deal-with-api-rate-limits/</guid>
          <pubDate>Thu, 25 Jan 2024 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">When I first had the idea for this post, I wanted to provide a collection of actionable ways to handle errors caused by API rate limits in your applications. But as it turns out, it’s not that straightforward (is it ever?). API rate limiting is a minefield, and at the time of writing, there are <b class="post__p--bold">no published standards</b> in terms of how to build and consume APIs that implement rate limiting. And while I will provide some code solutions in the second half of this post, I want to start by discussing why we need rate limits and highlight some of the inconsistencies you might find when dealing with rate-limited APIs.</p><h2 class="post__h2">Why do rate limits exist?</h2><p class="post__p">From the late 1990s to the early 2000s, the use of Software-as-a-Service (SaaS) tools was not mainstream. Authentication, content management, image storage, and optimization were painstakingly hand-crafted in-house. Frontend and backend weren’t separate entities or disciplines. The use of APIs as a middle layer between frontend and backend wasn’t common; database calls were made directly from page templates.</p><p class="post__p">When the need (and desire) for separation between the front and backend emerged, so did APIs as a middle layer. But these APIs were also built, scaled, and managed in-house while being hosted on physical servers on business premises. Development teams decided how to rate limit their own APIs if there was a need. Fast forward to the mid-2010s and the SaaS ecosystem is packed full of headless, serverless, cloud-based tools for anything and everything.</p><p class="post__p">And with SaaS APIs being publicly available to everyone, rate limiting was introduced as a traffic management strategy, on top of other low-level DDoS mitigation measures. It exists to maintain the stability of APIs and to prevent users (good or bad actors) from exhausting available system resources. Rate limiting is also part of a SaaS pricing model; pay a higher subscription fee and receive more generous limits.</p><p class="post__p">So, how do we deal with being rate-limited by the APIs we consume?</p><h2 class="post__h2">HTTP response status codes are varied and inconsistent</h2><p class="post__p">The standard HTTP response code to send with a rate-limited response is <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429" target="_blank">HTTP 429 Too Many Requests</a>. However, given that you might be rate-limited according to whether or not you are <b class="post__p--italic">authorized</b> to make a particular number of requests (perhaps your API pricing model has rate-limited you), you may receive a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403" target="_blank">HTTP 403 Forbidden</a>. I experienced this when I was doing some testing <a href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-unauthenticated-users" target="_blank">using the GitHub API as an unauthenticated user</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4a8Z0hy14aKhToAxCqUREI/6555ba7bcc940b2338babf7279a60487/403_in_GitHub.png" alt="A thrown exception shown in Sentry, highlighting an HTTP 403 status received from the GitHub API" height="3464" width="3780" /><p class="post__p">Whilst this is a valid use of a 403 HTTP, it suggests that, technically, there <b class="post__p--italic">could</b> be other valid HTTP status codes to return in a rate-limited API response other than 429. In this case, even <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418" target="_blank">418 I’m a teapot</a> could be a valid HTTP status code, given that “some websites use this response for requests they do not wish to handle.” In conclusion, when you’re working with rate-limited APIs and you want to evaluate the HTTP response status in code, you may need to do some testing to work out the full scope of HTTP responses you may receive in different cases. The same goes for HTTP response headers.</p><h2 class="post__h2">HTTP response headers could be any combination of key-value pairs</h2><p class="post__p">When consuming an API that implements rate limiting, you should receive a set of response headers with more information about the rate limit on each request, (whether or not your request has been allowed or denied) such as how many requests you have remaining in any given time period and when the number of requests is reset to the maximum.</p><p class="post__p">As stated in a draft proposal from the Internet Engineering Task Force, across the APIs I have consumed and tested for this post, I found that <a href="https://www.ietf.org/archive/id/draft-ietf-httpapi-ratelimit-headers-07.html#name-introduction-2:~:text=Currently%2C%20there%20is%20no%20standard%20way%20for%20servers%20to%20communicate%20quotas%20so%20that%20clients%20can%20throttle%20its%20requests%20to%20prevent%20errors." target="_blank">“there is no standard way for servers to communicate quotas so that clients can throttle its requests to prevent errors”</a>. For example:</p><div class="post__tableWrapper"><table class="post__table">
        <tbody><tr class="post__table__row"><th class="post__table__header">API</th><th class="post__table__header">Header key</th><th class="post__table__header">Header value</th></tr><tr class="post__table__row"><td class="post__table__cell"><a href="https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#exceeding-the-rate-limit" target="_blank">GitHub</a></td><td class="post__table__cell">X-RateLimit-Reset</td><td class="post__table__cell">epoch timestamp of expiration</td></tr><tr class="post__table__row"><td class="post__table__cell"><a href="https://docs.sentry.io/api/ratelimits/#headers" target="_blank">Sentry</a></td><td class="post__table__cell">X-Sentry-Rate-Limit-Reset</td><td class="post__table__cell">epoch timestamp of expiration</td></tr><tr class="post__table__row"><td class="post__table__cell"><a href="https://platform.openai.com/docs/guides/rate-limits/usage-tiers?context=tier-free" target="_blank">OpenAI</a></td><td class="post__table__cell">X-RateLimit-Reset-Requests</td><td class="post__table__cell">time period in minutes and seconds after which the limit is reset (e.g. 1m6s)</td></tr><tr class="post__table__row"><td class="post__table__cell"><a href="https://discord.com/developers/docs/topics/rate-limits#header-format" target="_blank">Discord</a></td><td class="post__table__cell">X-RateLimit-Reset</td><td class="post__table__cell">epoch timestamp of expiration</td></tr><tr class="post__table__row"><td class="post__table__cell">Discord</td><td class="post__table__cell">X-RateLimit-Reset-After</td><td class="post__table__cell">time duration in seconds</td></tr><tr class="post__table__row"><td class="post__table__cell">Discord</td><td class="post__table__cell">Retry-After</td><td class="post__table__cell">time duration in seconds if the limit has been exceeded</td></tr></tbody>
      </table></div><p class="post__p">With this many differences across just four APIs, and in the absence of standards around this, HTTP response headers relating to rate limiting could, theoretically, be <b class="post__p--italic">any combination of key-value pairs</b>.</p><h2 class="post__h2">What to do when you’re being rate-limited</h2><p class="post__p">When using APIs that implement rate limits, you can use a combination of the HTTP status code and HTTP response headers to determine how to handle responses to prevent unexpected errors. But should you always retry an API call? As usual, it depends, but it can be useful to consider:</p><ul><li><p class="post__p">Rate limit thresholds: Can you retry after one second, or do you need to wait for 60 minutes?</p></li><li><p class="post__p">Request urgency: Does one request need to be completed successfully before another can be made? If so, see point 1.</p></li><li><p class="post__p">Application type: Can you provide feedback to a user to ask them to try again in N minutes rather than providing a seemingly infinite loading spinner whilst the request is retried in the background?</p></li></ul><h3 class="post__h3">Use HTTP 429 with available response headers</h3><p class="post__p">Say (very hypothetically) you want to request information for 100 GitHub users on the client, and you don’t have access to a server to hide your authentication credentials. When using GitHub API in unauthenticated mode, you are limited to making 60 API calls per hour. In this scenario, you’d hit the rate limit immediately, and not be able to recover for 60 minutes. Realistically, you wouldn’t do something like this in a large-scale production app, but here’s how I decided to deal with this case of rate limiting in JavaScript.</p><p class="post__p">If we receive an HTTP status code of 429 or 403, grab the epoch time value from the <code>x-ratelimit-reset</code> header, and work out how long we need to wait. If that time is fewer than five seconds, we can retry after that many seconds (and to be honest, five seconds is probably still too long). Otherwise, we provide feedback about when to manually try again.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CTPzngjhUj"
      aria-describedby="CTPzngjhUj">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CTPzngjhUj">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="CTPzngjhUj" itemprop="text" content="async%20function%20getArbitraryUser()%20%7B%0A%20%20const%20response%20%3D%20await%20fetch(%22https%3A%2F%2Fapi.github.com%2Fusers%2Foctocat%22)%3B%0A%20%20return%20response%3B%0A%7D%0A%0A%2F%2F%20Thanks%20https%3A%2F%2Fflaviocopes.com%2Fawait-loop-javascript%2F%0Aconst%20wait%20%3D%20(ms)%20%3D%3E%20%7B%0A%20%20return%20new%20Promise((resolve)%20%3D%3E%20%7B%0A%20%20%20%20setTimeout(()%20%3D%3E%20resolve()%2C%20ms)%3B%0A%20%20%7D)%3B%0A%7D%3B%0A%0Aconst%20makeLotsOfRequests%20%3D%20async%20(action%2C%20n)%20%3D%3E%20%7B%0A%20%20for%20(let%20i%20%3D%201%3B%20i%20%3C%3D%20n%3B%20i%2B%2B)%20%7B%0A%20%20%20%20const%20result%20%3D%20await%20action()%3B%0A%0A%20%20%20%20%2F%2F%20for%20the%20unauthenticated%20API%2C%20we%20may%20receive%20429%20or%20403%0A%20%20%20%20if%20(result.status%20%3D%3D%3D%20429%20%7C%7C%20result.status%20%3D%3D%3D%20403)%20%7B%0A%20%20%20%20%20%20%2F%2F%20epoch%20time%20in%20seconds%0A%20%20%20%20%20%20const%20resetInSeconds%20%3D%20result.headers.get(%22x-ratelimit-reset%22)%3B%0A%0A%20%20%20%20%20%20if%20(resetInSeconds%20!%3D%3D%20null)%20%7B%0A%20%20%20%20%20%20%20%20const%20nowInSeconds%20%3D%20Math.round(new%20Date().valueOf()%20%2F%201000)%3B%0A%20%20%20%20%20%20%20%20const%20secondsToWait%20%3D%20resetInSeconds%20-%20nowInSeconds%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20Retry%20only%20if%20we%20need%20to%20wait%20fewer%20than%205%20seconds%0A%20%20%20%20%20%20%20%20%2F%2F%20we%20*could*%20be%20waiting%20for%20up%20to%2060%20minutes%20for%20the%20limit%20to%20reset%0A%20%20%20%20%20%20%20%20if%20(secondsToWait%20%3C%205)%20%7B%0A%20%20%20%20%20%20%20%20%20%20await%20wait(secondsToWait%20*%201000)%3B%0A%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20provide%20useful%20feedback%20to%20user%0A%20%20%20%20%20%20%20%20%20%20console.error(%0A%20%20%20%20%20%20%20%20%20%20%20%20%60HTTP%20%24%7Bresult.status%7D%3A%20Sorry%2C%20try%20again%20later%20in%20%24%7BMath.round(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20secondsToWait%20%2F%2060%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%7D%20mins.%60%2C%0A%20%20%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%3B%0A%0Aawait%20makeLotsOfRequests(getArbitraryUser%2C%20100)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getArbitraryUser</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">"https://api.github.com/users/octocat"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">return</span> response<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Thanks https://flaviocopes.com/await-loop-javascript/</span><br><span class="token keyword">const</span> <span class="token function-variable function">wait</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">ms</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token function">setTimeout</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">resolve</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> ms<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> <span class="token function-variable function">makeLotsOfRequests</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">action<span class="token punctuation">,</span> n</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span> i <span class="token operator">&lt;=</span> n<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">action</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// for the unauthenticated API, we may receive 429 or 403</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>result<span class="token punctuation">.</span>status <span class="token operator">===</span> <span class="token number">429</span> <span class="token operator">||</span> result<span class="token punctuation">.</span>status <span class="token operator">===</span> <span class="token number">403</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token comment">// epoch time in seconds</span><br>      <span class="token keyword">const</span> resetInSeconds <span class="token operator">=</span> result<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"x-ratelimit-reset"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>resetInSeconds <span class="token operator">!==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">const</span> nowInSeconds <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">valueOf</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">/</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">const</span> secondsToWait <span class="token operator">=</span> resetInSeconds <span class="token operator">-</span> nowInSeconds<span class="token punctuation">;</span><br><br>        <span class="token comment">// Retry only if we need to wait fewer than 5 seconds</span><br>        <span class="token comment">// we *could* be waiting for up to 60 minutes for the limit to reset</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>secondsToWait <span class="token operator">&lt;</span> <span class="token number">5</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          <span class="token keyword">await</span> <span class="token function">wait</span><span class="token punctuation">(</span>secondsToWait <span class="token operator">*</span> <span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>          <span class="token comment">// provide useful feedback to user</span><br>          console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><br>            <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">HTTP </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>result<span class="token punctuation">.</span>status<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">: Sorry, try again later in </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span><br>              secondsToWait <span class="token operator">/</span> <span class="token number">60</span><span class="token punctuation">,</span><br>            <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> mins.</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>          <span class="token punctuation">)</span><span class="token punctuation">;</span><br>          <span class="token keyword">break</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">await</span> <span class="token function">makeLotsOfRequests</span><span class="token punctuation">(</span>getArbitraryUser<span class="token punctuation">,</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Retry the request with exponential backoff using HTTP response statuses</h3><p class="post__p">In the example above, it’s unrealistic to check the response headers and wait for rate limits to reset. If N clients (from the same IP address) simultaneously wait for the limit to expire and subsequently retry at the same time, the limits would be hit again immediately. This is also known as the <a href="https://en.wikipedia.org/wiki/Thundering_herd_problem" target="_blank">thundering herd problem</a>.</p><p class="post__p">Instead of using rate limit headers, you can retry the request arbitrarily, increasing the wait time exponentially for each subsequent request. Here’s a code example in JavaScript. In the <code>doRetriesWithBackOff()</code> function, while the current retries are less than the maximum number of retries we have defined (in this case three) and we haven’t set the <code>retry</code> variable to false after getting a successful response, we continue to retry the request. For each subsequent request, we use the retries variable to generate a higher value for the <code>waitInMilliseconds</code> parameter. Given that we define the <code>retries</code> variable as 0 when the loop begins, the first <code>waitInMilliseconds</code> will also evaluate to 0.</p><p class="post__p">This example only handles receiving an HTTP 200 or 429. You could specify different behaviors depending on other HTTP status codes you expect, or you could send an error to <a href="https://sentry.io/welcome/" target="_blank">Sentry</a> to monitor the different types of responses your application is receiving to decide whether or not a case is worth implementing in the code. As with the example above, this is a somewhat arbitrary example that probably doesn’t cover all bases, but is intended to give you a starting point if you’re looking for this type of rate limit handling.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mLlLUQHTCP"
      aria-describedby="mLlLUQHTCP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mLlLUQHTCP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="mLlLUQHTCP" itemprop="text" content="const%20MAX_RETRIES%20%3D%203%3B%0A%0Afunction%20sleep(ms)%20%7B%0A%20%20return%20new%20Promise((resolve)%20%3D%3E%20setTimeout(resolve%2C%20ms))%3B%0A%7D%0A%0Aasync%20function%20doRetriesWithBackOff()%20%7B%0A%20%20let%20retries%20%3D%200%3B%0A%20%20let%20retry%20%3D%20true%3B%0A%0A%20%20do%20%7B%0A%20%20%20%20const%20waitInMilliseconds%20%3D%20(Math.pow(2%2C%20retries)%20-%201)%20*%20100%3B%0A%0A%20%20%20%20await%20sleep(waitInMilliseconds)%3B%0A%0A%20%20%20%20const%20response%20%3D%20await%20fetch(%22https%3A%2F%2Fapi-url.io%2Fget-me-something%22)%3B%0A%20%20%20%20switch%20(response.status)%20%7B%0A%20%20%20%20%20%20case%20200%3A%20%2F%2F%20Ok%2C%20successful%0A%20%20%20%20%20%20%20%20retry%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20console.log(%22successful%22)%3B%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20case%20429%3A%20%2F%2F%20Too%20Many%20Requests%0A%20%20%20%20%20%20%20%20console.log(%22retrying%22)%3B%0A%20%20%20%20%20%20%20%20retries%2B%2B%3B%0A%20%20%20%20%20%20%20%20retry%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%20%20default%3A%20%2F%2F%20Something%20unexpected%20happened%2C%20stop%20retrying%0A%20%20%20%20%20%20%20%20console.log(%22stopping%22)%3B%0A%20%20%20%20%20%20%20%20retry%20%3D%20false%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20You%20could%20send%20an%20error%20message%20to%20Sentry%20here%0A%20%20%20%20%20%20%20%20%20Sentry.captureException(%60%24%7Bresponse.status%7D%20received%20for%20API%20call%20to%20https%3A%2F%2Fapi-url.io%2Fget-me-something%60)%3B%0A%0A%20%20%20%20%20%20%20%20break%3B%0A%20%20%20%20%7D%0A%20%20%7D%20while%20(retry%20%26%26%20retries%20%3C%20MAX_RETRIES)%3B%0A%7D%0A%0A%2F%2F%20Arbitrary%20loop%20to%20call%20API%20500%20times%20for%20testing%20purposes%0Afor%20(let%20i%20%3D%200%3B%20i%20%3C%20500%3B%20i%2B%2B)%20%7B%0A%20%20await%20doRetriesWithBackOff()%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token constant">MAX_RETRIES</span> <span class="token operator">=</span> <span class="token number">3</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">sleep</span><span class="token punctuation">(</span><span class="token parameter">ms</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">setTimeout</span><span class="token punctuation">(</span>resolve<span class="token punctuation">,</span> ms<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">doRetriesWithBackOff</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> retries <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><br>  <span class="token keyword">let</span> retry <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">do</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> waitInMilliseconds <span class="token operator">=</span> <span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">pow</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> retries<span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">100</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">await</span> <span class="token function">sleep</span><span class="token punctuation">(</span>waitInMilliseconds<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">"https://api-url.io/get-me-something"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">switch</span> <span class="token punctuation">(</span>response<span class="token punctuation">.</span>status<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">case</span> <span class="token number">200</span><span class="token operator">:</span> <span class="token comment">// Ok, successful</span><br>        retry <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br>        console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"successful"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token keyword">break</span><span class="token punctuation">;</span><br>      <span class="token keyword">case</span> <span class="token number">429</span><span class="token operator">:</span> <span class="token comment">// Too Many Requests</span><br>        console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"retrying"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        retries<span class="token operator">++</span><span class="token punctuation">;</span><br>        retry <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span><br>        <span class="token keyword">break</span><span class="token punctuation">;</span><br>      <span class="token keyword">default</span><span class="token operator">:</span> <span class="token comment">// Something unexpected happened, stop retrying</span><br>        console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"stopping"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>        retry <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// You could send an error message to Sentry here</span><br>         Sentry<span class="token punctuation">.</span><span class="token function">captureException</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>response<span class="token punctuation">.</span>status<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> received for API call to https://api-url.io/get-me-something</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token keyword">break</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span> <span class="token keyword">while</span> <span class="token punctuation">(</span>retry <span class="token operator">&amp;&amp;</span> retries <span class="token operator">&lt;</span> <span class="token constant">MAX_RETRIES</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Arbitrary loop to call API 500 times for testing purposes</span><br><span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> <span class="token number">500</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">await</span> <span class="token function">doRetriesWithBackOff</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Don’t retry the rate-limited request</h3><p class="post__p">It might not always be appropriate to retry a rate-limited request (especially in the case of very hard rate limits, such as when using the unauthenticated GitHub API). In this case, you could reduce the overheads of function execution time and return a friendly message to rate-limited users — including the time they need to wait before manually retrying.</p><h2 class="post__h2">Being rate-limited is one of those “nice to have” problems</h2><p class="post__p">If your third-party API tools are rate-limiting your application, it means you have users. And depending on your pricing tier, it could mean you have lots of users. Congratulations! Now go and upgrade your SaaS tool plans.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Twitter ruined your link previews: here’s how Cloudinary can help</title>
          <description>Learn how to overlay text on your Open Graph images using the Cloudinary API and SDKs.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://cloudinary.com/blog/twitter-ruined-your-link-previews</link>
          <guid>https://cloudinary.com/blog/twitter-ruined-your-link-previews</guid>
          <pubDate>Wed, 20 Dec 2023 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In October 2023, Twitter rolled out a big and controversial update, which changed how links to the content you share are displayed on the timeline. Previously, link previews would come with a large or small image, and most importantly, the image was accompanied by the headline and description of the URL, giving crucial information to readers scrolling the feed. Now, link previews that include large images are stripped of this contextual data, which has been replaced by a tiny label of the top-level domain of the link, overlaid on the bottom left-hand corner of the image. </p><img src="https://images.ctfassets.net/56dzm01z6lln/6j4X6Du71SCTpf79lGbsIK/7496434108b0f0b950c2c3566b9eb631/image5.png" alt="Two tweets side by side showing the same link to a blog post. The before shows that the title and meta description are below the large image. The after shows there is no title and meta description displayed." height="1042" width="1526" /><p class="post__p">Today, links with large image previews don’t even look like links — and this is on purpose. Twitter doesn’t want you to click away from the platform anymore. Sneaky. Additionally, this change comes with some big barriers to accessibility.</p><p class="post__p">In <a href="https://twitter.com/MattEason/status/1709859258492231770" target="_blank">a tweet from October 5, 2023, designer and developer Matt Eason writes</a>: “The X/Twitter update to remove headlines from link previews has also completely broken their accessibility. The link/image can’t be tabbed to with the keyboard, and it’s been totally hidden from screen readers.” While the tech community on Twitter rushed to find quick fixes and temporary workarounds for this, others proved how this change could further exacerbate the sharing of misinformation, by posting links to articles that showed images of Elon Musk and other questionable public figures, accompanied by false, damaging, and highly NSFW headlines in the body of the tweet. </p><p class="post__p">While, unfortunately, this post can’t help with fixing the accessibility issues around link sharing on Twitter, it will help you add contextual information to your images that accompany your URLs on Twitter using Cloudinary. But first, let’s take a look at what powers those images.</p><h2 class="post__h2">The Open Graph Protocol</h2><p class="post__p">Created at Facebook in 2010, <a href="https://opengraphprotocol.org/" target="_blank">the Open Graph (OG) protocol</a> was designed to transform web links into visually rich content previews that looked like native Facebook content at the time. Today, you can use Open Graph meta tags in the <code>&lt;head&gt;</code> of an HTML page to expose information about web pages to social media platforms and other applications that unfurl URL metadata — such as Twitter. OG meta tags in the source code are identified by an attribute prefixed with <code>og</code>. The following code provides an image URL to be displayed when sharing links on platforms that support it:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="stsySFzOXj"
      aria-describedby="stsySFzOXj">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="stsySFzOXj">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="stsySFzOXj" itemprop="text" content="%3Cmeta%20property%3D%22og%3Aimage%22%20content%3D%22https%3A%2F%2Fexample.com%2Fimage.png%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/image.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Shortly after, <a href="https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/getting-started" target="_blank">Twitter rolled out its own custom implementation built on the OG protocol</a>, allowing you to configure the appearance of your web pages on Twitter specifically. On Twitter, you can choose to show a small square image by default, which appears next to the headline (page title) and description (meta description) of the page (and was unaffected by this change in October 2023), or a large landscape image, which used to be displayed above the headline and description.</p><p class="post__p">To instruct Twitter to show large full-width OG images when you share a link, as opposed to the default smaller image alongside the title and description, you can use this meta tag:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DlfkQoApUP"
      aria-describedby="DlfkQoApUP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DlfkQoApUP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="DlfkQoApUP" itemprop="text" content="%3Cmeta%20name%3D%22twitter%3Acard%22%20content%3D%22summary_large_image%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:card<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>summary_large_image<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <img src="https://images.ctfassets.net/56dzm01z6lln/5ffq5ZmpRgcYrxXePze9hf/4bfa1953674f3b5a35e3b304f39d8152/image3.png" alt="Two tweets side by side with link previews. On the left there is a default card showing a square image next to a title and meta description. On the right there is a summary large card link, which does not show title and meta description, just the open graph image." height="817" width="1504" /><p class="post__p">To fix a lot of this mess, we could all just revert to using the smaller default image in our link previews on Twitter. However, most people prefer to use larger images because they’re easier to notice, and more aligned with the original visual intentions of the Open Graph protocol created by Facebook in 2010. If you’d like to continue using large images for your URLs on Twitter, here’s how Cloudinary can help.</p><h2 class="post__h2">Use Cloudinary to add text to Open Graph images for Twitter</h2><p class="post__p">Before image transformation APIs, Open Graph images were created manually — for each blog post, website page, or anything else people wanted to share — uploaded to a CDN (or a folder in a repo!), and referenced by URL when they were needed. As you can imagine, this doesn’t scale, especially for businesses that are adding hundreds of new pages to their websites each week (think e-commerce). Now, with Cloudinary, you can generate dynamic Open Graph images on the fly, via programmatically constructed URLs, or your SDK of choice.</p><p class="post__p">The Cloudinary API offers huge flexibility in adding text to images, overlaying images on images, and further image transformations, but the quickest way to solve the problem Twitter created (if only for sighted people), is to use Cloudinary to add a page title to a base image. Here’s an Open Graph image for one of my blog posts, generated programmatically via the Cloudinary API.</p><img src="https://images.ctfassets.net/56dzm01z6lln/10b1C6LdJwDMpPWAV1G3IT/382a66e8f9de3fa1ff10f3aab7fe29e7/image1.png" alt="My open graph image for my website home page. It shows my name logo, and the title live streamer, software engineer and developer educator in white text over a pink and orange grungy background, with a faded headshot of me in the background." height="630" width="1200" /><p class="post__p">Let’s look at how to overlay text on an image uploaded to Cloudinary, using URL parameters. The following assumes you’ve <a href="https://cloudinary.com/users/register_free" target="_blank">signed up for a free Cloudinary account</a>, and you’re familiar with JavaScript.</p><h3 class="post__h3">Upload a base image to Cloudinary</h3><p class="post__p">Before writing any code, you’ll need a base image to work with. You might want to mock up what you want the finished product to look like in your image editing software of choice, to know how much space to leave on the design for text. A tactic I’ve used in the past is to find the shortest and longest page titles on my website, and work the design around those parameters (and I’ll also share a little bonus workaround to accommodate a variety of page title lengths).</p><p class="post__p">Log in to Cloudinary, click <b class="post__p--bold">Upload</b>, and add your base image to your Cloudinary Media Library.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1bMnrGTiGygJjStaPiLUEE/e1f31812a10d6a99f8b9eb09f30f6a3c/image2.png" alt="The Cloudinary media library dashboard, showing an upload button in the top right." height="902" width="1072" /><p class="post__p">Cloudinary will add an auto-generated suffix to your image name. Click the image to open it in the preview pane, and change the file name using the pencil icon, so that it’s easier to use the asset name in the code. Here’s my base image uploaded to Cloudinary, which includes my name, and a stylized image that matches my website branding.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1i4jf3DS71UzGp11uw9CaS/518cb0475b6a10d65859efdb622a9530/image4.png" alt="My base image uploaded to Cloudinary, showing my name logo overlaid on a pink and orange grungy background, featuring a semi-transparent headshot on the right." height="902" width="1072" /><p class="post__p">Now, let’s write some code!</p><h3 class="post__h3">The Cloudinary image URL: explained</h3><p class="post__p">Here’s the URL to the current Open Graph image for my website homepage, which overlays the text “Live streamer, software engineer and developer educator” onto my base image.</p><p class="post__p"><code>https://res.cloudinary.com/whitep4nth3r/image/upload/w_1200,h_630,c_fill,q_auto,f_auto/w_657,c_fit,g_south_west,co_rgb:ffffff,x_255,y_80,l_text:interblack.ttf_60:Live%20streamer%252C%20software%20engineer%20and%20developer%20educator/og_pink.png</code></p><p class="post__p">It looks overwhelming at first glance, but each part of the URL gives Cloudinary specific instructions on how to transform your image, like so:</p><p class="post__p"><code>https://res.cloudinary.com/{cloud_name}/image/upload/{image_transformations}/{text_parameters}:{title}/image_name.png</code></p><p class="post__p">Let’s take a look at each part of the URL, and how it works to overlay text on a base image.</p><h3 class="post__h3">The base URL and account slug</h3><p class="post__p">The base URL comprises the protocol <code>https://</code>, the CDN subdomain <code>res.cloudinary.com</code>, your cloud name (which is found and editable in Cloudinary account settings), followed by <code>/image/upload/</code>, indicating we’re accessing a resource of type <code>image</code> that is an <code>upload</code>. Putting that all together, we have:</p><p class="post__p"><code>https://res.cloudinary.com/{cloud_name}/image/upload/</code></p><h3 class="post__h3">Image transformations</h3><p class="post__p">The next part of the URL tells Cloudinary what to do with the base image. My parameters, provided as as comma separated string, are as follows:</p><ol><li><p class="post__p"><code>w_1200</code> tells Cloudinary to resize the width of the image to 1200px.</p></li><li><p class="post__p"><code>h_630</code> tells Cloudinary to resize the height of the image to 630px.</p></li><li><p class="post__p"><code>c_fill</code> <code>s</code>ets the crop parameter, denoted by <code>c_</code>, to <code>fill</code>.</p></li><li><p class="post__p"><code>q_auto</code> sets the quality parameter, denoted by <code>q_</code>, to <code>auto</code>.</p></li><li><p class="post__p"><code>f_auto </code>sets the requested file format, denoted by <code>f_</code>, to <code>auto</code>, which automatically generates (if needed) and <a href="https://cloudinary.com/documentation/transformation_reference#f_auto" target="_blank">delivers an asset in the most optimal format for the requesting browser</a> in order to minimize the file size.</p></li><li><p class="post__p">Indicate the end of the image transformations with a forward slash <code>/</code>.</p></li></ol><p class="post__p">While 1200×630 are the dimensions of my original base image, should I change my base image in the future and upload a different image size, using <code>w_1200</code> and <code>h_630</code> ensures that the text positioning will stay consistent without any further changes.</p><h3 class="post__h3">Text parameters</h3><p class="post__p">Next are the text parameters, provided as a comma-separated string. Note that these parameters can be in any order, but I’ve detailed them in the order they appear in the URL above.</p><ol><li><p class="post__p"><code>w_657</code> sets the width of the text container to 657px.</p></li><li><p class="post__p"><code>c_fit</code> tells Cloudinary to fit my text into the specified container and to wrap the text accordingly.</p></li><li><p class="post__p"><code>g_south_west</code> anchors my text container to the bottom left of the image boundary for positioning with X and Y coordinates.</p></li><li><p class="post__p"><code>co_rgb:ffffff</code> sets the text color to the hex code #ffffff (white).</p></li><li><p class="post__p"><code>x_255</code> moves the text container 255px along the X axis.</p></li><li><p class="post__p"><code>y_80</code> moves the text container 80px along the Y axis.</p></li><li><p class="post__p"><code>l_text:interblack.ttf_60</code> adds the text layer, where <code>l_</code>denotes “layer,” sets the font to <code>interblack.ttf</code>, which I uploaded to my Cloudinary Media Library, and sets the font size to 60.</p></li></ol><h3 class="post__h3">The text as an encoded string</h3><p class="post__p">Next, we’ll define the text we want to layer on the image, preceded by a colon. Given the text will be applied via the URL in this case, we’ll need to encode the string in code, so that “Live streamer, software engineer and developer education” becomes <code>Live%20streamer%252C%20software%20engineer%20and%20developer%20educator</code>.</p><h3 class="post__h3">Image name</h3><p class="post__p">The final part of the URL is the name of your image (that you edited to a more memorable name after you uploaded it). Make sure to include the file extension; in my example, it’s <code>og_pink.png</code>.</p><p class="post__p">Now let’s look at how to construct this URL in plain JavaScript, without any libraries or SDKs.</p><h2 class="post__h2">Generating the image URL programatically in code</h2><p class="post__p">Here’s how I construct the Cloudinary image URL using JavaScript. The <code>generateCloudinaryImageUrl t</code>akes a <code>title</code> parameter, which is a string. To make the code easier to read, I define two arrays: <code>imageTransformations </code>and <code>titleParameters</code>, which are joined by a comma. </p><p class="post__p">Above the <code>titleParameters</code> you’ll notice the bonus workaround to accommodate different page title lengths I mentioned earlier. Here, I count the number of words in the title using a Regular Expression. My default <code>titleSize</code> is 50. However, if I find 10 words or fewer, I set the titleSize to 60 to make it fill more of the image. Your implementation will depend on the font family you use, and the overall design of your Open Graph image, but it’s a good trick to ensure you account for a variety of title lengths in the code. Also note that I have switched out my custom font for a <a href="https://cloudinary.com/cookbook/add_a_text_to_an_image#:~:text=You%20can%20use%20any%20one,the%2Dfly%20in%20the%20cloud." target="_blank">Cloudinary-supported Google font</a> in the code.</p><p class="post__p">Finally, all parameters are combined with the necessary parts of the Cloudinary image URL described above, joined via a forward slash, and returned from the function.</p><p class="post__p">If you’d like to add extra information such as the author or the page description, it’s as straightforward as adding an extra text layer, and configuring the parameters accordingly.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="FxfdKpUCKu"
      aria-describedby="FxfdKpUCKu">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="FxfdKpUCKu">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="FxfdKpUCKu" itemprop="text" content="function%20generateCloudinaryImageUrl(title)%20%7B%0A%20%20%2F%2F%20configure%20image%20transformations%0A%20%20const%20imageTransformations%20%3D%20%5B%60w_1200%60%2C%20%60h_630%60%2C%20%22c_fill%22%2C%20%22q_auto%22%2C%20%22f_auto%22%5D.join(%22%2C%22)%3B%0A%0A%20%20%2F%2F%20if%2010%20words%20or%20fewer%2C%20increase%20font%20size%0A%20%20const%20wordCount%20%3D%20title.match(%2F(%5Cw%2B)%2Fg).length%3B%0A%20%20const%20titleSize%20%3D%20wordCount%20%3C%2010%20%3F%2060%20%3A%2050%3B%0A%0A%20%20const%20titleParameters%20%3D%20%5B%0A%20%20%20%20%60w_657%60%2C%0A%20%20%20%20%22c_fit%22%2C%0A%20%20%20%20%22g_south_west%22%2C%0A%20%20%20%20%22co_rgb%3Affffff%22%2C%0A%20%20%20%20%60x_255%60%2C%0A%20%20%20%20%60y_80%60%2C%0A%20%20%20%20%60l_text%3ARoboto_%24%7BtitleSize%7D%3A%24%7BencodeURIComponent(title)%7D%60%2C%0A%20%20%5D.join(%22%2C%22)%3B%0A%0A%20%20%2F%2F%20combine%20all%20URL%20parts%20to%20generate%20a%20Cloudinary%20URL%0A%20%20return%20%5B%0A%20%20%20%20%22https%3A%2F%2Fres.cloudinary.com%22%2C%0A%20%20%20%20%22whitep4nth3r%22%2C%0A%20%20%20%20%22image%22%2C%0A%20%20%20%20%22upload%22%2C%0A%20%20%20%20imageTransformations%2C%0A%20%20%20%20titleParameters%2C%0A%20%20%20%20%22og_pink.png%22%2C%0A%20%20%5D.join(%22%2F%22)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">generateCloudinaryImageUrl</span><span class="token punctuation">(</span><span class="token parameter">title</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// configure image transformations</span><br>  <span class="token keyword">const</span> imageTransformations <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">w_1200</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">h_630</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token string">"c_fill"</span><span class="token punctuation">,</span> <span class="token string">"q_auto"</span><span class="token punctuation">,</span> <span class="token string">"f_auto"</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">","</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// if 10 words or fewer, increase font size</span><br>  <span class="token keyword">const</span> wordCount <span class="token operator">=</span> title<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">(\w+)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>length<span class="token punctuation">;</span><br>  <span class="token keyword">const</span> titleSize <span class="token operator">=</span> wordCount <span class="token operator">&lt;</span> <span class="token number">10</span> <span class="token operator">?</span> <span class="token number">60</span> <span class="token operator">:</span> <span class="token number">50</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> titleParameters <span class="token operator">=</span> <span class="token punctuation">[</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">w_657</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token string">"c_fit"</span><span class="token punctuation">,</span><br>    <span class="token string">"g_south_west"</span><span class="token punctuation">,</span><br>    <span class="token string">"co_rgb:ffffff"</span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">x_255</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">y_80</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">l_text:Roboto_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>titleSize<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">encodeURIComponent</span><span class="token punctuation">(</span>title<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">","</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// combine all URL parts to generate a Cloudinary URL</span><br>  <span class="token keyword">return</span> <span class="token punctuation">[</span><br>    <span class="token string">"https://res.cloudinary.com"</span><span class="token punctuation">,</span><br>    <span class="token string">"whitep4nth3r"</span><span class="token punctuation">,</span><br>    <span class="token string">"image"</span><span class="token punctuation">,</span><br>    <span class="token string">"upload"</span><span class="token punctuation">,</span><br>    imageTransformations<span class="token punctuation">,</span><br>    titleParameters<span class="token punctuation">,</span><br>    <span class="token string">"og_pink.png"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">My website is primarily a static site, so my Open Graph image URLs are generated at build time, and added to the relevant meta tags in the head of my static HTML pages. Your implementation may vary, but ensure you add the following meta tag to the head of your website pages:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="PfRrSGnZeF"
      aria-describedby="PfRrSGnZeF">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="PfRrSGnZeF">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="PfRrSGnZeF" itemprop="text" content="%3Cmeta%20property%3D%22og%3Aimage%22%0Acontent%3D%22https%3A%2F%2Fyour_dynamically_generated_cloudinary_url%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span><br><span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://your_dynamically_generated_cloudinary_url<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">And that’s it!</p><h2 class="post__h2">Streamline your workflow with Cloudinary SDKs</h2><p class="post__p">Now that you’re familiar with adding text to images using Cloudinary URL parameters, you may want to level up your productivity by using one of the many <a href="https://cloudinary.com/documentation/cloudinary_sdks" target="_blank">Cloudinary SDKs</a> to achieve the same results in your front-end framework of choice, without needing to write your own bespoke transformation methods.</p><h3 class="post__h3">Transforming images with Cloudinary in NuxtJS</h3><p class="post__p">If you’re using Nuxt, Cloudinary provides a handy <a href="https://cloudinary.nuxtjs.org/components/cldogimage#cldogimagevue" target="_blank">Open Graph image component</a> that automatically generates and inserts the required Open Graph meta tags to the head of your pages.</p><p class="post__p">Follow the <a href="https://cloudinary.nuxtjs.org/getting-started/setup#installation" target="_blank">installation instructions for a Nuxt project</a> to install the dependency and add the module to the nuxt.config.ts file, and use the <code>&lt;CldOgImage&gt;</code> component to render Open Graph meta tags containing a transformed URL. Inspect the source code and notice that the generated HTML contains a meta tag for <code>twitter:card </code>set to <code>summary_large_image </code>to provide large Open Graph images for Twitter.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xzMGCBWETd"
      aria-describedby="xzMGCBWETd">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xzMGCBWETd">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="jsx">
      <meta data-code-id="xzMGCBWETd" itemprop="text" content="%3CCldOgImage%0A%20%20%20%20src%3D%22og_pink.png%22%0A%20%20%20%20height%3D%22630%22%0A%20%20%20%20width%3D%221200%22%0A%20%20%20%20%3Aoverlays%3D%22%5B%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20position%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20gravity%3A%20'south_west'%2C%0A%20%20%20%20%20%20%20%20%20%20y%3A%2080%2C%0A%20%20%20%20%20%20%20%20%20%20x%3A%20255%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20width%3A%20657%2C%0A%20%20%20%20%20%20%20%20crop%3A%20%20'fit'%2C%0A%20%20%20%20%20%20%20%20text%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20color%3A%20'%23ffffff'%2C%0A%20%20%20%20%20%20%20%20%20%20fontFamily%3A%20'Roboto'%2C%0A%20%20%20%20%20%20%20%20%20%20fontSize%3A%2060%2C%0A%20%20%20%20%20%20%20%20%20%20text%3A%20'Live%20streamer%2C%20software%20engineer%20and%20developer%20educator'%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%5D%22%0A%20%20%2F%3E">
      <pre class="language-jsx"><code class="language-jsx"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">CldOgImage</span></span><br>    <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og_pink.png<span class="token punctuation">"</span></span><br>    <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>630<span class="token punctuation">"</span></span><br>    <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1200<span class="token punctuation">"</span></span><br>    <span class="token attr-name">:overlays</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>[<br>      {<br>        position: {<br>          gravity: 'south_west',<br>          y: 80,<br>          x: 255,<br>        },<br>        width: 657,<br>        crop:  'fit',<br>        text: {<br>          color: '#ffffff',<br>          fontFamily: 'Roboto',<br>          fontSize: 60,<br>          text: 'Live streamer, software engineer and developer educator',<br>        },<br>      },<br>    ]<span class="token punctuation">"</span></span><br>  <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">With this library, you can also use a composable to construct the Cloudinary URL, should you want to use the image URL elsewhere. This gives the same result, so you don’t have to manage your own URL construction.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="SUvcNwuepI"
      aria-describedby="SUvcNwuepI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="SUvcNwuepI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="SUvcNwuepI" itemprop="text" content="%3Cscript%20lang%3D%22ts%22%20setup%3E%0Aconst%20%7B%20url%20%7D%20%3D%20useCldImageUrl(%7B%0A%20%20options%3A%20%7B%0A%20%20%20%20src%3A%20%22%2Fog_pink.png%22%2C%0A%20%20%20%20width%3A%201200%2C%0A%20%20%20%20height%3A%20630%2C%0A%20%20%20%20overlays%3A%20%5B%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20position%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20gravity%3A%20%22south_west%22%2C%0A%20%20%20%20%20%20%20%20%20%20x%3A%20255%2C%0A%20%20%20%20%20%20%20%20%20%20y%3A%2080%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20crop%3A%20%22fit%22%2C%0A%20%20%20%20%20%20%20%20width%3A%20657%2C%0A%20%20%20%20%20%20%20%20text%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20color%3A%20%22%23ffffff%22%2C%0A%20%20%20%20%20%20%20%20%20%20fontFamily%3A%20%22Roboto%22%2C%0A%20%20%20%20%20%20%20%20%20%20fontSize%3A%2060%2C%0A%20%20%20%20%20%20%20%20%20%20text%3A%20%22Live%20streamer%2C%20software%20engineer%20and%20developer%20educator%22%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%5D%2C%0A%20%20%7D%2C%0A%7D)%3B%0A%0Aconsole.log(url)%3B%0A%3C%2Fscript%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>ts<span class="token punctuation">"</span></span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br><span class="token keyword">const</span> <span class="token punctuation">{</span> url <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useCldImageUrl</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">options</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">src</span><span class="token operator">:</span> <span class="token string">"/og_pink.png"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">width</span><span class="token operator">:</span> <span class="token number">1200</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">height</span><span class="token operator">:</span> <span class="token number">630</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">overlays</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>      <span class="token punctuation">{</span><br>        <span class="token literal-property property">position</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token literal-property property">gravity</span><span class="token operator">:</span> <span class="token string">"south_west"</span><span class="token punctuation">,</span><br>          <span class="token literal-property property">x</span><span class="token operator">:</span> <span class="token number">255</span><span class="token punctuation">,</span><br>          <span class="token literal-property property">y</span><span class="token operator">:</span> <span class="token number">80</span><span class="token punctuation">,</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">crop</span><span class="token operator">:</span> <span class="token string">"fit"</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">width</span><span class="token operator">:</span> <span class="token number">657</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">text</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token literal-property property">color</span><span class="token operator">:</span> <span class="token string">"#ffffff"</span><span class="token punctuation">,</span><br>          <span class="token literal-property property">fontFamily</span><span class="token operator">:</span> <span class="token string">"Roboto"</span><span class="token punctuation">,</span><br>          <span class="token literal-property property">fontSize</span><span class="token operator">:</span> <span class="token number">60</span><span class="token punctuation">,</span><br>          <span class="token literal-property property">text</span><span class="token operator">:</span> <span class="token string">"Live streamer, software engineer and developer educator"</span><span class="token punctuation">,</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Transforming images with Cloudinary in Next.js</h3><p class="post__p">f you’re using Next.js, Cloudinary offers the same component functionality as Nuxt.  </p><p class="post__p">Follow the <a href="https://next.cloudinary.dev/installation#installation" target="_blank">installation instructions for a Next.js project</a> to install the dependency and use the <code>&lt;CldOgImage&gt;</code> component to render Open Graph meta tags containing a transformed URL in the same way.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="jCzyOvezUT"
      aria-describedby="jCzyOvezUT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="jCzyOvezUT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="jsx">
      <meta data-code-id="jCzyOvezUT" itemprop="text" content="import%20%7B%20CldOgImage%20%7D%20from%20'next-cloudinary'%3B%0A%0A%3CCldOgImage%0A%20%20src%3D%22og_pink.png%22%0A%20%20width%3D%221200%22%0A%20%20height%3D%22630%22%0A%20%20overlays%3D%7B%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20position%3A%20%7B%0A%20%20%20%20%20%20%20%20gravity%3A%20'south_west'%2C%0A%20%20%20%20%20%20%20%20y%3A%2080%2C%0A%20%20%20%20%20%20%20%20x%3A%20255%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20width%3A%20657%2C%0A%20%20%20%20%20%20crop%3A%20'fit'%2C%0A%20%20%20%20%20%20text%3A%20%7B%0A%20%20%20%20%20%20%20%20color%3A%20'%23ffffff'%2C%0A%20%20%20%20%20%20%20%20fontFamily%3A%20'Roboto'%2C%0A%20%20%20%20%20%20%20%20fontSize%3A%2060%2C%0A%20%20%20%20%20%20%20%20text%3A%20'Live%20streamer%2C%20software%20engineer%20and%20developer%20educator'%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%7D%2C%0A%20%20%5D%7D%0A%2F%3E%3B">
      <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> CldOgImage <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'next-cloudinary'</span><span class="token punctuation">;</span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span><span class="token class-name">CldOgImage</span></span><br>  <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og_pink.png<span class="token punctuation">"</span></span><br>  <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1200<span class="token punctuation">"</span></span><br>  <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>630<span class="token punctuation">"</span></span><br>  <span class="token attr-name">overlays</span><span class="token script language-javascript"><span class="token script-punctuation punctuation">=</span><span class="token punctuation">{</span><span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      <span class="token literal-property property">position</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">gravity</span><span class="token operator">:</span> <span class="token string">'south_west'</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">y</span><span class="token operator">:</span> <span class="token number">80</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">x</span><span class="token operator">:</span> <span class="token number">255</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">width</span><span class="token operator">:</span> <span class="token number">657</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">crop</span><span class="token operator">:</span> <span class="token string">'fit'</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">text</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">color</span><span class="token operator">:</span> <span class="token string">'#ffffff'</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">fontFamily</span><span class="token operator">:</span> <span class="token string">'Roboto'</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">fontSize</span><span class="token operator">:</span> <span class="token number">60</span><span class="token punctuation">,</span><br>        <span class="token literal-property property">text</span><span class="token operator">:</span> <span class="token string">'Live streamer, software engineer and developer educator'</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">}</span></span><br><span class="token punctuation">/></span></span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Image transformation in front-end frameworks and libraries</h3><p class="post__p">Cloudinary offers many more image transformation libraries, SDKs, and utilities for frameworks such as <a href="https://svelte.cloudinary.dev/" target="_blank">Svelte</a>, <a href="https://cloudinary.com/documentation/angular_integration" target="_blank">Angular</a>, and for the most popular JavaScript library on the web —<a href="https://cloudinary.com/documentation/jquery_integration" target="_blank"> jQuery</a>. Visit the <a href="https://cloudinary.com/documentation/cloudinary_sdks" target="_blank">official Cloudinary SDK Guide</a> to see the full list.</p><h2 class="post__h2">What else can you do?</h2><p class="post__p">Generating dynamic Open Graph images for sharing links on social sharing is only touching the surface of what Cloudinary can do with your images and media. Programmable image transformations remove so much of the manual image editing work that’s often required to create responsive and adaptive page designs, such as cropping, rounding corners, overlaying text and images such as company logos, and so much more, so you can focus on writing code and shipping features without waiting for bespoke design assets over and over again. Cloudinary also offers video transformation APIs, allowing you to crop videos, overlay images and videos on videos — all without video editing software. And this blows my mind.</p><p class="post__p">Although we can’t fix Twitter accessibility, we can be more intentional about how we share links. If you do want to continue using large Open Graph images on Twitter, even if you’re overlaying text on your images, I’d recommend providing the page title and a short description in your tweets, to ensure that people using screen readers and assistive technology can access the information, and understand what they’re clicking before they visit the link.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to fix error: invalid character in entity name in RSS feeds</title>
          <description>My RSS feed was broken for OVER A WEEK because I used an invalid character in the XML.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-fix-error-invalid-character-in-entity-name-rss-feeds/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-fix-error-invalid-character-in-entity-name-rss-feeds/</guid>
          <pubDate>Mon, 18 Dec 2023 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I use a GitHub action to <a href="https://whitep4nth3r.com/blog/how-to-github-actions-contentful-webhooks-to-show-latest-blog-posts-readme/" target="_blank">update my GitHub profile README with my latest blog posts</a>. For over a week the job had been failing, and I ignored it (rookie mistake). It turns out I had broken my RSS feed when I edited some blog post descriptions and used a character that wasn&#39;t valid in XML (referred to as an <b class="post__p--italic">invalid token</b>). </p><p class="post__p">The error in the GitHub action workflow run wasn&#39;t very helpful, though: &quot;Error: Invalid character in entity name&quot;. 🫠</p><img src="https://images.ctfassets.net/56dzm01z6lln/4hdrZE4RzYxgdhyFXqCdta/6d15d468b992c80e5b614c38f7192b37/github_action_workflow_fail.png" alt="Error feed.xml runner failed, please verify the configuration. Error: invalid character in entity name. Line 1579. Column 152. Char not specified. 0 blog posts fetched." height="247" width="613" /><p class="post__p">Luckily, the <a href="https://validator.w3.org/feed/" target="_blank">RSS Feed Validation Service</a> provided by W3C gave a better error report.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6Yqk5F6CKv6Xs9tlMszGB4/e0eaa2c19f2577dbec3a2e0abb6bd506/xml_invalid.png" alt="Sorry. This feed does not validate. Line 1597, column 151: XML parsing error, not well-formed (invalid token). The line that broke it was: information to screen-readers & (ampersand) assistive tech in the description tag." height="384" width="753" /><p class="post__p">This helped me track down the invalid character to an <b class="post__p--bold">ampersand (&amp;)</b>, which I added to a blog post description a week ago to reduce the character count. The resulting invalid description tag in the XML document looked like this:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="oadgDJeytm"
      aria-describedby="oadgDJeytm">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="oadgDJeytm">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="xml">
      <meta data-code-id="oadgDJeytm" itemprop="text" content="%3Citem%3E%0A%20%20%3Ctitle%3EWhen%20to%20use%20aria-labels%20in%20your%20HTML%3C%2Ftitle%3E%0A%20%20%3Cdescription%3EThis%20is%20one%20of%20the%20most%20important%20ways%20to%20use%20aria-labels%20so%20your%20code%20provides%20contextual%20information%20to%20screen-readers%20%26%20assistive%20tech.%0A%20%20%3C%2Fdescription%3E%0A%3C%2Fitem%3E">
      <pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>item</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>When to use aria-labels in your HTML<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>description</span><span class="token punctuation">></span></span>This is one of the most important ways to use aria-labels so your code provides contextual information to screen-readers &amp; assistive tech.<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>description</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>item</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">I did some further reading on XML and learned that ampersands, and left and right angled brackets (&lt; and &gt;) must be escaped in XML in order for the document to be valid, unless wrapped in a CDATA section. <a href="https://www.w3.org/TR/xml/#charsets" target="_blank">Read more about XML character data and markup on the official W3C docs</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Fallbacks for HTTP 404 images in HTML and JavaScript</title>
          <description>What if an image doesn’t exist anymore? What if someone accidentally deleted an image in your CMS? How do you detect and deal with this?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/fallbacks-for-http-404-images-in-html-and-javascript/</link>
          <guid>https://blog.sentry.io/fallbacks-for-http-404-images-in-html-and-javascript/</guid>
          <pubDate>Thu, 14 Dec 2023 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Your images are 404ing all over the place. You’ve got an angry email from a client. Their site is “broken”, images aren’t loading, cumulative layout shift is running riot, and everything is messed up. The crowds are mocking your broken code on Twitter. A fun GIF loaded via a Giphy URL no longer exists. And someone has accidentally deleted an image from the CMS.</p><p class="post__p">Now, whilst you can’t control third-party URLs or user errors in a CMS, you can prevent all of this from happening by providing fallbacks for image 404s in three different ways. Let’s take a look.</p><h2 class="post__h2">Image fallbacks in HTML</h2><p class="post__p">The first way is the HTML-only way: no JavaScript required. The <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object" target="_blank">HTML &lt;object&gt; element</a> is often overlooked amidst the fancy framework-based image components available in the frontend ecosystem today and is used to represent an external resource, which can be an image, a video, or a web page. Historically, it was used to embed and run plugins on a webpage to display Flash movies, run ActiveX controls, Java applets, and other kinds of old-school web things. Today, it’s a pretty perfect use case for handling third-party image URLs that you have no control over. Plus, it has full browser support.</p><p class="post__p">To use the HTML <code>&lt;object&gt;</code> element to provide a fallback image, assign the data attribute of the <code>&lt;object&gt;</code> element to the desired resource URL, and provide an HTML <code>&lt;img&gt;</code> element as a child of the <code>&lt;object&gt;</code>, pointing to the URL of a suitable fallback image. For accessibility reasons, provide an <code>aria-label</code> attribute on the <code>&lt;object&gt;</code> to describe the desired image (in place of an <code>alt</code> attribute). Additionally, ensure you specify the width and height of the image on both element tags to avoid cumulative layout shift as the page loads the resources and builds the page.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="QtrKtANore"
      aria-describedby="QtrKtANore">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="QtrKtANore">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="QtrKtANore" itemprop="text" content="%3Cobject%0A%20%20type%3D%22image%2Fpng%22%0A%20%20data%3D%22https%3A%2F%2Fsomedomain.com%2Fimage.png%22%0A%20%20width%3D%22150%22%0A%20%20height%3D%22150%22%0A%20%20aria-label%3D%22This%20image%20should%20exist%2C%20but%20alas%20it%20does%20not%22%0A%3E%0A%20%20%3Cimg%20src%3D%22%2Fpath%2Fto%2Ffallback.png%22%20alt%3D%22Fallback%20image%22%20width%3D%22150%22%20height%3D%22150%22%20%2F%3E%0A%3C%2Fobject%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>object</span><br>  <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/png<span class="token punctuation">"</span></span><br>  <span class="token attr-name">data</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://somedomain.com/image.png<span class="token punctuation">"</span></span><br>  <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>150<span class="token punctuation">"</span></span><br>  <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>150<span class="token punctuation">"</span></span><br>  <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>This image should exist, but alas it does not<span class="token punctuation">"</span></span><br><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/path/to/fallback.png<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Fallback image<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>150<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>150<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>object</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Image fallbacks using JavaScript</h2><p class="post__p">Another way to provide an image fallback is to hook into the <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/error_event" target="_blank">HTMLElement onerror event</a>. This fires on an element when a resource fails to load, or can’t be used. You can specify a new image source in the event of an error using the <code>onerror</code> attribute directly on the HTML <code>&lt;img&gt;</code> element.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CPTAgrPMWn"
      aria-describedby="CPTAgrPMWn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CPTAgrPMWn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="CPTAgrPMWn" itemprop="text" content="%3Cimg%0A%20%20src%3D%22https%3A%2F%2Fsomedomain.com%2Fimage.png%22%0A%20%20alt%3D%22This%20image%20should%20exist%2C%20but%20alas%20it%20does%20not%22%0A%20%20width%3D%22150%22%0A%20%20height%3D%22150%22%0A%20%20onerror%3D%22this.src%3D'%2Fpath%2Fto%2Ffallback.png'%22%0A%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span><br>  <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://somedomain.com/image.png<span class="token punctuation">"</span></span><br>  <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>This image should exist, but alas it does not<span class="token punctuation">"</span></span><br>  <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>150<span class="token punctuation">"</span></span><br>  <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>150<span class="token punctuation">"</span></span><br>  <span class="token special-attr"><span class="token attr-name">onerror</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value javascript language-javascript"><span class="token keyword">this</span><span class="token punctuation">.</span>src<span class="token operator">=</span><span class="token string">'/path/to/fallback.png'</span></span><span class="token punctuation">"</span></span></span><br><span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">If you’re unable to edit the HTML directly (say if you’re using a framework-based image component), you can use plain JavaScript (or your flavor of choice) to listen for the <code>onerror</code> event for all rendered HTML <code>&lt;img&gt;</code> elements, like so.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vFYHzLwIdi"
      aria-describedby="vFYHzLwIdi">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vFYHzLwIdi">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="vFYHzLwIdi" itemprop="text" content="const%20images%20%3D%20document.querySelectorAll(%22img%22)%3B%0A%0Aimages.forEach((image)%20%3D%3E%20%7B%0A%20%20image.addEventListener(%22error%22%2C%20(event)%20%3D%3E%20%7B%0A%20%20%20%20image.src%20%3D%20%22%2Fpath%2Fto%2Ffallback.png%22%3B%0A%20%20%7D)%3B%0A%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> images <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">"img"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>images<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">image</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  image<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"error"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    image<span class="token punctuation">.</span>src <span class="token operator">=</span> <span class="token string">"/path/to/fallback.png"</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Providing image fallback URLs from the server</h2><p class="post__p">If you’re requesting images from a backend managed by you or your team, check the HTTP status code of the image resource before attempting to return the URL via the API. If image URLs return a 404, you can send a URL to a fallback image in the response instead, reducing the need for extra logic and workarounds on the client.</p><p class="post__p">This method will vary depending on the programming language and framework, but there are some drawbacks to this method. If a request needs to return hundreds of image URLs (for example on e-commerce search pages), and you need to check the HTTP response code for each image resource before returning the response, this method might negatively impact your API response times. Lazar goes into detail on this in his latest post: <a href="https://blog.sentry.io/whats-the-difference-between-api-latency-and-api-response-time/" target="_blank">What’s the difference between API Latency and API Response Time?</a></p><h2 class="post__h2">Know when images are broken</h2><p class="post__p">Whilst you can code around broken images in your frontend applications to provide a better visual end-user experience, it’s also useful to know which images are failing to load so you can switch out broken URLs for working ones.</p><p class="post__p"><a href="https://sentry.io/welcome/" target="_blank">Sentry</a> doesn’t provide this functionality out of the box, but if it’s a growing concern in your frontend web applications, you can add this small snippet of JavaScript to your frontend to create an issue in Sentry if an image fails to load. This listens to all error events that may occur in the HTML document, and if the error occurs on an <code>&lt;img&gt;</code> element, it captures a message in Sentry with the source URL of the element that failed to load.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="gHpTALYZTU"
      aria-describedby="gHpTALYZTU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="gHpTALYZTU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="gHpTALYZTU" itemprop="text" content="document.body.addEventListener(%0A%20%20%22error%22%2C%0A%20%20(event)%20%3D%3E%20%7B%0A%20%20%20%20if%20(!event.target)%20return%3B%0A%20%20%20%20if%20(event.target.tagName%20%3D%3D%3D%20%22IMG%22)%20%7B%0A%20%20%20%20%20%20%20Sentry.captureException(%60Failed%20to%20load%20image%3A%20%24%7Bevent.target.src%7D%60%2C%20%22warning%22)%3B%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%20%20true%2C%0A)%3B">
      <pre class="language-javascript"><code class="language-javascript">document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><br>  <span class="token string">"error"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>event<span class="token punctuation">.</span>target<span class="token punctuation">)</span> <span class="token keyword">return</span><span class="token punctuation">;</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>target<span class="token punctuation">.</span>tagName <span class="token operator">===</span> <span class="token string">"IMG"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>       Sentry<span class="token punctuation">.</span><span class="token function">captureException</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Failed to load image: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>event<span class="token punctuation">.</span>target<span class="token punctuation">.</span>src<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token string">"warning"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token boolean">true</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">With this code snippet on your pages, you’ll see an issue created in Sentry when images 404. Additionally, you can do this for other resources, such as CSS files. <a href="https://docs.sentry.io/platforms/javascript/troubleshooting/#capturing-resource-404s" target="_blank">Read about capturing resource 404s on the Sentry docs</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2ZehQhVVrhSdRVxmmFkGeu/ee73b80b882c5d341ccbf1c654536a20/image_load_failure_in_sentry.png" alt="Sentry issue view showing message captured, with the URL of the image 404 as part of the message." height="2334" width="4064" /><h2 class="post__h2">Get notified when images are broken</h2><p class="post__p">If you, or a designated team member, would like to get notified of image load failures in your front-end applications, you can create a <a href="https://docs.sentry.io/product/alerts/" target="_blank">custom alert in Sentry</a> based on the “Failed to load image” message captured above.</p><ol><li><p class="post__p">Navigate to alerts, and click on Create Alert.</p></li><li><p class="post__p">The Errors &gt; Issues type is preselected for you. Click on Set Conditions.</p></li><li><p class="post__p">Add the filter for if “The event’s {attribute} value {match} {value}”.</p></li><li><p class="post__p">Leave the “message” and “contain” dropdown values as is, and set the “value” field to the message you send to Sentry when an image fails to load: “Failed to load image”.</p></li><li><p class="post__p">Next, choose your notification options, and set a name for this alert.</p></li><li><p class="post__p">Click save, and you’re done.</p></li></ol>
    <div class="post__arcadeEmbed">
      <div style="position: relative; padding-bottom: calc(70.16949152542374% + 41px); height: 0;"><iframe src="https://demo.arcade.software/QUBGFsfxdWARe0rrlPnc?embed" title="Create alert for image load failures" frameborder="0" loading="lazy" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;color-scheme: light;"></iframe></div>
    </div><h2 class="post__h2">Defensive coding FTW</h2><p class="post__p">Let’s be real. Broken images usually aren’t that much of a problem, and this solution might be overkill for you. But, by considering what might go wrong in your applications and coding defensively around those what-ifs, you can save some serious mental time and energy. This means more time and brain space to innovate, take a break, and find the perfect Giphy GIF that sums up your reaction to the latest weird trending news in tech.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Everything I install and set up on a new MacBook as a web developer</title>
          <description>Here are the developer tools, browsers, utility apps and other useful things I install on a new dev machine to help productivity.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/everything-i-install-and-set-up-on-a-new-macbook-as-a-web-developer/</link>
          <guid>https://whitep4nth3r.com/blog/everything-i-install-and-set-up-on-a-new-macbook-as-a-web-developer/</guid>
          <pubDate>Tue, 05 Dec 2023 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In 2023, I had to set up two fresh MacBook Pros (unexpectedly), and none of what I installed or how I set anything up was written down. And, oh boy, had I wished it was written down. So here’s me writing it down, for future me, and for <b class="post__p--italic">you</b> if you’re curious. </p><p class="post__p">Here’s everything I install and set up on a new MacBook that I use as a web developer.</p><h2 class="post__h2">Developer tools</h2><h3 class="post__h3">Terminal: iTerm2</h3><p class="post__p">I’ve tried other new and fancy terminals, but <a href="https://iterm2.com/" target="_blank">iTerm2</a> does the job. I use the <a href="https://fonts.google.com/specimen/Fira+Code" target="_blank">Fira Code</a> font (with ligatures enabled), and the <a href="https://draculatheme.com/iterm" target="_blank">Dracula colour palette</a>.</p><p class="post__p">I use the default <code>zsh</code> shell that ships with MacOS. I maintain a private version-controlled base <code>.zshrc</code> file on GitHub, which I copy to the <code>~</code> directory of a new machine. It includes some useful utility functions such as <a href="https://whitep4nth3r.com/blog/delete-all-merged-git-branches-one-terminal-command/" target="_blank">a command to delete all merged git branches</a>, and this function that removes and reinstalls dependencies in a Node-based JavaScript project.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="dQErUIKopJ"
      aria-describedby="dQErUIKopJ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="dQErUIKopJ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="dQErUIKopJ" itemprop="text" content="npmreset()%20%7B%0A%20%20echo%20%22%F0%9F%94%A5%20rm%20-rf%20node_modules%22%20%26%26%0A%20%20rm%20-rf%20node_modules%20%26%26%20%0A%20%20echo%20%22%F0%9F%94%A5%20rm%20package-lock.json%22%20%26%26%0A%20%20rm%20package-lock.json%20%26%26%20%0A%20%20echo%20%22%E2%9A%A1%EF%B8%8F%20npm%20i%22%20%26%26%0A%20%20npm%20i%0A%7D">
      <pre class="language-bash"><code class="language-bash"><span class="token function-name function">npmreset</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"🔥 rm -rf node_modules"</span> <span class="token operator">&amp;&amp;</span><br>  <span class="token function">rm</span> <span class="token parameter variable">-rf</span> node_modules <span class="token operator">&amp;&amp;</span> <br>  <span class="token builtin class-name">echo</span> <span class="token string">"🔥 rm package-lock.json"</span> <span class="token operator">&amp;&amp;</span><br>  <span class="token function">rm</span> package-lock.json <span class="token operator">&amp;&amp;</span> <br>  <span class="token builtin class-name">echo</span> <span class="token string">"⚡️ npm i"</span> <span class="token operator">&amp;&amp;</span><br>  <span class="token function">npm</span> i<br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">My <code>.zshrc</code> file also includes some configuration to display the current branch of a git repository, styled with colours from the Dracula colour palette.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="UezlpHdGRL"
      aria-describedby="UezlpHdGRL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="UezlpHdGRL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="UezlpHdGRL" itemprop="text" content="%23%20Load%20version%20control%20information%0Aautoload%20-Uz%20vcs_info%0Aprecmd()%20%7B%20vcs_info%20%7D%0A%0A%23%20Format%20the%20vcs_info_msg_0_%20variable%0Azstyle%20'%3Avcs_info%3Agit%3A*'%20formats%20'(%25b)'%0A%0A%23%20Set%20up%20the%20prompt%20(with%20git%20branch%20name)%0Asetopt%20PROMPT_SUBST%0APROMPT%3D'%25F%7Bmagenta%7D%3D%3D%3E%25f%20%25F%7Bblue%7D%24%7BPWD%2F%23%24HOME%2F~%7D%25f%20%25F%7Bcyan%7D%24%7Bvcs_info_msg_0_%7D%25f%25F%7Bgreen%7D%20%3D%3D%3E%20%25f'">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># Load version control information</span><br>autoload <span class="token parameter variable">-Uz</span> vcs_info<br><span class="token function-name function">precmd</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> vcs_info <span class="token punctuation">}</span><br><br><span class="token comment"># Format the vcs_info_msg_0_ variable</span><br>zstyle <span class="token string">':vcs_info:git:*'</span> formats <span class="token string">'(%b)'</span><br><br><span class="token comment"># Set up the prompt (with git branch name)</span><br>setopt PROMPT_SUBST<br><span class="token assign-left variable">PROMPT</span><span class="token operator">=</span><span class="token string">'%F{magenta}==>%f %F{blue}${PWD/#$HOME/~}%f %F{cyan}${vcs_info_msg_0_}%f%F{green} ==> %f'</span></code></pre>
    </div>
  </div>

  <p class="post__p">Here’s how it looks in the terminal:</p><img src="https://images.ctfassets.net/56dzm01z6lln/6snZNICOlBmSv4IqxEsYtw/4036f68b47d5a92f19cd72450f569fc2/terminal_prompt_dracula.png" alt="A pink ligature right facing arrow, pointing to the path of my blog repository in purple, showing the main branch name in brackets in blue, followed by a green ligature right facing arrow, pointing to a message in which that says kinda cute, right?" height="530" width="3680" /><h3 class="post__h3">Software management: Homebrew</h3><p class="post__p">I try to install as much as possible using <a href="https://brew.sh/" target="_blank">Homebrew</a>. The first packages I install when setting up a new machine are <b class="post__p--bold">node</b> and <b class="post__p--bold">nvm</b>. I also brew install <b class="post__p--bold">lolcat</b>, which lets me do this:</p><img src="https://images.ctfassets.net/56dzm01z6lln/5bmf1SRFDrrn4z3KpSx8RR/7195c2119e900fa7714570807d4c486d/terminal_lolcat_demo.png" alt="A terminal window, showing the command ls -la piped through lolcat, which outputs the contents of my dev folder, text coloured in a rainbow gradient." height="2382" width="3680" /><p class="post__p">It’s not that useful, but it’s a fun talking point whilst I’m <a href="https://twitch.tv/whitep4nth3r" target="_blank">live streaming on Twitch</a>.</p><h3 class="post__h3">Version control: SSH key</h3><p class="post__p">I follow <a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent" target="_blank">this guide from GitHub to generate a new SSH key</a> so that I can use ssh (rather than https) on the command line to interact with my repositories on GitHub.</p><h3 class="post__h3">Version control: .gitconfig</h3><p class="post__p">To configure Git version control ready for development, I run the following commands in my terminal:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fbPvMvpBLN"
      aria-describedby="fbPvMvpBLN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fbPvMvpBLN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="fbPvMvpBLN" itemprop="text" content="%23%20Configure%20my%20git%20user%0Agit%20config%20--global%20user.name%20%22My%20name%22%0Agit%20config%20--global%20user.email%20my%40email.com%0A%0A%23%20By%20default%2C%20set%20new%20branches%20to%20be%20pushed%20to%20the%20default%20remote%20%0A%23%20and%20set%20upstream%20tracking%0Agit%20config%20--global%20push.autoSetupRemote%20true%0A%0A%23%20Set%20the%20default%20branch%20name%20for%20new%20repositories%0Agit%20config%20--global%20init.defaultBranch%20main%0A%0A%23%20List%20all%20git%20settings%20in%20~%2F.gitconfig%0Agit%20config%20--list">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># Configure my git user</span><br><span class="token function">git</span> config <span class="token parameter variable">--global</span> user.name <span class="token string">"My name"</span><br><span class="token function">git</span> config <span class="token parameter variable">--global</span> user.email my@email.com<br><br><span class="token comment"># By default, set new branches to be pushed to the default remote </span><br><span class="token comment"># and set upstream tracking</span><br><span class="token function">git</span> config <span class="token parameter variable">--global</span> push.autoSetupRemote <span class="token boolean">true</span><br><br><span class="token comment"># Set the default branch name for new repositories</span><br><span class="token function">git</span> config <span class="token parameter variable">--global</span> init.defaultBranch main<br><br><span class="token comment"># List all git settings in ~/.gitconfig</span><br><span class="token function">git</span> config <span class="token parameter variable">--list</span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup" target="_blank">Full guidance on setting up Git for the first time</a> can be found on the official documentation. Also worth noting is that the first time you do anything with Git on MacOS, you’ll be prompted to download and install XCode command line tools.</p><h3 class="post__h3">CLI tools</h3><p class="post__p">Two CLI tools I install right away are the <a href="https://cli.github.com/" target="_blank">GitHub CLI</a> (via brew) and the <a href="https://docs.netlify.com/cli/get-started/" target="_blank">Netlify CLI</a> (via npm).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="qShBxNTqwY"
      aria-describedby="qShBxNTqwY">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="qShBxNTqwY">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="qShBxNTqwY" itemprop="text" content="brew%20install%20gh%0Anpm%20install%20netlify-cli%20-g">
      <pre class="language-bash"><code class="language-bash">brew <span class="token function">install</span> gh<br><span class="token function">npm</span> <span class="token function">install</span> netlify-cli <span class="token parameter variable">-g</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">IDE: VS Code</h3><p class="post__p">I use <a href="https://code.visualstudio.com/" target="_blank">VS Code</a>. It’s free and built on open source. I use the <a href="https://code.visualstudio.com/docs/editor/settings-sync" target="_blank">Settings Sync</a> feature which means that all my themes and plugins are managed in the cloud, so I don’t need to maintain an up-to-date list.</p><p class="post__p">I also make sure to run the <code>Install &#39;code&#39; command in PATH</code> command in VS Code, so that I can open repositories in VS Code from the command line by typing <code>code.</code> To do this, open the command palette in VS Code with CMD + SHIFT + P and type <code>shell command</code>. Confirm by pressing enter or clicking on <code>Install &#39;code&#39; command in PATH</code>. <a href="https://code.visualstudio.com/docs/setup/mac" target="_blank">Read more about the <code>code</code> command on the official documentation</a>.</p><h3 class="post__h3">Other developer tools</h3><p class="post__p">I need Docker for a couple of projects, so I install <a href="https://www.docker.com/products/docker-desktop/" target="_blank">Docker Desktop</a>.</p><h2 class="post__h2">Browsers and extensions</h2><p class="post__p">In 2023 I’ve been using <a href="https://arc.net/" target="_blank">Arc browser</a>. It’s built on Chromium so it can use all the Chrome store extensions. Essential browser extensions for me are:</p><ul><li><p class="post__p">AdBlock</p></li><li><p class="post__p">Dashlane password manager</p></li><li><p class="post__p">EditThisCookie</p></li></ul><p class="post__p">I also install Firefox and use Safari for cross-browser testing in development.</p><h2 class="post__h2">Utility apps</h2><p class="post__p">I was a Spectacle user for years, but I recently switched to using <a href="https://rectangleapp.com/" target="_blank">Rectangle for window management</a>. Rectangle has a few more options than Spectacle; I’m still getting used to it but I have no strong feelings either way.</p><p class="post__p">I also install <a href="https://www.raycast.com/" target="_blank">Raycast</a> as a Spotlight replacement. I probably don’t use it to its full potential right now, but one of the most useful features is being able to view my clipboard history with this custom key command: <code>ctrl+alt+cmd+/</code>.</p><h2 class="post__h2">Audio and visual apps</h2><p class="post__p">I’m fully in the <a href="https://www.elgato.com/us/en/s/downloads" target="_blank">Elgato ecosystem</a>, so I use Elgato Control Center (for my Keylight Airs), Elgato WaveLink (for my Wave 1 mic) and Elgato Camera Hub (for my Elgato Facecam — which I use for work meetings).</p><h2 class="post__h2">Useful things for content creation</h2><p class="post__p">If I’m recording a full screen capture for videos, I like to hide the app icons at the top of the screen. <a href="https://matthewpalmer.net/vanilla/" target="_blank">I use Vanilla for this</a>.</p><p class="post__p">For clearer screen recordings using a mouse pointer (and maybe because I’m getting old and my eyes are bad), I also increase the size of the mouse pointer in System Settings <b class="post__p--bold">→</b> Accessibility <b class="post__p--bold">→</b> Display <b class="post__p--bold">→</b> Pointer.</p><img src="https://images.ctfassets.net/56dzm01z6lln/XtF2swM52MVVQtvenSFhp/89a51e3c5500aa60e60b57827e3454cf/mouse_pointer_size.png" alt="MacOS accessibility settings showing my pointer size is around 33 perfect larger than normal." height="1164" width="1654" /><h2 class="post__h2">Useful things for work</h2><p class="post__p">I keep three calendars: a work calendar, a personal calendar, and a side project calendar. It’s useful to see a combined schedule at a glance, and for this I use <a href="https://meetingbar.app/" target="_blank">MeetingBar</a>.</p><h2 class="post__h2">Did I miss anything?</h2><p class="post__p">Probably. I’ll keep adding to this post when more software and tools make it into my regular rotation.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>A story about HTTP status codes and why you should read documentation</title>
          <description>Recently, I wrote some bad code. But this highlighted the importance of correct HTTP status codes, and how I should really read documentation.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/http-status-codes-and-reading-docs/</link>
          <guid>https://blog.sentry.io/http-status-codes-and-reading-docs/</guid>
          <pubDate>Fri, 10 Nov 2023 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Since 2020, I’ve been working on an Express (Node.js framework) application to power viewer interactions and events that happen whilst I’m streaming live coding on Twitch — my Twitch bot. Since using Sentry for <a href="https://sentry.io/for/error-monitoring/" target="_blank">error monitoring</a> and crashes using the <a href="https://docs.sentry.io/platforms/node/guides/express/" target="_blank">Sentry Node SDK</a>, I’ve already squashed quite a few bugs that were entirely a result of my own terrible code. But recently, I triggered a preventable application error via a seemingly failed call to a third-party API (which confused me since it returned an HTTP 200 status code). This reminded me of the importance of sending correct and representative HTTP status codes with responses — especially error responses — and how I should really read documentation better.</p><h2 class="post__h2">What triggered the error?</h2><p class="post__p">When streaming on Twitch, you have the ability to “shout out” other streamer accounts. When live, this sends a notification to their dashboard and pins a CTA to the top of your Twitch chat to encourage viewers to follow that streamer. My Twitch bot also extends this experience by sending an announcement to the chat window with the latest stream title and streamer’s category that was shouted out, using various calls to Twitch API endpoints to build up the response.</p><img src="https://images.ctfassets.net/56dzm01z6lln/38OpOVeT4vkbVI3ogO0Aoj/faaffface7f6b824fb707f8bb7495b75/twitch_chat_shoutout.png" alt="In twitch chat, I type !so drguthals, which triggers a response from my bot, saying Go check out DrGuthals at twitch.tv/drguthals and give them some panther PEW PEWS. They were last seen streaming: working on the Sentry + Fullstory package in talk shows and podcasts." height="725" width="582" /><p class="post__p">Recently when attempting to shout out a Twitch user, I made a typo (I’m only human, after all), and asked my Twitch bot to shout out a user that didn’t exist. The fact that this triggered an issue alert in Sentry (<b class="post__p--italic">TypeError: Cannot read properties of undefined</b>) highlighted that I had not coded defensively enough around making typos — and this post will detail how I improved upon this. However, through further inspection of the <a href="https://docs.sentry.io/platforms/node/guides/express/enriching-events/breadcrumbs/" target="_blank">breadcrumb trail</a> that led to this error event, I noticed what I thought was a flaw in the response from the Twitch API.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2oCgfZI9r4wiMKX76eV1qq/e812148886e9ad1db8be78e97aa636dc/breadcrumbs1.png" alt="Breadcrumbs showing the exception caused by TypeError: cannot read properties of undefined, and the previous event being an HTTP request to the Twitch API, asking it to get user data via a login name, and returning an HTTP 200 status code." height="1698" width="4064" /><p class="post__p">The breadcrumb that preceded the TypeError exception was an HTTP request to the Twitch API, requesting user data for a user via a login name string. Clearly, this user didn’t exist, but with the response, the Twitch API returned an HTTP status code 200, indicating that everything was “OK ”. Everything was, in fact, not “OK ”, at least in my application.</p><p class="post__p">I double-checked the non-existence of the login name requested (fa1kfaikfaik) by navigating to <b class="post__p--italic">twitch.tv/fa1kfaikfaik</b> in a browser — and this was confirmed in the UI. But the network tab also returned an HTTP 200 with the response, when I was expecting an HTTP 404 — not found.</p><img src="https://images.ctfassets.net/56dzm01z6lln/609zd5wOKTS2sZ13hWu6PL/23fca8d5ff3e1655455ac464deb5e515/typo_200_in_browser.png" alt="Browser showing twitch user not found, but network tab shows HTTP 200 OK status code." height="2334" width="4064" /><p class="post__p">Often, when dealing with HTTP responses, it’s good practice to check the status code of the response in order to proceed accordingly. For example, you may only want to proceed with sending a welcome email to a new customer if the account was created successfully, and the response returned from the account creation API contained an HTTP status code 201 (created). However, in this case, checking for the HTTP status code would not have solved my problem.</p><p class="post__p">Let’s take a look at the response data in code from this particular request for a user that does not exist. (Notice how Sentry has added the <code>sentry-trace</code> header to the Twitch API call to connect the dots between my application and external HTTP calls when reporting issues.)</p><img src="https://images.ctfassets.net/56dzm01z6lln/1XFgjhuk1WZaNZEKM24QVo/31ba48c0e21a774aec8df7ac65ec625a/terminal_log1.png" alt="Terminal window showing logs adding a sentry-trace header to the Twitch API call, requesting user data for fa1kfaikfaik, and that the response is an object, with one property data, that is an empty array." height="401" width="1102" /><p class="post__p">Now, technically this HTTP response is “OK ”. We have a response object, containing a property named data, which is an empty array; nothing is intrinsically wrong or malformed. But why was I getting an HTTP 200 when I was expecting an HTTP 404? The answer lies in the design of the Twitch API, and was implied in the shape of the return data logged to the terminal.</p><h2 class="post__h2">I wrote bad code and didn’t read the docs very well</h2><p class="post__p">The “/users” endpoint provided by the <a href="https://dev.twitch.tv/docs/api/reference/#get-users" target="_blank">Twitch API</a> allows you to get information about <b class="post__p--bold">one or more users</b>. The endpoint is <b class="post__p--bold">/users</b>, plural, not <b class="post__p--bold">/user</b>, singular, which is perfectly aligned with <a href="https://en.wikipedia.org/wiki/REST" target="_blank">RESTful API design</a>. Via this API endpoint, I am able to request data for up to 100 users. If just one out of 100 requests contained a typo and the API completely failed, that would be a pretty poor developer experience.</p><p class="post__p">Despite initially misunderstanding the API endpoint, this reinforces that understanding API design, coding defensively around any constraints, and using an <a href="https://sentry.io/for/performance/" target="_blank">application performance monitoring</a> tool like Sentry can help identify those gaps in your application code.</p><h2 class="post__h2">How I fixed my code</h2><p class="post__p">My Twitch bot contains a wrapper function around the Twitch /users API that purposefully only allows my application to request data for one single user. Here’s the original code that caused the TypeError issue in Sentry on an empty response, which makes a call to the Twitch API with the required credentials, and performs an account age check for security reasons. Given the <code>data</code> property of the response from Twitch was empty as shown in the terminal output above, <code>data[0]</code> did not exist when I tried to access the <code>created_at</code> property.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="nGRQsNEuZI"
      aria-describedby="nGRQsNEuZI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="nGRQsNEuZI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="nGRQsNEuZI" itemprop="text" content="async%20getUserByLogin(login)%20%7B%0A%20%20const%20accessTokenData%20%3D%20await%20accessTokenUtil.get()%3B%0A%0A%20%20if%20(accessTokenData)%20%7B%0A%20%20%20%20const%20request%20%3D%20await%20fetch(%0A%20%20%20%20%20%20%60https%3A%2F%2Fapi.twitch.tv%2Fhelix%2Fusers%3Flogin%3D%24%7Blogin%7D%60%2C%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20authorization%3A%20%60Bearer%20%24%7BaccessTokenData.accessToken%7D%60%2C%0A%20%20%20%20%20%20%20%20%20%20%22client-id%22%3A%20process.env.CLIENT_ID%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20)%3B%0A%0A%20%20%20%20const%20response%20%3D%20await%20request.json()%3B%0A%0A%20%20%20%20const%20passesAgeCheck%20%3D%20accountIsOlderThanSevenDays(%0A%20%20%20%20%20%20response.data%5B0%5D.created_at%2C%0A%20%20%20%20)%3B%0A%0A%20%20%20%20if%20(!passesAgeCheck)%20%7B%0A%20%20%20%20%20%20%20%2F%2F%20...%0A%20%20%20%20%7D%0A%0A%20%20%20%20return%20response.data%5B0%5D%3B%0A%20%20%7D%0A%0A%20%20return%20undefined%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">async</span> <span class="token function">getUserByLogin</span><span class="token punctuation">(</span><span class="token parameter">login</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> accessTokenData <span class="token operator">=</span> <span class="token keyword">await</span> accessTokenUtil<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>accessTokenData<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> request <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><br>      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://api.twitch.tv/helix/users?login=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>login<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token punctuation">{</span><br>        <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token literal-property property">authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>accessTokenData<span class="token punctuation">.</span>accessToken<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>          <span class="token string-property property">"client-id"</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CLIENT_ID</span><span class="token punctuation">,</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> request<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> passesAgeCheck <span class="token operator">=</span> <span class="token function">accountIsOlderThanSevenDays</span><span class="token punctuation">(</span><br>      response<span class="token punctuation">.</span>data<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>created_at<span class="token punctuation">,</span><br>    <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>passesAgeCheck<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>       <span class="token comment">// ...</span><br>    <span class="token punctuation">}</span><br><br>    <span class="token keyword">return</span> response<span class="token punctuation">.</span>data<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">return</span> <span class="token keyword">undefined</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">To make my code more resilient, I’ve now added a check for the length of the <code>data</code> array, and if I make a typo, I send a message to my Twitch chat to call me out on it.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="jfLIlGiDbQ"
      aria-describedby="jfLIlGiDbQ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="jfLIlGiDbQ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="jfLIlGiDbQ" itemprop="text" content="async%20getUserByLogin(login)%20%7B%0A%20%20const%20accessTokenData%20%3D%20await%20accessTokenUtil.get()%3B%0A%0A%20%20if%20(accessTokenData)%20%7B%0A%20%20%20%20const%20request%20%3D%20await%20fetch(%2F%2F%20...)%3B%0A%20%20%20%20const%20response%20%3D%20await%20request.json()%3B%0A%0A%2B%20%20%20if%20(response.data.length%20%3D%3D%3D%200)%20%7B%0A%2B%20%20%20%20%20tmi.say(config.channel%2C%20%22Typo%3A%20user%20not%20found%22)%3B%0A%2B%20%20%20%20%20return%20undefined%3B%0A%2B%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20...%0A%20%20%20%20return%20response.data%5B0%5D%3B%0A%20%20%7D%0A%0A%20%20return%20undefined%3B%0A%7D">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">async getUserByLogin(login) {<br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">const</span> accessTokenData <span class="token operator">=</span> <span class="token keyword">await</span> accessTokenUtil<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">if</span> <span class="token punctuation">(</span>accessTokenData<span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> request <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token comment">// ...);</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> request<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>response<span class="token punctuation">.</span>data<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>     tmi<span class="token punctuation">.</span><span class="token function">say</span><span class="token punctuation">(</span>config<span class="token punctuation">.</span>channel<span class="token punctuation">,</span> <span class="token string">"Typo: user not found"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">return</span> <span class="token keyword">undefined</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>   <span class="token comment">// ...</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">return</span> response<span class="token punctuation">.</span>data<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">return</span> <span class="token keyword">undefined</span><span class="token punctuation">;</span><br></span>}</code></pre>
    </div>
  </div>

  <p class="post__p">This may seem obvious, now. But it’s not uncommon. So thanks, <a href="https://sentry.io/welcome/" target="_blank">Sentry</a>, for calling me out on it, helping me better understand the API I was working with, and for enabling me to make my code more robust. This has also helped to reduce noise in Sentry caused by my inability to type correctly whilst streaming. After all, a typo isn’t an application error; it’s a user error.</p><h2 class="post__h2">Closing thoughts about API design and HTTP status codes</h2><p class="post__p">When interacting with APIs, understanding HTTP status codes sent with responses is a useful tool in helping you code defensively against errors in your application. Thorough handling of API responses based on returned HTTP status codes reduces the likelihood of uncaught exceptions, but most importantly, considering the different types of responses and HTTP codes that an API may return, leads to a better understanding of API design itself.</p><p class="post__p">And because I really have seen HTTP 200 status codes returned with legitimate error responses in my career, here are four things to consider when building APIs:</p><ul><li><p class="post__p">Return correct and representative <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" target="_blank">HTTP response status codes</a> with API responses</p></li><li><p class="post__p">Return consistent and predictable data structures</p></li><li><p class="post__p">Return organized data — don’t rely on the API consumer to order items unnecessarily, calculate values arbitrarily, or remove items they didn’t ask for</p></li><li><p class="post__p">Decide when an error really is an error; is an empty response an error, or is it actually “OK”?</p></li></ul><p class="post__p">And when consuming APIs, don’t do what I did. Read the documentation, understand how the API is designed, and code defensively around unexpected results. And if you really don’t have the time for that, Sentry and <a href="https://docs.sentry.io/platforms/node/guides/express/usage/distributed-tracing/" target="_blank">Distributed Tracing</a> has your back.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Using hooks to monitor and error track with Sentry when self-hosting Directus</title>
          <description>Learn how to set up Sentry monitoring and error tracking for your self-hosted Directus project by building custom hooks.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://docs.directus.io/blog/hooks-monitoring-error-tracking-sentry.html</link>
          <guid>https://docs.directus.io/blog/hooks-monitoring-error-tracking-sentry.html</guid>
          <pubDate>Wed, 11 Oct 2023 23:00:00 GMT</pubDate>
          <category>Tutorials</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you self-host Directus, it becomes your responsibility to ensure your project is running smoothly. Part of this is knowing when things are going wrong so you can triage issues, fix errors, and get on with your day.</p><p class="post__p">This is where <a href="https://sentry.io/welcome/" target="_blank">Sentry</a> comes in. Sentry is an error tracking and performance monitoring platform built for developers. With Sentry you can track and triage issues, warnings and crashes, and see issues replayed as they happened. Additionally, you can use Sentry to quickly identify <a href="https://docs.sentry.io/product/issues/issue-details/performance-issues/" target="_blank">performance issues</a>, and dive deep into the stack trace and breadcrumb trails that led to an error. Sentry is also Open Source, and supports a broad spectrum of <a href="https://docs.sentry.io/platforms/" target="_blank">programming languages and platforms via official SDKs</a>.</p><p class="post__p">In this post, we’ll create a <a href="https://docs.directus.io/extensions/hooks" target="_blank">hook extension</a> to set up Sentry error tracking on both the APIs that Directus generates, and the Data Studio applications.</p><h2 class="post__h2">Set up a new Directus project for extensions development</h2><p class="post__p">If you’re not already signed up to Sentry, <a href="https://sentry.io/signup/" target="_blank">create a free account</a>. Before we can get to the fun part, we’ll need to create a Directus project for extensions development. To do that:</p><ol><li><p class="post__p">Install Docker</p></li><li><p class="post__p">Create a new directory, for example <code>directus-self-hosted</code></p></li><li><p class="post__p">At the root of the new directory, create the following <code>docker-compose.yml</code> file, replacing the <code>KEY</code> and <code>SECRET</code> with random values.</p></li></ol><p class="post__p">Head on over to Sentry and set up two new projects — one for your back end project (Node.js), and one for the front end Directus Data Studio (Browser JavaScript).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VnMZtSgPsL"
      aria-describedby="VnMZtSgPsL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VnMZtSgPsL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="yaml">
      <meta data-code-id="VnMZtSgPsL" itemprop="text" content="version%3A%20'3'%0Aservices%3A%0A%20%20directus%3A%0A%20%20%20%20image%3A%20directus%2Fdirectus%3Alatest%0A%20%20%20%20ports%3A%0A%20%20%20%20%20%20-%208055%3A8055%0A%20%20%20%20volumes%3A%0A%20%20%20%20%20%20-%20.%2Fdatabase%3A%2Fdirectus%2Fdatabase%0A%20%20%20%20%20%20-%20.%2Fuploads%3A%2Fdirectus%2Fuploads%0A%20%20%20%20%20%20-%20.%2Fextensions%3A%2Fdirectus%2Fextensions%0A%20%20%20%20environment%3A%0A%20%20%20%20%20%20KEY%3A%20'replace-with-random-value'%0A%20%20%20%20%20%20SECRET%3A%20'replace-with-random-value'%0A%20%20%20%20%20%20ADMIN_EMAIL%3A%20'test%40example.com'%0A%20%20%20%20%20%20ADMIN_PASSWORD%3A%20'hunter2'%0A%20%20%20%20%20%20DB_CLIENT%3A%20'sqlite3'%0A%20%20%20%20%20%20DB_FILENAME%3A%20'%2Fdirectus%2Fdatabase%2Fdata.db'%0A%20%20%20%20%20%20WEBSOCKETS_ENABLED%3A%20true%0A%20%20%20%20%20%20EXTENSIONS_AUTO_RELOAD%3A%20true%0A%20%20%20%20%20%20CONTENT_SECURITY_POLICY_DIRECTIVES__SCRIPT_SRC%3A%20%22'self'%20'unsafe-eval'%20https%3A%2F%2Fjs.sentry-cdn.com%20https%3A%2F%2Fbrowser.sentry-cdn.com%22%0A%20%20%20%20%20%20SENTRY_DSN%3A%20'replace-with-back%20end-project-dsn'">
      <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">'3'</span><br><span class="token key atrule">services</span><span class="token punctuation">:</span><br>  <span class="token key atrule">directus</span><span class="token punctuation">:</span><br>    <span class="token key atrule">image</span><span class="token punctuation">:</span> directus/directus<span class="token punctuation">:</span>latest<br>    <span class="token key atrule">ports</span><span class="token punctuation">:</span><br>      <span class="token punctuation">-</span> 8055<span class="token punctuation">:</span><span class="token number">8055</span><br>    <span class="token key atrule">volumes</span><span class="token punctuation">:</span><br>      <span class="token punctuation">-</span> ./database<span class="token punctuation">:</span>/directus/database<br>      <span class="token punctuation">-</span> ./uploads<span class="token punctuation">:</span>/directus/uploads<br>      <span class="token punctuation">-</span> ./extensions<span class="token punctuation">:</span>/directus/extensions<br>    <span class="token key atrule">environment</span><span class="token punctuation">:</span><br>      <span class="token key atrule">KEY</span><span class="token punctuation">:</span> <span class="token string">'replace-with-random-value'</span><br>      <span class="token key atrule">SECRET</span><span class="token punctuation">:</span> <span class="token string">'replace-with-random-value'</span><br>      <span class="token key atrule">ADMIN_EMAIL</span><span class="token punctuation">:</span> <span class="token string">'test@example.com'</span><br>      <span class="token key atrule">ADMIN_PASSWORD</span><span class="token punctuation">:</span> <span class="token string">'hunter2'</span><br>      <span class="token key atrule">DB_CLIENT</span><span class="token punctuation">:</span> <span class="token string">'sqlite3'</span><br>      <span class="token key atrule">DB_FILENAME</span><span class="token punctuation">:</span> <span class="token string">'/directus/database/data.db'</span><br>      <span class="token key atrule">WEBSOCKETS_ENABLED</span><span class="token punctuation">:</span> <span class="token boolean important">true</span><br>      <span class="token key atrule">EXTENSIONS_AUTO_RELOAD</span><span class="token punctuation">:</span> <span class="token boolean important">true</span><br>      <span class="token key atrule">CONTENT_SECURITY_POLICY_DIRECTIVES__SCRIPT_SRC</span><span class="token punctuation">:</span> <span class="token string">"'self' 'unsafe-eval' https://js.sentry-cdn.com https://browser.sentry-cdn.com"</span><br>      <span class="token key atrule">SENTRY_DSN</span><span class="token punctuation">:</span> <span class="token string">'replace-with-back end-project-dsn'</span></code></pre>
    </div>
  </div>

  <p class="post__p">Head on over to Sentry and set up two new projects — one for your back end project (Node.js), and one for the front end Directus Data Studio (Browser JavaScript).</p><img src="https://images.ctfassets.net/56dzm01z6lln/IpMhqbXlxXAwTVrBGy8zb/b15b52c97808e0cc3f6151bb0adc1a80/directus_projects_in_sentry.png" alt="Sentry dashboard showing a backend Nodejs project and a browser JavaScript project. Both projects have no issues yet." height="1148" width="1999" /><p class="post__p">In Sentry, select your back end project, navigate to project settings, click on Client Keys (DSN), and copy the DSN (Data Source Name) value. Replace the <code>SENTRY_DSN</code> value in the <code>docker-compose.yml</code> file with the value from your Sentry project.</p><p class="post__p">Next, make sure Docker is running on your machine, and run <code>docker compose up</code> at the root of your project directory. You’ll see that the following directories have been created for you:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ICcieuqIQr"
      aria-describedby="ICcieuqIQr">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ICcieuqIQr">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="ICcieuqIQr" itemprop="text" content="directus-self-hosted%0A%E2%94%9C%20database%0A%E2%94%9C%20extensions%0A%E2%94%94%20uploads">
      <pre class="language-markdown"><code class="language-markdown">directus-self-hosted<br>├ database<br>├ extensions<br>└ uploads</code></pre>
    </div>
  </div>

  <p class="post__p">We’re going to create a Directus hook to be able to use Sentry in the back end application. In your terminal, navigate to the <code>extensions</code> directory, and run the following command with the following options to create the boilerplate code for your hook:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="SppclnwASP"
      aria-describedby="SppclnwASP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="SppclnwASP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="SppclnwASP" itemprop="text" content="npx%20create-directus-extension%40latest%0A%E2%94%9C%20extension%20type%3A%20hook%0A%E2%94%9C%20name%3A%20directus-extension-hook-sentry%0A%E2%94%94%20language%3A%20javascript">
      <pre class="language-markdown"><code class="language-markdown">npx create-directus-extension@latest<br>├ extension type: hook<br>├ name: directus-extension-hook-sentry<br>└ language: javascript</code></pre>
    </div>
  </div>

  <p class="post__p">Now the boilerplate has been created, navigate to the new hook directory, run the following command to install the Sentry Node.js SDK, and then open the directory in your code editor:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="leEWBAhxlT"
      aria-describedby="leEWBAhxlT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="leEWBAhxlT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="leEWBAhxlT" itemprop="text" content="cd%20directus-extension-hook-sentry%0Anpm%20install%20%40sentry%2Fnode">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> directus-extension-hook-sentry<br><span class="token function">npm</span> <span class="token function">install</span> @sentry/node</code></pre>
    </div>
  </div>

  <p class="post__p">Open <code>index.js</code> inside the <code>src</code> directory and delete the boilerplate. We’re ready to build the hook extension.</p><h2 class="post__h2">Understanding hooks in Directus</h2><p class="post__p">Custom API Hooks allow you to inject logic when specific events occur within your Directus project. These events include creating, updating, and deleting items in a collection, on a schedule, and at several points during Directus&#39; startup process.</p><p class="post__p">For this extension project, we&#39;ll use the <code>init</code> hooks to monitor the API by registering Sentry&#39;s <code>requestHandler</code>. For error tracking in the front end Data Studio application, we’ll use the <code>embed</code> method to inject custom JavaScript needed to track front end events in Sentry.</p><h2 class="post__h2">Monitor the Directus API using the Sentry Node SDK</h2><p class="post__p">Copy and paste the following code to the <code>index.js</code> file in your new hook directory. This imports the Sentry SDK, creates the initial export, and initializes the SDK. Due to how the Sentry SDK is built and the fact that Directus extensions are exclusively <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules" target="_blank">ES Modules</a>, we need to use <code>createRequire</code> from the <code>node:module</code> API:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="gbnzdbHDQk"
      aria-describedby="gbnzdbHDQk">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="gbnzdbHDQk">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="gbnzdbHDQk" itemprop="text" content="import%20%7B%20createRequire%20%7D%20from%20%22module%22%3B%20%0Aconst%20require%20%3D%20createRequire(import.meta.url)%3B%20%0Aconst%20Sentry%20%3D%20require('%40sentry%2Fnode')%3B%0A%0Aexport%20default%20(%7B%20init%20%7D%2C%20%7B%20env%20%7D)%20%3D%3E%20%7B%0A%09Sentry.init(%7B%0A%20%09%09dsn%3A%20env.SENTRY_DSN%2C%0A%09%09tracesSampleRate%3A%201.0%0A%09%7D)%3B%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> createRequire <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"module"</span><span class="token punctuation">;</span> <br><span class="token keyword">const</span> require <span class="token operator">=</span> <span class="token function">createRequire</span><span class="token punctuation">(</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span> <br><span class="token keyword">const</span> Sentry <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'@sentry/node'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> init <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> env <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>	Sentry<span class="token punctuation">.</span><span class="token function">init</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br> 		<span class="token literal-property property">dsn</span><span class="token operator">:</span> env<span class="token punctuation">.</span><span class="token constant">SENTRY_DSN</span><span class="token punctuation">,</span><br>		<span class="token literal-property property">tracesSampleRate</span><span class="token operator">:</span> <span class="token number">1.0</span><br>	<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">The first parameter of the default export makes the Directus <code>init</code> method available — this is used to define new <code>init</code> event types. In the Sentry initialization method, we’re passing in the DSN we defined in the <code>docker-compose.yml</code> file and the <code>tracesSampleRate</code>. The <code>tracesSampleRate</code> controls how many transactions arrive at Sentry and takes a value from 0.0 to 1.0 (from 0% to 100%). Whilst it may be useful to use a <code>tracesSampleRate</code> of 1.0 during testing, it is generally recommended to reduce this number in production.</p><p class="post__p">To start monitoring your back end application with Sentry, add two <code>init</code> hooks below the Sentry initialization. Under the hood, Directus uses Express for API routing. On <code>routes.before</code> we’re adding the Sentry <code>requestHandler</code>, which must be the first middleware registered on the app. On <code>routes.custom.after</code>, we’re adding the Sentry <code>errorHandler</code>, which must be registered before any other error middleware, and after all controllers.</p><p class="post__p">If you’d like more context about this implementation, you can read more about the <a href="https://docs.sentry.io/platforms/node/guides/express/" target="_blank">Sentry Express SDK</a> in the Sentry documentation.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CEJuntuejV"
      aria-describedby="CEJuntuejV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CEJuntuejV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="CEJuntuejV" itemprop="text" content="import%20%7B%20createRequire%20%7D%20from%20%22module%22%3B%20%0Aconst%20require%20%3D%20createRequire(import.meta.url)%3B%20%0Aconst%20Sentry%20%3D%20require('%40sentry%2Fnode')%3B%0A%0Aexport%20default%20(%7B%20init%20%7D%2C%20%7B%20env%20%7D)%20%3D%3E%20%7B%0A%09Sentry.init(%7B%0A%20%09%09dsn%3A%20env.SENTRY_DSN%2C%0A%09%09tracesSampleRate%3A%201.0%0A%09%7D)%3B%0A%0A%2B%09init('routes.before'%2C%20(%7B%20app%20%7D)%20%3D%3E%20%7B%20%0A%2B%09%09app.use(Sentry.Handlers.requestHandler())%3B%20%0A%2B%09%09console.log('--%20Sentry%20Request%20Handler%20Added%20--')%3B%20%0A%2B%09%7D)%3B%20%0A%2B%0A%2B%09init('routes.custom.after'%2C%20(%7B%20app%20%7D)%20%3D%3E%20%7B%20%0A%2B%09%09app.use(Sentry.Handlers.errorHandler())%3B%20%0A%2B%09%09console.log('--%20Sentry%20Error%20Handler%20Added%20--')%3B%20%0A%2B%09%7D)%3B%20%0A%7D%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">import { createRequire } from "module"; <br>const require = createRequire(import.meta.url); <br>const Sentry = require('@sentry/node');<br><br>export default ({ init }, { env }) => {<br>	Sentry.init({<br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>		<span class="token literal-property property">dsn</span><span class="token operator">:</span> env<span class="token punctuation">.</span><span class="token constant">SENTRY_DSN</span><span class="token punctuation">,</span><br></span>		tracesSampleRate: 1.0<br>	});<br><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>	<span class="token function">init</span><span class="token punctuation">(</span><span class="token string">'routes.before'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> app <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <br><span class="token prefix inserted">+</span>		app<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>Sentry<span class="token punctuation">.</span>Handlers<span class="token punctuation">.</span><span class="token function">requestHandler</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <br><span class="token prefix inserted">+</span>		console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'-- Sentry Request Handler Added --'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <br><span class="token prefix inserted">+</span>	<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <br><span class="token prefix inserted">+</span><br><span class="token prefix inserted">+</span>	<span class="token function">init</span><span class="token punctuation">(</span><span class="token string">'routes.custom.after'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> app <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <br><span class="token prefix inserted">+</span>		app<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>Sentry<span class="token punctuation">.</span>Handlers<span class="token punctuation">.</span><span class="token function">errorHandler</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <br><span class="token prefix inserted">+</span>		console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'-- Sentry Error Handler Added --'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <br><span class="token prefix inserted">+</span>	<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <br></span>};</code></pre>
    </div>
  </div>

  <p class="post__p">Next, let’s build the hook. In the <code>directus-extension-hook-sentry</code> directory, run <code>npm run build</code>. Restart the Directus Docker container, and you’ll see the two logs in your terminal.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5H92acCJrbcIhzRYLJZiuT/4169581cec8e85952d8a07278c27e0be/directus_terminal.png" alt="A terminal showing the Sentry Request Handler Added log and the Sentry Error Handler Added log, amidst the Docker logs." height="798" width="1999" /><h2 class="post__h2"><a href="https://docs.directus.io/blog/hooks-monitoring-error-tracking-sentry.html#monitor-the-directus-data-studio-using-the-sentry-loader-script" target="_blank">​</a>Monitor the Directus Data Studio using the Sentry Loader Script</h2><p class="post__p">Next, we’re going to add Sentry monitoring to your front end application (Directus Data Studio). To do this, we’ll need to inject some custom JavaScript to the page, and we can do this using embed hook events. Embed hook events allow custom JavaScript and CSS to be added to the <code>&lt;head&gt;</code> and <code>&lt;body&gt;</code> within the Directus Data Studio.</p><p class="post__p">Head over to Sentry, and navigate to the front end project you created earlier. Go to project settings, click on Loader Script, and copy the provided script tag code.</p>
    <div class="post__arcadeEmbed">
      <div style="position: relative; padding-bottom: calc(68.98305084745763% + 41px); height: 0; width: 100%"><iframe src="https://demo.arcade.software/gAUVKLxUizYOPhBNl6lC?embed" frameborder="0" loading="lazy" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;color-scheme: light;" title="Copy a front end project loader script in Sentry"></iframe></div>
    </div><p class="post__p">Back in the <code>index.js</code> file of your extension, make the <code>embed</code> method available in the exported function of the file:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="PoOxajbbmh"
      aria-describedby="PoOxajbbmh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="PoOxajbbmh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="PoOxajbbmh" itemprop="text" content="-%20export%20default%20(%7B%20init%20%7D%2C%20%7B%20env%20%7D)%20%3D%3E%20%7B%20%0A%2B%20export%20default%20(%7B%20init%2C%20embed%20%7D%2C%20%7B%20env%20%7D)%20%3D%3E%20%7B%20">
      <pre class="language-diff-javascript"><code class="language-diff-javascript"><span class="token deleted-sign deleted language-javascript"><span class="token prefix deleted">-</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> init <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> env <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> <br></span><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> init<span class="token punctuation">,</span> embed <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> env <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Below the two <code>init</code> hooks you created to monitor the back end application, add a new <code>embed</code> hook. The first parameter <code>head</code> instructs the extension to embed something into the <code>&lt;head&gt;</code> of your Directus Data Studio Application, and the second parameter is the front end Loader Script you copied from Sentry just now:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="kvCAzoFlNl"
      aria-describedby="kvCAzoFlNl">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="kvCAzoFlNl">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="kvCAzoFlNl" itemprop="text" content="embed(%0A%09%60head%60%2C%20%0A%09%60%3Cscript%20src%3D%22your-front%20end-project-loader-script-url%22%20crossorigin%3D%22anonymous%22%3E%3C%2Fscript%3E%60%0A)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token function">embed</span><span class="token punctuation">(</span><br>	<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">head</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <br>	<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;script src="your-front end-project-loader-script-url" crossorigin="anonymous">&lt;/script></span><span class="token template-punctuation string">`</span></span><br><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, rebuild the extension with <code>npm run build</code>, restart Directus again, and you have successfully implemented full stack Sentry error tracking and monitoring to your Directus project.</p><h2 class="post__h2">Test your full stack setup</h2><p class="post__p">Let’s send some test errors to Sentry to make sure everything is hooked up.</p><h3 class="post__h3">Test back end error tracking</h3><p class="post__p">We’re going to create a test endpoint to trigger an error event in Sentry by creating a new Directus extension. Navigate to the <code>extensions</code> directory, and run the following command with the following options to generate some boilerplate code for the test endpoint:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="NdpffWuRpp"
      aria-describedby="NdpffWuRpp">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="NdpffWuRpp">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="NdpffWuRpp" itemprop="text" content="npx%20create-directus-extension%40latest%0A%E2%94%9C%20type%3A%20endpoint%0A%E2%94%9C%20name%3A%20directus-extension-endpoint-fail%0A%E2%94%94%20language%3A%20javascript">
      <pre class="language-markdown"><code class="language-markdown">npx create-directus-extension@latest<br>├ type: endpoint<br>├ name: directus-extension-endpoint-fail<br>└ language: javascript</code></pre>
    </div>
  </div>

  <p class="post__p">You’ll now see a new directory, <code>directus-extension-endpoint-fail</code> in your extensions directory. Open the <code>index.js</code> file in the newly created directory and replace it with the following code, which will throw a new error intentionally.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CrsQLafSeM"
      aria-describedby="CrsQLafSeM">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CrsQLafSeM">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="CrsQLafSeM" itemprop="text" content="export%20default%20%7B%20%09%0A%20%20id%3A%20'fail'%2C%20%09%0A%20%20handler%3A%20(router)%20%3D%3E%20%7B%20%09%09%0A%20%20%20%20router.get('%2F'%2C%20(req%2C%20res)%20%3D%3E%20%7B%20%09%09%09%0A%20%20%20%20%20%20throw%20new%20Error('Intentional%20back%20end%20error%20for%20Sentry%20test')%3B%20%09%0A%20%20%20%20%20%20%20%20%7D)%3B%20%09%0A%20%20%20%20%7D%20%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span> 	<br>  <span class="token literal-property property">id</span><span class="token operator">:</span> <span class="token string">'fail'</span><span class="token punctuation">,</span> 	<br>  <span class="token function-variable function">handler</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">router</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 		<br>    router<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 			<br>      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'Intentional back end error for Sentry test'</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 	<br>        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 	<br>    <span class="token punctuation">}</span> <br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">In the root of the new extension directory, run <code>npm run build</code>, restart the Directus Docker container again, and navigate to <code>http://localhost:8055/fail</code> in your browser. You will see an error message on the browser page, in the terminal, and in your back end project&#39;s Sentry issues list. Boom!</p><img src="https://images.ctfassets.net/56dzm01z6lln/1vXnMaEm41vRg74gtHiH95/711c4619aefb2313fc7f7ccdf35b6cfb/directus_backend_error.png" alt="A backend error triggered successfully in Sentry." height="1148" width="1999" /><h3 class="post__h3">Test front end error tracking</h3><p class="post__p">Next, let’s confirm the front end Loader Script is tracking issues. Let’s create another extension to test an error in a front end template. Back in your Directus <code>extensions </code>directory, run the following command:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LvnJRWvJiY"
      aria-describedby="LvnJRWvJiY">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LvnJRWvJiY">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="LvnJRWvJiY" itemprop="text" content="npx%20create-directus-extension%40latest%0A%E2%94%9C%20type%3A%20module%0A%E2%94%9C%20name%3A%20directus-extension-module-fail%0A%E2%94%94%20language%3A%20javascript">
      <pre class="language-markdown"><code class="language-markdown">npx create-directus-extension@latest<br>├ type: module<br>├ name: directus-extension-module-fail<br>└ language: javascript</code></pre>
    </div>
  </div>

  <p class="post__p">Open the newly created extension&#39;s <code>module.vue</code> file and replace it with the following code:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mIXCSKrnRz"
      aria-describedby="mIXCSKrnRz">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mIXCSKrnRz">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="mIXCSKrnRz" itemprop="text" content="%3Ctemplate%3E%0A%09%3Cprivate-view%20title%3D%22My%20Custom%20Module%22%3E%0A%09%09%3Cv-button%20%40click%3D%22triggerError%22%3ETrigger%20Error%3C%2Fv-button%3E%0A%09%3C%2Fprivate-view%3E%0A%3C%2Ftemplate%3E%0A%0A%3Cscript%3E%0Aexport%20default%20%7B%0A%09methods%3A%20%7B%0A%09%09triggerError()%20%7B%0A%09%09%09const%20error%20%3D%20new%20Error('Intentional%20front%20end%20error%20for%20Sentry')%3B%0A%09%09%09Sentry.captureException(error)%3B%0A%09%09%7D%0A%09%7D%0A%7D%3B%0A%3C%2Fscript%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>template</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>private-view</span> <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>My Custom Module<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>		<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>v-button</span> <span class="token attr-name">@click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>triggerError<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Trigger Error<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>v-button</span><span class="token punctuation">></span></span><br>	<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>private-view</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>template</span><span class="token punctuation">></span></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span><br>	<span class="token literal-property property">methods</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>		<span class="token function">triggerError</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>			<span class="token keyword">const</span> error <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'Intentional front end error for Sentry'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>			Sentry<span class="token punctuation">.</span><span class="token function">captureException</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>		<span class="token punctuation">}</span><br>	<span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">From the extension directory, run <code>npm run build</code>, restart the Directus Docker container, and navigate to <code>http://localhost:8055/admin/settings/project</code> in your browser. Sign in to Directus using the credentials in your <code>docker-compose.yml</code> file. Scroll down to Modules, and check the checkbox to enable the new custom module. For reference, the name of the module is defined in the <code>index.js</code> file of the module extension.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3YK272bOvuWum2ze7do98y/df1ff2c2b740b102f261db9865bc51a0/directus_project_settings.png" alt="Project settings in Directus, showing the new custom module available." height="1148" width="1999" /><p class="post__p">Navigate to the new custom module using the icon on the left menu bar, and click the <b class="post__p--bold">Trigger Error</b> button.</p><img src="https://images.ctfassets.net/56dzm01z6lln/56It39VDO3TXWkRT4qjm9/f6f86e94bb188709129a81b64ae1285d/directus_custom_module.png" alt="Directus UI showing the custom module screen with the Trigger Error button." height="1148" width="1999" /><p class="post__p">You’ll now see the error message in your front end project&#39;s Sentry issue list. We’re done!</p><img src="https://images.ctfassets.net/56dzm01z6lln/14gp8B46COtzjrVRlRl4Dc/9ffb8f3fd5edfbd011fd39cbb5a01f90/directus_frontend_error.png" alt="A frontend error triggered successfully in Sentry." height="1148" width="1999" /><h2 class="post__h2">Summary</h2><p class="post__p">If you’re self-hosting Directus, you need a reliable way to monitor, triage and be alerted to issues in your back end and front end applications. Sentry makes this possible and ensures you spend less time searching for clues, and more time fixing what’s broken. Additionally, you can configure <a href="https://docs.sentry.io/product/sentry-basics/tracing/" target="_blank">Distributed Tracing</a> with Sentry to provide a connected view of related errors and transactions by capturing interactions among your entire suite of Directus extensions and software applications.</p><p class="post__p">Head over to the <a href="https://docs.sentry.io/platforms/" target="_blank">Sentry docs to learn about the wide range of language and platform support</a>, and if you’re still not convinced, try out the <a href="https://sandbox.sentry.io/" target="_blank">Sentry Sandbox</a> to explore the platform with a bucket load of pre-populated real-world data.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to use jQuery with Astro</title>
          <description>Understanding how to use jQuery in an Astro project was hard to Google. So I wrote my own guide for you and my future self.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-use-jquery-with-astro/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-use-jquery-with-astro/</guid>
          <pubDate>Thu, 28 Sep 2023 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Since it’s alpha release in 2021, <a href="https://astro.build" target="_blank">Astro</a> is an open source web framework that has been growing steadily in popularity. One of the unique selling points of Astro is that you can “bring your own UI library”, giving you the ability to use React, Preact, Vue, SolidJS, Svelte and more — all within a single Astro project — or even a single file!</p><p class="post__p">However, <a href="https://trends.builtwith.com/javascript/jQuery" target="_blank">jQuery is still the most popular and most used JavaScript library on the web</a>. And it was my first taste of writing JavaScript for the browser back in 2014 (yes, before I learned plain JavaScript syntax, don’t hate).</p><p class="post__p">And without a jQuery integration for Astro guide available (yet), I wanted to see if I could use jQuery in an Astro project. “Why, Salma, why?” I hear you cry. Well, why <b class="post__p--italic">not</b>? Even Astro co-creator Matthew Phillips loves jQuery.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3kbjUjt1CRf7GwkXXGWL93/05e283b0e3e857b4bbba02ec4474ae9d/tweet_1707049415704223904_20241114_142127_via_10015_io.png" alt="Matthew Philips on Twitter writes kQuery is my favorite frontend framework (yes, in 2023), on September 27 2023." height="360" width="1832" /><h2 class="post__h2">Use the is:inline template directive</h2><p class="post__p">By default, Astro processes, optimises and bundles any <code>&lt;script&gt;</code> tags that it finds in a layout, page or component file. Initially, I tried installing the jQuery package via npm, and importing it inside a script tag. But given the import was bundled up by Astro, the good old jQuery <code>$</code> wasn’t able to initialise and get access to the window object and DOM. I also found a highly experiment jQuery integration for Astro that… just didn’t work at all 🙃.</p><p class="post__p">You might be familiar with adding jQuery to an HTML file using a <code>&lt;script&gt;</code> tag referencing the minified source code from the <a href="https://releases.jquery.com/" target="_blank">jQuery CDN</a> or a local file. We can achieve this in Astro by preventing the bundling of the jQuery import by adding the <code>is:inline</code> directive to <code>&lt;script&gt;</code> tags in layout, page or component files. This is also really useful to know if you want to interact with elements on the page using plain JavaScript even <b class="post__p--italic">without</b> jQuery.</p><p class="post__p">To use jQuery in an Astro project, you can choose to reference the minified source code from the CDN, or download the source code to the <code>public</code> folder of your project, and reference it from there.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="OqYlVheqDS"
      aria-describedby="OqYlVheqDS">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="OqYlVheqDS">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="OqYlVheqDS" itemprop="text" content="%3Cscript%20is%3Ainline%20src%3D%22https%3A%2F%2Fcode.jquery.com%2Fjquery-3.7.0.min.js%22%3E%3C%2Fscript%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name"><span class="token namespace">is:</span>inline</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://code.jquery.com/jquery-3.7.0.min.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Using jQuery in Astro components and pages</h2><p class="post__p">If you choose to import jQuery to a layout file, you’re free to use jQuery syntax in your page and component files using a <code>&lt;script&gt;</code> tag with the <code>is:inline</code> directive — or let Astro bundle and optimise the code by omitting the directive. Like so:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="rWPHWhnSkG"
      aria-describedby="rWPHWhnSkG">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="rWPHWhnSkG">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="rWPHWhnSkG" itemprop="text" content="%3Cscript%20is%3Ainline%3E%0A%20%20%24(%22button%22).on(%22click%22%2C%20()%20%3D%3E%20%7Bconsole.log(%22Button%20clicked!%22)%7D)%3B%0A%3C%2Fscript%3E%0A%0A%3C!--%20or%20if%20you%20want%20the%20code%20bundled%20and%20optimised%20--%3E%0A%0A%3Cscript%3E%0A%20%20%24(%22button%22).on(%22click%22%2C%20()%20%3D%3E%20%7Bconsole.log(%22Button%20clicked!%22)%7D)%3B%0A%3C%2Fscript%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name"><span class="token namespace">is:</span>inline</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br>  <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"button"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Button clicked!"</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br><br><span class="token comment">&lt;!-- or if you want the code bundled and optimised --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br>  <span class="token function">$</span><span class="token punctuation">(</span><span class="token string">"button"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Button clicked!"</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">jQuery is great!</h2><p class="post__p">That’s it. That’s the headline. <a href="https://docs.astro.build/en/reference/directives-reference/#isinline" target="_blank">Read more about the is:inline directive on the official Astro documentation</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>From LCP to CLS: Improve your Core Web Vitals with Image Loading Best Practices</title>
          <description>Learn all about image lazy loading and how it can help improve performance, UX and core web vitals.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://blog.sentry.io/from-lcp-to-cls-improve-your-core-web-vitals-with-image-loading-best/</link>
          <guid>https://blog.sentry.io/from-lcp-to-cls-improve-your-core-web-vitals-with-image-loading-best/</guid>
          <pubDate>Thu, 21 Sep 2023 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you’re a front end developer, there’s a high probability you’ve built (or will build) an image-heavy page. And you’ll need to make it look great by serving high-quality image files. But you’ll also need to prioritize building a high-quality user experience by making sure your <a href="https://docs.sentry.io/product/performance/web-vitals/" target="_blank">Core Web Vitals</a> such as <a href="https://web.dev/cls/" target="_blank">Cumulative Layout Shift</a> and <a href="https://web.dev/lcp/" target="_blank">Largest Contentful Paint</a> aren’t negatively affected, which also help with your search engine rankings.</p><p class="post__p">In this post, we’ll learn about lazy loading, how lazy loading images can improve performance, user experience and Core Web Vitals, and how Sentry can help you <a href="https://sentry.io/for/performance/" target="_blank">monitor performance</a> and diagnose issues with your web apps.</p><h2 class="post__h2">What is lazy loading?</h2><p class="post__p">Lazy loading is a way to tell the browser to only load resources, such as images, when they’re needed. Great candidates for lazy loading are images that are not visible on page load, maybe they’re waaaay down at the bottom of the page. These images can be requested and loaded when a user scrolls an image into the viewport. This prevents forcing a user to download all images on page load, which could eat up their data allowances if they’re browsing on a mobile connection, and speeds up the perceived load speed of the page. The bottom line is, lazy loading makes things faster.</p><p class="post__p">You can tell if a web page is using lazy loading on images by opening up the network tab in your browser dev tools. Filter resource type by <code>img</code>, reload the page, and scroll down. You’ll notice new network requests for images as you scroll. These images have been lazy loaded.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1XV3aT48IuJLyADM06jQkE/2f435d2c60ee2b39ddf04ce51e3cc513/lazy_loading.gif" alt="animated GIF loop showing the network tab in a browser loading new image requests as the browser page is scrolled" height="371" width="600" /><h2 class="post__h2">Browser support for lazy loading</h2><p class="post__p">The great news is that all major browsers support lazy loading for images, so you can be confident in implementing lazy loading in your web apps straight away. And some browsers even support lazy loading for iframes. <a href="https://caniuse.com/?search=loading" target="_blank">You can find the current browser support for lazy loading on caniuse.com.</a></p><h2 class="post__h2">How to lazy load images</h2><p class="post__p">Lazy loading is configured by adding the <code>loading</code> attribute to any HTML <code>img</code> element, and setting the value to <code>lazy</code>. That’s it. This attribute instructs the browser to only load the image when it is visible in the viewport, shortening the <a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path" target="_blank">Critical Rendering Path</a>, and making your page load faster.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="rmypXvvgJy"
      aria-describedby="rmypXvvgJy">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="rmypXvvgJy">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="rmypXvvgJy" itemprop="text" content="%3Cimg%20src%3D%22https%3A%2F%2Fcdn.mywebsite.io%2Famazing_image.png%22%20%0A%20%20%20%20%20loading%3D%22lazy%22%20%0A%20%20%20%20%20alt%3D%22The%20most%20amazing%20image%20you've%20ever%20seen.%22%20%2F%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://cdn.mywebsite.io/amazing_image.png<span class="token punctuation">"</span></span> <br>     <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span> <br>     <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>The most amazing image you've ever seen.<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">The default value for the <code>loading</code> attribute is <code>eager</code>. You usually don’t need to specify this, but it’s often useful if your tooling automatically adds <code>loading=”lazy”</code> by default, and you know that an image will be visible in the viewport on page load. And this leads us to a very important rule when considering which images to lazy load.</p><h2 class="post__h2">Don’t lazy load every image</h2><p class="post__p"><a href="https://web.dev/lcp/" target="_blank">Largest Contentful Paint (LCP)</a> is the Core Web Vital that represents how quickly the main content of a web page is loaded, measuring the time from when the user requests the page until the largest image or text block is rendered within the viewport. If you lazy load images that are part of the LCP (often referred to as “above the fold”), they may begin loading after the layout confirms the image is in the viewport via the <a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Critical_rendering_path" target="_blank">Critical Rendering Path</a>, which is much later than they should.</p><p class="post__p">Deciding which loading strategy to use for which images can get tricky depending on your responsive layouts and the myriad device and viewport sizes that exist today. Set Studio recently shared data which indicated <a href="https://viewports.fyi/" target="_blank">there are over 2300 unique viewport sizes</a>. Browsers may also have different thresholds that determine the distance from the visible viewport that an image should be lazy loaded. <a href="https://web.dev/browser-level-image-lazy-loading/#improved-data-savings-and-distance-from-viewport-thresholds" target="_blank">Chrome recently published an update to these distances in June 2023.</a></p><p class="post__p">Your first instinct might be to use JavaScript to detect whether an image is visible or not in the viewport on initial page load, and apply the lazy loading strategy after the fact. But you’d need to do this after the layout stage of the Critical Rendering Path, which is after the HTML has requested JavaScript that may alter the DOM in the first instance. This may slow your resulting page load and negatively affect your LCP score.</p><p class="post__p">My recommendation would be to eagerly load a few more images than you might think will be visible in the viewport to account for such variety in viewport sizes. Combine this with loading the best image dimensions, resolutions and file types for that device and viewport size and you should be onto a winner.</p><h2 class="post__h2">How to prevent cumulative layout shift when lazy loading images</h2><p class="post__p">Have you ever accidentally clicked on a BUY NOW button after a rogue banner advertisement finally popped onto the page and moved everything around? <a href="https://web.dev/cls/" target="_blank">Cumulative Layout Shift</a> is when an element unexpectedly pops into view on a page, shifting the content around it. You might also see this happen when using custom fonts, and Lazar Nikolov provides great advice on how to prevent this in <a href="https://blog.sentry.io/web-fonts-and-the-dreaded-cumulative-layout-shift/" target="_blank">Web Fonts and the Dreaded Cumulative Layout Shift</a>. Lazy loading images also comes with this risk — but thankfully there’s also a way to prevent it.</p><p class="post__p">Add <code>width</code> and <code>height</code> attributes to your <code>img</code> elements to instruct the browser to reserve the correct ratio of space on the page in which to finally load your image when it scrolls into view. Add the <code>height</code> and <code>width</code> attributes to all <code>img</code> elements, regardless of whether they are lazy loaded or not. This will also minimize CLS during the LCP. Google advises that “to provide a good user experience, sites should strive to have a CLS score of 0.1 or less.”</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vSyGPbJCJu"
      aria-describedby="vSyGPbJCJu">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vSyGPbJCJu">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="vSyGPbJCJu" itemprop="text" content="%3Cimg%20src%3D%22https%3A%2F%2Fcdn.mywebsite.io%2Famazing_image.png%22%20%0A%20%20%20%20%20loading%3D%22lazy%22%0A%20%20%20%20%20height%3D%E2%80%9D1080%E2%80%9D%0A%20%20%20%20%20width%3D%E2%80%9D1920%E2%80%9D%0A%20%20%20%20%20alt%3D%22The%20most%20amazing%20image%20you've%20ever%20seen.%22%20%2F%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://cdn.mywebsite.io/amazing_image.png<span class="token punctuation">"</span></span> <br>     <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span><br>     <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span>”1080”</span><br>     <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span>”1920”</span><br>     <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>The most amazing image you've ever seen.<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Why monitoring Core Web Vitals is bigger than periodic checks</h2><p class="post__p">Google takes into consideration your Core Web Vitals scores in search engine rankings, so you want to make sure your scores are within the limits specified for a good user experience. You might already be conducting periodic Google Lighthouse tests on your pages in development via your browser to check for performance issues such as CLS and LCP. And this is often good practice. But in reality, when working on large-scale applications such as ecommerce stores, you may be building templates that will generate thousands of pages in production. It’s unrealistic to check every page, for every internet connection speed, device type and viewport size.</p><p class="post__p">Additionally, you might use automated Lighthouse testing in your CI/CD pipelines, configured to automatically fail deployments when test results fall below a specified threshold. But development teams are often expected to release many times a day, and adding time-consuming performance tests to your CI/CD processes can add hours onto release times. Not to mention the time it takes to triage a failed deployment, make changes, push new code, run CI/CD again, rinse and repeat.</p><p class="post__p">You need an efficient way to monitor the performance of every page of your application that doesn’t add hours to development time, doesn’t block releases, and only alerts you to the issues that matter. Because, let’s be honest, you’re not doing this every time. But Sentry <b class="post__p--italic">can</b> actually fix this!</p><h2 class="post__h2">How to monitor performance impacts of lazy loading</h2><p class="post__p">You’ve implemented lazy loading and you are now able to set up monitoring to confirm it has improved the performance of your site, without having to manually check dev tools every few weeks. <a href="https://docs.sentry.io/platforms/javascript/performance/instrumentation/automatic-instrumentation/" target="_blank">Sentry’s JavaScript Browser SDK</a> automatically collects all Core Web Vitals, and you also have the ability to send your own custom metrics to Sentry, such as memory usage and component render times. When initializing Sentry in your code, include the BrowserTracing integration so that a new transaction gets created in Sentry for each page load and navigation event.</p><p class="post__p">With BrowserTracing enabled, you’ll see transactions start to fill up your <a href="https://sentry.io/features/dashboards/" target="_blank">performance dashboard</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2k5rOGfC6kvHfxxmH3PMmL/52e4a61e639ab3956e2a6c65db85b084/performance_dash_sentry.jpg" alt="Performance dashboard in Sentry, showing data for worst LCP web vitals, worst FCP web vitals, p75 LCP, user misery and transactions per minute for a front end JavaScript project." height="1036" width="1392" /><p class="post__p">Luckily, our own tool helped bail us out here. We use Sentry performance monitoring to monitor the performance of Sentry performance monitoring. And with these huge insights into how Sentry was performing for users, the team were able to identify that lazy loading release and health data separately, but in parallel, <a href="https://www.youtube.com/watch?v=O37pD9DQlA0&list=PLOwEowqdeNMrDkr35goo7YuIbroVPf8-n&index=6&ab_channel=Sentry" target="_blank">led to a 22% faster UI and almost half a second faster load time</a>. We also encountered a similar issue on the <a href="https://sentry.io/welcome/" target="_blank">Sentry home page</a>, which serves over 150 images. We used our own performance monitoring to learn that we needed to implement lazy loading on all images that were not part of the LCP.</p><h2 class="post__h2">How to alert your team when your Core Web Vitals are miserable</h2><p class="post__p">So you don’t have to spend hours and hours painstakingly triggering manual Lighthouse tests — or add hours to your deployment processes with CI/CD testing, you can configure alerts in Sentry to let you know when your users are experiencing poor performance.</p><p class="post__p">Create a new performance alert, configure the metric, alert thresholds, and actions you want Sentry to perform. What’s more, by <a href="https://sentry.io/for/error-monitoring/" target="_blank">monitoring errors</a> and performance in Sentry, you’re getting insights and alerts based on what real users are experiencing on the pages that they’re visiting – not just data from the tests you run yourself.</p><p class="post__p">In this example, I set up a critical alert to fire and notify me by email when the average LCP for all site pages exceeds 3 seconds in the space of one hour. (Google recommends sites should maintain an <a href="https://web.dev/lcp/#what-is-a-good-lcp-score" target="_blank">LCP score lower than 2.5 seconds</a> to provide a good user experience.) This is a great way to <a href="https://sentry.io/features/releases/" target="_blank">monitor release health</a>. Should a change go out that negatively affects the average LCP, Sentry will notify me within the hour.</p>
    <div class="post__arcadeEmbed">
      <div style="position: relative; padding-bottom: calc(67.79661016949152% + 41px); height: 0; width: 100%"><iframe src="https://demo.arcade.software/xvkjmrHb8WDqWpfZkIm5?embed" frameborder="0" loading="lazy" webkitallowfullscreen mozallowfullscreen allowfullscreen style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;color-scheme: light;" title="Add LCP alert in Sentry"></iframe></div>
    </div><h2 class="post__h2">Wrapping up</h2><p class="post__p">Acing your Core Web Vital scores is crucial in providing a good user experience and pleasing the Google machine. Lazy loading images until they are visible in the viewport is a useful way to shorten the Critical Rendering Path, speed up perceived page load for users and improve your Largest Contentful Paint score. Ensure you add width and height attributes to image elements to prevent Cumulative Layout Shift for lazy loaded images, and always be mindful of different device sizes and viewport widths when deciding which images to lazy load. Monitor your Core Web Vitals and overall performance with Sentry, and set up alerts for critical performance issues without sacrificing development time or CI/CD resources.</p><p class="post__p">Check out the <a href="https://docs.sentry.io/platforms/python/guides/gcp-functions/performance/" target="_blank">docs pages to learn how to set up performance monitoring</a> in your applications, and we’d love to hear from you in the <a href="https://discord.gg/sentry" target="_blank">Sentry Discord</a> about how Sentry has helped you solve performance issues in your apps.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>5 reasons you should hire me as your next developer experience engineer</title>
          <description>I’m excited to be looking for my next role as a Developer Experience Engineer. Here are five reasons why you should hire me.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/hire-me/</link>
          <guid>https://whitep4nth3r.com/blog/hire-me/</guid>
          <pubDate>Sun, 16 Jul 2023 23:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <div class="post__callout">
  <h3 class="post__callout__title">Good news!</h3>
    <div class="post__callout__content">
      <p>I am now working at <a href="https://sentry.io">Sentry</a> as Senior Developer Advocate.</p>

    </div>
  </div><h2 class="post__h2">I have 10 years industry experience</h2><p class="post__p">I started my tech career as a full stack developer, and chose front end development as my specialism. After a few years honing my craft, I moved into a number of tech lead positions, working on the modernisation of a large and established bespoke e-commerce platform, and architecting and delivering a number of greenfield projects. </p><p class="post__p">I did a lot of soul-searching during the first lockdown of 2020, and in 2021 I began my career in developer relations to bring together my varied experience in music, performing, teaching and tech.</p><p class="post__p"><a href="https://www.linkedin.com/in/whitep4nth3r/" target="_blank">View my full career history on LinkedIn</a>.</p><h2 class="post__h2">I didn’t do a tech degree</h2><p class="post__p">I have a First Class Honours degree in composition from the Royal Northern College of Music (UK), a Post Graduate Certificate in Teaching, and Qualified Teacher Status (UK). I’ve always been a performer, confident in front of an audience of all ages, and I bring this to tech by delivering engaging conference talks, live coding streams and video content.</p><p class="post__p">I began my career in tech by seizing an opportunity and learning on the job. As a result I have a deep empathy for beginners, career-changers and anyone who’s just figuring things out.</p><p class="post__p">Learn more about my experience (including a brief career as musical comedian) in <a href="https://github.com/readme/podcast/salma-alam-naylor" target="_blank">my episode of the GitHub README podcast</a>, published in December 2021.</p><blockquote class="post__blockquote"><p class="post__p">It’s always awesome working with Salma! She’s a creative teacher, willing to learn in public, and just <b class="post__p--bold">incredibly authentic in everything she does</b>. The whitep4nth3r community is SO fun to be a part of! — Brad Van Vugt, <a href="https://play.battlesnake.com/" target="_blank">Battlesnake</a></p></blockquote><h2 class="post__h2">I span disciplines and departments by default</h2><p class="post__p">I’ve worked in engineering organisations, marketing departments and product agencies, and as a developer experience engineer, I get great results by acting as a bridge between these disciplines. </p><p class="post__p">As a DX Engineer, I will: </p><ul><li><p class="post__p">🛣️ road-test your new product features,</p></li><li><p class="post__p">🗣️ gather feedback from the developer community,</p></li><li><p class="post__p">📝 submit detailed issues for improvement, </p></li><li><p class="post__p">👩🏻‍💻 write code and submit a PR, </p></li><li><p class="post__p">📺 create engaging content to tell the world about what’s possible with your amazing new tech,</p></li><li><p class="post__p">📊 and work with you on analysing the data to strategise on what’s next.</p></li></ul><h2 class="post__h2">I’m not just a content creator</h2><p class="post__p">As a developer experience engineer, I don’t make content for the sake of content, and as a developer, I love exploring new tech, stretching its limits, and seeing how creative I can be.</p><p class="post__p">Everything I create and publish comes out of <b class="post__p--bold">deep product exploration</b>, <b class="post__p--bold">problem-solving for real-world use cases</b>, and the desire to <b class="post__p--bold">inspire developers of all abilities</b> to build cool stuff with tech.</p><p class="post__p">Check out:</p><ul><li><p class="post__p"><a href="https://github.com/whitep4nth3r" target="_blank">My projects on GitHub</a> </p></li><li><p class="post__p"><a href="/blog/" >My blog posts</a></p></li><li><p class="post__p"><a href="/talks/" >Conference and meetup talks</a></p></li></ul><blockquote class="post__blockquote"><p class="post__p">Salma is a builder. <b class="post__p--bold">You can&#39;t help but get excited about dev when she&#39;s the one teaching</b>. She&#39;s one of the first people I direct companies to when they&#39;re looking to make a better connection to web devs. — Jason Lengstorf, <a href="https://www.learnwithjason.dev/" target="_blank">Learn with Jason</a></p></blockquote><h2 class="post__h2">I get people excited about coding</h2><p class="post__p">At the heart of everything I do is a desire to make people feel good through having fun. Whether it’s through helping you learn a new technical concept, inspiring you to explore cutting edge technology, or by building a silly website to make you laugh — I’ll make you remember me. And when I work for you, <b class="post__p--bold">people will get excited by your product</b>.</p><p class="post__p">Some examples of how much fun you can have with code:</p><ul><li><p class="post__p">A <a href="https://randomcodegeneratorlol.netlify.app" target="_blank">random code generator</a> to learn and teach <a href="https://whitep4nth3r.com/blog/how-to-build-test-and-release-node-module-es6/">How to build, test and release a node module in ES6</a> (and if you google <code>build npm module es6</code> you&#39;ll see my blog post as the top result!)</p></li><li><p class="post__p">An <a href="https://gameofcodesparty.netlify.app/" target="_blank">HTTP status code quiz</a> to show what you can do without a front end framework</p></li><li><p class="post__p">A <a href="https://whitep4nth3r-live.netlify.app/" target="_blank">90s-inspired website</a> to show what you can do with CSS and a single HTML file</p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=HvD8M2uk6gY" target="_blank">I used ChatGPT to try and create websites</a> without writing any code</p></li></ul><p class="post__p"><a href="https://twitch.tv/whitep4nth3r" target="_blank">I love to showcase all of this as a partnered streamer on Twitch</a> to an engaged audience of over 9k followers, complemented by an <a href="/discord" >active Discord server</a>. At the time of writing, I&#39;m ranked in the top 0.68% of streamers, despite live coding on a platform that&#39;s primarily targeted at gamers.</p><blockquote class="post__blockquote"><p class="post__p"><b class="post__p--bold">Salma’s energy is contagious</b> and it shows through her passion teaching developers through engaging content. Highly recommend Salma to anyone looking to get an actionable and engaging story in front of developers. — Brian Douglas, <a href="https://opensauced.pizza/" target="_blank">OpenSauced</a></p></blockquote><h2 class="post__h2">Contact me</h2><p class="post__p">I&#39;m keeping an open mind about the type of company and technology space I&#39;d like to work with next.</p><p class="post__p"><a href="/about/" >Learn more about me on my about page</a>, and email me at <a href="mailto:whitep4nth3r@gmail.com" >whitep4nth3r@gmail.com</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I deploy my website using my Apple Watch</title>
          <description>TL:DR; a serverless function and build hook on Netlify, and an Apple Shortcut. </description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/deploy-website-using-apple-watch/</link>
          <guid>https://whitep4nth3r.com/blog/deploy-website-using-apple-watch/</guid>
          <pubDate>Sun, 25 Jun 2023 23:00:00 GMT</pubDate>
          <category>Serverless</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">The TL;DR is that I can deploy my website using my voice on any of my Apple devices by asking Siri to run a shortcut. And I feel like a <b class="post__p--bold"><b class="post__p--italic">genius</b></b> when I do it. Here’s a demo that I did whilst live streaming on Twitch:</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/hx24i-fuHek"
        title="Demo: deploying my website using my Apple Watch"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"><b class="post__p--italic"><b class="post__p--bold">But Salma, why on earth would you do this?</b></b></p><p class="post__p">My website is a static site built with <a href="https://www.11ty.dev/docs/" target="_blank">Eleventy</a>, and I used to redeploy my website manually (🫠) each time I went online or offline on Twitch, so that my homepage would update with current or previous stream information. Shortly after, I automated the redeployment using a <a href="https://docs.netlify.com/configure-builds/build-hooks/" target="_blank">build hook on Netlify</a> triggered by my Twitch bot, and just for fun, <a href="https://whitep4nth3r.com/blog/how-to-deploy-your-netlify-site-with-an-elgato-stream-deck/" target="_blank">I added a button to my Elgato Stream Deck to redeploy my website whenever I needed to</a>. And when I discovered that you could trigger any GET request via Apple Shortcuts, my party trick was born. Here’s how it’s done.</p><h2 class="post__h2">Create a new project — or use an existing one</h2><p class="post__p">For the next steps in this guide, you’ll need an existing project on Netlify that you want to redeploy via this ✨ magic ✨. This tutorial also assumes you’re comfortable with basic JavaScript and git version control (and you’ve connected your project on Netlify via git), and you use an <a href="https://support.apple.com/en-gb/guide/shortcuts/welcome/ios" target="_blank">Apple device with the Shortcuts app</a>.</p><p class="post__p">If you’re new to Netlify, check out the <a href="https://docs.netlify.com/get-started/" target="_blank">Getting Started with Netlify</a> guide, which will help you learn how to deploy a demo project on Netlify to make it available on the web. It will also introduce some of Netlify’s key features including serverless functions and environment variables — which you’ll use in this tutorial.</p><p class="post__p">Once you’ve got a site live on Netlify, you’re ready to go!</p><h2 class="post__h2">Create a build hook</h2><p class="post__p">Build hooks give you a unique URL you can use to trigger a new site build on Netlify with an HTTP POST request. Select your site in Netlify, click on <b class="post__p--bold">Site configuration</b>, <b class="post__p--bold">Build &amp; deploy</b>, and scroll down to <b class="post__p--bold">Build hooks</b>. Click on <b class="post__p--bold">Add build hook,</b> choose a name for your build hook (such as <code>Function deploy</code>), and click <b class="post__p--bold">Save</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/51A5Y2AdHRF7mXuKh0q0Vm/0ace905350bc1afbea86829f65481832/build_hooks_function_deploy.png" alt="Adding a new build hook on Netlify with the name Function deploy, building the main branch." height="504" width="907" /><p class="post__p">You’ll now see your build hook in the list. Copy this value as we&#39;ll need it in the next step.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2HulJwoj7ENvah2dn9MyhU/6f05f6edfa70640e83b5d3cc62bd3e4a/build_hooks_made.png" alt="Build hooks list in Netlify showing one build hook named function deploy. " height="350" width="907" /><h2 class="post__h2">Add environment variables for security</h2><p class="post__p">To prevent unauthorised redeployments of your site caused by bots and crawlers, or anyone who might view your code online, we’re going to store and use two environment variables for your site: the build hook value, and a secret query parameter that the serverless function will check for before it triggers a deploy.</p><p class="post__p">In the site configuration area in Netlify, click on <b class="post__p--bold">Environment variables</b>. Click on <b class="post__p--bold">Add variable</b>, <b class="post__p--bold">Add a single variable</b>, add <code>BUILD_HOOK_URL</code> as the key, and your build hook URL that you just created as the value. Click <b class="post__p--bold">Create variable</b> to save it.</p><img src="https://images.ctfassets.net/56dzm01z6lln/ixw7VRRA7RjR8ETr3gaNX/d2ba97584a31554b53cc087ddd11e108/build_hook_env_var.png" alt="Netlify UI showing adding a new environment variable with the name BUILD_HOOK_URL and the value of the build hook configured earlier." height="637" width="907" /><p class="post__p">Next, add another environment variable. This will be the secret URL parameter that we’ll check for in the serverless function. Name it <code>BUILD_HOOK_SECRET</code> and give it a value. This value can be anything you choose, but to remain non-guessable it should be similar to a secure password. You can use a random string generator, or make one yourself.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1guTWlVOUKNaAqHk9FA7hj/f7e5e7fccaf05646c414dddca47e6423/build_hook_secret.png" alt="Add a new environment variable interface on netlify with the name BUILD_HOOK_SECRET and an example value of make_this_really_secure." height="637" width="907" /><h2 class="post__h2">Create a serverless function</h2><p class="post__p"><a href="https://docs.netlify.com/functions/overview/" target="_blank">Serverless functions on Netlify</a> are automatically detected and deployed with your site when you add JavaScript files to a <code>netlify/functions</code> directory. Open up your project in your IDE of choice. If you’re not already using serverless functions, add a <code>netlify</code> directory to the root of your project, and inside that, a <code>functions</code> directory. Inside that, add a new file named <code>deployme.js</code>.</p><p class="post__p">When deployed, this function file will automatically be available to make a GET request to at <code>https://yoursite.netlify.app/.netlify/functions/deployme</code>. Add the following code to <code>deployme.js</code> and let’s walk it through.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="zmRvNjiNFx"
      aria-describedby="zmRvNjiNFx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="zmRvNjiNFx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="zmRvNjiNFx" itemprop="text" content="%2F%2F%20%2Fnetlify%2Ffunctions%2Fdeployme.js%0A%0Aexports.handler%20%3D%20async%20function%20(event%2C%20context)%20%7B%0A%20%20if%20(event.queryStringParameters.secret%20%3D%3D%3D%20process.env.BUILD_HOOK_SECRET)%20%7B%0A%20%20%20%20const%20response%20%3D%20await%20fetch(process.env.BUILD_HOOK_URL%2C%20%7B%0A%20%20%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20statusCode%3A%20200%2C%0A%20%20%20%20%20%20body%3A%20%22Site%20is%20deploying!%22%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20statusCode%3A%20403%2C%0A%20%20%20%20%20%20body%3A%20%22Access%20denied!%20Please%20include%20the%20correct%20secret%20URL%20parameter.%22%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /netlify/functions/deployme.js</span><br><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>queryStringParameters<span class="token punctuation">.</span>secret <span class="token operator">===</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">BUILD_HOOK_SECRET</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">BUILD_HOOK_URL</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">return</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">"Site is deploying!"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">403</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">"Access denied! Please include the correct secret URL parameter."</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">To redeploy a site using the build hook we set up earlier, we’ll make a GET request to this serverless function URL (i.e. open it in a web browser) that includes the <code>BUILD_HOOK_SECRET</code> as a <code>secret</code> parameter on the URL, for example: <code>https://yoursite.netlify.app/.netlify/functions/deployme?secret=make_this_really_secure</code>.</p><p class="post__p">The first line of the function checks for this secret on the URL. If the secret isn’t found or doesn’t match, we return an HTTP 403 status code (forbidden), and do nothing. If the correct secret is found, we make a POST request to the <code>BUILD_HOOK_URL</code> using <code>fetch</code>, return an HTTP 200 status code (ok), and send a success message in the response.</p><p class="post__p"><b class="post__p--italic">Note: </b><a href="https://docs.netlify.com/functions/optional-configuration/#node-js-version-for-runtime" target="_blank"><b class="post__p--italic">Node 18 is now the default Node version for all sites on Netlify</b></a><b class="post__p--italic"> when you have not specified an alternative. If you’re using a Node version prior to 18, you’ll need to install node-fetch in your project, and import it at the top of this file. Instructions are in this previous post: </b><a href="https://whitep4nth3r.com/blog/how-to-deploy-your-netlify-site-with-an-elgato-stream-deck/#create-the-serverless-function" target="_blank"><b class="post__p--italic">How to deploy your Netlify site with an Elgato Stream Deck</b></a><b class="post__p--italic">.</b></p><p class="post__p">Next, push up the new code to Netlify via git. View your deploy logs to see Netlify automatically detect and build your new serverless function!</p><img src="https://images.ctfassets.net/56dzm01z6lln/3QfKQpSWbMGWz91igukvUA/bc99baffe5244b0e7471c2ab91fca6a0/deploy_log_new_function.png" alt="Netlify deploy log showing functions bundling, packaging functions from netlify functions directory, deployme.js." height="426" width="1213" /><h2 class="post__h2">Test the endpoint in your browser</h2><p class="post__p">Let’s check it works! When the deployment is complete, visit the new serverless function endpoint in your browser with the secret URL parameter (e.g. <code>https://yoursite.netlify.app/.netlify/functions/deployme?secret=make_this_really_secure</code>), and you’ll see your success message. Go back to your Netlify site deploy list, and you’ll see that a new build has started! You’ll also notice that the name of the build hook you created earlier will be displayed as the deploy trigger.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4VgtP5Rlmt1SoMnIK7UCUN/800daafd4afa59e55f6af32107f6fb3d/function_deploy_trigger_in_deploy_list.png" alt="An entry in the Netlify deploy list showing the main branch was built, triggered by hook: function deploy." height="210" width="754" /><p class="post__p">You&#39;re now ready to set up an Apple Shortcut to redeploy your website!</p><h2 class="post__h2">Add an Apple Shortcut</h2><p class="post__p">Open the Shortcuts app on your Apple device of choice, and click the plus button to add a new shortcut. Give the shortcut a name, and search for <code>Expand URL</code> in the search field. Add that action to the shortcut, and give it the value of your serverless function URL, complete with the secret URL parameter.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6rE8xb5Twl7b7yfQfNl6Yk/d2380f9c1e902b8beb9fcb19472efac9/new_shortcut_expand_url.png" alt="Apple shortcut interface showing a new Expand URL action has been added to the new deploy website shortcut, populated with the value of the deploy me URL and the secret URL parameter." height="512" width="862" /><p class="post__p">You can now click this shortcut button, or use Spotlight to redeploy your website, too! </p><img src="https://images.ctfassets.net/56dzm01z6lln/7bLRcj4KpwCyEbnaq9riGG/a430f3dbc0ceb78fba68bddf5f9e034b/shortcut_tile.png" alt="Apple shortcuts window showing the deploy website tile. You can click this to run it." height="1024" width="1724" /><p class="post__p">Next, make sure that iCloud sync is on for your shortcuts and give it a little while to sync. 🤞🏼</p><img src="https://images.ctfassets.net/56dzm01z6lln/4acopV7wn0fLtVXEzOi84v/67c98e48b50142eae2ca0e723f3ac53a/sync_icloud_shortcuts.png" alt="Apple shortcut settings window, showing iCloud sync checked." height="642" width="864" /><h2 class="post__h2">Deploy your website with your voice!</h2><p class="post__p">When your shortcuts have synced, you’re ready to wow everyone with your party trick! Ask your Apple Watch, iPhone, HomePod — or anywhere where Siri is enabled — by saying “<b class="post__p--bold">Hey Siri! Shortcut: Deploy website</b>,” and watch in awe and amazement as your site is redeployed to Netlify! And you, too, can feel like a <b class="post__p--italic"><b class="post__p--bold">genius</b></b>.</p><p class="post__p"><b class="post__p--italic"><b class="post__p--bold">Note: I edited out the “shortcut” word from the demo video to prevent my website deploying over and over again when I watched the video without headphones!</b></b></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>The best light/dark mode theme toggle in JavaScript</title>
          <description>Learn how to build The Ultimate Theme Toggle™️ for your website using JavaScript, CSS custom properties, local storage and system settings.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/best-light-dark-mode-theme-toggle-javascript/</link>
          <guid>https://whitep4nth3r.com/blog/best-light-dark-mode-theme-toggle-javascript/</guid>
          <pubDate>Mon, 19 Jun 2023 23:00:00 GMT</pubDate>
          <category>CSS</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I used to disagree with light and dark mode toggles. “The toggle is the user system preferences!” I would exclaim naïvely, opting to let the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" target="_blank">prefers-color-scheme CSS media query</a> control the theming on my personal website. No toggle. No choice. 🫠</p><p class="post__p">I’ve been a dark mode user ever since it became a thing. But recently, I’ve preferred to use <b class="post__p--italic">some</b> websites and tools in light mode — including my personal website — whilst keeping my system settings firmly in the dark. I needed a toggle. I needed a choice! And so does everyone else.</p><p class="post__p">In this post I’ll show you how I built The Ultimate Theme Toggle™️ for my website in JavaScript that:</p><ol><li><p class="post__p">Stores and retrieves a theme preference in local browser storage,</p></li><li><p class="post__p">Falls back to user system preferences,</p></li><li><p class="post__p">Falls back to a default theme if none of the above are detected.</p></li></ol><p class="post__p">TL;DR: <a href="https://codepen.io/whitep4nth3r/pen/VwEqrQL" target="_blank">here’s the code on CodePen</a>.</p><h2 class="post__h2">Add a data attribute to your HTML tag</h2><p class="post__p">On your HTML tag, add a data attribute such as <code>data-theme</code> and give it a default value of light or dark. In the past I’ve used the custom attribute <code>color-mode</code> instead of a data attribute (e.g. <code>color-mode=&quot;light&quot;</code>). While this works, it’s not classed as valid HTML and I can’t find any documentation on it! Any insight on this is much appreciated. 😅</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="iwJfYcdBcC"
      aria-describedby="iwJfYcdBcC">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="iwJfYcdBcC">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="iwJfYcdBcC" itemprop="text" content="%3Chtml%20lang%3D%22en%22%20data-theme%3D%22light%22%3E%0A%09%3C!--%20all%20other%20HTML%20--%3E%0A%3C%2Fhtml%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span> <span class="token attr-name">data-theme</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>light<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>	<span class="token comment">&lt;!-- all other HTML --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Configure theming via CSS custom properties</h2><p class="post__p">In your CSS, configure your theme colours via <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties" target="_blank">CSS custom properties</a> (or variables) under each value for the <code>data-theme</code> attribute. Note that you don’t necessarily need to use <code>:root</code> in combination with <code>data-theme</code>, but it’s useful for global properties that don’t change with the theme (shown in the example below). <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:root" target="_blank">Learn more about the :root CSS pseudo-class on MDN</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xLjvBPylmw"
      aria-describedby="xLjvBPylmw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xLjvBPylmw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="xLjvBPylmw" itemprop="text" content="%3Aroot%20%7B%0A%20%20--grid-unit%3A%201rem%3B%0A%20%20--border-radius-base%3A%200.5rem%3B%0A%7D%0A%0A%5Bdata-theme%3D%22light%22%5D%20%7B%0A%20%20--color-bg%3A%20%23ffffff%3B%0A%20%20--color-fg%3A%20%23000000%3B%0A%7D%0A%0A%5Bdata-theme%3D%22dark%22%5D%20%7B%0A%20%20--color-bg%3A%20%23000000%3B%0A%20%20--color-fg%3A%20%23ffffff%3B%0A%7D%0A%0A%2F*%20example%20use%20of%20CSS%20custom%20properties%20*%2F%0Abody%20%7B%0A%20%20background-color%3A%20var(--color-bg)%3B%0A%20%20color%3A%20var(--color-fg)%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span><br>  <span class="token property">--grid-unit</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><br>  <span class="token property">--border-radius-base</span><span class="token punctuation">:</span> 0.5rem<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">[data-theme="light"]</span> <span class="token punctuation">{</span><br>  <span class="token property">--color-bg</span><span class="token punctuation">:</span> #ffffff<span class="token punctuation">;</span><br>  <span class="token property">--color-fg</span><span class="token punctuation">:</span> #000000<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">[data-theme="dark"]</span> <span class="token punctuation">{</span><br>  <span class="token property">--color-bg</span><span class="token punctuation">:</span> #000000<span class="token punctuation">;</span><br>  <span class="token property">--color-fg</span><span class="token punctuation">:</span> #ffffff<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">/* example use of CSS custom properties */</span><br><span class="token selector">body</span> <span class="token punctuation">{</span><br>  <span class="token property">background-color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-bg<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-fg<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Switch the <code>data-theme</code> attribute manually on your HTML tag, and you’ll see your theme change already (as long as you’re using those CSS properties to style your elements)!</p><h2 class="post__h2">Build a toggle button in HTML</h2><p class="post__p">Add an HTML button to your website header, or wherever you need the theme toggle. Add a <code>data-theme-toggle</code> attribute (we’ll use this to target the button in JavaScript later), and an <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label" target="_blank">aria-label</a> if you’re planning to use icons on your button (such as a sun and moon to represent light and dark mode respectively) so that screen readers and assistive technology can understand the purpose of the interactive button.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="zPsPrFgxoV"
      aria-describedby="zPsPrFgxoV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="zPsPrFgxoV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="zPsPrFgxoV" itemprop="text" content="%3Cbutton%0A%20%20%20%20type%3D%22button%22%0A%20%20%20%20data-theme-toggle%0A%20%20%20%20aria-label%3D%22Change%20to%20light%20theme%22%0A%20%20%3EChange%20to%20light%20theme%20(or%20icon%20here)%3C%2Fbutton%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>button</span><br>    <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>button<span class="token punctuation">"</span></span><br>    <span class="token attr-name">data-theme-toggle</span><br>    <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Change to light theme<span class="token punctuation">"</span></span><br>  <span class="token punctuation">></span></span>Change to light theme (or icon here)<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>button</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Calculate theme setting on page load</h2><p class="post__p">Here’s where we’ll calculate the theme setting based on what I call the “<b class="post__p--italic">preference cascade</b>”.</p><h3 class="post__h3">Get theme preference from local storage</h3><p class="post__p">We can use <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" target="_blank">the localStorage property in JavaScript</a> to save user preferences in a browser that persist between sessions (or until it is manually cleared). In The Ultimate Theme Toggle™️, the stored user preference is the most important setting, so we’ll look for that first.</p><p class="post__p">On page load, use <code>localStorage.getItem(&quot;theme&quot;)</code> to check for a previously stored preference. Later in the post, we’ll update the theme value each time the toggle is pressed. If there’s no local storage value, the value will be <code>null</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="kOWGMNQITa"
      aria-describedby="kOWGMNQITa">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="kOWGMNQITa">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="kOWGMNQITa" itemprop="text" content="%2F%2F%20get%20theme%20on%20page%20load%0AlocalStorage.getItem(%22theme%22)%3B%0A%0A%2F%2F%20set%20theme%20on%20button%20press%0AlocalStorage.setItem(%22theme%22%2C%20newTheme)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// get theme on page load</span><br>localStorage<span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token string">"theme"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token comment">// set theme on button press</span><br>localStorage<span class="token punctuation">.</span><span class="token function">setItem</span><span class="token punctuation">(</span><span class="token string">"theme"</span><span class="token punctuation">,</span> newTheme<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Detect user system settings in JavaScript</h3><p class="post__p">If there’s no stored theme preference in <code>localStorage</code>, we’ll detect the user’s system settings using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia" target="_blank">window.matchMedia() method</a> by passing in a media query string. You’ll only need to calculate one setting for the purposes of the preference cascade, but the code below shows how you can detect light or dark system settings.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="FHvHkJvlfi"
      aria-describedby="FHvHkJvlfi">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="FHvHkJvlfi">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="FHvHkJvlfi" itemprop="text" content="const%20systemSettingDark%20%3D%20window.matchMedia(%22(prefers-color-scheme%3A%20dark)%22)%3B%0A%2F%2F%20or%0Aconst%20systemSettingLight%20%3D%20window.matchMedia(%22(prefers-color-scheme%3A%20light)%22)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> systemSettingDark <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">"(prefers-color-scheme: dark)"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token comment">// or</span><br><span class="token keyword">const</span> systemSettingLight <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">"(prefers-color-scheme: light)"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p"><code>window.matchMedia()</code> returns a MediaQueryList containing the media query string you requested, and whether it <code>matches</code> (true/false) the user system settings.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="nwBTIPCNgH"
      aria-describedby="nwBTIPCNgH">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="nwBTIPCNgH">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="nwBTIPCNgH" itemprop="text" content="%7B%0A%09matches%3A%20true%2C%0A%09media%3A%20%22(prefers-color-scheme%3A%20dark)%22%2C%0A%09onchange%3A%20null%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token punctuation">{</span><br>	<span class="token literal-property property">matches</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>	<span class="token literal-property property">media</span><span class="token operator">:</span> <span class="token string">"(prefers-color-scheme: dark)"</span><span class="token punctuation">,</span><br>	<span class="token literal-property property">onchange</span><span class="token operator">:</span> <span class="token keyword">null</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Fall back to a default theme</h3><p class="post__p">Now you have access to a <code>localStorage</code> value and system settings via <code>window.matchMedia()</code>, you can calculate the preferred theme setting using the preference cascade (local storage, then system setting), and fall back to a default theme of your choice (which should be the default theme you specified on your HTML tag earlier).</p><p class="post__p">We’ll run this code on page load to calculate the current theme setting.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="PwyqHlAnus"
      aria-describedby="PwyqHlAnus">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="PwyqHlAnus">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="PwyqHlAnus" itemprop="text" content="function%20calculateSettingAsThemeString(%7B%20localStorageTheme%2C%20systemSettingDark%20%7D)%20%7B%0A%20%20if%20(localStorageTheme%20!%3D%3D%20null)%20%7B%0A%20%20%20%20return%20localStorageTheme%3B%0A%20%20%7D%0A%0A%20%20if%20(systemSettingDark.matches)%20%7B%0A%20%20%20%20return%20%22dark%22%3B%0A%20%20%7D%0A%0A%20%20return%20%22light%22%3B%0A%7D%0A%0Aconst%20localStorageTheme%20%3D%20localStorage.getItem(%22theme%22)%3B%0Aconst%20systemSettingDark%20%3D%20window.matchMedia(%22(prefers-color-scheme%3A%20dark)%22)%3B%0A%0Alet%20currentThemeSetting%20%3D%20calculateSettingAsThemeString(%7B%20localStorageTheme%2C%20systemSettingDark%20%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">calculateSettingAsThemeString</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> localStorageTheme<span class="token punctuation">,</span> systemSettingDark <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>localStorageTheme <span class="token operator">!==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> localStorageTheme<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>systemSettingDark<span class="token punctuation">.</span>matches<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token string">"dark"</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">return</span> <span class="token string">"light"</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">const</span> localStorageTheme <span class="token operator">=</span> localStorage<span class="token punctuation">.</span><span class="token function">getItem</span><span class="token punctuation">(</span><span class="token string">"theme"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> systemSettingDark <span class="token operator">=</span> window<span class="token punctuation">.</span><span class="token function">matchMedia</span><span class="token punctuation">(</span><span class="token string">"(prefers-color-scheme: dark)"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">let</span> currentThemeSetting <span class="token operator">=</span> <span class="token function">calculateSettingAsThemeString</span><span class="token punctuation">(</span><span class="token punctuation">{</span> localStorageTheme<span class="token punctuation">,</span> systemSettingDark <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Add an event listener to the toggle button</h2><p class="post__p">Next, we’ll set up an event listener to switch the theme when the button is pressed. Target the button in the DOM using the data attribute (<code>data-theme-toggle</code>) we added earlier, and add an <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener" target="_blank">event listener</a> to the button on click. The example below is quite verbose, and you might want to abstract out some of the functionality below into utility functions (which I’ve done in <a href="https://codepen.io/whitep4nth3r/pen/VwEqrQL?editors=1112" target="_blank">the example on CodePen</a>). Let’s walk this through:</p><ol><li><p class="post__p">Calculate the new theme as a string</p></li><li><p class="post__p">Calculate and update the button text (if you&#39;re using icons on your button, here&#39;s where you&#39;ll make the switch)</p></li><li><p class="post__p">Update the <code>aria-label</code> on the button</p></li><li><p class="post__p">Switch the <code>data-theme</code> attribute on the HTML tag</p></li><li><p class="post__p">Save the new theme preference in local storage</p></li><li><p class="post__p">Update the <code>currentThemeSetting</code> in memory</p></li></ol>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="OZoNYhCeNV"
      aria-describedby="OZoNYhCeNV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="OZoNYhCeNV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="OZoNYhCeNV" itemprop="text" content="%2F%2F%20target%20the%20button%20using%20the%20data%20attribute%20we%20added%20earlier%0Aconst%20button%20%3D%20document.querySelector(%22%5Bdata-theme-toggle%5D%22)%3B%0A%0Abutton.addEventListener(%22click%22%2C%20()%20%3D%3E%20%7B%0A%20%20const%20newTheme%20%3D%20currentThemeSetting%20%3D%3D%3D%20%22dark%22%20%3F%20%22light%22%20%3A%20%22dark%22%3B%0A%0A%20%20%2F%2F%20update%20the%20button%20text%0A%20%20const%20newCta%20%3D%20newTheme%20%3D%3D%3D%20%22dark%22%20%3F%20%22Change%20to%20light%20theme%22%20%3A%20%22Change%20to%20dark%20theme%22%3B%0A%20%20button.innerText%20%3D%20newCta%3B%20%20%0A%0A%20%20%2F%2F%20use%20an%20aria-label%20if%20you%20are%20omitting%20text%20on%20the%20button%0A%20%20%2F%2F%20and%20using%20sun%2Fmoon%20icons%2C%20for%20example%0A%20%20button.setAttribute(%22aria-label%22%2C%20newCta)%3B%0A%0A%20%20%2F%2F%20update%20theme%20attribute%20on%20HTML%20to%20switch%20theme%20in%20CSS%0A%20%20document.querySelector(%22html%22).setAttribute(%22data-theme%22%2C%20newTheme)%3B%0A%0A%20%20%2F%2F%20update%20in%20local%20storage%0A%20%20localStorage.setItem(%22theme%22%2C%20newTheme)%3B%0A%0A%20%20%2F%2F%20update%20the%20currentThemeSetting%20in%20memory%0A%20%20currentThemeSetting%20%3D%20newTheme%3B%0A%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// target the button using the data attribute we added earlier</span><br><span class="token keyword">const</span> button <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-theme-toggle]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>button<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"click"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> newTheme <span class="token operator">=</span> currentThemeSetting <span class="token operator">===</span> <span class="token string">"dark"</span> <span class="token operator">?</span> <span class="token string">"light"</span> <span class="token operator">:</span> <span class="token string">"dark"</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// update the button text</span><br>  <span class="token keyword">const</span> newCta <span class="token operator">=</span> newTheme <span class="token operator">===</span> <span class="token string">"dark"</span> <span class="token operator">?</span> <span class="token string">"Change to light theme"</span> <span class="token operator">:</span> <span class="token string">"Change to dark theme"</span><span class="token punctuation">;</span><br>  button<span class="token punctuation">.</span>innerText <span class="token operator">=</span> newCta<span class="token punctuation">;</span>  <br><br>  <span class="token comment">// use an aria-label if you are omitting text on the button</span><br>  <span class="token comment">// and using sun/moon icons, for example</span><br>  button<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"aria-label"</span><span class="token punctuation">,</span> newCta<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// update theme attribute on HTML to switch theme in CSS</span><br>  document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"html"</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"data-theme"</span><span class="token punctuation">,</span> newTheme<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// update in local storage</span><br>  localStorage<span class="token punctuation">.</span><span class="token function">setItem</span><span class="token punctuation">(</span><span class="token string">"theme"</span><span class="token punctuation">,</span> newTheme<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// update the currentThemeSetting in memory</span><br>  currentThemeSetting <span class="token operator">=</span> newTheme<span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">To confirm <code>localStorage</code> is being updated, open up your dev tools, navigate to the <code>Application</code> tab, expand <code>Local Storage</code> and select your site. You’ll see a key:value list; look for <code>theme</code> and click the button to watch it update in real time. Reload your page and you’ll see the theme preference preserved!</p><img src="https://images.ctfassets.net/56dzm01z6lln/2movXuGGngEbC5PdAIfiXL/970173b9b57bcd1bfac33f7fdc1ebac2/local_storage.png" alt="Browser window with dev tools open on application tab. Local storage on whitepanther dot com is selected, showing a key value pair stored in the browser of theme light." height="976" width="1255" /><h2 class="post__h2">Put it all together!</h2><p class="post__p">You can now build your very own Ultimate Theme Toggle™️ by:</p><ol><li><p class="post__p">Using CSS custom properties to specify different theme colours, switched via a data attribute on your HTML tag</p></li><li><p class="post__p">Using an HTML button to power the toggle</p></li><li><p class="post__p">Calculating the preferred theme on page load by using the preference cascade (local storage &gt; system settings &gt; fallback default theme)</p></li><li><p class="post__p">Switching the theme on click of the toggle button, and storing the user preference in the browser for future visits</p></li></ol><p class="post__p">Here’s the full CodePen, and you can check out the working version on <a href="https://whitep4nth3r.com" target="_blank">my personal website</a>. Happy toggling!</p>
    <div class="codePenEmbed__container" data-codepen data-embed-code=%3Cp%20class=%22codepen%22%20data-height=%22300%22%20data-default-tab=%22js,result%22%20data-slug-hash=%22VwEqrQL%22%20data-editable=%22true%22%20data-user=%22whitep4nth3r%22%20style=%22height:%20300px;%20box-sizing:%20border-box;%20display:%20flex;%20align-items:%20center;%20justify-content:%20center;%20border:%202px%20solid;%20margin:%201em%200;%20padding:%201em;%22%3E%0A%20%20%3Cspan%3ESee%20the%20Pen%20%3Ca%20href=%22https://codepen.io/whitep4nth3r/pen/VwEqrQL%22%3E%0A%20%20The%20Ultimate%20Theme%20Toggle%E2%84%A2%EF%B8%8F%3C/a%3E%20by%20whitep4nth3r%20(%3Ca%20href=%22https://codepen.io/whitep4nth3r%22%3E@whitep4nth3r%3C/a%3E)%0A%20%20on%20%3Ca%20href=%22https://codepen.io%22%3ECodePen%3C/a%3E.%3C/span%3E%0A%3C/p%3E>
      <div data-target></div>
    </div>

    <script>
      const codepen = document.querySelector("[data-codepen]");
      let loaded = false;
      const options = {
        root: null,
        threshold: 0.1
      }

      const loadCodePen = (entries, observer) => {
        entries.forEach(entry => {
          
          if(!loaded && entry.isIntersecting) {
            const target = entry.target.querySelector("[data-target]");
            const embedCode = entry.target.dataset.embedCode;

            target.innerHTML = decodeURI(embedCode);
            const script = document.createElement("script");
            script.src = "https://cpwebassets.codepen.io/assets/embed/ei.js";
            document.head.append(script);
            loaded = true;
          }
        });
      };

      const observer = new IntersectionObserver(loadCodePen, options);
      observer.observe(codepen);
    </script>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>A/B test CMS authored content with Netlify Edge Functions</title>
          <description>Learn how to run an A/B test using content from your CMS and Netlify Edge Functions.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/a-b-test-cms-authored-content-netlify-edge-functions/</link>
          <guid>https://www.netlify.com/blog/a-b-test-cms-authored-content-netlify-edge-functions/</guid>
          <pubDate>Thu, 18 May 2023 23:00:00 GMT</pubDate>
          <category>Web Dev</category><category>Tutorials</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In a previous post, I demonstrated <a href="https://www.netlify.com/blog/how-to-split-traffic-a-b-test-page-layouts-same-url/#track-your-test-variants-in-your-analytics-tool" target="_blank">how to A/B test different page layouts on the same user-facing URL using Netlify Edge Functions</a>. But what if you want to run an A/B test on a more granular level, such as changing the wording of a hero banner, without creating new page layouts and rewriting to different URLs, and ensuring your authors stay in control of the content?</p><h2 class="post__h2">Split test content controlled by your editors</h2><p class="post__p">While it’s possible to use edge functions to update page content for an A/B test by manually rewriting content in code, in larger organizations with multiple teams this could be more difficult to scale. Many Content Management System (CMS) providers offer the ability to store content in multiple areas — or spaces — of your CMS, which you can access with different API keys. Different spaces may be used for preparing content for a feature branch release, localization, A/B tests, and more. By providing a dedicated testing ground in your CMS, developers can focus on writing the code, whilst content authors remain in control of the content.</p><p class="post__p">In this code example, we’ll look at how you can A/B test messaging on a home page hero banner by using content from a separate area of your CMS. The following assumes that:</p><ol><li><p class="post__p">Your home page is statically generated at build time;</p></li><li><p class="post__p">Your site fetches data from a CMS at build time;</p></li><li><p class="post__p">Your home page is served at the root of your site (<code>/</code>);</p></li><li><p class="post__p">Your CMS has the capability to store content in separate areas or spaces that can be fetched with different API keys and access tokens;</p></li><li><p class="post__p">You have configured a “test” space in your CMS to deliver content in the same way as your production space;</p></li><li><p class="post__p">Your site is hosted on Netlify.</p></li></ol><h3 class="post__h3">Assign users to test buckets using browser cookies</h3><p class="post__p">At the root of your project, create a <code>netlify</code> directory if you don’t already have one, and inside that, create an <code>edge-functions</code> directory. Inside that, add a new file called <code>abtest-homepage-hero.ts</code>. You can write Edge Functions in JavaScript or TypeScript; in this example we’ll be using TypeScript.</p><p class="post__p">Add the following code, in which an edge function and config object is exported, configured to run on the home page only (<code>/</code>), and a test bucket cookie named <code>home_page_hero</code> is found or set. For a more in-depth explanation, the setup is described in more detail in the previous post: <a href="https://www.netlify.com/blog/how-to-split-traffic-a-b-test-page-layouts-same-url/" target="_blank">How to split traffic and A/B test different page layouts on the same URL</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="hoSfcUuYPp"
      aria-describedby="hoSfcUuYPp">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="hoSfcUuYPp">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="hoSfcUuYPp" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Fabtest-homepage-hero.ts%0A%0Aimport%20type%20%7B%20Context%2C%20Config%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0Aexport%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%20%20%2F%2F%20look%20for%20existing%20%22home_page_hero%22%20cookie%0A%20%20const%20bucketName%20%3D%20%22home_page_hero%22%3B%0A%20%20const%20bucket%20%3D%20context.cookies.get(bucketName)%3B%0A%0A%20%20%2F%2F%20return%20here%20if%20we%20find%20a%20cookie%0A%20%20if%20(bucket)%20%7B%0A%20%20%20%2F%2F...%0A%20%20%7D%0A%0A%20%20%2F%2F%20if%20no%20%22home_page_hero%22%20cookie%20is%20found%2C%20%0A%20%20%2F%2F%20assign%20the%20user%20to%20a%20bucket%0A%20%20%2F%2F%20in%20this%20example%20we're%20using%20two%20buckets%20(default%2C%20new)%20%0A%20%20%2F%2F%20with%20an%20equal%20weighting%20of%2050%2F50%0A%20%20const%20weighting%20%3D%200.5%3B%0A%20%20%2F%2F%20get%20a%20random%20number%20between%20(0-1)%0A%20%20const%20random%20%3D%20Math.random()%3B%0A%20%20const%20newBucketValue%20%3D%20random%20%3C%3D%20weighting%20%3F%20%22default%22%20%3A%20%22new%22%3B%0A%0A%20%20%2F%2F%20set%20the%20new%20%22home_page_hero%22%20cookie%0A%20%20context.cookies.set(%7B%0A%20%20%20%20name%3A%20bucketName%2C%0A%20%20%20%20value%3A%20newBucketValue%2C%0A%20%20%7D)%3B%0A%0A%20%20%2F%2F%20...%0A%7D%0A%0A%2F%2F%20this%20edge%20function%20will%20run%20on%20the%20home%20page%0Aexport%20const%20config%3A%20Config%20%3D%20%7B%0A%20%20path%3A%20%22%2F%22%2C%0A%7D%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// netlify/edge-functions/abtest-homepage-hero.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> Context<span class="token punctuation">,</span> Config <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token comment">// look for existing "home_page_hero" cookie</span><br>  <span class="token keyword">const</span> bucketName <span class="token operator">=</span> <span class="token string">"home_page_hero"</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> bucket <span class="token operator">=</span> context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>bucketName<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// return here if we find a cookie</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>bucket<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>   <span class="token comment">//...</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token comment">// if no "home_page_hero" cookie is found, </span><br>  <span class="token comment">// assign the user to a bucket</span><br>  <span class="token comment">// in this example we're using two buckets (default, new) </span><br>  <span class="token comment">// with an equal weighting of 50/50</span><br>  <span class="token keyword">const</span> weighting <span class="token operator">=</span> <span class="token number">0.5</span><span class="token punctuation">;</span><br>  <span class="token comment">// get a random number between (0-1)</span><br>  <span class="token keyword">const</span> random <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> newBucketValue <span class="token operator">=</span> random <span class="token operator">&lt;=</span> weighting <span class="token operator">?</span> <span class="token string">"default"</span> <span class="token operator">:</span> <span class="token string">"new"</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// set the new "home_page_hero" cookie</span><br>  context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    name<span class="token operator">:</span> bucketName<span class="token punctuation">,</span><br>    value<span class="token operator">:</span> newBucketValue<span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// ...</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// this edge function will run on the home page</span><br><span class="token keyword">export</span> <span class="token keyword">const</span> config<span class="token operator">:</span> Config <span class="token operator">=</span> <span class="token punctuation">{</span><br>  path<span class="token operator">:</span> <span class="token string">"/"</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Fetch data from your CMS for the test bucket at runtime</h3><p class="post__p">If you’re already fetching data from a CMS at build time to pre-generate your site pages, you’ll be familiar with using environment variables. Add the necessary environment variables for your test CMS space to allow you to fetch alternative content for the A/B test. The example below shows four environment variables, two for production and two for the test space.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3LRLbH3JbPf6WFZKVbA5bk/71bc149407617da6c361f47220cdaad5/env_vars_cms_abtest.png" alt="Netlify environment variable dashboard showing the four environment variables described" height="923" width="1529" /><p class="post__p">Netlify Edge Functions have access to the <a href="https://docs.netlify.com/edge-functions/api/#netlify-global-object" target="_blank">Netlify global object</a>, which you can use to manage environment variables at runtime. The code below demonstrates how to get two environment variables required to access data in the test environment, and provides boilerplate for how you might fetch that test content. Your method will vary depending on the CMS you are using.</p><p class="post__p">Additionally, the code now demonstrates how to return the unmodified HTTP response should the value of the <code>home_page_hero</code> cookie be <code>default</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="FNntGnmNWz"
      aria-describedby="FNntGnmNWz">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="FNntGnmNWz">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="FNntGnmNWz" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Fabtest-homepage-hero.ts%0A%0A%20%20import%20type%20%7B%20Context%2C%20Config%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0A%20%20export%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%20%20%20%20const%20bucketName%20%3D%20%22home_page_hero%22%3B%0A%20%20%20%20const%20bucket%20%3D%20context.cookies.get(bucketName)%3B%0A%0A%2B%20%20%20%2F%2F%20get%20stored%20environment%20variables%20for%20test%20content%0A%2B%20%20%20const%20CMS_SPACE_ID_TEST%20%3D%20Netlify.env.get(%22CMS_SPACE_ID_TEST%22)%3B%0A%2B%20%20%20const%20CMS_ACCESS_TOKEN_TEST%20%3D%20Netlify.env.get(%22CMS_ACCESS_TOKEN_TEST%22)%3B%0A%0A%20%20%20%20if%20(bucket)%20%7B%0A%2B%20%20%20%20%20if%20(bucket%20%3D%3D%3D%20%22default%22)%20%7B%0A%2B%20%20%20%20%20%20%20%2F%2F%20return%20unmodifed%20HTTP%20response%0A%2B%20%20%20%20%20%20%20return%3B%0A%2B%20%20%20%20%20%7D%0A%0A%2B%20%20%20%20%20%2F%2F%20fetch%20test%20content%20from%20CMS%0A%2B%20%20%20%20%20%2F%2F%20this%20is%20a%20boilerplate%20example%20and%20your%20method%20%0A%2B%20%20%20%20%20%2F%2F%20and%20query%20will%20vary%0A%2B%20%20%20%20%20%2F%2F%20fetch%20only%20the%20content%20you%20need%0A%2B%20%20%20%20%20const%20testContent%20%3D%20await%20fetch(%60https%3A%2F%2Fcms.url%2Fspaces%2F%24%7BCMS_SPACE_ID_TEST%7D%60%2C%20%7B%0A%2B%20%20%20%20%20%20%20headers%3A%20%7B%0A%2B%20%20%20%20%20%20%20%20%20Authorization%3A%20%60Bearer%20%24%7BCMS_ACCESS_TOKEN_TEST%7D%60%2C%0A%2B%20%20%20%20%20%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%2B%20%20%20%20%20%20%20%7D%2C%0A%2B%20%20%20%20%20%7D)%3B%0A%0A%2B%20%20%20%20%20const%20jsonData%20%3D%20await%20testContent.json()%3B%0A%0A%2B%20%20%20%20%20%2F%2F%20...%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20weighting%20%3D%200.5%3B%0A%20%20%20%20const%20random%20%3D%20Math.random()%3B%0A%20%20%20%20const%20newBucketValue%20%3D%20random%20%3C%3D%20weighting%20%3F%20%22default%22%20%3A%20%22test%22%3B%0A%0A%20%20%20%20context.cookies.set(%7B%0A%20%20%20%20%20%20name%3A%20bucketName%2C%0A%20%20%20%20%20%20value%3A%20newBucketValue%2C%0A%20%20%20%20%7D)%3B%0A%0A%2B%20%20%20if%20(newBucketValue%20%3D%3D%3D%20%22default%22)%20%7B%0A%2B%20%20%20%20%20return%3B%0A%2B%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20...%0A%20%20%7D%3B%0A%0A%20%20export%20const%20config%3A%20Config%20%3D%20%7B%0A%20%20%20%20path%3A%20%22%2F%22%2C%0A%20%20%7D%3B">
      <pre class="language-diff-typescript"><code class="language-diff-typescript">// netlify/edge-functions/abtest-homepage-hero.ts<br><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> Context<span class="token punctuation">,</span> Config <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> bucketName <span class="token operator">=</span> <span class="token string">"home_page_hero"</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> bucket <span class="token operator">=</span> context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>bucketName<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>   <span class="token comment">// get stored environment variables for test content</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">const</span> <span class="token constant">CMS_SPACE_ID_TEST</span> <span class="token operator">=</span> Netlify<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"CMS_SPACE_ID_TEST"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">const</span> <span class="token constant">CMS_ACCESS_TOKEN_TEST</span> <span class="token operator">=</span> Netlify<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"CMS_ACCESS_TOKEN_TEST"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>bucket<span class="token punctuation">)</span> <span class="token punctuation">{</span><br></span><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>     <span class="token keyword">if</span> <span class="token punctuation">(</span>bucket <span class="token operator">===</span> <span class="token string">"default"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>       <span class="token comment">// return unmodifed HTTP response</span><br><span class="token prefix inserted">+</span>       <span class="token keyword">return</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>     <span class="token punctuation">}</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>     <span class="token comment">// fetch test content from CMS</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// this is a boilerplate example and your method </span><br><span class="token prefix inserted">+</span>     <span class="token comment">// and query will vary</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// fetch only the content you need</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">const</span> testContent <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://cms.url/spaces/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CMS_SPACE_ID_TEST</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>       headers<span class="token operator">:</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>         Authorization<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CMS_ACCESS_TOKEN_TEST</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>         <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>       <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>     <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>     <span class="token keyword">const</span> jsonData <span class="token operator">=</span> <span class="token keyword">await</span> testContent<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>     <span class="token comment">// ...</span><br></span><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> weighting <span class="token operator">=</span> <span class="token number">0.5</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> random <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> newBucketValue <span class="token operator">=</span> random <span class="token operator">&lt;=</span> weighting <span class="token operator">?</span> <span class="token string">"default"</span> <span class="token operator">:</span> <span class="token string">"test"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>     name<span class="token operator">:</span> bucketName<span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>     value<span class="token operator">:</span> newBucketValue<span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>newBucketValue <span class="token operator">===</span> <span class="token string">"default"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">return</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token comment">// ...</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">const</span> config<span class="token operator">:</span> Config <span class="token operator">=</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   path<span class="token operator">:</span> <span class="token string">"/"</span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Modify the HTTP response using HTMLRewriter</h3><p class="post__p">Now we have the content from the CMS, it’s time to modify the HTTP response if the <code>home_page_hero</code> cookie value is <code>new</code>. In your HTML, add a unique identifier to your existing hero banner headline, or whichever element contains the content you’d like to test. In these types of tests, I prefer to target elements using data attributes rather than CSS classes in order to separate concerns. This example shows the data attribute <code>data-hero-banner-headline</code> added to an H1 element.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="pHnrSvXnfF"
      aria-describedby="pHnrSvXnfF">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="pHnrSvXnfF">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="pHnrSvXnfF" itemprop="text" content="%3C!--%20index.html%20--%3E%0A%0A%3Ch1%20data-hero-banner-headline%3E%0A%20%20The%20original%20unmodified%20headline%20in%20the%20original%20HTTP%20response%0A%3C%2Fh1%3E">
      <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- index.html --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span> <span class="token attr-name">data-hero-banner-headline</span><span class="token punctuation">></span></span><br>  The original unmodified headline in the original HTTP response<br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">There are a number of ways we can modify the HTML response, including by parsing the HTML as a string and finding and replacing using a regular expression. For this example we’re going to use <a href="https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/" target="_blank">Cloudflare’s HTMLRewriter</a> which offers us the ability to target and modify the HTML response with the kind of JavaScript syntax we’re familiar with using in the browser. This is imported at the top of the file.</p><p class="post__p">The additions to the code example below demonstrate:</p><ol><li><p class="post__p">How to grab the next HTTP response in the chain in order to be modified;</p></li><li><p class="post__p">Using HTMLRewriter to target the H1 element via its data attribute;</p></li><li><p class="post__p">Modifying the content of the H1 element with the data from the test area of the CMS we fetched earlier;</p></li><li><p class="post__p">Returning the transformed response (make sure you do this in two places, you may want to write a utility function to keep your code DRY).</p></li></ol>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="OcwDezLHVn"
      aria-describedby="OcwDezLHVn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="OcwDezLHVn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="OcwDezLHVn" itemprop="text" content="%20%2F%2F%20netlify%2Fedge-functions%2Fabtest-homepage-hero.ts%0A%0A%20%20import%20type%20%7B%20Context%2C%20Config%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%2B%20import%20%7B%20HTMLRewriter%20%7D%20from%20%22https%3A%2F%2Fghuc.cc%2Fworker-tools%2Fhtml-rewriter%2Findex.ts%22%3B%0A%0A%20%20export%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%20%20%20%20const%20bucketName%20%3D%20%22home_page_hero%22%3B%0A%20%20%20%20const%20bucket%20%3D%20context.cookies.get(bucketName)%3B%0A%0A%2B%20%20%20%2F%2F%20get%20the%20next%20HTTP%20response%20in%20the%20chain%20to%20be%20modified%0A%2B%20%20%20const%20response%20%3D%20await%20context.next()%3B%0A%0A%20%20%20%20%2F%2F%20get%20stored%20environment%20variables%20for%20test%20content%0A%20%20%20%20const%20CMS_SPACE_ID_TEST%20%3D%20Netlify.env.get(%22CMS_SPACE_ID_TEST%22)%3B%0A%20%20%20%20const%20CMS_ACCESS_TOKEN_TEST%20%3D%20Netlify.env.get(%22CMS_ACCESS_TOKEN_TEST%22)%3B%0A%0A%20%20%20%20if%20(bucket)%20%7B%0A%20%20%20%20%20%20if%20(bucket%20%3D%3D%3D%20%22default%22)%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20return%20unmodifed%20HTTP%20response%0A%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20fetch%20test%20content%20from%20CMS%0A%20%20%20%20%20%20%2F%2F%20this%20is%20a%20boilerplate%20example%20and%20your%20method%20%0A%20%20%20%20%20%20%2F%2F%20and%20query%20will%20vary%0A%20%20%20%20%20%20%2F%2F%20fetch%20only%20the%20content%20you%20need%0A%20%20%20%20%20%20const%20testContent%20%3D%20await%20fetch(%60https%3A%2F%2Fcms.url%2Fspaces%2F%24%7BCMS_SPACE_ID_TEST%7D%60%2C%20%7B%0A%20%20%20%20%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20Authorization%3A%20%60Bearer%20%24%7BCMS_ACCESS_TOKEN_TEST%7D%60%2C%0A%20%20%20%20%20%20%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%20%20const%20jsonData%20%3D%20await%20testContent.json()%3B%0A%0A%2B%20%20%20%20%20%2F%2F%20modify%20the%20hero%20banner%20title%20in%20the%20HTTP%20response%0A%2B%20%20%20%20%20%2F%2F%20select%20the%20element%20by%20data%20attribute%20or%20another%20selector%0A%2B%20%20%20%20%20%2F%2F%20rewrite%20the%20inner%20content%20with%20the%20new%20headline%20from%0A%2B%20%20%20%20%20%2F%2F%20the%20CMS%20test%20space%0A%2B%20%20%20%20%20return%20new%20HTMLRewriter()%0A%2B%20%20%20%20%20%20%20.on(%22%5Bdata-hero-banner-headline%5D%22%2C%20%7B%0A%2B%20%20%20%20%20%20%20%20%20element(element)%20%7B%0A%2B%20%20%20%20%20%20%20%20%20%20%20element.setInnerContent(jsonData.headline)%3B%0A%2B%20%20%20%20%20%20%20%20%20%7D%2C%0A%2B%20%20%20%20%20%20%20%7D)%0A%2B%20%20%20%20%20%20%20.transform(response)%3B%0A%20%20%20%7D%0A%0A%20%20%20%20const%20weighting%20%3D%200.5%3B%0A%20%20%20%20const%20random%20%3D%20Math.random()%3B%0A%20%20%20%20const%20newBucketValue%20%3D%20random%20%3C%3D%20weighting%20%3F%20%22default%22%20%3A%20%22test%22%3B%0A%0A%20%20%20%20context.cookies.set(%7B%0A%20%20%20%20%20%20name%3A%20bucketName%2C%0A%20%20%20%20%20%20value%3A%20newBucketValue%2C%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20if%20(newBucketValue%20%3D%3D%3D%20%22default%22)%20%7B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%2B%20%2F**%0A%2B%20%20*%20Finally%2C%20return%20a%20transformed%20response%20when%20the%20value%20%0A%2B%20%20*%20of%20the%20cookie%20is%20'test'%0A%2B%20%20*%0A%2B%20%20*%201.%20Fetch%20content%20as%20above%0A%2B%20%20*%202.%20Rewrite%20HTML%20response%20as%20above%20and%20return%0A%2B%20%20*%203.%20You%20may%20want%20to%20write%20a%20utility%20function%0A%2B%20%20*%20%20%20%20to%20keep%20your%20code%20DRY!%0A%2B%20%20*%2F%0A%20%20%7D%3B%0A%0A%20%20export%20const%20config%3A%20Config%20%3D%20%7B%0A%20%20%20%20path%3A%20%22%2F%22%2C%0A%20%20%7D%3B">
      <pre class="language-diff-typescript"><code class="language-diff-typescript">// netlify/edge-functions/abtest-homepage-hero.ts<br><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> Context<span class="token punctuation">,</span> Config <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br></span><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> HTMLRewriter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://ghuc.cc/worker-tools/html-rewriter/index.ts"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> bucketName <span class="token operator">=</span> <span class="token string">"home_page_hero"</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> bucket <span class="token operator">=</span> context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>bucketName<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>   <span class="token comment">// get the next HTTP response in the chain to be modified</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token comment">// get stored environment variables for test content</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> <span class="token constant">CMS_SPACE_ID_TEST</span> <span class="token operator">=</span> Netlify<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"CMS_SPACE_ID_TEST"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> <span class="token constant">CMS_ACCESS_TOKEN_TEST</span> <span class="token operator">=</span> Netlify<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"CMS_ACCESS_TOKEN_TEST"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>bucket<span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>     <span class="token keyword">if</span> <span class="token punctuation">(</span>bucket <span class="token operator">===</span> <span class="token string">"default"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>       <span class="token comment">// return unmodifed HTTP response</span><br><span class="token prefix unchanged"> </span>       <span class="token keyword">return</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>     <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>     <span class="token comment">// fetch test content from CMS</span><br><span class="token prefix unchanged"> </span>     <span class="token comment">// this is a boilerplate example and your method </span><br><span class="token prefix unchanged"> </span>     <span class="token comment">// and query will vary</span><br><span class="token prefix unchanged"> </span>     <span class="token comment">// fetch only the content you need</span><br><span class="token prefix unchanged"> </span>     <span class="token keyword">const</span> testContent <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://cms.url/spaces/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CMS_SPACE_ID_TEST</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>       headers<span class="token operator">:</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>         Authorization<span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CMS_ACCESS_TOKEN_TEST</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>         <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>       <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>     <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>     <span class="token keyword">const</span> jsonData <span class="token operator">=</span> <span class="token keyword">await</span> testContent<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>     <span class="token comment">// modify the hero banner title in the HTTP response</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// select the element by data attribute or another selector</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// rewrite the inner content with the new headline from</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// the CMS test space</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">HTMLRewriter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token prefix inserted">+</span>       <span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"[data-hero-banner-headline]"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>         <span class="token function">element</span><span class="token punctuation">(</span>element<span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>           element<span class="token punctuation">.</span><span class="token function">setInnerContent</span><span class="token punctuation">(</span>jsonData<span class="token punctuation">.</span>headline<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>         <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>       <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token prefix inserted">+</span>       <span class="token punctuation">.</span><span class="token function">transform</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>  <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> weighting <span class="token operator">=</span> <span class="token number">0.5</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> random <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> newBucketValue <span class="token operator">=</span> random <span class="token operator">&lt;=</span> weighting <span class="token operator">?</span> <span class="token string">"default"</span> <span class="token operator">:</span> <span class="token string">"test"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>     name<span class="token operator">:</span> bucketName<span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>     value<span class="token operator">:</span> newBucketValue<span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>newBucketValue <span class="token operator">===</span> <span class="token string">"default"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>     <span class="token keyword">return</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span> <span class="token comment">/**<br><span class="token prefix inserted">+</span>  * Finally, return a transformed response when the value <br><span class="token prefix inserted">+</span>  * of the cookie is 'test'<br><span class="token prefix inserted">+</span>  *<br><span class="token prefix inserted">+</span>  * 1. Fetch content as above<br><span class="token prefix inserted">+</span>  * 2. Rewrite HTML response as above and return<br><span class="token prefix inserted">+</span>  * 3. You may want to write a utility function<br><span class="token prefix inserted">+</span>  *    to keep your code DRY!<br><span class="token prefix inserted">+</span>  */</span><br></span><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">const</span> config<span class="token operator">:</span> Config <span class="token operator">=</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   path<span class="token operator">:</span> <span class="token string">"/"</span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Track your test variants in your analytics tool</h3><p class="post__p">In order to track which hero banner messaging performs better in the A/B test, you’ll need to add some extra information to your analytics tooling. Ultimately, there will be a number of ways this can be achieved depending on your architecture and tooling. If you’re using Google Analytics, you can <a href="https://www.netlify.com/blog/how-to-split-traffic-a-b-test-page-layouts-same-url/#track-your-test-variants-in-your-analytics-tool" target="_blank">add an extra bit of information to your tracking scripts as described in a previous post.</a></p><h3 class="post__h3">Bonus content: use environment variables to control which A/B tests are running</h3><p class="post__p">To give content authors and marketers even more control over which A/B tests are live, you could go one step further and use environment variables to control which tests to account for in your edge function code. This could be as straightforward as checking for an environment variable at the top of the edge function, and returning early if the value of the variable is <code>off</code>, like so.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="lLFKyRQwIX"
      aria-describedby="lLFKyRQwIX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="lLFKyRQwIX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="lLFKyRQwIX" itemprop="text" content="const%20HERO_BANNER_TEST_STATE%20%3D%20Netlify.env.get(%22HERO_BANNER_TEST_STATE%22)%3B%0A%0Aif%20(HERO_BANNER_TEST_STATE%20%3D%3D%3D%20%22off%22)%20%7B%0A%20%20return%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token constant">HERO_BANNER_TEST_STATE</span> <span class="token operator">=</span> Netlify<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"HERO_BANNER_TEST_STATE"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token constant">HERO_BANNER_TEST_STATE</span> <span class="token operator">===</span> <span class="token string">"off"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Wrapping up</h2><p class="post__p">In a large organization, it’s essential to empower different teams to do their jobs without too many dependencies or blockers. Whilst this example shows how content authors and developers can work in parallel to execute a granular A/B test, this is only scratching the surface of what a modern composable architecture is capable of in terms of how you can experiment, iterate and ship new features faster. To learn more about what’s available to unlock, check out the official <a href="https://docs.netlify.com/edge-functions/overview/" target="_blank">Netlify Edge Functions documentation</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to split traffic and A/B test different page layouts on the same URL</title>
          <description>Learn how to split traffic for A/B testing between different page layouts without changing the URL using Netlify Edge Functions.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/how-to-split-traffic-a-b-test-page-layouts-same-url/</link>
          <guid>https://www.netlify.com/blog/how-to-split-traffic-a-b-test-page-layouts-same-url/</guid>
          <pubDate>Sun, 14 May 2023 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">A/B testing is an experimentation technique used to split website traffic between two versions of a page or user journey to determine which performs better in terms of a predefined goal, such as click-through rate, sign ups, or conversion. By showing a different version of a page to different groups of users at random and measuring the results, A/B testing can provide valuable insights into user behavior and preferences, helping you make data-driven decisions about design and content in a controlled environment.</p><p class="post__p">A/B testing can be implemented in a number of ways, and often involves using third-party tools to modify pages using client-side JavaScript. This usually results in an undesirable “flash of unstyled content” (FOUC) — or flash of the original page — before the JavaScript has loaded, and often means the browsing experience is slower, which <b class="post__p--italic">can</b> skew your test results.</p><p class="post__p">At Netlify, we have a more robust way for you to run A/B tests, using no third party tools to split users into test buckets and show different test variants, and no extra client-side JavaScript which, although is a common technique from other popular services, can impact performance and invalidate your results.</p><h2 class="post__h2">A/B testing with Netlify Edge Functions</h2><p class="post__p">Using Netlify Edge Functions, you can run A/B tests by setting browser cookies to assign users to test buckets, and modifying page requests on the fly according to those cookies — before they get to the browser. The benefits of this approach include:</p><ul><li><p class="post__p">The ability to modify statically pre-generated pages at the time of the request;</p></li><li><p class="post__p">The test variants (pages, components etc) and code to split traffic are stored with your code repository — not in a third party tool without version control;</p></li><li><p class="post__p">Users in the test bucket see no FOUC in the browser or experience any degraded rendering performance that can influence test results;</p></li><li><p class="post__p">Variant affinity — users will be shown a consistent experience throughout their browsing sessions and returning visits whilst an A/B test is active;</p></li><li><p class="post__p">The ability to use this approach with any front end framework or no framework at all! (If you’re using Next.js, we recommend using <a href="https://www.netlify.com/blog/rewrite-html-transform-page-props-in-nextjs/" target="_blank">Netlify’s Next.js Advanced Middleware as described in this tutorial</a>.)</p></li></ul><p class="post__p">In this post, we’ll use Netlify Edge Functions to run an A/B test on different page layouts served from the same user-facing URL. To read up on Netlify Edge Functions before getting into the example use-case, check out the <a href="https://docs.netlify.com/edge-functions/overview/" target="_blank">Edge Functions documentation</a>, or <a href="https://www.netlify.com/blog/how-to-improve-e-commerce-page-load-time-even-during-traffic-spikes/#benefits-of-enhancing-at-the-edge" target="_blank">the benefits of enhancing your pages at the edge</a>.</p><h2 class="post__h2">Split testing product page layouts on your e-commerce site</h2><p class="post__p">A/B testing in e-commerce is a great way to find out what works best for your customers, ultimately helping to boost sales. Perhaps you want to test which messaging reduces bounce rates, or maybe you want to experiment with how innovative you can be with your page layouts without compromising on conversion.</p><p class="post__p">Let’s A/B test a new product page layout by using an edge function to split traffic using browser cookies, while maintaining the original user-facing URL. The following code examples assume:</p><ol><li><p class="post__p">Your product page URLs follow the format <code>/product/{productId}/</code>;</p></li><li><p class="post__p">Your site is hosted on Netlify;</p></li><li><p class="post__p">You are using any front end framework (apart from Next.js as described above);</p></li><li><p class="post__p">You’ve built a new page layout that you’d like to test against an existing layout.</p></li></ol><p class="post__p">At the root of your project, create a <code>netlify</code> directory if you don’t already have one, and inside that, create an <code>edge-functions</code> directory. Inside that, add a new file called <code>abtest.ts</code>. You can write Edge Functions in JavaScript or TypeScript; in this example we’ll be using TypeScript.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="XbyzAnkOgx"
      aria-describedby="XbyzAnkOgx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="XbyzAnkOgx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="XbyzAnkOgx" itemprop="text" content=".%0A%E2%94%94%E2%94%80%E2%94%80%20netlify%0A%20%20%20%20%E2%94%94%E2%94%80%E2%94%80%20edge-functions%0A%20%20%20%20%20%20%20%20%E2%94%94%E2%94%80%E2%94%80%20abtest.ts">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">.</span><br>└── netlify<br>    └── edge-functions<br>        └── abtest.ts</code></pre>
    </div>
  </div>

  <p class="post__p">Add the following code to <code>abtest.ts</code>. This imports the required types, and exports the edge function and a <code>config</code> object, specifying on which path the code will run. Given the assumed format of the URL (<code>/product/{productId}/</code>), we’re using a wildcard (<code>*</code>) to instruct the edge function to run on any child paths of <code>/product</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fXLyZhkICf"
      aria-describedby="fXLyZhkICf">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fXLyZhkICf">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="fXLyZhkICf" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Fabtest.ts%0A%0Aimport%20%7B%20Config%2C%20Context%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0Aexport%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%7D%0A%0Aexport%20const%20config%3A%20Config%20%3D%20%7B%0A%20%20path%3A%20%22%2Fproduct%2F*%22%2C%0A%7D%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// netlify/edge-functions/abtest.ts</span><br><br><span class="token keyword">import</span> <span class="token punctuation">{</span> Config<span class="token punctuation">,</span> Context <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">const</span> config<span class="token operator">:</span> Config <span class="token operator">=</span> <span class="token punctuation">{</span><br>  path<span class="token operator">:</span> <span class="token string">"/product/*"</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Assign users to test buckets using browser cookies</h3><p class="post__p">The following code lays the foundations for using a cookie named <code>layout_test</code> to split traffic to different product page layouts. First, look for the cookie by using the <a href="https://docs.netlify.com/edge-functions/api/#netlify-specific-context-object" target="_blank">Netlify Context API</a> — <code>context.cookies.get()</code>. If there’s no existing cookie, assign one using <code>context.cookies.set()</code>, based on the desired weighting using a random number between 0 and 1.</p><p class="post__p">This basic example demonstrates a 50/50 weighting combined with a random number generated by JavaScript. You may want to configure your weighting or random number generation to your specific needs.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="lMxozWptcj"
      aria-describedby="lMxozWptcj">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="lMxozWptcj">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="lMxozWptcj" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Fabtest.ts%0A%0Aimport%20type%20%7B%20Config%2C%20Context%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0Aexport%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%20%20%2F%2F%20look%20for%20existing%20%22layout_test%22%20cookie%0A%20%20const%20bucketName%20%3D%20%22layout_test%22%3B%0A%20%20const%20bucket%20%3D%20context.cookies.get(bucketName)%3B%0A%0A%20%20%2F%2F%20return%20here%20if%20we%20find%20a%20cookie%0A%20%20if%20(bucket)%20%7B%0A%20%20%20%2F%2F...%0A%20%20%7D%0A%0A%20%20%2F%2F%20if%20no%20%22layout_test%22%20cookie%20is%20found%2C%20assign%20the%20user%20to%20a%20bucket%0A%20%20%2F%2F%20in%20this%20example%20we're%20using%20two%20buckets%20(default%2C%20new)%20%0A%20%20%2F%2F%20with%20an%20equal%20weighting%20of%2050%2F50%0A%20%20const%20weighting%20%3D%200.5%3B%0A%20%20%2F%2F%20get%20a%20random%20number%20between%20(0-1)%0A%20%20const%20random%20%3D%20Math.random()%3B%0A%20%20const%20newBucketValue%20%3D%20random%20%3C%3D%20weighting%20%3F%20%22default%22%20%3A%20%22new%22%3B%0A%0A%20%20%2F%2F%20set%20the%20new%20%22layout_test%22%20cookie%0A%20%20context.cookies.set(%7B%0A%20%20%20%20name%3A%20bucketName%2C%0A%20%20%20%20value%3A%20newBucketValue%2C%0A%20%20%7D)%3B%0A%0A%20%20%2F%2F%20...%0A%7D%3B%0A%0Aexport%20const%20config%3A%20Config%20%3D%20%7B%0A%20%20path%3A%20%22%2Fproduct%2F*%22%2C%0A%7D%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// netlify/edge-functions/abtest.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> Config<span class="token punctuation">,</span> Context <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token comment">// look for existing "layout_test" cookie</span><br>  <span class="token keyword">const</span> bucketName <span class="token operator">=</span> <span class="token string">"layout_test"</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> bucket <span class="token operator">=</span> context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>bucketName<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// return here if we find a cookie</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>bucket<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>   <span class="token comment">//...</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token comment">// if no "layout_test" cookie is found, assign the user to a bucket</span><br>  <span class="token comment">// in this example we're using two buckets (default, new) </span><br>  <span class="token comment">// with an equal weighting of 50/50</span><br>  <span class="token keyword">const</span> weighting <span class="token operator">=</span> <span class="token number">0.5</span><span class="token punctuation">;</span><br>  <span class="token comment">// get a random number between (0-1)</span><br>  <span class="token keyword">const</span> random <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> newBucketValue <span class="token operator">=</span> random <span class="token operator">&lt;=</span> weighting <span class="token operator">?</span> <span class="token string">"default"</span> <span class="token operator">:</span> <span class="token string">"new"</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// set the new "layout_test" cookie</span><br>  context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    name<span class="token operator">:</span> bucketName<span class="token punctuation">,</span><br>    value<span class="token operator">:</span> newBucketValue<span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// ...</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">const</span> config<span class="token operator">:</span> Config <span class="token operator">=</span> <span class="token punctuation">{</span><br>  path<span class="token operator">:</span> <span class="token string">"/product/*"</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Split traffic between different page layouts on the same URL</h3><p class="post__p">Next, use the <code>layout_test</code> cookie to decide which page layout is served to customers. Given the assumed format of the URL (<code>/product/{productId}/</code>), the code example below contains some code to parse the <code>productId</code> from the URL, and your method may vary.</p><p class="post__p">If a cookie with the value <code>new</code> is found, return a new URL with the path to a new layout page. This <b class="post__p--bold">rewrites</b> the HTTP response of the original request behind the scenes, <b class="post__p--bold">so that customers see the new product page layout on the original URL</b>.</p><p class="post__p">If a cookie with a value of <code>default</code> is found, return an empty <code>return</code> to bypass the current function and continue the request chain. This serves the user the original and unmodified HTTP response. For more information, take a look at the <a href="https://docs.netlify.com/edge-functions/api/#return-a-rewrite" target="_blank">Netlify documentation on URL rewrites</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YAvJHkEwVg"
      aria-describedby="YAvJHkEwVg">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YAvJHkEwVg">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="YAvJHkEwVg" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Fabtest.ts%0A%0A%20%20import%20type%20%7B%20Config%2C%20Context%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0A%20%20export%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%20%20%20%20const%20bucketName%20%3D%20%22layout_test%22%3B%0A%20%20%20%20const%20bucket%20%3D%20context.cookies.get(bucketName)%3B%0A%0A%2B%20%20%20%20%20%2F%2F%20get%20productId%20from%20URL%0A%2B%20%20%20%20%20%2F%2F%20assuming%20URL%20format%20is%20https%3A%2F%2Fdomain.tld%2Fproduct%2FproductId%0A%2B%20%20%20%20%20const%20url%20%3D%20new%20URL(request.url)%3B%0A%2B%20%20%20%20%20const%20pathParts%20%3D%20url.pathname.split('%2F')%3B%0A%2B%20%20%20%20%20const%20productId%20%3D%20pathParts%5B2%5D%3B%0A%0A%20%20%20%20if%20(bucket)%20%7B%0A%2B%20%20%20%20%20if%20(bucket%20%3D%3D%3D%20%22default%22)%20%7B%0A%2B%20%20%20%20%20%20%20%2F%2F%20better%20performance%20than%20returning%20context.next()!%0A%2B%20%20%20%20%20%20%20return%3B%20%0A%2B%20%20%20%20%20%7D%0A%2B%0A%2B%20%20%20%20%20%2F%2F%20rewrite%20to%20a%20new%20page%20behind%20the%20scenes%0A%2B%20%20%20%20%20%2F%2F%20browser%20URL%20stays%20the%20same%0A%2B%20%20%20%20%20return%20new%20URL(%60%2Fnew-product-page-layout%2F%24%7BproductId%7D%2F%60%2C%20request.url)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20weighting%20%3D%200.5%3B%0A%20%20%20%20const%20random%20%3D%20Math.random()%3B%0A%20%20%20%20const%20newBucketValue%20%3D%20random%20%3C%3D%20weighting%20%3F%20%22default%22%20%3A%20%22new%22%3B%0A%0A%20%20%20%20context.cookies.set(%7B%0A%20%20%20%20%20%20name%3A%20bucketName%2C%0A%20%20%20%20%20%20value%3A%20newBucketValue%2C%0A%20%20%20%20%7D)%3B%0A%0A%2B%20%20%20if%20(newBucketValue%20%3D%3D%3D%20%22default%22)%20%7B%0A%2B%20%20%20%20%20return%3B%0A%2B%20%20%20%7D%0A%2B%20%20%20%20%20%0A%2B%20%20%20return%20new%20URL(%60%2Fnew-product-page-layout%2F%24%7BproductId%7D%2F%60%2C%20request.url)%3B%0A%20%20%7D%3B%0A%0A%20%20export%20const%20config%3A%20Config%20%3D%20%7B%0A%20%20%20%20path%3A%20%22%2Fproduct%2F*%22%2C%0A%20%20%7D%3B">
      <pre class="language-diff-typescript"><code class="language-diff-typescript">// netlify/edge-functions/abtest.ts<br><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> Config<span class="token punctuation">,</span> Context <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> bucketName <span class="token operator">=</span> <span class="token string">"layout_test"</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> bucket <span class="token operator">=</span> context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>bucketName<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>     <span class="token comment">// get productId from URL</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// assuming URL format is https://domain.tld/product/productId</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name"><span class="token constant">URL</span></span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">const</span> pathParts <span class="token operator">=</span> url<span class="token punctuation">.</span>pathname<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'/'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">const</span> productId <span class="token operator">=</span> pathParts<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>bucket<span class="token punctuation">)</span> <span class="token punctuation">{</span><br></span><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>     <span class="token keyword">if</span> <span class="token punctuation">(</span>bucket <span class="token operator">===</span> <span class="token string">"default"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>       <span class="token comment">// better performance than returning context.next()!</span><br><span class="token prefix inserted">+</span>       <span class="token keyword">return</span><span class="token punctuation">;</span> <br><span class="token prefix inserted">+</span>     <span class="token punctuation">}</span><br><span class="token prefix inserted">+</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// rewrite to a new page behind the scenes</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// browser URL stays the same</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name"><span class="token constant">URL</span></span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/new-product-page-layout/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>productId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> request<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> weighting <span class="token operator">=</span> <span class="token number">0.5</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> random <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> newBucketValue <span class="token operator">=</span> random <span class="token operator">&lt;=</span> weighting <span class="token operator">?</span> <span class="token string">"default"</span> <span class="token operator">:</span> <span class="token string">"new"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span>   context<span class="token punctuation">.</span>cookies<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>     name<span class="token operator">:</span> bucketName<span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>     value<span class="token operator">:</span> newBucketValue<span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-typescript"><span class="token prefix inserted">+</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>newBucketValue <span class="token operator">===</span> <span class="token string">"default"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">return</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token punctuation">}</span><br><span class="token prefix inserted">+</span>     <br><span class="token prefix inserted">+</span>   <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name"><span class="token constant">URL</span></span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/new-product-page-layout/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>productId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> request<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-typescript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">const</span> config<span class="token operator">:</span> Config <span class="token operator">=</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   path<span class="token operator">:</span> <span class="token string">"/product/*"</span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Track your test variants in your analytics tool</h3><p class="post__p">In order to track which product page layout customers are viewing when they click that add to cart button, you’ll need to add some extra information to your analytics tooling. Ultimately, there will be a number of ways this can be achieved depending on your architecture and tooling, but if you’re using Google Analytics (GA), you can use client-side JavaScript to check for a browser cookie (e.g. <code>layout_test</code>) and pass the relevant information to your GA tracking script.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="nOExViBmTG"
      aria-describedby="nOExViBmTG">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="nOExViBmTG">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="nOExViBmTG" itemprop="text" content="%3C!--%20install%20Google%20Analytics%E2%80%99%20JS%20tracker%20before%20using%20this%20code%20--%3E%0A%3Cscript%3E%0A%20%20%2F%2F%20get%20all%20cookies%20from%20document%20as%20a%20string%20separating%0A%20%20%2F%2F%20cookies%20with%20%3B%0A%20%20%2F%2F%20eg%3A%20%60layout_test%3Dnew%3B%20another_cookie%3Dvalue%3B%60%0A%09const%20cookies%20%3D%20document.cookie%3B%0A%0A%20%20%2F%2F%20search%20cookies%20string%20for%20the%20%22layout_test%22%20string%0A%20%20%2F%2F%20split%20and%20grab%20the%20value%0A%20%20const%20pageLayout%20%3D%20cookies%0A%20%20.split(%22%3B%20%22)%0A%20%20.find((row)%20%3D%3E%20row.startsWith(%22layout_test%3D%22))%0A%20%20%3F.split(%22%3D%22)%5B1%5D%3B%0A%0A%20%20ga('send'%2C%20'add_to_cart'%2C%20%7B%0A%20%20%20%20%2F%2F%20...%0A%20%20%20%20'Layout'%3A%20pageLayout%0A%20%20%7D)%3B%0A%3C%2Fscript%3E">
      <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- install Google Analytics’ JS tracker before using this code --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript"><br>  <span class="token comment">// get all cookies from document as a string separating</span><br>  <span class="token comment">// cookies with ;</span><br>  <span class="token comment">// eg: `layout_test=new; another_cookie=value;`</span><br>	<span class="token keyword">const</span> cookies <span class="token operator">=</span> document<span class="token punctuation">.</span>cookie<span class="token punctuation">;</span><br><br>  <span class="token comment">// search cookies string for the "layout_test" string</span><br>  <span class="token comment">// split and grab the value</span><br>  <span class="token keyword">const</span> pageLayout <span class="token operator">=</span> cookies<br>  <span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">"; "</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">find</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">row</span><span class="token punctuation">)</span> <span class="token operator">=></span> row<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">"layout_test="</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token operator">?.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">"="</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br>  <span class="token function">ga</span><span class="token punctuation">(</span><span class="token string">'send'</span><span class="token punctuation">,</span> <span class="token string">'add_to_cart'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>    <span class="token comment">// ...</span><br>    <span class="token string-property property">'Layout'</span><span class="token operator">:</span> pageLayout<br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Split testing doesn’t have to be a stress test</h2><p class="post__p">With just one edge function and a tiny addition to your analytics configuration, you can effectively split traffic to A/B test different page layouts without the overheads of managing third-party tools. Using the above methods, this type of split test could also be implemented across <b class="post__p--bold">entire</b> user-journeys from landing page to checkout.</p><p class="post__p">For more information on how Netlify is helping businesses like yours increase conversions through the tooling we provide, check out our <a href="https://www.netlify.com/composable-commerce/" target="_blank">Composable Commerce resources</a>. Happy testing!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>The universal CSS * selector isn't actually universal</title>
          <description>For my ENTIRE career I have been living with an enormous misconception: the universal CSS selector doesn't *actually* select EVERYTHING.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/universal-css-selector-pseudo-elements/</link>
          <guid>https://whitep4nth3r.com/blog/universal-css-selector-pseudo-elements/</guid>
          <pubDate>Wed, 03 May 2023 23:00:00 GMT</pubDate>
          <category>CSS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Whenever I start a new web project, I begin by writing a very small &quot;CSS reset&quot;, which uses the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors" target="_blank">universal selector (*)</a> to apply desired styles to <b class="post__p--italic">every single element</b>. My CSS reset usually looks something like this:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DkKKFhyJmV"
      aria-describedby="DkKKFhyJmV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DkKKFhyJmV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="DkKKFhyJmV" itemprop="text" content="*%20%7B%0A%20%20box-sizing%3A%20border-box%3B%0A%20%20margin%3A%200%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">*</span> <span class="token punctuation">{</span><br>  <span class="token property">box-sizing</span><span class="token punctuation">:</span> border-box<span class="token punctuation">;</span><br>  <span class="token property">margin</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">This little block of code sets all elements to use a box-sizing value I like best (see <a href="https://whitep4nth3r.com/talks/this-box-will-change-your-life/" target="_blank">my talk on the CSS box model</a> for more information), and removes all default margins from everything — just because I like it that way. I haven&#39;t encountered any issues with this method, and you probably haven&#39;t and you probably won&#39;t — but here&#39;s a little nugget of knowledge that might save you hours of debugging in the future. </p><h2 class="post__h2">The CSS universal selector doesn&#39;t apply to pseudo elements</h2><p class="post__p">First off, if you&#39;re unfamiliar with pseudo elements, read this post: <a href="https://whitep4nth3r.com/blog/pseudo-classes-and-pseudo-elements/">What's the difference between : and :: in CSS?</a></p><p class="post__p">Any properties declared using the CSS * selector don&#39;t apply to pseudo elements. In the case of my CSS reset, whilst you don&#39;t <b class="post__p--italic">need</b> to remove margins from pseudo elements (because there is no margin set by default), if you wanted to do something bolder using the CSS universal selector, such as adding a red border to all elements and pseudo elements, you&#39;ll have to set the value explicitly on the element itself. </p><p class="post__p">I&#39;m not sure why you&#39;d want to do this, but you <b class="post__p--italic">could</b>, right?</p><h2 class="post__h2">But a pseudo element isn&#39;t a real element, obvs</h2><p class="post__p">Of course. You could also argue that a pseudo element isn&#39;t an actual element to be selected using the * selector, since it&#39;s not. It&#39;s a pseudo element; it&#39;s fake! This is <b class="post__p--italic">obviously</b> <b class="post__p--bold">technically</b> correct. But I felt like writing about it anyway. Here&#39;s a demo on CodePen for you to mess with.</p>
    <div class="codePenEmbed__container" data-codepen data-embed-code=%3Cp%20class=%22codepen%22%20data-height=%22300%22%20data-default-tab=%22css,result%22%20data-slug-hash=%22qBJVzOG%22%20data-editable=%22true%22%20data-user=%22whitep4nth3r%22%20style=%22height:%20300px;%20box-sizing:%20border-box;%20display:%20flex;%20align-items:%20center;%20justify-content:%20center;%20border:%202px%20solid;%20margin:%201em%200;%20padding:%201em;%22%3E%0A%20%20%3Cspan%3ESee%20the%20Pen%20%3Ca%20href=%22https://codepen.io/whitep4nth3r/pen/qBJVzOG%22%3E%0A%20%20The%20universal%20CSS%20*%20selector%20isn't%20actually%20universal%3C/a%3E%20by%20whitep4nth3r%20(%3Ca%20href=%22https://codepen.io/whitep4nth3r%22%3E@whitep4nth3r%3C/a%3E)%0A%20%20on%20%3Ca%20href=%22https://codepen.io%22%3ECodePen%3C/a%3E.%3C/span%3E%0A%3C/p%3E>
      <div data-target></div>
    </div>

    <script>
      const codepen = document.querySelector("[data-codepen]");
      let loaded = false;
      const options = {
        root: null,
        threshold: 0.1
      }

      const loadCodePen = (entries, observer) => {
        entries.forEach(entry => {
          
          if(!loaded && entry.isIntersecting) {
            const target = entry.target.querySelector("[data-target]");
            const embedCode = entry.target.dataset.embedCode;

            target.innerHTML = decodeURI(embedCode);
            const script = document.createElement("script");
            script.src = "https://cpwebassets.codepen.io/assets/embed/ei.js";
            document.head.append(script);
            loaded = true;
          }
        });
      };

      const observer = new IntersectionObserver(loadCodePen, options);
      observer.observe(codepen);
    </script>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to hide text in CSS pseudo elements from screen readers</title>
          <description>Learn how to hide decorative text generated by CSS from screen readers, so that your weird designs don't interrupt the flow of the text.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/hide-text-in-css-pseudo-elements-from-screen-readers/</link>
          <guid>https://whitep4nth3r.com/blog/hide-text-in-css-pseudo-elements-from-screen-readers/</guid>
          <pubDate>Sun, 23 Apr 2023 23:00:00 GMT</pubDate>
          <category>CSS</category><category>Accessibility</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Unfortunately this doesn&#39;t yet work in Safari or Firefox, but you can keep up to date with support on <a href="https://caniuse.com/?search=content%20alt%20text" target="_blank">caniuse.com</a>.</p><p class="post__p"><b class="post__p--bold"><b class="post__p--italic">TL:DR; Add empty &quot;alternative text&quot; to the pseudo element content to prevent screen readers announcing your decorative design elements, like so:</b></b></p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="wPvCPzfxvl"
      aria-describedby="wPvCPzfxvl">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="wPvCPzfxvl">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="wPvCPzfxvl" itemprop="text" content=".someClass%3A%3Abefore%20%7B%0A%20%20content%3A%20%22M%22%20%2F%20%22%22%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.someClass::before</span> <span class="token punctuation">{</span><br>  <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">"M"</span> / <span class="token string">""</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">I&#39;m currently redesigning my website, and part of the design includes decorative elements provided by a font, specified as single letters in the content of CSS pseudo elements. Psst! If you don&#39;t know what a CSS pseudo element is, read this post first: <a href="https://whitep4nth3r.com/blog/pseudo-classes-and-pseudo-elements/">What's the difference between : and :: in CSS?</a></p><p class="post__p">Here&#39;s some code that adds a large stylised paint brush glyph as a pseudo element attached to the <code>&lt;main&gt;</code> HTML element of the page.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xhaDuOeAuH"
      aria-describedby="xhaDuOeAuH">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xhaDuOeAuH">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="xhaDuOeAuH" itemprop="text" content="main%3A%3Abefore%20%7B%0A%20%20font-family%3A%20%22Atomic%20Marker%20Extras%22%3B%0A%20%20content%3A%20%22M%22%3B%0A%20%20position%3A%20fixed%3B%0A%20%20z-index%3A%20-1%3B%0A%20%20%2F*%20...%20*%2F%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">main::before</span> <span class="token punctuation">{</span><br>  <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">"Atomic Marker Extras"</span><span class="token punctuation">;</span><br>  <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">"M"</span><span class="token punctuation">;</span><br>  <span class="token property">position</span><span class="token punctuation">:</span> fixed<span class="token punctuation">;</span><br>  <span class="token property">z-index</span><span class="token punctuation">:</span> -1<span class="token punctuation">;</span><br>  <span class="token comment">/* ... */</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">When using <a href="https://support.apple.com/en-gb/guide/voiceover/welcome/mac" target="_blank">VoiceOver for MacOS</a> to read the page using that code, the letter M is announced. Not ideal! You can also confirm what will be announced using the Accessibility Tree view (<a href="https://youtube.com/shorts/XpmLoOcMK_M?feature=share" target="_blank">enabled via an experimental flag</a> in Chromium browsers), which gives you a visual display of all elements and their content that accessibility tools will have access to.</p><img src="https://images.ctfassets.net/56dzm01z6lln/40eORKhbhIc1PuoeMYPUnA/961059d8dc08738bb848bce1b7287e78/statictext_m_pseudo.png" alt="Browser accessibility tree view, showing a static text element with the content M as referenced in the code example above, that will be read out by screen readers." height="601" width="937" /><p class="post__p">Fortunately, the CSS <code>content</code> property comes with the ability to <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/content#image_combined_with_alternative_text" target="_blank">specify alternative text</a>, which is especially useful if you&#39;re using images in your pseudo elements that <b class="post__p--italic">actually mean something</b>. And what&#39;s more, if you want to tell the browser that the content is purely decorative and doesn&#39;t need to be seen by assistive technology, you can provide an empty value for the alternative text, like so.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xaBIYsRgcn"
      aria-describedby="xaBIYsRgcn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xaBIYsRgcn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="xaBIYsRgcn" itemprop="text" content="%20%20main%3A%3Abefore%20%7B%0A%20%20%20%20font-family%3A%20%22Atomic%20Marker%20Extras%22%3B%0A-%20%20%20content%3A%20%22M%22%3B%0A%2B%20%20%20content%3A%20%22M%22%20%2F%20%22%22%3B%0A%20%20%20%20position%3A%20fixed%3B%0A%20%20%20%20z-index%3A%20-1%3B%0A%20%20%20%20%2F*%20...%20*%2F%0A%20%20%7D">
      <pre class="language-diff-css"><code class="language-diff-css">main::before {<br><span class="token unchanged language-css"><span class="token prefix unchanged"> </span>   <span class="token property">font-family</span><span class="token punctuation">:</span> <span class="token string">"Atomic Marker Extras"</span><span class="token punctuation">;</span><br></span><span class="token deleted-sign deleted language-css"><span class="token prefix deleted">-</span>   <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">"M"</span><span class="token punctuation">;</span><br></span><span class="token inserted-sign inserted language-css"><span class="token prefix inserted">+</span>   <span class="token property">content</span><span class="token punctuation">:</span> <span class="token string">"M"</span> / <span class="token string">""</span><span class="token punctuation">;</span><br></span><span class="token unchanged language-css"><span class="token prefix unchanged"> </span>   <span class="token property">position</span><span class="token punctuation">:</span> fixed<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token property">z-index</span><span class="token punctuation">:</span> -1<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token comment">/* ... */</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">And that&#39;s it! One handy lesser-known feature of CSS to help you make your weird and wonderful website designs more enjoyable to browse, for everyone, everywhere.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>The Acronyms of Rendering on the Web</title>
          <description>Learn about different types of rendering on the web, and how it impacts user experience, site performance, and SEO. </description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/the-acronyms-of-rendering/</link>
          <guid>https://www.netlify.com/blog/the-acronyms-of-rendering/</guid>
          <pubDate>Sun, 16 Apr 2023 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">There are <b class="post__p--italic">so many</b> acronyms and initialisms in web development, it’s difficult to keep up. Does SSR affect my CWV? How many HTTP methods does it take to make a REST API? Does a SPA use CSR? Help, I need CPR!</p><p class="post__p">Don’t worry, I’m here for you. Let’s break down the acronyms and initialisms of rendering on the web so you can get some much needed R&amp;R.</p><h2 class="post__h2">What is rendering?</h2><p class="post__p">Rendering is the process of generating HTML markup to display web pages in the browser. How, and most importantly, <b class="post__p--italic">where</b> that rendering process happens can have a significant impact on user experience, site performance, and Search Engine Optimization (SEO).</p><h2 class="post__h2">Types of rendering</h2><p class="post__p">Let’s take a look at the different types of rendering available on the modern web today, and which types of sites, pages and data they are most suited to.</p><h3 class="post__h3">Static rendering</h3><p class="post__p">In the early days of the web, all websites were static sites — collections of hand-written HTML files stored on servers, most probably uploaded via <a href="https://en.wikipedia.org/wiki/File_Transfer_Protocol" target="_blank">FTP clients</a> (oh, nostalgia!), and served directly to users in their web browsers. Static rendering is still a great option to use today, and is particularly suited to sites serving a single HTML file, such as a single landing page of content. There’s no server computation required — so your page will load fast. And a single HTML file is super easy to host on Netlify, either via connecting a Git repository, or uploading via <a href="https://app.netlify.com/drop" target="_blank">Netlify Drop</a>. 🫳🎤 <a href="https://whitep4nth3r-live.netlify.app/" target="_blank">Here’s one I made earlier</a>.</p><h3 class="post__h3">Server-Side Rendering (SSR)</h3><p class="post__p">As the web evolved, the need for larger sites and more dynamic experiences emerged, and with this came the rise of Server-Side Rendering (SSR). SSR is a rendering method where web pages are built on a server at the time of the request.</p><ol><li><p class="post__p">Type a web address into a browser</p></li><li><p class="post__p">Submit the request</p></li><li><p class="post__p">That request travels to a server in a fixed location, where the server processes the request, builds the web page in real-time, and sends it back to the browser as an HTML document.</p></li></ol><p class="post__p">SSR is still the most prevalent rendering method on the web today, being the default for application frameworks such as Wordpress and large monolithic tech stacks. Historically, SSR required a persistently running managed server, which often comes with undesirable overheads in terms of maintenance, scaling and security. Fortunately, modern front end JavaScript frameworks such as Astro, Next.js, Remix, Nuxt and Gatsby now provide configuration options for using SSR via modern web development platforms such as Netlify, by using <a href="https://docs.netlify.com/functions/overview/" target="_blank">serverless functions</a> under the hood.</p><p class="post__p">SSR is best suited to serving pages that need to contain up-to-date, dynamic data, such as product stock levels or pricing if you’re building an e-commerce site, or personalized pages, such as if a user is logged in to an account on any site.</p><p class="post__p">The drawback to SSR is potentially longer latency. Servers usually exist in fixed geographical locations. The further the original request is from the origin server, the longer the request will take to make the journey there and back to the browser. And if your site is being viewed on a smart phone over a 3G or 4G connection, the request may take even longer.</p><h3 class="post__h3">Client-Side Rendering (CSR)</h3><p class="post__p">Client-side Rendering (CSR) is the process of rendering content in the browser using JavaScript. When a request is a made for a web page that uses CSR, the server sends back a placeholder HTML document with a JavaScript file that will render the rest of the page and fill in the blanks in the browser.</p><p class="post__p">CSR became increasingly popular with the mainstream adoption of JavaScript in the browser during the late 1990s. Its place as a core component in the web ecosystem was further solidified with the evolution of Single Page Application (SPA) frontend framework technologies such as React, Angular and Vue. Like SSR, CSR is best suited to dynamic up-to-date data, but it comes with some drawbacks.</p><p class="post__p">With potentially megabytes of JavaScript to process on pages using CSR, your site may end up being slow to load and show data. Additionally, a combination of slow internet speeds, old devices, increasing web page complexity, buggy browser plugins or JavaScript simply not being available in the browser all point to using CSR <b class="post__p--italic">sparingly</b>.</p><p class="post__p">What’s more, CSR isn’t ideal for SEO. Most search engines can only crawl the content returned from URL — not the result of what might happen in the browser. If you use CSR to render your entire website, search engines will only ever be able to read your placeholder content, rather than the rich content that is eventually loaded in by JavaScript.</p><h3 class="post__h3">Static Site Generation (SSG)</h3><p class="post__p">Static Site Generation (SSG) is the process of pre-generating HTML pages ahead of time, so that they’re ready to serve to your users instantly without the need for SSR or CSR. In the mid 2010s came the rise in popularity of static site generator tools such as Jekyll, which allowed developers to generate any number of static HTML files from templates during a build process. No more hand-crafting time-consuming single HTML files to get the benefits of static rendering anymore — yay!</p><p class="post__p">And with this came the ability to serve your site from a Content Delivery Network (CDN), such as <a href="https://www.netlify.com/blog/2016/04/15/make-your-site-faster-with-netlifys-intelligent-cdn/" target="_blank">Netlify’s CDN</a>, which serves your static files and assets from the closest server node location to the request — making your site really, <b class="post__p--italic">really</b> fast. What’s more, given your website pages are pre-generated as full HTML files containing actual content, you’ll score more SEO points.</p><p class="post__p">There are hundreds of static site generators in the web ecosystem today, allowing you to build static sites using (most probably) <b class="post__p--italic">any</b> programming language your heart desires, including JavaScript, Go, Ruby, Python, PHP and Rust. Check out a huge <a href="https://jamstack.org/generators/" target="_blank">list of static site generators on Jamstack.org</a>.</p><p class="post__p">SSG is a rendering method best suited to content sites and pages that don’t change often. Blogs, portfolios, documentation sites and informational content are all great use cases for SSG. To update content, trigger a rebuild of your site, and the newly pre-generated assets will be ready to serve from the CDN once the build process has completed.</p><h3 class="post__h3">Incremental Static Regeneration (ISR)</h3><p class="post__p">Incremental Static Regeneration (ISR) is Next.js’s proprietary implementation of a caching pattern called Stale While Revalidate (SWR). This allows for the regeneration of <b class="post__p--italic">single</b> statically rendered pages that have been modified, rather than rebuilding an entire site from scratch. With SWR, you can publish changes to a specific page — via a webhook trigger in a CMS, for example — without triggering a full site rebuild, resulting in faster site updates.</p><p class="post__p">SWR allows for very quick updates to static content while retaining the benefits of SSG. When you use SWR to render a specific page, a version of that page will be statically generated and cached during an initial build. When that page is updated, a rebuild of that page is <b class="post__p--italic"><b class="post__p--bold">not</b></b> triggered straight away, but the next time someone requests that page. The previous (stale) version of the page is served until the page has been revalidated and regenerated in the background, and the next request for that page will receive the updated version.</p><p class="post__p">Bear in mind that with SWR/ISR, some of your website visitors may see outdated content as the updated page is rebuilt on the server and cached. You won’t want to use SWR for pages displaying data that should be accurate and up to date, such as pricing data. You’ll also want to generate a fallback page to serve if you’re using SWR/ISR to generate <b class="post__p--italic">new</b> pages, just to make sure your site doesn’t look broken or serve a 404.</p><h3 class="post__h3">Distributed Persistent Rendering (DPR)</h3><p class="post__p">Distributed Persistent Rendering (DPR) is a handy rendering method provided by Netlify to use on very large sites in order to dramatically reduce build times. Instead of pre-building your entire site in advance using SSG, you can choose to statically pre-generate only your most popular and/or critical pages, and enhance your rendering strategy with DPR.</p><p class="post__p">DPR allows you to statically generate and cache pages <b class="post__p--italic">on demand</b> when they are requested for the first time. The <b class="post__p--italic">first request</b> to a page using DPR will result in an SSR-like experience, after which the generated pages will be served from the cache.</p><p class="post__p">Netlify supports DPR and SWR through the use of <a href="https://docs.netlify.com/configure-builds/on-demand-builders/" target="_blank">On-demand Builders</a> — serverless functions used to generate web content as needed that’s automatically cached on Netlify’s Edge CDN.</p><h3 class="post__h3">Edge Side Rendering (ESR)</h3><p class="post__p">Here’s where things get really exciting. Remember the CDN model we talked about, where static pages and assets are delivered to users from the closest geographical server location? Edge Side Rendering (ESR) harnesses the power of the CDN to deliver SSR as close to users as possible, providing the benefits that come with traditional SSR such as personalization and dynamic data, with improved speed for everyone around the world. ESR can be implemented for a full site, single pages, or even for just parts of pages.</p><p class="post__p">ESR on Netlify is provided by <a href="https://docs.netlify.com/edge-functions/overview/" target="_blank">Netlify Edge Functions</a> — serverless functions executed at the edge — that can intercept an HTTP request and modify the HTTP response <b class="post__p--italic">before</b> it is sent to the browser. This means that you can use ESR to <b class="post__p--italic">enhance your static sites and pages at the time of the request</b>. When you pre-build as much as possible with SSG, and use Edge Functions to modify pages when you need, you retain the speed of static rendering with the power of making dynamic updates to your pages when you need to. ESR is an excellent candidate for personalization, localization, internationalization and more — providing a kind of super-powered SSR wherever your site visitors are around the world.</p><h2 class="post__h2">Wrapping up</h2><p class="post__p">That’s a lot of rendering options! And you most probably don’t want to use them <b class="post__p--italic">all</b> in a single project. Ultimately, the technologies you choose — such as hosting platform and frontend framework — will determine which rendering modes are available to you in your project. Understanding the pros and cons, and the fit of the different rendering approaches to your projects and the types of sites you build is a great way to help inform your choice of tools and technologies, rather than letting those choices dictate your approach.</p><p class="post__p">Happy rendering! TTYL.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Improve E-commerce Site Performance with Rendering Strategies</title>
          <description>Learn how to optimize UX, site performance and SEO by taking a strategic approach to how you render different pages of your e-commerce store.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/improve-e-commerce-site-performance-with-rendering-strategies/</link>
          <guid>https://www.netlify.com/blog/improve-e-commerce-site-performance-with-rendering-strategies/</guid>
          <pubDate>Wed, 12 Apr 2023 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Building an e-commerce store can be a challenge, especially as you start to scale your business and grow your product catalog. With potentially thousands of product detail pages, landing pages and campaigns to manage, it can be tricky to ensure your website is snappy and enjoyable for customers to browse, add to cart, and check out. Additionally, you want to enable development teams to release new features early and often — so that you can experiment, iterate, and grow. The good news is that you can enable all of the above by taking a strategic approach to how you <b class="post__p--italic">render</b> different pages of your e-commerce store.</p><p class="post__p">In this blog post, we&#39;ll explore the definition of rendering, different rendering methods, and and how you might make strategic rendering choices for different areas of an online store to boost the end-user experience, site performance, and Search Engine Optimization (SEO).</p><h2 class="post__h2">What is rendering?</h2><p class="post__p">Rendering is the process of generating HTML markup to display web pages in the browser. How, and most importantly, <b class="post__p--italic">where</b> that rendering process occurs can have a significant impact on user experience, site performance, and SEO. Let’s take a look at the different types of rendering available on the modern web today, and the types of data and pages in your online store that each rendering mode is most suited to.</p><h2 class="post__h2">Server-side Rendering (SSR)</h2><p class="post__p">In the early days of the web, all websites were static sites — collections of meticulously handcrafted HTML files stored on servers, delivered directly to users in their web browsers on request.</p><p class="post__p">As the web evolved, the need for richer and more dynamic experiences emerged, and with this came the rise of Server-side Rendering (SSR). SSR is a rendering method where web pages are built on a server at the time of the request.</p><ol><li><p class="post__p">Type a web address into a browser.</p></li><li><p class="post__p">Submit the request.</p></li><li><p class="post__p">That request travels to a server in a fixed location, where the server processes the request, builds the web page in real-time, and sends it back to the browser as an HTML document.</p></li></ol><p class="post__p">SSR is still the most prevalent rendering method on the web today, especially if you’re building with a legacy monolithic architecture. It is suited to serving pages containing up-to-date, dynamic data such as product stock levels, pricing, or cart pages, so that customers are served accurate information.</p><p class="post__p">The drawback to SSR is potentially longer latency. Google recommends that in order to provide a “Good” end-user experience, <a href="https://developers.google.com/speed/docs/insights/v5/about#categories" target="_blank">pages should load in 2.5 seconds or less</a>. Because servers usually exist in fixed geographical locations, the further the original request is from the origin server, the longer the request will take to make the journey there and back to the customer. And if your customers are browsing on a smart phone over a 3G or 4G connection, the request may take even longer. <a href="https://www.thinkwithgoogle.com/marketing-strategies/app-and-mobile/mobile-page-speed-new-industry-benchmarks/" target="_blank">This 2018 report from Google</a> highlights that a bounce rate probability increases by 32% if page load time increases to three seconds, and that probability soars to 123% if page load time increases to 10 seconds.</p><p class="post__p">Not ideal! So what’s the alternative? Is there a way to make pages <b class="post__p--italic">feel</b> faster to users to prevent them bouncing away?</p><h2 class="post__h2">Client-side Rendering (CSR)</h2><p class="post__p">Client-side Rendering (CSR) is the process of rendering content in the browser using JavaScript. When a request is a made for a web page that uses CSR, the server sends back a placeholder HTML document with a JavaScript file that will render the rest of the page and fill in the blanks in the browser. Websites that use CSR often use “skeleton loaders” — animated UI placeholders that simulate the layout of a web page while JavaScript is fetching data, building HTML and adding it to the page. We use skeleton loaders in the Netlify app whilst we’re busy fetching your data in the background.</p><img src="https://images.ctfassets.net/56dzm01z6lln/42IJCV5pruUYNcECmcEtPD/2c6a84b579845edb4345b9f2fff091b3/skeleton.gif" alt="Netlify sites dashboard, showing the animated light grey skeleton loaders before the site data is available." height="566" width="800" /><p class="post__p">CSR became increasingly popular with the mainstream adoption of JavaScript in the browser during the late 1990s. Its place as a core component in the web ecosystem was further solidified with the evolution of Single Page Application (SPA) frontend framework technologies such as React. Like SSR, CSR is best suited to dynamic up-to-date data, but it comes with some drawbacks.</p><p class="post__p">While we can use skeleton loaders to give the <b class="post__p--italic">impression</b> of a faster site as JavaScript is being processed, initial page load times can creep up without careful monitoring and testing. A combination of slow internet speeds, old devices, increasing web page complexity, buggy browser plugins or JavaScript simply not being available in the browser all contribute to CSR not being the best option to use across your <b class="post__p--italic">entire</b> e-commerce site.</p><p class="post__p">Additionally, CSR may degrade your search engine rankings. Most search engines can only crawl the content returned from URL — not the result of what might happen in the browser. If you use CSR to render your entire website, search engines will only ever be able to read your placeholder content, rather than the rich content that is loaded in by JavaScript. Technology is evolving, though. In releasing big updates to Googlebot in 2019, Google reassured us that “<a href="https://youtu.be/Ey0N1Ry0BPM?t=282" target="_blank">modern JavaScript is no longer a blocker for SEO</a>.” But in 2023, many people are also using alternative search engines. Regardless of web crawler technology, it’s always worth being strategic about which content you choose to load with CSR to craft the optimum user experience.</p><p class="post__p">While CSR and skeleton loaders are a great solution for a web app like Netlify — where we certainly don’t want our customers’ sites, builds and deploys discovered by search engine crawlers — for product pages and landing pages you’ll want to use a rendering method that sends <b class="post__p--italic">actual</b> content from a server (not placeholder content), so that all search engines can read it and contextualize it, and your products can show up in search results.</p><p class="post__p">SSR and CSR are both suited to dynamic data that’s often personalized. But what if you need to serve content and pages that don’t change very often, such as FAQ pages, company information and editorial content?</p><h2 class="post__h2">Static Site Generation (SSG)</h2><p class="post__p">Static Site Generation (SSG) is the process of pre-generating HTML pages ahead of time, so that they’re ready to serve to customers instantly without the need for SSR. With the rise in popularity of static site generator tools in the mid 2010s came the ability to serve your site from a Content Delivery Network (CDN), such as <a href="https://www.netlify.com/blog/2016/04/15/make-your-site-faster-with-netlifys-intelligent-cdn/" target="_blank">Netlify’s CDN</a>. With a CDN, serving static pages becomes even faster because pages are cached and served from the closest server node location to the request, meaning the combination of SSG and a CDN can dramatically improve the perceived and actual performance of your e-commerce store. What’s more, given your website pages are pre-generated as full HTML files containing actual content, you’ll score more SEO points.</p><img src="https://images.ctfassets.net/56dzm01z6lln/RKo32QjnsNOCc5drnYU6b/c85b6ce76f066758bba820cc7dd59adf/netlify_cdn.png" alt="llustration of a globe, with dots representing server nodes, with lines connecting them." height="675" width="1200" /><p class="post__p">SSG is the ideal rendering method for your e-commerce store pages that don’t change often. Interestingly, what is considered “not often” in this context has probably changed wildly in the last few years thanks to modern CI/CD, automation and Netlify. “Not often” in a composable architecture might mean “not more than a few times a day”, whereas with some legacy monolithic architectures, that frequency would be unheard of!</p><p class="post__p">Blog posts, FAQs and campaign landing pages are all great candidates to be statically pre-generated. All you need to do is a quick rebuild of your site when you need to make content updates or publish new pages. However, as you increase the number of pages on your site and the number of content editors in your team, you may come up against some challenges.</p><p class="post__p">Each new static page you add to your site adds a little extra build time. As your site grows with new pages and content, your build times could end up long enough that they cause bottlenecks in your workflow. Additionally, as your content team grows and begins to publish more and more pages each day, your website could be in a constant state of build — which may prevent your developers from shipping new features and iterations. It’s worth mentioning that many modern static site generators are becoming capable of faster and faster builds, so this is often not a serious blocker, but we should still be mindful of this for very large and complex sites.</p><p class="post__p">Fortunately, there’s a way to update or publish new static pages on-the-fly without the need for a full site rebuild.</p><h2 class="post__h2">Stale While Revalidate (SWR)</h2><p class="post__p">Stale While Revalidate (SWR) is a caching pattern that allows for the regeneration of <b class="post__p--italic">single</b> statically rendered pages that have been modified or published, rather than rebuilding an entire site from scratch. With SWR, content authors can publish changes to a specific page without triggering a full site rebuild, resulting in faster site updates that don’t block your development teams.</p><p class="post__p">SWR allows for near-instant updates to static content while retaining the benefits of SSG. When you use SWR to render a specific page, a version of that page will be statically generated and cached during an initial build. When that page is updated (usually via a WebHook triggered from a Content Management System by your content authors), a rebuild of that page is <b class="post__p--italic"><b class="post__p--bold">not</b></b> triggered instantly, but the next time a customer requests that page — which kicks off a <b class="post__p--italic">revalidation</b>. If the data has changed, this causes the page to be regenerated in the background, whilst the cached (<b class="post__p--italic">stale</b>) version is served, and the next request for that page will receive the updated version.</p><p class="post__p">You may know of SWR as a way to regenerate changed page content without a full site rebuild from Next.js’s proprietary implementation — Incremental Static Regeneration (ISR). It’s important to bear in mind that with SWR/ISR, <b class="post__p--bold">some</b> customers may see outdated content as the updated page is rebuilt on the server and cached. You won’t want to use SWR for pages displaying data that should be accurate and up to date, such as pricing.</p><p class="post__p">While you can use SWR to make incremental updates to pages without a full site rebuild, it’s best practice to provide a fallback page to serve if a <b class="post__p--italic">new</b> page hasn’t been generated in time with SWR via a background process. Fortunately, there’s also a way to speed up new page generation on large sites that doesn’t require a lengthy full site rebuild.</p><h2 class="post__h2">Distributed Persistent Rendering (DPR)</h2><p class="post__p">Being selective about which pages you choose to pre-generate can significantly decrease your build times and speed up your workflow. Instead of pre-building your entire site in advance (using SSG, for example) and updating pages incrementally with SWR, Distributed Persistent Rendering (DPR) allows you to statically generate pages <b class="post__p--italic">on demand</b> when they are requested for the first time, and subsequent requests for that page will be served from the cache. The <b class="post__p--italic">first request</b> to a page using DPR will result in an SSR-like experience. While you’ll want to ensure that your most popular content pages are statically pre-generated, DPR is useful for pages that are less critical to customer conversion.</p><p class="post__p">Netlify supports DPR and SWR through the use of <a href="https://docs.netlify.com/configure-builds/on-demand-builders/" target="_blank">On-demand Builders</a> — serverless functions used to generate web content as needed that’s automatically cached on Netlify’s Edge CDN.</p><p class="post__p">SSG, SWR and DPR put the focus on <b class="post__p--italic">static</b> pages that are pre-generated or re-generated on demand. But with an e-commerce store, a personalized and more dynamic shopping experience can deliver higher conversion rates. But how can you personalize store pages without sacrificing performance and falling back to using SSR or CSR?</p><h2 class="post__h2">Edge Side Rendering (ESR)</h2><p class="post__p">Edge Side Rendering (ESR) harnesses the power of CDN edge nodes to deliver SSR at the closest server location to individual users, providing the benefits that come with traditional SSR such as personalization and dynamic data, with improved speed for everyone around the world. ESR can be implemented for a full site, single pages, or even for just parts of pages — which is where ESR on Netlify really shines.</p><p class="post__p">ESR on Netlify is provided by <a href="https://docs.netlify.com/edge-functions/overview/" target="_blank">Netlify Edge Functions</a>, which are serverless functions executed at the edge that can intercept an HTTP request and modify the HTTP response <b class="post__p--italic">before</b> it is sent to the browser. This means that you can use ESR to <b class="post__p--italic">enhance your static sites and pages at the time of the request</b>. When you pre-build as much as possible — or as much is appropriate for your store — and use Edge Functions to modify pages when you need, you retain the speed of SSG with the power of making dynamic updates to your pages when you need to.</p><p class="post__p">Netlify Edge Functions can move much of the personalization work traditionally performed on a fixed-location server or in a browser using JavaScript to the edge. For example, you might want to show different types of product collections based on browser cookies, query parameters, or HTTP referrer headers, or you may want to redirect customers to different URLs.</p><p class="post__p">Phew! That’s a lot of acronyms: SSR, CSR, SSG, SWR, ISR, DPR, ESR. But how do you decide which types of rendering to use for your online store?</p><h2 class="post__h2">The Rule of Least Power</h2><p class="post__p">The Rule of Least Power is a principle that advises developers to use the simplest solution that achieves their goals. In the context of rendering, this means using the rendering mode that uses the least computational complexity (on the server and in the browser) to provide the best user experience (speed, personalization, data accuracy, etc.) in combination with the needs of your content team and developers.</p><p class="post__p">The Rule of Least Power with regards to rendering can be summarized as: <b class="post__p--italic"><b class="post__p--bold">Where possible, use SSG as standard, and enhance with other rendering modes where it’s needed</b></b>. Does your site comprise mostly static content with a few dynamic elements? Use SSG and enhance with ESR. Is your content team publishing 1000s of new editorial pages per week? Consider using SSG enhanced with DPR, and so on.</p><h2 class="post__h2">Choosing your rendering approach</h2><p class="post__p">Ultimately, the technologies you choose — such as hosting platform and frontend framework — will determine which rendering modes are available to your online store. But understanding the pros and cons, and the fit of the different rendering approaches to your project is a great way to help you choose your tools and technologies, rather than letting those choices dictate your approach.</p><p class="post__p">If you’ve outlined your rendering needs for your e-commerce store, and you’d like to make the move to a composable architecture on Netlify with all of this in mind, take a look at our <a href="https://www.netlify.com/composable-commerce/" target="_blank">Composable Commerce guides and resources</a>, or <a href="https://www.netlify.com/p/composable-web-assessment/?attr=composable-commerce-landing-page-hero" target="_blank">get in touch with Netlify today</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Make time</title>
          <description>I used use a week of PTO to see what tips and tricks I could experiment with to try and fix my life. Here’s how it went.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/make-time/</link>
          <guid>https://whitep4nth3r.com/blog/make-time/</guid>
          <pubDate>Fri, 17 Mar 2023 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I recently returned to work from a short week of PTO and a rejuvenating weekend visit with a very good friend. To be honest, I really needed it. I was feeling entirely overwhelmed with conflicting priorities at work, the seemingly never-ending winter of the UK, and the <a href="https://www.youtube.com/watch?v=RBZJM5v4KB8" target="_blank">absolute relentlessness of life</a>. It was time to take stock and make some changes.</p><p class="post__p">A few weeks ago I had read about the book <a href="https://www.amazon.com/Make-Time-Focus-Matters-Every/dp/0525572422/" target="_blank">Make Time: How to focus on what matters every day by Jake Knapp and John Zeratsky</a> (not sponsored) via <a href="https://buttondown.email/cassidoo/archive/talent-without-working-hard-is-nothing-cristiano/" target="_blank">Cassidy Williams’ newsletter</a> and it piqued my interest. I added it to my cart that day, checked out, and decided to use my week of PTO to see what tips and tricks I could experiment with to try and fix some of the nonsense I described above. Here’s how it went.</p><h2 class="post__h2">I changed how I used my calendars</h2><h3 class="post__h3">I scheduled Focus Time</h3><p class="post__p">I added a block of Focus Time for 2.5 hours every morning — from 08:30-11:00. This is when I have fewest (or mainly zero) meetings due to most of my colleagues being in the US, and it&#39;s when I&#39;m at my best in terms of energy.</p><h3 class="post__h3">I scheduled Admin Time</h3><p class="post__p">I don&#39;t get very many emails, but I need to keep on top of a lot of activity in the Netlify Slack. I added half an hour each day of Admin Time to my calendar in the afternoon, giving myself a dedicated window to properly filter through the messages I needed to care about, without being reactive throughout the day. Adding this calendar block to the afternoon also meant that I could be online and available for my US colleagues without feeling like I was distracted. </p><h3 class="post__h3">I scheduled Exercise Time as part of my work day</h3><p class="post__p">As described in <a href="https://whitep4nth3r.com/blog/2022-in-review/">2022: what I made, what I learned, and what I didn't do</a>, I&#39;m still doing my daily walks (141 days and counting!), and I have been scheduling time on my calendar for this since October last year. However, this was previously scheduled on my personal calendar meaning the event always felt adjacent to my work day, rather than part of it. To remedy this I&#39;ve moved the exercise block to my work calendar, and this has also made it easier to tesselate events in my day by managing one calendar, instead of switching back and forth between the two. (Yes, I use a calendar consolidation tool, but I had to keep flipping back and forth between Google profiles to manage events on both work and personal calendars.)</p><h3 class="post__h3">I revised the schedule when it changed</h3><p class="post__p">I also used Jake&#39;s strategy of reflecting and revising my schedule when it changed (as described on page 67-68 of Make Time). Sometimes I&#39;d finished what I wanted to achieve in my Focus Time early, so I decided to move my Admin Time to earlier in the day. When this happened I moved the event on my calendar, encouraging me to reflect on how I used time compared to how I planned it. As the weeks go on, I&#39;ll be able to plan my time more effectively.</p><h2 class="post__h2">I changed how I prioritise work</h2><p class="post__p">Previously, everything was a priority that needed to be done <b class="post__p--italic">now</b>. No wonder I&#39;d been feeling overwhelmed.</p><h3 class="post__h3">The Burner List</h3><p class="post__p">This week I experimented with using Jake&#39;s technique of prioritisation: <b class="post__p--italic">The Burner List</b>. </p><blockquote class="post__blockquote"><p class="post__p">It won&#39;t track every detail of every project or help you juggle a million tasks. But that&#39;s exactly the point. The Burner List is intentionally limited. It forces you to acknowledge that you can&#39;t take on every project or task that comes your way. Like time and mental energy, the Burner List is limited, and so it forces you to say no when you need to and stay focused on your number one priority. — <b class="post__p--italic">Make Time, page 52</b></p></blockquote><p class="post__p">Jake recommends to build your Burner List on a piece of paper. But I decided to do it using colour-coded post it notes on my office window. (It turns out I&#39;d <b class="post__p--italic">really</b> missed tracking work physically since moving to full time remote work!) </p><img src="https://images.ctfassets.net/56dzm01z6lln/65KhTFDYBO1jTybU9qF9sa/bc75578a3299ebdb28b6cb2d623b110b/burner_list.png" alt="Photo of my burner list in colour coded post it notes on my window, with my garden out of focus in the background." height="3024" width="3024" /><p class="post__p">Building a Burner List forced me to really hone in on my priorities, and discard anything from my brain that was slowing me down. As a result of making my front burner project something work <b class="post__p--italic">adjacent</b> (my website redesign), I made sure that I switched my highlight each day back and forth between my front burner project and some back burner projects. This felt really productive! It also meant that things felt varied and exciting, and progress felt <b class="post__p--italic">extremely </b>tangible.</p><p class="post__p">I&#39;m going to continue to use the Burner List for a few weeks and see how it settles. I&#39;m already looking forward to next Monday when I can rebuild it and reprioritise.</p><h2 class="post__h2">I changed how I use my phone</h2><blockquote class="post__blockquote"><p class="post__p">A distraction-free phone restores a feeling of quiet throughout my day. The slower pace of attention is not only helpful when I&#39;m trying to get into Laser mode; it&#39;s also just a more pleasant way to spend time. — <b class="post__p--italic">JZ, Make Time, page 94</b></p></blockquote><p class="post__p">My phone has been on silent for <b class="post__p--italic">years</b>, and most notifications are turned off (including email). However, there was still some work to do to encourage me to be less distracted by the temptations of my phone. </p><p class="post__p">I shifted all my apps around to declutter and to <b class="post__p--italic">force myself to scroll even further to distracting apps</b>. When I unlock my phone, I now see a blank home screen (with a wallpaper that tells me to STAY FOCUSED), giving me that little bit of extra time to consider whether I want to scroll to the right. </p><img src="https://images.ctfassets.net/56dzm01z6lln/2FQC8iG9bRgZk8uMOV6gzZ/09372f64b8f951bc14429ccef7895d70/stay_focused.png" alt="The words stay focused in capital letters in white, on a background of nice green trees." height="627" width="627" /><p class="post__p">I also split up different types of apps into separate screens to create more space, and this also forces me to have to scroll right <b class="post__p--italic">seven times</b> if I want to open up my email app. (I&#39;d love to remove email from my phone altogether but right now it&#39;s important to keep for a number of life admin reasons!)</p><h2 class="post__h2">How I feel after one week</h2><h3 class="post__h3">I actually made time</h3><p class="post__p">Over the years, I have been <b class="post__p--italic">incredibly </b>guilty of this:</p><blockquote class="post__blockquote"><p class="post__p">After a good hour or even fifteen minutes of productive time, I&#39;ll often think to myself: &quot;Man, that was a solid chunk of work. I should reward myself by checking Twitter!&quot; — <b class="post__p--italic">JZ, Make Time, page 218</b> </p></blockquote><p class="post__p">I caught myself thinking this on a number of occasions throughout the week, but I resisted. And the positive effect on both my productivity and perceived chaos around me were noticeable. This week, I have mainly been checking twitter during <b class="post__p--italic">Admin Time</b>, when I actually had to post something, or not at all. I had better things to do. Yes, I slipped up a few times and had to find ways to laser in on my focus, but honestly, this was probably one of the most productive weeks I’ve had in a very long time.</p><h3 class="post__h3">I was more focussed in meetings</h3><p class="post__p">As well as giving myself uninterrupted Focus Time, I gave other people the same. It felt good, more wholesome, and more rewarding. (Y&#39;all know you&#39;re checking Twitter during meetings... 👀)</p><h3 class="post__h3">I let things wait</h3><p class="post__p">I’m someone who wants to get things done as soon as they come to me. <b class="post__p--italic">Get it done and tick it off the to do list and get it out of my life</b>. But I’ve grown tired of being reactive. It&#39;s chaotic and draining. And as Jason Lengstorf once said to me: “Nothing in your job is an emergency.” I applied this to my home life too. And honestly, it was liberating. If new tasks came my way, I scheduled them and dealt with them at an appropriate time. On reflection, it was extremely beneficial not having scramble to fit in everything under the facade of “ending the day with an empty to do list.” </p><p class="post__p">The point is there&#39;s <b class="post__p--italic">never</b> an empty to do list. Because life is relentless. We just have to find ways to cope.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How do I get started in web development?</title>
          <description>Asking “How do I get started with web dev?” is like asking “How do I get started with cooking?”</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-do-i-get-started-in-web-development/</link>
          <guid>https://whitep4nth3r.com/blog/how-do-i-get-started-in-web-development/</guid>
          <pubDate>Fri, 06 Jan 2023 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Asking “How do I get started with web dev?” is like asking “How do I get started with cooking?”</p><p class="post__p">Decide what you want to make. And then reframe the question.</p><p class="post__p">“<b class="post__p--bold">How do I make lasagne?</b>” or “<b class="post__p--bold">How might I build a blog?</b>” is more straightforward to answer — and you’ll get better help.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>2022: what I made, what I learned, and what I didn't do</title>
          <description>Let's take a look back at 2022: what I made, what I learned — both professionally and personally — and what I didn't get around to.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/2022-in-review/</link>
          <guid>https://whitep4nth3r.com/blog/2022-in-review/</guid>
          <pubDate>Tue, 03 Jan 2023 00:00:00 GMT</pubDate>
          <category>Off-Topic</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Welcome to 2023, and to the first kind of &quot;off-topic&quot; blog post from me. What I&#39;d really like to do this year is use this blog to write about more than just tech, the web and tutorials. I&#39;d like to share and explore some more personal thoughts, create more short-form content, and use this space on the internet to let you learn more about me.</p><p class="post__p">But first, let&#39;s take a look back at 2022: what I made, what I learned — both professionally and personally — and what I didn&#39;t get around to.</p><h2 class="post__h2">Working at Netlify</h2><p class="post__p">On 11th January 2022, I joined Netlify as a Staff Developer Experience Engineer. I&#39;m really happy here; I have some wonderful colleagues, I love the work I do, and for the first time in two years, I&#39;m not changing jobs this January!</p><p class="post__p">One of the biggest highlights of my work at Netlify this year was working on the FYI Fridays series, for which I also composed and performed the theme tune! More useful tips and tricks from Netlify are coming your way in 2023.</p><p class="post__p">I also received a <a href="https://mvp.microsoft.com/en-us/PublicProfile/5004440?fullName=Salma%20Alam-Naylor" target="_blank">Microsoft MVP Award</a> for the second year running for my contributions to the Developer Technologies community. 🎉</p><h2 class="post__h2">Lots of writing</h2><p class="post__p">In 2022 I wrote <b class="post__p--bold">29 blog posts</b>. I gained three top 7 spots on DEV.to for <a href="https://dev.to/devteam/top-7-featured-dev-posts-from-the-past-week-2p64" target="_blank">CSS</a>, <a href="https://dev.to/devteam/top-7-featured-dev-posts-from-the-past-week-1oaj" target="_blank">HTML</a> and <a href="https://dev.to/devteam/top-7-featured-dev-posts-from-the-past-week-4phi" target="_blank">git</a> articles, and earned 10 web ecosystem newsletter features including <a href="https://jamstack.email/issues/55" target="_blank">Jamstack</a> and <a href="https://serverless.email/issues/243" target="_blank">Serverless Cooperpress</a>, <a href="https://javascriptweekly.com/issues/609" target="_blank">JavaScript Weekly</a>, and <a href="https://thisweekinreact.com/newsletter/119" target="_blank">This Week in React</a>.</p><p class="post__p">I also co-authored the <a href="https://almanac.httparchive.org/en/2022/jamstack" target="_blank">2022 Jamstack chapter of the Web Almanac</a> with Laurie Voss.</p><p class="post__p">Here&#39;s the list!</p><ul><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/how-to-use-really-long-environment-variables-in-netlify-functions/">How to use really long environment variables in Netlify functions</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/debug-css-layouts/">Debug your CSS layouts with this one simple trick</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/how-to-build-html-accordion-no-javascript/">How to build an HTML-only accordion — no JavaScript required!</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/improved-google-lighthouse-seo-score/">How I improved your Google Lighthouse SEO score</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/how-to-deploy-your-netlify-site-with-an-elgato-stream-deck/">How to deploy your Netlify site with an Elgato Stream Deck</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/previewing-posts-best-decoupled-content-management-workflow-for-your-static-site/">Build a CMS preview workflow for your Jamstack site</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/women-of-jamstack-prototype-eleventy/">A prototype is all you need to launch a site</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/why-ship-silly-side-projects/">Why you should ship your silly side projects</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/delete-all-merged-git-branches-one-terminal-command/">How to delete all merged git branches with one terminal command </a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/how-to-format-dates-for-rss-feeds-rfc-822/">How to format dates for RSS feeds (RFC-822)</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/how-i-improved-website-performance/">How I massively improved my website performance by using the right tool for the job</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/html-is-all-you-need-to-make-a-website/">HTML is all you need to make a website</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/what-is-the-edge-serverless-functions/">We're all living on it. But what exactly is The Edge?</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/what-is-jamstack/">What is Jamstack?</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/quick-light-dark-mode-css/">Light and dark mode in just 14 lines of CSS</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/level-up-your-link-previews-in-slack/">Level up your link previews in Slack</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/build-a-business-card-cli-tool/">Build a business card CLI tool</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/how-to-deploy-astro/">How to deploy an Astro site</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/should-i-write-a-new-javascript-framework/">Should I write a new JavaScript framework?</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/view-google-lighthouse-scores-visualizations/">How to view Google Lighthouse scores for your site in Netlify</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/add-personalization-to-static-html-with-edge-functions-no-browser-javascript/">Add personalization to static HTML with Netlify Edge Functions — no browser JavaScript required</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/rewrite-html-transform-page-props-in-nextjs/">Rewrite HTML and transform page props in Next.js with Next.js Advanced Middleware</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/pseudo-classes-and-pseudo-elements/">What's the difference between : and :: in CSS?</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/rewrite-git-history/">Rewrite your git history in 4 friendly commands</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/write-a-new-javascript-framework/">I changed my mind about writing new JavaScript frameworks</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/personalize-static-site-based-on-previous-site-referral/">Personalize your static site based on a previous site referral</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/twitter-tech-history-spa/">Single-page applications, multi-page applications, the history of Twitter tech, and a failed project</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/responsive-striped-css-pattern-80s-vhs-tapes/">A responsive striped CSS gradient inspired by 80s VHS tapes</a></p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/how-to-get-timezone-in-javascript-with-edge-functions/">How to get the user’s timezone in JavaScript with Edge Functions</a></p></li></ul><h2 class="post__h2">Live Streams, Videos and Events</h2><p class="post__p">In 2022 I streamed <a href="https://twitch.tv/whitep4nth3r" target="_blank">live coding on Twitch</a> for a total of <b class="post__p--bold">225 hours and 27 minutes</b>! Live streaming still remains my most favourite way to connect with a community of developers whilst learning new things, experimenting, and yolo-deploying new projects. And whilst I stopped focussing on follower counts and growth so much in 2022 for the sake of my mental health, I think it&#39;s worth mentioning that I more-than-doubled my Twitch following last year, gaining 4,631 followers and surpassing 8k. 🥳</p><p class="post__p">On YouTube, <a href="https://www.youtube.com/@whitep4nth3r/videos" target="_blank">I released five videos</a> of Twitch stream highlights (I still struggle with creating my own YouTube-focussed content) and <a href="https://www.youtube.com/@whitep4nth3r/shorts" target="_blank">five experimental shorts</a>. I&#39;m still not sure whether YouTube is my bag, but I&#39;ll talk more about this another time.</p><p class="post__p">I also published two new tutorials on the Netlify YouTube channel on <a href="https://www.youtube.com/watch?v=6pEVhH37xQE" target="_blank">Edge Functions</a> and <a href="https://www.youtube.com/watch?v=_KZIs-8oulw" target="_blank">Next.js Advanced Middleware</a>, and was <a href="https://www.youtube.com/watch?v=k_gNnGrKsug" target="_blank">interviewed by Tom at Code Creative</a>.</p><p class="post__p">I featured in nine podcasts:</p><ul><li><p class="post__p"><a href="https://thatsmyjamstack.com/episodes/salma-alam-naylor/" target="_blank">That&#39;s My Jamstack: Shipping, learning and rendering</a></p></li><li><p class="post__p"><a href="https://remotelyinteresting.netlify.com/episodes/029-living-on-the-edge-of-glory/" target="_blank">Remotely Interesting: Living On The Edge of Glory</a></p></li><li><p class="post__p"><a href="https://remotelyinteresting.netlify.com/episodes/031-minimum-viable-web-dev-knowledge/" target="_blank">Remotely Interesting: Minimum Viable Web Dev Knowledge</a></p></li><li><p class="post__p"><a href="https://stackoverflow.blog/2022/06/24/living-on-the-edge-with-netlify-ep-456/" target="_blank">Stack Overflow Podcast: Living on the Edge with Netlify</a></p></li><li><p class="post__p"><a href="https://www.remotelyinteresting.dev/episodes/036-tool-decision-paralysis/" target="_blank">Remotely Interesting: Tool Decision Paralysis</a></p></li><li><p class="post__p"><a href="https://www.remotelyinteresting.dev/episodes/038-thinking-in-serverless/" target="_blank">Remotely Interesting: Thinking in Serverless</a></p></li><li><p class="post__p"><a href="https://podrocket.logrocket.com/livestreaming-whitep4nth3r" target="_blank">PodRocket: Live streaming and learning in public</a></p></li><li><p class="post__p"><a href="https://developers.wpengine.com/podcast/11722594" target="_blank">The JAMstack | Headless WP Podcast</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=s1MJxA55NJQ" target="_blank">The Dev Morning Show at Night: What Music Can Teach Us About Code</a></p></li></ul><p class="post__p">and 10 talks and events:</p><ul><li><p class="post__p"><a href="https://www.netlify.com/resources/webinars/next-js-on-netlify-a-powerful-combination/" target="_blank">Next.js on Netlify live webinar</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=-ttCul_-nns" target="_blank">Learning with Luce @ MongoDB</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=wpd9gtI407s" target="_blank">April 11ty Meetup</a></p></li><li><p class="post__p"><a href="https://coderpad.io/events/jamstack-enabling-beginners-to-do-more/" target="_blank">Jamstack: Enabling Beginners to Do More @ CoderPad</a></p></li><li><p class="post__p"><a href="https://someantics.dev/right-tool-for-the-job/" target="_blank">Some Antics: Picking the Right Tool for the Job on the Jamstack with Ben Myers</a></p></li><li><p class="post__p"><a href="https://www.netlify.com/events/headless-commerce-summit-2022/on-demand/" target="_blank">MC @ Headless Commerce Summit</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=l5TxXc3tD-w" target="_blank">Purrfect Dev</a></p></li><li><p class="post__p"><a href="https://cfe.dev/sessions/moar2022-living-on-the-edge/" target="_blank">Moar Serverless</a></p></li><li><p class="post__p"><a href="https://meetup.thefusionhub.co.uk/events/meetup-2022-09-29" target="_blank">The Fusion Group Meetup</a></p></li><li><p class="post__p"><a href="https://lu.ma/fh-holiday-2022" target="_blank">Front End Horse Holiday Snowtacular 2022</a></p></li></ul><h2 class="post__h2">Websites and projects</h2><p class="post__p">In 2022, I deployed <a href="https://your-year-on.netlify.com/whitepanther/9f259804a7f5/" target="_blank">21 new sites</a>! These range from demo sites, single HTML files, fun experiments and most recently — <a href="https://github.com/whitep4nth3r/the-claw-webring-widget" target="_blank">a Web Ring Web Component</a>. </p><img src="https://images.ctfassets.net/56dzm01z6lln/13XUJfXDKsFoBuFUbrOkli/91c1bbfd16703d8797f11ae3d7188cd7/year_on_netlify_2022.jpeg" alt="My year on Netlify. 3 years on Netlify. 21 new sites. 326 builds. 17 day build streak. 59 deploys on Friday. 7 frameworks used. 164644 function calls on 15 sites." height="630" width="1200" /><p class="post__p">The biggest lesson I learned this year is that <b class="post__p--bold">not all new projects need to be built and engineered for longevity</b>. Some projects can be ephemeral, some projects can exist as small ideas to inspire others, and some projects probably aren&#39;t even projects at all (and that&#39;s why I&#39;m not listing them all here). And what&#39;s more, it&#39;s okay to admit defeat, archive things, and move on. </p><h3 class="post__h3">I archived some projects</h3><p class="post__p">A running theme of 2022 has been about reducing my mental load to be able to focus on what&#39;s most important at any given time. For this reason I made the heavy decision to archive three projects that I launched in 2021 and 2022: Women of Jamstack, unbreak.tech and Women Who Stream. The story behind these projects and why I archived them is a blog post for another time, but let&#39;s just say I wanted to shift my public-facing focus to being a technologist first, rather than an activist. (Because let&#39;s face it, none of us can <b class="post__p--italic">really</b> change the world, can we?)</p><h2 class="post__h2">The Claw Stream Team and Community</h2><p class="post__p">I continued to grow <a href="https://theclaw.team/" target="_blank">The Claw Stream Team</a>. We&#39;re now up to 22 members and the Discord community is steadily growing at a healthy rate. We staged two team raidathons in 2022 with massive success, and held the <a href="https://theclaw.team/awards/2022" target="_blank">second annual community awards ceremony</a>. More of this to come in 2023!</p><h2 class="post__h2">I moved house!</h2><p class="post__p">After ten years in our first very tiny house, we moved to a proper family house in June 2022. It&#39;s been wonderful to finally have the space to spread out, and to work from home more comfortably! Given combining project-managing renovations, full time jobs and parenting proved to be a little stressful, we&#39;re taking a bit of a break for the first half of 2023. I have to say though, it would have been <b class="post__p--italic">much</b> worse without being able to escape the noise in my little office at the bottom of the garden! Look at that sunrise. 😍</p><img src="https://images.ctfassets.net/56dzm01z6lln/3YC8J3fPfvTRYRD9RrsPof/d81492a13fa7bc102766cbc8e559a498/office_sunset.png" alt="My wooden chalet style office at the bottom of the garden, with a very orange sunrise backdrop fading into a purple hue." height="2223" width="2964" /><h2 class="post__h2">I took control of my health</h2><p class="post__p">After two and a half years of pain, fatigue and weirdness that started at the beginning of the pandemic, I finally received a diagnosis in October 2022 of a chronic illness (gastroparesis, for the curious). Before the diagnosis, I was convinced that the treatment would be a quick fix; I&#39;d have an operation, and I&#39;d go back to my normal life pain-free. But given there&#39;s no cure for this condition, I had to take matters into my own hands to improve my quality of life. </p><h3 class="post__h3">Physical health</h3><p class="post__p">At the time of writing this post, <b class="post__p--bold">I have exercised daily for 71 consecutive days for at least 30 minutes</b>, rain or shine, without fail, regardless of how tired or stressed or busy or grumpy I am. I cannot stress how much this has improved not just how manageable my condition is, but also my mental health and my physical fitness. I&#39;ve been taking around 10k steps a day (including on Christmas Day!), and according to my Apple Watch, I have shifted my cardio fitness levels from below average to above average. For me, exercise used to always be about losing weight. And whilst I have become healthier in this regard in the last 71 days, it&#39;s actually so much more than that. Physical activity is something <b class="post__p--italic">for me</b>, it&#39;s now part of my life. I can&#39;t <b class="post__p--italic">not</b> do it. And plus, a nice healthy walk outside gives me some extra vitamin D and some time to myself to think.</p><h3 class="post__h3">Mental health</h3><p class="post__p">After a conversation with a friend where I vented about not being able to make as much music as I wanted, she recommended a 12-week course laid out in <a href="https://www.theartistswaybook.com/" target="_blank">The Artist&#39;s Way</a> by Julia Cameron. I&#39;m currently on week eight of the course (which I should probably do all over again to get better results), but the most transformative part of the course for me has been <a href="https://juliacameronlive.com/basic-tools/morning-pages/" target="_blank">The Morning Pages</a>. What are Morning Pages, I hear you ask?</p><p class="post__p">In the morning, ideally before you do anything else, write three pages of longhand, stream of consciousness writing. Sometimes I write one page or two pages or three pages (usually in bed with a cup of tea), but I always do it. I&#39;ve done this for around the same length of time as my daily exercise, and this is something that I will take with me to 2023. The Morning Pages allows me to vent, explore and generate ideas, work out anxieties, and just be myself for myself. And in fact, the very idea for this blog post came out of my morning pages today.</p><h2 class="post__h2">I changed how I use social media</h2><p class="post__p">On the theme of mental health, in October 2022 I realised I had been &quot;online&quot; and working <b class="post__p--italic">continuously</b> for two and a half years, all thanks to social media and the blurry lines of personal and professional lives in the tech industry. I had tried the whole &quot;no social media&quot; and going off grid half-heartedly in the past, but this time I went all out.</p><p class="post__p"><b class="post__p--bold">Since October 2022, I don&#39;t use any social media outside of work hours</b>. I deleted all apps from my phone and iPad, deleted social media accounts that weren&#39;t making me happy (including TikTok and Polywork), and my mental health has greatly improved. For the first time since I started learning in public, I finally have separation between my work life and personal life. I have more space for hobbies (I&#39;m taking up cross-stitch!), connecting with friends and family, and just switching off in general. I highly recommend this.</p><h2 class="post__h2">I travelled!</h2><p class="post__p">In 2022 I went to the US for the first time for a Netlify team event. Despite not getting over the jet lag for the whole four-day trip, it was fun to fly long-haul for the first time since visiting Canada in 2012. I&#39;m looking forward to more of this in 2023!</p><h2 class="post__h2">What I didn&#39;t get around to</h2><p class="post__p">There were so many more things I wanted to do in 2022, but I&#39;m trying not to beat myself up too much about it. Here&#39;s a selection of to-do items on my Trello board.</p><p class="post__p">I had planned on starting up a YouTube series called <b class="post__p--bold">Weird Web News Hole</b> where I wanted to showcase the weird, wonderful and nostalgic things happening across the web every week. But again, YouTube-focussed content is something I really struggle with; I prefer to create my content live. </p><p class="post__p">I wanted to start work on an <b class="post__p--bold">HTML for Kids</b> YouTube series.</p><p class="post__p">I wanted to start up a series of casual events in The Claw Discord, but I&#39;m only one person, so I might have to rally a group of volunteers to help.</p><p class="post__p">There&#39;s also the very pressing issue of needing to refactor most of my Twitch Bot before the old APIs are deprecated. I should really prioritise that. </p><p class="post__p">I wanted to make a new Twitch trailer, and a trailer for the stream team featuring some of the best clips. </p><p class="post__p">I wanted to write ten more short blog posts.</p><p class="post__p">I wanted to build two Chromium extensions.</p><p class="post__p">I wanted to dive into the <a href="https://vice-emu.sourceforge.io/" target="_blank">VICE Commodore 64 emulator</a> to relive some of my childhood.</p><p class="post__p">And finally, I wanted to get back to making music, which was the driver that got me started on The Morning Pages. Maybe <a href="https://indieweb.social/@whitep4nth3r/109505397816287755" target="_blank">my new musical toy</a> will help with that, but I&#39;m probably just a sucker for <a href="https://en.wikipedia.org/wiki/Shopping_addiction#Gear_Acquisition_Syndrome_(G.A.S.)" target="_blank">gear acquisition syndrome</a>.</p><p class="post__p">2022 was a year, wasn&#39;t it? Here&#39;s to 2023. 🥂</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to get the user’s timezone in JavaScript with Edge Functions</title>
          <description>You don‘t need client-side JavaScript to localize dates and times according to timezone — use timezone data in Netlify Edge Functions.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://ntl.fyi/3FjEMGX</link>
          <guid>https://ntl.fyi/3FjEMGX</guid>
          <pubDate>Mon, 12 Dec 2022 00:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In this vast, virtual and remote-first world, where we work, socialize and live our lives across timezones, it’s often helpful if dates and times are formatted and adapted according to different locales and timezones. Attending a virtual conference in the US but you’re located in the UK? Scheduling a call with a friend half way around the world? You most definitely want to view the event schedules in your timezone! </p><p class="post__p">Unfortunately, adapting dates and times according to timezones can prove to be <b class="post__p--italic">extremely</b> tricky. On top of accounting for timezone offsets from UTC (Coordinated Universal Time) — which can vary even if locations are on the same longitude(!) — timezones also change at various points throughout the year due to daylight savings time. And the time of year in which timezones shift also varies depending on location! And this is also subject to change 😅! Thankfully, JavaScript provides us with a nice little library of tools to work with dates and times.</p><h2 class="post__h2">How to adapt and localize times in the browser with JavaScript</h2><p class="post__p">To adjust a date by timezone in JavaScript, you can use the Date object and its <code>toLocaleString()</code> method.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BusssROman"
      aria-describedby="BusssROman">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BusssROman">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="BusssROman" itemprop="text" content="%2F%2F%20create%20a%20new%20Date%20object%20with%20the%20current%20date%20and%20time%0Avar%20date%20%3D%20new%20Date()%3B%0A%0A%2F%2F%20use%20the%20toLocaleString()%20method%20to%20display%20the%20date%20in%20different%20timezones%0Aconst%20easternTime%20%3D%20date.toLocaleString(%22en-US%22%2C%20%7BtimeZone%3A%20%22America%2FNew_York%22%7D)%3B%0A%0Aconst%20londonTime%20%3D%20date.toLocaleString(%22en-GB%22%2C%20%7BtimeZone%3A%20%22Europe%2FLondon%22%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// create a new Date object with the current date and time</span><br><span class="token keyword">var</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token comment">// use the toLocaleString() method to display the date in different timezones</span><br><span class="token keyword">const</span> easternTime <span class="token operator">=</span> date<span class="token punctuation">.</span><span class="token function">toLocaleString</span><span class="token punctuation">(</span><span class="token string">"en-US"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token literal-property property">timeZone</span><span class="token operator">:</span> <span class="token string">"America/New_York"</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> londonTime <span class="token operator">=</span> date<span class="token punctuation">.</span><span class="token function">toLocaleString</span><span class="token punctuation">(</span><span class="token string">"en-GB"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><span class="token literal-property property">timeZone</span><span class="token operator">:</span> <span class="token string">"Europe/London"</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">You can also use the <code>Intl.DateTimeFormat()</code> method to localize the date and time and format it in a specific way, such as using a 24-hour clock or displaying the timezone abbreviation.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="FfkzIpbcUw"
      aria-describedby="FfkzIpbcUw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="FfkzIpbcUw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="FfkzIpbcUw" itemprop="text" content="%2F%2F%20create%20a%20new%20Date%20object%20with%20the%20current%20date%20and%20time%0Avar%20date%20%3D%20new%20Date()%3B%0A%0A%2F%2F%20create%20a%20DateTimeFormat%20object%20with%20specific%20options%0Avar%20dateFormat%20%3D%20new%20Intl.DateTimeFormat(%22en-US%22%2C%20%7B%0A%09timeZone%3A%20%22America%2FNew_York%22%2C%0A%09timeZoneName%3A%20%22short%22%0A%7D)%3B%0A%0A%2F%2F%20use%20the%20format()%20method%20to%20display%20the%20date%20and%20time%20in%20the%20specified%20format%0A%2F%2F%20(e.g.%20%2212%2F8%2F2020%2C%20EST%22)%0Aconst%20formatted%20%3D%20dateFormat.format(date)%3B%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// create a new Date object with the current date and time</span><br><span class="token keyword">var</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token comment">// create a DateTimeFormat object with specific options</span><br><span class="token keyword">var</span> dateFormat <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Intl<span class="token punctuation">.</span>DateTimeFormat</span><span class="token punctuation">(</span><span class="token string">"en-US"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>	<span class="token literal-property property">timeZone</span><span class="token operator">:</span> <span class="token string">"America/New_York"</span><span class="token punctuation">,</span><br>	<span class="token literal-property property">timeZoneName</span><span class="token operator">:</span> <span class="token string">"short"</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token comment">// use the format() method to display the date and time in the specified format</span><br><span class="token comment">// (e.g. "12/8/2020, EST")</span><br><span class="token keyword">const</span> formatted <span class="token operator">=</span> dateFormat<span class="token punctuation">.</span><span class="token function">format</span><span class="token punctuation">(</span>date<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">The problem with adapting and localizing times in the browser</h2><p class="post__p">Whilst using JavaScript in the browser is a good solution, there are — as usual! — some drawbacks. Most notably, users might see a flash of un-personalized content (FLUNP™️ — please send all credit for this fun acronym to <a href="https://ntl.fyi/3UOmCTI" target="_blank">Phil Hawksworth</a>) before the JavaScript kicks in, or they might not see a localized date at all if JavaScript fails to load.</p><p class="post__p">The traditional solution to this problem would be to perform any conditional logic on a server and deliver an adjusted and formatted date and time to the client at request-time. However, if you’re building a mainly static site, you probably don’t want to introduce a server just for the purposes of avoiding <b class="post__p--italic">potential</b> client-side drawbacks.  </p><p class="post__p">But I have some great news for you! <b class="post__p--italic"><b class="post__p--bold">Timezone data is now available in Netlify Edge Functions</b></b>, removing the need to use <b class="post__p--italic">any</b> client-side JavaScript to successfully localize dates and times <b class="post__p--italic">or</b> introduce a fully-fledged server. And what’s more, you can use all the JavaScript native Date APIs you’re already familiar with inside the edge function code. </p><p class="post__p">If you’re not yet familiar with Netlify Edge Functions, head on over to a <a href="https://ntl.fyi/3PjshOQ" target="_blank">previous tutorial</a> which shows you how to add personalization to a static HTML project using an edge function from scratch. There’s also a handy template project that you can deploy to Netlify at the click of a button.</p><p class="post__p"><b class="post__p--bold">The rest of this post assumes you’re familiar with Netlify Edge Functions and you’re using the Netlify CLI to develop and test your code locally.</b> Let’s do it 😎.</p><h2 class="post__h2">Get timezone data from an HTTP request</h2><p class="post__p">In a new or existing project, create a new edge function file and configure it in your netlify.toml file. In this example, I want the edge function code to run on `index.html`.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="WsNQSIgvkW"
      aria-describedby="WsNQSIgvkW">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="WsNQSIgvkW">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="WsNQSIgvkW" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Flocalize-time.js%0A%0Aexport%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// netlify/edge-functions/localize-time.js</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="SlAnjYGKTs"
      aria-describedby="SlAnjYGKTs">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="SlAnjYGKTs">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="SlAnjYGKTs" itemprop="text" content="%23%20netlify.toml%0A%0A%5B%5Bedge_functions%5D%5D%0A%20%20path%20%3D%20%22%2F%22%0A%20%20function%20%3D%20%22localize-time%22%0A">
      <pre class="language-toml"><code class="language-toml"><span class="token comment"># netlify.toml</span><br><br><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">edge_functions</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br>  <span class="token key property">path</span> <span class="token punctuation">=</span> <span class="token string">"/"</span><br>  <span class="token key property">function</span> <span class="token punctuation">=</span> <span class="token string">"localize-time"</span></code></pre>
    </div>
  </div>

  <p class="post__p">Just like in the browser, we’re going to use the JavaScript Date <code>toLocaleString()</code> method to adapt the date and time. We need two bits of information for this: the locale and the timezone. Assign the <code>accept-language</code> header from the <code>request</code> to a <code>locale</code> variable (and provide a sensible fallback), and grab the <code>timezone</code> data from <code>context.geo</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="qQRdfCbBfP"
      aria-describedby="qQRdfCbBfP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="qQRdfCbBfP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="qQRdfCbBfP" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Flocalize-time.js%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%2B%20%20%20const%20locale%20%3D%20request.headers%5B%22accept-language%22%5D%20%7C%7C%20%22en-GB%22%3B%20%20%20%0A%2B%20%20%20const%20%7B%20timezone%20%7D%20%3D%20context.geo%3B%0A%20%20%7D%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/localize-time.js<br><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br></span><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token keyword">const</span> locale <span class="token operator">=</span> request<span class="token punctuation">.</span>headers<span class="token punctuation">[</span><span class="token string">"accept-language"</span><span class="token punctuation">]</span> <span class="token operator">||</span> <span class="token string">"en-GB"</span><span class="token punctuation">;</span>   <br><span class="token prefix inserted">+</span>   <span class="token keyword">const</span> <span class="token punctuation">{</span> timezone <span class="token punctuation">}</span> <span class="token operator">=</span> context<span class="token punctuation">.</span>geo<span class="token punctuation">;</span><br></span><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Set up the HTML to be modified</h2><p class="post__p">In the HTML where you’re currently displaying a date and/or time, add a data attribute — <code>data-time=&quot;{YOUR_TIME}&quot;</code> — to the HTML element. In this example I’m using the JavaScript ISO date format. The <code>Z</code> at the end of the ISO string denotes that the time is in UTC. We’re going to use HTMLRewriter to grab this value, adjust it according to the timezone data and <code>accept-language</code> header, and modify the HTML response.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="qnkQbaCFCI"
      aria-describedby="qnkQbaCFCI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="qnkQbaCFCI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="qnkQbaCFCI" itemprop="text" content="%3C!--%20index.html%20--%3E%0A%0A%3Cspan%20data-time%3D%222022-12-09T12%3A00%3A00Z%22%3E2022-12-09T12%3A00%3A00Z%3C%2Fspan%3E%0A">
      <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- index.html --></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>span</span> <span class="token attr-name">data-time</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2022-12-09T12:00:00Z<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>2022-12-09T12:00:00Z<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>span</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Format and localize a date with JavaScript in Deno</h2><p class="post__p">Import HTMLRewriter at the top of your edge function, and add the following code to modify and return the HTML. Customize the date/time result to your preferences using the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options" target="_blank">options parameter</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="qFbZbacGZq"
      aria-describedby="qFbZbacGZq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="qFbZbacGZq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="qFbZbacGZq" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Flocalize-time.js%0A%0A%2B%20import%20%7B%20HTMLRewriter%20%7D%20from%20%22https%3A%2F%2Fghuc.cc%2Fworker-tools%2Fhtml-rewriter%2Findex.ts%22%3B%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20%20%20const%20locale%20%3D%20request.headers%5B%22accept-language%22%5D%20%7C%7C%20%22en-GB%22%3B%0A%20%20%20%20const%20%7B%20timezone%20%7D%20%3D%20context.geo%3B%0A%0A%2B%20%20%20%2F%2F%20capture%20the%20HTTP%20response%20so%20we%20can%20modify%20it%0A%2B%20%20%20const%20response%20%3D%20await%20context.next()%3B%0A%2B%0A%2B%20%20%20return%20new%20HTMLRewriter()%0A%2B%20%20%20%20%20.on(%22%5Bdata-time%5D%22%2C%20%7B%0A%2B%20%20%20%20%20%20%20element(element)%20%7B%0A%2B%20%20%20%20%20%20%20%20%20%2F%2F%20get%20the%20date%20value%20as%20a%20string%20from%20the%20HTML%20data%20attribute%0A%2B%20%20%20%20%20%20%20%20%20const%20dateString%20%3D%20element.getAttribute(%22data-time%22)%3B%0A%2B%0A%2B%20%20%20%20%20%20%20%20%20%2F%2F%20convert%20the%20string%20to%20a%20JavaScript%20date%0A%2B%20%20%20%20%20%20%20%20%20const%20date%20%3D%20new%20Date(dateString)%3B%0A%2B%0A%2B%20%20%20%20%20%20%20%20%20%2F%2F%20use%20toLocaleString()%20with%20the%20locale%20from%20the%20request%0A%2B%20%20%20%20%20%20%20%20%20%2F%2F%20and%20the%20timezone%20from%20context.geo%0A%2B%20%20%20%20%20%20%20%20%20const%20localizedTime%20%3D%20date.toLocaleString(locale%2C%20%7B%0A%2B%20%20%20%20%20%20%20%20%20%20%20timeZone%3A%20timezone%2C%0A%2B%20%20%20%20%20%20%20%20%20%20%20hour%3A%20%22numeric%22%2C%0A%2B%20%20%20%20%20%20%20%20%20%20%20minute%3A%20%22numeric%22%2C%0A%2B%20%20%20%20%20%20%20%20%20%20%20day%3A%20%22numeric%22%2C%0A%2B%20%20%20%20%20%20%20%20%20%20%20weekday%3A%20%22short%22%2C%0A%2B%20%20%20%20%20%20%20%20%20%20%20month%3A%20%22short%22%2C%0A%2B%20%20%20%20%20%20%20%20%20%7D)%3B%0A%2B%0A%2B%20%20%20%20%20%20%20%20%20%2F%2F%20modify%20the%20HTML%20element%0A%2B%20%20%20%20%20%20%20%20%20element.setInnerContent(%60%24%7BlocalizedTime%7D%60)%3B%0A%2B%20%20%20%20%20%20%20%7D%2C%0A%2B%20%20%20%20%20%7D)%0A%2B%20%20%20%20%20%2F%2F%20transform%20the%20original%20response!%0A%2B%20%20%20%20%20.transform(response)%3B%0A%20%20%7D%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/localize-time.js<br><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> HTMLRewriter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://ghuc.cc/worker-tools/html-rewriter/index.ts"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> locale <span class="token operator">=</span> request<span class="token punctuation">.</span>headers<span class="token punctuation">[</span><span class="token string">"accept-language"</span><span class="token punctuation">]</span> <span class="token operator">||</span> <span class="token string">"en-GB"</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> <span class="token punctuation">{</span> timezone <span class="token punctuation">}</span> <span class="token operator">=</span> context<span class="token punctuation">.</span>geo<span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token comment">// capture the HTTP response so we can modify it</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">HTMLRewriter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token prefix inserted">+</span>     <span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"[data-time]"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>       <span class="token function">element</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>         <span class="token comment">// get the date value as a string from the HTML data attribute</span><br><span class="token prefix inserted">+</span>         <span class="token keyword">const</span> dateString <span class="token operator">=</span> element<span class="token punctuation">.</span><span class="token function">getAttribute</span><span class="token punctuation">(</span><span class="token string">"data-time"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span><br><span class="token prefix inserted">+</span>         <span class="token comment">// convert the string to a JavaScript date</span><br><span class="token prefix inserted">+</span>         <span class="token keyword">const</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span>dateString<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span><br><span class="token prefix inserted">+</span>         <span class="token comment">// use toLocaleString() with the locale from the request</span><br><span class="token prefix inserted">+</span>         <span class="token comment">// and the timezone from context.geo</span><br><span class="token prefix inserted">+</span>         <span class="token keyword">const</span> localizedTime <span class="token operator">=</span> date<span class="token punctuation">.</span><span class="token function">toLocaleString</span><span class="token punctuation">(</span>locale<span class="token punctuation">,</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>           <span class="token literal-property property">timeZone</span><span class="token operator">:</span> timezone<span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>           <span class="token literal-property property">hour</span><span class="token operator">:</span> <span class="token string">"numeric"</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>           <span class="token literal-property property">minute</span><span class="token operator">:</span> <span class="token string">"numeric"</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>           <span class="token literal-property property">day</span><span class="token operator">:</span> <span class="token string">"numeric"</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>           <span class="token literal-property property">weekday</span><span class="token operator">:</span> <span class="token string">"short"</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>           <span class="token literal-property property">month</span><span class="token operator">:</span> <span class="token string">"short"</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>         <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span><br><span class="token prefix inserted">+</span>         <span class="token comment">// modify the HTML element</span><br><span class="token prefix inserted">+</span>         element<span class="token punctuation">.</span><span class="token function">setInnerContent</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>localizedTime<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>       <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>     <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token prefix inserted">+</span>     <span class="token comment">// transform the original response!</span><br><span class="token prefix inserted">+</span>     <span class="token punctuation">.</span><span class="token function">transform</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">How I personalize timezones on my website with Edge Functions</h2><p class="post__p">I use the code in this post on my personal website homepage, where I localize the time of my next Twitch stream so viewers know when to tune in!</p><img src="https://images.ctfassets.net/56dzm01z6lln/6ghNsEXuD9VL2uBjK5FACj/28db8052e987c4002b7ce13e6f448a01/next_twitch_stream.png" alt="My website screenshot, showing my home page with a next Twitch stream section, showing the time in GMT" height="2188" width="2760" /><h2 class="post__h2">Learn more about website personalization using Edge Functions</h2><p class="post__p">Explore more <a href="https://ntl.fyi/3UOmCTI" target="_blank">ideas and personalization use cases for Netlify Edge Functions on the blog</a> and browse a library of examples for inspiration on our <a href="https://edge-functions-examples.netlify.app/" target="_blank">Edge Functions Examples site</a>. Happy coding! 👩🏻‍💻</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>A responsive striped CSS gradient inspired by 80s VHS tapes</title>
          <description>Create a responsive, horizontal-striped CSS gradient inspired by 80s VHS tapes using CSS custom properties and a linear gradient.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/responsive-striped-css-pattern-80s-vhs-tapes/</link>
          <guid>https://whitep4nth3r.com/blog/responsive-striped-css-pattern-80s-vhs-tapes/</guid>
          <pubDate>Tue, 22 Nov 2022 00:00:00 GMT</pubDate>
          <category>CSS</category><category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I&#39;m really into retro-tech at the moment. And whilst I still haven&#39;t gotten round to playing with my new <a href="https://vice-emu.sourceforge.io/" target="_blank">Commodore 64 Emulator</a>, I have been collecting images of old VHS tapes that inspired the design of <a href="https://whattheframework.netlify.app" target="_blank">my most recent website project</a>. Just look at the beauty that is the Sony T-120!</p><img src="https://images.ctfassets.net/56dzm01z6lln/4w0yd033qyZTWznEWcm7Z3/f521ee6c34f56810fc43af1739ce571f/sony_t120.jpg" alt="Front and back sleeve covers of the Sony T-120 Dynamicron VHS tape." height="1583" width="2048" /><p class="post__p">Inspired by the Sony T-120 and other similar retro VHS designs by <a href="https://chriskirknielsen.com/designs/" target="_blank">Christopher Kirk-Nielsen</a> (four of which are hanging proudly in my office!), I created a moodboard and sent it on to my good friend, designer and live streamer <a href="https://twitch.tv/aaoa_" target="_blank">aaoa_</a>. He created this beautiful retro striped pattern for me, and my challenge was to build this in pure CSS without using a background image.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3ZQsLrv9IZLHPr5XzBHx1Q/089d1dcd814a06c37159d1d06c08cdf0/css_stripes.png" alt="Pure CSS gradient horizontal striped background using hard stops, ranging from yellow at the top, through to orange, red, pink, purple, dark blue and bright blue." height="858" width="1525" /><h2 class="post__h2">How it looks</h2>
    <div class="codePenEmbed__container" data-codepen data-embed-code=%3Cp%20class=%22codepen%22%20data-height=%22500%22%20data-theme-id=%22dark%22%20data-default-tab=%22css,result%22%20data-slug-hash=%22BaVJgaa%22%20data-editable=%22true%22%20data-user=%22whitep4nth3r%22%20style=%22height:%20500px;%20box-sizing:%20border-box;%20display:%20flex;%20align-items:%20center;%20justify-content:%20center;%20border:%202px%20solid;%20margin:%201em%200;%20padding:%201em;%22%3E%0A%20%20%3Cspan%3ESee%20the%20Pen%20%3Ca%20href=%22https://codepen.io/whitep4nth3r/pen/BaVJgaa%22%3E%0A%20%20Responsive%20striped%20background%20inspired%20by%20old%20VHS%20tape%20designs%3C/a%3E%20by%20whitep4nth3r%20(%3Ca%20href=%22https://codepen.io/whitep4nth3r%22%3E@whitep4nth3r%3C/a%3E)%0A%20%20on%20%3Ca%20href=%22https://codepen.io%22%3ECodePen%3C/a%3E.%3C/span%3E%0A%3C/p%3E>
      <div data-target></div>
    </div>

    <script>
      const codepen = document.querySelector("[data-codepen]");
      let loaded = false;
      const options = {
        root: null,
        threshold: 0.1
      }

      const loadCodePen = (entries, observer) => {
        entries.forEach(entry => {
          
          if(!loaded && entry.isIntersecting) {
            const target = entry.target.querySelector("[data-target]");
            const embedCode = entry.target.dataset.embedCode;

            target.innerHTML = decodeURI(embedCode);
            const script = document.createElement("script");
            script.src = "https://cpwebassets.codepen.io/assets/embed/ei.js";
            document.head.append(script);
            loaded = true;
          }
        });
      };

      const observer = new IntersectionObserver(loadCodePen, options);
      observer.observe(codepen);
    </script>
    <h2 class="post__h2">How it works</h2><h3 class="post__h3">Hard stops</h3><p class="post__p">The pattern is created using a CSS <code>linear-gradient</code> (using the direction &quot;to bottom&quot;) with what are known as &quot;<a href="https://css-tricks.com/books/greatest-css-tricks/hard-stop-gradients/" target="_blank">hard stops</a>&quot;. A hard stop is created by defining the end of one colour <b class="post__p--italic">in the same place as the start of the next colour</b>.</p><h3 class="post__h3">vh units</h3><p class="post__p">The biggest design challenge was making sure the full pattern of 14 colours was visible at any viewport size, and didn&#39;t scroll with the content. The key was to use <code>vh</code> unit to determine the size of each colour stop. The <code>vh</code> unit is a relative unit, and describes a percentage of the viewport height. <code>1vh</code> is 1% of the viewport height, <code>50vh</code> is 50% of the viewport height and so on. Using <code>vh</code> is what enables the full gradient pattern to <b class="post__p--italic">respond</b> to the height of the viewport.  <a href="https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Values_and_units" target="_blank">Read more about different units in CSS on MDN</a>.</p><p class="post__p">Here&#39;s a two-colour hard stop gradient as an example, in which we&#39;re using the <code>vh</code> unit to specify the size of each colour stop. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vtDBoGckkX"
      aria-describedby="vtDBoGckkX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vtDBoGckkX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="vtDBoGckkX" itemprop="text" content=".twoColorStop%20%7B%0A%20%20min-height%3A%20100vh%3B%0A%20%20background%3A%20linear-gradient(%0A%20%20%20%20to%20bottom%2C%20%0A%20%20%20%20red%200%2050vh%2C%0A%20%20%20%20yellow%2050vh%20100vh%0A%20%20)%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.twoColorStop</span> <span class="token punctuation">{</span><br>  <span class="token property">min-height</span><span class="token punctuation">:</span> 100vh<span class="token punctuation">;</span><br>  <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">linear-gradient</span><span class="token punctuation">(</span><br>    to bottom<span class="token punctuation">,</span> <br>    red 0 50vh<span class="token punctuation">,</span><br>    yellow 50vh 100vh<br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Here&#39;s the result.</p><img src="https://images.ctfassets.net/56dzm01z6lln/jI6M6EV1eIYLVKVammLbV/85da14ac039cb0c2b555380a6df0cdc1/yellow_red_hard_stop.png" alt="A red block on top of a yellow block. Each block is the same size." height="1478" width="2704" /><p class="post__p">Notice that:</p><ul><li><p class="post__p">the first colour (red) <b class="post__p--bold">ends at 50vh</b></p></li><li><p class="post__p">the second colour (yellow) <b class="post__p--bold">starts at 50vh</b></p></li></ul><p class="post__p">Where red stops, yellow begins, creating a hard stop. There are ways you can write the CSS above in a shorthand form, but I&#39;ve written it out verbosely to demonstrate the concept more clearly.</p><p class="post__p">The only differences between this example and the full example is that I&#39;m using 14 colours instead of two colours, and I&#39;m using <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties" target="_blank">CSS custom properties</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/calc" target="_blank">CSS calc()</a> to avoid writing out 14 lines of magic numbers. Notice though, that where one colour ends, another colour begins at the same <code>vh</code> value.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6TviUFQ87y42Kw35ZWLGY5/b4bbc09f98316221ef2db99955b4d4e3/stop_size_example.png" alt="Screenshot of the CSS code using custom properties, showing that the end stop of the second colour is the same as the start stop of the third colour." height="1052" width="1628" /><div class="post__callout">
  <h3 class="post__callout__title">Today I learned!</h3>
    <div class="post__callout__content">
      <p><a href="https://indieweb.social/@oliverturner@toot.cafe/109387154309652197">Oliver Turner</a> turned up on Mastodon with some solid advice that I'd not discovered before!</p>
<blockquote>
<p>One interesting property of linear gradients that can make generating stripes less verbose is that <em>any</em> number less than the previous stop works to create a hard border, so instead of needing to track the previous value you can use 0.</p>
</blockquote>
<p><a href="https://codepen.io/oliverturner/pen/JjZpprN">Here's the demo</a>. Thanks, Oliver!</p>

    </div>
  </div><p class="post__p">And finally, to allow the page content to scroll whilst the background pattern stays fixed in place in the viewport, it&#39;s wrapped in a vertically-scrollable element inside the container providing the background gradient pattern. <b class="post__p--bold">Set </b><b class="post__p--bold"><code>height: 100vh</code></b><b class="post__p--bold"> on this element to prevent the content increasing the height of the last colour in the gradient. </b>Experiment in the CodePen above and see what happens if you remove the height property on the <code>.wrapper</code> class!</p><p class="post__p">See what you can make with this CodePen. Take it, fork it, copy and paste it... Take this linear gradient wherever your heart desires! </p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Single-page applications, multi-page applications, the history of Twitter tech, and a failed project</title>
          <description>After I changed my mind about writing new JavaScript frameworks, I paused development on a new project, What the Framework (WTF). Here's why.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/twitter-tech-history-spa/</link>
          <guid>https://whitep4nth3r.com/blog/twitter-tech-history-spa/</guid>
          <pubDate>Tue, 15 Nov 2022 00:00:00 GMT</pubDate>
          <category>Web Dev</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">After <a href="https://whitep4nth3r.com/blog/write-a-new-javascript-framework/" target="_blank">I changed my mind about writing new JavaScript frameworks</a>, I paused development on a new project, <a href="https://whattheframework.netlify.app/" target="_blank"><b class="post__p--italic">What the Framework</b></a> (WTF). The project was intended to be a way to help people choose a JavaScript framework to use for their next project based on the features they needed from a framework, rather than opinion or the latest hype. During the project’s development, however, it became clear that in the exponentially changing technical landscape of 2022, the question of whether to specifically choose to build a Single-Page Application (SPA) or Multi-Page Application (MPA) was not as simple as it seemed.</p><p class="post__p">It’s not all bad, though! Whilst deep in my research for the WTF project, I discovered some interesting historical information about Twitter tech and its development over time — and I’ve reached an interesting conclusion. Given that there’s a lot of talk about Twitter’s new ownership and where this might lead the platform right now, I thought it would be a good time to share my findings. But first, let’s take a look at what we mean by a SPA.</p><h2 class="post__h2">What is a SPA?</h2><p class="post__p">A SPA is a JavaScript application that, once loaded in the browser, updates the page view locally by modifying the DOM, rather than by navigating to a new page as an HTML response from the server. As a result, SPAs are able to persist parts of a page that don’t change between routes, such as login state or work in progress. Whilst there are certainly ways to maintain state <b class="post__p--italic">without</b> using a SPA, such as using session cookies or your own bespoke JavaScript, the SPA approach is often useful for front end developers who don’t want to venture into more back-endy territory. The core concept of SPAs is to make web pages feel like native device applications through faster page transitions. Examples of SPA frameworks include Next.js, Nuxt, SvelteKit, Remix, Gatsby, and React.</p><h3 class="post__h3">How can you tell if a website is a SPA?</h3><p class="post__p">Open the network tab in your browser dev tools, filter by “Doc”,  and refresh the page. You should see an HTML document loaded in.</p><p class="post__p">If you’re viewing a SPA, you may either see a blank page in the network tab, or a message added by the app developers stating that the website needs in-browser JavaScript to work properly, such as in the case of Twitter.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4Kqi3DDvWEq5vW9phokxdU/8dc1c21b3bb0ac48881903b8b22b66dc/network_tab_twitter.png" alt="Screenshot of Twitter timeline with network tab open. The headline on the HTML doc previewed in the network tab says JavaScript is not available, with some more supporting information." height="2334" width="4064" /><p class="post__p">What’s happening here, is that Twitter’s index.html is attempting to load the JavaScript application code required to rebuild and customise the HTML page locally. Without a browser environment with JavaScript enabled, JavaScript can’t run to update the HTML with new content, and the developers have kindly provided a fallback message to let you know.</p><h2 class="post__h2">In brief: MPA</h2><p class="post__p">In a MPA, HTML documents are rendered on a server, either at build-time in the case of pre-generated static sites, or at request time for server-side rendered pages. Every page in an MPA has its own file or route. When you navigate to a new page in an MPA, your browser requests a new page of HTML from the server, the server builds it and sends it back. Traditional MPA frameworks include Ruby on Rails, Python Django, PHP Laravel, WordPress, and static site builders like Eleventy or Jekyll. This is the way all websites worked until the late 2000s.</p><h2 class="post__h2">Was Twitter the original SPA?</h2><p class="post__p">The web community often regard Twitter as one of the first modern Single-Page Applications. During my research for WTF, I stumbled upon a blog post from Twitter published on 20 September 2010: <a href="https://blog.twitter.com/engineering/en_us/a/2010/the-tech-behind-the-new-twittercom" target="_blank">The Tech Behind the New Twitter.com</a>.</p><h3 class="post__h3">2010</h3><p class="post__p">In this post, Britt Selvitelle describes how Twitter “<b class="post__p--bold">began implementing a new architecture almost entirely in JavaScript</b>” alongside a redesign, and how one of the primary goals was “to make page navigation easier and faster” by providing “a rich web application that behaves like a traditional web site” — hello SPA! One of the most common concerns with SPAs (even to this day) is around supporting a wide range of search engine crawlers and conditions where JavaScript either failed, or was not able to run; if you can’t load JavaScript (i.e. you’re not in a browser environment), you can’t load a JavaScript application, and therefore you can’t load content that relies on JavaScript to understand what the page is about. </p><p class="post__p">Encouragingly, Twitter made sure to consider this carefully in 2010 by building a <a href="https://mustache.github.io/" target="_blank">Mustache</a>-based rendering system that ran on both the server and client. (It’s a shame this didn’t persist into the present day!)</p><h3 class="post__h3">2012</h3><p class="post__p">Fast-forward to 2012 and <a href="https://blog.twitter.com/engineering/en_us/a/2012/improving-performance-on-twittercom" target="_blank">Twitter released a new blog post</a> announcing that to “take back control of [their] front-end performance”, they decided to move much of the rendering <b class="post__p--italic">back to the server(!)</b>, given that the SPA-based approach “lacked support for various optimizations available only on the server.”</p><h3 class="post__h3">2013</h3><p class="post__p">There’s a bit of a gap in announcements between 2012 and 2017, but on 11 July 2013 (just nine days after <a href="https://github.com/facebook/react/releases/tag/v0.3.0" target="_blank">React’s first public release</a>) Twitter released <a href="https://flightjs.github.io/" target="_blank">Flight</a> — “a lightweight, component-based JavaScript framework that maps behavior to DOM nodes” — which was used by Twitter at the time. The last release of Flight was in 2015 and it’s not under active development in 2022, but it’s interesting to note that whilst Flight was “organized around the existing DOM model” to “take advantage of native features”, React’s JSX abstraction ultimately gained more traction at the time. And React is still growing almost a decade later, as proved by the results of the <a href="https://jamstack.org/survey/2022/" target="_blank">latest Jamstack Community Survey</a>:</p><blockquote class="post__blockquote"><p class="post__p">React continued to grow to an almost unprecedented 71% share of developers, and Next.js rode that wave and is now used by 1 in every 2 developers.</p></blockquote><h3 class="post__h3">2017</h3><p class="post__p">In further technical developments, in 2017 <a href="https://blog.twitter.com/engineering/en_us/topics/open-source/2017/how-we-built-twitter-lite" target="_blank">Twitter released Twitter Lite</a> — a Progressive Web App (PWA). <a href="https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps" target="_blank">Progressive Web Apps</a> go even further to replicate that native app-like experience of a SPA, by being “installable” on devices, supporting offline use and push notifications, whilst also being discoverable on the web and sharable via a URL. Initially aimed at improving the mobile Twitter experience by minimising data usage, loading quicker on slower connections and taking up less than 1MB of device space, you can still install Twitter on any device today, including desktop machines.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5wHuazHvG1k0vweNFXed0y/45c36c943790d823a9568ab80e1335a0/install_twitter.png" alt="My Twitter profile in Brave browser, showing the install app popup available from the address bar, which asks you to confirm whether you want to install the Twitter PWA on that machine." height="858" width="1072" /><p class="post__p">Twitter Lite was available at <a href="http://mobile.twitter.com" >mobile.twitter.com</a> — <b class="post__p--italic">seemingly</b> splitting the codebases for desktop and mobile devices at the time. Before hand-held devices became as powerful as they are today — and before CSS became more responsive by default with tools such as <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout" target="_blank">grid</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/clamp" target="_blank">clamp</a> — this was the way things were done. All companies I worked at in the 2010s maintained separate desktop and mobile experiences, although we began merging the two experiences starting in 2017/2018. </p><p class="post__p">It’s interesting to note that the web dev industry took so long to appreciate that the performance benefits and features of “mobile sites” are just as compelling on non-mobile devices, and that responsive web design and progressive enhancement could let us build the best experiences for users no matter where they are or what device they’re using.</p><h3 class="post__h3">2019</h3><p class="post__p">In 2019, Twitter followed suit and <a href="https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/buildingthenewtwitter" target="_blank">launched another new Twitter</a>, this time with a “write once, run everywhere” philosophy, merging the desktop and mobile device experiences in code. This was further enhanced by only downloading visual elements to a user’s browser or device when they  were needed, in order to prioritise data-saving on all devices.</p><h2 class="post__h2">And the cycle continues</h2><p class="post__p">Twitter tech has been on a journey — and so have many, many other codebases and products. Render everything on the server. Do everything in JavaScript in the browser. Do less in Javascript and move critical functionality to the server. Move all functionality back to the server. Use the platform. Write HTML. Abstract away the platform. Use <code>{INSERT_NEW_SYNTAX_HERE}</code>. And the cycle continues to the present day.</p><p class="post__p">The initial question that prompted this post was: “Should we <b class="post__p--italic">really</b> be asking whether people should build a SPA or MPA?” I think the answer is yes… BUT — there’s more. I am fascinated by the technical journey that Twitter has been on over the last 12 years, and a lot of it mirrors the technical trends I have observed and built myself during my career as a front end developer and tech lead. But this journey also highlights the extra nuances to take into account when building a product, and ultimately when choosing the tech.</p><p class="post__p"><b class="post__p--bold">The technical choices you make don’t just rely on the features your product will need</b>. These choices are also massively influenced by how people will use your product.</p><ul><li><p class="post__p">Who is your audience?</p></li><li><p class="post__p">How fast is their internet connection?</p></li><li><p class="post__p">What devices do they use?</p></li><li><p class="post__p">Do they use more than one device frequently?</p></li><li><p class="post__p">How do they actually use the product?</p></li></ul><p class="post__p">And what’s more, the options available to developers in 2022 are more varied than ever. Newer solutions to client-side JavaScript hydration, such as <a href="https://jasonformat.com/islands-architecture/" target="_blank">Islands Architecture</a> are paving the way for more hybrid applications whilst prioritising shipping static HTML by default, and progressively enhancing the interactivity of the user experience in a more intentional way.</p><p class="post__p">The changing landscape of hardware, internet connectivity, and social patterns — these all play a part in choosing the tech for our products. And this is likely to change as the world evolves around us. And so in conclusion: I’m not sure I can really build <b class="post__p--italic">What the Framework</b> effectively.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Personalize your static site based on a previous site referral</title>
          <description>Learn how to use Netlify Edge Functions to personalize static HTML pages based on the HTTP referer header.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://ntl.fyi/3SBU2DY</link>
          <guid>https://ntl.fyi/3SBU2DY</guid>
          <pubDate>Tue, 25 Oct 2022 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">My blog posts are often shared on Reddit, Hacker News and other similar sites. Hooray for more post views! But the internet is often a <b class="post__p--italic">wild</b> place, and some people forget that us writers are real human beings with thoughts and feelings and tricky brain chemistry. And so I had an idea for an experiment. What if I could show people a friendly message on my website based on the site they came from, to remind readers that I’m real?</p><p class="post__p">There were two conditions that influenced my solution.</p><ul><li><p class="post__p">My blog site is a static site — currently built using <a href="https://www.11ty.dev/" target="_blank">Eleventy</a></p></li><li><p class="post__p">I didn’t want to use client-side JavaScript — I’m currently on a mission <a href="https://whitep4nth3r.com/blog/how-i-improved-website-performance/" target="_blank">to use as little client-side JS as possible</a></p></li></ul><p class="post__p">Enter Netlify Edge Functions, which allow us to intercept and modify HTTP requests and responses based on cookies, geolocation data, request headers and any custom logic you might need. You can add this functionality to any new or existing project on Netlify — even if your site is pre-generated at build time and served as static files from Netlify’s CDN (like mine is), all with no client-side JS.</p><p class="post__p">In addition to showing messages based on where my blog visitors came from, this can also extend to many similar real-world use-cases, including</p><ul><li><p class="post__p">showing targeted discount codes based on the site you came from</p></li><li><p class="post__p">prioritising news or product content based on the previous site you visited</p></li></ul><p class="post__p">Were you browsing the Apple site for a new iPhone? Visit your network provider’s home page next and they can show you their iPhone plans first!</p><p class="post__p">Let’s take a look at how we can do this with Edge Functions.</p><h2 class="post__h2">Using the HTTP referer header</h2><p class="post__p">With Netlify Edge Functions, we can access the values of HTTP request headers, and act based on the value of those headers. We can rewrite URLs, modify HTML, set cookies, rewrite those headers, and more — all at the server-level without using client-side JS.</p><p class="post__p">The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer" target="_blank">HTTP referer header</a> contains an absolute or partial address of the page that makes the request, allowing a server to identify where visitors came from — i.e. where they have been <b class="post__p--bold">referred</b> from — as long as the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/noreferrer" target="_blank">noreferrer attribute</a> is <b class="post__p--bold">not</b> set on the anchor tag of the inbound link.</p><h2 class="post__h2">The tutorial</h2><p class="post__p">In this tutorial, you’ll learn how to detect the HTTP referer header and enhance a static HTML page in five steps:</p><ol><li><p class="post__p">Add a static HTML page to a fresh project</p></li><li><p class="post__p">Create a new Edge Function file and configure it to run on the static HTML page</p></li><li><p class="post__p">Add a placeholder to the HTML page to modify using the Edge Function</p></li><li><p class="post__p">Grab the HTTP referer header from the request</p></li><li><p class="post__p">Use HTMLRewriter to modify the placeholder in the HTML response if the referer header matches a particular string</p></li></ol><h3 class="post__h3">Why not do this with client-side JavaScript?</h3><p class="post__p">As always, there’s more than one way to make the magic happen. You <b class="post__p--italic">can</b> achieve similar results using client-side JavaScript, but using an Edge Function is better for resilience, performance, and user experience.</p><h4 class="post__h4">Resilience</h4><p class="post__p">Edge Functions intercept the HTML response, allowing the HTML to be transformed it before it’s sent to the browser. This is more robust; if users have JavaScript disabled, they’ll still see the transformed HTML. And this all works on static HTML/pre-generated pages.</p><h4 class="post__h4">Performance and user experience</h4><p class="post__p">JavaScript in the browser takes time to load, run, detect the HTTP referer header, and modify the DOM. As well as adding precious extra milliseconds to page load times, with this method users will likely experience Cumulative Layout Shift (CLS) — where the layout of the page shifts some time after the initial page content loads and new content is rendered. As well as not being ideal for the user, <a href="https://web.dev/cls/" target="_blank">Google may also penalise your site in search engine results</a>.</p><p class="post__p">Let&#39;s get coding! 👩🏻‍💻✨</p><h3 class="post__h3">Add a static HTML page to a fresh project</h3><p class="post__p">Create a new project directory, and add an <code>index.html</code> file. Open the file in your IDE of choice, and add some HTML boilerplate. Here’s some boilerplate I prepared earlier.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="InjZxnVZPq"
      aria-describedby="InjZxnVZPq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="InjZxnVZPq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="InjZxnVZPq" itemprop="text" content="%3C!DOCTYPE%20html%3E%0A%3Chtml%20lang%3D%22en%22%3E%0A%20%20%3Chead%3E%0A%20%20%20%20%3Cmeta%20charset%3D%22UTF-8%22%20%2F%3E%0A%20%20%20%20%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1.0%22%20%2F%3E%0A%20%20%20%20%3Ctitle%3EHTTP%20referer%20header%20tutorial%3C%2Ftitle%3E%0A%20%20%3C%2Fhead%3E%0A%20%20%3Cbody%3E%0A%20%20%20%20%3Ch1%3EHTTP%20referer%20header%20tutorial%3C%2Fh1%3E%0A%20%20%3C%2Fbody%3E%0A%3C%2Fhtml%3E">
      <pre class="language-html"><code class="language-html"><span class="token doctype"><span class="token punctuation">&lt;!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>UTF-8<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1.0<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>HTTP referer header tutorial<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span>HTTP referer header tutorial<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Add a new Edge Function to your project</h3><p class="post__p">At the root of your project:</p><ul><li><p class="post__p">create a <code>netlify </code>directory</p></li><li><p class="post__p">inside that, add an <code>edge-functions </code>directory</p></li><li><p class="post__p">inside that, add a new file, <code>referer.js</code></p></li></ul><p class="post__p">This is where we’ll write the code for the Edge Function. Here’s what your project structure should look like so far.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="beFzBRQPVw"
      aria-describedby="beFzBRQPVw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="beFzBRQPVw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="beFzBRQPVw" itemprop="text" content="%23%20project%20structure%0A%0A%E2%94%9C%E2%94%80%E2%94%80%20index.html%0A%E2%94%94%E2%94%80%E2%94%80%20netlify%0A%20%20%20%20%E2%94%94%E2%94%80%E2%94%80%20edge-functions%0A%20%20%20%20%20%20%20%20%E2%94%94%E2%94%80%E2%94%80%20referer.js">
      <pre class="language-markup"><code class="language-markup"># project structure<br><br>├── index.html<br>└── netlify<br>    └── edge-functions<br>        └── referer.js</code></pre>
    </div>
  </div>

  <p class="post__p">Next, add a <code>netlify.toml</code> file to the root of your project. This is a configuration file that specifies how Netlify builds and deploys your site — including redirects, branch settings, Edge Function declarations and more. In your <code>netlify.toml</code> file, add the following code.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="gCNHvbzLmP"
      aria-describedby="gCNHvbzLmP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="gCNHvbzLmP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="gCNHvbzLmP" itemprop="text" content="%23%20netlify.toml%0A%0A%5B%5Bedge_functions%5D%5D%0A%20%20path%20%3D%20%22%2F%22%0A%20%20function%20%3D%20%22referer%22">
      <pre class="language-toml"><code class="language-toml"><span class="token comment"># netlify.toml</span><br><br><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">edge_functions</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br>  <span class="token key property">path</span> <span class="token punctuation">=</span> <span class="token string">"/"</span><br>  <span class="token key property">function</span> <span class="token punctuation">=</span> <span class="token string">"referer"</span></code></pre>
    </div>
  </div>

  <p class="post__p">This tells Netlify to run the <code>referer</code> Edge Function when a visitor lands on <code>index.html</code>. In a real-world example, you may want to configure the Edge Function to run on a group of specific paths, or all paths. Learn more about <a href="https://docs.netlify.com/edge-functions/declarations/" target="_blank">Edge Functions declarations</a> on the Netlify docs.</p><p class="post__p"><b class="post__p--italic"><b class="post__p--bold">Note:</b></b><b class="post__p--italic"> This tutorial is a framework-agnostic method which shows Edge Functions working out of the </b><b class="post__p--italic"><code>netlify</code></b><b class="post__p--italic"> directory. If you’re using a framework, you can still follow this tutorial successfully, but you may want to take advantage of the workflow and feature benefits Netlify provides for other specific technologies.</b></p><p class="post__p"><b class="post__p--italic">For example, if you’re an avid user of Next.js, you can get to work straight away in your </b><b class="post__p--italic"><code>middleware.ts</code></b><b class="post__p--italic"> file, where you have full access to the HTTP request and response on Netlify, allowing you to </b><a href="https://www.netlify.com/blog/rewrite-html-transform-page-props-in-nextjs/" target="_blank"><b class="post__p--italic">modify HTML and update page props</b></a><b class="post__p--italic"> on both static and dynamic pages. This is automatically supported in all Next.js sites on Netlify, powered by Netlify’s Next.js Runtime and Advanced Middleware capabilities. It just works™️.</b></p><h3 class="post__h3">Add a placeholder to modify in your HTML file</h3><p class="post__p">Next, update your <code>index.html</code> file and add an empty placeholder element. Add <code>id=referer</code> to the element, or a similar selector of your choosing. This is how we’ll target the HTML response for rewriting in the Edge Function.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="JMOMSlkoOI"
      aria-describedby="JMOMSlkoOI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="JMOMSlkoOI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="JMOMSlkoOI" itemprop="text" content="%20%20%2F%2F%20index.html%0A%20%20%3C!DOCTYPE%20html%3E%0A%20%20%3Chtml%20lang%3D%22en%22%3E%0A%20%20%20%20%20%3Chead%3E%0A%20%20%20%20%20%20%20%3Cmeta%20charset%3D%22UTF-8%22%20%2F%3E%0A%20%20%20%20%20%20%20%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1.0%22%20%2F%3E%0A%20%20%20%20%20%20%20%3Ctitle%3EHTTP%20referer%20header%20tutorial%3C%2Ftitle%3E%0A%20%20%20%20%20%3C%2Fhead%3E%0A%20%20%20%20%20%3Cbody%3E%0A%2B%20%20%20%20%20%20%20%3Cp%20id%3D%22referer%22%3E%3C%2Fp%3E%0A%20%20%20%20%20%20%20%20%3Ch1%3EHTTP%20referer%20header%20tutorial%3C%2Fh1%3E%0A%20%20%20%20%20%3C%2Fbody%3E%0A%20%20%20%3C%2Fhtml%3E">
      <pre class="language-diff-html"><code class="language-diff-html">// index.html<br><span class="token unchanged language-html"><span class="token prefix unchanged"> </span> <span class="token doctype"><span class="token punctuation">&lt;!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span><br><span class="token prefix unchanged"> </span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><span class="token prefix unchanged"> </span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br><span class="token prefix unchanged"> </span>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>UTF-8<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token prefix unchanged"> </span>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1.0<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token prefix unchanged"> </span>      <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>HTTP referer header tutorial<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br><span class="token prefix unchanged"> </span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br><span class="token prefix unchanged"> </span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br></span><span class="token inserted-sign inserted language-html"><span class="token prefix inserted">+</span>       <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>referer<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br></span><span class="token unchanged language-html"><span class="token prefix unchanged"> </span>       <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span><span class="token punctuation">></span></span>HTTP referer header tutorial<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><br><span class="token prefix unchanged"> </span>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token prefix unchanged"> </span>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">The Edge Function code</h3><p class="post__p">Let’s set up the Edge Function to intercept requests. In <code>referer.js</code>, add the following code. Netlify Edge Functions take two parameters: <code>request</code>, which represents the incoming HTTP request, and <code>context</code>, which is a <a href="https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/api/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=ef-context-object#netlify-specific-context-object" target="_blank">Netlify specific API</a> that exposes geolocation data, lets you work with cookies, rewrites, site information and more.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="omDEGDhKCq"
      aria-describedby="omDEGDhKCq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="omDEGDhKCq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="omDEGDhKCq" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0Aexport%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// netlify/edge-functions/referer.js</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Let’s get the value of the referer header from the request. Given Netlify Edge Functions use standard web APIs, we can do this using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Headers/get" target="_blank">Headers.get() method</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="lPiRNWhZze"
      aria-describedby="lPiRNWhZze">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="lPiRNWhZze">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="lPiRNWhZze" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%2B%20%20%20%2F%2F%20get%20HTTP%20referer%20header%20value%0A%2B%20%20%20const%20referer%20%3D%20request.headers.get(%22referer%22)%3B%0A%20%20%7D%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/referer.js<br><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br></span><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token comment">// get HTTP referer header value</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">const</span> referer <span class="token operator">=</span> request<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"referer"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Let’s do some defensive coding and return early if we don’t find a referer header (for example if the <code>noreferrer</code> attribute is set on the inbound link). Add the following code to get the next HTTP response in the chain by awaiting <code>context.next()</code>, and return it if <code>referer === null</code>. This tells the Edge Function to serve the <code>index.html</code> file that had been requested without us making any changes to it along the way.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="qXpoKfHJxx"
      aria-describedby="qXpoKfHJxx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="qXpoKfHJxx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="qXpoKfHJxx" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F%20get%20HTTP%20referer%20header%20value%0A%20%20%20%20const%20referer%20%3D%20request.headers.get(%22referer%22)%3B%0A%0A%2B%20%20%20%2F%2F%20get%20the%20next%20HTTP%20response%20in%20the%20chain%0A%2B%20%20%20const%20response%20%3D%20await%20context.next()%3B%0A%0A%2B%20%20%20%2F%2F%20if%20no%20referer%2C%20return%20the%20response%0A%2B%20%20%20if%20(referer%20%3D%3D%3D%20null)%20%7B%0A%2B%20%20%20%20%20return%20response%3B%0A%2B%20%20%20%7D%0A%0A%20%20%7D%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/referer.js<br><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token comment">// get HTTP referer header value</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> referer <span class="token operator">=</span> request<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"referer"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token comment">// get the next HTTP response in the chain</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token comment">// if no referer, return the response</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>referer <span class="token operator">===</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">return</span> response<span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, when you’ve decided on the referer header value that you want to target, set up a check to return the response early if the incoming referer doesn’t match. In this example I’m using the Reddit referer <code>https://www.reddit.com/</code>. Note that this is a basic equality check example and you may want to get a little more fancy with fuzzy checks or some logic based on multiple values.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="TDkRNQbSFV"
      aria-describedby="TDkRNQbSFV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="TDkRNQbSFV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="TDkRNQbSFV" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F%20get%20HTTP%20referer%20header%20value%0A%20%20%20%20const%20referer%20%3D%20request.headers.get(%22referer%22)%3B%0A%0A%20%20%20%20%2F%2F%20get%20the%20next%20HTTP%20response%20in%20the%20chain%0A%20%20%20%20const%20response%20%3D%20await%20context.next()%3B%0A%0A%20%20%20%20%2F%2F%20if%20no%20referer%2C%20return%20the%20response%0A%20%20%20%20if%20(referer%20%3D%3D%3D%20null)%20%7B%0A%20%20%20%20%20%20return%20response%3B%0A%20%20%20%20%7D%0A%0A%2B%20%20%20%2F%2F%20check%20for%20the%20referer%20value%20you%20care%20about%0A%2B%20%20%20%2F%2F%20return%20if%20it%20doesn't%20match%0A%2B%20%20%20if%20(referer%20!%3D%3D%20%22https%3A%2F%2Fwww.reddit.com%2F%22)%20%7B%0A%2B%20%20%20%20%20return%20response%3B%0A%2B%20%20%20%7D%0A%0A%20%20%7D%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/referer.js<br><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token comment">// get HTTP referer header value</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> referer <span class="token operator">=</span> request<span class="token punctuation">.</span>headers<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"referer"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>   <span class="token comment">// get the next HTTP response in the chain</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>   <span class="token comment">// if no referer, return the response</span><br><span class="token prefix unchanged"> </span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>referer <span class="token operator">===</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>     <span class="token keyword">return</span> response<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><br></span><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token comment">// check for the referer value you care about</span><br><span class="token prefix inserted">+</span>   <span class="token comment">// return if it doesn't match</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>referer <span class="token operator">!==</span> <span class="token string">"https://www.reddit.com/"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>     <span class="token keyword">return</span> response<span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>   <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, it’s time to modify the HTML response when we find a referer value match. There are a number of ways we can modify the HTML, including by parsing the HTML as a string and finding and replacing using a regular expression. For this example, however, we’re going to import and use <a href="https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/" target="_blank">Cloudflare’s HTMLRewriter</a> because it offers us the ability to target and modify the HTML response with the kind of JavaScript syntax we’re familiar with using in the browser.</p><p class="post__p">Netlify Edge Functions are powered by the Deno runtime, and package imports are supported via URL imports rather than local package installation, as in the case of Node.js environments. Add the URL import to HTMLRewriter at the top of the file.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="yjFwsofUuk"
      aria-describedby="yjFwsofUuk">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="yjFwsofUuk">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="yjFwsofUuk" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0A%2B%20import%20%7B%20HTMLRewriter%20%7D%20from%20%22https%3A%2F%2Fghuc.cc%2Fworker-tools%2Fhtml-rewriter%2Findex.ts%22%3B%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20%20%2F%2F%20...%0A%20%20%7D">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/referer.js<br><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span> <span class="token keyword">import</span> <span class="token punctuation">{</span> HTMLRewriter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://ghuc.cc/worker-tools/html-rewriter/index.ts"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>  <span class="token comment">// ...</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Finally, add the following code. This updates the HTML in the HTTP response by finding the element we want to modify (the paragraph tag with <code>id=&quot;referer&quot;</code>), and setting its content to “Hello, Reddit user!”</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YxBKzaUPvR"
      aria-describedby="YxBKzaUPvR">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YxBKzaUPvR">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="YxBKzaUPvR" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0A%20%20import%20%7B%20HTMLRewriter%20%7D%20from%20%22https%3A%2F%2Fghuc.cc%2Fworker-tools%2Fhtml-rewriter%2Findex.ts%22%3B%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F%20...%0A%0A%2B%20%20%20%2F%2F%20if%20we%20do%20have%20a%20referer%20match%2C%20rewrite%20the%20element%0A%2B%20%20%20%2F%2F%20in%20the%20response%20HTML%20with%20a%20friendly%20message%0A%2B%20%20%20return%20new%20HTMLRewriter()%0A%2B%20%20%20%20%20.on(%22p%23referer%22%2C%20%7B%0A%2B%20%20%20%20%20%20%20element(element)%20%7B%0A%2B%20%20%20%20%20%20%20%20%20element.setInnerContent(%22Hello%2C%20Reddit%20user!%22)%3B%0A%2B%20%20%20%20%20%20%20%7D%2C%0A%2B%20%20%20%20%20%7D)%0A%2B%20%20%20%20%20.transform(response)%3B%0A%20%20%7D%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/referer.js<br><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">import</span> <span class="token punctuation">{</span> HTMLRewriter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://ghuc.cc/worker-tools/html-rewriter/index.ts"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token comment">// ...</span><br></span><br><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token comment">// if we do have a referer match, rewrite the element</span><br><span class="token prefix inserted">+</span>   <span class="token comment">// in the response HTML with a friendly message</span><br><span class="token prefix inserted">+</span>   <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">HTMLRewriter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token prefix inserted">+</span>     <span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"p#referer"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>       <span class="token function">element</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix inserted">+</span>         element<span class="token punctuation">.</span><span class="token function">setInnerContent</span><span class="token punctuation">(</span><span class="token string">"Hello, Reddit user!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix inserted">+</span>       <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token prefix inserted">+</span>     <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token prefix inserted">+</span>     <span class="token punctuation">.</span><span class="token function">transform</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">The above is just a text example. If you want to add HTML to your referrer message, perhaps to include an onward link, set <code>html: true</code> on <code>setInnerContent</code>, like so.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CalFEsnbWe"
      aria-describedby="CalFEsnbWe">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CalFEsnbWe">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="CalFEsnbWe" itemprop="text" content="element.setInnerContent(%0A%20%20%60Hello%2C%20Reddit%20user!%20%3Ca%20href%3D%22https%3A%2F%2Fgo%22%3EVisit%20this%20link!%3C%2Fa%3E%60%2C%20%0A%20%20%7Bhtml%3A%20true%7D%0A)%3B">
      <pre class="language-javascript"><code class="language-javascript">element<span class="token punctuation">.</span><span class="token function">setInnerContent</span><span class="token punctuation">(</span><br>  <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Hello, Reddit user! &lt;a href="https://go">Visit this link!&lt;/a></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <br>  <span class="token punctuation">{</span><span class="token literal-property property">html</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">}</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">You may also want to hide or show the paragraph tag based on whether it’s rewritten or not. You might need to do this if your referrer message is styled with a background color and padding which you don’t want to show when the element is empty. In this case, you can hide the element by default using CSS, and use HTMLRewriter to add a class to the element during the HTML transformation.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LuPHxzANoU"
      aria-describedby="LuPHxzANoU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LuPHxzANoU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="LuPHxzANoU" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0A%20%20import%20%7B%20HTMLRewriter%20%7D%20from%20%22https%3A%2F%2Fghuc.cc%2Fworker-tools%2Fhtml-rewriter%2Findex.ts%22%3B%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F%20...%0A%0A%20%20%20%20return%20new%20HTMLRewriter()%0A%20%20%20%20%20%20.on(%22p%23referer%22%2C%20%7B%0A%20%20%20%20%20%20%20%20element(element)%20%7B%0A%20%20%20%20%20%20%20%20%20%20element.setInnerContent(%22Hello%2C%20Reddit%20user!%22)%3B%0A%2B%20%20%20%20%20%20%20%20%20element.setAttribute(%22class%22%2C%20%22showMessage%22)%3B%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D)%0A%20%20%20%20%20%20.transform(response)%3B%0A%20%20%7D%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/referer.js<br><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">import</span> <span class="token punctuation">{</span> HTMLRewriter <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://ghuc.cc/worker-tools/html-rewriter/index.ts"</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token comment">// ...</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>   <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">HTMLRewriter</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br><span class="token prefix unchanged"> </span>     <span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"p#referer"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>       <span class="token function">element</span><span class="token punctuation">(</span><span class="token parameter">element</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>         element<span class="token punctuation">.</span><span class="token function">setInnerContent</span><span class="token punctuation">(</span><span class="token string">"Hello, Reddit user!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>         element<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"class"</span><span class="token punctuation">,</span> <span class="token string">"showMessage"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br></span><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>       <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>     <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token prefix unchanged"> </span>     <span class="token punctuation">.</span><span class="token function">transform</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Alright, now we’re ready to test this out in development!</p><p class="post__p">We can use the Netlify CLI to spin up a development server and run the Edge Function code. Install the Netlify CLI globally on your machine by running the following command in your terminal.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="coFzMOfRhG"
      aria-describedby="coFzMOfRhG">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="coFzMOfRhG">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="coFzMOfRhG" itemprop="text" content="npm%20install%20netlify-cli%20-g">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> netlify-cli <span class="token parameter variable">-g</span></code></pre>
    </div>
  </div>

  <p class="post__p"><b class="post__p--bold">Before we start the development server, a note about HTTP headers.</b> Unfortunately there’s no way to reliably fake or force values on HTTP headers for security reasons. To test that the HTML is rewritten in a development environment, we need to make two changes.</p><p class="post__p">First, change the referer header value check to your development environment URL in <code>referer.js</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="rLfCFDbwgD"
      aria-describedby="rLfCFDbwgD">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="rLfCFDbwgD">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="rLfCFDbwgD" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F...%0A%0A-%20%20%20if%20(referer%20!%3D%3D%20%22https%3A%2F%2Fwww.reddit.com%2F%22)%20%7B%0A%2B%20%20%20if%20(referer%20!%3D%3D%20%22http%3A%2F%2Flocalhost%3A8888%2F%22)%20%7B%0A%20%20%20%20%20return%20response%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F...%0A%20%20%7D">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/referer.js<br><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token comment">//...</span><br></span><br><span class="token deleted-sign deleted language-javascript"><span class="token prefix deleted">-</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>referer <span class="token operator">!==</span> <span class="token string">"https://www.reddit.com/"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br></span><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>referer <span class="token operator">!==</span> <span class="token string">"http://localhost:8888/"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br></span><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>    <span class="token keyword">return</span> response<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>   <span class="token comment">//...</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">And on <code>index.html</code>, add an anchor tag that points to the same page — <code>/</code>. Clicking on this link will set the HTTP referer header to the development URL.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ElOTJsePJA"
      aria-describedby="ElOTJsePJA">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ElOTJsePJA">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="ElOTJsePJA" itemprop="text" content="%2F%2F%20index.html%0A%0A%3Ca%20href%3D%22%2F%22%3EClick%20here%20to%20set%20the%20referer%20header%3C%2Fa%3E">
      <pre class="language-html"><code class="language-html">// index.html<br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Click here to set the referer header<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Start the development server by running the following command in your terminal.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="XbyflMeSCJ"
      aria-describedby="XbyflMeSCJ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="XbyflMeSCJ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="XbyflMeSCJ" itemprop="text" content="netlify%20dev">
      <pre class="language-bash"><code class="language-bash">netlify dev</code></pre>
    </div>
  </div>

  <p class="post__p">Navigate to <a href="http://localhost:8888/" >http://localhost:8888/</a>. <b class="post__p--bold">You won’t see the rewritten HTML yet! There&#39;s no previous site, so there&#39;s no HTTP referer header set.</b> Click on the link you just created to set a referer header. And when the page reloads, you’ll see that the HTML has been transformed by the Edge Function! 🎉</p><p class="post__p">To remove the HTTP referer header in development, navigate to <a href="http://localhost:8888/" >http://localhost:8888/</a> again <b class="post__p--bold">without clicking on the link on the page</b>. Open up the network tab to watch the HTTP referer header appear and disappear depending on your behaviour!</p><h3 class="post__h3">Bonus content: contextual environment variables</h3><p class="post__p">To avoid having to change the HTTP referer header to <code>localhost</code> in development, you may wish to use an <a href="https://docs.netlify.com/environment-variables/overview/" target="_blank">environment variable</a> when you deploy your site. To do this, set an <code>HTTP_REFERER_CHECK</code> environment variable in your production site, such as <code>https://www.reddit.com/</code> using the Netlify UI or the <a href="https://cli.netlify.com/commands/env" target="_blank">CLI</a>. Then, override the value in your development environment by adding the following entry to the <code>netlify.toml</code> file.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZAHYwCIXBX"
      aria-describedby="ZAHYwCIXBX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZAHYwCIXBX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="ZAHYwCIXBX" itemprop="text" content="%23%20netlify.toml%0A%0A%5Bcontext.dev.environment%5D%0A%20%20HTTP_REFERER_CHECK%20%3D%20%22http%3A%2F%2Flocalhost%3A8888%2F%22">
      <pre class="language-toml"><code class="language-toml"><span class="token comment"># netlify.toml</span><br><br><span class="token punctuation">[</span><span class="token table class-name">context.dev.environment</span><span class="token punctuation">]</span><br>  <span class="token key property">HTTP_REFERER_CHECK</span> <span class="token punctuation">=</span> <span class="token string">"http://localhost:8888/"</span></code></pre>
    </div>
  </div>

  <p class="post__p">Finally, in <code>referer.js</code>, replace the string equality check with the environment variable, retrieved using the <code>Deno env API</code>:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="tdavhMVcvx"
      aria-describedby="tdavhMVcvx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="tdavhMVcvx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="tdavhMVcvx" itemprop="text" content="%20%20%2F%2F%20netlify%2Fedge-functions%2Freferer.js%0A%0A%20%20export%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F...%0A%0A-%20%20%20if%20(referer%20!%3D%3D%20%22http%3A%2F%2Flocalhost%3A8888%2F%22)%20%7B%0A%2B%20%20%20if%20(referer%20!%3D%3D%20Deno.env.get(%22HTTP_REFERER_CHECK%22))%20%7B%0A%20%20%20%20%20return%20response%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F...%0A%20%20%7D">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// netlify/edge-functions/referer.js<br><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token comment">//...</span><br></span><br><span class="token deleted-sign deleted language-javascript"><span class="token prefix deleted">-</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>referer <span class="token operator">!==</span> <span class="token string">"http://localhost:8888/"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br></span><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span>   <span class="token keyword">if</span> <span class="token punctuation">(</span>referer <span class="token operator">!==</span> Deno<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"HTTP_REFERER_CHECK"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br></span><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>    <span class="token keyword">return</span> response<span class="token punctuation">;</span><br><span class="token prefix unchanged"> </span>   <span class="token punctuation">}</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span>   <span class="token comment">//...</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">And lastly, you may want to check for multiple referer header values — in which case a single string may not suffice, and you may want to do something like I do on <a href="https://github.com/whitep4nth3r/mk2-p4nth3rblog/tree/main/src/_edge-functions/referer.js" target="_blank">my website</a>.</p><p class="post__p">If you’re ready to deploy, <a href="https://www.netlify.com/blog/add-personalization-to-static-html-with-edge-functions-no-browser-javascript/#add-git-version-control" target="_blank">add git version control and deploy using the Netlify CLI by following this guide</a>.</p><h2 class="post__h2">Let’s recap</h2><p class="post__p">In this tutorial, we created a new project that served a static HTML file. We used an Edge Function to detect the value of the HTTP referer header, and transform the HTML response to add a sprinkle of ✨personalization✨. We also looked at how we could replicate functionality seamlessly in development and production environments using environment variables.</p><p class="post__p">To see this code in action, <a href="https://personalize-with-http-referer.netlify.app/" target="_blank">visit the live demo site</a> — which checks to see if you’ve been referred from Netlify, and includes the same functionality we set up in this guide. And finally, check out the <a href="https://github.com/whitep4nth3r/personalize-with-http-referer" target="_blank">demo repository on GitHub</a> to explore the code, fork it, and make it your own.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5pGUML5SUtAWJJRMJBhVSa/d81e6ad8596b3b7a64e00737bb803623/http_referer_demo_screenshot.png" alt="Screenshot of demo website, showing a personalized message at the top, including the message you can from the netlify blog post, showing it personalized the page based on the netlify.com referer header" height="2382" width="3680" /><h2 class="post__h2">Next steps</h2><p class="post__p">You’ve learned how you can modify static HTML pages based on the HTTP referer header, but did you know you can also perform actions based on other things like location, time, cookies and more? Check out the official <a href="https://ntl.fyi/3Qojhck" target="_blank">Netlify Edge Functions docs</a>, or browse our <a href="https://edge-functions-examples.netlify.app/" target="_blank">Edge Functions examples site</a> for some inspiration. Happy coding! 👩🏻‍💻</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>I changed my mind about writing new JavaScript frameworks</title>
          <description>Maybe you *should* write a new JavaScript framework. And here’s why.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/write-a-new-javascript-framework/</link>
          <guid>https://whitep4nth3r.com/blog/write-a-new-javascript-framework/</guid>
          <pubDate>Mon, 03 Oct 2022 23:00:00 GMT</pubDate>
          <category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">A few months ago, whilst deep in a dark cloud of personal overwhelm caused by the unrelenting exponential growth of the web ecosystem, I wrote about <a href="https://whitep4nth3r.com/blog/should-i-write-a-new-javascript-framework/" target="_blank">how you probably shouldn’t write a new JavaScript framework</a>. But I’ve changed my mind. Maybe you <b class="post__p--italic">should</b> write a new JavaScript framework. And here’s why.</p><h2 class="post__h2">There are fewer options than we thought</h2><p class="post__p">I’m currently building <a href="https://whattheframework.netlify.app/" target="_blank">WTF: What the Framework?</a> — a new tool to help developers choose their next JavaScript framework based on the features they need for their new project. The idea for WTF came out of the most frequently asked question in the Jamstack community: “What JavaScript framework should I use?” Whilst the answer is <b class="post__p--italic">always</b> “it depends,” I wanted to empower developers to drill into those dependencies and make a decision based on the <b class="post__p--italic">type of project</b> they’re building.</p><p class="post__p">WTF asks just two questions about your project, and shows you a list of JavaScript frameworks that are suited to building your project.</p><ol><li><p class="post__p">What type of website are you building?</p></li><li><p class="post__p">Do you need your website to maintain state across multiple pages?</p></li></ol><p class="post__p">The TL;DR is that the first question determines if you’re building a static site, a site that needs server-side rendering, or if you need a combination of the two; and the second question determines if you need a Single-Page Application (SPA) — a JavaScript application that builds HTML for different routes locally in the browser, or a Multi-Page Application (MPA) where every route has its own HTML file that’s delivered from the server and doesn’t rely on client-side JavaScript.</p><p class="post__p">The criteria for including a JavaScript framework on the WTF list were as follows:</p><ol><li><p class="post__p">The code is actively maintained (a release happened in the last year)</p></li><li><p class="post__p">The project has published a stable release (at least version 1)</p></li></ol><p class="post__p">And here’s where my <b class="post__p--italic">ridiculous</b> 300-new-JavaScript-frameworks-a-week-exaggeration falls a little flat. Say my project needs to serve both static content and server-side rendered pages, and I don’t need to manage state across multiple routes. There are currently only four <a href="https://whattheframework.netlify.app/hybrid/mpa" target="_blank">options to choose from on WTF</a>: Eleventy, RedwoodJS, Next.js and Gatsby. WTF?</p><p class="post__p">And here’s where it gets even more frustrating.</p><p class="post__p">Next.js or Gatsby aren’t entirely appropriate for my use case, because they’re actually <b class="post__p--italic">SPAs by default</b>. However, you <b class="post__p--italic">can </b><a href="https://nextjs.org/docs/advanced-features/static-html-export" target="_blank">generate a static HTML export with Next.js</a> or <a href="https://www.gatsbyjs.com/docs/conceptual/rendering-options/#static-site-generation-ssg" target="_blank">render a static site with Gatsby</a>, but you’d need to remember to use native anchor tags instead of the <code>&lt;Link /&gt;</code> component to prevent JavaScript pre-fetching and to preserve the concept of an MPA. What’s more, static site builds in both Next.js and Gatsby can’t make use of server-side rendering functionality. So with these two frameworks, your project is either a hybrid SPA, or a static MPA with workarounds.</p><p class="post__p">Eleventy is a good option, but the <a href="https://www.11ty.dev/docs/plugins/edge/" target="_blank">server-side rendering on the edge functionality</a> is currently an experimental feature that only works on Netlify.</p><p class="post__p">And so we’re left with RedwoodJS, a full-stack framework which could be a great option in theory, but appears to come with a lot of overheads and integrations that I’m not sure I need just yet!</p><p class="post__p">There simply aren’t <b class="post__p--bold">enough options</b>.</p><div class="post__callout">
  <h3 class="post__callout__title">Update: Nuxt is also a valid option!</h3>
    <div class="post__callout__content">
      <p>After sharing this post on Twitter, <a href="https://twitter.com/danielcroe">Daniel Roe</a> let me know that Nuxt 3 provides a hybrid combination of static pages and server-side rendered pages as an MPA. He also submitted a <a href="https://github.com/whitep4nth3r/whattheframework/pull/1">pull request to update the WTF project</a>. Thanks, Daniel!</p>

    </div>
  </div><h2 class="post__h2">There are problems to be solved</h2><p class="post__p">I’ve presented only one problem here — and there are <b class="post__p--italic">many</b>. <a href="https://www.zachleat.com/web/ssr-overloaded/" target="_blank">As Zach Leatherman (creator of Eleventy) points out</a>, there are also many ways to define server-side rendering, so how do I decide which framework to use, if I don’t know which type of SSR my new project needs, <b class="post__p--italic">or if it even needs SSR at all</b>? And what’s more, as the web continues to evolve, developers are hypothesising that <a href="https://twitter.com/BHolmesDev/status/1566793272995061761?s=20&t=rGJuTU8I138lvsOiQ7z2nA" target="_blank">servers and serverless can actually cut down build times <b class="post__p--italic">and</b> match your static site&#39;s speed</a>. So I <b class="post__p--italic">should</b> definitely be using servers, then? Should we even be building static sites anymore? 🌶</p><p class="post__p">And the SPA vs MPA debate rages on. <a href="https://twitter.com/BHolmesDev/status/1567886432181653505?s=20&t=rGJuTU8I138lvsOiQ7z2nA" target="_blank">Ben Holmes, core maintainer of Astro, argues that modern browser APIs could make Multi-Page Applications <b class="post__p--italic">feel</b> like Single-Page Apps</a> — rendering SPA frameworks much less necessary in the future.</p><p class="post__p">And this is only touching the surface. Whilst React has been the go-to SPA framework for almost a decade, younger frameworks such as <a href="https://www.solidjs.com/" target="_blank">SolidJS</a> are making moves to tackle the performance constraints and overheads that come with reactive DOM updates. There are certainly problems that could be solved with new frameworks. And we’re developers; we build to solve problems, right?</p><h2 class="post__h2">Frameworks can help solve problems</h2><p class="post__p">As <a href="https://twitter.com/ryancarniato" target="_blank">Ryan Carniato</a>, creator of SolidJS says: “We need new frameworks. We need innovation right now.” He goes on to describe how he’s committed to bringing framework authors together to work on building a better web ecosystem — together. “Qwik, Astro, Solid, Marko, 11ty, and Angular now share a friendly space to share ideas [and to] bridge the gap between these technologies and the people that would use them.” I’m really grateful to Ryan for sharing his insights on being a framework author. It’s actually what helped challenge my views on this part of the web ecosystem and inspired me to write this post.</p><p class="post__p">So if there’s a problem you want to solve, and if it could be solved through building a new JavaScript framework — build that new JavaScript framework. I’m with you. And I’d love to try it out one day.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Rewrite your git history in 4 friendly commands</title>
          <description>Did you make a series of unfortunate commits? Learn how to clean up your nonsense.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/rewrite-git-history/</link>
          <guid>https://whitep4nth3r.com/blog/rewrite-git-history/</guid>
          <pubDate>Mon, 05 Sep 2022 23:00:00 GMT</pubDate>
          <category>Git</category><category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I build a lot of public experimental demo repos to support my tutorials, and I often encourage people to fork them and deploy them to <a href="https://ntl.fyi/3vA7krR" target="_blank">Netlify</a> to try things out. The problem is, these experimental repos can often come with a <b class="post__p--italic">terribly messy git history</b> — and I don&#39;t want people to inherit that. And so, before I publish and promote a demo project, I like to clean up the git history ✨.</p><p class="post__p">First, the TL;DR: use these four commands — with caution (more on this later!) — and enjoy a tidy ship.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="QcDrnqSqyr"
      aria-describedby="QcDrnqSqyr">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="QcDrnqSqyr">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="QcDrnqSqyr" itemprop="text" content="%23%201.%20Reset%20the%20repo%20to%20the%20initial%20commit%2C%20preserving%20the%20current%20state%20of%20the%20files%20on%20your%20local%20machine%0Agit%20reset%20--soft%20%7BINITIAL_COMMIT_HASH%7D%0A%0A%23%202.%20Stage%20all%20files%0Agit%20add%20.%0A%0A%23%203.%20Make%20a%20commit%0Agit%20commit%20-m%20%22Initial%20commit%22%0A%0A%23%204.%20Force%20push%20to%20the%20origin%0Agit%20push%20--force">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># 1. Reset the repo to the initial commit, preserving the current state of the files on your local machine</span><br><span class="token function">git</span> reset <span class="token parameter variable">--soft</span> <span class="token punctuation">{</span>INITIAL_COMMIT_HASH<span class="token punctuation">}</span><br><br><span class="token comment"># 2. Stage all files</span><br><span class="token function">git</span> <span class="token function">add</span> <span class="token builtin class-name">.</span><br><br><span class="token comment"># 3. Make a commit</span><br><span class="token function">git</span> commit <span class="token parameter variable">-m</span> <span class="token string">"Initial commit"</span><br><br><span class="token comment"># 4. Force push to the origin</span><br><span class="token function">git</span> push <span class="token parameter variable">--force</span></code></pre>
    </div>
  </div>

  <p class="post__p">Let&#39;s look in detail at what each command does.</p><h2 class="post__h2">Soft reset to the initial commit</h2><p class="post__p">Each time you push a change to git, a unique identifier called a <b class="post__p--bold">commit hash</b> is generated from various bits of data associated with the commit, such as the commit message, file changes, commit author and date. Commit hashes point to the state of the repository when the commit was made, and allow us to move back and forward in time in the repository.</p><p class="post__p">If your repository is hosted on GitHub, you can view a list of your commit hashes from the home page of your repository, by clicking on the commits link at the top right of the file list.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5rxiTUVZq5UBWTXRwcOK9g/dd41d348a749932525c494c8832bf41d/commits.png" alt="Screenshot of a github repo, showing the commits list link in the active blue colour, highlighted with a white rounded border." height="1167" width="2032" /><p class="post__p">This will take you to a list of commits, showing a shortened commit hash to the right of the commit message. Scroll down to find the first commit, click to view the commit and full commit hash, or use the copy button to copy the full commit hash to your clipboard, which might look something like this: <code>64d52970e9b73551e7d837ec367610p</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4tQDm0mavR6r3MNOrAIrD3/26afce5aab81cb4ae84cf4d9b34ef4b2/commit_list.png" alt="A screenshot of a GitHub commit list, showing a tooltip on the top commit hash, which says view commit details." height="1167" width="2032" /><p class="post__p">There <b class="post__p--bold">are</b> ways to get the first commit hash using a terminal command, but the method may vary depending on the number of root commits in the history — which is beyond the scope of this blog post.</p><p class="post__p">When you&#39;ve got your initial commit hash, run the following commands in your terminal. The <code>reset</code> command instructs git to reset the repository to a particular state, in this case the initial commit, and the <code>--soft</code> flag leaves all files in <b class="post__p--italic">their most recent up-to-date state.</b></p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="TKkiDiJaKc"
      aria-describedby="TKkiDiJaKc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="TKkiDiJaKc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="TKkiDiJaKc" itemprop="text" content="%23%20reset%20the%20repo%20to%20the%20initial%20commit%0Agit%20reset%20--soft%20%7BINITIAL_COMMIT_HASH%7D%0A%0A%23%20check%20the%20status%20of%20your%20local%20repository%0Agit%20status">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># reset the repo to the initial commit</span><br><span class="token function">git</span> reset <span class="token parameter variable">--soft</span> <span class="token punctuation">{</span>INITIAL_COMMIT_HASH<span class="token punctuation">}</span><br><br><span class="token comment"># check the status of your local repository</span><br><span class="token function">git</span> status</code></pre>
    </div>
  </div>

  <p class="post__p">You should now see a message stating your branch is behind the main branch by however many commits you made after the first commit, and that there are some changes to be committed.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3HnxHF2YN4VBb6yPOxVuTC/14d4f2461eb1c05f1b580f50db8cd795/did_a_reset.png" alt="Terminal output showing a git reset --soft with a hash, then git status which shows the branch is behind origin main by 9 commits, and there are 7 file changes." height="1804" width="3680" /><h2 class="post__h2">Stage all files</h2><p class="post__p">Next, use <code>git add .</code> to stage all files. Given that your <code>.gitignore</code> file will remain up-to-date and include any sensitive files you don&#39;t want to commit, you should be good to go. But at this point it&#39;s worth double-checking that running <code>git status</code> didn&#39;t list any files you don&#39;t want to add to the repository.</p><h2 class="post__h2">Make a commit</h2><p class="post__p">Next it&#39;s time to overwrite that history with a nice, tidy commit message. Use <code>Initial commit</code> or whatever commit message you&#39;d like.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CcwGwFfEoh"
      aria-describedby="CcwGwFfEoh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CcwGwFfEoh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="CcwGwFfEoh" itemprop="text" content="git%20commit%20-m%20'I%20am%20rewriting%20history'">
      <pre class="language-bash"><code class="language-bash"><span class="token function">git</span> commit <span class="token parameter variable">-m</span> <span class="token string">'I am rewriting history'</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Force push to the origin</h2><p class="post__p">It&#39;s time to push your new commit to rewrite the history using <code>git push --force</code>. Force pushing tells git to <b class="post__p--italic">prioritise the changes in your local branch over the changes pushed to the remote</b>. And this is exactly what we want to do in this case! Run the following command in your terminal, and enjoy the power it brings. </p><p class="post__p"><b class="post__p--bold">Full disclaimer</b> that you should only force push in git when you absolutely know what you&#39;re doing, or if your project is tiny and you&#39;re the only developer working on it — like most of mine!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="oAaVikOcZw"
      aria-describedby="oAaVikOcZw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="oAaVikOcZw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="oAaVikOcZw" itemprop="text" content="git%20push%20--force">
      <pre class="language-bash"><code class="language-bash"><span class="token function">git</span> push <span class="token parameter variable">--force</span></code></pre>
    </div>
  </div>

  <p class="post__p">And just like that, you&#39;ve pushed a forced update to the repository.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7tS9BbaI6CnxBg4wi3PwRE/04acba73967e84e3973c838796ff80fa/rewriting_history.png" alt="Terminal output showing git commit -m I am rewriting history and git push --force" height="1884" width="3680" /><h2 class="post__h2">The result</h2><p class="post__p">Go back to GitHub and refresh the list of commits, and you&#39;ll see your shiny new commit in the commit list!</p><img src="https://images.ctfassets.net/56dzm01z6lln/7gIXNQSaoitl1k2ptf7tIS/b406a30dbd9a9c6febe2485be344378a/only_two_commits.png" alt="A screenshot of the repo I forced push to, showing only two commits." height="2334" width="4064" /><p class="post__p">The keen-eyed perfectionists out there might notice that I now have <b class="post__p--italic">two commits</b> in the history rather than one single initial commit. However, for me and my purposes this a quick and friendly way to clean up my experimental git history — and gives us all a reminder that nothing in code is ever, really perfect. </p><p class="post__p">If you are interested in completely resetting a git repository to its initial <b class="post__p--italic">initial</b> commit, the process involves a few more steps (which I think are a little less friendly). One way you can achieve this is by creating an orphan branch from main that contains all the current files, creating a single commit to that branch, and eventually replacing the original main branch with the new branch. Engineering Manager <a href="https://twitter.com/candosten" target="_blank">Candost Dagdeviren</a> has a <a href="https://candost.blog/how-to-reset-first-commit-in-git/" target="_blank">great blog post</a> which lays out the steps. </p><p class="post__p">So go forth and have a little spring clean of your git history — but please remember to <b class="post__p--bold">do it with caution</b> and only when you really, <b class="post__p--italic">really</b> need to!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>What's the difference between : and :: in CSS?</title>
          <description>I spent years Googling this question before the information stayed in my brain. Sound familiar? Then this post is for you.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/pseudo-classes-and-pseudo-elements/</link>
          <guid>https://whitep4nth3r.com/blog/pseudo-classes-and-pseudo-elements/</guid>
          <pubDate>Wed, 31 Aug 2022 23:00:00 GMT</pubDate>
          <category>CSS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I spent years Googling “the difference between : and :: in CSS” before the information stayed in my brain. Sound familiar? Then this post is for you.</p><p class="post__p">First up: if you need a quick answer to the title of this blog post then look no further!</p><ul><li><p class="post__p"><code>:</code> refers to pseudo-classes, such as <code>:visited</code> or <code>:hover</code></p></li><li><p class="post__p"><code>::</code> is for pseudo-elements, such as <code>::first-of-type</code> or <code>::after</code></p></li></ul><p class="post__p">And if you want a more detailed explanation, let’s dive into some examples.</p><h2 class="post__h2">What does “pseudo” mean?</h2><p class="post__p">The English definition of the word <b class="post__p--italic">pseudo</b> is “fake” or “not real”. So what on earth do we mean by <b class="post__p--italic">fake classes</b> and <b class="post__p--italic">fake elements</b>? Pseudo-classes and pseudo-elements are not <b class="post__p--italic">manually</b> written into HTML and don’t appear in the DOM (or document tree), but instead are <b class="post__p--bold">created by CSS</b>!</p><h2 class="post__h2">What’s a pseudo-class?</h2><p class="post__p">Pseudo-classes allow you to select elements in CSS based on information outside of the HTML written on the page, such as user interaction or information stored in the browser. Pseudo-classes are accessed by a single colon (:) followed by the pseudo-class name.</p><p class="post__p">You can use pseudo-classes to style an element based on its state. You might often see links you’ve visited on a page appear as a different colour. This is achieved by targeting the <code>:visited</code> pseudo-class of an anchor tag (<code>a</code>) in CSS to style it.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ArNsBeLaao"
      aria-describedby="ArNsBeLaao">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ArNsBeLaao">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="ArNsBeLaao" itemprop="text" content="a%3Avisited%20%7B%0A%20%20color%3A%20%23c58af9%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">a:visited</span> <span class="token punctuation">{</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> #c58af9<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">You can observe this in action on Google’s search engine. Head on over to <a href="https://google.com/" target="_blank">google.com</a> and search for something you know you’ve visited. Open up the browser dev tools, and find the <code>a:visited</code> selector in the CSS inspector.</p><img src="https://images.ctfassets.net/56dzm01z6lln/840kHdQ2QSUnEMHQhoQh3/dbb9db5d54732ad213fec733122a2464/visited_and_unvisited_google_results.png" alt="Screenshot of dark mode google search results showing the link to the top site I have visited is purple, and the second site in the list which I have not visited, which is blue. Dev tools is open and shows the anchor visited styles coming from the stylesheet." height="2382" width="3680" /><p class="post__p">As well as being affected by browser information such as visited links, pseudo-classes can also be affected (added or removed) by user actions on the page, such as hovering over or focussing on elements. Here’s the <code>:hover</code> pseudo-class in action on Google search results.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="okUMiQELpS"
      aria-describedby="okUMiQELpS">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="okUMiQELpS">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="okUMiQELpS" itemprop="text" content="a%3Ahover%20%7B%0A%20%20text-decoration%3A%20underline%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">a:hover</span> <span class="token punctuation">{</span><br>  <span class="token property">text-decoration</span><span class="token punctuation">:</span> underline<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <img src="https://images.ctfassets.net/56dzm01z6lln/1qbePHwU4spRkGm8XctMUd/c01941f0b097467ced18872aa8895d2a/hover_google_search.png" alt="Screenshot of google search results showing I am hovering over the top result, which is adding an underline to the link. Dev tools are open showing the style applied using the hover pseudo class." height="2382" width="3680" /><p class="post__p">To read more about the different types of pseudo-classes available to target in CSS, you can check out the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes" target="_blank">extensive documentation on MDN</a>.</p><h2 class="post__h2">What’s a pseudo-element?</h2><p class="post__p">Pseudo-element selectors allow you to use CSS to style a <b class="post__p--italic">specific part</b> of a DOM element. Pseudo-elements are accessed by a double colon (::) followed by the pseudo-element selector. Unlike pseudo-classes, pseudo-elements cannot be used to style an element according to its <b class="post__p--italic">state</b>.</p><p class="post__p">Here’s an example. Often, article-based websites use “drop caps”, a print convention that has existed for thousands of years, which uses a very large single letter to mark the start of a block of text. You can achieve this by targeting the <code>::first-letter</code> pseudo-element in CSS.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VtIHWJpUVS"
      aria-describedby="VtIHWJpUVS">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VtIHWJpUVS">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="VtIHWJpUVS" itemprop="text" content="p%3A%3Afirst-letter%20%7B%0A%20%20font-size%3A%20300%25%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">p::first-letter</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 300%<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Here’s a visual example showing how drop caps would look on my blog posts.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6APs2BIZWCBXSPCtbCnrhu/e799b7b14d25cb2d179bf69069338a4d/first_letter_my_blog.png" alt="A blog post on my site showing a large uppercase letter E at the start of a paragraph" height="2382" width="3680" /><p class="post__p">You can also choose to target the first line of an element, using the <code>::first-line</code> selector.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="EgrJMyVfUD"
      aria-describedby="EgrJMyVfUD">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="EgrJMyVfUD">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="EgrJMyVfUD" itemprop="text" content="p%3A%3Afirst-line%20%7B%0A%20%20font-size%3A%20300%25%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">p::first-line</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 300%<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Other common pseudo-element selectors you might use in your CSS files include:</p><ul><li><p class="post__p"><code>::before</code></p></li><li><p class="post__p"><code>::after</code></p></li><li><p class="post__p"><code>::first-of-type</code></p></li><li><p class="post__p"><code>::last-of-type</code></p></li><li><p class="post__p">and <code>::placeholder</code>.</p></li></ul><p class="post__p">Read more about the different types of pseudo-elements available to target in CSS on the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements" target="_blank">official MDN documentation</a>.</p><p class="post__p">And that’s it! May this be the last time you google the difference between <code>:</code> and <code>::</code> in CSS!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Rewrite HTML and transform page props in Next.js with Next.js Advanced Middleware</title>
          <description>Learn how to use Next.js Advanced Middleware from Netlify.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://ntl.fyi/nextjs-rewrite-html-at-the-edge</link>
          <guid>https://ntl.fyi/nextjs-rewrite-html-at-the-edge</guid>
          <pubDate>Wed, 24 Aug 2022 15:00:00 GMT</pubDate>
          <category>Tutorials</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Calling all Next.js developers! Do you wish you could do <b class="post__p--italic">more</b> with middleware? Do you want to be able to intercept and rewrite the response of a Next.js statically generated page at the edge based on geolocation data? Do you also want to be able to transform page props on the fly?</p><p class="post__p">Now you can! With Next.js Advanced Middleware. It’s powered by <a href="https://ntl.fyi/3OKYiyS" target="_blank">Netlify Edge Functions</a>, brand new and only on Netlify.</p><p class="post__p">In this tutorial, I show you how to use Next.js Advanced Middleware on Netlify to intercept a request to a statically pre-generated page, and rewrite the HTML response to change some text and page props based on geolocation data.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/_KZIs-8oulw"
        title="Next.js Advanced Middleware on Netlify"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <h2 class="post__h2">Deploy the tutorial code to Netlify</h2><p class="post__p">Here&#39;s the <a href="https://nextjs-advanced-middleware-demo.netlify.app/" target="_blank">demo site</a> I made earlier! If you want to dive in immediately, you can fork the tutorial’s code repository on GitHub, and deploy the site to your own Netlify account in seconds. Click on the Deploy to Netlify button below, and take it for a spin!</p>
    <a href="https://ntl.fyi/3K7uewQ" class="post__deployToNetlifyButton" title="Next.js Advanced Middleware Demo" target="_blank">
      <img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify Button">
    </a>
  <h2 class="post__h2">Build it yourself</h2><p class="post__p">Alternatively, if you’d like to build out the code from scratch, you can follow the guide below to create a new Next.js project, add a new static route, and use middleware to rewrite the HTML of the response at The Edge.</p><h3 class="post__h3">Prerequisites</h3><p class="post__p">Before getting started, check you’ve got the tools you’ll need to complete the tutorial.</p><ul><li><p class="post__p"><a href="https://nodejs.org/en/" target="_blank">Node.js</a> (LTS recommended)</p></li><li><p class="post__p"><a href="https://cli.github.com/" target="_blank">GitHub CLI</a></p></li><li><p class="post__p"><a href="https://www.npmjs.com/package/netlify-cli" target="_blank">Netlify CLI</a> — latest version</p></li></ul><h3 class="post__h3">Project setup</h3><p class="post__p">Let’s create a new Next.js app. Head on over to your terminal, and run the following command, substituting <code>YOUR_PROJECT_NAME</code> for a name of your choice.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="dmeHWvPCjN"
      aria-describedby="dmeHWvPCjN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="dmeHWvPCjN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="dmeHWvPCjN" itemprop="text" content="npx%20create-next-app%20%7BYOUR_PROJECT_NAME%7D">
      <pre class="language-bash"><code class="language-bash">npx create-next-app <span class="token punctuation">{</span>YOUR_PROJECT_NAME<span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, to make the magic happen, we need the <code>@netlify/next</code> package. Run the following command in your terminal, which will install the package as a dependency in the project’s package.json file.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="oSTtCEyZNx"
      aria-describedby="oSTtCEyZNx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="oSTtCEyZNx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="oSTtCEyZNx" itemprop="text" content="npm%20install%20%40netlify%2Fnext">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> @netlify/next</code></pre>
    </div>
  </div>

  <p class="post__p">Next, open up the project in your IDE of choice.</p><h3 class="post__h3">Add a new static route</h3><p class="post__p">Next.js offers file-based routing. Adding a JavaScript or TypeScript file into the <code>pages</code> directory creates a new browser route, with the same name as the file. Create a new file in the pages directory named <code>static.js</code>, and add the following code.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YUwFkpsLhY"
      aria-describedby="YUwFkpsLhY">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YUwFkpsLhY">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="jsx">
      <meta data-code-id="YUwFkpsLhY" itemprop="text" content="%2F%2F%20pages%2Fstatic.js%0A%0Aconst%20Page%20%3D%20()%20%3D%3E%20%7B%0A%20%20return%20(%0A%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%3Ch1%20id%3D%22message%22%3EA%20static%20page%3C%2Fh1%3E%0A%20%20%20%20%3C%2Fmain%3E%0A%20%20)%3B%0A%7D%3B%0A%0Aexport%20default%20Page%3B">
      <pre class="language-jsx"><code class="language-jsx"><span class="token comment">// pages/static.js</span><br><br><span class="token keyword">const</span> <span class="token function-variable function">Page</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">></span></span><span class="token plain-text"><br>      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token plain-text">A static page</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text"><br>    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">></span></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> Page<span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Back in your terminal, start the development server using the Netlify CLI by running the following command.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="aCTZAczfdU"
      aria-describedby="aCTZAczfdU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="aCTZAczfdU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="aCTZAczfdU" itemprop="text" content="netlify%20dev">
      <pre class="language-bash"><code class="language-bash">netlify dev</code></pre>
    </div>
  </div>

  <p class="post__p">A browser tab will automatically open up on localhost:8888. Navigate to <a href="http://localhost:8888/static" >http://localhost:8888/static</a> in your browser, and there’s your new static page.</p><img src="https://images.ctfassets.net/56dzm01z6lln/pvo8iwTDETmjQskK85zlk/9399c761cdf18d56f40a144e3186bf9e/static_page_1.png" alt="Screenshot of a browser window with the text a static page" height="1266" width="1826" /><p class="post__p">We’re going to pre-generate this page at build time with some base data before we transform it using Next.js Advanced Middleware. By exporting a function called <code>getStaticProps</code> from a page file, Next.js will pre-render this page at build time using the data returned by <code>getStaticProps()</code>.</p><p class="post__p">Add the following code to <code>static.js</code>, and pass the <code>message</code> prop into the <code>Page</code> function.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VWHaCPFatn"
      aria-describedby="VWHaCPFatn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VWHaCPFatn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="jsx">
      <meta data-code-id="VWHaCPFatn" itemprop="text" content="%2F%2F%20pages%2Fstatic.js%0A%0Aexport%20async%20function%20getStaticProps()%20%7B%0A%20%20return%20%7B%0A%20%20%20%20props%3A%20%7B%0A%20%20%20%20%20%20message%3A%20%22This%20is%20a%20static%20page%20%E2%80%94%20and%20now%20this%20is%20a%20prop!%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%3B%0A%7D%0A%0Aconst%20Page%20%3D%20(%7B%20message%20%7D)%20%3D%3E%20%7B%0A%20%20return%20(%0A%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%3Ch1%20id%3D%22message%22%3E%7Bmessage%7D%3C%2Fh1%3E%0A%20%20%20%20%3C%2Fmain%3E%0A%20%20)%3B%0A%7D%3B%0A%0Aexport%20default%20Page%3B">
      <pre class="language-jsx"><code class="language-jsx"><span class="token comment">// pages/static.js</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getStaticProps</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">"This is a static page — and now this is a prop!"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">const</span> <span class="token function-variable function">Page</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> message <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">></span></span><span class="token plain-text"><br>      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token punctuation">{</span>message<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text"><br>    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">></span></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> Page<span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">And just to confirm everything&#39;s wired up, here’s our updated static page, using the message prop.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2mMAtB1FYQHMRhslNS5x5c/c5ee449636e8c1906a7de4d934c836d2/static_page_2.png" alt="Screenshot of a browser window with the text this is a static page — and now this is a prop" height="1266" width="1826" /><p class="post__p">Next, onto the middleware!</p><h3 class="post__h3">HTML rewrites and page data transforms</h3><p class="post__p">Middleware in Next.js allows you to run code before an HTTP request is completed — and sits in the middle of the request and response. You can create a middleware file in Next.js using JavaScript or TypeScript, and for the purposes of this tutorial we’re going to write middleware in TypeScript.</p><p class="post__p">At the root of your project, create a new file and name it <code>middleware.ts</code>. This file will run on every request when your app is running — including pages, static files and assets. In <code>middleware.ts</code>, add the following code. We’re importing the type <code>NextRequest</code>, which represents the incoming HTTP request, exporting an async function named <code>middleware</code>, and passing in the request as <code>nextRequest</code> to the function, which is a <code>NextRequest</code> object.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ptHJdnHnUi"
      aria-describedby="ptHJdnHnUi">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ptHJdnHnUi">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="ptHJdnHnUi" itemprop="text" content="%2F%2F%20middleware.ts%0A%0Aimport%20type%20%7B%20NextRequest%20%7D%20from%20%22next%2Fserver%22%3B%0A%0Aexport%20async%20function%20middleware(nextRequest%3A%20NextRequest)%20%7B%0A%0A%7D">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// middleware.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> NextRequest <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"next/server"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">middleware</span><span class="token punctuation">(</span>nextRequest<span class="token operator">:</span> NextRequest<span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">In this demo, we want the middleware to run on the route named <code>static</code>. Let’s get the pathname from the request, and prepare the function to do something when the pathname starts with <code>static</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="AkwEmiofyT"
      aria-describedby="AkwEmiofyT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="AkwEmiofyT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="AkwEmiofyT" itemprop="text" content="%2F%2F%20middleware.ts%0A%0Aimport%20type%20%7B%20NextRequest%20%7D%20from%20%22next%2Fserver%22%3B%0A%0Aexport%20async%20function%20middleware(nextRequest%3A%20NextRequest)%20%7B%0A%20%20const%20pathname%20%3D%20nextRequest.nextUrl.pathname%3B%0A%0A%20%20if%20(pathname.startsWith(%22%2Fstatic%22))%20%7B%0A%20%20%20%20%2F%2F%20do%20something%20here%0A%20%20%7D%0A%7D">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// middleware.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> NextRequest <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"next/server"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">middleware</span><span class="token punctuation">(</span>nextRequest<span class="token operator">:</span> NextRequest<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> pathname <span class="token operator">=</span> nextRequest<span class="token punctuation">.</span>nextUrl<span class="token punctuation">.</span>pathname<span class="token punctuation">;</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>pathname<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">"/static"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// do something here</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, it’s time for the Netlify magic. Let’s rewrite the HTML of the static page we created earlier. Near the top of the file, import <code>MiddlewareRequest</code> as a named import from <code>@netlify/next</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="dFShsDlBaN"
      aria-describedby="dFShsDlBaN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="dFShsDlBaN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="dFShsDlBaN" itemprop="text" content="%2F%2F%20middleware.ts%0A%0Aimport%20type%20%7B%20NextRequest%20%7D%20from%20%22next%2Fserver%22%3B%0Aimport%20%7B%20MiddlewareRequest%20%7D%20from%20%22%40netlify%2Fnext%22%3B%0A%0Aexport%20async%20function%20middleware(nextRequest%3A%20NextRequest)%20%7B%0A%20%20const%20pathname%20%3D%20nextRequest.nextUrl.pathname%3B%0A%0A%20%20if%20(pathname.startsWith(%22%2Fstatic%22))%20%7B%0A%20%20%20%20%2F%2F%20do%20something%20here%0A%20%20%7D%0A%7D">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// middleware.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> NextRequest <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"next/server"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> MiddlewareRequest <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@netlify/next"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">middleware</span><span class="token punctuation">(</span>nextRequest<span class="token operator">:</span> NextRequest<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> pathname <span class="token operator">=</span> nextRequest<span class="token punctuation">.</span>nextUrl<span class="token punctuation">.</span>pathname<span class="token punctuation">;</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>pathname<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">"/static"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// do something here</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Inside the middleware function, let’s initialize a new Netlify <code>MiddlewareRequest, </code>passing in the original <code>NextRequest </code>as <code>nextRequest</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ISRJJCVJtE"
      aria-describedby="ISRJJCVJtE">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ISRJJCVJtE">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="ISRJJCVJtE" itemprop="text" content="import%20type%20%7B%20NextRequest%20%7D%20from%20%22next%2Fserver%22%3B%0Aimport%20%7B%20MiddlewareRequest%20%7D%20from%20%22%40netlify%2Fnext%22%3B%0A%0Aexport%20async%20function%20middleware(nextRequest%3A%20NextRequest)%20%7B%0A%20%20const%20pathname%20%3D%20nextRequest.nextUrl.pathname%3B%0A%0A%20%20const%20middlewareRequest%20%3D%20new%20MiddlewareRequest(nextRequest)%3B%0A%0A%20%20if%20(pathname.startsWith(%22%2Fstatic%22))%20%7B%0A%20%20%20%20%2F%2F%20do%20something%20here%0A%20%20%7D%0A%7D">
      <pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> NextRequest <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"next/server"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> MiddlewareRequest <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@netlify/next"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">middleware</span><span class="token punctuation">(</span>nextRequest<span class="token operator">:</span> NextRequest<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> pathname <span class="token operator">=</span> nextRequest<span class="token punctuation">.</span>nextUrl<span class="token punctuation">.</span>pathname<span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> middlewareRequest <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">MiddlewareRequest</span><span class="token punctuation">(</span>nextRequest<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>pathname<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">"/static"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// do something here</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">This enhances the original request object by adding in all of the good stuff available in <a href="https://ntl.fyi/3OKYiyS" target="_blank">Netlify Edge Functions</a>. Netlify Edge Functions are very much like serverless functions. You can write them using JavaScript or TypeScript, but instead of using <a href="https://nodejs.org/en/" target="_blank">Node.js</a> under the hood, they are powered by <a href="https://deno.land/" target="_blank">Deno</a> — an open source runtime built on web standards — and they are executed at the closest server location to a request. This enables full personalization of pages at The Edge in Next.js, with no need to use <code>getServerSideProps()</code> in your page routes. It also works with Incremental Static Regeneration.</p><p class="post__p">Now our <code>NextRequest</code> object is levelled-up with Netlify&#39;s <code>MiddlewareRequest</code>, we have the power to modify the HTTP response before it&#39;s returned — because we can actually send the request to the origin to fetch it and work with it.</p><p class="post__p">Add the following code inside the <code>if</code> statement, and let’s unpack what it does.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="eEVXPLEAlI"
      aria-describedby="eEVXPLEAlI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="eEVXPLEAlI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="eEVXPLEAlI" itemprop="text" content="%2F%2F%20middleware.ts%0A%0Aimport%20type%20%7B%20NextRequest%20%7D%20from%20%22next%2Fserver%22%3B%0Aimport%20%7B%20MiddlewareRequest%20%7D%20from%20%22%40netlify%2Fnext%22%3B%0A%0Aexport%20async%20function%20middleware(nextRequest%3A%20NextRequest)%20%7B%0A%20%20const%20pathname%20%3D%20nextRequest.nextUrl.pathname%3B%0A%0A%20%20const%20middlewareRequest%20%3D%20new%20MiddlewareRequest(nextRequest)%3B%0A%0A%20%20if%20(pathname.startsWith(%22%2Fstatic%22))%20%7B%0A%20%20%20%20const%20response%20%3D%20await%20middlewareRequest.next()%3B%09%0A%0A%20%20%20%20const%20message%20%3D%20%60This%20was%20a%20static%20page%20but%20has%20been%20transformed%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20in%20%24%7BnextRequest%3F.geo%3F.city%7D%2C%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24%7BnextRequest%3F.geo%3F.country%7D%20using%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%40netlify%2Fnext%20in%20middleware.ts!%60%3B%0A%0A%20%20%20%20response.replaceText(%22%23message%22%2C%20message)%3B%0A%20%20%20%20response.setPageProp(%22message%22%2C%20message)%3B%0A%0A%20%20%20%20return%20response%3B%0A%20%20%7D%0A%7D">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// middleware.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> NextRequest <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"next/server"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> MiddlewareRequest <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@netlify/next"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">middleware</span><span class="token punctuation">(</span>nextRequest<span class="token operator">:</span> NextRequest<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> pathname <span class="token operator">=</span> nextRequest<span class="token punctuation">.</span>nextUrl<span class="token punctuation">.</span>pathname<span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> middlewareRequest <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">MiddlewareRequest</span><span class="token punctuation">(</span>nextRequest<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>pathname<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">"/static"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> middlewareRequest<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>	<br><br>    <span class="token keyword">const</span> message <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">This was a static page but has been transformed<br>                     in </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nextRequest<span class="token operator">?.</span>geo<span class="token operator">?.</span>city<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, <br>                     </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nextRequest<span class="token operator">?.</span>geo<span class="token operator">?.</span>country<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> using <br>                     @netlify/next in middleware.ts!</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>    response<span class="token punctuation">.</span><span class="token function">replaceText</span><span class="token punctuation">(</span><span class="token string">"#message"</span><span class="token punctuation">,</span> message<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    response<span class="token punctuation">.</span><span class="token function">setPageProp</span><span class="token punctuation">(</span><span class="token string">"message"</span><span class="token punctuation">,</span> message<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">return</span> response<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">If the pathname starts with <code>static</code>, we grab the next response in the HTTP chain by awaiting <code>middlewareRequest.next()</code> — this is our static page — and assign it to the variable <code>response</code>. Then, using geolocation data from <code>nextRequest</code>, we construct a new personalized message to replace the static page content.</p><p class="post__p">Next.js Advanced Middleware from Netlify includes a <code>replaceText</code> function, and also has support for more powerful transforms using the <a href="https://github.com/worker-tools/html-rewriter" target="_blank">HTML Rewriter</a> stream transformer. To get access to the message from the DOM in the middleware function, we add an ID to the DOM node containing the message we want to transform on the static page.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="qCxdPLRThH"
      aria-describedby="qCxdPLRThH">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="qCxdPLRThH">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="jsx">
      <meta data-code-id="qCxdPLRThH" itemprop="text" content="%2F%2F%20pages%2Fstatic.js%0A%0Aconst%20Page%20%3D%20(%7B%20message%20%7D)%20%3D%3E%20%7B%0A%20%20return%20(%0A%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%3Ch1%20id%3D%22message%22%3E%7Bmessage%7D%3C%2Fh1%3E%0A%20%20%20%20%3C%2Fmain%3E%0A%20%20)%3B%0A%7D%3B">
      <pre class="language-jsx"><code class="language-jsx"><span class="token comment">// pages/static.js</span><br><br><span class="token keyword">const</span> <span class="token function-variable function">Page</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> message <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>main</span><span class="token punctuation">></span></span><span class="token plain-text"><br>      </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>h1</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>message<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token punctuation">{</span>message<span class="token punctuation">}</span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>h1</span><span class="token punctuation">></span></span><span class="token plain-text"><br>    </span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>main</span><span class="token punctuation">></span></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Then, we run <code>replaceText</code> on the response, passing in the DOM ID we added to the message in <code>static.js</code>, and the new message we constructed. To ensure the hydrated content on the client matches the server-side rendered HTML (and to avoid a <code>React hydration error</code>), we can also update page props using <code>setPageProp</code> to update the underlying message data on the page as well.</p><p class="post__p">Finally, return the <code>response</code>! Go back to your browser, and check out the updated message on a previously statically generated page.</p><img src="https://images.ctfassets.net/56dzm01z6lln/63zEt2BCvqJZiIA7EkOetr/0407a94bdc088791ba70300f4b0a0e6a/static_page_3.png" alt="Screenshot of browser with the text this was a static page but has been transformed in Manchester, GB using @netlify/next in middleware.ts!" height="1266" width="1826" /><p class="post__p">The message has been transformed in the middleware file and the HTML of a static page has been rewritten. And what’s more — the page prop has also been updated! Inspect the page source, search for <code>NEXT_DATA</code> in the DOM, and check out the updated page message prop — all powered by Next.js Advanced Middleware from Netlify.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7JVLgOUVrxrrZUcOYBRqNd/8039f3d6c6ebdeef71064336642d560b/next_data_updated.png" alt="Screenshot of DOM showing the script tag with id NEXT_DATA, showing the JSON in the page props has been updated with the new message." height="1282" width="2386" /><p class="post__p">Let’s deploy to Netlify to watch HTML rewrites happen in real-time on a live URL.</p><h3 class="post__h3">Connect your git repo</h3><p class="post__p">We have a number of options for how we <a href="https://ntl.fyi/3dtPoJ1" target="_blank">deploy a Next.js project to Netlify</a>. In this tutorial, we’ll connect to a new git repo so that we get CI/CD all rolled in and configured for free.</p><p class="post__p">Next.js projects come with a git repo already initialized. Stage and commit your files using the following commands.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="dqRLDFiLbO"
      aria-describedby="dqRLDFiLbO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="dqRLDFiLbO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="dqRLDFiLbO" itemprop="text" content="git%20add%20.%20%23%20stage%20your%20files%0Agit%20commit%20-m%20'Initial%20commit'%20%23%20commit%20your%20files%20with%20a%20commit%20message%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token function">git</span> <span class="token function">add</span> <span class="token builtin class-name">.</span> <span class="token comment"># stage your files</span><br><span class="token function">git</span> commit <span class="token parameter variable">-m</span> <span class="token string">'Initial commit'</span> <span class="token comment"># commit your files with a commit message</span></code></pre>
    </div>
  </div>

  <p class="post__p">The next steps will take you through adding your repository to GitHub via the GitHub CLI — but you can push your project to GitHub however you’re most comfortable.</p><p class="post__p">Run the following command in your terminal to create a new repository that’s connected to your GitHub account:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xrKCCYMumc"
      aria-describedby="xrKCCYMumc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xrKCCYMumc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="xrKCCYMumc" itemprop="text" content="gh%20repo%20create">
      <pre class="language-bash"><code class="language-bash">gh repo create</code></pre>
    </div>
  </div>

  <p class="post__p">When prompted on the kind of repository you&#39;d like to create, select: <code>Push an existing local repository to GitHub</code>. Follow the remaining prompts to fill out the relevant project details. Now, you’re ready to deploy to Netlify!</p><h3 class="post__h3">Deploy to Netlify using the Netlify CLI</h3><p class="post__p">If you&#39;re not already logged in to the Netlify CLI, run the following command in your terminal, and follow the prompts to authorize with Netlify (a browser window will open).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="piHyenAzXV"
      aria-describedby="piHyenAzXV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="piHyenAzXV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="piHyenAzXV" itemprop="text" content="netlify%20login">
      <pre class="language-bash"><code class="language-bash">netlify login</code></pre>
    </div>
  </div>

  <p class="post__p">Next, run the following command to initialize a new project on Netlify.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fcARyCvmmk"
      aria-describedby="fcARyCvmmk">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fcARyCvmmk">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="fcARyCvmmk" itemprop="text" content="netlify%20init">
      <pre class="language-bash"><code class="language-bash">netlify init</code></pre>
    </div>
  </div>

  <ul><li><p class="post__p"><b class="post__p--bold">What would you like to do? </b><code>Create &amp; configure a new site</code></p></li><li><p class="post__p"><b class="post__p--bold">Team: </b><code>YOUR_TEAM</code></p></li><li><p class="post__p"><b class="post__p--bold">Site name (optional): </b><code>CHOOSE_UNIQUE_SITE_NAME</code></p></li></ul><p class="post__p">The Netlify CLI will auto-detect that you’re using Next.js, so the following prompts will be pre-filled. Hit enter to use the defaults.</p><ul><li><p class="post__p"><b class="post__p--bold">Your build command (hugo build/yarn run build/etc): </b><code>(next build)</code></p></li><li><p class="post__p"><b class="post__p--bold">Directory to deploy (blank for current dir): </b><code>(.next)</code></p></li><li><p class="post__p"><b class="post__p--bold">Netlify functions folder: </b><code>(netlify/functions)</code></p></li></ul><p class="post__p">Wait a few seconds… and your new site is deployed! 🎉 You can now demonstrate your new personalization skills in Next.js by sending the live URL to your friends.</p><h2 class="post__h2">Find out more</h2><p class="post__p">If you’d like to find out more, visit <a href="https://ntl.fyi/3QPeDUJ" target="_blank">Netlify’s official documentation</a> to learn about what’s now possible with Next.js Advanced Middleware — only on Netlify.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Add personalization to static HTML with Netlify Edge Functions — no browser JavaScript required</title>
          <description>Check out the video tutorial and accompanying walk-through.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/add-personalization-to-static-html-with-edge-functions-no-browser-javascript/</link>
          <guid>https://www.netlify.com/blog/add-personalization-to-static-html-with-edge-functions-no-browser-javascript/</guid>
          <pubDate>Thu, 11 Aug 2022 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>Serverless</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Personalization — the process of creating customized experiences for visitors to a website — is a hot topic on the web in 2022. In a crowded world of digital experiences clambering for our attention, businesses are turning to personalization to connect more deeply with their customers, and ultimately, generate more sales and growth.</p><p class="post__p">And shipping less client-side JavaScript is an even hotter topic. New and emerging front-end frameworks such as <a href="https://astro.build/" target="_blank">Astro</a> and <a href="https://www.11ty.dev/" target="_blank">Eleventy</a> are pushing the boundaries of the JavaScript framework era of the late 2010s, making it their core mission to ship less and less JavaScript <b class="post__p--italic">by default</b>, and bring developers’ focus back to a lean, native and performant web.</p><p class="post__p">The great news is that with <a href="https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/api/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=ef-context-object#netlify-specific-context-object" target="_blank">Netlify Edge Functions</a> you can achieve dynamic personalization with <b class="post__p--italic">no</b> client-side JavaScript — resulting in a great developer experience, and an even better end-user outcome. There’s also no need introduce the overhead of a front-end framework simply to gain this ability in your projects.</p><p class="post__p">In this video tutorial, I show you how to use Netlify Edge Functions to intercept an HTTP request on a static HTML page, transform the response on the fly by injecting the geolocation data of the request, and return the updated response to the browser.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/6pEVhH37xQE"
        title="Getting started with Netlify Edge Functions"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <h2 class="post__h2">Deploy the tutorial code to Netlify</h2><p class="post__p">If you want to dive in immediately, you can fork the tutorial’s code repository on GitHub, and deploy the site to your own Netlify account in seconds. Click on the Deploy to Netlify button below, and take it for a spin!</p>
    <a href="https://ntl.fyi/3C1Uwi2" class="post__deployToNetlifyButton" title="Deploy getting started with Netlify Edge Functions Tutorial" target="_blank">
      <img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify Button">
    </a>
  <h2 class="post__h2">Build it from scratch</h2><p class="post__p">Alternatively, if you’d like to build out the code from scratch, you can follow the guide below to create a simplified version of the project, without the hello world Edge Function included in the video tutorial.</p><h3 class="post__h3">Prerequisites</h3><p class="post__p">Before getting started, check you’ve got the tools you’ll need to complete the tutorial.</p><ul><li><p class="post__p"><a href="https://nodejs.org/en/" target="_blank">Node.js</a> (LTS recommended)</p></li><li><p class="post__p"><a href="https://cli.github.com/" target="_blank">GitHub CLI</a></p></li><li><p class="post__p"><a href="https://docs.netlify.com/cli/get-started/?utm_campaign=devex-san&utm_source=netlify-blog&utm_term=docs-cli-get-started&utm_content=docs-cli-get-started" target="_blank">Netlify CLI</a></p></li></ul><h3 class="post__h3">Setup a project to serve static HTML files</h3><p class="post__p">In a new project directory, create a public directory. And inside that, add an <code>index.html</code>, and a file named <code>hello-template.html</code>.</p><p class="post__p">Add some basic boilerplate to the index.html file (if you’re using VSCode you can type <code>!</code> followed by tab in an HTML file to generate some HTML boilerplate), or you can <a href="https://github.com/whitep4nth3r/getting-started-with-netlify-edge-functions/blob/main/public/index.html" target="_blank">copy and paste the file from the tutorial repository</a>.</p><p class="post__p">Next, add some similar boilerplate to <code>hello-template.html</code>, as well as some placeholder content that we’ll replace using an Edge Function in the next step. We’re going to be replacing the placeholder with some geolocation data, so I’ve named my placeholder <code>LOCATION_UNKNOWN</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3jc2QrJhFEx761XDo79KOy/ceaa11cd41767c69724f25be87c422ef/setup_static_files_vscode.png" alt="A screenshot of VSCode, showing two html files in a public directory. The visible code is the hello template file with the location data placeholder." height="2382" width="3680" /><p class="post__p">Next, we’re going to set up the project to serve those two static HTML files from the public directory using <a href="https://docs.netlify.com/configure-builds/file-based-configuration/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-file-config" target="_blank">Netlify’s file-based configuration</a>. At the root of your project, add a <code>netlify.toml</code> file. This is a configuration file that specifies how Netlify builds and deploys your site — including redirects, branch settings, edge function routing and more.</p><p class="post__p">Add the following code to <code>netlify.toml</code> to instruct Netlify to serve your static files from the public directory.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="unhAuXWvYa"
      aria-describedby="unhAuXWvYa">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="unhAuXWvYa">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="unhAuXWvYa" itemprop="text" content="%5Bbuild%5D%0A%20%20publish%20%3D%20%22%2Fpublic%22">
      <pre class="language-toml"><code class="language-toml"><span class="token punctuation">[</span><span class="token table class-name">build</span><span class="token punctuation">]</span><br>  <span class="token key property">publish</span> <span class="token punctuation">=</span> <span class="token string">"/public"</span></code></pre>
    </div>
  </div>

  <p class="post__p">Let’s check everything’s wired up. In a terminal window at the root of your project, run the following command to start up a development server with the Netlify CLI:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZRDcVkBaeq"
      aria-describedby="ZRDcVkBaeq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZRDcVkBaeq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="ZRDcVkBaeq" itemprop="text" content="netlify%20dev">
      <pre class="language-bash"><code class="language-bash">netlify dev</code></pre>
    </div>
  </div>

  <p class="post__p">When the development server is up and running, head on over to your browser, and you’ll see your <code>index.html</code> on <a href="http://localhost:8888/" >http://localhost:8888</a>, and <code>hello-template.html</code> on <a href="http://localhost:8888/hello-template" >http://localhost:8888/hello-template</a>. You’re now ready to transform this static HTML file with an Edge Function!</p><img src="https://images.ctfassets.net/56dzm01z6lln/2WTQI7vpanCdNJnNY0H3YV/0d1704ae0bc1cc63f58d18305cba2052/template_with_placeholder.png" alt="A browser screenshot, showing the hello template file with the placeholder location unknown" height="2382" width="3680" /><h3 class="post__h3">Transform an HTTP response with an Edge Function</h3><p class="post__p">We’re going to write an Edge Function to intercept a request to <code>hello-template</code> using a URL query parameter (<code>?method=transform</code>), grab the geolocation data of the request, and transform the HTTP response to include the city and country name.</p><p class="post__p">You can write Edge Functions using JavaScript or TypeScript. In this tutorial, we’re going to write Edge Function code in JavaScript. Edge Function files live in a special directory so that Netlify knows how to bundle them when you deploy your site. At the root of your project, create a <code>netlify</code> directory, and inside that add an <code>edge-functions</code> directory. Create a new file inside the edge-functions directory and name it <code>transform-hello-template.js</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/n1Ohm1TzUjmoaIjlTBEgq/cd924474ac8cd914a8493ee43d5812be/add_ef_files.png" alt="A screenshot of VSCode, showing the transform hello template js file added into an edge functions directory, inside a netlify directory." height="2382" width="3680" /><p class="post__p">Before we write the Edge Function code, let’s wire up the file to the development environment. In your <code>netlify.toml</code> file, add the following code, below the build settings. This tells Netlify to run the <code>transform-hello-template</code> function when a user requests the <code>hello-template</code> page in the browser. Stop and start your development server so the CLI picks up the changes in netlify.toml.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xxeLCYemOC"
      aria-describedby="xxeLCYemOC">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xxeLCYemOC">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="xxeLCYemOC" itemprop="text" content="%23%20netlify.toml%0A%0A%5B%5Bedge_functions%5D%5D%0A%20%20path%20%3D%20%22%2Fhello-template%22%0A%20%20function%20%3D%20%22transform-hello-template%22">
      <pre class="language-toml"><code class="language-toml"><span class="token comment"># netlify.toml</span><br><br><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">edge_functions</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br>  <span class="token key property">path</span> <span class="token punctuation">=</span> <span class="token string">"/hello-template"</span><br>  <span class="token key property">function</span> <span class="token punctuation">=</span> <span class="token string">"transform-hello-template"</span></code></pre>
    </div>
  </div>

  <p class="post__p">In <code>transform-hello-template.js</code>, add the following code. And let’s unpack what it does.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="iUweEBFKHd"
      aria-describedby="iUweEBFKHd">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="iUweEBFKHd">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="iUweEBFKHd" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Ftransform-hello-template.js%0A%0Aexport%20default%20async%20(request%2C%20context)%20%3D%3E%20%7B%0A%20%20const%20url%20%3D%20new%20URL(request.url)%3B%0A%0A%20%20%2F%2F%20Look%20for%20the%20%22%3Fmethod%3Dtransform%22%20query%20parameter%0A%20%20%2F%2F%20return%20if%20we%20don't%20find%20it%0A%20%20if%20(url.searchParams.get(%22method%22)%20!%3D%3D%20%22transform%22)%20%7B%0A%20%20%20%20return%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20Get%20the%20page%20content%20that%20will%20be%20served%20next%0A%20%20%2F%2F%20In%20this%20tutorial%2C%20it%20will%20be%20the%20content%20from%20hello-template%0A%20%20const%20response%20%3D%20await%20context.next()%3B%0A%20%20const%20page%20%3D%20await%20response.text()%3B%0A%0A%20%20%2F%2F%20Search%20for%20the%20placeholder%0A%20%20const%20regex%20%3D%20%2FLOCATION_UNKNOWN%2Fi%3B%0A%0A%20%20%2F%2F%20Get%20the%20location%20from%20the%20Context%20object%0A%20%20const%20location%20%3D%20%60%24%7Bcontext.geo.city%7D%2C%20%24%7Bcontext.geo.country.name%7D%60%3B%0A%0A%20%20%2F%2F%20Replace%20the%20content%20with%20the%20current%20location%0A%20%20const%20updatedPage%20%3D%20page.replace(regex%2C%20location)%3B%0A%0A%20%20%2F%2F%20Return%20the%20response%0A%20%20return%20new%20Response(updatedPage%2C%20response)%3B%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// netlify/edge-functions/transform-hello-template.js</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">request<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Look for the "?method=transform" query parameter</span><br>  <span class="token comment">// return if we don't find it</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>url<span class="token punctuation">.</span>searchParams<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">"method"</span><span class="token punctuation">)</span> <span class="token operator">!==</span> <span class="token string">"transform"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token comment">// Get the page content that will be served next</span><br>  <span class="token comment">// In this tutorial, it will be the content from hello-template</span><br>  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> context<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> page <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">text</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Search for the placeholder</span><br>  <span class="token keyword">const</span> regex <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">LOCATION_UNKNOWN</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Get the location from the Context object</span><br>  <span class="token keyword">const</span> location <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>context<span class="token punctuation">.</span>geo<span class="token punctuation">.</span>city<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>context<span class="token punctuation">.</span>geo<span class="token punctuation">.</span>country<span class="token punctuation">.</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Replace the content with the current location</span><br>  <span class="token keyword">const</span> updatedPage <span class="token operator">=</span> page<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span>regex<span class="token punctuation">,</span> location<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Return the response</span><br>  <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span>updatedPage<span class="token punctuation">,</span> response<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">We start by exporting a default async function. Edge Functions take two parameters, <code>request</code> — which represents the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request" target="_blank">incoming HTTP request</a>, and <code>context</code> — which is a <a href="https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/api/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=ef-context-object#netlify-specific-context-object" target="_blank">Netlify specific API</a> that exposes geolocation data, lets you work with cookies, rewrites, site information and more.</p><p class="post__p">First, we parse the URL from the request. Then, we look for the <code>method=transform</code> query parameter on the URL, using <code>url.searchParams.get()</code>. If we don’t find the required query parameter on the URL, we return and exit out of the Edge Function early, which will serve the hello-template with the placeholder unchanged.</p><p class="post__p">If we <b class="post__p--italic">do</b> find <code>?method=transform</code> on the URL, we use <code>context.next()</code> to grab the next request in the HTTP chain — in this case it’s our hello-template file — and then we grab the text content of the page, assigning it to the variable <code>page</code>. Next, we use a regular expression to search for the placeholder in the text of hello-template, which we want to replace with geolocation data.</p><p class="post__p">Geolocation data is available on the <code>context</code> object, as <code>context.geo</code>. Using JavaScript string interpolation, we can construct a friendly, readable string comprising the city and country name, which we then use to replace the string matched by the regular expression.</p><p class="post__p">Finally, we return a new <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response" target="_blank">HTTP Response</a> containing the updated page. Navigate to the following URL in your browser: <a href="http://localhost:8888/hello-template?method=transform" >http://localhost:8888/hello-template?method=transform</a> — and you’ll see the placeholder updated with Edge-powered geolocation data! Personalization with no JavaScript: achievement unlocked! 🏆</p><img src="https://images.ctfassets.net/56dzm01z6lln/12BOwHSDM9zaa1shuZHrxX/8f364cbd3b499db8e0e8591b6e7493d4/personalisation_done.png" alt="A screenshot of the finished result in the browser, where the placeholder template has been updated with Manchester, United Kingdom" height="2382" width="3680" /><h3 class="post__h3">Add git version control</h3><p class="post__p">Now that we have the project working locally, we have a number of options for how we <a href="https://docs.netlify.com/site-deploys/overview/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_term=site-deploys-overview&utm_content=site-deploys-overview" target="_blank">deploy it to Netlify</a>. In this tutorial, we’ll connect to a new git repo so that we get CI/CD all rolled in and configured for free.</p><p class="post__p">Initialize a new git repository by running the following command:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CiMcpSRbEX"
      aria-describedby="CiMcpSRbEX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CiMcpSRbEX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="CiMcpSRbEX" itemprop="text" content="git%20init">
      <pre class="language-bash"><code class="language-bash"><span class="token function">git</span> init</code></pre>
    </div>
  </div>

  <p class="post__p">Stage and commit your files:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="rAdEqzuphg"
      aria-describedby="rAdEqzuphg">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="rAdEqzuphg">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="rAdEqzuphg" itemprop="text" content="git%20add%20.%20%23%20stage%20your%20files%0Agit%20commit%20-m%20'Initial%20commit'%20%23%20commit%20your%20files%20with%20a%20commit%20message%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token function">git</span> <span class="token function">add</span> <span class="token builtin class-name">.</span> <span class="token comment"># stage your files</span><br><span class="token function">git</span> commit <span class="token parameter variable">-m</span> <span class="token string">'Initial commit'</span> <span class="token comment"># commit your files with a commit message</span></code></pre>
    </div>
  </div>

  <p class="post__p">The next steps will take you through adding your repository to GitHub via the GitHub CLI — but you can push your project to GitHub however you’re most comfortable.</p><p class="post__p">Run the following command in your terminal to create a new repository that’s connected to your GitHub account:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VkuyMTBaif"
      aria-describedby="VkuyMTBaif">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VkuyMTBaif">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="VkuyMTBaif" itemprop="text" content="gh%20repo%20create">
      <pre class="language-bash"><code class="language-bash">gh repo create</code></pre>
    </div>
  </div>

  <p class="post__p">When prompted on the kind of repository you&#39;d like to create, select: <code>Push an existing local repository to GitHub</code>. Follow the remaining prompts to fill out the relevant project details. Now, you’re ready to deploy to Netlify!</p><h3 class="post__h3">Deploy to Netlify using the Netlify CLI</h3><p class="post__p">If you&#39;re not already logged in to the Netlify CLI, run the following command in your terminal, and follow the prompts to authorize with Netlify (a browser window will open).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YAFeKPiYxA"
      aria-describedby="YAFeKPiYxA">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YAFeKPiYxA">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="YAFeKPiYxA" itemprop="text" content="netlify%20login">
      <pre class="language-bash"><code class="language-bash">netlify login</code></pre>
    </div>
  </div>

  <p class="post__p">Next, run the following command to initialize a new project on Netlify.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="cGNSHCtpzy"
      aria-describedby="cGNSHCtpzy">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="cGNSHCtpzy">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="cGNSHCtpzy" itemprop="text" content="netlify%20init">
      <pre class="language-bash"><code class="language-bash">netlify init</code></pre>
    </div>
  </div>

  <p class="post__p">Fill out the following prompts to set up the new project:</p><ul><li><p class="post__p"><b class="post__p--bold">What would you like to do? </b><code>Create &amp; configure a new site</code></p></li><li><p class="post__p"><b class="post__p--bold">Team: </b><code>YOUR_TEAM</code></p></li><li><p class="post__p"><b class="post__p--bold">Site name (optional): </b><code>CHOOSE_UNIQUE_SITE_NAME</code></p></li></ul><p class="post__p">The following prompts will be pre-filled given that we’ve provided a netlify.toml file in the repository. Hit enter to use the defaults.</p><ul><li><p class="post__p"><b class="post__p--bold">Your build command (hugo build/yarn run build/etc): </b><code>(we&#39;re not using a framework or a build script, so this stays blank!)</code></p></li><li><p class="post__p"><b class="post__p--bold">Directory to deploy (blank for current dir): </b><code>(public)</code></p></li><li><p class="post__p"><b class="post__p--bold">Netlify functions folder: </b><code>(netlify/functions)</code></p></li></ul><p class="post__p">Wait a few seconds… and your new site is deployed! 🎉</p><p class="post__p">Run the following commands in your terminal to open the site in your browser, and open the Netlify admin panel.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="prAULQZTAP"
      aria-describedby="prAULQZTAP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="prAULQZTAP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="prAULQZTAP" itemprop="text" content="netlify%20open%3Asite%20%23%20open%20the%20site%20in%20your%20browser%0Anetlify%20open%3Aadmin%20%23%20open%20the%20Netlify%20admin%20panel%20in%20your%20browser">
      <pre class="language-bash"><code class="language-bash">netlify open:site <span class="token comment"># open the site in your browser</span><br>netlify open:admin <span class="token comment"># open the Netlify admin panel in your browser</span></code></pre>
    </div>
  </div>

  <p class="post__p">And you’re done! Now you can test out your <b class="post__p--italic"><b class="post__p--bold">no-JavaScript-personalization-at-the-edge</b></b> by sending the site URL to your friends, and showing off your new, shiny skills!</p><h2 class="post__h2">Final thoughts</h2><p class="post__p">When we talk about the new-found power that Edge Functions bring us as developers and users of the web, the question that often comes to mind is, “<a href="https://youtu.be/qWlOfC-HwiQ?t=1024" target="_blank">Should I put everything on The Edge, now?</a>” Whilst the answer is usually, “It depends,” it’s always worth framing these new tools and technologies as <b class="post__p--italic">additions to the evolving web ecosystem</b> rather than <b class="post__p--italic">replacements of existing capabilities.</b></p><p class="post__p">Whilst The Edge certainly gives us a different set of advantages over serverless functions — most notably being able to run code from the closest server location possible to any user around the world — there are times when you might need control over where your code runs in the cloud, perhaps at the closest location to your database services, for example. In any case, Edge Functions are a brilliant use case for reducing client-side JavaScript in the browser, whilst still being able to provide a personalized experience for users of the web.</p><p class="post__p">And I, for one, am excited about what else is to come.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to view Google Lighthouse scores for your site in Netlify</title>
          <description>Install the integration. Check your scores. Profit.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/view-google-lighthouse-scores-visualizations/</link>
          <guid>https://www.netlify.com/blog/view-google-lighthouse-scores-visualizations/</guid>
          <pubDate>Sun, 24 Jul 2022 23:00:00 GMT</pubDate>
          <category>Tutorials</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Google Lighthouse is an essential tool in your workflow as a developer. Ensuring that your site’s performance, accessibility, best practises and SEO scores are up to scratch is crucial in building great UX, retaining customers, and ranking well in search engines. But with an ever-expanding toolbox at our disposal in a fast-paced work environment, it can be tricky to keep on top of it all!</p><p class="post__p">How many times have you forgotten to review your Google Lighthouse scores before you’ve pushed out a new feature? If the answer is, “At least once!” then you’re in good hands with Netlify. With the Lighthouse Scores Visualizations plugin, you can view your Google Lighthouse scores — per deploy — right there in the Netlify UI. Let’s turn it on.</p><p class="post__p">And if you&#39;re new to Lighthouse Scores, or want to learn more about how they&#39;re calculated, check out the <a href="https://www.netlify.com/blog/2021/06/11/the-developers-intro-to-core-web-vitals/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=intro-core-web-vitals" target="_blank">Developer&#39;s Intro to Core Web Vitals</a>.</p><h2 class="post__h2">Install the Lighthouse Plugin to your site</h2><p class="post__p">Before we can enable the visualizations, we need to install the Lighthouse plugin. Skip this step if you’re already using it! Otherwise, navigate to <b class="post__p--bold">Plugins</b> and search for <b class="post__p--bold">Lighthouse</b>. Click <b class="post__p--bold">Install</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2e9O7je5eWt6Sj8JUM5BCd/a7fbdbb3ad60277b5e072de247816025/step_1.png" alt="The plugins page of Netlify. Lighthouse is in the search box. The lighthouse plugin is shown in the search results with an install button on the right." height="624" width="1030" /><p class="post__p">Select the site to which you’d like to install the plugin, and confirm.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1X5G83c1zztHFXQczNAMww/b29e4a96c07b37dd32cccf6c60ca565c/step_2.png" alt="Install Lighthouse Plugin page. I have searched for whitep4nth3r.com in the search box, and the site is showing in the search results, with an onward action arrow to the right of the result." height="778" width="1030" /><p class="post__p">You’ll now see Google Lighthouse scores in the logs for each deploy, even if you don’t activate the visualizations. But that’s what we’re here for, right? 😎</p><h2 class="post__h2">Activate the Lighthouse Scores Visualizations plugin in Netlify Labs</h2><p class="post__p">Click on your user avatar to open the settings menu, and click on <b class="post__p--bold">Netlify Labs</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/x2aplYa73SMgL7pVu8uay/6b7d53388a08fbfdfc57e1a568cfb457/step_3.png" alt="The user settings menu dropdown showing my name, user settings, netlify labs, and sign out." height="287" width="287" /><p class="post__p">Scroll down to the Experimental features area, find <b class="post__p--bold">Lighthouse Score Visualizations</b>, and click <b class="post__p--bold">Enable</b>. Heads up! Each member of your team will need to activate this <b class="post__p--italic">individually</b> in Labs.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2aQ2b7OWEOUa8jDNauMuUG/c46232f3e5eb0b781b8eb1f793a28eec/step_4.png" alt="The experimental features list. All options are blurred out except for Lighthouse Score Visualizations which is the fourth item in the list." height="682" width="912" /><p class="post__p">Next, kick off a deploy. When your build is complete, you’ll see a new <code>onPostBuild</code> event from <code>@netlify/plugin-lighthouse</code> in the logs.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1TRfWzBUZsW6jP5n1orVvE/3e7da0a5198dbfb25275995bde83d589/step_5.png" alt="The deploy logs, showing the plugin-lighthouse task starting." height="424" width="998" /><p class="post__p">When your deploy is complete, scroll up, and you&#39;ll see your Google Lighthouse scores summary displayed beautifully at the top of the page! Never miss an unexpected score change again — even if you forget to check before you push. Amazing.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4sPuA6FBGnQFLZWTLJJVen/11a587d0a907b8bc0288a356d1fd796a/step_6.png" alt="Published deploy details with the usual links to open published deploy and lock publishing to this deploy. Below is a lighthouse summary section, which shows the following scores. 100 performance, 100 accessibility, 100 best practices, 100 SEO, 80 progressive web app." height="659" width="1200" /><h2 class="post__h2">Further reading</h2><p class="post__p"><a href="https://docs.netlify.com/netlify-labs/experimental-features/lighthouse-visualization/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-lighthouse-viz" target="_blank">Check out our documentation</a> for further reading and information about extra features available to paid Netlify accounts. We have a lot planned for this feature and will be adding functionality regularly — but we’d also love to hear your thoughts! Please <a href="https://netlify.qualtrics.com/jfe/form/SV_1NTbTSpvEi0UzWe" target="_blank">share your feedback</a> about this experimental feature and tell us what you think. Happy Lighthousing! 👩🏽‍💻</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Should I write a new JavaScript framework?</title>
          <description>Do you often ask yourself, should I write a new JavaScript framework? Here's some things you should consider, and some tips to get you started.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/should-i-write-a-new-javascript-framework/</link>
          <guid>https://whitep4nth3r.com/blog/should-i-write-a-new-javascript-framework/</guid>
          <pubDate>Wed, 22 Jun 2022 23:00:00 GMT</pubDate>
          <category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <div class="post__callout">
  <h3 class="post__callout__title">I changed my mind</h3>
    <div class="post__callout__content">
      <p>What a silly funny post ha ha 💀. I'm an idiot. So I wrote a talk about how we actually need more JavaScript frameworks. <a href="https://whitep4nth3r.com/talks/we-need-more-javascript-frameworks/">Check it out</a>.</p>

    </div>
  </div><p class="post__p">Each week, around 300 blazing-fast, game-changing, full-stack JavaScript frameworks are released into the developer ecosystem. They promise the discerning developer radical new approaches, life-affirming developer experiences, zero-config, maximum-config, all-bells, no-whistles, and the dopamine high of falling in love — over and over and <b class="post__p--italic">over</b> again.</p><h2 class="post__h2">Do you want to be a hero?</h2><p class="post__p">Open source maintainers of the <b class="post__p--bold">billions</b> of JavaScript frameworks now available for public consumption are worshipped as heroes of a new era, as they lead us boldly into a new landscape of JavaScript — and beyond. </p><p class="post__p">And you&#39;re probably asking yourself — &quot;Should <b class="post__p--italic">I</b> write a new JavaScript framework?&quot;</p><h2 class="post__h2">What&#39;s the answer?</h2><p class="post__p">The answer to this question is far beyond the scope of this post. Instead, I built a website. This website takes you on an inspiring journey through the essential concepts to consider when building your new JavaScript framework, and gives you the confidence to finally <code>mkdir bestframework.js &amp;&amp; cd bestframework.js &amp;&amp; git init</code>.</p><p class="post__p"><a href="https://should-i-write-a-new-javascript-framework.netlify.app/" target="_blank">Start your journey here</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to deploy an Astro site</title>
          <description>Deploy an Astro project as a static, server-rendered, or edge-rendered site, try out some Netlify templates, and learn how to deploy to Netlify.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/how-to-deploy-astro/</link>
          <guid>https://www.netlify.com/blog/how-to-deploy-astro/</guid>
          <pubDate>Sun, 12 Jun 2022 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Astro is a new kind of static site generator for the <b class="post__p--italic">modern web</b>, and is centered around helping you ship less client-side JavaScript in your projects. When you combine Astro with Netlify — <b class="post__p--italic">the platform for modern web development</b> — deploying your new Astro site is a seamless and enjoyable experience! In this tutorial, we&#39;ll take a look at the different ways you can deploy a new Astro site on Netlify, and the choices you need to make along the way. But first, a little overview of Astro.</p><h2 class="post__h2">An overview of Astro</h2><p class="post__p"><a href="https://astro.build/" target="_blank">Astro</a> is currently in beta (version 1.0.0 at the time of writing), and was launched to the web development community in early 2021. In an Astro project, you write JavaScript, HTML and CSS in <code>.astro</code> files, and the extra special thing about Astro is that you can also build projects using your favorite JavaScript front end frameworks. Configure your projects to use React, Preact, Svelte, Vue, SolidJS, AlpineJS and Lit by <a href="https://docs.astro.build/en/core-concepts/framework-components/" target="_blank">installing integrations via the Astro config file</a>, and you’re good to go.</p><h2 class="post__h2">Starting a new Astro project</h2><p class="post__p">There are a number of ways you can spin up a new Astro project depending on your preferences. Let’s take a look at:</p><ul><li><p class="post__p"><a href="#using-the-astro-cli" >how to start an Astro project using the Astro CLI</a></p></li><li><p class="post__p"><a href="#using-in-browser-environments" >how to get started with pre-configured in-browser environments</a></p></li><li><p class="post__p"><a href="#deploy-an-astro-template-to-netlify" >how to deploy Netlify templates</a></p></li></ul><h3 class="post__h3">Using the Astro CLI</h3><p class="post__p">Open up your terminal, and run the following command depending on your preferred package manager.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KYpEJOctRH"
      aria-describedby="KYpEJOctRH">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KYpEJOctRH">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="KYpEJOctRH" itemprop="text" content="%23%20create%20a%20new%20project%20with%20npm%0Anpm%20create%20astro%40latest%0A%0A%23%20or%20yarn%0Ayarn%20create%20astro%0A%0A%23%20or%20pnpm%0Apnpm%20create%20astro%40latest">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># create a new project with npm</span><br><span class="token function">npm</span> create astro@latest<br><br><span class="token comment"># or yarn</span><br><span class="token function">yarn</span> create astro<br><br><span class="token comment"># or pnpm</span><br><span class="token function">pnpm</span> create astro@latest</code></pre>
    </div>
  </div>

  <p class="post__p">The Astro CLI will guide you through setting up your new project, including prompting you to select one of the pre-configured templates.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4isbm9N7KBF4uEHaYKeTIm/95386d1cfbf224fd05ad4dd54b065ee1/astro_cli_step1.png" alt="A terminal showing that npm create astro@latest has been run. The cursor has selected the template named Just the basics" height="1222" width="3060" /><p class="post__p">Next, the CLI will ask if you want to install dependencies (select yes if you want to run a development server on your machine), and whether you’d like to add support for component frameworks. Don’t worry if you’re not ready to add component frameworks just yet — you can do it later. Here’s how a finished installation looks in the terminal with the Astro CLI. At this stage, you’re already ready to deploy!</p><img src="https://images.ctfassets.net/56dzm01z6lln/6smk5lMP1EIj8TthxsJPuu/3f56cc9171a2cf5d3c9ccf2612a695a3/astro_cli_step2.png" alt="A terminal window showing a freshly created Astro project using the Astro CLI. The latest instructions are to cd into the new project directory and run npm run dev." height="2002" width="3060" /><h3 class="post__h3">Using in-browser environments</h3><p class="post__p">Astro offers you lots of ways to get started with a template project using browser-based development environments such as StackBlitz, CodeSandbox and GitPod. Head on over to <a href="https://astro.new/" target="_blank">astro.new</a>, and get started with an empty project or a template that uses a component framework. Clicking on the buttons for your preferred environment will open up the code repository in the same window.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4n0m1WLiWkVAtI7htowuy5/9a515725de3827b830dd917e3fc9473d/astro_new.png" alt="The astro.new page showing a selection of new projects to open in either StackBlitz, CodeSandbox or GitPod" height="2382" width="3680" /><p class="post__p">You can connect your browser environment to GitHub to fork the repository to your GitHub account when you’re ready to deploy your Astro site. Here’s the “Just the basics” template in CodeSandbox.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7FIbGDnCZRgsklDhggnRoG/a9a622b7b134348c4737bb8a8a4ee085/astro_codesandbox.png" alt="The Astro Just the basics project opened in CodeSandbox" height="2382" width="3680" /><h3 class="post__h3">Deploy an Astro Template to Netlify</h3><p class="post__p">If you’d like to skip the steps above and try out Astro on Netlify in <b class="post__p--bold">just two minutes ✨</b>, we built an Astro quick start template you can deploy to Netlify. <a href="https://www.netlify.com/blog/deploy-your-astro-project-fast/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=deploy-astro-fast" target="_blank">Check out this blog post to learn more</a>, or click the Deploy to Netlify button below to try it out.</p>
    <a href="https://app.netlify.com/start/deploy?repository=https://github.com/netlify-templates/astro-quickstart" class="post__deployToNetlifyButton" title="Deploy Netlify Astro Quickstart" target="_blank">
      <img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify Button">
    </a>
  <p class="post__p">The great thing about the template deploy method is that Netlify automatically creates a new repository in your GitHub account for you, kicks off a new build, runs tests, and assigns you a live URL — all in around two minutes.</p><p class="post__p">Now you&#39;ve created a new Astro site either with the Astro CLI or an in-browser environment, it&#39;s time to deploy! But first, there are some choices you need to make about how you’d like to deploy your project according to the <b class="post__p--italic">type of project</b> you’re building.</p><h2 class="post__h2">Deploy Astro in 3 ways</h2><p class="post__p">Astro and Netlify offer you <b class="post__p--bold">three ways</b> to deploy your site to Netlify depending on the <b class="post__p--italic">type of functionality</b> you require. You can deploy Astro as a static site, a server-rendered site, or an edge-rendered site.</p><h3 class="post__h3">Astro as a static site</h3><p class="post__p">Astro sites are static sites out of the box. This means that all pages are pre-rendered as static HTML at build-time, cached, and served from the Netlify Content Delivery Network (CDN) on each request. Static sites are suited to content-heavy sites that don’t change often, such as marketing sites, landing pages and documentation. To update changes to your static site, you need to re-build it on Netlify either by pushing to Git, by setting up a <a href="https://docs.netlify.com/configure-builds/build-hooks/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-build-hooks" target="_blank">build hook</a>, or by triggering a build manually in your Netlify dashboard. You don’t need any extra configuration to deploy a static Astro site to Netlify. It just works™.</p><h3 class="post__h3">Astro as a server-rendered site</h3><p class="post__p">If your Astro project relies on dynamic data, such as authentication for login state and e-commerce baskets, or if you need to fetch data from an API at request time, you can configure your project to run as a server-side rendered (SSR) site. This means that every time someone makes a request to your site, the page is built at runtime on the server (powered by Netlify serverless functions), before it&#39;s sent back to the browser. You can configure caching — but by default, an Astro SSR site is uncached.</p><p class="post__p">At the time of writing this article, SSR in Astro is currently an all-or-nothing opt-in. When you enable SSR in Astro, <b class="post__p--bold">every route</b> in your <code>pages</code> directory becomes a server-rendered route — which on Netlify, uses Netlify Functions under the hood.</p><p class="post__p">To enable SSR in Astro on Netlify, install the Netlify adapter to your project’s dependencies.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="tlIBtpkMXz"
      aria-describedby="tlIBtpkMXz">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="tlIBtpkMXz">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="tlIBtpkMXz" itemprop="text" content="npm%20install%20%40astrojs%2Fnetlify">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> @astrojs/netlify</code></pre>
    </div>
  </div>

  <p class="post__p">Next, add the adapter to your <code>astro.config.mjs</code> file by importing the dependency, adding the Netlify adapter function to the default export, and specifying <code>output: server</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KrEsuNtmFi"
      aria-describedby="KrEsuNtmFi">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KrEsuNtmFi">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="KrEsuNtmFi" itemprop="text" content="%2F%2F%20astro.config.mjs%0Aimport%20%7B%20defineConfig%20%7D%20from%20'astro%2Fconfig'%3B%0Aimport%20netlify%20from%20'%40astrojs%2Fnetlify%2Ffunctions'%3B%0A%0Aexport%20default%20defineConfig(%7B%0A%20%20output%3A%20%22server%22%2C%0A%20%20adapter%3A%20netlify()%2C%0A%7D)%3B%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// astro.config.mjs</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> defineConfig <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'astro/config'</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> netlify <span class="token keyword">from</span> <span class="token string">'@astrojs/netlify/functions'</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">defineConfig</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">output</span><span class="token operator">:</span> <span class="token string">"server"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">adapter</span><span class="token operator">:</span> <span class="token function">netlify</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://docs.astro.build/en/guides/server-side-rendering/" target="_blank">Read more about SSR and Astro on the official Astro documentation</a>.</p><h3 class="post__h3">Astro as an edge-rendered site</h3><blockquote class="post__blockquote"><p class="post__p"><b class="post__p--italic">At the time of writing this article, Netlify Edge Functions and Astro on The Edge are experimental features in beta. Functionality and implementation described below may be subject to change!</b></p></blockquote><p class="post__p">We partnered with the Astro team during the launch of <a href="https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-ef" target="_blank">Netlify Edge Functions</a> in April 2022 to give you the option of server-side rendering your whole Astro project at <b class="post__p--bold">The Edge</b>. This means that on every request, Netlify will build the page on the Edge server <b class="post__p--bold">at the closest location to the user</b> before it’s sent back to the browser, meaning your Astro site on Netlify is fast for everyone around the world. This is particularly useful for e-commerce sites, where users need a seamless and fast experience to find what they need quickly and convert — so you can make those sales!</p><p class="post__p">To enable the Netlify Edge adapter in your Astro site, change the <code>netlify/functions</code> import line in the Astro config file to use <code>netlify/edge-functions</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="oOkFifpaRl"
      aria-describedby="oOkFifpaRl">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="oOkFifpaRl">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="oOkFifpaRl" itemprop="text" content="%20%20%2F%2F%20astro.config.mjs%0A%20%20import%20%7B%20defineConfig%20%7D%20from%20'astro%2Fconfig'%3B%0A-%20import%20netlify%20from%20'%40astrojs%2Fnetlify%2Ffunctions'%3B%0A%2B%20import%20netlify%20from%20'%40astrojs%2Fnetlify%2Fedge-functions'%3B%0A%0A%20%20export%20default%20defineConfig(%7B%0A%20%20%20%20output%3A%20%22server%22%2C%0A%20%20%20%20adapter%3A%20netlify()%2C%0A%20%20%7D)%3B">
      <pre class="language-diff-javascript"><code class="language-diff-javascript">// astro.config.mjs<br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">import</span> <span class="token punctuation">{</span> defineConfig <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'astro/config'</span><span class="token punctuation">;</span><br></span><span class="token deleted-sign deleted language-javascript"><span class="token prefix deleted">-</span> <span class="token keyword">import</span> netlify <span class="token keyword">from</span> <span class="token string">'@astrojs/netlify/functions'</span><span class="token punctuation">;</span><br></span><span class="token inserted-sign inserted language-javascript"><span class="token prefix inserted">+</span> <span class="token keyword">import</span> netlify <span class="token keyword">from</span> <span class="token string">'@astrojs/netlify/edge-functions'</span><span class="token punctuation">;</span><br></span><br><span class="token unchanged language-javascript"><span class="token prefix unchanged"> </span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">defineConfig</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br><span class="token prefix unchanged"> </span>   <span class="token literal-property property">output</span><span class="token operator">:</span> <span class="token string">"server"</span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span>   <span class="token literal-property property">adapter</span><span class="token operator">:</span> <span class="token function">netlify</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br><span class="token prefix unchanged"> </span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Head over to GitHub check out a <a href="https://github.com/sarahetter/astro-netlify-edge-starter" target="_blank">complete code example for an Astro project deployed to The Edge on Netlify</a>. Alternatively, deploy the project to Netlify right now using the Deploy to Netlify button below!</p>
    <a href="https://app.netlify.com/start/deploy?repository=https://github.com/sarahetter/astro-netlify-edge-starter" class="post__deployToNetlifyButton" title="Deploy Astro Netlify Edge Starter" target="_blank">
      <img src="https://www.netlify.com/img/deploy/button.svg" alt="Deploy to Netlify Button">
    </a>
  <p class="post__p">Now we&#39;ve covered the three ways you can deploy a new Astro project to Netlify — static site, server-rendered site, or edge-rendered site — you’re ready to start deploying. Let&#39;s take a look at the options.</p><h2 class="post__h2">Deploying an Astro site</h2><p class="post__p">If you deployed the <a href="https://github.com/netlify-templates/astro-quickstart" target="_blank">Netlify Astro Quickstart template</a> or the <a href="https://github.com/sarahetter/astro-netlify-edge-starter" target="_blank">Astro Netlify Edge Starter</a> using the Deploy to Netlify buttons, you don’t need to do anything else! Your Astro site is cloned to your GitHub account and already deployed to a live URL on Netlify. You may want to update your site name before you share it with your friends. To do this, navigate to the site in Netlify, and click on Site settings.</p><img src="https://images.ctfassets.net/56dzm01z6lln/59FYpyENDG3ZLAF1bVu4qR/4656d0fafbd0e46daa0fc9fd0cfc2900/go_to_site_settings_netify.png" alt="Netlify dashboard header showing the Site settings button is highlighted" height="574" width="1742" /><p class="post__p">Click on Change site name, edit the site name, and click Save.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5kAIGoLo1q4tK7pSokz41T/c12d818f76056cd3c3568124613fad8c/change_site_name_netlify.png" alt="Netlify site name settings showing my-new-site-name added to the input field" height="958" width="1622" /><h3 class="post__h3">Deploy an existing Git repository to Netlify</h3><p class="post__p">Once you’ve got your new Astro project pushed up to GitHub, GitLab, Bitbucket or Azure DevOps via Git, head on over to your Netlify dashboard and click Add new site, and select Import an existing project.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1Vk2DL7lo7Oauw98V9uOgy/e9dd5c5372395e7b4d49a161acfa4375/add_site_import_from_list_netlify.png" alt="Dropdown button named Add new site, showing a menu with import an existing project highlighted" height="1210" width="1272" /><p class="post__p">Click on the button for your Git provider of choice, and follow the authentication instructions in your browser.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6N1WeHcqu7aFzmUeFlixDX/1aef350c2cb98e1c9d0d711f1adfbd12/import_from_git_netlify.png" alt="Import an existing project from a Git repository, showing buttons to connect GitHub, GitLab, Bitbucket and Azure DevOps" height="2040" width="2254" /><p class="post__p">At this stage, you may need to give Netlify access to the Git repository you wish to deploy. Follow the instructions for your particular Git provider. Find your project in the repository list, and click to configure the deployment.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1edIZ5ycBc3XfJTL11dMH0/725aa19007e0f0035c760ed72f0dcbaa/pick_site_from_list_astro_netlify.png" alt="Pick a repository from GitHub. The search field is populated with the word astro, and one matching repository is shown in the list below" height="2306" width="3436" /><p class="post__p">Netlify will automatically detect the build settings for your new Astro site, so unless you’ve done something fancy in your project, you will be good to go with the default settings. Click Deploy site.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2DiUxAqtyzL3EeEVCaDDy6/4d4be5a1c5eab2d2c32825692939b0bb/build_settings_prepopulated_netlify.png" alt="Site settings, build command and publish directory for the new site are already pre-populated" height="2382" width="3680" /><p class="post__p">Next, you’ll be redirected to the site overview page where Netlify has already kicked off a production build. Click into the deployment to watch the build progress.</p><img src="https://images.ctfassets.net/56dzm01z6lln/56vR6LiqcNhgGstwxZ5q8y/5fe5cb995266d266bdab33efad1c60c9/first_build_started_netlify.png" alt="A new site with the auto generated URL famous-kangaroo-93944b is currently building" height="2382" width="3680" /><p class="post__p">When the build is complete, click on Open published deploy to view your new Astro site on Netlify! You can now update your site name and URL <a href="#deploying-an-astro-site" >as described above</a>.</p><p class="post__p">In the example above I deployed the Astro Netlify Edge Starter repository to my Netlify account. This example project added the Netlify Edge adapter to the Astro config file, and <b class="post__p--bold">there were no extra steps to take when deploying an Astro edge-rendered site</b>! Deploying a server-rendered or edge-rendered Astro site is as straightforward as deploying a static Astro site! 🚀</p><h3 class="post__h3">Link your Astro project and deploy using the Netlify CLI</h3><p class="post__p">If you love working on the command line, you can also create a new site on Netlify and link up your Git repository using the <a href="https://cli.netlify.com/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=cli-docs" target="_blank">Netlify CLI</a>.</p><p class="post__p">Get started by installing the Netlify CLI globally to your machine using npm.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VNgNacnlmX"
      aria-describedby="VNgNacnlmX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VNgNacnlmX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="VNgNacnlmX" itemprop="text" content="npm%20install%20netlify-cli%20-g">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> netlify-cli <span class="token parameter variable">-g</span></code></pre>
    </div>
  </div>

  <p class="post__p">Log in to the Netlify CLI using <code>netlify login</code> in your terminal, and authorize via your browser.</p><img src="https://images.ctfassets.net/56dzm01z6lln/VkwzWp1b15j2NVVjEuOkw/5774253ccb32394645547cdc3c33d786/run_netlify_login_terminal.png" alt="Netlify login has logged me in to my Netlify account on the CLI" height="1300" width="2346" /><p class="post__p">Run <code>netlify init</code> to start the process of creating and deploying a new site on Netlify. Choose to Create &amp; configure a new site.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5waYwMIYMZu7tbiwBTsTd1/7188488507853bf42156187d5ad23028/netlify_cli_create_new_site.png" alt="Create and configure a new site is selected" height="832" width="2584" /><p class="post__p">Select your team and choose a name for your new site.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6d4yCuwnX5Sr13Bety5hNF/76653efe30b8d79fffcfc57e5e14b757/netlifi_cli_choose_site_name.png" alt="My new site name is my-astro-site" height="1066" width="2584" /><p class="post__p">The Netlify CLI automatically detects the build settings (<code>astro build</code>), deploy directory (<code>dist</code>), and Netlify Functions directory (should you need it) for your Astro site. Press enter to confirm.</p><img src="https://images.ctfassets.net/56dzm01z6lln/0jng6tbcaSIs8KQsUYmPS/d04808a49298f90253afc7aa60dc7172/netlify_cli_site_created.png" alt="Netlify has created the site, and is confirming the build command is astro build" height="1768" width="2584" /><p class="post__p">The CLI will then offer to automatically generate a <code>netlify.toml</code> file with those build and deploy settings. The .toml file is not necessary for this deployment, but you can press Y and enter to confirm if you want to create the file.</p><img src="https://images.ctfassets.net/56dzm01z6lln/0jng6tbcaSIs8KQsUYmPS/d04808a49298f90253afc7aa60dc7172/netlify_cli_site_created.png" alt="Netlify has created the site, and is confirming the build command is astro build" height="1768" width="2584" /><p class="post__p">Next, the CLI will add a deploy key to the repository, which means your site will be automatically rebuilt on Netlify every time you push a change to Git. And it’s done!</p><img src="https://images.ctfassets.net/56dzm01z6lln/6wnTs18XaRDK0MCl4Dng4n/417485e3445926a25a79f149be491d60/netlify_cli_site_created_done.png" alt="The deploy key has been added to the repository and CI/CD has been configured to automatically deploy from GitHub branches" height="1690" width="2822" /><p class="post__p">Open your Netlify dashboard, and you’ll see a build in progress. Give it a minute, and wait for <code>Site is live ✨</code> in the build logs. You successfully linked up your Astro site Git repository and deployed a build to Netlify using the Netlify CLI!</p><h2 class="post__h2">Further reading</h2><p class="post__p">Do you want to learn more about Astro on Netlify? Here’s a list of recommended reading.</p><ul><li><p class="post__p"><a href="https://docs.netlify.com/integrations/frameworks/astro/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-frameworks-astro" target="_blank">Astro and Netlify documentation</a></p></li><li><p class="post__p"><a href="https://astro.build/blog/netlify-edge-functions/" target="_blank">Astro on Netlify Edge Functions</a> on the Astro blog</p></li><li><p class="post__p"><a href="https://whitep4nth3r.com/blog/what-is-the-edge-serverless-functions/" target="_blank">We&#39;re all living on it. But what exactly is The Edge?</a></p></li></ul><p class="post__p">You can also catch up on a live stream where I was joined by Astro’s very own <a href="https://www.netlify.com/blog/authors/ben-holmes/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=view-ben-holmes" target="_blank">Ben Holmes</a>. In this stream we used the Astro CLI to create a new Astro “Just the basics” project, deployed it to Netlify, integrated React and Svelte into the project, and discussed server-side rendering and edge-rendering an Astro site.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/A3HDN_dPq7k"
        title="Getting started with astro.build"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p">And finally — have you deployed an Astro project on Netlify? Come show it off in the <a href="https://discord.gg/jamstack" target="_blank">Jamstack Discord</a>!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Build a business card CLI tool</title>
          <description>Learn how to use Node.js, npm and npx to build a CLI tool to output a business card to the terminal. Bonus demo repository included!</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/build-a-business-card-cli-tool/</link>
          <guid>https://whitep4nth3r.com/blog/build-a-business-card-cli-tool/</guid>
          <pubDate>Wed, 08 Jun 2022 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>NodeJS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">This week I built my own command line business card! Open up a terminal, and run the following command to see it in action.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="TGQocODDrF"
      aria-describedby="TGQocODDrF">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="TGQocODDrF">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="TGQocODDrF" itemprop="text" content="npx%20whitep4nth3r">
      <pre class="language-bash"><code class="language-bash">npx whitep4nth3r</code></pre>
    </div>
  </div>

  <p class="post__p">After running the command, you&#39;ll see something that looks like this (depending on your base terminal styles). Pretty cool, right? 😎</p><img src="https://images.ctfassets.net/56dzm01z6lln/3PcPQZebqmZHgIrt8pPo1T/2c7d5fa6aed955ddbac42888b83a8357/npx_whitep4nth3r_terminal.png" alt="A terminal showing the command npx whitep4nth3r which has output a box with a list of links inside, the titles of the links are coloured with the brand colours, eg youtube is white text on a red background, and there's a little intro paragraph as well." height="790" width="892" /><p class="post__p">In this post, I&#39;ll take you through how to build your own command line business card. By the end of this tutorial, you&#39;ll know how to:</p><ul><li><p class="post__p">Create a new npm package,</p></li><li><p class="post__p">configure a JavaScript file to run via the Node package runner (npx),</p></li><li><p class="post__p">publish the code to npm,</p></li><li><p class="post__p">and add optional styles to the terminal output.</p></li></ul><h2 class="post__h2">Prerequisites</h2><p class="post__p">Ensure you’ve installed <a href="https://nodejs.org/en/" target="_blank">Node.js and npm</a> on your machine.</p><h3 class="post__h3">Create an account on npm</h3><p class="post__p">You’ll need this to be able to publish your package. <a href="https://www.npmjs.com/signup" target="_blank">Sign up here</a>.</p><h3 class="post__h3">Login to npm via your terminal</h3><p class="post__p">Run <code>npm login</code> in your terminal and enter your username, password and email. This will ensure you can publish your package later via the CLI.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2LaGBjAFuLOE5Doud4ho1X/debefa2ffce3c7708c655e08a6e50290/npm_login.png" alt="A screenshot of the output in a terminal after running npm login" height="694" width="2352" /><h2 class="post__h2">Project set up</h2><p class="post__p">Using your terminal, create a new project directory. Name it whatever you like. And then <code>cd</code> into that directory.</p><p class="post__p"><b class="post__p--bold">Note: you won&#39;t be able to publish an npm package named &quot;fancy-business-card&quot; unless it&#39;s a </b><b class="post__p--italic"><b class="post__p--bold">scoped</b></b><b class="post__p--bold"> package — because I already published one of that name! You can read more about how to publish scoped packages in this blog post: </b><a href="https://whitep4nth3r.com/blog/how-to-build-test-and-release-node-module-es6/">How to build, test and release a node module in ES6</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CcadEYXLDp"
      aria-describedby="CcadEYXLDp">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CcadEYXLDp">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="CcadEYXLDp" itemprop="text" content="mkdir%20fancy-business-card%0Acd%20fancy-business-card">
      <pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> fancy-business-card<br><span class="token builtin class-name">cd</span> fancy-business-card</code></pre>
    </div>
  </div>

  <p class="post__p">Run the following command in your new project directory. This will create a <code>package.json</code> file to build the scaffolding for your CLI tool.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xAWDRKaSkg"
      aria-describedby="xAWDRKaSkg">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xAWDRKaSkg">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="xAWDRKaSkg" itemprop="text" content="npm%20init">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> init</code></pre>
    </div>
  </div>

  <p class="post__p">Follow the instructions in your terminal. When the setup is complete, you should have something that looks like this. (Note: I removed the auto generated &quot;no test specified&quot; message which appears in <code>&quot;scripts&quot;</code>. We won&#39;t be writing tests in this tutorial.)</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vXdGrrhBZD"
      aria-describedby="vXdGrrhBZD">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vXdGrrhBZD">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="vXdGrrhBZD" itemprop="text" content="%7B%0A%20%20%22name%22%3A%20%22fancy-business-card%22%2C%0A%20%20%22version%22%3A%20%221.0.0%22%2C%0A%20%20%22description%22%3A%20%22A%20fancy%20business%20card%20that%20outputs%20to%20the%20terminal%20using%20npx.%22%2C%0A%20%20%22main%22%3A%20%22index.js%22%2C%0A%20%20%22scripts%22%3A%20%7B%7D%2C%0A%20%20%22author%22%3A%20%22whitep4nth3r%22%2C%0A%20%20%22license%22%3A%20%22MIT%22%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"fancy-business-card"</span><span class="token punctuation">,</span><br>  <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"1.0.0"</span><span class="token punctuation">,</span><br>  <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"A fancy business card that outputs to the terminal using npx."</span><span class="token punctuation">,</span><br>  <span class="token property">"main"</span><span class="token operator">:</span> <span class="token string">"index.js"</span><span class="token punctuation">,</span><br>  <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token property">"author"</span><span class="token operator">:</span> <span class="token string">"whitep4nth3r"</span><span class="token punctuation">,</span><br>  <span class="token property">"license"</span><span class="token operator">:</span> <span class="token string">"MIT"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">If you&#39;d like to add version control to your CLI tool, run the following command to initialise a git repository.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mXewlbMCaZ"
      aria-describedby="mXewlbMCaZ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mXewlbMCaZ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="mXewlbMCaZ" itemprop="text" content="git%20init">
      <pre class="language-bash"><code class="language-bash"><span class="token function">git</span> init</code></pre>
    </div>
  </div>

  <h2 class="post__h2">Add the script file</h2><p class="post__p">Create a new file inside the project directory and call it <code>index.js</code>. This is where we will write the code to output the business card to the terminal.</p><img src="https://images.ctfassets.net/56dzm01z6lln/Vul1iSaJpt6kgmgTKyUMS/c6519b5eadb114bde999d36cd239b0fa/packagejson_indexjs.png" alt="Screenshot from VSCode of an index.js file next to a package.json file in the file explorer." height="230" width="840" /><p class="post__p">Add the following code to <code>index.js</code>. You can output whatever you like in the <code>console.log</code> at this stage, but it&#39;s really just to test everything is working correctly. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LgogNhaPtc"
      aria-describedby="LgogNhaPtc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LgogNhaPtc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="LgogNhaPtc" itemprop="text" content="%2F%2Findex.js%0A%0Aconsole.log(%22My%20fancy%20business%20card!%22)">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">//index.js</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"My fancy business card!"</span><span class="token punctuation">)</span></code></pre>
    </div>
  </div>

  <p class="post__p">Head over to your terminal. Inside the project directory, run the following command.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LRCmUqFuMh"
      aria-describedby="LRCmUqFuMh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LRCmUqFuMh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="LRCmUqFuMh" itemprop="text" content="node%20index.js">
      <pre class="language-bash"><code class="language-bash"><span class="token function">node</span> index.js</code></pre>
    </div>
  </div>

  <p class="post__p">You should see the text inside your <code>console.log</code> output to the terminal. Now we&#39;re ready to configure the script to run with <code>npx</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/49iP0cdalZbxmpXs9sKLon/ea1dfd34765e763b5db18f9759f7d4df/run_node_indexjs_command_line.png" alt="Terminal showing running the command node index.js which has output the text my fancy business card! " height="310" width="878" /><h2 class="post__h2">Set up the npx CLI tool</h2><p class="post__p"><code>npm</code> stands for &quot;Node package manager&quot;, which allows you to use open source JavaScript and TypeScript packages in your projects. When you run <code>npm install {package-name}</code> in your project, npm fetches the code for that package and adds it to a node_modules directory in your project on your machine.</p><p class="post__p"><code>npx</code> is the <a href="https://nodejs.dev/learn/the-npx-nodejs-package-runner" target="_blank">Node.js package runner</a>. This lets you <b class="post__p--bold"><b class="post__p--italic">run</b></b> code built with Node.js and published through the npm registry — without needing to download the code to your machine.</p><p class="post__p">Add the following code to the top of <code>index.js</code>. This is used to tell Node.js that this is a CLI tool.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="NVFcQePBRn"
      aria-describedby="NVFcQePBRn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="NVFcQePBRn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="NVFcQePBRn" itemprop="text" content="%2F%2Findex.js%0A%0A%23!%2Fusr%2Fbin%2Fenv%20node">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">//index.js</span><br><br>#<span class="token operator">!</span><span class="token operator">/</span>usr<span class="token operator">/</span>bin<span class="token operator">/</span>env node</code></pre>
    </div>
  </div>

  <p class="post__p">Add the following code to your <code>package.json</code> file. This tells Node.js what the executable command is, and what file to run.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="GwJZkaMZPB"
      aria-describedby="GwJZkaMZPB">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="GwJZkaMZPB">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="GwJZkaMZPB" itemprop="text" content="%22bin%22%3A%20%7B%0A%20%20%22fancy-business-card%22%3A%20%22.%2Findex.js%22%0A%7D%2C">
      <pre class="language-json"><code class="language-json"><span class="token property">"bin"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  <span class="token property">"fancy-business-card"</span><span class="token operator">:</span> <span class="token string">"./index.js"</span><br><span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre>
    </div>
  </div>

  <p class="post__p">The code above tells Node that our command is <code>fancy-business-card</code>. Running <code>npx fancy-business-card</code> will run the <code>index.js</code> file code, and output the <code>console.log</code> we wrote above to the terminal. Switch out &quot;fancy-business-card&quot; for your own command — such as your name or Bluesky handle, or use the name of your project directory.</p><p class="post__p">Now, let&#39;s test that <code>npx</code> is wired up correctly.</p><h2 class="post__h2">Test the CLI tool locally</h2><p class="post__p">We can use <code>npm link</code> to test out the functionality of an npm package before publishing it to the npm registry.</p><p class="post__p">In your project directory, run the following command:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="RjQZWjcTwl"
      aria-describedby="RjQZWjcTwl">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="RjQZWjcTwl">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="RjQZWjcTwl" itemprop="text" content="npm%20link%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">link</span></code></pre>
    </div>
  </div>

  <p class="post__p">Open up a separate terminal window, and run your <code>npx</code> command. Make sure to switch out &quot;fancy-business-card&quot; for whatever you specified in the &quot;bin&quot; section of your <code>package.json</code> file.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ohjDvQgYlS"
      aria-describedby="ohjDvQgYlS">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ohjDvQgYlS">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="ohjDvQgYlS" itemprop="text" content="npx%20fancy-business-card">
      <pre class="language-bash"><code class="language-bash">npx fancy-business-card</code></pre>
    </div>
  </div>

  <p class="post__p">And look! Node package runner has executed the code in the <code>index.js</code> file, and output the <code>console.log</code> to the terminal.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2QXtNLhwN0G3lHilN0wewj/20c16a9662813bdc99bf428e63b62735/npm_link_run_cli.png" alt="Two terminal windows showing npm link has run in the left window, and npx fancy-business-card has run in the right window, outputting the console log from index.js." height="460" width="1522" /><p class="post__p">At this point, feel free to add more information and links to the <code>console.log</code> of your <code>index.js</code> file. Next up, it&#39;s time to publish the package to npm.</p><h2 class="post__h2">Publish to npm</h2><p class="post__p">Let&#39;s publish the fancy business card CLI tool to npm. At this stage, you might like to commit and push the files to git using your preferred method. I like using the <a href="https://cli.github.com/" target="_blank">GitHub CLI</a>.</p><p class="post__p">Make sure you are logged into npm via the CLI <a href="#login-to-npm-via-your-terminal" >as described above</a>. At the root of your project directory, run the following command in your terminal and follow the instructions. If you have 2FA enabled for npm, you&#39;ll be prompted for a one time passcode (OTP) from your authenticator app.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="HrxqFvyBnr"
      aria-describedby="HrxqFvyBnr">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="HrxqFvyBnr">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="HrxqFvyBnr" itemprop="text" content="npm%20publish">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> publish</code></pre>
    </div>
  </div>

  <p class="post__p">Once your package is published to npm, you can run <code>npx {your-command}</code> to execute your script wherever and whenever you like!</p><p class="post__p"><a href="https://www.npmjs.com/package/fancy-business-card" target="_blank">View the demo repo on npm</a> or <a href="https://github.com/whitep4nth3r/fancy-business-card" target="_blank">fork the demo repo on GitHub</a> to view the code in full.</p><h2 class="post__h2">Optional: style your business card</h2><p class="post__p">There are many tools available to help with styling your command line output. For my business card, I used a combination of <a href="https://www.npmjs.com/package/boxen" target="_blank">boxen</a> to draw the box around the content, and <a href="https://www.npmjs.com/package/chalk" target="_blank">chalk</a> to power the font styles and colours. I&#39;ll leave this part up to you, but you can <a href="https://github.com/whitep4nth3r/whitep4nth3r/blob/main/scripts/business.mjs" target="_blank">view the code on GitHub</a> to see how I did it. Be aware that if you want to use <a href="https://nodejs.org/api/esm.html" target="_blank">ES6 imports in Node</a>, you&#39;ll need to update your <code>index.js</code> file extension to <code>.mjs</code>, and edit the <code>package.json &quot;bin&quot;</code> section accordingly.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3PcPQZebqmZHgIrt8pPo1T/2c7d5fa6aed955ddbac42888b83a8357/npx_whitep4nth3r_terminal.png" alt="A terminal showing the command npx whitep4nth3r which has output a box with a list of links inside, the titles of the links are coloured with the brand colours, eg youtube is white text on a red background, and there's a little intro paragraph as well." height="790" width="892" /><h2 class="post__h2">Publishing new changes to npm</h2><p class="post__p">After you&#39;ve styled your business card, publish your new changes to npm using <code>npm publish</code> in your terminal. <b class="post__p--bold">Remember to bump the version number in package.json each time you want to publish new changes!</b></p><p class="post__p">And you&#39;re done! You&#39;ve just published a node module that can be executed on the fly using <code>npx</code>.</p><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Level up your link previews in Slack</title>
          <description>Add extra metadata to the head tag in your web pages to show richer previews in Slack when your link is unfurled.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/level-up-your-link-previews-in-slack/</link>
          <guid>https://whitep4nth3r.com/blog/level-up-your-link-previews-in-slack/</guid>
          <pubDate>Sun, 29 May 2022 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Web Dev</category><category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I recently learned how to improve web page link previews unfurled in Slack by including extra metadata using Open Graph. In this post you&#39;ll learn how to display additional data such as a page author and reading time to your Slack link previews.</p><img src="https://images.ctfassets.net/56dzm01z6lln/zAYDZ6D9QvHk1U4NyZWEA/c29036b49478ae144249ea47e77c5221/slack_preview.png" alt="Screenshot of a link shared in Slack showing the title, description, author, reading time and image preview." height="224" width="657" /><p class="post__p">But first, a little background on the Open Graph protocol. Alternatively, <a href="#open-graph-previews-in-slack" >skip straight to the code example</a>. </p><h2 class="post__h2">The Open Graph Protocol</h2><p class="post__p"><a href="https://opengraphprotocol.org/" target="_blank">The Open Graph (OG) protocol</a> was created at Facebook in 2010 to transform web links into visually rich content previews, with similar functionality and appearance to other content posted on Facebook. </p><p class="post__p">Open Graph meta tags are used in the <code>&lt;head&gt;</code> of an HTML page to expose information about web pages to social media platforms and other applications that unfurl URL metadata — such as Slack. OG meta tags are identified by an attribute prefixed with <code>og</code>. Inspect the code of this page in your browser to identify the OG meta tags in the HTML <code>&lt;head&gt;</code> and investigate! </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="PmChylabKR"
      aria-describedby="PmChylabKR">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="PmChylabKR">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="PmChylabKR" itemprop="text" content="%3Cmeta%20property%3D%22og%3Aimage%22%20content%3D%22https%3A%2F%2Fexample.com%2Fimage.png%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/image.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Open Graph previews on Twitter</h2><p class="post__p">OG meta tags can also be used to customise the appearance of your web pages according to the <b class="post__p--bold">platform</b> on which they&#39;re shared. Twitter rolled out their own custom implementation built on the OG protocol. This code snippet tells Twitter to show large full-width OG images when you share a link to Twitter, as opposed to the default smaller image alongside the title and description.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="yMlYUgGCcX"
      aria-describedby="yMlYUgGCcX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="yMlYUgGCcX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="yMlYUgGCcX" itemprop="text" content="%3Cmeta%20name%3D%22twitter%3Acard%22%20content%3D%22summary_large_image%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:card<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>summary_large_image<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Large image preview</h3><img src="https://images.ctfassets.net/56dzm01z6lln/7LoNhdNxXBNRLOmFClY5EG/718b0f7316b3e0d515811468b5fb273b/twitter_large_image_preview.png" alt="A blog post shared on Twitter that includes a large open graph image above the title and meta description of the post." height="771" width="679" /><h3 class="post__h3">Default image preview</h3><img src="https://images.ctfassets.net/56dzm01z6lln/47L8aMKv2PkuyvmAWyb8mZ/79c5cc7bb8560b079824e9a373401afc/twitter_small_image_preview.png" alt="A tweet with a link shared that shows a small square Open Graph image to the left of the page title and description." height="351" width="668" /><h2 class="post__h2">Open Graph previews in Slack</h2><p class="post__p">By default, Slack will unfurl the <code>og:title</code> and <code>og:description</code> meta tags found in the HTML page of the link you share.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4p21XukQ192QxD6yZEQDrU/f912a8c2283ab5bf84a16e5569914577/twitter_og_preview.png" alt="Screenshot of a link to twitter.com shared in Slack, which shows only the page title and description." height="150" width="655" /><p class="post__p">Navigate to twitter.com (without being logged in), inspect the source code, and you&#39;ll see the following meta tags in the HTML <code>&lt;head&gt;</code>. Interestingly, Twitter doesn&#39;t include a link to an image for an Open Graph preview on their home page!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="TycvFJrmDI"
      aria-describedby="TycvFJrmDI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="TycvFJrmDI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="TycvFJrmDI" itemprop="text" content="%3C!--%20Open%20Graph%20title%20--%3E%0A%3Cmeta%20content%3D%22Twitter.%20It%E2%80%99s%20what%E2%80%99s%20happening%22%20property%3D%22og%3Atitle%22%3E%0A%0A%3C!--%20Open%20Graph%20meta%20description%20--%3E%0A%3Cmeta%20content%3D%22From%20breaking%20news%20and%20entertainment%20to%20sports%20and%20politics%2C%20get%20the%20full%20story%20with%20all%20the%20live%20commentary.%22%20property%3D%22og%3Adescription%22%3E">
      <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- Open Graph title --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Twitter. It’s what’s happening<span class="token punctuation">"</span></span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:title<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br><br><span class="token comment">&lt;!-- Open Graph meta description --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>From breaking news and entertainment to sports and politics, get the full story with all the live commentary.<span class="token punctuation">"</span></span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:description<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Add an <code>og:image</code> meta tag, and Slack shows an image when it unfurls the link.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="TXWLpQKrwv"
      aria-describedby="TXWLpQKrwv">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="TXWLpQKrwv">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="TXWLpQKrwv" itemprop="text" content="%3Cmeta%20property%3D%22og%3Aimage%22%20content%3D%22https%3A%2F%2Fexample.com%2Fimage.png%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/image.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <img src="https://images.ctfassets.net/56dzm01z6lln/7CcaWwUR3viuDQGQM2bD9P/61a5f3a11b6d9822d5115a3a73045354/slack_preview_twitch.png" alt="Screenshot of a link to my twitch profile shared in Slack, showing the title, description and an image of me to the right." height="178" width="654" /><p class="post__p"><b class="post__p--bold">And here&#39;s how you level up.</b> Use a combination of <code>twitter:label</code> and <code>twitter:data</code> to display up to two extra bits of contextual information that Slack will unfurl. Interestingly, whilst these OG meta tags include <code>twitter</code> in the name, <u><b class="post__p--bold">the data does not appear in links shared on Twitter</b></u><b class="post__p--bold">.</b></p><p class="post__p">Including the code below in the HTML <code>&lt;head&gt;</code> will show author and reading time information when a link to that page is shared in Slack. You can switch out author and reading time for any information you choose. For example, if the page is about an event, you may wish to include the event date or location.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="zlKCmKgfAN"
      aria-describedby="zlKCmKgfAN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="zlKCmKgfAN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="zlKCmKgfAN" itemprop="text" content="%3Cmeta%20name%3D%22twitter%3Alabel1%22%20content%3D%22Written%20by%22%20%2F%3E%0A%3Cmeta%20name%3D%22twitter%3Adata1%22%20content%3D%22Salma%20Alam-Naylor%22%20%2F%3E%0A%0A%3Cmeta%20name%3D%22twitter%3Alabel2%22%20content%3D%22Reading%20time%22%20%2F%3E%0A%3Cmeta%20name%3D%22twitter%3Adata2%22%20content%3D%226%20minutes%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:label1<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Written by<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:data1<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Salma Alam-Naylor<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:label2<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Reading time<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:data2<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>6 minutes<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">And here&#39;s how it looks in Slack.</p><img src="https://images.ctfassets.net/56dzm01z6lln/zAYDZ6D9QvHk1U4NyZWEA/c29036b49478ae144249ea47e77c5221/slack_preview.png" alt="Screenshot of a link shared in Slack showing the title, description, author, reading time and image preview." height="224" width="657" /><h2 class="post__h2">A complete Open Graph example</h2><p class="post__p">Support for Open Graph meta tags across different platforms is currently inconsistent. However, I&#39;ve included as much rich information as possible on my blog post pages from the <a href="https://opengraphprotocol.org/" target="_blank">Open Graph specification</a>, should platforms support unfurling it in the future. </p><p class="post__p">Here&#39;s a complete example of all Open Graph meta tags included in this blog post: <a href="https://whitep4nth3r.com/blog/quick-light-dark-mode-css/">Light and dark mode in just 14 lines of CSS</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="NJWFcVbuAW"
      aria-describedby="NJWFcVbuAW">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="NJWFcVbuAW">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="NJWFcVbuAW" itemprop="text" content="%3C!--%20twitter%20card%20--%3E%0A%3Cmeta%20name%3D%22twitter%3Acard%22%20content%3D%22summary_large_image%22%20%2F%3E%0A%3Cmeta%20name%3D%22twitter%3Asite%22%20content%3D%22%40whitep4nth3r%22%20%2F%3E%0A%3Cmeta%20name%3D%22twitter%3Acreator%22%20content%3D%22%40whitep4nth3r%22%20%2F%3E%0A%0A%3C!--%20OG%20base%20data%20--%3E%0A%3Cmeta%20property%3D%22og%3Aurl%22%20content%3D%22https%3A%2F%2Fwhitep4nth3r.com%2Fblog%2Fquick-light-dark-mode-css%2F%22%20%2F%3E%0A%3Cmeta%20property%3D%22og%3Atitle%22%20content%3D%22Light%20and%20dark%20mode%20in%20just%2014%20lines%20of%20CSS%22%20%2F%3E%0A%3Cmeta%20property%3D%22og%3Adescription%22%20content%3D%22Add%20quick%20support%20for%20light%20mode%20and%20dark%20mode%20to%20your%20website%20using%20only%20CSS%20by%20combining%20two%20CSS%20custom%20properties%20with%20a%20media%20query.%22%20%2F%3E%0A%3Cmeta%20property%3D%22og%3Asite_name%22%20content%3D%22whitep4nth3r.com%22%20%2F%3E%0A%3Cmeta%20property%3D%22og%3Alocale%22%20content%3D%22en_GB%22%20%2F%3E%0A%0A%3C!--%20OG%20image%20data%20--%3E%0A%3Cmeta%20property%3D%22og%3Aimage%22%20content%3D%22https%3A%2F%2Flinktoimage.png%22%20%2F%3E%0A%3Cmeta%20property%3D%22og%3Aimage%3Aalt%22%20content%3D%22An%20image%20with%20dark%20background%20featuring%20a%20large%20panther%20on%20the%20left.%20The%20title%20%E2%80%94%20Light%20and%20dark%20mode%20in%20just%2014%20lines%20of%20CSS%20%E2%80%94%20is%20in%20white%20text%20at%20the%20centre%2C%20and%20below%20are%20icons%20representing%20the%20topics%20of%20the%20shared%20page.%22%20%2F%3E%0A%3Cmeta%20property%3D%22og%3Aimage%3Awidth%22%20content%3D%221140%22%20%2F%3E%0A%3Cmeta%20property%3D%22og%3Aimage%3Aheight%22%20content%3D%22600%22%20%2F%3E%0A%0A%3C!--%20extra%20metadata%20for%20Slack%20unfurls%20--%3E%0A%3Cmeta%20name%3D%22twitter%3Alabel1%22%20content%3D%22Written%20by%22%20%2F%3E%0A%3Cmeta%20name%3D%22twitter%3Adata1%22%20content%3D%22Salma%20Alam-Naylor%22%20%2F%3E%0A%3Cmeta%20name%3D%22twitter%3Alabel2%22%20content%3D%22Reading%20time%22%20%2F%3E%0A%3Cmeta%20name%3D%22twitter%3Adata2%22%20content%3D%223%20minutes%22%20%2F%3E%0A%0A%3C!--%20extra%20metadata%20%E2%80%94%20unknown%20support%20--%3E%0A%3Cmeta%20property%3D%22og%3Atype%22%20content%3D%22article%22%20%2F%3E%0A%3Cmeta%20property%3D%22article%3Asection%22%20content%3D%22Technology%22%20%2F%3E%0A%3Cmeta%20property%3D%22article%3Atag%22%20content%3D%22CSS%22%20%2F%3E%0A%3Cmeta%20property%3D%22article%3Atag%22%20content%3D%22Snippets%22%20%2F%3E%0A">
      <pre class="language-html"><code class="language-html"><span class="token comment">&lt;!-- twitter card --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:card<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>summary_large_image<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:site<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>@whitep4nth3r<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:creator<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>@whitep4nth3r<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><br><span class="token comment">&lt;!-- OG base data --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:url<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://whitep4nth3r.com/blog/quick-light-dark-mode-css/<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:title<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Light and dark mode in just 14 lines of CSS<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:description<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Add quick support for light mode and dark mode to your website using only CSS by combining two CSS custom properties with a media query.<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:site_name<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>whitep4nth3r.com<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:locale<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en_GB<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><br><span class="token comment">&lt;!-- OG image data --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://linktoimage.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image:alt<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>An image with dark background featuring a large panther on the left. The title — Light and dark mode in just 14 lines of CSS — is in white text at the centre, and below are icons representing the topics of the shared page.<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image:width<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1140<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image:height<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>600<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><br><span class="token comment">&lt;!-- extra metadata for Slack unfurls --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:label1<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Written by<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:data1<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Salma Alam-Naylor<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:label2<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Reading time<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:data2<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>3 minutes<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><br><span class="token comment">&lt;!-- extra metadata — unknown support --></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:type<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>article<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>article:section<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Technology<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>article:tag<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>CSS<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>article:tag<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Snippets<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Further reading</h2><p class="post__p">It turns out the <code>twitter:label</code> and <code>twitter:data</code> tags have been around for quite some time! If you want to learn more about how Slack unfurls link previews, check out this post from Matt Haughy in 2015: <a href="https://medium.com/slack-developer-blog/everything-you-ever-wanted-to-know-about-unfurling-but-were-afraid-to-ask-or-how-to-make-your-e64b4bb9254" target="_blank">Everything you ever wanted to know about unfurling but were afraid to ask /or/ How to make your site previews look amazing in Slack</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Light and dark mode in just 14 lines of CSS</title>
          <description>Combine two CSS custom properties with a media query to get set up with light and dark mode in seconds.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/quick-light-dark-mode-css/</link>
          <guid>https://whitep4nth3r.com/blog/quick-light-dark-mode-css/</guid>
          <pubDate>Wed, 11 May 2022 23:00:00 GMT</pubDate>
          <category>CSS</category><category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I like to <b class="post__p--bold">dark mode all the things</b>. But I also know lots of people who prefer light mode! To respect personal preferences and consider accessibility from the start, I add support for native light and dark mode as soon as I begin a new web project. </p><p class="post__p"><b class="post__p--bold">This solution uses no JavaScript</b>, so we&#39;re not building a light and dark mode <u><b class="post__p--italic">toggle</b></u>. Instead, it detects the user&#39;s system settings with a CSS media query, and uses two custom CSS properties to determine a basic colour scheme. Here&#39;s how it&#39;s done.</p><h2 class="post__h2">Declare 2 CSS custom properties</h2><p class="post__p">CSS custom properties are also referred to as <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties" target="_blank">CSS variables or cascading variables</a>. You can define CSS custom properties anywhere in your CSS files, and they follow the same cascading and specificity patterns as other CSS rules. For example, you can define CSS variables at the document <code>root</code>, and override them in more specific CSS classes. What&#39;s great is you can also inspect and debug declared CSS variables in browser dev tools, which appear below the stylesheet rules.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7wlUHpUIzHtffEt6HsZThs/3b7f52fa48cec635d126f027c49988e6/view_css_custom_props.png" alt="Screenshot of whitepanther dot com open in brave browser, showing a list of custom CSS properties in the style inspector tab." height="2334" width="4064" /><p class="post__p">CSS custom properties are declared by words prefixed with two dashes (--), and accessed using the <code>var()</code> function. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="feUgcCxjoo"
      aria-describedby="feUgcCxjoo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="feUgcCxjoo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="feUgcCxjoo" itemprop="text" content="%3Aroot%20%7B%0A%20%20--my-color-variable%3A%20%23000000%3B%0A%7D%0A%0A.element%20%7B%0A%20%20%2F*%20This%20is%20calculated%20as%20%23000000!%20*%2F%0A%20%20color%3A%20var(--my-color-variable)%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span><br>  <span class="token property">--my-color-variable</span><span class="token punctuation">:</span> #000000<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.element</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* This is calculated as #000000! */</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--my-color-variable<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">You can also pass a second parameter into the <code>var()</code> function, which acts as a fallback value if the custom property doesn&#39;t exist when you try to use it.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="sxEyOZaUni"
      aria-describedby="sxEyOZaUni">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="sxEyOZaUni">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="sxEyOZaUni" itemprop="text" content=".element%20%7B%0A%20%20color%3A%20var(--my-new-color%2C%20%23ff0000)%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.element</span> <span class="token punctuation">{</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--my-new-color<span class="token punctuation">,</span> #ff0000<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">For this light/dark mode solution, define two colour variables at the document <code>root</code> — one for the <b class="post__p--bold">foreground colour</b>, and one for the <b class="post__p--bold">background colour</b>. I tend to choose dark mode by default, so I set the background colour to black (<code>--color-bg</code>) and the foreground colour to white (<code>--color-fg</code>).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mdlwMyIxYp"
      aria-describedby="mdlwMyIxYp">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mdlwMyIxYp">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="mdlwMyIxYp" itemprop="text" content="%3Aroot%20%7B%0A%20%20--color-bg%3A%20%23000000%3B%0A%20%20--color-fg%3A%20%23ffffff%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">:root</span> <span class="token punctuation">{</span><br>  <span class="token property">--color-bg</span><span class="token punctuation">:</span> #000000<span class="token punctuation">;</span><br>  <span class="token property">--color-fg</span><span class="token punctuation">:</span> #ffffff<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Use the prefers-color-scheme media query</h2><p class="post__p">Next, we&#39;re going to hook into system settings using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" target="_blank">prefers-color-scheme</a> CSS media query and flip the variable declarations for the background and foreground colours. The following code sets the <code>--color-bg</code> to white and the <code>--color-fg</code> to black when a light theme setting is detected.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="FOnEbNkYst"
      aria-describedby="FOnEbNkYst">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="FOnEbNkYst">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="FOnEbNkYst" itemprop="text" content="%40media%20(prefers-color-scheme%3A%20light)%20%7B%0A%20%20%3Aroot%20%7B%0A%20%20%20%20--color-bg%3A%20%23ffffff%3B%0A%20%20%20%20--color-fg%3A%20%23000000%3B%0A%20%20%7D%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> light<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>  <span class="token selector">:root</span> <span class="token punctuation">{</span><br>    <span class="token property">--color-bg</span><span class="token punctuation">:</span> #ffffff<span class="token punctuation">;</span><br>    <span class="token property">--color-fg</span><span class="token punctuation">:</span> #000000<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Add body styles</h2><p class="post__p">Finally, using the CSS custom properties, set the <code>background-color</code> (for the page colour) and <code>color</code> (for the text) on the HTML <code>body</code> element, which all child elements will inherit if they&#39;re not overwritten.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VMlLuQaizs"
      aria-describedby="VMlLuQaizs">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VMlLuQaizs">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="VMlLuQaizs" itemprop="text" content="body%20%7B%0A%20%20background-color%3A%20var(--color-bg)%3B%0A%20%20color%3A%20var(--color-fg)%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">body</span> <span class="token punctuation">{</span><br>  <span class="token property">background-color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-bg<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-fg<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And that&#39;s it — support for native light and dark mode preferences in just 14 lines of CSS!</p><h2 class="post__h2">View the code on CodePen</h2><p class="post__p">Here&#39;s the <a href="https://codepen.io/whitep4nth3r/pen/abqmJMO" target="_blank">full example on CodePen</a>, which will display light or dark mode depending on your system preferences. Toggle your system settings to watch the CodePen switch themes!</p>
    <div class="codePenEmbed__container" data-codepen data-embed-code=%3Cp%20class=%22codepen%22%20data-height=%22300%22%20data-theme-id=%22dark%22%20data-default-tab=%22css,result%22%20data-slug-hash=%22abqmJMO%22%20data-editable=%22true%22%20data-user=%22whitep4nth3r%22%20style=%22height:%20300px;%20box-sizing:%20border-box;%20display:%20flex;%20align-items:%20center;%20justify-content:%20center;%20border:%202px%20solid;%20margin:%201em%200;%20padding:%201em;%22%3E%0A%20%20%3Cspan%3ESee%20the%20Pen%20%3Ca%20href=%22https://codepen.io/whitep4nth3r/pen/abqmJMO%22%3E%0A%20%20Light%20mode/dark%20mode%20in%2012%20lines%20of%20CSS%3C/a%3E%20by%20whitep4nth3r%20(%3Ca%20href=%22https://codepen.io/whitep4nth3r%22%3E@whitep4nth3r%3C/a%3E)%0A%20%20on%20%3Ca%20href=%22https://codepen.io%22%3ECodePen%3C/a%3E.%3C/span%3E%0A%3C/p%3E>
      <div data-target></div>
    </div>

    <script>
      const codepen = document.querySelector("[data-codepen]");
      let loaded = false;
      const options = {
        root: null,
        threshold: 0.1
      }

      const loadCodePen = (entries, observer) => {
        entries.forEach(entry => {
          
          if(!loaded && entry.isIntersecting) {
            const target = entry.target.querySelector("[data-target]");
            const embedCode = entry.target.dataset.embedCode;

            target.innerHTML = decodeURI(embedCode);
            const script = document.createElement("script");
            script.src = "https://cpwebassets.codepen.io/assets/embed/ei.js";
            document.head.append(script);
            loaded = true;
          }
        });
      };

      const observer = new IntersectionObserver(loadCodePen, options);
      observer.observe(codepen);
    </script>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>What is Jamstack?</title>
          <description>Let's explore Jamstack, “Jamstack-adjacent” technologies, and how you can get started building on the Jamstack.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/what-is-jamstack/</link>
          <guid>https://whitep4nth3r.com/blog/what-is-jamstack/</guid>
          <pubDate>Tue, 10 May 2022 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">A common question I get asked is “What is Jamstack?” I’ve been working with Jamstack solidly for over five years now, and I wanted to take an opportunity to demystify this technology stack, and reassure you that Jamstack has absolutely nothing to do with toast and butter (even though we wish it did). In this post, we’ll explore what Jamstack is, why Jamstack entered the web dev scene, “Jamstack-adjacent” technologies, and how you can get started building on the Jamstack.</p><p class="post__p">If you’re here for the TL;DR, I got you.</p><h2 class="post__h2">A Jamstack definition</h2><p class="post__p">Jamstack is an architectural model centred on serving pre-generated, highly portable static assets from web development platforms, usually via a Content Delivery Network (CDN). Jamstack architecture is elevated and enhanced by a wide variety of modern adjacent development tooling such as serverless and <a href="https://whitep4nth3r.com/blog/what-is-the-edge-serverless-functions/" target="_blank">edge functions,</a> content management systems, version control integration, and API services.</p><p class="post__p">Jamstack is a <b class="post__p--italic">way of working</b>. It’s not a group of frameworks or services or tied to any particular brands or tech stack. Jamstack is defined by <b class="post__p--italic">how</b> you build websites, rather than the tools with which you choose to build them. And it evolved out of the need to solve some very interesting problems.</p><h2 class="post__h2">About the JAM in Jamstack</h2><p class="post__p">The term “Jamstack” has been around since 2015, and came from the brains of Matt Biilmann and Chris Bach, founders of platform for modern web development <a href="https://netlify.com?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=netilfy-home" target="_blank">Netlify</a>. Back then, Jamstack meant using a combination of JavaScript, APIs, and Markup together to build static sites — websites that are packaged up and served as static HTML files and assets from a CDN. These static sites might exist as a collection of HTML files in a code repository, or they may be generated using a static site generator or build script, where a set of instructions as code turn template code into static HTML files and store them on a CDN.</p><p class="post__p">Jamstack was originally named <b class="post__p--bold">JAMstack</b> with uppercase JAM to describe the combination of technologies mentioned above. It has since moved to <b class="post__p--bold">Jamstack</b> to reflect its evolution to an architectural approach, instead of describing the technologies that might be used. You don’t necessarily have to use APIs — or even JavaScript — to build a Jamstack site!</p><p class="post__p">That’s the terminology covered, but what was <b class="post__p--italic">different</b> about Jamstack when it emerged in 2015?</p><h2 class="post__h2">Jamstack made websites faster</h2><p class="post__p">Traditionally, requests for web pages were processed by a managed server (more on this later), which did the job of building HTML documents in real-time before sending it back to the user’s browser. If the web page was big and complex, and if the page request was far away from the origin server, this meant that websites could be slow, resulting in a bad experience for the end-user. As more and more people started to use the web for <b class="post__p--italic">everything</b>, a fast experience was key to making websites usable, accessible — especially on slower internet connections, making sales, distributing information, and growing your business.</p><p class="post__p">Jamstack solved this problem in two ways. Firstly, Jamstack sites centre on pre-generating and storing website pages in advance — meaning a server doesn’t need to build <b class="post__p--italic">anything</b> at the time of a page request. Secondly, these pre-generated static assets are served from a Content Delivery Network (CDN) — a network of servers distributed around the world, working together to serve cached content to users from the closest server location possible. This guarantees a much faster experience than the traditional request, build, and serve model from a fixed server location. (It’s worth mentioning here that the traditional architecture described above is still alive and kicking, and Jamstack provides an alternative way of working, should it fit the needs of the project or product.)</p><h2 class="post__h2">Jamstack made developers faster</h2><p class="post__p">Before Jamstack, websites were built using entire full stack frameworks that comprised the back end server code, front end templates, databases — everything. New product features could take weeks to build, test, and release to production, software upgrades could be risky and difficult to roll back, and development environments were difficult to provision. Jamstack shifted to a <b class="post__p--bold">decoupled architecture</b>. This means back end API services, front end templates, databases, and all other services are separated and streamlined, allowing developers of different disciplines to build, test and release in parallel with each other, and go faster.</p><p class="post__p">Jamstack sites are front end (client) code that may or may not speak to a separate back end server and/or APIs. During the build process, a Jamstack site can call out to any number of external API services to fetch data to pre-generate static pages. Once a Jamstack site is built, it is cached as an <a href="https://jamstack.org/glossary/immutable/" target="_blank">immutable deploy</a> — a set of static files that cannot be changed. This means that a Jamstack site in production can be rolled back and forward quickly and easily between deploys should the need arise. What’s more, a breaking back end API change would have no effect on a static Jamstack site until the next time the site is rebuilt, making Jamstack sites more stable and less prone to bugs once they’re deployed to production. Speaking from experience, this decoupled approach also empowers front end developers to have full ownership of their code, processes and practices, without being at the mercy of a massive monolithic full stack scary codebase.</p><h2 class="post__h2">Jamstack made scaling less complicated</h2><p class="post__p">During the time of the massive monolith and the request-build-serve model, development and IT operations teams were responsible for managing their own servers (hence, “managed server” above). This came with a number of challenges, including managing security and software upgrades, DNS (!), and scaling the availability of servers up or down when website traffic increased or decreased. Jamstack hosting providers take care of all of this for you, meaning you can focus on writing the code that builds your product, instead of stressing about the infrastructure.</p><p class="post__p">This has also greatly lowered the bar for entry to building a website and putting it live for the world to enjoy. With Jamstack, even a beginner developer can put a website live in a matter of minutes. Write a static HTML file, put it on a CDN, automatically get a URL and SSL certificate, and share it with your friends.</p><h2 class="post__h2">Adjacent Jamstack technologies</h2><p class="post__p">Remember — Jamstack is an architectural way of working. Jamstack does not dictate what technologies, frameworks or web development platforms you choose. All Jamstack requires is that you centre your project on the concept of serving static assets from a CDN. You can choose to use serverless functions, edge functions, content management systems, other API services and more to enhance and enrich your Jamstack site — but at the core of Jamstack is a static-first approach without a managed server.</p><h2 class="post__h2">How to get started with your first Jamstack site</h2><p class="post__p">Jamstack projects have the following characteristics:</p><ul><li><p class="post__p">Ready-to-serve assets</p></li><li><p class="post__p">Any interactivity provided without a managed server</p></li><li><p class="post__p">Any interactivity provided by some combination of JavaScript and decoupled services via APIs</p></li></ul><p class="post__p">If you’re brand new to Jamstack, I personally don’t recommend starting out with a static site generator right away. Instead, I advise starting as small as possible. A Jamstack site can be as simple as:</p><ul><li><p class="post__p">serving a single HTML file (a ready-to-serve asset)</p></li><li><p class="post__p">from a web development platform such as <a href="https://docs.netlify.com/get-started/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-get-started" target="_blank">Netlify</a> (no managed server)</p></li><li><p class="post__p">that calls out to an external API on the client to get something done (if it needs to)</p></li></ul><p class="post__p">And the great thing about Jamstack is when you’re ready to add new features and functionality to your site, you don’t need to start again from scratch. Add extra pages when you need to, maybe add in a static site generator or build tool to generate dynamic pages at build time. Add extra functionality when you need to; maybe you’ll use adjacent technologies like serverless functions — if it fits.</p><p class="post__p">There’s no magic formula for Jamstack, but at its core, a Jamstack site is a set of pre-generated static assets served from a CDN. So, choose your project, choose your platform, start small, relish in the glow of serving static files from a CDN, and enjoy the ride.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>We're all living on it. But what exactly is The Edge?</title>
          <description>But what is The Edge? What are Edge Functions? And why does it matter?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/what-is-the-edge-serverless-functions/</link>
          <guid>https://whitep4nth3r.com/blog/what-is-the-edge-serverless-functions/</guid>
          <pubDate>Tue, 26 Apr 2022 23:00:00 GMT</pubDate>
          <category>Serverless</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">More and more cloud hosting providers and software-as-a-service platforms are offering serverless functions at The Edge. But what is <b class="post__p--bold">The Edge</b>? What are Edge Functions? <b class="post__p--italic">And why does it matter?</b></p><p class="post__p">First, let&#39;s learn a little bit about serverless functions.</p><h2 class="post__h2">What is a serverless function?</h2><p class="post__p">Contrary to what the term suggests, serverless actually doesn&#39;t mean the absence of a server! Serverless is a model that removes the limitations and constraints from traditional physical or virtual servers, allowing developers to build and scale applications by considering only the functions that need to be run, rather than needing to manage the overheads of persistently running applications. It&#39;s a simpler, more cost-effective way to build and run applications in the cloud.</p><p class="post__p">A server is essentially a computer connected to the internet that can store data, and store and execute code. &quot;Backend code&quot; such as APIs that read and write information to a database need a running server to execute. Before the dawn of serverless and &quot;The Cloud&quot; in around 2008-2010, in order to run any code that required a server, you had to store and run that code on your own physical servers (or computers), and maintain, manage and scale them yourself. </p><p class="post__p">For small projects, you could get away with hosting a server on your computer at home — but this doesn&#39;t scale when you have thousands of website visitors around the world generating millions of requests! Larger businesses required a group of networked physical servers to ensure a fast experience for users — and the cost of running these physical servers could get expensive very quickly!</p><p class="post__p">Serverless functions are a backend as a service — on demand. Cloud hosting providers such as Netlify, AWS and Vercel have offered serverless functions for a few years now, meaning that everyone from hobby developers to large businesses can execute backend code on demand, without having to manage, maintain and scale their own servers. When you run a serverless function on a cloud hosting provider, you pay only for the processing required, without worrying about increasing the number of servers available when your website traffic grows — it&#39;s all taken care of for you. </p><p class="post__p">What&#39;s more, most cloud providers come with generous free plans, so you can usually run hobby projects that use serverless functions at little to no financial cost. </p><h2 class="post__h2">Serverless and Jamstack</h2><p class="post__p">Serverless is an adjacent technology to the Jamstack architecture, and together they can achieve great things! Jamstack&#39;s core offering centres around serving pre-generated static assets, which are bundled up at build-time, from a Content Delivery Network (CDN). A CDN is a network of servers, distributed around the world, working together to serve cached content to users from the closest server location possible. As a result, people around the world get a fast experience on static sites served by a CDN. Netlify has a great explanation of how their CDN works on <a href="https://www.netlify.com/blog/2016/04/15/make-your-site-faster-with-netlifys-intelligent-cdn/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=intelligent-can" target="_blank">this blog post from 2016</a>.</p><h2 class="post__h2">What is The Edge?</h2><p class="post__p">A CDN is also known as an Edge Network, referring to the servers that sit on the &quot;edge&quot; of multiple networks, which act as bridges to route traffic between networks across the CDN. Serverless functions have historically been restricted to running on servers in specific locations. Take for example, the US West server on AWS. For me, making a request to US West from the UK takes longer than making a request to one of the European servers, due to the distance the data has to travel across the wire. </p><p class="post__p">If you&#39;re using serverless functions for server side rendering to enrich your Jamstack site, you need high availability on global servers in order to make your website fast for everyone in the world coupled with an intelligent network which knows how to route requests to the closest location to the user. <b class="post__p--italic"><b class="post__p--bold">And this is now possible with Edge Functions! </b></b></p><h2 class="post__h2">Edge Functions are serverless functions at The Edge</h2><p class="post__p">Edge Computing — running code at The Edge — now gives developers the power to execute serverless functions at the closest location to a request. Using Edge Computing, developers can collectively make the web faster and more accessible to more people, and we may also begin to see a reduction in carbon emissions as a result of data being served quicker over a shorter distance.</p><p class="post__p">Last week, <a href="https://www.netlify.com/blog/announcing-serverless-compute-with-edge-functions/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=announce-ef" target="_blank">Netlify released Edge Functions</a>, and you can now add Edge Functions to any new or existing project on Netlify. What&#39;s more, if you&#39;re on a starter plan, you can execute up to 3 million Edge Functions per month!</p><h2 class="post__h2">Edge Functions on Netlify</h2><p class="post__p">Edge Functions are very much like serverless functions. You can write them using JavaScript or TypeScript, but instead of using <a href="https://nodejs.org/en/" target="_blank">Node.js</a> under the hood, they are powered by <a href="https://deno.land/" target="_blank">Deno</a> — an open source runtime built on web standards. With Netlify Edge Functions, you can transform HTTP Requests and Responses, stream server rendered content, and even run full server side rendered applications at The Edge!</p><h3 class="post__h3">Hello, world!</h3><p class="post__p">Let&#39;s take a look at writing &quot;Hello, world&quot; as a Netlify Edge Function using TypeScript.</p><p class="post__p">Create a new project directory, and run <code>npm init</code> to create a package.json file by entering the following in your terminal.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="RjnTyjrFlM"
      aria-describedby="RjnTyjrFlM">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="RjnTyjrFlM">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="RjnTyjrFlM" itemprop="text" content="mkdir%20my-edge-functions-project%0A%0Acd%20my-edge-functions-project%0A%0Anpm%20init">
      <pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> my-edge-functions-project<br><br><span class="token builtin class-name">cd</span> my-edge-functions-project<br><br><span class="token function">npm</span> init</code></pre>
    </div>
  </div>

  <p class="post__p">Create the following directories and files inside your project folder.</p><ul><li><p class="post__p">netlify &gt; edge-functions &gt; hello.ts</p></li><li><p class="post__p">netlify.toml</p></li></ul><img src="https://images.ctfassets.net/56dzm01z6lln/1JkFm5G4flQXtbo1tWzoLw/8950ccf331c4ec8e60f17c92e4e27795/edge_functions_dirs.png" alt="The file explorer in VS Code showing the netlify directory, inside that an edge dash functions directory, and inside that a hello.ts file. Adjacent to the netlify directory is a netlify dot toml file and a package dot json file." height="245" width="513" /><p class="post__p">Next, open up the netlify.toml file and add an <code>[[edge_functions]]</code> entry for the hello.ts function.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mRWcTTQpWh"
      aria-describedby="mRWcTTQpWh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mRWcTTQpWh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="mRWcTTQpWh" itemprop="text" content="%23%20netlify.toml%0A%5B%5Bedge_functions%5D%5D%0A%20%20path%20%3D%20%22%2Fhello%22%0A%20%20function%20%3D%20%22hello%22">
      <pre class="language-toml"><code class="language-toml"><span class="token comment"># netlify.toml</span><br><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">edge_functions</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br>  <span class="token key property">path</span> <span class="token punctuation">=</span> <span class="token string">"/hello"</span><br>  <span class="token key property">function</span> <span class="token punctuation">=</span> <span class="token string">"hello"</span></code></pre>
    </div>
  </div>

  <p class="post__p">Copy and paste the following code into <code>hello.ts</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="yPlogaLtAJ"
      aria-describedby="yPlogaLtAJ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="yPlogaLtAJ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="yPlogaLtAJ" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Fhello.ts%0A%0Aimport%20type%20%7B%20Context%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0Aexport%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%20%20return%20new%20Response(%22Hello%2C%20World!%22%2C%20%7B%0A%20%20%20%20headers%3A%20%7B%20%22content-type%22%3A%20%22text%2Fhtml%22%20%7D%2C%0A%20%20%7D)%3B%0A%7D%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// netlify/edge-functions/hello.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> Context <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span><span class="token string">"Hello, World!"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>    headers<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"text/html"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Note: You might see a red underline under the type import if you&#39;re using VS Code. The Edge Function will still execute, but if you&#39;d like to say goodbye to the red line, run <code>ntl recipes vscode</code> in your terminal. This will install a VS Code settings directory and JSON file at the root of your project. <a href="https://cli.netlify.com/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=cli-docs" target="_blank">Read more about Netlify recipes on the CLI docs</a>.</p><p class="post__p">We start by exporting a <code>default async function</code> which receives two arguments.</p><ul><li><p class="post__p">A standard <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request" target="_blank">Request object</a> representing the incoming HTTP request</p></li><li><p class="post__p"><a href="https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/api/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=ef-context-object#netlify-specific-context-object" target="_blank">A Netlify-specific Context object</a> which comes with a LOT of useful functionality</p></li></ul><p class="post__p">The return value from an Edge Function is either:</p><ul><li><p class="post__p">a standard <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response" target="_blank">Response object</a> representing the HTTP response to be delivered to the client</p></li><li><p class="post__p">undefined (if you choose to bypass the current function)</p></li></ul><p class="post__p">Using the Netlify CLI (make sure you&#39;ve got <a href="https://www.npmjs.com/package/netlify-cli" target="_blank">the latest version</a>!), run <code>netlify dev</code> in your terminal, and navigate to the path in your browser specified in the netlify.toml file, which should be <a href="https://localhost:8888/hello" target="_blank">https://localhost:8888/hello</a>.</p><p class="post__p">And bam! <b class="post__p--bold">You&#39;ve executed an Edge Function in your local development environment!</b></p><img src="https://images.ctfassets.net/56dzm01z6lln/px2jD0ZTx2gORNmzmYJfR/87285fb6a212a80219ac6428b51a7fa3/hello_world_edge.png" alt="A browser on localhost slash hello showing the plain text Hello, World" height="2334" width="4064" /><p class="post__p"><a href="https://docs.netlify.com/get-started/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=get-started-deploy#deploy-a-project-to-netlify" target="_blank">Deploy your site to Netlify</a>, and Netlify will automatically bundle your Edge Function code. Visit your new site at {your_site_name}/hello, and boom! Now you&#39;re running a serverless function on Netlify&#39;s edge network!</p><p class="post__p">Now you have an Edge Function running on Netlify, let&#39;s explore what else is possible with the Netlify Context object and more.</p><h2 class="post__h2">What else is possible?</h2><p class="post__p">The <a href="https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/api/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=ef-context-object#netlify-specific-context-object" target="_blank">Netlify Context object</a> comes bundled with Edge-specific APIs and utility methods including a Geolocation API, Cookies API and handy <code>log()</code> and <code>json()</code> helpers. Let&#39;s look at some of my favourites.</p><h3 class="post__h3">Geolocation API</h3><p class="post__p">You can use Edge Functions to get information about a user&#39;s location to serve location-specific content and personalise their experience. Geolocation information is available on the <code>Context.geo</code> object. In hello.ts, <code>console.log</code> out the <code>Context.geo</code> object to view your geolocation data in the terminal!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZDePBZPcCG"
      aria-describedby="ZDePBZPcCG">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZDePBZPcCG">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="ZDePBZPcCG" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Fhello.ts%0A%0Aimport%20type%20%7B%20Context%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0Aexport%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%0A%20%20%2F%2F%20Let's%20inspect%20the%20geolocation%20data!%0A%20%20console.log(context.geo)%3B%0A%0A%20%20return%20new%20Response(%22Hello%2C%20World!%22%2C%20%7B%0A%20%20%20%20headers%3A%20%7B%20%22content-type%22%3A%20%22text%2Fhtml%22%20%7D%2C%0A%20%20%7D)%3B%0A%7D%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// netlify/edge-functions/hello.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> Context <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><br>  <span class="token comment">// Let's inspect the geolocation data!</span><br>  <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>geo<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Response</span><span class="token punctuation">(</span><span class="token string">"Hello, World!"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>    headers<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"text/html"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Here&#39;s mine!</p><img src="https://images.ctfassets.net/56dzm01z6lln/5ho4nLP3cIOZI4puDrYsDZ/e8e7b7a1e10246c4379055a42acc60c9/geo_output.png" alt="Terminal output showing my geolocation data from the hello edge function. City Manchester, country code GB, name United Kingdom, subdivision code ENG, name England." height="1554" width="2256" /><h3 class="post__h3">Context.json() method</h3><p class="post__p">We can return the geolocation data to the browser as JSON using a convenience method available on the Context object. Instead of returning a standard Response of type text/html, we can return the data using <code>context.json()</code>, like so:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BlydyGBVSe"
      aria-describedby="BlydyGBVSe">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BlydyGBVSe">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="BlydyGBVSe" itemprop="text" content="%2F%2F%20netlify%2Fedge-functions%2Fhello.ts%0A%0Aimport%20type%20%7B%20Context%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0Aexport%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%0A%20%20%2F%2F%20Send%20the%20geolocation%20data%20as%20JSON%20to%20the%20browser!%0A%20%20return%20context.json(context.geo)%3B%0A%7D%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token comment">// netlify/edge-functions/hello.ts</span><br><br><span class="token keyword">import</span> <span class="token keyword">type</span> <span class="token punctuation">{</span> Context <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><br>  <span class="token comment">// Send the geolocation data as JSON to the browser!</span><br>  <span class="token keyword">return</span> context<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span>context<span class="token punctuation">.</span>geo<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here&#39;s what we see in the browser!</p><img src="https://images.ctfassets.net/56dzm01z6lln/6FQ7Xf65P6mJGpUNJ1Iroh/dbe550cd8e09f7bf91edd5997b4112cd/edge_geo_json_in_browser.png" alt="A browser window showing a formatted JSON response containing my geolocation information." height="1316" width="2144" /><h3 class="post__h3">Proxy requests to other APIs</h3><p class="post__p">As developers we often work with third-party APIs to get and post data. Edge Functions allow us to use <a href="https://developer.mozilla.org/en-US/docs/Web/API/fetch" target="_blank">fetch()</a> (without an import!) to make requests to other sources. Here&#39;s an example of requesting data from a third-party API, and returning that data as JSON.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="HXOnUExKEt"
      aria-describedby="HXOnUExKEt">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="HXOnUExKEt">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="typescript">
      <meta data-code-id="HXOnUExKEt" itemprop="text" content="import%20%7B%20Context%20%7D%20from%20%22https%3A%2F%2Fedge.netlify.com%22%3B%0A%0Aexport%20default%20async%20(request%3A%20Request%2C%20context%3A%20Context)%20%3D%3E%20%7B%0A%0A%20%20const%20joke%20%3D%20await%20fetch(%22https%3A%2F%2Ficanhazdadjoke.com%2F%22%2C%20%7B%0A%20%20%20%20%22headers%22%3A%20%7B%0A%20%20%20%20%20%20%22Accept%22%3A%20%22application%2Fjson%22%0A%20%20%20%20%7D%0A%20%20%7D)%3B%0A%0A%20%20const%20jsonData%20%3D%20await%20joke.json()%3B%0A%20%20return%20context.json(jsonData)%3B%0A%7D%3B">
      <pre class="language-typescript"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span> Context <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"https://edge.netlify.com"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token operator">:</span> Request<span class="token punctuation">,</span> context<span class="token operator">:</span> Context<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br><br>  <span class="token keyword">const</span> joke <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">"https://icanhazdadjoke.com/"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>    <span class="token string-property property">"headers"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token string-property property">"Accept"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> jsonData <span class="token operator">=</span> <span class="token keyword">await</span> joke<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">return</span> context<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span>jsonData<span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Deploy your own and explore</h2><p class="post__p"><a href="https://twitter.com/philhawksworth" target="_blank">Phil Hawksworth</a> and I put together an <a href="https://edge-functions-examples.netlify.app/" target="_blank">Edge Functions Examples</a> site for you to explore the APIs and functionality available to you on Netlify. You can also browse the <a href="https://github.com/netlify/edge-functions-examples" target="_blank">source code on GitHub,</a> fork the repository, or copy and paste code examples to your project.</p><p class="post__p">If you want to deploy your own version of the site to Netlify, there&#39;s also a handy &quot;Deploy to Netlify&quot; button on the example site.</p><h2 class="post__h2">Watch the video</h2><p class="post__p">I tried out Netlify Edge Functions during the launch week <a href="https://twitch.tv/whitep4nth3r" target="_blank">live on Twitch</a>. Catch up on the stream below for an explanation of The Edge, a walkthrough of the <a href="https://docs.netlify.com/netlify-labs/experimental-features/edge-functions/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-ef" target="_blank">Netlify documentation</a>, and a showcase of the <a href="https://edge-functions-examples.netlify.app/" target="_blank">Edge Functions Examples</a> site.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/AfQy0Ulf3i8"
        title="What is The Edge? | Edge Functions on Netlify"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p">In short, taking serverless to The Edge is making the web faster, more accessible, and more personalised. Things like this make me excited for the future of the web and I can&#39;t wait to see what&#39;s next!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>HTML is all you need to make a website</title>
          <description>HTML-only websites are a controversial and divisive topic. But why?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/html-is-all-you-need-to-make-a-website/</link>
          <guid>https://whitep4nth3r.com/blog/html-is-all-you-need-to-make-a-website/</guid>
          <pubDate>Mon, 04 Apr 2022 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I recently wrote about <a href="https://whitep4nth3r.com/blog/how-i-improved-website-performance/">How I massively improved my website performance by using the right tool for the job</a>, and the TL;DR is that being mindful and purposeful about using less JavaScript, CSS, third-party scripts and whatever else we build websites with these days — dramatically increases site performance, accessibility and the end-user experience. </p><p class="post__p">This got me thinking about a <a href="https://css-tricks.com/add-less/" target="_blank">great post by Cassidy Williams</a> about <b class="post__p--italic">adding less</b>.</p><blockquote class="post__blockquote"><p class="post__p"><b class="post__p--bold">Your websites start fast until you add too much to make them slow.</b> Do you need any framework at all? Could you do what you want natively in the browser?</p></blockquote><p class="post__p">Websites are natively fast. And at the heart of every website is pure, unassuming, unadulterated HTML. At the dawn of Web 1.0, there was only HTML. <a href="http://info.cern.ch/hypertext/WWW/TheProject.html" >The first website</a> <b class="post__p--italic">ever</b> is still online, and yes — it&#39;s just HTML. It&#39;s fast and it just works™. In a world full of JavaScript frameworks, SPAs, cutting-edge CSS animations, powerful devices and fast internet connections, I wanted to celebrate the perfect validity of HTML-only websites and see what people were building. </p><h2 class="post__h2">HTML-only showcase</h2><p class="post__p">After kicking off the showcase with <a href="https://motherfuckingwebsite.com/" target="_blank">this iconic website</a> (don&#39;t click if you&#39;re uncomfortable with profanity!), I received some great submissions. It was wonderful to see that there are plenty of developers out there who are unashamedly building HTML-only websites.</p><p class="post__p"><a href="https://twitter.com/TheIdOfAlan" target="_blank">Alan Smith</a> had fun with creating pixelart using HTML tables and a few deprecated HTML tags on <a href="https://pixel-art.alanwsmith.com/" target="_blank">https://pixel-art.alanwsmith.com/</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3L1wG03RaZKxXFbeFM4qYa/2652642790e6d60ac21496c7ebbdde0a/alan_pixelart.png" alt="A screenshot of pixel-art.alanwsmith.com showing a pixel version of the man with the apple on his face." height="2382" width="3680" /><p class="post__p"><a href="https://twitter.com/tigt_" target="_blank">Taylor Hunt</a> shared a very clever <a href="https://yukiiro-nite.github.io/just-html/quiz/index.html" target="_blank">HTML-only math quiz</a> built by his friend Michael Bryant.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5LhqWQgXFfsl8dubBjNxmc/253056abf6e1240213e1a1b29bf7f158/math_quix.png" alt="A screenshot of a plain HTML only quiz, asking the question what is 9 divided by 3." height="2382" width="3680" /><p class="post__p"><a href="https://twitter.com/fimion" target="_blank">Alex Riviere</a> showed us what&#39;s possible using HTML table attributes to &quot;fake&quot; CSS on <a href="https://html-only.netlify.app/" target="_blank">https://html-only.netlify.app/</a>. I love that Alex calls out that &quot;Pretty HTML only pages are difficult to make well, and accessible. But here we are. I&#39;m trying my best.&quot;</p><img src="https://images.ctfassets.net/56dzm01z6lln/1x5yXWwvdfxgSQbKsrZnoG/f8a1ea8ac89014bbe6b49f25f7365a92/html_only_alex.png" alt="A screenshot of Alex's website, showing three tables with the headlines What is this, How does this work and But why" height="2382" width="3680" /><p class="post__p">Design engineer <a href="https://twitter.com/lochieaxon" target="_blank">Lochie Axon</a> shared <a href="https://lochieaxon.com/" target="_blank">their website</a> with us. &quot;My website has been html only for years now, excluding one very important element.&quot; </p><img src="https://images.ctfassets.net/56dzm01z6lln/3eV4njFHw3srPyJnycNFQa/5f35195d59e860a29c101d7044fc128c/lochie_website.png" alt="A screenshot of Lochie's website, showing their name, job title and some simple internet-blue links. The bonus element is a green sentence Design is my passion which is slightly rotated and looks wonderfully nostalgic" height="2382" width="3680" /><p class="post__p"><a href="https://twitter.com/domvo_" target="_blank">Dom</a> shared <a href="https://blog.fefe.de/" target="_blank">a popular blog site in Germany</a> — and it comes complete with an RSS feed!</p><img src="https://images.ctfassets.net/56dzm01z6lln/2zzPievMHXp90QdOlb8RBr/9e0b3db492f5e535dcde5075bb9a6a99/blog_fefe_de.png" alt="A screenshot of blog.fefe.de showing plain text and internet blue links, beautifully laid out." height="2382" width="3680" /><p class="post__p"><a href="https://twitter.com/5t3ph" target="_blank">Stephanie Eckles</a> showcases <a href="https://nojson.dev" target="_blank">nojson.dev</a> — a site &quot;lovingly crafted with NoJSON HTML&quot;. <a href="https://twitter.com/5t3ph/status/1343566815763509248" target="_blank">Read the full story here</a> (it&#39;s a good read!).</p><img src="https://images.ctfassets.net/56dzm01z6lln/53yCswuEXOaGX9CFYRDmcw/08ccb404dcb933160bf6fbfa218f8712/nojson_full.png" alt="A screenshot of nojson.dev. It reads: In a move considered revolutionary by many... This site has been delivered straight to your device with pure, unadulterated HTML. Our exclusive method is called NoJSON and is able to deliver just HTML over the wire. They said it couldn't be done! - Internet People" height="2382" width="3680" /><p class="post__p">And finally, <a href="https://twitter.com/dazulu" target="_blank">Adrian</a> baked us <a href="https://battenberg.netlify.app/" target="_blank">a delicious HTML cake</a> he made using a delicious HTML table.</p><img src="https://images.ctfassets.net/56dzm01z6lln/xLNqUXJEEDabCj4jYInlp/1fdf3d1ead035e17190bc03237712178/battenberg.png" alt="A screenshot of a website featuring a grid layout that resembles a battenberg cross section." height="2382" width="3680" /><h2 class="post__h2">HTML-only is controversial, apparently</h2><p class="post__p">Amidst the fun showcase of HTML-only websites, as with anything on the internet, there was of course, some controversy. </p><p class="post__p"><b class="post__p--bold">HTML is all you need to build a website!</b></p><p class="post__p">Some people asked, &quot;Why?&quot; I say, <b class="post__p--italic">&quot;Why not?&quot;</b></p><p class="post__p">Some people claimed that websites <b class="post__p--bold">without</b> CSS and JavaScript are &quot;bland&quot;. <b class="post__p--italic">Who cares?</b> If your content is readable and accessible without the noisy bells and whistles of loading animations and a fancy-pants design, then ship it.</p><p class="post__p">Someone else said HTML-only websites are &quot;ugly as hell.&quot; I disagree. They&#39;re <b class="post__p--italic">beautiful</b>.</p><p class="post__p">Why all this backlash against HTML-only websites? It is, after all, the fundamental tool of the web — our history. In the web ecosystem of the 2020s, developers are inundated with shiny things. Build tools, JavaScript frameworks, CSS frameworks, and more. These are all fun toys to play with — and a lot of them do a great job — <b class="post__p--bold">but at what cost</b>? Do we run the risk of losing our roots and where it all started? Or do we view HTML-only websites as being too <b class="post__p--italic">easy</b>? Too <b class="post__p--italic">simple</b>? <b class="post__p--italic"><b class="post__p--bold">Not impressive enough?</b></b></p><p class="post__p">I&#39;m not sure. But what I am sure about, is that websites don&#39;t need to be complicated to be effective. Add less. Ship less. Worry about less. And remember — HTML is where it all started. <b class="post__p--bold">And it&#39;s all you ever need to get started</b>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I massively improved my website performance by using the right tool for the job</title>
          <description>I rebuilt my website AGAIN with the aim of using as little JavaScript as possible to improve performance. Did I succeed? And what did I learn?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-i-improved-website-performance/</link>
          <guid>https://whitep4nth3r.com/blog/how-i-improved-website-performance/</guid>
          <pubDate>Tue, 29 Mar 2022 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Great website performance and top notch <a href="https://web.dev/vitals/#core-web-vitals" target="_blank">Core Web Vitals</a> are key to ensuring your site is fast, accessible and ranks well in search engines. From JavaScript frameworks to static site generators to lightweight build tools — there&#39;s a tool for every job! <b class="post__p--bold">And choosing the right tool for the job is essential to ensure you maximise performance, accessibility, and developer and end-user experience on your website. </b></p><p class="post__p">Before we take a look at the performance improvements I made in the third iteration of my blog, let&#39;s take a tour back in time to when I built my first and second blog sites on the Jamstack.</p><p class="post__p">To get straight to the data — <a href="#measuring-performance" >click here</a>.</p><h2 class="post__h2">2020: Svelte + Sapper</h2><p class="post__p">I built my first simple blog site in 2020 using <a href="https://sapper.svelte.dev/" target="_blank">Svelte and Sapper</a>. The blog posts were powered by markdown files stored in the repository, and it was a great starting point.</p><p class="post__p"><a href="https://whitep4nth3rcodes.netlify.app/" target="_blank">View the Svelte site for fun</a>!</p><h2 class="post__h2">2021: Next.js + Contentful</h2><p class="post__p">In January 2021, I joined <a href="https://contentful.com/developers" target="_blank">Contentful</a> as a Developer Advocate, and my focus was helping developers use Contentful with popular front end JavaScript frameworks. In March 2021, I rebuilt my blog site using Contentful and <a href="https://nextjs.org/" target="_blank">Next.js</a> — a React front-end framework that combines static pre-rendering, server-side rendering and in-built serverless functions.</p><p class="post__p">Throughout the year I continued to write blog posts, iterate and add functionality to the site to help developers use Next.js and Contentful together. But given the amount of features I loaded into the site, this version of my blog began to feel a little over-engineered. It was becoming more difficult to make small updates to the site, and the way I managed the content in such atomic ways felt more suited to a larger product team comprising multiple developers and content editors.</p><p class="post__p"><a href="https://p4nth3rblog.netlify.app" target="_blank">View the site to refer to later</a>.</p><h2 class="post__h2">2022: Eleventy + Contentful </h2><p class="post__p">Fast-forward to January 2022, when I joined the Developer Experience team at <a href="https://netlify.com?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=netilfy-home" target="_blank">Netlify</a>. And this got me thinking more about the core value proposition of the Jamstack: <b class="post__p--bold">serving highly optimised static pages and assets created during a build process</b>. Furthermore, the talk I gave in 2021 — <a href="https://whitep4nth3r.com/talks/how-to-prevent-the-collapse-of-society-by-building-an-accessible-web/" target="_blank">How to prevent the collapse of society by building an accessible web</a> — which highlights that delivering many, many JavaScript files to the browser can hinder performance, accessibility and user experience, made me want to challenge myself to build a website that <b class="post__p--bold">delivered as little JavaScript to the browser as possible</b>. </p><p class="post__p">And so I turned to <a href="https://www.11ty.dev/" target="_blank">Eleventy</a> — a static site generator built with JavaScript — <b class="post__p--bold">that ships no JavaScript to the browser by default</b>. It&#39;s more of a build tool than a front end framework, and gives developers total control of the files and assets that are built and served to a browser from a CDN. The great thing about this migration was that I could continue to use Contentful to manage my content.</p><h2 class="post__h2">Full disclaimer: all tools are valid tools!</h2><p class="post__p">Before we get into the performance improvements I made switching from Next.js to Eleventy, <b class="post__p--bold">I want to make it absolutely clear that all tools are valid</b>, and me moving from Next.js does not mean I think it&#39;s a terrible tool! I think it&#39;s a great tool — especially for larger development teams that don&#39;t have the time to build their own software design patterns and scaleable architecture from the ground up. It&#39;s always super quick to get a large-scale production-ready app launched using Next.js!</p><h2 class="post__h2">Choosing your tools</h2><p class="post__p">Choosing a static site generator or front end framework for your new project should depend on a number of factors, including:</p><ul><li><p class="post__p">What the project is and how it might grow (and this could also change!)</p></li><li><p class="post__p">How large your development team is</p></li><li><p class="post__p">Whether you need to cater for an editorial content team</p></li><li><p class="post__p">Who your end-users are, what devices they use, and where they are in the world!</p></li></ul><p class="post__p">My answers to these questions pushed me in the direction of building with a more lightweight solution than Next.js. Plus, this was a great opportunity to learn how to use<b class="post__p--italic"> yet another static site generator</b>.</p><p class="post__p">Additionally, more and more developers I have connected with over the last few months are using Eleventy to build lean, performant sites. I felt like this would be a good opportunity to learn from others, and help others in the process as well.</p><p class="post__p">Now, let&#39;s look at the data!</p><h2 class="post__h2">Measuring performance</h2><p class="post__p">In this study, success was measured against the following four objectives:</p><ul><li><p class="post__p">Ship less JavaScript to the browser</p></li><li><p class="post__p">Reduce network requests</p></li><li><p class="post__p">Improve all Core Web Vitals</p></li><li><p class="post__p">Improve Lighthouse performance scores</p></li></ul><p class="post__p">Performance was compared between the Next.js site and Eleventy site using three free tools: <a href="https://developers.google.com/web/tools/lighthouse/" target="_blank">Google Lighthouse</a>, <a href="https://web.dev/measure/" target="_blank">web.dev/measure</a> and <a href="https://www.webpagetest.org/" target="_blank">Web Page Test</a>. Lighthouse tests were run in Brave Browser dev tools, Web Page Test runs were conducted via the web app using the London, UK - EC2 server, and web.dev tests were conducted in the browser. Given that I advocate for mobile-first development — and that&#39;s where performance is most likely to be impacted given the unpredictable speed of mobile data — all tests were conducted in an emulated mobile environment using a medium 3G network speed, on iPhone 6/7/8.</p><h2 class="post__h2">Ship less JavaScript to the browser</h2><p class="post__p">Using Web Page Test, I looked at the breakdown by MIME type of the home page for both sites.</p><h4 class="post__h4">Before</h4><p class="post__p">The Next.js site delivered <b class="post__p--bold">33</b> JavaScript files to browser, <b class="post__p--bold">totalling 2.94MB</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4YLV5RHvq7rxoRdi1LFZZa/3eac7c01c4cdafbb95ca6ab43e6fedf4/wpt_nextjs_mimetype.png" alt="Screenshot from web page test showing the breakdown by MIME type for the Next.js site. The main data point is that there are 33 JS files coming to 2944566 bytes uncompressed." height="887" width="1289" /><h4 class="post__h4">After</h4><p class="post__p">The Eleventy site delivers just <b class="post__p--bold">1</b> JavaScript file to browser, totalling just <b class="post__p--bold">20.5kb</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6HjiZOXKAQwT3qdObb6H59/644018e711714e8e62c334d7496dce34/wpt_eleventy_mimetype.png" alt="Screenshot from web page test showing the breakdown by MIME type for the Eleventy site. The main data point is that there is JS file coming to 20525 bytes uncompressed." height="879" width="1287" /><p class="post__p">In comparing the visual design of the <a href="https://p4nth3rblog.netlify.app" target="_blank">Next.js site</a> with the <a href="https://whitep4nth3r.com/" target="_blank">Eleventy site</a>, you might argue that the two home pages are so vastly different that there are bound to be such discrepancies. But this is down to the differences in how Next.js and Eleventy build and bundle files for production.</p><p class="post__p">Next.js, by default, ships chunked JavaScript files to the browser for pre-rendering dynamic content from the Next.js server, and for interactivity on the client-side through React. In contrast, Eleventy ships no JavaScript by default, and instead outputs static HTML files as part of the build process.</p><p class="post__p">What&#39;s also important to note is that <b class="post__p--bold">design is performance</b>. I made a bunch of design decisions during the migration with performance in mind and to cut down on third-party dependencies. This included:</p><ul><li><p class="post__p">removing the &quot;latest YouTube video&quot; from the home page</p></li><li><p class="post__p">removing Google Analytics from the whole site</p></li><li><p class="post__p">self-hosting fonts rather than requesting them from the Google Fonts CDN</p></li></ul><p class="post__p">Furthermore, the only JavaScript file that the Eleventy site ships on the home page is to power the time localisation for the &quot;latest Twitch stream&quot; details — and this was a purposeful addition by me.</p><p class="post__p"><b class="post__p--bold">✅ </b>Ship less JavaScript to the browser — <b class="post__p--bold">success</b>!</p><h2 class="post__h2">Reduce network requests</h2><p class="post__p">As shown in the images above, the Next.js home page made 75 network requests for HTML, CSS, JavaScript, fonts, images and other (JSON etc) files. These network requests totalled 3.98MB.</p><p class="post__p">The Eleventy home page makes just 9 network requests, totalling just 325.5kb.</p><p class="post__p"><b class="post__p--bold">This makes the new home page just 8% of the size of the old home page!</b> The largest payload on the Eleventy site comes from font files — which I could do well to optimise further in the future.</p><p class="post__p">✅ Reduce network requests — <b class="post__p--bold">success</b>!</p><h2 class="post__h2">Improve Core Web Vitals</h2><p class="post__p">Core Web Vitals are currently scored on three aspects of user experience — loading, interactivity and visual stability. These tests were conducted on the home page of both sites using <a href="https://web.dev/measure/" target="_blank">web.dev/measure</a>.</p><h3 class="post__h3">Loading</h3><p class="post__p">Loading performance is measured by the <b class="post__p--bold">Largest Contentful Paint (LCP).</b> To provide a good user experience, the LCP should happen within 2.5 seconds of when the page first starts loading. </p><p class="post__p"><b class="post__p--bold">And the results:</b></p><ul><li><p class="post__p">Next.js site — LCP = 9.9s</p></li><li><p class="post__p">Eleventy site — LCP = 1s</p></li></ul><p class="post__p">✅ Improve LCP — <b class="post__p--bold">success</b>!</p><h3 class="post__h3">Interactivity</h3><p class="post__p">Interactivity is measured by the <b class="post__p--bold">First Input Delay (FID)</b> and measures how soon your web application responds to user input such as clicking buttons, selecting text and typing into form fields. It&#39;s difficult to accurately measure FID as it involves testing with real users, so we can use the <a href="https://web.dev/interactive/#what-tti-measures" target="_blank">Time to Interactive (TTI)</a> metric to calculate how long a page takes to become fully interactive.</p><p class="post__p"><b class="post__p--bold">And the results:</b></p><ul><li><p class="post__p">Next.js site — TTI = 11s</p></li><li><p class="post__p">Eleventy site — TTI = 2.5s</p></li></ul><p class="post__p">✅ Improve TTI — <b class="post__p--bold">success</b>!</p><h3 class="post__h3">Visual stability</h3><p class="post__p">Visual stability is measured by <b class="post__p--bold">Cumulative Layout Shift (CLS)</b>. Have you ever clicked on a part of a web page, only to find that you unexpectedly clicked on something else after a rogue element or image was finally loaded? CLS is where content pops into view once it has loaded, often pushing content down or sideways on the page — and can be extremely frustrating! A good user experience maintains a CLS score of 0.1 or less.</p><p class="post__p"><b class="post__p--bold">And the results:</b></p><ul><li><p class="post__p">Next.js site — TTI = 0.002</p></li><li><p class="post__p">Eleventy site — TTI = 0.032</p></li></ul><p class="post__p">😬 Improve CLS — not really! It&#39;s still way below 0.1, which is good! But I&#39;m really not sure what increased the CLS on the Eleventy site! This needs more investigation.</p><p class="post__p">Here are the test results from a single run of web.dev/measure for comparison. Green numbers all round!</p><h4 class="post__h4">Before</h4><img src="https://images.ctfassets.net/56dzm01z6lln/6BNpf6m3ZAwXh06cZD2a7y/0925ad7e7e257325128b2aa42f44b3a7/measure_nextjs.png" alt="A screenshot from web.dev measure showing a performance score of 34, LCP of 2.5 seconds, TTI of 11 seconds and CLS of 0.0002 for the next.js site." height="882" width="1501" /><h4 class="post__h4">After</h4><img src="https://images.ctfassets.net/56dzm01z6lln/3oien7Ww2VKj2BuXm2G0RH/c90f33f2123606723902d765eab40e40/measure_eleventy.png" alt="A screenshot from web.dev measure showing a performance score of 100, LCP of 1 second, TTI of 2.5 seconds and CLS of 0.032 for the Eleventy site." height="883" width="1500" /><h2 class="post__h2">Improve Lighthouse performance scores</h2><p class="post__p">Given that I had focussed on the home page only in previous tests, I also tested the Google Lighthouse performance score of various blog posts with different characteristics to ensure I&#39;d made an improvement across the site.</p><h3 class="post__h3">Home page</h3><p class="post__p">Here&#39;s our baseline, showing a great improvement from 73 to 100!</p>
    <div class="lighthouse">
      <span class="lighthouse__header">
        <span>
          <p class="lighthouse__title">Google Lighthouse Performance Score Comparison on Mobile</p>
          <a href="https://whitep4nth3r.com/" target="_blank" class="lighthouse__subTitle">https://whitep4nth3r.com/</a>
        </span>
        <span aria-hidden="true">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      role="img"
      aria-label="Google Lighthouse"
      height="60"
      width="60"
      viewBox="0 0 512 548.20728733">
      <g fill="none" fill-rule="evenodd">
        <circle cx="256" cy="256" fill="#0535c1" fill-rule="nonzero" r="256" />
        <path d="m311.273 116.364h151.272v151.272h-151.272z" />
        <path
          d="m424.727 179.386h-15.313c-7.62-21.77-29.847-34.855-52.58-30.955s-39.326 23.646-39.254 46.71c.006 26.11 21.17 47.273 47.278 47.28h59.87c16.998-.566 30.485-14.51 30.485-31.518s-13.487-30.951-30.486-31.517z"
          fill="#eaeaea"
          fill-rule="nonzero"
        />
        <path
          d="m456.25 211.293v-.384c-.006-17.407-14.116-31.516-31.523-31.523h-15.755v.791h15.755c17.248-.001 31.294 13.859 31.523 31.104z"
          fill="#fff"
          fill-opacity=".2"
          fill-rule="nonzero"
        />
        <g fill-rule="nonzero">
          <path
            d="m364.858 147.887 25.554 80.71c8.54-8.577 21.713-20.4 21.713-33.444-.006-26.108-21.17-47.272-47.278-47.278z"
            fill="#e1e1e1"
          />
          <circle cx="364.858" cy="195.153" fill="#eee" r="47.279" />
          <path
            d="m364.858 148.666c25.955.012 47.05 20.94 47.267 46.895v-.384c.01-26.108-21.147-47.28-47.255-47.29s-47.28 21.147-47.29 47.255v.384c.236-25.945 21.332-46.855 47.278-46.86z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m424.727 241.63h-59.88c-25.955-.013-47.05-20.942-47.267-46.895v.384c.012 26.104 21.174 47.26 47.278 47.266h59.87c17.402-.006 31.51-14.108 31.522-31.51v-.385c-.216 17.257-14.265 31.134-31.523 31.14z"
            fill="#212121"
            fill-opacity=".1"
          />
          <path d="m186.182 107.636h133.818v116.364h-133.818z" fill="#fff176" />
          <path d="m171.636 285.09h160v160h-160z" fill="#fff" />
          <g fill="#f4481e">
            <path d="m349.09 212.364h23.274v46.545h-232.728v-46.545h23.273v-93.091l93.091-58.183 93.09 58.182zm-46.545 0v-67.294l-46.545-29.09-46.545 29.09v67.294z" />
            <path d="m129.303 478.499 39.424-266.135h174.546l39.424 266.135a254.836 254.836 0 0 1 -126.697 33.501 254.836 254.836 0 0 1 -126.697-33.501zm178.817-185.833-109.499 35.574-13.311 89.83 134.889-43.834z" />
          </g>
        </g>
        <path d="m55.273 165.818h174.545v174.546h-174.545z" />
        <g fill-rule="nonzero">
          <path
            d="m186.182 238.545h-17.676c-8.83-25.075-34.455-40.129-60.657-35.632-26.202 4.498-45.341 27.235-45.304 53.82.02 30.118 24.434 54.527 54.552 54.54h69.085c19.725-.5 35.455-16.633 35.455-36.364s-15.73-35.865-35.455-36.364z"
            fill="#fafafa"
          />
          <path
            d="m222.545 275.34v-.43c0-20.074-16.29-36.365-36.363-36.365h-18.188v.908h18.188c19.895.005 36.096 15.993 36.363 35.887z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m117.097 202.182 29.475 93.114c9.856-9.89 25.053-23.529 25.053-38.586-.02-30.114-24.426-54.52-54.54-54.54z"
            fill="#e1e1e1"
          />
          <circle cx="117.097" cy="256.733" fill="#fff" r="54.551" />
          <path
            d="m117.097 203.09c29.946.018 54.284 24.163 54.54 54.109v-.431a54.545 54.545 0 1 0 -109.092-.035v.43c.269-29.938 24.612-54.068 54.552-54.074z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m186.182 310.365h-69.097c-29.946-.018-54.284-24.164-54.54-54.109v.43c.013 30.123 24.43 54.539 54.552 54.552h69.085c20.073 0 36.363-16.291 36.363-36.364v-.442c-.242 19.912-16.45 35.928-36.363 35.933z"
            fill="#212121"
            fill-opacity=".1"
          />
          <path
            d="m256 2.676c140.94 0 255.244 113.885 255.977 254.662l.023-1.338c0-141.382-114.618-256-256-256s-256 114.618-256 256c0 .442.035.873.035 1.338.721-140.765 115.002-254.662 255.965-254.662z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m511.977 254.662c-.745 140.777-115.037 254.662-255.977 254.662-140.951 0-255.244-113.897-255.965-254.662 0 .465-.035.896-.035 1.338 0 141.382 114.618 256 256 256s256-114.618 256-256z"
            fill="#263238"
            fill-opacity=".2"
          />
        </g>
      </g>
    </svg>
  </span>
      </span>

      <div class="lighthouse__scores">
        <span class="lighthouse__col">
          <span class="lighthouse__scoresNo lighthouse__scoresNo--orange" aria-label="Score of 73 before">
            73
            <svg class="lighthouse__circle" viewbox="0 0 100 100" width="200" height="200" style="stroke-dasharray: 215.5760878893316 999;">
              <circle cx="50" cy="50" r="47" />  
            </svg>
          </span>
          <p class="lighthouse__type" aria-hidden="true">Before</p>
        </span>
        <span class="lighthouse__col">
          <span class="lighthouse__scoresNo lighthouse__scoresNo--green" aria-label="Score of 100 after">
            100
            <svg class="lighthouse__circle" viewbox="0 0 100 100" width="200" height="200" style="stroke-dasharray: 295.3097094374406 999;">
              <circle cx="50" cy="50" r="47" />  
            </svg>
          </span>
          <p class="lighthouse__type" aria-hidden="true">After</p>
        </span>
      </div>
    </div>
  <h3 class="post__h3">A long blog post with lots of code examples</h3><p class="post__p">Lighthouse performances scores are often impacted by an &quot;excessive DOM size&quot;, which for my blog posts, is usually the result of large code examples and the way the code snippets are highlighted using lots and lots of <code>&lt;span&gt;</code> tags. This blog post saw a great improvement on performance from 89 to 100.</p>
    <div class="lighthouse">
      <span class="lighthouse__header">
        <span>
          <p class="lighthouse__title">Google Lighthouse Performance Score Comparison on Mobile</p>
          <a href="https://whitep4nth3r.com/blog/personalized-image-social-sharing-with-cloudinary-nextjs/" target="_blank" class="lighthouse__subTitle">https://whitep4nth3r.com/blog/personalized-image-social-sharing-with-cloudinary-nextjs/</a>
        </span>
        <span aria-hidden="true">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      role="img"
      aria-label="Google Lighthouse"
      height="60"
      width="60"
      viewBox="0 0 512 548.20728733">
      <g fill="none" fill-rule="evenodd">
        <circle cx="256" cy="256" fill="#0535c1" fill-rule="nonzero" r="256" />
        <path d="m311.273 116.364h151.272v151.272h-151.272z" />
        <path
          d="m424.727 179.386h-15.313c-7.62-21.77-29.847-34.855-52.58-30.955s-39.326 23.646-39.254 46.71c.006 26.11 21.17 47.273 47.278 47.28h59.87c16.998-.566 30.485-14.51 30.485-31.518s-13.487-30.951-30.486-31.517z"
          fill="#eaeaea"
          fill-rule="nonzero"
        />
        <path
          d="m456.25 211.293v-.384c-.006-17.407-14.116-31.516-31.523-31.523h-15.755v.791h15.755c17.248-.001 31.294 13.859 31.523 31.104z"
          fill="#fff"
          fill-opacity=".2"
          fill-rule="nonzero"
        />
        <g fill-rule="nonzero">
          <path
            d="m364.858 147.887 25.554 80.71c8.54-8.577 21.713-20.4 21.713-33.444-.006-26.108-21.17-47.272-47.278-47.278z"
            fill="#e1e1e1"
          />
          <circle cx="364.858" cy="195.153" fill="#eee" r="47.279" />
          <path
            d="m364.858 148.666c25.955.012 47.05 20.94 47.267 46.895v-.384c.01-26.108-21.147-47.28-47.255-47.29s-47.28 21.147-47.29 47.255v.384c.236-25.945 21.332-46.855 47.278-46.86z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m424.727 241.63h-59.88c-25.955-.013-47.05-20.942-47.267-46.895v.384c.012 26.104 21.174 47.26 47.278 47.266h59.87c17.402-.006 31.51-14.108 31.522-31.51v-.385c-.216 17.257-14.265 31.134-31.523 31.14z"
            fill="#212121"
            fill-opacity=".1"
          />
          <path d="m186.182 107.636h133.818v116.364h-133.818z" fill="#fff176" />
          <path d="m171.636 285.09h160v160h-160z" fill="#fff" />
          <g fill="#f4481e">
            <path d="m349.09 212.364h23.274v46.545h-232.728v-46.545h23.273v-93.091l93.091-58.183 93.09 58.182zm-46.545 0v-67.294l-46.545-29.09-46.545 29.09v67.294z" />
            <path d="m129.303 478.499 39.424-266.135h174.546l39.424 266.135a254.836 254.836 0 0 1 -126.697 33.501 254.836 254.836 0 0 1 -126.697-33.501zm178.817-185.833-109.499 35.574-13.311 89.83 134.889-43.834z" />
          </g>
        </g>
        <path d="m55.273 165.818h174.545v174.546h-174.545z" />
        <g fill-rule="nonzero">
          <path
            d="m186.182 238.545h-17.676c-8.83-25.075-34.455-40.129-60.657-35.632-26.202 4.498-45.341 27.235-45.304 53.82.02 30.118 24.434 54.527 54.552 54.54h69.085c19.725-.5 35.455-16.633 35.455-36.364s-15.73-35.865-35.455-36.364z"
            fill="#fafafa"
          />
          <path
            d="m222.545 275.34v-.43c0-20.074-16.29-36.365-36.363-36.365h-18.188v.908h18.188c19.895.005 36.096 15.993 36.363 35.887z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m117.097 202.182 29.475 93.114c9.856-9.89 25.053-23.529 25.053-38.586-.02-30.114-24.426-54.52-54.54-54.54z"
            fill="#e1e1e1"
          />
          <circle cx="117.097" cy="256.733" fill="#fff" r="54.551" />
          <path
            d="m117.097 203.09c29.946.018 54.284 24.163 54.54 54.109v-.431a54.545 54.545 0 1 0 -109.092-.035v.43c.269-29.938 24.612-54.068 54.552-54.074z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m186.182 310.365h-69.097c-29.946-.018-54.284-24.164-54.54-54.109v.43c.013 30.123 24.43 54.539 54.552 54.552h69.085c20.073 0 36.363-16.291 36.363-36.364v-.442c-.242 19.912-16.45 35.928-36.363 35.933z"
            fill="#212121"
            fill-opacity=".1"
          />
          <path
            d="m256 2.676c140.94 0 255.244 113.885 255.977 254.662l.023-1.338c0-141.382-114.618-256-256-256s-256 114.618-256 256c0 .442.035.873.035 1.338.721-140.765 115.002-254.662 255.965-254.662z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m511.977 254.662c-.745 140.777-115.037 254.662-255.977 254.662-140.951 0-255.244-113.897-255.965-254.662 0 .465-.035.896-.035 1.338 0 141.382 114.618 256 256 256s256-114.618 256-256z"
            fill="#263238"
            fill-opacity=".2"
          />
        </g>
      </g>
    </svg>
  </span>
      </span>

      <div class="lighthouse__scores">
        <span class="lighthouse__col">
          <span class="lighthouse__scoresNo lighthouse__scoresNo--orange" aria-label="Score of 89 before">
            89
            <svg class="lighthouse__circle" viewbox="0 0 100 100" width="200" height="200" style="stroke-dasharray: 262.82564139932214 999;">
              <circle cx="50" cy="50" r="47" />  
            </svg>
          </span>
          <p class="lighthouse__type" aria-hidden="true">Before</p>
        </span>
        <span class="lighthouse__col">
          <span class="lighthouse__scoresNo lighthouse__scoresNo--green" aria-label="Score of 100 after">
            100
            <svg class="lighthouse__circle" viewbox="0 0 100 100" width="200" height="200" style="stroke-dasharray: 295.3097094374406 999;">
              <circle cx="50" cy="50" r="47" />  
            </svg>
          </span>
          <p class="lighthouse__type" aria-hidden="true">After</p>
        </span>
      </div>
    </div>
  <h3 class="post__h3">A long blog post with lots of images</h3><p class="post__p">I did a lot of work on the Next.js blog with regards to optimising image formats for different browsers, and lazy loading those images where supported. The score still improved from 98 to 99, though! Read the blog post linked below to learn more about how to load responsive images in AVIF and WebP using the HTML <code>&lt;picture&gt;</code> tag!</p>
    <div class="lighthouse">
      <span class="lighthouse__header">
        <span>
          <p class="lighthouse__title">Google Lighthouse Performance Score Comparison on Mobile</p>
          <a href="https://whitep4nth3r.com/blog/how-to-load-responsive-images-in-avif-and-webp-using-html-picture-element/" target="_blank" class="lighthouse__subTitle">https://whitep4nth3r.com/blog/how-to-load-responsive-images-in-avif-and-webp-using-html-picture-element/</a>
        </span>
        <span aria-hidden="true">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      role="img"
      aria-label="Google Lighthouse"
      height="60"
      width="60"
      viewBox="0 0 512 548.20728733">
      <g fill="none" fill-rule="evenodd">
        <circle cx="256" cy="256" fill="#0535c1" fill-rule="nonzero" r="256" />
        <path d="m311.273 116.364h151.272v151.272h-151.272z" />
        <path
          d="m424.727 179.386h-15.313c-7.62-21.77-29.847-34.855-52.58-30.955s-39.326 23.646-39.254 46.71c.006 26.11 21.17 47.273 47.278 47.28h59.87c16.998-.566 30.485-14.51 30.485-31.518s-13.487-30.951-30.486-31.517z"
          fill="#eaeaea"
          fill-rule="nonzero"
        />
        <path
          d="m456.25 211.293v-.384c-.006-17.407-14.116-31.516-31.523-31.523h-15.755v.791h15.755c17.248-.001 31.294 13.859 31.523 31.104z"
          fill="#fff"
          fill-opacity=".2"
          fill-rule="nonzero"
        />
        <g fill-rule="nonzero">
          <path
            d="m364.858 147.887 25.554 80.71c8.54-8.577 21.713-20.4 21.713-33.444-.006-26.108-21.17-47.272-47.278-47.278z"
            fill="#e1e1e1"
          />
          <circle cx="364.858" cy="195.153" fill="#eee" r="47.279" />
          <path
            d="m364.858 148.666c25.955.012 47.05 20.94 47.267 46.895v-.384c.01-26.108-21.147-47.28-47.255-47.29s-47.28 21.147-47.29 47.255v.384c.236-25.945 21.332-46.855 47.278-46.86z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m424.727 241.63h-59.88c-25.955-.013-47.05-20.942-47.267-46.895v.384c.012 26.104 21.174 47.26 47.278 47.266h59.87c17.402-.006 31.51-14.108 31.522-31.51v-.385c-.216 17.257-14.265 31.134-31.523 31.14z"
            fill="#212121"
            fill-opacity=".1"
          />
          <path d="m186.182 107.636h133.818v116.364h-133.818z" fill="#fff176" />
          <path d="m171.636 285.09h160v160h-160z" fill="#fff" />
          <g fill="#f4481e">
            <path d="m349.09 212.364h23.274v46.545h-232.728v-46.545h23.273v-93.091l93.091-58.183 93.09 58.182zm-46.545 0v-67.294l-46.545-29.09-46.545 29.09v67.294z" />
            <path d="m129.303 478.499 39.424-266.135h174.546l39.424 266.135a254.836 254.836 0 0 1 -126.697 33.501 254.836 254.836 0 0 1 -126.697-33.501zm178.817-185.833-109.499 35.574-13.311 89.83 134.889-43.834z" />
          </g>
        </g>
        <path d="m55.273 165.818h174.545v174.546h-174.545z" />
        <g fill-rule="nonzero">
          <path
            d="m186.182 238.545h-17.676c-8.83-25.075-34.455-40.129-60.657-35.632-26.202 4.498-45.341 27.235-45.304 53.82.02 30.118 24.434 54.527 54.552 54.54h69.085c19.725-.5 35.455-16.633 35.455-36.364s-15.73-35.865-35.455-36.364z"
            fill="#fafafa"
          />
          <path
            d="m222.545 275.34v-.43c0-20.074-16.29-36.365-36.363-36.365h-18.188v.908h18.188c19.895.005 36.096 15.993 36.363 35.887z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m117.097 202.182 29.475 93.114c9.856-9.89 25.053-23.529 25.053-38.586-.02-30.114-24.426-54.52-54.54-54.54z"
            fill="#e1e1e1"
          />
          <circle cx="117.097" cy="256.733" fill="#fff" r="54.551" />
          <path
            d="m117.097 203.09c29.946.018 54.284 24.163 54.54 54.109v-.431a54.545 54.545 0 1 0 -109.092-.035v.43c.269-29.938 24.612-54.068 54.552-54.074z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m186.182 310.365h-69.097c-29.946-.018-54.284-24.164-54.54-54.109v.43c.013 30.123 24.43 54.539 54.552 54.552h69.085c20.073 0 36.363-16.291 36.363-36.364v-.442c-.242 19.912-16.45 35.928-36.363 35.933z"
            fill="#212121"
            fill-opacity=".1"
          />
          <path
            d="m256 2.676c140.94 0 255.244 113.885 255.977 254.662l.023-1.338c0-141.382-114.618-256-256-256s-256 114.618-256 256c0 .442.035.873.035 1.338.721-140.765 115.002-254.662 255.965-254.662z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m511.977 254.662c-.745 140.777-115.037 254.662-255.977 254.662-140.951 0-255.244-113.897-255.965-254.662 0 .465-.035.896-.035 1.338 0 141.382 114.618 256 256 256s256-114.618 256-256z"
            fill="#263238"
            fill-opacity=".2"
          />
        </g>
      </g>
    </svg>
  </span>
      </span>

      <div class="lighthouse__scores">
        <span class="lighthouse__col">
          <span class="lighthouse__scoresNo lighthouse__scoresNo--green" aria-label="Score of 98 before">
            98
            <svg class="lighthouse__circle" viewbox="0 0 100 100" width="200" height="200" style="stroke-dasharray: 289.40351524869175 999;">
              <circle cx="50" cy="50" r="47" />  
            </svg>
          </span>
          <p class="lighthouse__type" aria-hidden="true">Before</p>
        </span>
        <span class="lighthouse__col">
          <span class="lighthouse__scoresNo lighthouse__scoresNo--green" aria-label="Score of 99 after">
            99
            <svg class="lighthouse__circle" viewbox="0 0 100 100" width="200" height="200" style="stroke-dasharray: 292.35661234306616 999;">
              <circle cx="50" cy="50" r="47" />  
            </svg>
          </span>
          <p class="lighthouse__type" aria-hidden="true">After</p>
        </span>
      </div>
    </div>
  <h3 class="post__h3">A blog post with a YouTube video embed</h3><p class="post__p">Eleventy has a thriving community plugin ecosystem, and I used <a href="https://www.npmjs.com/package/eleventy-plugin-youtube-embed" target="_blank">eleventy-plugin-youtube-embed</a> to optimise my YouTube video embeds. Using this plugin, third-party YouTube scripts are only loaded if and when someone interacts with the video, rather than loading the JavaScript as soon as the page loads. This greatly improved the performance score from 60 to 98!</p>
    <div class="lighthouse">
      <span class="lighthouse__header">
        <span>
          <p class="lighthouse__title">Google Lighthouse Performance Score Comparison on Mobile</p>
          <a href="https://whitep4nth3r.com/blog/how-to-generate-an-rss-feed-for-your-blog-with-javascript-and-netlify/" target="_blank" class="lighthouse__subTitle">https://whitep4nth3r.com/blog/how-to-generate-an-rss-feed-for-your-blog-with-javascript-and-netlify/</a>
        </span>
        <span aria-hidden="true">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      role="img"
      aria-label="Google Lighthouse"
      height="60"
      width="60"
      viewBox="0 0 512 548.20728733">
      <g fill="none" fill-rule="evenodd">
        <circle cx="256" cy="256" fill="#0535c1" fill-rule="nonzero" r="256" />
        <path d="m311.273 116.364h151.272v151.272h-151.272z" />
        <path
          d="m424.727 179.386h-15.313c-7.62-21.77-29.847-34.855-52.58-30.955s-39.326 23.646-39.254 46.71c.006 26.11 21.17 47.273 47.278 47.28h59.87c16.998-.566 30.485-14.51 30.485-31.518s-13.487-30.951-30.486-31.517z"
          fill="#eaeaea"
          fill-rule="nonzero"
        />
        <path
          d="m456.25 211.293v-.384c-.006-17.407-14.116-31.516-31.523-31.523h-15.755v.791h15.755c17.248-.001 31.294 13.859 31.523 31.104z"
          fill="#fff"
          fill-opacity=".2"
          fill-rule="nonzero"
        />
        <g fill-rule="nonzero">
          <path
            d="m364.858 147.887 25.554 80.71c8.54-8.577 21.713-20.4 21.713-33.444-.006-26.108-21.17-47.272-47.278-47.278z"
            fill="#e1e1e1"
          />
          <circle cx="364.858" cy="195.153" fill="#eee" r="47.279" />
          <path
            d="m364.858 148.666c25.955.012 47.05 20.94 47.267 46.895v-.384c.01-26.108-21.147-47.28-47.255-47.29s-47.28 21.147-47.29 47.255v.384c.236-25.945 21.332-46.855 47.278-46.86z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m424.727 241.63h-59.88c-25.955-.013-47.05-20.942-47.267-46.895v.384c.012 26.104 21.174 47.26 47.278 47.266h59.87c17.402-.006 31.51-14.108 31.522-31.51v-.385c-.216 17.257-14.265 31.134-31.523 31.14z"
            fill="#212121"
            fill-opacity=".1"
          />
          <path d="m186.182 107.636h133.818v116.364h-133.818z" fill="#fff176" />
          <path d="m171.636 285.09h160v160h-160z" fill="#fff" />
          <g fill="#f4481e">
            <path d="m349.09 212.364h23.274v46.545h-232.728v-46.545h23.273v-93.091l93.091-58.183 93.09 58.182zm-46.545 0v-67.294l-46.545-29.09-46.545 29.09v67.294z" />
            <path d="m129.303 478.499 39.424-266.135h174.546l39.424 266.135a254.836 254.836 0 0 1 -126.697 33.501 254.836 254.836 0 0 1 -126.697-33.501zm178.817-185.833-109.499 35.574-13.311 89.83 134.889-43.834z" />
          </g>
        </g>
        <path d="m55.273 165.818h174.545v174.546h-174.545z" />
        <g fill-rule="nonzero">
          <path
            d="m186.182 238.545h-17.676c-8.83-25.075-34.455-40.129-60.657-35.632-26.202 4.498-45.341 27.235-45.304 53.82.02 30.118 24.434 54.527 54.552 54.54h69.085c19.725-.5 35.455-16.633 35.455-36.364s-15.73-35.865-35.455-36.364z"
            fill="#fafafa"
          />
          <path
            d="m222.545 275.34v-.43c0-20.074-16.29-36.365-36.363-36.365h-18.188v.908h18.188c19.895.005 36.096 15.993 36.363 35.887z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m117.097 202.182 29.475 93.114c9.856-9.89 25.053-23.529 25.053-38.586-.02-30.114-24.426-54.52-54.54-54.54z"
            fill="#e1e1e1"
          />
          <circle cx="117.097" cy="256.733" fill="#fff" r="54.551" />
          <path
            d="m117.097 203.09c29.946.018 54.284 24.163 54.54 54.109v-.431a54.545 54.545 0 1 0 -109.092-.035v.43c.269-29.938 24.612-54.068 54.552-54.074z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m186.182 310.365h-69.097c-29.946-.018-54.284-24.164-54.54-54.109v.43c.013 30.123 24.43 54.539 54.552 54.552h69.085c20.073 0 36.363-16.291 36.363-36.364v-.442c-.242 19.912-16.45 35.928-36.363 35.933z"
            fill="#212121"
            fill-opacity=".1"
          />
          <path
            d="m256 2.676c140.94 0 255.244 113.885 255.977 254.662l.023-1.338c0-141.382-114.618-256-256-256s-256 114.618-256 256c0 .442.035.873.035 1.338.721-140.765 115.002-254.662 255.965-254.662z"
            fill="#fff"
            fill-opacity=".2"
          />
          <path
            d="m511.977 254.662c-.745 140.777-115.037 254.662-255.977 254.662-140.951 0-255.244-113.897-255.965-254.662 0 .465-.035.896-.035 1.338 0 141.382 114.618 256 256 256s256-114.618 256-256z"
            fill="#263238"
            fill-opacity=".2"
          />
        </g>
      </g>
    </svg>
  </span>
      </span>

      <div class="lighthouse__scores">
        <span class="lighthouse__col">
          <span class="lighthouse__scoresNo lighthouse__scoresNo--orange" aria-label="Score of 60 before">
            60
            <svg class="lighthouse__circle" viewbox="0 0 100 100" width="200" height="200" style="stroke-dasharray: 177.18582566246434 999;">
              <circle cx="50" cy="50" r="47" />  
            </svg>
          </span>
          <p class="lighthouse__type" aria-hidden="true">Before</p>
        </span>
        <span class="lighthouse__col">
          <span class="lighthouse__scoresNo lighthouse__scoresNo--green" aria-label="Score of 98 after">
            98
            <svg class="lighthouse__circle" viewbox="0 0 100 100" width="200" height="200" style="stroke-dasharray: 289.40351524869175 999;">
              <circle cx="50" cy="50" r="47" />  
            </svg>
          </span>
          <p class="lighthouse__type" aria-hidden="true">After</p>
        </span>
      </div>
    </div>
  <p class="post__p">✅ Improve Lighthouse performance scores — <b class="post__p--bold">success</b>!</p><h2 class="post__h2">Qualitative learnings</h2><p class="post__p">It took a little while to shift my thinking from Next.js to Eleventy. Next.js has a justifiably opinionated way of architecting a front end application — and that&#39;s fine — but in moving to Eleventy, it showed me I had perhaps become too reliant on the patterns of Next.js. In going back to web basics with Eleventy, and focussing on shipping plain HTML, CSS and JavaScript to the browser, I feel like I&#39;ve refreshed and reinvigorated my knowledge of how the web works natively. And this has equipped me to approach my next Next.js project with a different, more informed mindset.</p><h2 class="post__h2">The second system effect</h2><p class="post__p">Reflecting on the journey my blog iterations have taken reminds me of the <a href="https://en.wikipedia.org/wiki/Second-system_effect" target="_blank">Second System Effect</a>. The first implementation wasn&#39;t scaleable enough for me, the second implementation was slightly over-engineered, and the third iteration is just about right — for now — thanks to all I&#39;ve learned along the way!</p><p class="post__p">So, do I recommend you build your next blog site with Eleventy? It depends! </p><h2 class="post__h2">Use the right tool for the job</h2><p class="post__p">People often ask me what front end framework or static site generator they should get started with, and my answer is always — it depends! The most solid advice I can give you is to decide on the features you want, decide how you might need to scale the application (in terms of team size, content management, systems and patterns), and don&#39;t be afraid to try things out. You might not get it right first time, but always endeavour to use the right tool for the job. Try not to over-engineer, and always be mindful of what you&#39;re asking your site visitors to download to their browsers. You can check out a huge list of static site generators over on <a href="https://jamstack.org/generators" target="_blank">Jamstack.org</a>.</p><p class="post__p">Through iterating the technology stack used for my blog, I&#39;ve learned how to use three front end frameworks, how to power a blog using markdown files or a CMS — and how all of these things contribute to the developer experience, and end-user experience of a website. </p><p class="post__p">As always, I would encourage you to try things, ship things, and iterate, iterate, iterate.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to format dates for RSS feeds (RFC-822)</title>
          <description>Here's a selection of links, guidance and code snippets to help you format dates for RSS feeds (RFC-822).</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-format-dates-for-rss-feeds-rfc-822/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-format-dates-for-rss-feeds-rfc-822/</guid>
          <pubDate>Mon, 21 Mar 2022 00:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Web Dev</category><category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I built my first RSS feed back in March 2021. Since I learned all about RSS, I like to find opportunities to build RSS feeds for new projects. However, I often have trouble remembering the valid <code>pubDate</code> format for RSS feeds, and I&#39;ve always found it difficult to Google for the correct date format in plain English (RFC-822). Here&#39;s a selection of links, guidance and code snippets all about the correct date format for RSS feeds.</p><h2 class="post__h2">Validating your RSS feed</h2><p class="post__p">I use the <a href="https://validator.w3.org/feed/" target="_blank">W3C Feed Validation Service</a> to validate RSS feeds, where you can validate an RSS document either by URI or direct XML input.</p><p class="post__p">Here&#39;s what happens when a <code>pubDate</code> is invalid. The validator reports that &quot;<b class="post__p--bold">pubDate must be an RFC-822 date-time</b>&quot;. </p><img src="https://images.ctfassets.net/56dzm01z6lln/1twarE3TSQz7yjcLb5QmhA/638f1725e86994c47e789b6c2342b36d/incorrect_rss.png" alt="A screenshot from the W3C Feed Validation Service that shows the dates on the thing of the day RSS feed are incorrect. The highlighted message states that pubDate myst be an RFC-822 date-time." height="1990" width="3438" /><p class="post__p">There&#39;s a <b class="post__p--bold">help</b> link directly after the warning, <b class="post__p--italic">but this is the part I always miss</b>. And so I end up Googling for &quot;RFC-822 date-time format&quot; over and over and over with no good results — until I finally remember to click the help link 😅. </p><p class="post__p">Maybe I&#39;ll remember to search on my blog next time for the following crucial information below!</p><h2 class="post__h2">Valid RFC-822 date format</h2><p class="post__p">Valid RFC-822 dates should follow this format — <b class="post__p--bold">Wed, 02 Oct 2002 08:00:00 EST</b> — which comprises:</p><ul><li><p class="post__p">Three letter day of the week followed by a comma (e.g. <code>Wed,</code>)</p></li><li><p class="post__p">Day date with leading zero (e.g. <code>02</code>)</p></li><li><p class="post__p">Three letter month of the year (e.g. <code>Oct</code>)</p></li><li><p class="post__p">Full year (e.g. <code>2002</code>)</p></li><li><p class="post__p">Days, minutes, and seconds in 24 hour format (e.g. <code>08:00:00</code>)</p></li><li><p class="post__p">Three letter timezone code OR timezone offset (e.g. <code>GMT</code> or <code>+0200</code>)</p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LsHishePoX"
      aria-describedby="LsHishePoX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LsHishePoX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="xml">
      <meta data-code-id="LsHishePoX" itemprop="text" content="%3CpubDate%3EWed%2C%2002%20Oct%202002%2008%3A00%3A00%20EST%3C%2FpubDate%3E%0A%3CpubDate%3EWed%2C%2002%20Oct%202002%2013%3A00%3A00%20GMT%3C%2FpubDate%3E%0A%3CpubDate%3EWed%2C%2002%20Oct%202002%2015%3A00%3A00%20%2B0200%3C%2FpubDate%3E">
      <pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>pubDate</span><span class="token punctuation">></span></span>Wed, 02 Oct 2002 08:00:00 EST<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>pubDate</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>pubDate</span><span class="token punctuation">></span></span>Wed, 02 Oct 2002 13:00:00 GMT<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>pubDate</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>pubDate</span><span class="token punctuation">></span></span>Wed, 02 Oct 2002 15:00:00 +0200<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>pubDate</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://validator.w3.org/feed/docs/error/InvalidRFC2822Date.html" target="_blank">Read the full help doc on the W3C validation service</a>.</p><h2 class="post__h2">Using JavaScript to build an RFC-822 date</h2><p class="post__p">Here&#39;s how I take a date string and convert it to an RFC-822 date in plain JavaScript with no dependencies.</p><p class="post__p"><b class="post__p--bold">Disclaimer!</b> I save dates in two timezones — GMT or BST — and so <b class="post__p--italic">the code only accounts for those two timezones.</b> You may need to work your own magic for your own timezones.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="UerTutLPPI"
      aria-describedby="UerTutLPPI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="UerTutLPPI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="UerTutLPPI" itemprop="text" content="%2F%2F%20add%20a%20leading%200%20to%20a%20number%20if%20it%20is%20only%20one%20digit%0Afunction%20addLeadingZero(num)%20%7B%0A%20%20const%20numString%20%3D%20num.toString()%3B%0A%20%20while%20(numString.length%20%3C%202)%20num%20%3D%20%220%22%20%2B%20num%3B%0A%20%20return%20num%3B%0A%7D%0A%0Afunction%20buildRFC822Date(dateString)%20%7B%0A%20%20const%20dayStrings%20%3D%20%5B%22Sun%22%2C%20%22Mon%22%2C%20%22Tue%22%2C%20%22Wed%22%2C%20%22Thu%22%2C%20%22Fri%22%2C%20%22Sat%22%5D%3B%0A%20%20const%20monthStrings%20%3D%20%5B%22Jan%22%2C%20%22Feb%22%2C%20%22Mar%22%2C%20%22Apr%22%2C%20%22May%22%2C%20%22Jun%22%2C%20%22Jul%22%2C%20%22Aug%22%2C%20%22Sep%22%2C%20%22Oct%22%2C%20%22Nov%22%2C%20%22Dec%22%5D%3B%0A%0A%20%20const%20timeStamp%20%3D%20Date.parse(dateString)%3B%0A%20%20const%20date%20%3D%20new%20Date(timeStamp)%3B%0A%0A%20%20const%20day%20%3D%20dayStrings%5Bdate.getDay()%5D%3B%0A%20%20const%20dayNumber%20%3D%20addLeadingZero(date.getDate())%3B%0A%20%20const%20month%20%3D%20monthStrings%5Bdate.getMonth()%5D%3B%0A%20%20const%20year%20%3D%20date.getFullYear()%3B%0A%20%20const%20time%20%3D%20%60%24%7BaddLeadingZero(date.getHours())%7D%3A%24%7BaddLeadingZero(date.getMinutes())%7D%3A00%60%3B%0A%20%20const%20timezone%20%3D%20date.getTimezoneOffset()%20%3D%3D%3D%200%20%3F%20%22GMT%22%20%3A%20%22BST%22%3B%0A%0A%20%20%2F%2FWed%2C%2002%20Oct%202002%2013%3A00%3A00%20GMT%0A%20%20return%20%60%24%7Bday%7D%2C%20%24%7BdayNumber%7D%20%24%7Bmonth%7D%20%24%7Byear%7D%20%24%7Btime%7D%20%24%7Btimezone%7D%60%3B%0A%7D%0A%0A%2F%2F%20date%20in%20GMT%20timezone%0Aconst%20exampleOne%20%3D%20buildRFC822Date(%222021-11-29T00%3A00%3A00.000Z%22)%3B%0A%2F%2F%20date%20in%20BST%20timezone%0Aconst%20exampleTwo%20%3D%20buildRFC822Date(%222021-09-08T00%3A00%3A00.000%2B01%3A00%22)%3B%0A%0Aconsole.log(exampleOne)%3B%0Aconsole.log(exampleTwo)%3B%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// add a leading 0 to a number if it is only one digit</span><br><span class="token keyword">function</span> <span class="token function">addLeadingZero</span><span class="token punctuation">(</span><span class="token parameter">num</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> numString <span class="token operator">=</span> num<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">while</span> <span class="token punctuation">(</span>numString<span class="token punctuation">.</span>length <span class="token operator">&lt;</span> <span class="token number">2</span><span class="token punctuation">)</span> num <span class="token operator">=</span> <span class="token string">"0"</span> <span class="token operator">+</span> num<span class="token punctuation">;</span><br>  <span class="token keyword">return</span> num<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">buildRFC822Date</span><span class="token punctuation">(</span><span class="token parameter">dateString</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> dayStrings <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"Sun"</span><span class="token punctuation">,</span> <span class="token string">"Mon"</span><span class="token punctuation">,</span> <span class="token string">"Tue"</span><span class="token punctuation">,</span> <span class="token string">"Wed"</span><span class="token punctuation">,</span> <span class="token string">"Thu"</span><span class="token punctuation">,</span> <span class="token string">"Fri"</span><span class="token punctuation">,</span> <span class="token string">"Sat"</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> monthStrings <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"Jan"</span><span class="token punctuation">,</span> <span class="token string">"Feb"</span><span class="token punctuation">,</span> <span class="token string">"Mar"</span><span class="token punctuation">,</span> <span class="token string">"Apr"</span><span class="token punctuation">,</span> <span class="token string">"May"</span><span class="token punctuation">,</span> <span class="token string">"Jun"</span><span class="token punctuation">,</span> <span class="token string">"Jul"</span><span class="token punctuation">,</span> <span class="token string">"Aug"</span><span class="token punctuation">,</span> <span class="token string">"Sep"</span><span class="token punctuation">,</span> <span class="token string">"Oct"</span><span class="token punctuation">,</span> <span class="token string">"Nov"</span><span class="token punctuation">,</span> <span class="token string">"Dec"</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> timeStamp <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>dateString<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span>timeStamp<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> day <span class="token operator">=</span> dayStrings<span class="token punctuation">[</span>date<span class="token punctuation">.</span><span class="token function">getDay</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> dayNumber <span class="token operator">=</span> <span class="token function">addLeadingZero</span><span class="token punctuation">(</span>date<span class="token punctuation">.</span><span class="token function">getDate</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> month <span class="token operator">=</span> monthStrings<span class="token punctuation">[</span>date<span class="token punctuation">.</span><span class="token function">getMonth</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> year <span class="token operator">=</span> date<span class="token punctuation">.</span><span class="token function">getFullYear</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> time <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">addLeadingZero</span><span class="token punctuation">(</span>date<span class="token punctuation">.</span><span class="token function">getHours</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">addLeadingZero</span><span class="token punctuation">(</span>date<span class="token punctuation">.</span><span class="token function">getMinutes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:00</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> timezone <span class="token operator">=</span> date<span class="token punctuation">.</span><span class="token function">getTimezoneOffset</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token number">0</span> <span class="token operator">?</span> <span class="token string">"GMT"</span> <span class="token operator">:</span> <span class="token string">"BST"</span><span class="token punctuation">;</span><br><br>  <span class="token comment">//Wed, 02 Oct 2002 13:00:00 GMT</span><br>  <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>day<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>dayNumber<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>month<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>year<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>time<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>timezone<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// date in GMT timezone</span><br><span class="token keyword">const</span> exampleOne <span class="token operator">=</span> <span class="token function">buildRFC822Date</span><span class="token punctuation">(</span><span class="token string">"2021-11-29T00:00:00.000Z"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token comment">// date in BST timezone</span><br><span class="token keyword">const</span> exampleTwo <span class="token operator">=</span> <span class="token function">buildRFC822Date</span><span class="token punctuation">(</span><span class="token string">"2021-09-08T00:00:00.000+01:00"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>exampleOne<span class="token punctuation">)</span><span class="token punctuation">;</span><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>exampleTwo<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://github.com/whitep4nth3r/rfc-822" target="_blank">Find the code on GitHub</a> if you&#39;d like to fork the repository and run it. Instructions are in the README.</p><p class="post__p"><b class="post__p--bold">This is your periodic reminder to write down and explore anything you repeatedly struggle with for your future self.</b> Hopefully, after writing this post, building RSS feeds in the future will be plain-sailing for me! </p><p class="post__p">If you&#39;d like to learn more about RSS feeds and how to build one from scratch, check out the post I wrote about it — <a href="https://whitep4nth3r.com/blog/how-to-generate-an-rss-feed-for-your-blog-with-javascript-and-netlify/">How to generate an RSS feed for your blog with JavaScript and Netlify functions</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to delete all merged git branches with one terminal command </title>
          <description>Automate your git cleanup! Here's a shell function to add to your bashrc/zshrc file to delete all merged git branches in one command.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/delete-all-merged-git-branches-one-terminal-command/</link>
          <guid>https://whitep4nth3r.com/blog/delete-all-merged-git-branches-one-terminal-command/</guid>
          <pubDate>Mon, 07 Mar 2022 00:00:00 GMT</pubDate>
          <category>Git</category><category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Are you feeling overwhelmed with too many git branches? Do you know which branches have been merged and which can be safely deleted? Do you leave your git branches to pile up on your local machine until your <code>git branch</code> command spits out one hundred terminal pages of text? There&#39;s no need to live like this!</p><img src="https://images.ctfassets.net/56dzm01z6lln/4C99WNggBiOJxmcI2ljKWp/6eea8c2b69f9b5f64a2a44d1c895e901/lots_of_branches.png" alt="A terminal window running the command git branch, showing a list of seven branches." height="1156" width="2192" /><p class="post__p">Here&#39;s how you can safely delete all merged git branches with one terminal command, using a little shell function.</p><div class="post__callout">
  <h3 class="post__callout__title">Want to delete all squash-merged branches?</h3>
    <div class="post__callout__content">
      <p>Here's a <a href="https://whitep4nth3r.com/blog/how-to-delete-all-squash-merged-local-git-branches-with-one-terminal-command/">blog post with an updated script</a> that deletes all merged and squashed branches. You'll also learn some more fun things about Git!</p>

    </div>
  </div><h2 class="post__h2">The script</h2><p class="post__p">To delete all merged git branches on your local machine:</p><ul><li><p class="post__p">add the following code to your <code>.bashrc</code> or <code>.zshrc</code> file (switch out <code>main</code> for what you usually call your main branches if necessary)</p></li><li><p class="post__p">save the file</p></li><li><p class="post__p">source the new changes (or reload your terminal)</p></li><li><p class="post__p">run <code>dam</code> in a git project directory (dam is my acronym for <b class="post__p--italic">delete all merged</b>)</p></li><li><p class="post__p">and it&#39;s done!</p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vPLcfhaMhe"
      aria-describedby="vPLcfhaMhe">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vPLcfhaMhe">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="vPLcfhaMhe" itemprop="text" content="%23%20delete%20all%20branches%20merged%20into%20main%0Adam()%20%7B%0A%20%20echo%20%22%3D%3D%3D%20Deleting%20all%20merged%20branches%20%3D%3D%3D%22%0A%20%20git%20checkout%20main%20%26%26%20git%20branch%20--merged%20%7C%20grep%20-vE%20%22(%5E%5C*%7Cmain)%22%20%7C%20xargs%20git%20branch%20-d%0A%20%20echo%20%22%E2%98%91%EF%B8%8F%20Done!%22%0A%7D">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># delete all branches merged into main</span><br><span class="token function-name function">dam</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"=== Deleting all merged branches ==="</span><br>  <span class="token function">git</span> checkout main <span class="token operator">&amp;&amp;</span> <span class="token function">git</span> branch <span class="token parameter variable">--merged</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-vE</span> <span class="token string">"(^\*|main)"</span> <span class="token operator">|</span> <span class="token function">xargs</span> <span class="token function">git</span> branch <span class="token parameter variable">-d</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"☑️ Done!"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">In short, the code above checks out the main branch, lists all merged branches, searches for and lists branches that are not named exactly &quot;main&quot;, and uses git to delete those branches on your local machine.</p><p class="post__p">Here&#39;s a detailed breakdown.</p><h3 class="post__h3"><code>git checkout main &amp;&amp; git branch --merged</code></h3><p class="post__p">This command checks out the main branch, and lists all branches that can be detected as being merged into the main branch. <a href="https://git-scm.com/docs/git-branch#Documentation/git-branch.txt---mergedltcommitgt" target="_blank">The official git documentation</a> writes this as &quot;list branches whose tips are reachable from the specified commit (<code>HEAD</code> if not specified)&quot;. We&#39;re not specifying a commit, so the command will use the <code>HEAD</code> — the current local state of the main branch.</p><h3 class="post__h3"><code>| grep -vE &quot;(^\*|main)&quot;</code></h3><p class="post__p">We use <code>grep</code> (or <b class="post__p--italic">extended grep using the E flag</b>) to search using extended regular expressions. The <code>-v</code> flag instructs grep to print lines that <b class="post__p--bold">do not match</b> a given regular expression. The regular expression we&#39;re passing in — <code>&quot;(^\*|main)&quot;</code> — matches the characters &quot;main&quot; from the start of the line. So with the -v flag, <b class="post__p--bold">we&#39;re asking grep to find every branch that isn&#39;t named &quot;main&quot;</b>.</p><p class="post__p">The <code>|</code> (or pipe) at the beginning of the line means we&#39;re &quot;piping&quot; — or sending — the output of the previous command (<code>git branch --merged</code>) into <code>grep</code> to perform the regular expression search. <a href="https://man7.org/linux/man-pages/man1/grep.1.html" target="_blank">Read more about grep</a>.</p><h3 class="post__h3"><code>| xargs git branch -d</code></h3><p class="post__p">Finally, we pipe the result of the <code>egrep</code> search to the <code>xargs</code> command (short for extended arguments). <code>xargs</code> takes input from standard input (basically, a string of text — or a list of git branches in this case), and splits the string where there are spaces into separate arguments that can be used in a command.</p><p class="post__p">For each of the branches returned by <code>egrep</code> above, the script will run <code>git branch -d</code>, which will delete the branch on your local machine.</p><p class="post__p">And here&#39;s the script in action in the terminal. Enjoy!</p><img src="https://images.ctfassets.net/56dzm01z6lln/7LgKuOtEtSgTXTa6rAliYu/84436d7d871cfa0d8a23329c7f9c572e/running_dam.png" alt="A terminal window showing the command dam running. It shows output that results in the branch refactor-youtube-call being deleted." height="980" width="2512" /><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Why you should ship your silly side projects</title>
          <description>Let's take a look at what we learned building a very silly site for the Netlify Dusty Domains project in December 2021.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/why-ship-silly-side-projects/</link>
          <guid>https://whitep4nth3r.com/blog/why-ship-silly-side-projects/</guid>
          <pubDate>Fri, 04 Mar 2022 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In December 2021, I took part in <a href="https://dusty.domains/" target="_blank">Netlify&#39;s Dusty Domains project</a>, which encouraged developers to <b class="post__p--bold">finally</b> ship code to those unused domains they&#39;d been saving for later. For each new site shipped, Netlify and friends donated $500 to a selection of charities, which resulted in a total of $100,000 donated to <a href="https://www.code2040.org/" target="_blank">Code2040</a>, <a href="https://www.resilientcoders.org/" target="_blank">Resilient Coders</a>, <a href="https://www.stemtank.org/" target="_blank">STEMTank</a>, and <a href="https://www.blackgirlscode.com/" target="_blank">Black Girls Code</a>. Great work, team!</p><h2 class="post__h2">I built a silly website</h2><p class="post__p">I really had no idea how my contribution to Dusty Domains was going to turn out. But I knew I wanted to launch <a href="https://whitep4nth3r-live.netlify.app/" target="_blank">whitep4nth3r.live</a>, and I knew I wanted keep things simple. As usual, I built this project <a href="https://twitch.tv/whitep4nth3r" target="_blank">live on Twitch</a> — <b class="post__p--bold">and it turned out to be a little bit silly.</b> But silly websites and silly ideas are often some of the most creative vehicles for learning. And that&#39;s what I want to talk about in this post.</p><h2 class="post__h2">Silly things stick</h2><p class="post__p">I remember vividly and fondly a time at primary school — 25 years ago — when we were learning about the effects of alcohol on our bodies. Instead of standing at the front of the class with a serious face — our teacher spent most of the lesson wobbling around the classroom, slurring his speech, and <b class="post__p--bold">generally being silly</b> — all whilst delivering the teaching material. I don&#39;t think I&#39;ll ever forget this lesson, and I always drew on this approach for inspiration when I was a classroom music teacher.</p><h2 class="post__h2">Learning points in whitep4nth3r.live</h2><p class="post__p"><a href="https://whitep4nth3r-live.netlify.app/" target="_blank">whitep4nth3r.live</a> is a raw GeoCities throwback. It&#39;s ugly, it&#39;s kitsch, and it doesn&#39;t really offer much functionality, apart from watching my Twitch stream when I&#39;m live. It includes a CSS-simulated scrolling marquee with a randomised visitor counter, a custom mouse pointer, custom scrollbars, and a tiled GIF background.</p><img src="https://images.ctfassets.net/56dzm01z6lln/EOnCkf87xYkWGAWJTbUb5/91557f25fd579f1291c3df97fbfe8f1e/whitep4nth3rlive.png" alt="A screenshot of whitep4nth3r.live, showing a terrible under construction GIF, a sparkly background, a twitch player embed, and a simulated scrolling marquee." height="2382" width="3672" /><p class="post__p">And the best part is — it scores all greens on Google Lighthouse, proving that <b class="post__p--bold">even silly things can be built </b><u><b class="post__p--bold">well</b></u><b class="post__p--bold"> for the web</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4Qzgbb5nEvoCncsEo9X06a/0bf59c0d6f35aec6f94f566686d6cccc/whitep4nth3rlivelightout.png" alt="The Google Lighthouse scores for whitep4nth3r.live, showing 97 performance, 100 accessibility, 100 best practises, and 100 SEO." height="736" width="3452" /><p class="post__p">So what important things did we consider during the build that helped us nail these scores? Let&#39;s take a look at a few examples.</p><h3 class="post__h3">prefers-color-scheme</h3><p class="post__p"><a href="https://whitep4nth3r-live.netlify.app/" target="_blank">whitep4nth3r.live</a> shipped with light and dark colour schemes. I&#39;m not a fan of dark and light mode toggles on websites, but rather I prefer to implement dark and light modes based on system settings — <b class="post__p--bold">that is the toggle!</b> To respect user system settings, we can use the CSS media query <code>prefers-color-scheme</code> together with CSS variables (also called custom properties) to build light and dark theme palettes. </p><p class="post__p">In <a href="https://github.com/whitep4nth3r/whitep4nth3r.live/blob/64d52970e9b73f1763cd1551e7d837ec3e67618d/style.css#L32" target="_blank">style.css</a>, I defined CSS variables (prefixed with <code>--</code>) for the colour theming in the document <code>:root</code> (<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:root" target="_blank">read more about the root element on MDN</a>) — a few colours, a border style, and a background image. I chose a dark theme as default, and adapted the CSS variables in the <code>prefers-color-scheme: light</code> media query for a light theme. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="jmnmNhHeWh"
      aria-describedby="jmnmNhHeWh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="jmnmNhHeWh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="jmnmNhHeWh" itemprop="text" content="%2F*%20style.css%20*%2F%0A%0A%3Aroot%20%7B%0A%20%20--color-bg%3A%20%23000000%3B%0A%20%20--color-fg%3A%20%23ffffff%3B%0A%20%20--color-bs%3A%20%23ffb626%3B%0A%20%20--color-sb%3A%20%23f11012%3B%0A%0A%20%20--border-style%3A%20groove%3B%0A%20%20--bg-tile%3A%20%22.%2Fbg_stars.gif%22%3B%0A%7D%0A%0A%40media%20(prefers-color-scheme%3A%20light)%20%7B%0A%20%20%3Aroot%20%7B%0A%20%20%20%20--color-bg%3A%20%23ffffff%3B%0A%20%20%20%20--color-fg%3A%20%230f111a%3B%0A%20%20%20%20--color-bs%3A%20%23f11012%3B%0A%20%20%20%20--color-sb%3A%20%23ffb626%3B%0A%0A%20%20%20%20--border-style%3A%20solid%3B%0A%20%20%20%20--bg-tile%3A%20%22.%2Fbg_squiggle.gif%22%3B%0A%20%20%7D%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token comment">/* style.css */</span><br><br><span class="token selector">:root</span> <span class="token punctuation">{</span><br>  <span class="token property">--color-bg</span><span class="token punctuation">:</span> #000000<span class="token punctuation">;</span><br>  <span class="token property">--color-fg</span><span class="token punctuation">:</span> #ffffff<span class="token punctuation">;</span><br>  <span class="token property">--color-bs</span><span class="token punctuation">:</span> #ffb626<span class="token punctuation">;</span><br>  <span class="token property">--color-sb</span><span class="token punctuation">:</span> #f11012<span class="token punctuation">;</span><br><br>  <span class="token property">--border-style</span><span class="token punctuation">:</span> groove<span class="token punctuation">;</span><br>  <span class="token property">--bg-tile</span><span class="token punctuation">:</span> <span class="token string">"./bg_stars.gif"</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> light<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>  <span class="token selector">:root</span> <span class="token punctuation">{</span><br>    <span class="token property">--color-bg</span><span class="token punctuation">:</span> #ffffff<span class="token punctuation">;</span><br>    <span class="token property">--color-fg</span><span class="token punctuation">:</span> #0f111a<span class="token punctuation">;</span><br>    <span class="token property">--color-bs</span><span class="token punctuation">:</span> #f11012<span class="token punctuation">;</span><br>    <span class="token property">--color-sb</span><span class="token punctuation">:</span> #ffb626<span class="token punctuation">;</span><br><br>    <span class="token property">--border-style</span><span class="token punctuation">:</span> solid<span class="token punctuation">;</span><br>    <span class="token property">--bg-tile</span><span class="token punctuation">:</span> <span class="token string">"./bg_squiggle.gif"</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">You might expect to see a larger palette of colours in a &quot;more serious&quot; project, but this isn&#39;t always the case. Take, for example, <a href="https://github.com/whitep4nth3r/mk2-p4nth3rblog/blob/260172ab6bad770ccb8531f8ccfc631b27c8977d/src/_sass/_root.scss#L51" target="_blank">my colour theming for this website</a> — whitep4nth3r.com — where in the <code>prefers-color-scheme: light </code>media query, I only need to change five colours to switch the theme!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ExDTTcuzBh"
      aria-describedby="ExDTTcuzBh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ExDTTcuzBh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="ExDTTcuzBh" itemprop="text" content="%40media%20(prefers-color-scheme%3A%20light)%20%7B%0A%20%20%3Aroot%20%7B%0A%20%20%20%20--color-bg%3A%20var(--white)%3B%0A%20%20%20%20--color-bg-lighter%3A%20var(--white)%3B%0A%20%20%20%20--color-fg%3A%20var(--black)%3B%0A%20%20%20%20--color-link%3A%20var(--color-fg)%3B%0A%20%20%20%20--color-highlight%3A%20var(--red)%3B%0A%20%20%7D%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">prefers-color-scheme</span><span class="token punctuation">:</span> light<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>  <span class="token selector">:root</span> <span class="token punctuation">{</span><br>    <span class="token property">--color-bg</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--white<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">--color-bg-lighter</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--white<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">--color-fg</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--black<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">--color-link</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--color-fg<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">--color-highlight</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--red<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" target="_blank">Read more about prefers-color-scheme on MDN</a>.</p><p class="post__p">And of course, we had to make sure that both the dark and light themes passed colour contrast checks for accessibility. Check out this blog post — <a href="https://whitep4nth3r.com/blog/how-to-make-your-code-blocks-accessible-on-your-website/">How to make your code blocks accessible on your website</a> — if you&#39;d like to find out more about how I check for accessibility and colour contrast.</p><h3 class="post__h3">prefers-reduced-motion</h3><p class="post__p">In keeping with the throwback GeoCities theme, we built a fake scrolling marquee with CSS, using the following CSS keyframe animation.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="GeZLJiECRN"
      aria-describedby="GeZLJiECRN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="GeZLJiECRN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="GeZLJiECRN" itemprop="text" content="%40keyframes%20marquee%20%7B%0A%20%200%25%20%7B%0A%20%20%20%20-webkit-transform%3A%20translateX(100%25)%3B%0A%20%20%20%20transform%3A%20translateX(100%25)%3B%0A%20%20%7D%0A%20%20100%25%20%7B%0A%20%20%20%20-webkit-transform%3A%20translateX(-100%25)%3B%0A%20%20%20%20transform%3A%20translateX(-100%25)%3B%0A%20%20%7D%0A%7D%0A%0A.fakeMarquee%20%7B%0A%20%20%2F*%20other%20styles%20*%2F%0A%20%20animation%3A%20marquee%2060s%20infinite%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@keyframes</span> marquee</span> <span class="token punctuation">{</span><br>  <span class="token selector">0%</span> <span class="token punctuation">{</span><br>    <span class="token property">-webkit-transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span>100%<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span>100%<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br>  <span class="token selector">100%</span> <span class="token punctuation">{</span><br>    <span class="token property">-webkit-transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span>-100%<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span>-100%<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.fakeMarquee</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* other styles */</span><br>  <span class="token property">animation</span><span class="token punctuation">:</span> marquee 60s infinite<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Animations and motion can be great — but some people may prefer to minimise the amount of non-essential motion that happens on a website. We can use the <code>prefers-reduced-motion</code> media query to detect this setting. Now, <code>prefers-reduced-motion</code> doesn&#39;t always mean <b class="post__p--italic">no motion</b>, but in the case of fake scrolling marquee, we chose to display it statically at the centre of the page when this setting is detected.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="InuGSSgZPm"
      aria-describedby="InuGSSgZPm">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="InuGSSgZPm">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="InuGSSgZPm" itemprop="text" content="%40media%20(prefers-reduced-motion)%20%7B%0A%20%20.fakeMarkquee%20%7B%0A%20%20%20%20%2F*%20other%20styles%20*%2F%0A%20%20%20%20animation%3A%20unset%3B%0A%20%20%7D%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span>prefers-reduced-motion<span class="token punctuation">)</span></span> <span class="token punctuation">{</span><br>  <span class="token selector">.fakeMarkquee</span> <span class="token punctuation">{</span><br>    <span class="token comment">/* other styles */</span><br>    <span class="token property">animation</span><span class="token punctuation">:</span> unset<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion" target="_blank">Read more about prefers-reduced-motion on MDN</a>.</p><h3 class="post__h3">You don&#39;t necessarily need a framework</h3><p class="post__p"><a href="https://whitep4nth3r-live.netlify.app/" target="_blank">whitep4nth3r.live</a> is built with no frameworks — just good old HTML, CSS and JavaScript. Whilst frameworks can provide you with, well, a <b class="post__p--italic">framework</b> with which to build your projects— they&#39;re really not always necessary. I love building with no frameworks. It&#39;s liberating! And it helps you learn so much more about native browser APIs, empowering you to build even better when you finally do move on to using a framework. </p><h2 class="post__h2">Go on, build your silly project!</h2><p class="post__p">I had used <code>prefers-reduced-motion</code> and <code>prefers-color-scheme</code> before building this project, but the simplicity of the implementation on <a href="https://whitep4nth3r-live.netlify.app/" target="_blank">whitep4nth3r.live</a> coupled with the silly GeoCities-centric design really made these concepts stick for me — and the viewers in the live stream. And this is all at the heart of why I urge you all to build stuff, learn things and <b class="post__p--bold">love what you do</b>. If you&#39;re not loving it, you&#39;re not having fun. <b class="post__p--bold"><b class="post__p--italic">And if you&#39;re not having fun... what are you even doing?</b></b> 🤪</p><p class="post__p">Take a look at the <a href="https://github.com/whitep4nth3r/whitep4nth3r.live" target="_blank">source code on GitHub for whitep4nth3r.live</a> for inspiration. And next time you&#39;ve got an idea for a project that might be a bit silly — go ahead and build it. You never know what you might learn!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>A prototype is all you need to launch a site</title>
          <description>Catch up on a Twitch live stream where I prototyped Women of Jamstack with 11ty and YOLO deployed it to Netlify on a custom domain.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/women-of-jamstack-prototype-eleventy/</link>
          <guid>https://whitep4nth3r.com/blog/women-of-jamstack-prototype-eleventy/</guid>
          <pubDate>Tue, 01 Mar 2022 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p"><a href="https://womenofjamstack.com" target="_blank">Women of Jamstack</a> is officially launching on International Women&#39;s Day 2022, and celebrates the women that are working with the Jamstack and adjacent technologies. The <a href="https://github.com/whitep4nth3r/womenofjamstack" target="_blank">code on GitHub</a> is open source, and naturally, I&#39;ve been building the project <a href="https://twitch.tv/whitep4nth3r" target="_blank">live on Twitch</a>.</p><p class="post__p">When I shipped Women of Jamstack to the custom domain <b class="post__p--bold">it had no CSS</b>. No design. And no fancy JavaScript interactivity. <b class="post__p--italic">It didn&#39;t need it</b>. Yes, the design came later. Yes, the other fancy stuff came later. But for launch — an idea is all you need.</p><p class="post__p">I&#39;m a huge fan of building simple prototypes, testing out concepts, and shipping them as early as possible to get community feedback and validate ideas. <b class="post__p--bold">And I urge you to do the same.</b> Next time you&#39;re reluctant to release because something isn&#39;t quite finished yet, or not everything is pixel-perfect — just push the button. It&#39;s exciting, it&#39;s empowering — and it&#39;s meaningful.</p><p class="post__p">I&#39;m not sure if I invented the phrase or not — but I love a good <b class="post__p--italic">YOLO deploy</b>.</p><h2 class="post__h2">Catch up on the prototype build and release on YouTube now!</h2>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/P8Kj6RxnDKw"
        title="Building with Eleventy — prototyping Women of Jamstack — launching on IWD 2022"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Build a CMS preview workflow for your Jamstack site</title>
          <description>Learn how to preview your draft content stored in Contentful by building a custom app that builds a preview branch of your static site.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/previewing-posts-best-decoupled-content-management-workflow-for-your-static-site/</link>
          <guid>https://www.netlify.com/blog/previewing-posts-best-decoupled-content-management-workflow-for-your-static-site/</guid>
          <pubDate>Tue, 22 Feb 2022 00:00:00 GMT</pubDate>
          <category>Tutorials</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Static sites powered by data from a CMS are fantastic. Manage your content in a tidy UI. Publish your content. Kick off a new build with a build hook, and bam! Your new blog post is live in a matter of minutes, served as a static asset from a CDN. But what if you’d like to <b class="post__p--bold">preview</b> your new blog post without pushing the publish button — on a shareable URL, as a static asset, served from a CDN? Because we all make typos once in a while, right?</p><p class="post__p">The good news is this is entirely possible on Netlify using a combination of build hooks, branch deploys, and a bit of custom code. <b class="post__p--bold">In theory, this approach could work for any static site, hosted on any platform, powered by any CMS!</b> And in this article we’re going to learn how to preview your draft content stored in <a href="https://www.contentful.com/developers" target="_blank">Contentful</a> by building a custom Contentful app, which builds a preview branch of your static site.</p><p class="post__p">To make our system for content previews possible, we’ll do five things. We will:</p><ul><li><p class="post__p">Create a new branch deploy in Netlify that we’ll use to preview draft content</p></li><li><p class="post__p">Create a build hook URL to deploy the preview branch to a live URL</p></li><li><p class="post__p">Create a new custom Contentful app to trigger the build hook</p></li><li><p class="post__p">Deploy your new Contentful app to Netlify</p></li><li><p class="post__p">Automatically keep your preview branch up to date with changes in production with a bonus GitHub Action</p></li></ul><p class="post__p">Ready to dig in? Great, let’s get into the details. But before we get started, this article assumes you are familiar with building a static site with data fetched from a CMS at build-time, and:</p><ul><li><p class="post__p">You fetch data from Contentful and build the content to a static site,</p></li><li><p class="post__p">You host your static site on Netlify,</p></li><li><p class="post__p">You want to preview draft content in Contentful without deploying to production,</p></li><li><p class="post__p">And you are familiar with the concept of a <a href="https://www.contentful.com/developers/docs/extensibility/app-framework/" target="_blank">Contentful app</a>.</p></li></ul><p class="post__p">Let’s get to it.</p><h2 class="post__h2">Configure the preview branch</h2><p class="post__p">We’re going to create a new branch from our production branch that will fetch our draft content and be deployed separately to Netlify as a <a href="https://www.netlify.com/blog/2021/12/05/unlimited-environments-thanks-to-branch-deploys/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=blog-branch-deploys-announce" target="_blank">branch deploy</a>. Think of this as a separate <b class="post__p--bold">environment</b> to production. You might be familiar with the concept of a staging environment, for example, that development teams used to test changes on a live URL.</p><p class="post__p">The first step is to configure a preview branch that will fetch draft content from Contentful. Using Git, checkout a new branch from main (I called mine <code>preview</code>) and let’s modify the code that fetches data from Contentful to use the <a href="https://www.contentful.com/developers/docs/references/content-preview-api/" target="_blank">preview API</a>. This will be unique to your situation depending on whether you’re using the GraphQL API, or a client library that uses the REST API.</p><p class="post__p">If you’re using the GraphQL API, pass in <code>preview: true</code> to the Contentful query.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="daBGGlRoEz"
      aria-describedby="daBGGlRoEz">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="daBGGlRoEz">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="daBGGlRoEz" itemprop="text" content="query%20%7B%0A%20%20blogPostCollection(preview%3A%20true)%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20title%0A%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20publishedVersion%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token keyword">query</span> <span class="token punctuation">{</span><br>  <span class="token property-query">blogPostCollection</span><span class="token punctuation">(</span><span class="token attr-name">preview</span><span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token property">title</span><br>      <span class="token object">sys</span> <span class="token punctuation">{</span><br>        <span class="token property">publishedVersion</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">To confirm that the code is returning draft content, you can inspect <code>sys.publishedVersion</code> on each item, which will return null for unpublished content. Here’s a screenshot from the Contentful GraphQL Playground App to demonstrate.</p><img src="https://images.ctfassets.net/56dzm01z6lln/54oWD6oF1vIDeekrYflMYF/b84388d56f969a0e20ca542de6db1fcb/showing_unpublished_gql_explorer.png" alt="A screenshot from the Contentful GraphQL Playground app, showing a GraphQL response with an item that returns sys dot published version as null to confirm it is unpublished and we are receiving draft content using preview: true." height="915" width="1918" /><p class="post__p">Fetch the data from the Contentful GraphQL API in your code using a <b class="post__p--bold">Content Preview Access Token</b>, which you can find in <b class="post__p--bold">Settings &gt; API Keys</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3Y0kT3ifz30vTjZoLXOUMR/958300ea73a12fdedadb3c291862c1dd/api_tokens_view.png" alt="A screenshot from the Contentful Settings, API Keys area, showing the obfuscated preview token highlighted with a yellow rectangle." height="860" width="1049" /><p class="post__p">Make sure to add the access token as an environment variable to Netlify, so you’re not storing a secret in the code. If you’re using the <a href="https://cli.netlify.com/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=cli-docs" target="_blank">Netlify CLI</a>, you can add new environment variables from the command line — no <code>.env</code> file required! Running the app locally with <code>netlify dev</code> will inject environment variables stored against your Netlify site.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="XrxHbUxVEB"
      aria-describedby="XrxHbUxVEB">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="XrxHbUxVEB">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="XrxHbUxVEB" itemprop="text" content="netlify%20env%3Aset%20CONTENTFUL_PREVIEW_ACCESS_TOKEN%20your_access_token_value">
      <pre class="language-bash"><code class="language-bash">netlify env:set CONTENTFUL_PREVIEW_ACCESS_TOKEN your_access_token_value</code></pre>
    </div>
  </div>

  <p class="post__p">Here’s a short snippet showing how to call the Contentful GraphQL API for your preview content using JavaScript <code>fetch</code>. This is just an example, and the final implementation will be unique to you.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="EiJqydYWLY"
      aria-describedby="EiJqydYWLY">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="EiJqydYWLY">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="EiJqydYWLY" itemprop="text" content="async%20function%20getPreviewContent()%20%7B%0A%20%20const%20query%20%3D%20%60query%20%7B%0A%20%20%20%20blogPostCollection(preview%3A%20true)%20%7B%0A%20%20%20%20%20%20items%20%7B%0A%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%60%3B%0A%0A%20%20const%20data%20%3D%20await%20fetch(%60https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%24%7BCONTENTFUL_SPACE_ID%7D%60%2C%20%7B%0A%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20Authorization%3A%20%60Bearer%20%24%7BCONTENTFUL_PREVIEW_ACCESS_TOKEN%7D%60%2C%0A%20%20%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%20%20body%3A%20JSON.stringify(%7B%20query%20%7D)%2C%0A%20%20%7D).then((response)%20%3D%3E%20response.json())%3B%0A%0A%20%20return%20data%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getPreviewContent</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">query {<br>    blogPostCollection(preview: true) {<br>      items {<br>        title<br>      }<br>    }<br>  }</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://graphql.contentful.com/content/v1/spaces/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CONTENTFUL_SPACE_ID</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">CONTENTFUL_PREVIEW_ACCESS_TOKEN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> data<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">If you’re using the Contentful REST API via the JavaScript client library, modify your call to <code>contentful.createClient</code> to use the preview <code>accessToken</code> and preview <code>host</code> to fetch draft content.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fLqZokfMdQ"
      aria-describedby="fLqZokfMdQ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fLqZokfMdQ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="fLqZokfMdQ" itemprop="text" content="const%20contentful%20%3D%20require('contentful')%0A%0Aconst%20client%20%3D%20contentful.createClient(%7B%0A%20%20space%3A%20'%3Cspace_id%3E'%2C%0A%20%20accessToken%3A%20'%3Ccontent_preview_api_key%3E'%2C%0A%20%20host%3A%20'preview.contentful.com'%0A%7D)">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> contentful <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'contentful'</span><span class="token punctuation">)</span><br><br><span class="token keyword">const</span> client <span class="token operator">=</span> contentful<span class="token punctuation">.</span><span class="token function">createClient</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  <span class="token literal-property property">space</span><span class="token operator">:</span> <span class="token string">'&lt;space_id>'</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">accessToken</span><span class="token operator">:</span> <span class="token string">'&lt;content_preview_api_key>'</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">host</span><span class="token operator">:</span> <span class="token string">'preview.contentful.com'</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre>
    </div>
  </div>

  <p class="post__p">For more guidance on how to fetch draft content from Contentful using your particular client library or language, <a href="https://www.contentful.com/developers/docs/references/content-preview-api/" target="_blank">visit the official documentation</a>. Once you’ve configured your new preview branch, commit your code and push up that branch.</p><h2 class="post__h2">Set up a new branch deploy in Netlify</h2><p class="post__p">By default, Netlify deploys your site’s <b class="post__p--bold">production</b> branch after every change. A branch deploy allows you to deploy additional branches as you push changes, as well!</p><p class="post__p">In the Netlify UI, select your site on the Sites page. Then go to <b class="post__p--bold">Site settings &gt; Build &amp; deploy &gt; Continuous Deployment &gt; Branches</b>, and select <b class="post__p--bold">Edit settings</b>. Select <b class="post__p--bold">Let me add individual branches</b>, and below <b class="post__p--bold">Additional branches</b>, add the name of your preview branch.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4K5twuBpJrdD9lP0KpYs4g/85c4b5d9900c1b61a83b5055a79050ba/add_preview_branch_deploy.png" alt="A screenshot from the Netlify UI, showing adding a branch deploy for a branch named preview." height="652" width="884" /><h2 class="post__h2">Create a build hook for your preview branch</h2><p class="post__p">Build hooks are URLs you can use to trigger new builds and deploys. If you’re already deploying your static on Netlify site each time you publish to Contentful, you’ve probably already set up a deploy hook for your production branch.</p><p class="post__p">Navigate to <b class="post__p--bold">Site settings &gt; Build &amp; deploy &gt; Continuous deployment &gt; Build hooks</b>, and click on <b class="post__p--bold">Add build hook</b>. Give your new build hook a name (I chose <b class="post__p--bold">Contentful preview</b>), choose your preview branch from the dropdown, and click <b class="post__p--bold">Save</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2w5PzkUke1K0IOZsRkAMdH/76a405ab81b6222f62c5942b2ad3e6b7/add_build_hook_for_preview.png" alt="A screenshot from the Netlify UI showing how to add a new build hook. The build hook name is Contentful preview and the branch to build has been typed in as preview." height="447" width="878" /><p class="post__p">Your new build hook for your preview branch is now configured, and we’ll come back to grab this value later.</p><img src="https://images.ctfassets.net/56dzm01z6lln/36l35jCfjl18KP3ukODN8e/752e5fd0a166c270cbe095f02e7fa8fb/new_build_hook.png" alt="A screenshot from Netlify of two build hooks, named Contentful deploy and Contentful preview." height="411" width="885" /><p class="post__p">Now we’ve created a preview branch to fetch draft content, set up a branch deploy, and configured a new build hook, the next step is to create a custom Contentful app that will deploy the preview branch at the click of a button.</p><h2 class="post__h2">Create a new Contentful app</h2><p class="post__p">Contentful apps are React apps that you can use to add custom functionality to your Contentful space. The <a href="https://www.contentful.com/developers/docs/extensibility/app-framework/" target="_blank">Contentful App Framework</a> also comes with its own design system and components — Forma 36 — that you can use to speed up development, and ensure the app’s look and feel fits with the style of the Contentful UI.</p><p class="post__p">We’re going to create a new Contentful app that will trigger the preview branch build hook at the click of a button — and we’re also going to host this on Netlify! If you’re not already familiar with Contentful apps and you’d like some guidance on getting started, <a href="https://www.contentful.com/developers/docs/extensibility/app-framework/tutorial/" target="_blank">check out this tutorial from Contentful</a> that takes you through building your first app from scratch.</p><p class="post__p">To create a new Contentful app, open up your terminal and use the following command. This will bootstrap a new Contentful app and install all dependencies for you. You can choose any name you like for your new app, I chose <b class="post__p--bold">netlify-preview-contentful-app</b>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="swdgEpfIZY"
      aria-describedby="swdgEpfIZY">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="swdgEpfIZY">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="swdgEpfIZY" itemprop="text" content="npx%20create-contentful-app%20init%20netlify-preview-contentful-app">
      <pre class="language-bash"><code class="language-bash">npx create-contentful-app init netlify-preview-contentful-app</code></pre>
    </div>
  </div>

  <h3 class="post__h3">Add your Contentful app to Netlify</h3><p class="post__p">Before we write any custom code, let’s set up the infrastructure. First, push your bootstrapped Contentful app to GitHub/GitLab/BitBucket, and let’s add the app to Netlify using the Netlify CLI.</p><p class="post__p">Run the following command to install the <a href="https://cli.netlify.com/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=cli-docs" target="_blank">Netlify CLI</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fUkKuSABxs"
      aria-describedby="fUkKuSABxs">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fUkKuSABxs">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="fUkKuSABxs" itemprop="text" content="npm%20install%20netlify-cli%20-g">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> netlify-cli <span class="token parameter variable">-g</span></code></pre>
    </div>
  </div>

  <p class="post__p">Using your terminal, run:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="cINwAUYeNX"
      aria-describedby="cINwAUYeNX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="cINwAUYeNX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="cINwAUYeNX" itemprop="text" content="netlify%20init">
      <pre class="language-bash"><code class="language-bash">netlify init</code></pre>
    </div>
  </div>

  <p class="post__p">Select the team for your app, and give it a name. There’s no need to deploy just yet!</p><h3 class="post__h3">Add two environment variables</h3><p class="post__p">We’re going to add two environment variables to our Contentful app on Netlify. Given that this is a React app, all environment variables need to be prefixed with <code>REACT_APP_</code> .</p><p class="post__p">First, add the <code>REACT_APP_NETLIFY_BUILD_HOOK</code> environment variable. The value of this variable is the <b class="post__p--bold">Contentful preview build hook</b> you created earlier.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DwKlBojfIi"
      aria-describedby="DwKlBojfIi">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DwKlBojfIi">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="DwKlBojfIi" itemprop="text" content="netlify%20env%3Aset%20REACT_APP_NETLIFY_BUILD_HOOK%20https%3A%2F%2Fapi.netlify.com%2Fbuild_hooks%2F678910">
      <pre class="language-bash"><code class="language-bash">netlify env:set REACT_APP_NETLIFY_BUILD_HOOK https://api.netlify.com/build_hooks/678910</code></pre>
    </div>
  </div>

  <p class="post__p">Next, if you’d like to create a button to open your Netlify dashboard after you’ve created a preview deploy, add the <code>REACT_APP_NETLIFY_URL</code> environment variable, switching out the value for whichever URL you like.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="poldQvOHgN"
      aria-describedby="poldQvOHgN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="poldQvOHgN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="poldQvOHgN" itemprop="text" content="netlify%20env%3Aset%20REACT_APP_NETLIFY_URL%20https%3A%2F%2Fapp.netlify.com%2Fteams%2Fmyteam%2Foverview">
      <pre class="language-bash"><code class="language-bash">netlify env:set REACT_APP_NETLIFY_URL https://app.netlify.com/teams/myteam/overview</code></pre>
    </div>
  </div>

  <p class="post__p">Now it’s time to install this app to your Contentful space.</p><h2 class="post__h2">Install your app to Contentful</h2><p class="post__p">Follow the instructions on <a href="https://www.contentful.com/developers/docs/extensibility/app-framework/tutorial/" target="_blank">this tutorial from Contentful</a> under “<b class="post__p--bold">Embed your app in the Contentful web app</b>” to install your local app to your Contentful space. The magic of the Contentful App Framework is that you can install locally running apps to your Contentful space whilst you’re developing. Install your app using the URL <code>http://localhost:3000</code>, and we’ll switch that out for the live Netlify URL later.</p><p class="post__p">When creating your <b class="post__p--bold">AppDefinition</b>, name your app “Netlify preview” or similar, make sure to select the <b class="post__p--bold">Entry Sidebar</b> location, and click <b class="post__p--bold">Create</b> at the top right of the screen. Install your app, and make sure to assign your app to a content type (such as <code>blogPost</code>), and configure it to show in the entry editor sidebar. (All instructions for this are in the linked Contentful tutorial!)</p><p class="post__p">When you see an empty “Netlify Preview” app in the sidebar of your entry editor — it’s time to write some code!</p><img src="https://images.ctfassets.net/56dzm01z6lln/6QDsopkiDooVyiw0flvRDf/c91e2e984ac040a8059aee93305f1423/fresh_installed_app.png" alt="A screenshot of the Contentful entry editor, showing an empty app in the sidebar, with the title Netlify Preview, ready for development." height="623" width="1278" /><h2 class="post__h2">Build the Netlify buttons</h2><p class="post__p">In your new Contentful app code, open up the <code>Sidebar.tsx</code> file found in <code>src/components/</code>. We’re going to use two components from Forma 36 to build our app. Add the following to the top of the <code>Sidebar.tsx</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="bIaTPkIyGa"
      aria-describedby="bIaTPkIyGa">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="bIaTPkIyGa">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="bIaTPkIyGa" itemprop="text" content="%2F%2F%20src%2Fcomponents%2FSidebar.tsx%0A%0Aimport%20%7B%20Button%20%7D%20from%20%22%40contentful%2Ff36-button%22%3B%0Aimport%20%7B%20Stack%20%7D%20from%20%22%40contentful%2Ff36-core%22%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// src/components/Sidebar.tsx</span><br><br><span class="token keyword">import</span> <span class="token punctuation">{</span> Button <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@contentful/f36-button"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> Stack <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@contentful/f36-core"</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, let’s set up the function that will trigger the build hook. Anywhere in the file, add the following async function code. When run, this function will send a post request to the build hook you set up for the preview branch — and kick off a deploy!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KJFXIWYSBi"
      aria-describedby="KJFXIWYSBi">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KJFXIWYSBi">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="KJFXIWYSBi" itemprop="text" content="%2F%2F%20src%2Fcomponents%2FSidebar.tsx%0A%0Aasync%20function%20triggerBuildHook()%20%7B%0A%20%20try%20%7B%0A%20%20%20%20await%20fetch(%60%24%7Bprocess.env.REACT_APP_NETLIFY_BUILD_HOOK%7D%60%2C%20%7B%0A%20%20%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20%7D)%3B%0A%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20console.log(e)%3B%0A%20%20%7D%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// src/components/Sidebar.tsx</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">triggerBuildHook</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">REACT_APP_NETLIFY_BUILD_HOOK</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, add two <code>Button</code> components inside a <code>Stack</code> component in the return of the <code>Sidebar</code> function. The first button will run our <code>triggerBuildHook</code> function, and the second (optional) button will open up our Netlify dashboard.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="RBvokNFKqr"
      aria-describedby="RBvokNFKqr">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="RBvokNFKqr">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="RBvokNFKqr" itemprop="text" content="%2F%2F%20src%2Fcomponents%2FSidebar.tsx%0A%0Aconst%20Sidebar%20%3D%20(props%3A%20SidebarProps)%20%3D%3E%20%7B%0A%20%20return%20(%0A%20%20%20%20%3CStack%20flexDirection%3D%22column%22%20spacing%3D%22spacingM%22%3E%0A%20%20%20%20%20%20%3CButton%20size%3D%22medium%22%20isFullWidth%20onClick%3D%7BtriggerBuildHook%7D%3E%0A%20%20%20%20%20%20%20%20Build%20Netlify%20Preview%0A%20%20%20%20%20%20%3C%2FButton%3E%0A%20%20%20%20%20%20%3CButton%20size%3D%22medium%22%20isFullWidth%20as%3D%22a%22%20href%3D%7Bprocess.env.REACT_APP_NETLIFY_URL%7D%20target%3D%22_blank%22%3E%0A%20%20%20%20%20%20%20%20Open%20Netlify%0A%20%20%20%20%20%20%3C%2FButton%3E%0A%20%20%20%20%3C%2FStack%3E%0A%20%20)%3B%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// src/components/Sidebar.tsx</span><br><br><span class="token keyword">const</span> <span class="token function-variable function">Sidebar</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">props</span><span class="token operator">:</span> SidebarProps</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>Stack flexDirection<span class="token operator">=</span><span class="token string">"column"</span> spacing<span class="token operator">=</span><span class="token string">"spacingM"</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span>Button size<span class="token operator">=</span><span class="token string">"medium"</span> isFullWidth onClick<span class="token operator">=</span><span class="token punctuation">{</span>triggerBuildHook<span class="token punctuation">}</span><span class="token operator">></span><br>        Build Netlify Preview<br>      <span class="token operator">&lt;</span><span class="token operator">/</span>Button<span class="token operator">></span><br>      <span class="token operator">&lt;</span>Button size<span class="token operator">=</span><span class="token string">"medium"</span> isFullWidth <span class="token keyword">as</span><span class="token operator">=</span><span class="token string">"a"</span> href<span class="token operator">=</span><span class="token punctuation">{</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">REACT_APP_NETLIFY_URL</span><span class="token punctuation">}</span> target<span class="token operator">=</span><span class="token string">"_blank"</span><span class="token operator">></span><br>        Open Netlify<br>      <span class="token operator">&lt;</span><span class="token operator">/</span>Button<span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>Stack<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Run your app locally with <code>netlify dev</code> to make sure the app picks up the environment variables you set earlier. Now, given that Contentful has access to your app running locally on <code>https://localhost:3000</code>, when you click the “Build Netlify Preview” button, a new Netlify deploy of your preview branch will be triggered! It’s like magic!</p><img src="https://images.ctfassets.net/56dzm01z6lln/jQ0agFvrNnG9PlDjJJT9x/aeebe3b632a6c66f9df00f1cc26691bd/app_in_situ.png" alt="A screenshot of the Contentful entry editor, showing the Netlify preview app in the sidebar which comprises two buttons. The top button is labelled Build Netlify Preview and the bottom button is labeled Open Netlify." height="633" width="1273" /><p class="post__p">Commit and push your Contentful app code, and let’s deploy!</p><h2 class="post__h2">Deploy your Contentful app to Netlify</h2><p class="post__p">Head on over to your terminal. In the root of your Contentful app project, use the following command to deploy the app to Netlify. Your environment variables are all set up, so you’re good to go.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xdoBJjfmHW"
      aria-describedby="xdoBJjfmHW">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xdoBJjfmHW">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="xdoBJjfmHW" itemprop="text" content="netlify%20deploy%20--prod">
      <pre class="language-bash"><code class="language-bash">netlify deploy <span class="token parameter variable">--prod</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Point your Contentful app to the live URL on Netlify</h3><p class="post__p">In the Contentful UI, navigate to <b class="post__p--bold">Apps &gt; Manage Apps</b> and scroll down to find your Netlify Preview app. Click on the three dots to the right and select <b class="post__p--bold">Edit app definition</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4yhum6Bqh59MymRHfCeS2V/6b7ab4781f84b77f3fc132980d5ce7b9/edit_app_config.png" alt="A screenshot of an installed app list from the app area in Contentful, showing the menu item "edit app definition" for the Netlify Preview app." height="382" width="885" /><p class="post__p">Switch out <code>http://localhost:3000</code> for the live URL of your app on Netlify.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2I7FkO51bHjeALRdzlstQc/94404da7e492266ba6354b0a9b83dbb8/new_url_for_app.png" alt="A screenshot of the Netlify Preview app configuration screen, showing I have changed the app URL from localhost to a live URL." height="545" width="871" /><p class="post__p">And you’re done! You have successfully deployed a custom Contentful app that publishes draft content to your preview branch on your static site. Great work!</p><p class="post__p">But we’re not done yet. Here’s some bonus content for you to finish up.</p><h2 class="post__h2">Bonus content: create a GitHub action to keep your preview branch up to date</h2><p class="post__p">The preview branch we created will exist forever — or as long as you want to keep deploying content previews(!) — and won’t be merged into the production branch. However, your production branch code is likely to change, in which case you’ll want to make sure your preview branch stays up to date with production as the code evolves. If this sounds too much like manual hard work, don’t despair! We can automate this!</p><p class="post__p">If your project code is hosted on GitHub, we can write a GitHub Action to automatically merge a production branch back into the preview branch — every time a PR is merged. Huge thanks to Alexander Karlstad for <a href="https://medium.com/@karlstad/create-a-github-actions-workflow-that-auto-merges-the-master-back-to-dev-branch-8b1ebe7009b3" target="_blank">this blog post</a> that provided the boilerplate for this handy addition to the workflow!</p><p class="post__p">At the root of your static site project, create a <code>.github</code> directory, and inside that, create a <code>workflows</code> directory. (Skip this step if you already use GitHub Actions in your project.)</p><p class="post__p">Inside the <code>workflows</code> directory, create a new file. Name it <code>auto-merge.yml</code> and add the following code:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fxcEZLgKUb"
      aria-describedby="fxcEZLgKUb">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fxcEZLgKUb">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="fxcEZLgKUb" itemprop="text" content="name%3A%20Merge%20main%20to%20preview%0Aon%3A%0A%20%20pull_request%3A%0A%20%20%20%20branches%3A%20%5Bmain%5D%0A%20%20%20%20types%3A%20%5Bclosed%5D%0Ajobs%3A%0A%20%20merge-main-to-preview%3A%0A%20%20%20%20if%3A%20github.event.pull_request.merged%20%3D%3D%20true%0A%20%20%20%20timeout-minutes%3A%202%0A%20%20%20%20runs-on%3A%20ubuntu-latest%0A%20%20%20%20steps%3A%0A%20%20%20%20%20%20-%20uses%3A%20actions%2Fcheckout%40v2%0A%20%20%20%20%20%20-%20name%3A%20Set%20Git%20config%0A%20%20%20%20%20%20%20%20run%3A%20%7C%0A%20%20%20%20%20%20%20%20%20%20git%20config%20--local%20user.email%20%22actions%40github.com%22%0A%20%20%20%20%20%20%20%20%20%20git%20config%20--local%20user.name%20%22Github%20Actions%22%0A%20%20%20%20%20%20-%20name%3A%20Merge%20main%20to%20preview%0A%20%20%20%20%20%20%20%20run%3A%20%7C%0A%20%20%20%20%20%20%20%20%20%20git%20fetch%20--unshallow%0A%20%20%20%20%20%20%20%20%20%20git%20checkout%20preview%0A%20%20%20%20%20%20%20%20%20%20git%20pull%0A%20%20%20%20%20%20%20%20%20%20git%20merge%20--no-ff%20main%20-m%20%22Auto-merge%20main%20to%20preview%22%0A%20%20%20%20%20%20%20%20%20%20git%20push">
      <pre class="language-markdown"><code class="language-markdown">name: Merge main to preview<br>on:<br>  pull_request:<br>    branches: [main]<br>    types: [closed]<br>jobs:<br>  merge-main-to-preview:<br>    if: github.event.pull_request.merged == true<br>    timeout-minutes: 2<br>    runs-on: ubuntu-latest<br>    steps:<br>      <span class="token list punctuation">-</span> uses: actions/checkout@v2<br>      <span class="token list punctuation">-</span> name: Set Git config<br>        run: |<br>          git config --local user.email "actions@github.com"<br>          git config --local user.name "Github Actions"<br>      <span class="token list punctuation">-</span> name: Merge main to preview<br>        run: |<br>          git fetch --unshallow<br>          git checkout preview<br>          git pull<br>          git merge --no-ff main -m "Auto-merge main to preview"<br>          git push</code></pre>
    </div>
  </div>

  <p class="post__p">Commit and push the new GitHub Action to GitHub. This will trigger when a pull request to the production branch (main) is closed and merged, and merges the production branch (main) into the preview branch (preview). Switch out <b class="post__p--bold">main</b> and <b class="post__p--bold">preview</b> if your branches are named differently. And just like that, your preview branch is kept up to date!</p><h2 class="post__h2">View the source code</h2><p class="post__p">If you’d like to take a look at the source code for my Contentful app that deploys a preview branch with my draft content, <a href="https://github.com/whitep4nth3r/netlify-preview-contentful-app" target="_blank">head on over to the repo on GitHub</a>.</p><p class="post__p">And that’s a wrap!</p><p class="post__p">Previewing draft content on your static site gives you peace of mind and the confidence to hit that publish button. Happy previewing!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to deploy your Netlify site with an Elgato Stream Deck</title>
          <description>Use a little serverless function to kick off a site build with a button.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/how-to-deploy-your-netlify-site-with-an-elgato-stream-deck/</link>
          <guid>https://www.netlify.com/blog/how-to-deploy-your-netlify-site-with-an-elgato-stream-deck/</guid>
          <pubDate>Tue, 15 Feb 2022 00:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Serverless</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I’m a big fan of my Elgato Stream Deck. Not only is it incredibly useful whilst I’m live streaming, but I also use it to give me quick access to some of the things I do regularly, such as opening particular URLs in my browser or toggling my office lighting. And this got me thinking — what if I could kick off a site build on Netlify at the press of a button? And as it turns out, we can do this with a little serverless function! Let’s take a look.</p><h2 class="post__h2">Create a build hook</h2><p class="post__p">The first thing we’re going to do is create <a href="https://docs.netlify.com/configure-builds/build-hooks/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-build-hooks" target="_blank">a new build hook</a>. Build hooks give you a unique URL you can use to trigger a build with an HTTP POST request.</p><p class="post__p">Head on over to your site settings in Netlify, click on <b class="post__p--bold">Build &amp; deploy</b> and scroll down to <b class="post__p--bold">Build hooks</b>. Click on <b class="post__p--bold">Add build hook</b>, choose a name for your new hook — I chose Function deploy — and click <b class="post__p--bold">Save</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4SOwGFVz76KO6CY7o1E0Pc/bad99cd7ddb123e2bee37595008c0385/deploy_stream_deck_1.png" alt="A screenshot showing adding a new build hook named Function deploy." height="422" width="942" /><p class="post__p">Copy your new build hook to your clipboard for the next part.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1bdcqGUg66qb3YIe7gQkRG/ced32c6a87920355ce057fa07701a5c6/deploy_stream_deck_2.png" alt="A screenshot showing the new build hook has been added." height="334" width="939" /><h2 class="post__h2">Add two new environment variables</h2><p class="post__p">We want to make sure that we add some protection around this deploy mechanism. We don’t want bots and naughty people deploying your site without your knowledge! We’re going to create two new environment variables — or secrets — that only you and your code have access to.</p><h3 class="post__h3">DEPLOY_ME_URL</h3><p class="post__p">First, let’s add the build hook URL secret. You can do this in the Netlify UI by navigating to your site settings &gt; Build &amp; deploy &gt; Environment. Click on <b class="post__p--bold">Add variable</b>, add <code>DEPLOY_ME_URL</code> as the key, and your build hook URL as the value. Click <b class="post__p--bold">Save</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5FpEYpb2fa2RqyhReWquUn/5ccdde35c20db37cf3bafbebdd47f965/deploy_stream_deck_3.png" alt="A screenshot showing the deploy me url has been added into the UI, ready to click save." height="438" width="938" /><p class="post__p">You can also do this using the Netlify CLI. In your terminal, use the following command, making sure to switch out <code>{https://your_build_hook}</code> for the build hook URL you created above.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="HZaJYCJyVr"
      aria-describedby="HZaJYCJyVr">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="HZaJYCJyVr">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="HZaJYCJyVr" itemprop="text" content="netlify%20env%3Aset%20DEPLOY_ME_URL%20%7Bhttps%3A%2F%2Fyour_build_hook%7D">
      <pre class="language-bash"><code class="language-bash">netlify env:set DEPLOY_ME_URL <span class="token punctuation">{</span>https://your_build_hook<span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">DEPLOY_ME_SECRET</h3><p class="post__p">To keep our serverless function secure, we’re going to set it up to run <b class="post__p--bold">only if a particular query parameter is passed into the function on the URL</b>. This string can be anything you choose, but to remain non-guessable it should be similar to a secure password. You can use a random string generator, or build one yourself. </p><p class="post__p">Add your deploy secret as an environment variable using the key <code>DEPLOY_ME_SECRET</code>. Now we’re ready to write some code!</p><h2 class="post__h2">Create the serverless function</h2><p class="post__p">If you don’t have any Netlify functions in your project already, create a functions directory at the root of your project and name it <code>functions</code>. This will automatically be picked up by Netlify when you deploy. Add a new file into this directory, and name it <code>deployme.js</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3oO1XLnbKWWRWkqdlnH0yn/b3d0e390f653fda17c2b84d72e6ada9c/deploy_stream_deck_4.png" alt="A screenshot of a file tree in VSCode showing the functions directory with deployme.js inside." height="144" width="610" /><h3 class="post__h3">Install node-fetch</h3><p class="post__p">Run the following command in your terminal. This will install node-fetch for use in the function.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="PaujHsfxyS"
      aria-describedby="PaujHsfxyS">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="PaujHsfxyS">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="PaujHsfxyS" itemprop="text" content="npm%20install%20node-fetch">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> node-fetch</code></pre>
    </div>
  </div>

  <p class="post__p">Add the following code to <code>deployme.js</code>, and let’s unpack what it does.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="eQKUKClTZc"
      aria-describedby="eQKUKClTZc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="eQKUKClTZc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="eQKUKClTZc" itemprop="text" content="%2F%2F%20%2Ffunctions%2Fdeployme.js%0A%0Aconst%20fetch%20%3D%20require(%22node-fetch%22)%3B%0A%0Aexports.handler%20%3D%20async%20function%20(event%2C%20context)%20%7B%0A%20%20if%20(event.queryStringParameters.secret%20%3D%3D%3D%20process.env.DEPLOY_ME_SECRET)%20%7B%0A%20%20%20%20const%20response%20%3D%20await%20fetch(process.env.DEPLOY_ME_URL%2C%20%7B%0A%20%20%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20statusCode%3A%20200%2C%0A%20%20%20%20%20%20body%3A%20%22Your%20site%20is%20now%20deploying!%20Have%20a%20great%20day!%22%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20statusCode%3A%20403%2C%0A%20%20%20%20%20%20body%3A%20%22Access%20denied!%20Please%20include%20the%20correct%20secret%20URL%20parameter.%22%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /functions/deployme.js</span><br><br><span class="token keyword">const</span> fetch <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"node-fetch"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>queryStringParameters<span class="token punctuation">.</span>secret <span class="token operator">===</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">DEPLOY_ME_SECRET</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">DEPLOY_ME_URL</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">return</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">"Your site is now deploying! Have a great day!"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">403</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">"Access denied! Please include the correct secret URL parameter."</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">The first line of the function checks if the secret query parameter on the URL (<code>?secret=</code>) matches the value you added as the <code>DEPLOY_ME_SECRET</code>. If the secret doesn’t match, the function returns an HTTP 403 (forbidden), and the site doesn’t deploy.</p><p class="post__p">If the secret value matches the environment variable, the function makes a post request to your <code>DEPLOY_ME_URL</code>, tells you to have a nice day, and kicks off a site build in Netlify! </p><p class="post__p">Next, commit and push your code to your repo. Once the code has deployed to Netlify, it’s time to hook up the Stream Deck!</p><h2 class="post__h2">Set up the Stream Deck button</h2><p class="post__p">Head on over to your Stream Deck profile, and drag a new <b class="post__p--bold">Website</b> button to your deck. Name the button whatever you like, and add the following URL, making sure to add your own site domain and <code>DEPLOY_ME_SECRET</code>. You can choose to check &quot;GET request in background&quot; but I prefer to open a browser window for that sweet, sweet feedback.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="OBPvnUIVhK"
      aria-describedby="OBPvnUIVhK">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="OBPvnUIVhK">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="OBPvnUIVhK" itemprop="text" content="https%3A%2F%2F%7Byoursitedomain%7D%2F.netlify%2Ffunctions%2Fdeployme.js%3Fsecret%3D%7BDEPLOY_ME_SECRET%7D">
      <pre class="language-bash"><code class="language-bash">https://<span class="token punctuation">{</span>yoursitedomain<span class="token punctuation">}</span>/.netlify/functions/deployme.js?secret<span class="token operator">=</span><span class="token punctuation">{</span>DEPLOY_ME_SECRET<span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <img src="https://images.ctfassets.net/56dzm01z6lln/1jmYrIskRAt8mU2SNCiRy2/e6daae04da4f6268d32aef5bfba20810/deploy_stream_deck_5.png" alt="A screenshot of a new Stream Deck profile with a new website button added, with the label DEPLOY." height="840" width="952" /><p class="post__p">And you’re done! Whenever you click that button, a new site deploy on Netlify will begin! If you’d like to read more about serverless functions on Netlify, be sure to <a href="https://functions.netlify.com/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-functions" target="_blank">check out the docs</a>, and <a href="https://bsky.app/profile/whitep4nth3r.com" target="_blank">let me know on Bluesky</a> if you’ve found any other fancy uses for a Stream Deck!</p><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I improved your Google Lighthouse SEO score</title>
          <description>Why has Google Lighthouse been penalising us for canonical links on different domains? I set out to solve this conundrum once and for all. </description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/improved-google-lighthouse-seo-score/</link>
          <guid>https://whitep4nth3r.com/blog/improved-google-lighthouse-seo-score/</guid>
          <pubDate>Sun, 13 Feb 2022 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I cross-post blog content regularly on <a href="https://dev.to/whitep4nth3r" target="_blank">DEV.to</a>, and I like to cross-post articles I write for companies to my personal blog site. This means that after a post is published in its original location, I publish it on another domain, word for word. This helps me get my content to more people. Cross-posting is perfectly legitimate, and when you do cross-post, <b class="post__p--bold">you need to let Google know</b> that your content is a duplicate of the original.</p><h2 class="post__h2">How to reference duplicate content</h2><p class="post__p">When cross-posting content to other domains, reference duplicate content with a canonical link in the <code>head</code> tag of the web page, like so.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="GugLQdXKDN"
      aria-describedby="GugLQdXKDN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="GugLQdXKDN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="GugLQdXKDN" itemprop="text" content="%3Clink%20rel%3D%22canonical%22%20href%3D%22https%3A%2F%2Foriginaldomain.com%2Flink%2Fto%2Foriginal-post%22%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>canonical<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://originaldomain.com/link/to/original-post<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">If you run regular Google Lighthouse checks, you might have noticed that <b class="post__p--bold">you were penalised for pointing your canonical link to a different domain</b> where the original content lives! But hold up. Isn&#39;t this what canonical links are for?</p><img src="https://images.ctfassets.net/56dzm01z6lln/3LLS5JkyXypyHhE2g2xG6D/3f4d749c4e446916b6be73996969f2cc/canonical_previous.png" alt="A screenshot of a Google Lighthouse SEO report showing a score of 92% due to a canonical link pointing to a different domain error." height="1139" width="2032" /><h2 class="post__h2">Canonical links on the same domain</h2><p class="post__p">Now, there are perfectly valid reasons to include canonical links that point to the <b class="post__p--bold">same domain</b>. <a href="https://support.google.com/webmasters/answer/10347851?hl=en" target="_blank">Google Search Console</a> tells us:</p><blockquote class="post__blockquote"><p class="post__p">A <b class="post__p--italic">canonical URL </b>is the URL of the best representative page from a group of duplicate pages, according to Google.</p></blockquote><p class="post__p">Why might you have duplicate pages on your site? Take for example an e-commerce site, where your search page URLs might exist in duplicate forms. Without a specified canonical link, Google will choose one (at random, maybe) as canonical.</p><p class="post__p">For the following URL examples, you should set your canonical link to https://shop.com/search.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="UDVjLwdlzn"
      aria-describedby="UDVjLwdlzn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="UDVjLwdlzn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="UDVjLwdlzn" itemprop="text" content="https%3A%2F%2Fshop.com%2Fsearch%3Fbrand%26page%3D1%26filters%3Dcat%3AT%2BShirts%0Ahttps%3A%2F%2Fshop.com%2Fsearch%3Fbrand%26page%3D2%26filters%3Dcat%3AT%2BShirts%0Ahttps%3A%2F%2Fshop.com%2Fsearch%3Fbrand%26page%3D3%26filters%3Dcat%3AT%2BShirts">
      <pre class="language-markup"><code class="language-markup">https://shop.com/search?brand&amp;page=1&amp;filters=cat:T+Shirts<br>https://shop.com/search?brand&amp;page=2&amp;filters=cat:T+Shirts<br>https://shop.com/search?brand&amp;page=3&amp;filters=cat:T+Shirts</code></pre>
    </div>
  </div>

  <h2 class="post__h2">The investigation begins</h2><p class="post__p">I was confused. And I was ashamed that my Lighthouse SEO scores were lower than I thought they should have been! (Oh, gamification, how you taunt me!) And so, I took to Twitter to investigate. Here&#39;s <a href="https://twitter.com/g33konaut/status/1397847634015105032" target="_blank">a thread started by Tamas</a>, reaching out to Martin — a Developer Advocate at Google.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2SH2XPa6Sj5f4t6t7VDL5f/0b12e6ba3ec052b5b40e7390fe1492b2/tweet_1397845994100998158_20241114_142752_via_10015_io.png" alt="Tamas Piros asks on Twitter: Legit question by @whitep4nth3r - I hope you can help @g33konaut. In this article, it's stated that canonical links can be added cross domain. However, there's a lighthouse test that fails such tests ("Document does not have a valid rel=canonical.")" height="1390" width="1832" /><img src="https://images.ctfassets.net/56dzm01z6lln/7gZZSYVTxGzdxEZzSZW2jU/5828b654b2e6d91987394c0d2a0788c1/tweet_1397847634015105032_20241114_143005_via_10015_io.png" alt="Martin Splitt replies to Tamas: While that is absolutely acceptable for Google Search (as documented in the link you shared), Lighthouse is vendor-agnostic and Bing and Yahoo seem to be unhappy about cross-domain canonicals. See this comment explaining that in Lighthouse." height="696" width="1832" /><p class="post__p">Martin suggests that Yahoo and Bing don&#39;t like cross-domain canonical links, which was <a href="https://github.com/GoogleChrome/lighthouse/blob/2d3059bcd676ddf27fccea89425532bde6b5d203/lighthouse-core/audits/seo/canonical.js#L176" target="_blank">referenced in the source code for Lighthouse</a>. I wasn&#39;t happy with this! And so I continued down the rabbit hole, and found a light at the end of the tunnel.</p><h2 class="post__h2">Bing Webmaster to the rescue</h2><p class="post__p">I found the <a href="https://www.bing.com/webmasters/help/webmaster-guidelines-30fba23a" target="_blank">Bing Webmaster Guidelines</a>, and the guidance for canonical links in 2021 stated:</p><blockquote class="post__blockquote"><p class="post__p">Do not reuse content from other sources. It is critical that content on your page must be unique in its final form. If you choose to host content from a third party, either use the canonical tag (rel=&quot;canonical&quot; to identify the original source or use the alternate tag (rel=&quot; alternate&quot;).</p></blockquote><p class="post__p">Given that Bing was recommending <code>rel=&quot;canonical&quot;</code>, and <a href="https://uk.help.yahoo.com/kb/SLN2213.html" target="_blank">Yahoo uses results crawled from Bing</a>, <a href="https://github.com/GoogleChrome/lighthouse/issues/12572" target="_blank">I opened an issue on the Google Lighthouse repo</a> with my findings.</p><h2 class="post__h2">I opened a PR to Google Lighthouse</h2><p class="post__p">After a few months of discussion on the issue, we concluded that the advice for canonical links was indeed, outdated, and that all the major indexers began to support cross origin canonical urls in 2009. This was great news! And so, I opened a <a href="https://github.com/GoogleChrome/lighthouse/pull/13412" target="_blank">pull request to remove the cross-origin check for canonical URLs</a>, which was merged to main in November 2021.</p><h2 class="post__h2">Your scores are now improved</h2><p class="post__p">A few more months of waiting in excitement, and the code change is now available to everyone in Chromium browsers! <b class="post__p--bold">Your SEO scores for pages that use canonical links that point to a different domain are now improved.</b></p><img src="https://images.ctfassets.net/56dzm01z6lln/71TImUXzWaCpPNSefNDHAn/64c9b33783acd6e95b6022024916c5e2/canonical_now.png" alt="A screenshot of Google Lighthouse showing the same article with a canonical link, but this time with an SEO score of 100%." height="2382" width="3680" /><p class="post__p">What&#39;s my one piece of advice after all of this? <b class="post__p--bold">Question everything</b>. You never know what it might lead to. <b class="post__p--italic">You could end up improving the web for everyone!</b></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to build an HTML-only accordion — no JavaScript required!</title>
          <description>You don't need JavaScript to build accordions! Use HTML only and just four lines of code.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-build-html-accordion-no-javascript/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-build-html-accordion-no-javascript/</guid>
          <pubDate>Fri, 11 Feb 2022 00:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Web Dev</category><category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you&#39;ve been watching <a href="https://twitch.tv/whitep4nth3r" target="_blank">my Twitch streams</a> lately, you&#39;ll know I&#39;m currently rebuilding, redesigning and reimagining the full <a href="https://whitep4nth3r.com/" target="_blank">whitep4nth3r.com</a> experience — and I&#39;m trying to do this with <b class="post__p--italic">as little JavaScript as possible</b>. And whilst building the table of contents for the new blog page layout, I discovered a way to build an accordion <b class="post__p--bold">with no JavaScript</b> in just four lines of code! Let&#39;s take a look!</p><h2 class="post__h2">Use the HTML details element</h2><p class="post__p">To build the markup for an HTML accordion, use the <code>&lt;details&gt;</code> element. Use a <code>&lt;summary&gt;</code> tag to provide the title for the accordion. Add your content, and you&#39;re done!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="skQHNNOCzO"
      aria-describedby="skQHNNOCzO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="skQHNNOCzO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="skQHNNOCzO" itemprop="text" content="%3Cdetails%3E%0A%20%20%3Csummary%3ESection%20title%3C%2Fsummary%3E%0A%20%20%3Cp%3EHere%20is%20the%20content!%3C%2Fp%3E%0A%3C%2Fdetails%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>details</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>summary</span><span class="token punctuation">></span></span>Section title<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>summary</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Here is the content!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>details</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Load the open accordion by default</h3><p class="post__p">By default, the <code>&lt;details&gt;</code> element loads in a <b class="post__p--bold">closed</b> state, and I always prefer to not hide content from readers when the page loads. We can load the accordion open by default by adding the <code>open</code> attribute to the <code>&lt;details&gt;</code> element. Perfect!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="tHefgyfiIq"
      aria-describedby="tHefgyfiIq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="tHefgyfiIq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="tHefgyfiIq" itemprop="text" content="%3Cdetails%20open%3E%0A%20%20%3Csummary%3ETitle%3C%2Fsummary%3E%0A%20%20%3Cp%3EHere%20is%20the%20content%20that%20is%20open%20by%20default!%3C%2Fp%3E%0A%3C%2Fdetails%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>details</span> <span class="token attr-name">open</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>summary</span><span class="token punctuation">></span></span>Title<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>summary</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span><span class="token punctuation">></span></span>Here is the content that is open by default!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>details</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Readers can click to close the accordion to minimise clutter if they so wish — especially when on a mobile device. </p><img src="https://images.ctfassets.net/56dzm01z6lln/DJL4sXAwOdDsqXJWPLDU4/4c0a5fed73291f2eea323611f92ba34b/open_close.gif" alt="A video showing the clicking of the table of contents that is opening and closing." height="1080" width="1920" /><h2 class="post__h2">Browser support and accessibility</h2><p class="post__p">At the time of writing this article, there&#39;s <a href="https://caniuse.com/?search=details" target="_blank">full modern browser support for the details element as reported by caniuse.com</a>, apart from Internet Explorer (obviously!) and Opera. </p><p class="post__p">I also confirmed that the <code>&lt;details&gt;</code> element is keyboard-accessible in Chromium, Firefox and Safari. Tab to the element and use space or enter to open and close.</p><h2 class="post__h2">Further reading</h2><p class="post__p">If you&#39;re curious, you can <a href="https://github.com/whitep4nth3r/mk2-p4nth3rblog/blob/17e9eadc3f887c844cc8764779462d7bfe5bfbf8/src/_components/tableOfContents.js#L44" target="_blank">view the source code</a> that creates the full table of contents. <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details" target="_blank">Read more about the details element on MDN</a> and have fun building with HTML!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Debug your CSS layouts with this one simple trick</title>
          <description>Are you battling with layouts in CSS? Use this one line of CSS to help you debug what's up and get you back on the road to success.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/debug-css-layouts/</link>
          <guid>https://whitep4nth3r.com/blog/debug-css-layouts/</guid>
          <pubDate>Sun, 06 Feb 2022 00:00:00 GMT</pubDate>
          <category>CSS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you&#39;ve been watching <a href="https://twitch.tv/whitep4nth3r" target="_blank">my Twitch streams</a> lately, you&#39;ll know I&#39;m currently rebuilding, redesigning and reimagining the full <a href="https://whitep4nth3r.com" target="_blank">whitep4nth3r.com</a> experience. Whilst building the new blog page layout, I came across a <b class="post__p--bold">classic</b> CSS conundrum. </p><p class="post__p">On smaller device sizes, the content was spilling out of the viewport, causing a horizontal scroll, like this:</p><img src="https://images.ctfassets.net/56dzm01z6lln/aZIv7D7qXmVfz4uxZbUJY/f2278fbda78b403bbcd7f9dbcf5db775/viewport_overflow.png" alt="A screenshot of the chromium mobile emulator showing the iPhone SE window on the left. The HTML and CSS inspector is open on the right. The iPhone window shows the content spilling out of the viewport." height="1862" width="3450" /><p class="post__p">Now, I&#39;ve felt this pain many, many times. But there&#39;s no need to lose sleep anymore. Here&#39;s how I debug my page layouts to find out what&#39;s up — <b class="post__p--bold">with just one line of CSS</b>.</p><h2 class="post__h2">This line of CSS will save you</h2><p class="post__p">Add the following code snippet to your CSS file or your browser dev tools. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="RgWkuENdKg"
      aria-describedby="RgWkuENdKg">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="RgWkuENdKg">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="RgWkuENdKg" itemprop="text" content="*%20%7B%0A%20%20outline%3A%201px%20solid%20red%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">*</span> <span class="token punctuation">{</span><br>  <span class="token property">outline</span><span class="token punctuation">:</span> 1px solid red<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">This line of CSS adds a red outline (or whatever colour you choose) to <b class="post__p--bold">every single element</b> (*) on your page. By scrolling through the content, you should be able to see which elements are spilling out of your viewport. Now you can experiment with removing or adding CSS properties in dev tools as needed. </p><img src="https://images.ctfassets.net/56dzm01z6lln/4cY0yOvzpeMhEnbk1tDqzO/4095637362555ebabce5f1978b39a8e1/outline_red.png" alt="A screenshot of the same view as above, but with the CSS rule applied in the CSS dev tools. The iPhone preview window has been updated to show outlines around each HTML element on the page." height="1864" width="3452" /><p class="post__p"><b class="post__p--bold">A quick note: </b>I would always recommend debugging CSS using your browser dev tools rather than going back and forth between updating CSS in your IDE and refreshing your browser. It&#39;s much more time-efficient this way!</p><p class="post__p">In my case, I was able to discover that the <code>section</code> tag with <code>class=&quot;post&quot;</code> was causing the issue. Notice — in the image above — how the red outlines showed the content spilling out of that element! (The fix isn&#39;t important, but if you&#39;re curious, I needed to remove <code>display: grid</code> from the <code>post</code> class at that viewport width.)</p><h2 class="post__h2">Why outline and not border?</h2><p class="post__p">The reason why we use <b class="post__p--bold">outline</b> rather than <b class="post__p--bold">border</b> is all down to how the CSS box model works. Outline widths <b class="post__p--italic">do not affect the layout of elements </b>according to your CSS box-sizing rules — but border widths <b class="post__p--bold">might</b>!</p><p class="post__p">If some elements are set to use <code>box-sizing: border-box</code>, and some elements are using the browser default of <code>box-sizing: content-box</code>, then the layout of those elements will respond differently to different border widths. </p><ul><li><p class="post__p"><code>border-box</code> <b class="post__p--bold"><b class="post__p--italic">includes</b></b> border widths in the calculated width of the element,</p></li><li><p class="post__p"><code>content-box</code> <b class="post__p--bold"><b class="post__p--italic">adds</b></b> border widths to the calculated element width. </p></li></ul><p class="post__p">For a more in-depth look at the CSS box model (and how it will change your life!), <a href="https://whitep4nth3r.com/talks/this-box-will-change-your-life/" target="_blank">check out this talk I did at CodeLand Conf 2021</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to use really long environment variables in Netlify functions</title>
          <description>Here’s how you can use a Netlify build plugin to use longer environment variables in your functions.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.netlify.com/blog/how-to-use-really-long-environment-variables-in-netlify-functions/</link>
          <guid>https://www.netlify.com/blog/how-to-use-really-long-environment-variables-in-netlify-functions/</guid>
          <pubDate>Tue, 18 Jan 2022 00:00:00 GMT</pubDate>
          <category>Tutorials</category><category>NodeJS</category><category>Serverless</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Regardless of what stage you’re at in your development career, you’ve probably given or received this particular piece of advice: do not store secrets and API keys in your code repositories. Instead, use environment variables!</p><p class="post__p">Popular front end JavaScript frameworks such as React, Next.js, Vue.js and Gatsby have built-in support for using environment variables with <code>.env</code> files, and Netlify allows you to manage environment variables for your projects via the Netlify UI, CLI or configuration files. But there is a tiny catch. Due to the limitations of AWS Lambda under the hood, <b class="post__p--bold">stored environment variable values that exceed a maximum length of 256 characters cannot be used in Netlify serverless functions</b>. This might sound like bad news if, for example, you need to store a private key as an environment variable for use in your functions files.</p><p class="post__p">But all is not lost! We can harness the power of a handy Netlify build plugin to allow you to use longer environment variables in your functions files. Let’s take a look.</p><h2 class="post__h2">Prerequisites</h2><p class="post__p">This guide assumes you are familiar with Netlify functions and have configured the location of your Netlify functions folder either in the Netlify UI or with a <code>netlify.toml</code> build configuration file in your repository. If you’re new to Netlify serverless functions, <a href="https://docs.netlify.com/functions/overview/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-func-overview" target="_blank">check out the official documentation to learn more</a>.</p><h2 class="post__h2">Installing the plugin</h2><p class="post__p">We’re going to install the <a href="https://github.com/bencao/netlify-plugin-inline-functions-env" target="_blank">netlify-plugin-inline-functions-env</a> plugin by <b class="post__p--bold">bencao</b>. This will inline build-time environment variables into Netlify function code, making them available at run-time. This build plugin does not affect your source code, edit your environment variables stored in Netlify, or expose your environment variables to a client. All transformed code lives on the Netlify servers and is only changed at build-time when you push a deployment to your site.</p><h3 class="post__h3">Installation via the Netlify UI</h3><p class="post__p">On the Netlify UI dashboard, click on Plugins. Search for “Inline functions environment variables” and press enter. Click the install button next to the plugin in the list.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5sNNm75KxQJ0J0x9c3KRjY/1d9cc2e9b5c60abd565231d88077c94c/image_1.png" alt="A screenshot of the Netlify UI showing the inline functions environment variables in the plugin list." height="732" width="956" /><p class="post__p">Choose which site you’d like to add the plugin to, and confirm.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6BeXVwFbXtbJfaRkQk3abn/02d18425de6e70bf0bb8f98a6f8d5f3c/image_2.png" alt="A screenshot of the next step of the plugin installation process, asking where I would like to install the plugin. In the search box is whitep4nth3r.com showing the UI is ready for me to select my site." height="757" width="945" /><p class="post__p">Technically, you’re now good to go! All environment variables that you use in your Netlify function files will now be inlined at build-time. This means that function code that looks like this in your repository:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YalPpEfujc"
      aria-describedby="YalPpEfujc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YalPpEfujc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="YalPpEfujc" itemprop="text" content="exports.handler%20%3D%20function%20(event%2C%20context)%20%7B%0A%20%20return%20%7B%0A%20%20%20%20statusCode%3A%20200%2C%0A%20%20%20%20body%3A%20JSON.stringify(%7B%0A%20%20%20%20%20%20message%3A%20%22I'm%20inlining%20my%20environment%20variables!%22%2C%0A%20%20%20%20%20%20myEnvVar%3A%20process.env.REALLY_LONG_ENV_VAR%2C%0A%20%20%20%20%7D)%2C%0A%20%20%7D%3B%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript">exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      <span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">"I'm inlining my environment variables!"</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">myEnvVar</span><span class="token operator">:</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">REALLY_LONG_ENV_VAR</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">will be transformed to this at build-time — and stored on Netlify servers — when you push your code to Netlify:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="kDTbFjlQRa"
      aria-describedby="kDTbFjlQRa">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="kDTbFjlQRa">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="kDTbFjlQRa" itemprop="text" content="exports.handler%20%3D%20function%20(event%2C%20context)%20%7B%0A%20%20return%20%7B%0A%20%20%20%20statusCode%3A%20200%2C%0A%20%20%20%20body%3A%20JSON.stringify(%7B%0A%20%20%20%20%20%20message%3A%20%22I'm%20inlining%20my%20environment%20variables!%22%2C%0A%20%20%20%20%20%20myEnvVar%3A%20%22KYwvDpY5yNzMnvHqQMF3pgp7QPNC4rAtrZhnz44RDKBYcyU3JLGRuCGvBHEK38Smu5XkBCNZjyNGWkRLZZX8zUBePeGvnd6krczZ...%22%0A%20%20%20%20%7D)%2C%0A%20%20%7D%3B%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript">exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      <span class="token literal-property property">message</span><span class="token operator">:</span> <span class="token string">"I'm inlining my environment variables!"</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">myEnvVar</span><span class="token operator">:</span> <span class="token string">"KYwvDpY5yNzMnvHqQMF3pgp7QPNC4rAtrZhnz44RDKBYcyU3JLGRuCGvBHEK38Smu5XkBCNZjyNGWkRLZZX8zUBePeGvnd6krczZ..."</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">However, you might want more control over which environment variables are transformed. Let’s look at how we can do that with a Netlify configuration file.</p><h2 class="post__h2">Configuring build plugin options</h2><p class="post__p">Build plugin options can be configured in your code with a Netlify configuration file. If you don’t have a configuration file in your repository already, create a <code>netlify.toml</code> file at the root of your project. To learn more about configuration files with Netlify, <a href="https://docs.netlify.com/configure-builds/file-based-configuration/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-file-config" target="_blank">check out our official documentation</a>.</p><p class="post__p">Add the following to your <code>netlify.toml</code> file:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ihrMfccCVn"
      aria-describedby="ihrMfccCVn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ihrMfccCVn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="ihrMfccCVn" itemprop="text" content="%5B%5Bplugins%5D%5D%0A%20%20package%20%3D%20%22netlify-plugin-inline-functions-env%22">
      <pre class="language-toml"><code class="language-toml"><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">plugins</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br>  <span class="token key property">package</span> <span class="token punctuation">=</span> <span class="token string">"netlify-plugin-inline-functions-env"</span></code></pre>
    </div>
  </div>

  <p class="post__p">If you already have a <code>netlify.toml</code> file which currently uses plugins, make sure to add the full code snippet above, including <code>[[plugins]]</code>.</p><p class="post__p">To specify environment variables you’d like the build plugin to include, use the <code>include</code> option.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="hcXXLcmTbL"
      aria-describedby="hcXXLcmTbL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="hcXXLcmTbL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="hcXXLcmTbL" itemprop="text" content="%5B%5Bplugins%5D%5D%0Apackage%20%3D%20%22netlify-plugin-inline-functions-env%22%0A%20%20%5Bplugins.inputs%5D%0A%20%20include%20%3D%20%5B%22REALLY_LONG_ENV_VAR%22%5D">
      <pre class="language-toml"><code class="language-toml"><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">plugins</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br><span class="token key property">package</span> <span class="token punctuation">=</span> <span class="token string">"netlify-plugin-inline-functions-env"</span><br>  <span class="token punctuation">[</span><span class="token table class-name">plugins.inputs</span><span class="token punctuation">]</span><br>  <span class="token key property">include</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"REALLY_LONG_ENV_VAR"</span><span class="token punctuation">]</span></code></pre>
    </div>
  </div>

  <p class="post__p">To configure the build plugin to transform all environment variables by default, but exclude specific values, use the <code>exclude</code> option.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="XiJZWRexND"
      aria-describedby="XiJZWRexND">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="XiJZWRexND">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="toml">
      <meta data-code-id="XiJZWRexND" itemprop="text" content="%5B%5Bplugins%5D%5D%0Apackage%20%3D%20%22netlify-plugin-inline-functions-env%22%0A%20%20%5Bplugins.inputs%5D%0A%20%20exclude%20%3D%20%5B%22DO_NOT_TRANSFORM_ME%22%5D">
      <pre class="language-toml"><code class="language-toml"><span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token table class-name">plugins</span><span class="token punctuation">]</span><span class="token punctuation">]</span><br><span class="token key property">package</span> <span class="token punctuation">=</span> <span class="token string">"netlify-plugin-inline-functions-env"</span><br>  <span class="token punctuation">[</span><span class="token table class-name">plugins.inputs</span><span class="token punctuation">]</span><br>  <span class="token key property">exclude</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"DO_NOT_TRANSFORM_ME"</span><span class="token punctuation">]</span></code></pre>
    </div>
  </div>

  <p class="post__p">Commit and push your <code>netlify.toml</code> file changes to create a new deployment on Netlify. The environment variables you specified to include via the build plugin options will be converted to plain text and inlined with your function code — all behind the scenes on the server without affecting your committed code! You can now use super-long environment variables in your Netlify projects!</p><p class="post__p">For further reading, <a href="https://docs.netlify.com/configure-builds/build-plugins/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=docs-config-plugins" target="_blank">check out the official documentation on the power of build plugins</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>An introduction to GraphQL and how to use GraphQL APIs</title>
          <description>Here's everything I learned this year about GraphQL for anyone getting started.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/12/13/what-is-graphql/</link>
          <guid>https://www.contentful.com/blog/2021/12/13/what-is-graphql/</guid>
          <pubDate>Mon, 13 Dec 2021 00:00:00 GMT</pubDate>
          <category>GraphQL</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I’ve been using the Contentful GraphQL API pretty extensively for the past year to build a variety of projects on the Jamstack, and I got sucked in pretty quickly without really knowing much about the inner workings of GraphQL. While it’s easy to start making your first query with GraphQL with very little research, I thought it would be useful to consolidate all I’ve learned throughout the year and provide a high-level introduction to GraphQL for anyone interested in getting started. </p><h2 class="post__h2">What is an API?</h2><p class="post__p">API stands for Application Programming Interface, which is a way to communicate between different software services. Different types of APIs are used in programming hardware and software, including Web APIs — like the APIs that Contentful provides. If you’re new to development, I recommend you check out this blog post — <a href="https://whitep4nth3r.com/blog/what-is-an-api/">What is an API?</a> — to get a good grounding on APIs and the role they play in web development.</p><h2 class="post__h2">A brief history of GraphQL</h2><p class="post__p">GraphQL is a language used to communicate with backend systems, allowing you to fetch and manipulate your data. It was developed by Facebook in 2012, and released to the public in 2015. In 2019, the project was moved to the non-profit <a href="https://graphql.org/foundation/" target="_blank">GraphQL Foundation</a>, with a mission to ensure that the GraphQL community is able to focus on its continued evolution on a neutral platform.</p><h2 class="post__h2">What does GraphQL mean?</h2><p class="post__p">GraphQL is language-agnostic, which means it can be used with any backend framework or programming language, and is essentially an addition to a backend service that allows developers to communicate with the database and request data in a specific way. </p><p class="post__p">To create a GraphQL server for your data source, you can use specialized tools such as <a href="https://www.apollographql.com/" target="_blank">ApolloGraphQL</a>, which allow you to create a layer on top of your backend and database(s), enabling your data to be represented, queried and changed via the Graph Query Language.</p><p class="post__p">And finally, GraphQL stands for “Graph Query Language.” Let’s unpack what we mean by “graph” and “query language.”</p><h2 class="post__h2">Your data, represented in a graph</h2><p class="post__p">GraphQL creates a representation of your data that is designed to feel familiar and natural, like a visual graph. <a href="https://www.oxfordlearnersdictionaries.com/definition/english/graph" target="_blank">The Oxford Learner’s Dictionary</a> defines a graph as:</p><blockquote class="post__blockquote"><p class="post__p">[A] diagram consisting of a line or lines, showing how two or more sets of numbers are related to each other.</p></blockquote><p class="post__p">The graph part of GraphQL describes a data structure of a collection of objects — or nodes — that are connected to each other through a set of links, or edges. Relationships between different objects can be represented in a user interface as a result of this graph structure. The key concept is that the data structure is <b class="post__p--bold">non-linear,</b> meaning that one object can be connected to more than one other object, and relationships can also be circular.</p><p class="post__p">There are specialized tools available, such as <a href="https://graphql-dotnet.github.io/docs/getting-started/graphiql/" target="_blank">GraphiQL</a>, to represent the relationships between node types on a GraphQL API. Here’s a screenshot from another great tool — the <a href="https://www.contentful.com/marketplace/app/graphql-playground/" target="_blank">GraphQL Playground app available in Contentful’s app marketplace</a> — showing the relationships between data in the Contentful space that powers my personal website. Notice how you can drill down and inspect the data types in each content type, and understand how they are related to each other. </p><img src="https://images.ctfassets.net/56dzm01z6lln/74NA5bwHgmfD2UURexJKm0/9cbb80e6fe7fca6127b18a4bdf27f298/docs_relationships_example.png" alt="A screenshot of the GraphQL playground in Contentful, showing the docs tabs open, showing the connection between objects or nodes in the GraphQL schema. In this particular example, a blogPost node contains an author node, which contains an image node, which contains a fileName of type String." height="1169" width="2032" /><p class="post__p">In this particular example, a <code>blogPost</code> node contains an <code>author</code> node, which contains an <code>image</code> node, which contains a <code>fileName</code> of <b class="post__p--italic">type String</b>. Trace the relationships down the tree by following the highlighted nodes. </p><p class="post__p">Being able to inspect all types that exist in your data is a really powerful tool with predictable results to help you build robust and scalable applications. You might already be starting to realize that having this level of visibility and insight over the data structures that power your content — all within a user interface — is starting to look and feel pretty powerful if you’re a front-end developer! If you want to read more about “thinking in graphs,” have a look at <a href="https://graphql.org/learn/thinking-in-graphs/" target="_blank">this fantastic resource from graphql.org</a>.</p><p class="post__p">So that’s the <b class="post__p--bold">graph</b> part of GraphQL. Next, let’s take a look at the <b class="post__p--bold">query language</b> and how a type system ties everything together.</p><h2 class="post__h2">A type system makes things predictable</h2><p class="post__p">You may already be familiar with the concept of a REST API, which adopts a resource-based approach to data reading and writing, determined by the format of the API URLs over HTTP. (If you’re new to REST and you want some more context, check out this post — <a href="https://whitep4nth3r.com/blog/what-is-a-rest-api/">What is a REST API?</a>) In contrast to REST, the GraphQL query language allows you to request data from multiple different “resources” (such as blog posts, pages, authors and anything else) in just one API call using a single URL endpoint — usually over HTTP.</p><p class="post__p">When building up a web page, you may need to display different types of data in different UI components. Perhaps you might need to fetch a blog post, an author, a set of navigation links, a list of related blog posts, and so on — all for a single page. When using GraphQL, <b class="post__p--bold">you can request all the different data types you need to build up your UIs in a single API call</b>, instead of having to make multiple requests for different “resources.”</p><p class="post__p">Here’s an example of a GraphQL query which is requesting data from four different content types (or resources) in Contentful: a <code>blogPost</code>, an <code>author</code>, an <code>event</code>, and a <code>talk</code>. Essentially, we’re requesting four different objects, and particular fields on those objects. For example, on the <code>eventCollection</code> object, we’re requesting the <code>items</code> field, and for each of those objects in <code>items</code>, the <code>name</code> field. The query is prefixed with the word query — on the GraphQL schema, a query is of type <code>Query</code>!</p><p class="post__p">When using a REST API, you’d need to make four different API calls to request data from these four different content types. With GraphQL, you can do it all in one go!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xBiVUyusVd"
      aria-describedby="xBiVUyusVd">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xBiVUyusVd">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="xBiVUyusVd" itemprop="text" content="query%20%7B%0A%20%20blogPostCollection%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20title%0A%20%20%20%20%20%20author%20%7B%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20eventCollection%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20talkCollection%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20title%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token keyword">query</span> <span class="token punctuation">{</span><br>  <span class="token object">blogPostCollection</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token property">title</span><br>      <span class="token object">author</span> <span class="token punctuation">{</span><br>        <span class="token property">name</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br>  <span class="token object">eventCollection</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token property">name</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br>  <span class="token object">talkCollection</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token property">title</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s the result of the query. Notice we have all the data we requested in one response. (Note: I added limits into the query below to be able to demonstrate this in a single page view.) What’s also great about the GraphQL response object is that the object keys returned are exactly the same as we requested. This makes GraphQL predictable and intuitive to use. You get exactly what you ask for in the response — no more, no less — in the same order, with the same structure.</p><img src="https://images.ctfassets.net/56dzm01z6lln/46w1hS5eGv15cwHUC4bmHv/ad717bbef63ef8ee5feac4b54e65e06b/query_example_multiple_resources.png" alt="A screenshot of the GraphQL playground in Contentful showing the result of the query described above." height="1169" width="2032" /><p class="post__p">Use the keyboard shortcut <b class="post__p--bold">ctrl + space</b> when you’re building up a query in a GraphQL explorer to see the fields available to select on an object. This is a handy way to get inline hints about the schema of your data as you build up your queries — and the data type of those fields.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3DNDy8vcyCVn424iTmAtjl/2b86de5812b3882441081b2d4e30a784/show_type_menu.png" alt="An annotated diagram showing the hint or context menu that shows in the GraphQL explorer when you press ctrl and space. The menu shows the fields available to query on the current node, and the type of field it is when it is selected." height="820" width="689" /><p class="post__p">If you want to take a deeper dive into types in GraphQL, <a href="https://graphql.org/learn/schema/#type-system" target="_blank">take a look at the official GraphQL documentation on schemas and types</a>.</p><p class="post__p">You might be thinking — how is this graph structure generated from the GraphQL schema? Let’s take a look at introspection queries.</p><h2 class="post__h2">Introspection queries expose a GraphQL schema</h2><p class="post__p">GraphQL comes with an introspection query system, which is a way to query the type system and data structures in your database in order to generate the documentation and provide hints in the GraphQL UI. If you’re not familiar with the word <b class="post__p--bold">introspection</b>, it means to look inward and examine your own state.</p><p class="post__p">You can actually query your GraphQL schema using GraphQL itself! This root-level query returns all of the data types you have in your schema.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BRzeqTyhlW"
      aria-describedby="BRzeqTyhlW">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BRzeqTyhlW">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="BRzeqTyhlW" itemprop="text" content="query%20%7B%0A%20%20__schema%20%7B%0A%20%20%20%20types%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token keyword">query</span> <span class="token punctuation">{</span><br>  <span class="token object">__schema</span> <span class="token punctuation">{</span><br>    <span class="token object">types</span> <span class="token punctuation">{</span><br>      <span class="token property">name</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <img src="https://images.ctfassets.net/56dzm01z6lln/3iKeUHb2yvtCdi8MgI6hC2/bcab8b6bad7d4cb4beb50fd73fe2462f/introspection_demo.png" alt="A screenshot of the GraphQL playground showing the result of the schema query made above." height="1003" width="1334" /><p class="post__p">You’ll see three different types of data returned in an introspection query.</p><ol><li><p class="post__p">Types defined by the schema: in the case of my personal website built with Contentful, these will be types such as <code>BlogPost</code>, <code>Asset</code>, <code>Talk</code>, <code>Event</code>, <code>Project</code> and so on.</p></li><li><p class="post__p">Types defined by the type of field data (scalar types): such as <code>String</code>, <code>Boolean</code>.</p></li><li><p class="post__p">Internal introspection query types: such as <code>__Schema</code>, <code>__Type</code>, <code>__Field</code> — and these are prefixed by a double underscore.</p></li></ol><p class="post__p">In GraphQL, each field in a query acts as a function which returns the next field type, and the next type, and the next type, until the field returned is a scalar value, such as a String or a Boolean. This is demonstrated in the example above from my personal website, where a <code>blogPost</code> node contained an <code>author</code> node, which contained an <code>image</code> node, which contained a <code>fileName</code> of <b class="post__p--italic">type String</b> — a scalar value — and that’s where the journey ends.</p><p class="post__p">When any field is executed, a <b class="post__p--bold">resolver</b> is called to produce the field value. When developing your own GraphQL servers and making database changes, you’ll need to provide a <b class="post__p--bold">resolver</b> for each new field type you add to the API. <a href="https://graphql.org/learn/execution/" target="_blank">Learn more about resolvers from GraphQL.org</a>.   </p><p class="post__p">A great feature of the Contentful GraphQL API is that the GraphQL schema of your data is generated at request time, meaning it is always up to date with the current status of your Contentful space. You don’t need to worry about resolvers — we handle it for you. </p><p class="post__p">If you want to find out more about introspection queries and how to watch them run in the network tab of the browser when loading up a GraphQL explorer, <a href="https://www.youtube.com/watch?v=udou5eV9QFw" target="_blank">check out this video on the Contentful Youtube channel</a>.</p><p class="post__p">So far we’ve focused on just reading data from a GraphQL API. But writing to a database via GraphQL is also possible — and this comes with another special word —<b class="post__p--bold"> mutation</b>. </p><h2 class="post__h2">Write data with GraphQL mutations</h2><p class="post__p">To mutate something means to change, or manipulate it. You can manipulate your server-side data via GraphQL using the <code>mutation</code> keyword that prefixes a query. <b class="post__p--bold">The Contentful GraphQL API is read-only</b>, so you can’t perform mutations in Contentful using GraphQL, but in theory, here’s how it works if I wanted to create a new blog post, save the title, and return the new data in the response.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VeLbIkaxvO"
      aria-describedby="VeLbIkaxvO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VeLbIkaxvO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="VeLbIkaxvO" itemprop="text" content="mutation%20CreateNewBlogPost(%24title%3A%20String!)%20%7B%0A%20%20createBlogPost(title%3A%20%24title)%20%7B%0A%20%20%20%20title%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token keyword">mutation</span> <span class="token definition-mutation function">CreateNewBlogPost</span><span class="token punctuation">(</span><span class="token variable variable-input">$title</span><span class="token punctuation">:</span> <span class="token scalar">String</span><span class="token operator">!</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token property-query property-mutation">createBlogPost</span><span class="token punctuation">(</span><span class="token attr-name">title</span><span class="token punctuation">:</span> <span class="token variable variable-input">$title</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token property">title</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <ul><li><p class="post__p">The query begins with the word <code>mutation</code>, and in GraphQL APIs that support mutations, this is of type <code>Mutation</code>, in the same way that a read-query is of type <code>Query</code> as described above. </p></li><li><p class="post__p">Next, we need to wrap the mutation in what looks like a function, where we define the variables needed — and their types — to complete the mutation. This is <code>CreateNewBlogPost</code> which takes in a variable <code>title</code> which is of type <code>String</code>. The <code>!</code> means that the variable is required in order for the mutation to happen.  For more information on how to use variables in GraphQL, check out this blog post I wrote a little while back — <a href="https://whitep4nth3r.com/blog/how-to-use-graphql-variables/">TIL: How to use GraphQL variables to give my queries type safety</a>.</p></li><li><p class="post__p">Inside the “function,” you would use the mutation available on the GraphQL API (use introspection queries to find it!). In this case, it’s <code>createBlogPost</code>. Inside the mutation, define the variable(s) to pass in.</p></li><li><p class="post__p">And finally, inside the mutation, define the fields you’d like to return in the response. In this case, I’m just creating a title, so I just want to return the new title in the response.</p></li></ul><p class="post__p">Make a request to the API with the mutation query and variables, and here’s the response you’ll see from the API after the mutation has completed. Again, the output is in the same recognizable format as the input!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ROrmkKDwum"
      aria-describedby="ROrmkKDwum">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ROrmkKDwum">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="ROrmkKDwum" itemprop="text" content="%7B%0A%20%20%22data%22%3A%20%7B%0A%20%20%20%20%22createBlogPost%22%3A%20%7B%0A%20%20%20%20%20%20%22title%22%3A%20%22My%20new%20blog%20post%20title!%22%2C%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"createBlogPost"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"My new blog post title!"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">To read more on GraphQL mutations, <a href="https://graphql.org/learn/queries/#mutations" target="_blank">check out the official documentation from GraphQL.org</a>.</p><p class="post__p">So now we’ve covered how to read from (query) and write to (mutate) a GraphQL API where supported, let’s take a brief look at how we can communicate with a GraphQL API to perform those actions.</p><h2 class="post__h2">How to communicate with a GraphQL API</h2><p class="post__p">GraphQL is usually served over HTTP via a GraphQL server. There are other ways to communicate with GraphQL, such as via WebSockets, but HTTP is largely the most popular choice. If you’d like to read up on how to use GraphQL with cURL, Python, JavaScript, Ruby and PHP over HTTP, check out this blog post: <a href="https://www.contentful.com/blog/2021/01/14/GraphQL-via-HTTP-in-five-ways/" target="_blank">GraphQL via HTTP in five ways</a>.</p><p class="post__p">Here’s a quick example of how to request a title and excerpt from a blog post stored in Contentful via GraphQL, using JavaScript fetch. You can make this same HTTP call using any language or framework as long as it supports HTTP!</p><p class="post__p">In this example we’re sending the request via HTTP POST by sending the access token in an Authorization header and the GraphQL query in the body of the request. You can also make the same request via HTTP GET, but you’ll need to append the query and access token to the URL of the request. <a href="https://www.contentful.com/developers/docs/references/graphql/#/introduction/http-methods" target="_blank">Read more about using HTTP POST and GET on the Contentful GraphQL API in the documentation</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="bblrtaeIvP"
      aria-describedby="bblrtaeIvP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="bblrtaeIvP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="bblrtaeIvP" itemprop="text" content="%2F%2F%20Build%20the%20GraphQL%20Query%0Aconst%20query%20%3D%20%60%7B%0A%20%20blogPostCollection(limit%3A%201)%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20title%0A%20%20%20%20%20%20excerpt%0A%20%20%20%20%7D%0A%20%20%7D%60%3B%0A%0A%2F%2F%20Send%20a%20POST%20request%20via%20fetch%20to%20the%20Contentful%20GraphQL%20URL%20endpoint%0Aconst%20response%20%3D%20await%20fetch(%22https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%7ByourSpaceId%7D%22%2C%20%7B%0A%20%20method%3A%20%22POST%22%2C%0A%20%20%2F%2F%20Include%20Authorization%20and%20Content-Type%20HTTP%20headers%0A%20%20headers%3A%20%7B%0A%20%20%20%20Authorization%3A%20%22Bearer%20%7ByourAccessToken%7D%22%2C%0A%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%7D%2C%0A%20%20%2F%2F%20Send%20the%20GraphQL%20query%20in%20the%20body%20of%20the%20request%0A%20%20body%3A%20JSON.stringify(%7B%20query%20%7D)%2C%0A%7D).then((response)%20%3D%3E%20console.log(response.json()))%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// Build the GraphQL Query</span><br><span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">{<br>  blogPostCollection(limit: 1) {<br>    items {<br>      title<br>      excerpt<br>    }<br>  }</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br><span class="token comment">// Send a POST request via fetch to the Contentful GraphQL URL endpoint</span><br><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">"https://graphql.contentful.com/content/v1/spaces/{yourSpaceId}"</span><span class="token punctuation">,</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>  <span class="token comment">// Include Authorization and Content-Type HTTP headers</span><br>  <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token string">"Bearer {yourAccessToken}"</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token comment">// Send the GraphQL query in the body of the request</span><br>  <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">And boom, you’ve got data from a GraphQL API! Remember, GraphQL is language-agnostic, so you can read and write to a GraphQL API using any programming language or framework — as long as it supports the protocol you need, such as HTTP.</p><h2 class="post__h2">Further reading</h2><p class="post__p">GraphQL is a powerful way to organize how you communicate with your backend services and databases. I’ve really enjoyed exploring what’s possible with GraphQL this year, and I’m excited to dive deeper into new projects. I might even experiment with building my own GraphQL API!</p><p class="post__p">To find out more about using GraphQL to get your Contentful data — including published and preview content — <a href="https://www.contentful.com/developers/docs/references/graphql/" target="_blank">check out the official documentation</a>. And if you want to explore the wonderful world of GraphQL in more detail, take a look at the official GraphQL specification and learning materials on <a href="https://graphql.org/" target="_blank">graphql.org</a>. Maybe it’ll inspire you to build your own GraphQL API. Happy querying!</p><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to load responsive images in AVIF and WebP using the HTML picture element</title>
          <description>A complete guide on how to serve responsive images in WebP and the new AVIF format where supported, using the HTML picture element.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/11/29/load-avif-webp-using-html/</link>
          <guid>https://www.contentful.com/blog/2021/11/29/load-avif-webp-using-html/</guid>
          <pubDate>Mon, 29 Nov 2021 00:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Web Dev</category><category>Accessibility</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p"><a href="https://www.contentful.com/developers/changelog/#images-api-now-supports-avif-format-conversion" target="_blank">We released support for the new AVIF image format</a> on the Contentful Images API at our Fast Forward 2021 conference. This means you can return your images stored in Contentful in the new AVIF format via the REST API with a query parameter, or via the GraphQL API with a transform function in your query.</p><p class="post__p">But what is AVIF — and should you be using it right now? </p><p class="post__p">Now this stuff is tricky, so strap in and get ready for a wild ride! In this post, we’ll cover:</p><ul><li><p class="post__p">Image formats, compression and browser support</p></li><li><p class="post__p">Why image compression is important</p></li><li><p class="post__p">The HTML <code>&lt;picture&gt;</code> element</p></li><li><p class="post__p">Lazy loading and browser support</p></li><li><p class="post__p">Performance improvements gained with AVIF</p></li></ul><p class="post__p">Are you ready? Let’s go.</p><h2 class="post__h2">What is AVIF?</h2><p class="post__p">In short, AVIF is the new kid on the block when it comes to image compression. Released in 2019 by the <a href="https://aomedia.org/" target="_blank">Alliance for Open Media</a>, AVIF claims to offer <a href="https://aomedia.org/av1-features/" target="_blank">30% better compression than current alternatives</a> such as WebP. It’s optimized for the web, designed with a low computation footprint, and what’s more — it’s open source.</p><h2 class="post__h2">Why is image compression important?</h2><p class="post__p">Websites in 2021 are an incredibly visual experience, full of images, animations and full-page photography. If you’re not careful, you can end up forcing visitors to your website to download hundreds of megabytes of images on a visit to your page. This risks making your website slow, inevitably causing your visitors to bounce. And we don’t want that!</p><p class="post__p">A fast experience on the web is crucial to providing a great user experience for visitors to your website, and as of June 2021, Google uses <a href="https://web.dev/vitals/" target="_blank">Core Web Vitals</a> scores to rank websites in search results. Core Web Vitals are currently scored on three aspects of user experience — loading, interactivity and visual stability.</p><h3 class="post__h3">Loading</h3><p class="post__p">Loading performance is measured by the <b class="post__p--bold">Largest Contentful Paint (LCP).</b> (This has nothing to do with the Contentful CMS in this context! 🙈 ). If you’re forcing users to download megabytes of images when the page first starts to load — for example, large hero images at the top of the page — your LCP time will increase. To provide a good user experience, the LCP should happen within 2.5 seconds of when the page first starts loading.</p><h3 class="post__h3">Interactivity</h3><p class="post__p">Interactivity is measured by the <b class="post__p--bold">First Input Delay (FID) </b>— and measures how soon your web application responds to user input such as clicking and typing into form fields. To provide a good user experience, pages should have an FID of 100 milliseconds or less.</p><h3 class="post__h3">Visual stability</h3><p class="post__p">Visual stability is measured by <b class="post__p--bold">Cumulative Layout Shift (CLS)</b>. Have you ever clicked on a part of a web page, only to find that you unexpectedly clicked on something else after a rogue element or image was finally loaded? CLS is where content pops into view once it has loaded, often pushing content down or sideways on the web page — and can be extremely frustrating! CLS makes your web page unstable — and usually, large images that take time to load are to blame. A good user experience maintains a CLS score of 0.1 or less.</p><h3 class="post__h3">Why is image compression important? </h3><p class="post__p">Did you know that the <a href="https://worldpopulationreview.com/country-rankings/internet-speeds-by-country" target="_blank">average internet speed across the world in 2021</a> is <b class="post__p--bold">only 55.13Mbs</b>? That’s only marginally faster than the speed of a slow 3G connection as simulated in Chromium Dev Tools. If you’re the nerdy type and want to look at the source code for the throttling simulations — <a href="https://github.com/ChromeDevTools/devtools-frontend/blob/80c102878fd97a7a696572054007d40560dcdd21/front_end/sdk/NetworkManager.js#L261" target="_blank">check out this link on GitHub!</a></p><img src="https://images.ctfassets.net/56dzm01z6lln/18JkFEDAv2mhvxYDSG66EB/1ec0c411beb07a596d8d4dd6dd642c9c/average_internet_speed.png" alt="A graph showing the average internet speed in the world in 2021 a surveyed by world population review dot com as 55.15Mbs." height="1746" width="1472" /><p class="post__p">In &quot;<a href="https://www.smashingmagazine.com/2021/09/modern-image-formats-avif-webp/" target="_blank">Using Modern Image Formats: AVIF And WebP</a>&quot;, Addy Osmani, engineering manager at Google Chrome, gives us this fascinating piece of data: </p><blockquote class="post__blockquote"><p class="post__p">If you’re optimizing for the Web Vitals, you might be interested to hear that images account for ~42% of the Largest Contentful Paint element for websites.</p></blockquote><p class="post__p">If almost 50% of your LCP is down to images — you need to deliver them to your website visitors in as few bytes as possible. The smaller the image file sizes are on your web pages, the less time it takes for visitors to your websites to download them — and this is especially important on slow internet connections and older, slower devices. </p><p class="post__p">Minimizing the size of your images ensures your Core Web Vitals scores are as good as possible across the board, which means you provide a better experience for your users, and ultimately means your pages rank better in Google search results. And to minimise the size of your images — you need to use the image format that yields the lightest results.</p><p class="post__p">So — if AVIF images currently offer the smallest image file sizes, we should all get on the AVIF train ASAP, right?</p><h2 class="post__h2">Should you convert all your images to AVIF right now?</h2><p class="post__p">Whilst AVIF offers better compression and smaller resulting file sizes than WebP, there are some downsides to adopting this new format in 2021.</p><p class="post__p">AVIF may not be able to compress non-photographic images as well as PNG or lossless WebP. I found this to be the case in my initial experiments with the hero image on the homepage of <a href="https://whitep4nth3r.com" target="_blank">my personal website</a>. </p><img src="https://images.ctfassets.net/56dzm01z6lln/5JFvCDLcP8ezptjymaFMLC/4d4fb6903c1b1c83c0c3a8880ebc7663/stream_screenshot_sep2021.png" alt="A screenshot from my Twitch stream, where I am looking to the left and smiling, with VSCode open in the background." height="1021" width="1920" /><p class="post__p">Whilst this image <b class="post__p--italic">does</b> contain photographic imagery of my face, most of the image is text and illustrations. As a WebP image, this image came in at 118kb, whilst as an AVIF image, the image size increased to 125kb. </p><p class="post__p">Secondly, at the time of writing this article, <a href="https://caniuse.com/?search=avif" target="_blank">not all browsers currently support the AVIF image format</a>. Edge, Safari and some mobile browsers are not quite there yet.</p><img src="https://images.ctfassets.net/56dzm01z6lln/25TDkZGHrC56cf5IPesQNE/b5d910a93ba99a4268393b55cbc24b9e/avif_caniuse.png" alt="A screenshot of can I use dot com showing that the support for AVIF is currently only available in Firefox, Chrome, Opera, Android and Samsung Internet browser." height="2418" width="3808" /><h3 class="post__h3">But all is not lost! </h3><p class="post__p">There is a beautiful way we can harness the power of native HTML to serve different supported image formats to browsers using the HTML <code>&lt;picture&gt;</code> tag. And what’s more, when other browsers <b class="post__p--italic">do</b> catch up with the AVIF train — you won’t need to change any code! </p><p class="post__p">Before we look at the HTML, let’s take a look at how you can convert your images stored in Contentful to the new AVIF format using the REST and GraphQL APIs.</p><h2 class="post__h2">How to convert your images to AVIF using the Contentful Images API</h2><p class="post__p"><a href="https://www.contentful.com/developers/docs/references/images-api/#/reference/retrieval" target="_blank">The Contentful Images API</a> offers a variety of image transformations and manipulations as query parameters on the URL pointing to an image asset. You can change the quality of an image, resize it, add a background color, crop it, add rounded corners and more, as well as change the <b class="post__p--bold">format</b> of an image. </p><h3 class="post__h3">Using query parameters on an image URL</h3><p class="post__p">To convert your images stored in Contentful to AVIF, add <code>fm=avif</code> as a query parameter to your image URL. We’ll be using this technique later in the HTML example code.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="iJbQcCmeKj"
      aria-describedby="iJbQcCmeKj">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="iJbQcCmeKj">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="iJbQcCmeKj" itemprop="text" content="https%3A%2F%2Fimages.ctfassets.net%2F%7Bspace_id%7D%2F%7Basset_id%7D%2F%7Bunique_id%7D%2F%7Bfile_name%7D%3Ffm%3Davif">
      <pre class="language-bash"><code class="language-bash">https://images.ctfassets.net/<span class="token punctuation">{</span>space_id<span class="token punctuation">}</span>/<span class="token punctuation">{</span>asset_id<span class="token punctuation">}</span>/<span class="token punctuation">{</span>unique_id<span class="token punctuation">}</span>/<span class="token punctuation">{</span>file_name<span class="token punctuation">}</span>?fm<span class="token operator">=</span>avif</code></pre>
    </div>
  </div>

  <p class="post__p">Here’s an example URL for you to play with in your browser. Experiment with changing the format parameter to <code>jpg</code>, <code>png</code>, <code>webp</code>, <code>gif</code> or <code>avif</code> and compare the size of the image returned in the network tab! </p><p class="post__p"><a href="https://images.ctfassets.net/zz0ob82dbd6h/5YwujztZwQrte5WfgZf3eV/9eba3414d91e8e1e376fc96c0c8e6d63/cat-heavy-breathing-intensifies.gif?fm=avif" target="_blank">https://images.ctfassets.net/zz0ob82dbd6h/5YwujztZwQrte5WfgZf3eV/9eba3414d91e8e1e376fc96c0c8e6d63/cat-heavy-breathing-intensifies.gif?fm=avif</a></p><p class="post__p">For the impatient among you, here’s how the image formats compare in terms of size:</p><ul><li><p class="post__p">jpg: <b class="post__p--bold">76.8kb</b></p></li><li><p class="post__p">png:<b class="post__p--bold"> 125kb</b></p></li><li><p class="post__p">webp: <b class="post__p--bold">167kb</b></p></li><li><p class="post__p">gif: <b class="post__p--bold">1.2MB</b></p></li><li><p class="post__p">avif: <b class="post__p--bold">25.3kb</b></p></li></ul><img src="https://images.ctfassets.net/56dzm01z6lln/7dBJsTpG3FCb7esOnIMBPk/6b26ce67af5c950135bd82e99a1f017f/image_comparisons.png" alt="A stacked collage image showing the same heavy breathing cat gif in five image formats, showing that the avif image is the fewest bytes." height="1804" width="830" /><p class="post__p">AVIF is the clear winner in terms of reduction in size for this image. However, one interesting thing to note is that whereas the original animated GIF comes in at a whopping 1.2MB, converting the image to WebP reduces the image size by a massive 87% <b class="post__p--bold">and preserves the animation frames!</b></p><h3 class="post__h3">Using GraphQL</h3><p class="post__p">If you’re using GraphQL, you can convert your images to AVIF via a transform function on the image URL in the GraphQL query, like so:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="nJeSwEkjvF"
      aria-describedby="nJeSwEkjvF">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="nJeSwEkjvF">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="nJeSwEkjvF" itemprop="text" content="query%20%7B%0A%20%20blogPostCollection(limit%3A%201)%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20image%20%7B%0A%20%20%20%20%20%20%20%20url(transform%3A%20%7B%20format%3A%20AVIF%20%7D)%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token keyword">query</span> <span class="token punctuation">{</span><br>  <span class="token property-query">blogPostCollection</span><span class="token punctuation">(</span><span class="token attr-name">limit</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token object">image</span> <span class="token punctuation">{</span><br>        <span class="token property-query">url</span><span class="token punctuation">(</span><span class="token attr-name">transform</span><span class="token punctuation">:</span> <span class="token punctuation">{</span> <span class="token attr-name">format</span><span class="token punctuation">:</span> <span class="token constant">AVIF</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s the response returned. You’ll notice that this is the same image URL we used in the example above, demonstrating that the GraphQL API communicates directly with Contentful’s Images API to return what you need in your GraphQL response.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="GThftaDbJI"
      aria-describedby="GThftaDbJI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="GThftaDbJI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="GThftaDbJI" itemprop="text" content="%7B%0A%20%20%22data%22%3A%20%7B%0A%20%20%20%20%22blogPostCollection%22%3A%20%7B%0A%20%20%20%20%20%20%22items%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22image%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22url%22%3A%20%22https%3A%2F%2Fimages.ctfassets.net%2Fzz0ob82dbd6h%2F5YwujztZwQrte5WfgZf3eV%2F9eba3414d91e8e1e376fc96c0c8e6d63%2Fcat-heavy-breathing-intensifies.gif%3Ffm%3Davif%22%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%5D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"blogPostCollection"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token property">"items"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>        <span class="token punctuation">{</span><br>          <span class="token property">"image"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>            <span class="token property">"url"</span><span class="token operator">:</span> <span class="token string">"https://images.ctfassets.net/zz0ob82dbd6h/5YwujztZwQrte5WfgZf3eV/9eba3414d91e8e1e376fc96c0c8e6d63/cat-heavy-breathing-intensifies.gif?fm=avif"</span><br>          <span class="token punctuation">}</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">]</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">However — while you <b class="post__p--italic">can</b> retrieve images in AVIF format in GraphQL — <u><b class="post__p--bold">remember that not all browsers support this image format yet</b></u> — so you don’t want to lock yourself down to the AVIF image format. </p><p class="post__p">Now let’s look at how we can take a base image URL and serve it in different ways to browsers that support different image formats using Contentful’s Images API and the native HTML <code>&lt;picture&gt;</code> tag. </p><p class="post__p">And here is what you’ve all been waiting for!</p><h2 class="post__h2">How to serve responsive images with AVIF in supported browsers using the HTML &lt;picture&gt; tag</h2><p class="post__p">I’ve attempted to understand <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images" target="_blank">the low-down on responsive images from MDN</a> for many years and I must admit it was hard to grasp! <a href="https://www.stefanjudis.com/snippets/a-picture-element-to-load-correctly-resized-webp-images-in-html/" target="_blank">This post from Stefan Judis</a> provided a great help in getting to grips with this, in conjunction with MDN.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2lksr5HxnNIDSkIAgqD151/89397ef37623a4adf6cb86c4a87a084e/one_does_not.png" alt="The "one does not simply" meme that reads: "One does not simply use width:100% to create responsive images"." height="383" width="651" /><p class="post__p">Before we get into the code, we need to understand what we mean by responsive images. Responsive images are not simply about giving all images <code>width: 100%</code> to fill the size of a container! The real power in responsive images is serving different image files of different sizes to different viewport sizes — and what’s more — screens with different <b class="post__p--bold">pixel densities or display resolutions</b>. This is called <b class="post__p--bold">Resolution Switching, </b>and by providing browsers with an array of image URL options by configuring a <code>&lt;source&gt;</code> element’s <code>srcset</code>, <code>media</code>, and <code>type</code> attributes, the most compatible image — according to the current layout and display capabilities — will be displayed. </p><p class="post__p">Let’s get straight into the HTML code example, and unpack what it does.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="azlCDjtHMw"
      aria-describedby="azlCDjtHMw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="azlCDjtHMw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="azlCDjtHMw" itemprop="text" content="%3Cpicture%3E%0A%20%20%3Csource%0A%20%20%20%20type%3D%22image%2Favif%22%0A%20%20%20%20srcset%3D%22%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D500%26fm%3Davif%20500w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D900%26fm%3Davif%20900w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1300%26fm%3Davif%201300w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1700%26fm%3Davif%201700w%2C%0A%20%20%20%20%22%0A%20%20%20%20sizes%3D%22(max-width%3A%20735px)%20100vw%2C%20736px%22%0A%20%20%2F%3E%0A%20%20%3Csource%0A%20%20%20%20type%3D%22image%2Fwebp%22%0A%20%20%20%20srcset%3D%22%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D500%26fm%3Dwebp%20500w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D900%26fm%3Dwebp%20900w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1300%26fm%3Dwebp%201300w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1700%26fm%3Dwebp%201700w%0A%20%20%20%20%22%0A%20%20%20%20sizes%3D%22(max-width%3A%20735px)%20100vw%2C%20736px%22%0A%20%20%2F%3E%0A%20%20%3Cimg%0A%20%20%20%20srcset%3D%22%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D500%20500w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D900%20900w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1300%201300w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1700%201700w%0A%20%20%20%20%22%0A%20%20%20%20sizes%3D%22(max-width%3A%20735px)%20100vw%2C%20736px%22%0A%20%20%20%20src%3D%22https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%22%0A%20%20%20%20alt%3D%22Some%20great%20alternative%20text%22%0A%20%20%20%20loading%3D%22lazy%22%0A%20%20%20%20decoding%3D%22async%22%0A%20%20%20%20width%3D%222032%22%0A%20%20%20%20height%3D%221076%22%0A%20%20%2F%3E%0A%3C%2Fpicture%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>picture</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span><br>    <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/avif<span class="token punctuation">"</span></span><br>    <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=500&amp;fm=avif 500w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=900&amp;fm=avif 900w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1300&amp;fm=avif 1300w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1700&amp;fm=avif 1700w,<br>    <span class="token punctuation">"</span></span><br>    <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(max-width: 735px) 100vw, 736px<span class="token punctuation">"</span></span><br>  <span class="token punctuation">/></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span><br>    <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/webp<span class="token punctuation">"</span></span><br>    <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=500&amp;fm=webp 500w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=900&amp;fm=webp 900w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1300&amp;fm=webp 1300w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1700&amp;fm=webp 1700w<br>    <span class="token punctuation">"</span></span><br>    <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(max-width: 735px) 100vw, 736px<span class="token punctuation">"</span></span><br>  <span class="token punctuation">/></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span><br>    <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=500 500w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=900 900w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1300 1300w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1700 1700w<br>    <span class="token punctuation">"</span></span><br>    <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(max-width: 735px) 100vw, 736px<span class="token punctuation">"</span></span><br>    <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://images.ctfassets.net/.../img.png<span class="token punctuation">"</span></span><br>    <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Some great alternative text<span class="token punctuation">"</span></span><br>    <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span><br>    <span class="token attr-name">decoding</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>async<span class="token punctuation">"</span></span><br>    <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2032<span class="token punctuation">"</span></span><br>    <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1076<span class="token punctuation">"</span></span><br>  <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>picture</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">HTML &lt;picture&gt; element</h3><p class="post__p">The <code>&lt;picture&gt;</code> HTML element wraps <code>&lt;source&gt;</code> elements and one <code>&lt;img&gt;</code> element to offer alternative versions of an image for different displays, devices or browsers (depending on the image formats supported).</p><p class="post__p">The magic in the <code>&lt;picture&gt;</code> element is that the <b class="post__p--bold">browser chooses</b> the most appropriate <code>&lt;source&gt;</code> element to display. If no matches are found — or the browser doesn&#39;t support the <code>&lt;picture&gt;</code> element — the browser falls back to the URL in the <code>src</code> attribute of the <code>&lt;img&gt;</code> element. The selected image is then presented in the <b class="post__p--bold">space occupied by the </b><code><b class="post__p--bold">&lt;img&gt;</b></code><b class="post__p--bold"> element</b>. To prevent Cumulative Layout Shift as the image is loading, it’s important to add the height and width attributes to the <code>&lt;img&gt;</code> element for this fallback.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LjpUkZeSPr"
      aria-describedby="LjpUkZeSPr">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LjpUkZeSPr">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="LjpUkZeSPr" itemprop="text" content="%3Cpicture%3E%0A%20%20%3Csource%20...%20%2F%3E%0A%20%20%3Csource%20...%20%2F%3E%0A%20%20%3Cimg%0A%20%20%20%20src%3D%22https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%22%0A%20%20%20%20alt%3D%22Some%20great%20alternative%20text%22%0A%20%20%20%20width%3D%222032%22%0A%20%20%20%20height%3D%221076%22%0A%20%20%2F%3E%0A%3C%2Fpicture%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>picture</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">...</span> <span class="token punctuation">/></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">...</span> <span class="token punctuation">/></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span><br>    <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://images.ctfassets.net/.../img.png<span class="token punctuation">"</span></span><br>    <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Some great alternative text<span class="token punctuation">"</span></span><br>    <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2032<span class="token punctuation">"</span></span><br>    <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1076<span class="token punctuation">"</span></span><br>  <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>picture</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">HTML &lt;source&gt; element</h3><p class="post__p">The HTML <code>&lt;source&gt;</code> element provides multiple media types for the <code>&lt;picture&gt;</code> element to select the best-fit for the browser or device. You can also use the <code>&lt;source&gt;</code> element inside HTML <code>&lt;audio&gt;</code> or <code>&lt;video&gt;</code> elements in the same way.</p><p class="post__p">In this example, the <code>&lt;source&gt;</code> elements are offering image formats in <code>type=&quot;image/avif&quot;</code> and <code>type=&quot;image/webp&quot;</code>, which browsers will choose to display in that order depending on support.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="alzDDWzFOL"
      aria-describedby="alzDDWzFOL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="alzDDWzFOL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="alzDDWzFOL" itemprop="text" content="%3Csource%20type%3D%22image%2Favif%22%0A%20%20...%0A%2F%3E%0A%3Csource%20type%3D%22image%2Fwebp%22%0A%20%20...%0A%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/avif<span class="token punctuation">"</span></span><br>  <span class="token attr-name">...</span><br><span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/webp<span class="token punctuation">"</span></span><br>  <span class="token attr-name">...</span><br><span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">HTML srcset and sizes attributes</h3><p class="post__p">The HTML <code>srcset</code> attribute is a comma-separated list of strings of image URLs and either a width descriptor — such as <code>300w</code>, or a pixel density descriptor — such as <code>1.5x</code>. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="JfJfmGeIlO"
      aria-describedby="JfJfmGeIlO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="JfJfmGeIlO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="JfJfmGeIlO" itemprop="text" content="srcset%3D%22%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D500%26fm%3Davif%20500w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D900%26fm%3Davif%20900w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1300%26fm%3Davif%201300w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1700%26fm%3Davif%201700w%0A%20%20%20%20%22%0A">
      <pre class="language-html"><code class="language-html">srcset="<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=500&amp;fm=avif 500w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=900&amp;fm=avif 900w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1300&amp;fm=avif 1300w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1700&amp;fm=avif 1700w<br>    "</code></pre>
    </div>
  </div>

  <p class="post__p">The HTML <code>sizes</code> attribute is a comma-separated list of media conditions paired with sizes to describe the final rendered image width — <b class="post__p--bold">in CSS pixels, not physical pixels</b> <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio" target="_blank">(read more about different types of pixels on MDN</a>). The browser wants to know the best-fit resources to request as the page is loading. The <code>sizes</code> attribute helps the browser to calculate the layout of the page and request the most suitable images for the layout — even before the CSS has loaded.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="HbaBnPFEug"
      aria-describedby="HbaBnPFEug">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="HbaBnPFEug">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="HbaBnPFEug" itemprop="text" content="sizes%3D%22(max-width%3A%20735px)%20100vw%2C%20736px%22">
      <pre class="language-html"><code class="language-html">sizes="(max-width: 735px) 100vw, 736px"</code></pre>
    </div>
  </div>

  <p class="post__p"><b class="post__p--bold">The </b><code>srcset</code><b class="post__p--bold"> and </b><code>sizes</code><b class="post__p--bold"> attributes work together in the browser to determine which image defined in the </b><code>srcset</code><b class="post__p--bold"> to request as the page is being rendered or the viewport resized.</b></p><p class="post__p">In this code example, four image URLs are provided in the <code>srcset</code> for four different image widths: 500w, 900w, 1300w, 1700w. Notice that the <code>w={width}</code> parameter on the image URLs is resizing the base image to match with size of the width descriptor <a href="https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/change-the-resizing-behavior" target="_blank">using the capabilities of the Contentful Images API</a>. When not using the Contentful Images API, you may wish to choose completely different image URLs at different viewport widths to serve different image sizes. </p><h3 class="post__h3">A note on the magic 736 number</h3><p class="post__p">For blog posts on my personal website, the maximum width of the image container will only ever be 736px — in CSS pixels.</p><p class="post__p">The <code>sizes</code> attribute states that under a viewport width of 736px, choose the most appropriate image from the <code>srcset</code>, considering that the image will be laid out across the entire viewport width (100vw). Depending on the device, full viewport width (100vw) could translate to 300 device pixels at 1 DPR, 600 device pixels at 2 DPR and 900 device pixels at 3DPR.</p><p class="post__p">At a viewport width of 736px and above (remember, it’s 736 CSS pixels), the browser knows that the image widths will not exceed 736 CSS pixels as the CSS is controlling the container width. Depending on the device, 736 CSS pixels will be 1472px at 2DPR and 2208px at 3DPR. At all viewport widths, the browser will choose the closest match from the <code>srcset</code> list. Like magic!</p><p class="post__p">This is an entirely personal example, and you should configure your image widths according to the layout of your web pages. You can also make further optimizations using CSS <code>calc()</code> to account for the padding values inside the container to load even smaller images at different viewport widths.</p><p class="post__p">It’s also important to note that the <code>srcset</code> and <code>sizes</code> attributes only come into effect when the <code>&lt;source&gt;</code> element is a direct child of a <code>&lt;picture&gt;</code> element.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KEtODRdIHo"
      aria-describedby="KEtODRdIHo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KEtODRdIHo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="KEtODRdIHo" itemprop="text" content="%3Csource%0A%20%20%20%20type%3D%22image%2Favif%22%0A%20%20%20%20srcset%3D%22%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D500%26fm%3Davif%20500w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D900%26fm%3Davif%20900w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1300%26fm%3Davif%201300w%2C%0A%20%20%20%20%20%20https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimg.png%3Fq%3D75%26w%3D1700%26fm%3Davif%201700w%0A%20%20%20%20%22%0A%20%20%20%20sizes%3D%22(max-width%3A%20735px)%20100vw%2C%20736px%22%0A%20%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>source</span><br>    <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/avif<span class="token punctuation">"</span></span><br>    <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=500&amp;fm=avif 500w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=900&amp;fm=avif 900w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1300&amp;fm=avif 1300w,<br>      https://images.ctfassets.net/.../img.png?q=75&amp;w=1700&amp;fm=avif 1700w<br>    <span class="token punctuation">"</span></span><br>    <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(max-width: 735px) 100vw, 736px<span class="token punctuation">"</span></span><br>  <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">HTML loading=”lazy” and decoding=”async”</h3><p class="post__p">Lazy loading is a strategy to identify resources such as images, video and other media as non-blocking, or non-critical — and to load these only when needed, such as when a user scrolls that resource into view.</p><p class="post__p">To prevent visitors to web pages downloading a full page of images that aren’t visible in the viewport yet, we can use <a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading" target="_blank">native HTML lazy loading</a> with the <code>loading=&quot;lazy&quot;</code> attribute to defer image loading until a user scrolls near it — <b class="post__p--bold">no JavaScript required</b>! As stated before, to prevent Cumulative Layout Shift, be sure to add the <code>height</code> and <code>width</code> of the base image as attributes to the <code>&lt;img&gt;</code> element, which will instruct the browser to leave the correct space on the page in which to finally load your image when it scrolls into view.</p><p class="post__p">And in addition, the <code>decoding=&quot;async&quot;</code> attribute tells the browser that the<a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decoding" target="_blank"> image loading can be performed asynchronously</a>, so that content below the resource — such as paragraphs of text — are not blocked by the image taking a moment to load.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="sofqZviTBP"
      aria-describedby="sofqZviTBP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="sofqZviTBP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="sofqZviTBP" itemprop="text" content="%3Cimg%0A%20%20...%0A%20%20loading%3D%22lazy%22%0A%20%20decoding%3D%22async%22%0A%20%20width%3D%222032%22%0A%20%20height%3D%221076%22%0A%2F%3E%0A">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>img</span><br>  <span class="token attr-name">...</span><br>  <span class="token attr-name">loading</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>lazy<span class="token punctuation">"</span></span><br>  <span class="token attr-name">decoding</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>async<span class="token punctuation">"</span></span><br>  <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2032<span class="token punctuation">"</span></span><br>  <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1076<span class="token punctuation">"</span></span><br><span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">It’s worth a mention that <code>loading=&quot;lazy&quot;</code> isn’t entirely supported across all browsers yet, but at the time of writing this article, <a href="https://webkit.org/blog/12040/release-notes-for-safari-technology-preview-135/" target="_blank">Safari just shipped it in the newest Tech Preview</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5clZZ86dPKa1LkLEj2kyhI/d5d8ff2c7181f8b055ce0e82682e0214/loading_caniuse.png" alt="A screenshot from can I use dot com showing that lazy loading is supported only in Safari's newest tech preview. Firefox, chrome, Opera, Android, Edge, and Samsung Internet are shown to support it." height="2418" width="3808" /><h2 class="post__h2">The code in action</h2><p class="post__p">Here’s a video of the code example in action at 1 DPR. Notice the lazy-loading in action as the page is scrolled, and how the browser loads different image URLs from the srcset when the browser is resized! And let me reiterate — <b class="post__p--bold">no JavaScript required</b>!</p><img src="https://images.ctfassets.net/56dzm01z6lln/3N98TaFSRIbrqro2jRHFLJ/aa8b79904a0a3ba720d4bf9762195a0a/responsive_avif.gif" alt="An animated image showing the network tab in Chromium dev tools is loading differently sizes images when the browser window is resized." height="450" width="800" /><h2 class="post__h2">How did serving AVIF improve my website?</h2><p class="post__p">The code example above is <b class="post__p--bold">a lot</b> of HTML to write! But I like it for how self-documenting it is. And what’s more, I’ve bagged some significant performance improvements for my personal website while learning all about AVIF.</p><p class="post__p">For image-heavy blog posts, and in particular <a href="https://whitep4nth3r.com/blog/personalized-image-social-sharing-with-cloudinary-nextjs/" target="_blank">this blog post</a>, converting all images from WebP to AVIF reduced the cumulative size of images on a desktop screen at 2 DPR from just over 1Mb to just 404kb. And on mobile devices at 1 DPR, it reduced the cumulative sizes of images from 430kb to 124kb. <u><b class="post__p--bold">That’s a 60-70% reduction across device sizes!</b></u><b class="post__p--bold"> </b>Those are some huge savings! And plus, all images are lazy-loaded, which means the browser doesn’t request them until the image is near to the visible viewport.</p><h2 class="post__h2">The bottom line</h2><p class="post__p">Responsive images on the web are a tricky business to get right! But if you get them right, you provide a better user experience for your website visitors across browsers, devices and screen sizes, and you get bonus points from Google in your Core Web Vitals scores.</p><p class="post__p">If you’d like to see the final code example as a React component in the code for my personal blog site, <a href="https://github.com/whitep4nth3r/p4nth3rblog/blob/main/components/ResponsiveImage/index.js" target="_blank">check it out on GitHub</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>TIL: How to use GraphQL variables to give my queries type safety</title>
          <description>How can you make sure your GraphQL queries are safe from nasties? Let’s find out.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/10/22/how-to-use-graphql-variables/</link>
          <guid>https://www.contentful.com/blog/2021/10/22/how-to-use-graphql-variables/</guid>
          <pubDate>Thu, 21 Oct 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>GraphQL</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">One of the things I love about GraphQL is how straightforward it is to get up and running with little to no experience. Using a browser-based GraphiQL interface — such as the GraphiQL explorer provided by Contentful — you can inspect your schema right there in the browser, and construct your queries in no time. But how can you make sure your GraphQL queries are safe from nasties? Let’s find out.</p><p class="post__p">To inspect your schema in Contentful’s GraphiQL interface and construct a GraphQL query, enter this URL in your browser, and swap out the <code>SPACE_ID</code> for your Contentful space ID, and <code>ACCESS_TOKEN</code> for your Contentful Delivery API Key.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BmGklXVJLM"
      aria-describedby="BmGklXVJLM">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BmGklXVJLM">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="BmGklXVJLM" itemprop="text" content="https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%7BSPACE_ID%7D%2Fexplore%3Faccess_token%3D%7BACCESS_TOKEN%7D">
      <pre class="language-bash"><code class="language-bash">https://graphql.contentful.com/content/v1/spaces/<span class="token punctuation">{</span>SPACE_ID<span class="token punctuation">}</span>/explore?access_token<span class="token operator">=</span><span class="token punctuation">{</span>ACCESS_TOKEN<span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Make an HTTP POST request with your programming language of choice — and boom — you’ve got data. </p><p class="post__p">This is an example of a query that we can use to request data from a single blog post by slug. Notice how we’re using a <code>where</code> clause to filter the items by a <code>slug</code> that matches a <b class="post__p--bold">string</b> we supply.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="eykidziNJo"
      aria-describedby="eykidziNJo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="eykidziNJo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="eykidziNJo" itemprop="text" content="%7B%0A%20%20blogPostCollection(where%3A%20%7Bslug%3A%20%22what-is-a-rest-api%22%7D)%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20slug%0A%20%20%20%20%20%20title%0A%20%20%20%20%20%20excerpt%0A%20%20%20%20%20%20readingTime%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token punctuation">{</span><br>  <span class="token property-query">blogPostCollection</span><span class="token punctuation">(</span><span class="token attr-name">where</span><span class="token punctuation">:</span> <span class="token punctuation">{</span><span class="token attr-name">slug</span><span class="token punctuation">:</span> <span class="token string">"what-is-a-rest-api"</span><span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token property">slug</span><br>      <span class="token property">title</span><br>      <span class="token property">excerpt</span><br>      <span class="token property">readingTime</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s the data we get back.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7uXikzzfXrqTkE3lppxQJr/0719b1f961913c237068038bce5bfde4/query_1.png" alt="A screenshot of the GraphiQL explorer showing a query made using a string in the where clause, and the data successfully returned." height="1108" width="1782" /><p class="post__p">Here’s how we can make the same request using JavaScript fetch (and no external dependencies!).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="hvxVqFCTud"
      aria-describedby="hvxVqFCTud">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="hvxVqFCTud">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="hvxVqFCTud" itemprop="text" content="const%20query%20%3D%20%60%7B%0A%20%20blogPostCollection(where%3A%20%7Bslug%3A%20%22what-is-a-rest-api%22%7D)%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20slug%0A%20%20%20%20%20%20title%0A%20%20%20%20%20%20excerpt%0A%20%20%20%20%20%20readingTime%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%60%3B%0A%0Aconst%20response%20%3D%20await%20fetch(%0A%20%20%60https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%24%7BSPACE_ID%7D%2Fenvironments%2Fmaster%60%2C%0A%20%20%7B%0A%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20Authorization%3A%20%60Bearer%20%25ACCESS_TOKEN%25%60%2C%0A%20%20%20%20%20%20%22content-type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%20%20body%3A%20JSON.stringify(%7B%20query%20%7D)%2C%0A%20%20%7D%2C%0A).then((response)%20%3D%3E%20response.json())%3B%0A%0Aconsole.log(response)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">{<br>  blogPostCollection(where: {slug: "what-is-a-rest-api"}) {<br>    items {<br>      slug<br>      title<br>      excerpt<br>      readingTime<br>    }<br>  }<br>}</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><br>  <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://graphql.contentful.com/content/v1/spaces/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">SPACE_ID</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/environments/master</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>  <span class="token punctuation">{</span><br>    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer %ACCESS_TOKEN%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">All of this is great, and perfectly valid GraphQL. And, if you’re using a static site generator such as Next.js, Gatsby or Nuxt which will pre-render your pages at build-time and serve static pages to the client, you should be good to go. I’d been doing this for months using Contentful’s GraphQL API to fetch my data to power <a href="https://whitep4nth3r.com/" target="_blank">my personal website built with Next.js</a>. </p><p class="post__p">However, whilst queries like this are super-fast to write and get your projects out fast — what if you’re making GraphQL queries dynamically on the client and not as part of a static site build? What if someone could play around with your data in real-time by inserting an incorrect data type, a GraphQL mutation or similar instead of a string? </p><p class="post__p"><b class="post__p--bold">Here’s where GraphQL variables save the day!</b></p><p class="post__p">It’s worth mentioning that because the Contentful GraphQL API is read-only, this kind of scenario won’t happen — but security considerations are always good to bear in mind regardless.<b class="post__p--italic"> </b>Let’s take a look!</p><h2 class="post__h2">Use GraphQL variables for type safety and self-documenting queries</h2><p class="post__p">GraphQL variables offer an extra layer of protection in your queries, namely type safety — meaning that a query will only accept dynamic variables of certain data types, such as String, Int (number), DateTime and so on. And what’s more, <b class="post__p--bold">there isn’t much more work needed to make your GraphQL queries safer!</b></p><p class="post__p">To use variables in your GraphQL queries:</p><ol><li><p class="post__p">Create what looks like a traditional function prefaced by the word <code>query</code>. You can name this query function whatever you like. I’ve named mine <code>GetBlogPostBySlug</code>.</p></li><li><p class="post__p">Inside the parentheses of the function declaration, name and define the types of the variables the query will accept, and prefix your variable names with a <code>$</code>. The query below will accept a variable named <code>$slug</code>, which is of type String. The bang or exclamation mark that comes after the type name means that it <b class="post__p--bold">is a required variable</b> for the query to execute.</p></li><li><p class="post__p">In an HTTP POST request to the GraphQL API, variables are passed to the GraphQL request as a separate property inside the body of the request. Click on the query variables pane at the bottom of the GraphiQL explorer. Create an object, and add your stringified variable name and value as “key”: “value” (it’s important to stringify the key name here!).</p></li></ol><img src="https://images.ctfassets.net/56dzm01z6lln/6MPbNNiS94zsuYXtMWn8kY/d795e918d3ab3b68d35c03c178631abf/query_2.png" alt="A screenshot of the GraphiQL explorer showing the slug variable used correctly, passed in via a separate query variables tab, and the data returned is the same as the previous query." height="1108" width="1782" /><p class="post__p">Let’s look at an example of using GraphQL variables using JavaScript fetch. Notice how we have replaced the original query with the function-style query from above, and have created a variable named <code>variables</code> that we pass into the body of the HTTP request.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="PxsYrubJTs"
      aria-describedby="PxsYrubJTs">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="PxsYrubJTs">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="PxsYrubJTs" itemprop="text" content="const%20query%20%3D%20%60query%20GetBlogPostBySlug(%24slug%3A%20String!)%20%7B%0A%20%20blogPostCollection(where%3A%20%7Bslug%3A%20%24slug%7D)%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20slug%0A%20%20%20%20%20%20title%0A%20%20%20%20%20%20excerpt%0A%20%20%20%20%20%20readingTime%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%60%3B%0A%0Aconst%20variables%20%3D%20%7B%20slug%3A%20%22what-is-a-rest-api%22%20%7D%3B%0A%0Aconst%20response%20%3D%20await%20fetch(%0A%20%20%60https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%24%7BSPACE_ID%7D%2Fenvironments%2Fmaster%60%2C%0A%20%20%7B%0A%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20Authorization%3A%20%60Bearer%20%25ACCESS_TOKEN%25%60%2C%0A%20%20%20%20%20%20%22content-type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%20%20body%3A%20JSON.stringify(%7B%20query%2C%20variables%20%7D)%2C%0A%20%20%7D%2C%0A).then((response)%20%3D%3E%20response.json())%3B%0A%0Aconsole.log(response)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">query GetBlogPostBySlug($slug: String!) {<br>  blogPostCollection(where: {slug: $slug}) {<br>    items {<br>      slug<br>      title<br>      excerpt<br>      readingTime<br>    }<br>  }<br>}</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> variables <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">slug</span><span class="token operator">:</span> <span class="token string">"what-is-a-rest-api"</span> <span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><br>  <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://graphql.contentful.com/content/v1/spaces/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">SPACE_ID</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/environments/master</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>  <span class="token punctuation">{</span><br>    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer %ACCESS_TOKEN%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query<span class="token punctuation">,</span> variables <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">And that’s how I learned to make my GraphQL queries type-safe and free from nasty attacks on dynamic API calls! </p><h2 class="post__h2">Going further with more types</h2><p class="post__p">There are a variety of different variable data types available on Contentful’s GraphQL API. As well as the standard data types such as String, Int and DateTime, you can also pass variables to a query that are entry-specific and API-specific.</p><p class="post__p">To inspect the types available on your schema, click on the Docs links at the top right of the GraphiQL explorer:</p><img src="https://images.ctfassets.net/56dzm01z6lln/5Wzx0O9i60Et4txXSIFXGk/0b1fde44b7be10cfa67e0da834d52f22/query_3.png" alt="A screenshot of the GraphiQL explorer, showing the docs button a the top right highlighted." height="1108" width="1782" /><p class="post__p">Click on Query:</p><img src="https://images.ctfassets.net/56dzm01z6lln/2DnyrmfRnZ9BfxoDg8b6Oh/4273c9f15a21b371722a6f92ab6dae4a/query_4.png" alt="A screenshot of the GraphiQL explorer, showing the docs tab open and the root types — query area highlighted." height="1108" width="1782" /><p class="post__p">And find the content type you’d like to inspect.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3ypTfZyM8HTTVXi73yHODG/7c25e3ed52ecb131de08bad22b96c6cb/query_5.png" alt="A screenshot of the GraphiQL explorer, showing the query tab open to inspect the scheme, with the type order: EventOrder highlighted." height="1108" width="1782" /><p class="post__p">Another thing I learned on this journey is that you can’t use variables in GraphQL for <b class="post__p--italic">everything</b> — namely <b class="post__p--bold">keys</b> in WHERE clauses.</p><p class="post__p">I recently created a GraphQL query to fetch the events on my website (note: this is no longer relevant on my current website!). On the main events page, I wanted to show future events in ascending order, and on the past events page, events in descending order.</p><p class="post__p">The two <b class="post__p--bold">supported</b> variables involved in this query are:</p><ul><li><p class="post__p"><code>$order</code> — date_ASC or date_DESC</p></li><li><p class="post__p"><code>$date</code> — as an ISO string</p></li></ul><p class="post__p">But I also needed a <b class="post__p--bold">third dynamic variable</b> — which was to control whether the API returned events before (<code>date_lt</code> — date less than) or after (<code>date_gt</code> — date greater than) a particular date. Unfortunately, this part of a GraphQL query cannot be controlled with a variable, and so I had to be creative and pass in a calculated string to the query like so:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="MSBoBItdum"
      aria-describedby="MSBoBItdum">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="MSBoBItdum">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="MSBoBItdum" itemprop="text" content="%2F%2F%20https%3A%2F%2Fgithub.com%2Fwhitep4nth3r%2Fp4nth3rblog%2Fblob%2Fmain%2Fcontentful%2FEvents.js%0A%0Aimport%20ContentfulApi%20from%20%22%40contentful%2FApi%22%3B%0A%0Aconst%20defaultOptions%20%3D%20%7B%0A%20%20future%3A%20true%2C%0A%7D%3B%0A%0A%2F*%0A%20*%20Get%20all%20events%20--%20future%20by%20default%0A%20*%2F%0Astatic%20async%20getEvents(options%20%3D%20defaultOptions)%20%7B%0A%20%20%2F%2F%20Calculate%20date_ASC%20for%20future%20events%2C%20or%20date_DESC%20for%20past%20events%0A%20%20const%20order%20%3D%20options.future%20%3F%20%22date_ASC%22%20%3A%20%22date_DESC%22%3B%0A%0A%20%20%2F%2F%20Generate%20today's%20date%0A%20%20const%20date%20%3D%20new%20Date()%3B%0A%0A%20%20%2F%2F%20And%20format%20it%20to%20an%20ISO%20String%0A%20%20const%20formattedDate%20%3D%20date.toISOString()%3B%0A%0A%20%20%2F%2F%20Decide%20on%20the%20date%20filter%20to%20pass%20in%20as%20a%20string%0A%20%20const%20dateFilter%20%3D%20options.future%20%3F%20%22date_gt%22%20%3A%20%22date_lt%22%3B%0A%0A%20%20%2F%2F%20Construct%20variables%20object%20to%20send%20with%20the%20HTTP%20POST%20request%0A%20%20const%20variables%20%3D%20%7B%20date%3A%20formattedDate%2C%20order%20%7D%3B%0A%0A%20%20%2F%2F%20Build%20the%20query%0A%20%20const%20query%20%3D%20%60query%20GetEvents(%24date%3A%20DateTime!%2C%20%24order%3A%20%5BEventOrder%5D!)%20%7B%0A%20%20%20%20eventCollection(where%3A%20%7B%24%7BdateFilter%7D%3A%20%24date%7D%2C%20order%3A%20%24order)%20%7B%0A%20%20%20%20%20%20items%20%7B%0A%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20date%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20link%0A%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%20%20timeTbc%0A%20%20%20%20%20%20%20%20isVirtual%0A%20%20%20%20%20%20%20%20image%20%7B%0A%20%20%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%20%20%20%20height%0A%20%20%20%20%20%20%20%20%20%20width%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%60%3B%0A%0A%20%20%2F%2F%20Call%20out%20to%20the%20base%20API%20call%0A%20%20const%20response%20%3D%20await%20this.callContentful(query%2C%20variables)%3B%0A%0A%20%20const%20eventCollection%20%3D%20response.data.eventCollection.items%0A%20%20%20%20%3F%20response.data.eventCollection.items%0A%20%20%20%20%3A%20%5B%5D%3B%0A%0A%20%20return%20eventCollection%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// https://github.com/whitep4nth3r/p4nth3rblog/blob/main/contentful/Events.js</span><br><br><span class="token keyword">import</span> ContentfulApi <span class="token keyword">from</span> <span class="token string">"@contentful/Api"</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> defaultOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">future</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token comment">/*<br> * Get all events -- future by default<br> */</span><br><span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">getEvents</span><span class="token punctuation">(</span><span class="token parameter">options <span class="token operator">=</span> defaultOptions</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// Calculate date_ASC for future events, or date_DESC for past events</span><br>  <span class="token keyword">const</span> order <span class="token operator">=</span> options<span class="token punctuation">.</span>future <span class="token operator">?</span> <span class="token string">"date_ASC"</span> <span class="token operator">:</span> <span class="token string">"date_DESC"</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Generate today's date</span><br>  <span class="token keyword">const</span> date <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// And format it to an ISO String</span><br>  <span class="token keyword">const</span> formattedDate <span class="token operator">=</span> date<span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Decide on the date filter to pass in as a string</span><br>  <span class="token keyword">const</span> dateFilter <span class="token operator">=</span> options<span class="token punctuation">.</span>future <span class="token operator">?</span> <span class="token string">"date_gt"</span> <span class="token operator">:</span> <span class="token string">"date_lt"</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Construct variables object to send with the HTTP POST request</span><br>  <span class="token keyword">const</span> variables <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">date</span><span class="token operator">:</span> formattedDate<span class="token punctuation">,</span> order <span class="token punctuation">}</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Build the query</span><br>  <span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">query GetEvents($date: DateTime!, $order: [EventOrder]!) {<br>    eventCollection(where: {</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>dateFilter<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">: $date}, order: $order) {<br>      items {<br>        sys {<br>          id<br>        }<br>        date<br>        name<br>        link<br>        description<br>        timeTbc<br>        isVirtual<br>        image {<br>          url<br>          description<br>          height<br>          width<br>        }<br>      }<br>    }<br>  }</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Call out to the base API call</span><br>  <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">callContentful</span><span class="token punctuation">(</span>query<span class="token punctuation">,</span> variables<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> eventCollection <span class="token operator">=</span> response<span class="token punctuation">.</span>data<span class="token punctuation">.</span>eventCollection<span class="token punctuation">.</span>items<br>    <span class="token operator">?</span> response<span class="token punctuation">.</span>data<span class="token punctuation">.</span>eventCollection<span class="token punctuation">.</span>items<br>    <span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> eventCollection<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">One other thing to notice is that the <code>$order</code> variable is of type EventOrder, which we saw when we inspected the schema above, which is an API and entry-specific type!</p><p class="post__p">So there you have it. Fancy and safe GraphQL queries, so you can build great stuff with the <a href="https://graphql.contentful.com/" target="_blank">Contentful GraphQL API</a> without worrying. You can <a href="https://github.com/whitep4nth3r/p4nth3rblog/tree/main/contentful" target="_blank">check out the code on GitHub</a> for the full range of queries I make with GraphQL on my website, and if you’re curious about GraphQL and want to learn more, you can learn along with <a href="https://www.contentful.com/developers/videos/learn-graphql/" target="_blank">Stefan Judis’ React and GraphQL video course</a> in our developer portal. Happy querying, friends!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Next.js Dynamic Routes with Contentful's GraphQL API — with Jason Lengstorf</title>
          <description>In this episode of Learn with Jason, I teach Jason how to use Contentful’s GraphQL API to power Next.js dynamic routes.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/next-js-dynamic-routes-with-contentfuls-graphql-api-with-jason-lengstorf/</link>
          <guid>https://whitep4nth3r.com/blog/next-js-dynamic-routes-with-contentfuls-graphql-api-with-jason-lengstorf/</guid>
          <pubDate>Sun, 10 Oct 2021 23:00:00 GMT</pubDate>
          <category>GraphQL</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I sat down with <a href="https://twitter.com/jlengstorf" target="_blank">Jason Lengstorf</a> to talk about <a href="https://nextjs.org/docs" target="_blank">Next.js</a> and teach him how to generate dynamic routes powered by the <a href="https://graphql.contentful.com" target="_blank">Contentful GraphQL API</a>.</p><p class="post__p">In this video, we cover:</p><ul><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=6" target="_blank">Intro and a bit about my background</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=172" target="_blank">What I like about Next.js and Contentful</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=414" target="_blank">Thoughts on GraphQL vs REST</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=735" target="_blank">GraphQL is more environmentally friendly than REST and making tech more accessible to hobby developers</a> </p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=866" target="_blank">Planning today&#39;s project</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=1231" target="_blank">Setting up the content model in Contentful</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=1610" target="_blank">Adding entries to Contentful</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=1894" target="_blank">Installing the GraphQL Playground app in Contentful</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=2074" target="_blank">Building the GraphQL query in the GraphQL playground</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=2154" target="_blank">Creating a new Next.js application and using the GitHub CLI and Netlify CLI to set up environment variables to deploy</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=2557" target="_blank">Using getStaticProps to fetch data for the home page via the Contentful GraphQL API</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=3049" target="_blank">Building a list of client-side anchor links using next/link</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=3172" target="_blank">Using getStaticPaths to create dynamic routes at build-time</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=3744" target="_blank">Using getStaticProps to fetch data for individual dynamic routes with GraphQL</a> </p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=4161" target="_blank">Styling the front end</a></p></li><li><p class="post__p"><a href="https://youtu.be/OWgeN6O707s?t=4946" target="_blank">Deploying the site to Netlify</a></p></li></ul><p class="post__p">I had a great time on Learn with Jason — and learned a few things along the way, too! I&#39;m excited to speed up my development and deployment process using the <a href="https://cli.netlify.com/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=cli-docs" target="_blank">Netlify CLI</a> next time I launch a new project. </p><p class="post__p">Thanks for having me, Jason!</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/OWgeN6O707s"
        title="Learn with Jason — Next.js Dynamic Routes With Contentful's GraphQL API"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <h2 class="post__h2">Resources and links</h2><ul><li><p class="post__p"><a href="https://graphql.contentful.com/" target="_blank">https://graphql.contentful.com/</a></p></li><li><p class="post__p"><a href="https://jamstackconf.com/2021-videos" target="_blank">https://jamstackconf.com/2021-videos</a></p></li><li><p class="post__p"><a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation" target="_blank">https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation</a></p></li><li><p class="post__p"><a href="https://twitter.com/jamstackconf/status/1446132787480649735" target="_blank">https://twitter.com/jamstackconf/status/1446132787480649735</a></p></li><li><p class="post__p"><a href="https://github.com/whitep4nth3r/nextjs-contentful-blog-starter" target="_blank">https://github.com/whitep4nth3r/nextjs-contentful-blog-starter</a></p></li><li><p class="post__p"><a href="https://jamstack-memories.netlify.app/" target="_blank">https://jamstack-memories.netlify.app</a></p></li><li><p class="post__p"><a href="https://www.learnwithjason.dev/next-js-dynamic-routes-with-contentful-s-graphql-api" target="_blank">View the video transcript</a></p></li></ul><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>What is a REST API?</title>
          <description>REST? API? What does it all mean? Let’s break it down.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/10/04/what-is-a-rest-api/</link>
          <guid>https://www.contentful.com/blog/2021/10/04/what-is-a-rest-api/</guid>
          <pubDate>Mon, 04 Oct 2021 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you’ve had a look at the Contentful docs, you’ll have seen the acronyms REST and API in at least a few places! But what does it all mean? Let’s break it down.</p><h2 class="post__h2">What is an API?</h2><p class="post__p">API stands for Application Programming Interface, which is a way to communicate between different software services. Different types of APIs are used in programming hardware and software, including operating system APIs, remote APIs and web APIs — like the APIs that Contentful provides. If you’re new to development, I recommend you check out this blog post — <a href="https://whitep4nth3r.com/blog/what-is-an-api/">What is an API?</a> to get a good grounding on APIs and HTTP, in order to prepare for what we’re going to cover in this post.</p><p class="post__p">Now that you’ve got up to speed on APIs and HTTP, how does REST fit into it? Let’s take a look.</p><h2 class="post__h2">What does REST mean?</h2><p class="post__p">REST is an acronym for Representational State Transfer, which is a term introduced in 2000 by American computer scientist Roy Fielding in his dissertation “<a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm" target="_blank">Architectural Styles and the Design of Network-based Software Architectures</a>.” REST is a set of rules and guidelines for creating resource-based web services that are stateless (we’ll unpack this later).</p><p class="post__p">You might hear REST APIs described as RESTful, or following a RESTful architecture.</p><h2 class="post__h2">Rules of Representational State Transfer</h2><p class="post__p">Let’s take a look at some of the most important concepts of a RESTful API. This list is not exhaustive but is designed to give an overview of how REST architecture is designed. Not all APIs are RESTful APIs — and some APIs take inspiration from the principles of REST without following the rules completely.</p><h2 class="post__h2">RESTful APIs have a predefined set of operations available to a user</h2><p class="post__p">Developers and documentation often talk about “functionality being exposed via an API.” APIs usually do not allow for complete control over back-end systems. Instead, predefined functionality and operations are made available to users of APIs. Good APIs come with great documentation that describes how to send data in the correct format and what data to expect from each piece of API functionality. </p><p class="post__p">New features are often added to APIs using a versioning system to ensure that large-scale applications are protected from potential breaking changes. As a result, you may end up working with differently versioned API URLs in your applications. For example, you might see <code>https://myawesomeapi.dev/api/v1/users</code> vs <code>https://myawesomeapi.dev/api/v2/users</code> in the wild. Notice the difference between the URLs — /v1 vs /v2.</p><h2 class="post__h2">RESTful APIs are based on resources</h2><p class="post__p">Responses from RESTful API calls over HTTP — usually called payloads — are returned as HTML, XML, JSON or similar text-based representations of resources that exist as stored objects. A stored object — or resource — can be <b class="post__p--italic">anything</b>, such as a blog post stored in a document database, a URL to a hosted image, or data about a user stored across multiple tables in a relational database.</p><p class="post__p">The format of the URLs for RESTful API endpoints are descriptive and self-documenting. Each URL — requesting a resource or requesting to modify a resource — describes <b class="post__p--italic">what type of resource </b>is being requested from the API. </p><p class="post__p">For example, here’s a <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/" target="_blank">Contentful Content Delivery API</a> (CDA) URL that requests information about a single Contentful space via an HTTP GET request. A Contentful space is like a bucket for your Contentful content, which has a name and unique ID.</p><p class="post__p">Notice the <code>/spaces</code> part of the URL — which defines that the resource we’re asking for is data that represents a space.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="AWxyLWjQhr"
      aria-describedby="AWxyLWjQhr">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="AWxyLWjQhr">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="AWxyLWjQhr" itemprop="text" content="https%3A%2F%2Fcdn.contentful.com%2Fspaces%2F%7Bspace_id%7D%3Faccess_token%3D%7Baccess_token%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">https</span><span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span>cdn<span class="token punctuation">.</span>contentful<span class="token punctuation">.</span>com<span class="token operator">/</span>spaces<span class="token operator">/</span><span class="token punctuation">{</span>space_id<span class="token punctuation">}</span><span class="token operator">?</span>access_token<span class="token operator">=</span><span class="token punctuation">{</span>access_token<span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">The URL requires two dynamic URL parameters — <code>space_id</code> and <code>access_token</code>. The <code>space_id</code> is the unique identifier of the space we’d like to get information about from the database, and the <code>access_token</code> is for authentication (more on this later when we talk about how REST APIs are stateless).</p><p class="post__p">Here’s a second example from the <a href="https://www.contentful.com/developers/docs/references/content-management-api/#/reference/environments/environment-collection/get-all-environments-of-a-space/console/curl" target="_blank">Contentful Content Management API</a> (CMA), this time asking for a specified <b class="post__p--bold">environment</b> resource available inside a space via an HTTP GET request (see <b class="post__p--bold">/environments </b>in the URL). Contentful spaces allow for multiple environments in order to manage content and feature migrations in your applications. This API endpoint takes two dynamic URL parameters — <code>space_id</code> and <code>environment_id</code>, and requires an authentication token to be sent via an HTTP Authorization header.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xOTBjhDOnI"
      aria-describedby="xOTBjhDOnI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xOTBjhDOnI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="xOTBjhDOnI" itemprop="text" content="https%3A%2F%2Fapi.contentful.com%2Fspaces%2F%7Bspace_id%7D%2Fenvironments%2F%7Benviroment_id%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">https</span><span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span>api<span class="token punctuation">.</span>contentful<span class="token punctuation">.</span>com<span class="token operator">/</span>spaces<span class="token operator">/</span><span class="token punctuation">{</span>space_id<span class="token punctuation">}</span><span class="token operator">/</span>environments<span class="token operator">/</span><span class="token punctuation">{</span>enviroment_id<span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">RESTful APIs allow resources to be read or modified</h2><p class="post__p">In the examples above, we used HTTP GET requests to <b class="post__p--bold">read </b>information about a Contentful space from the CDA and <b class="post__p--bold">read </b>information about an environment inside a space from the CMA. In addition to retrieving information about stored resources, RESTful APIs also allow you to <b class="post__p--bold">create, update </b>or <b class="post__p--bold">delete</b> resources in separate API calls. </p><p class="post__p">Using the CMA, we can also create, update or delete resources using HTTP POST, PUT and DELETE methods. What’s great about this, is that not only do RESTful API URLs describe the <b class="post__p--bold">resources</b> concerned in the format of the API URLs, they also describe the <b class="post__p--bold">action</b> you’re performing on the API according to the HTTP method implemented.</p><p class="post__p">You may have heard of the acronym <b class="post__p--bold">CRUD</b> when referring to RESTful API architecture. CRUD stands for <b class="post__p--bold">Create, Read, Update, Delete </b>— which are the standard actions available on RESTful APIs over HTTP.</p><p class="post__p">Now we know how to <b class="post__p--bold">read</b> from the Contentful APIs, let’s take a look at how we can <b class="post__p--bold">create</b>, <b class="post__p--bold">update</b> and <b class="post__p--bold">delete</b>.</p><h2 class="post__h2">Creating a new environment with HTTP POST</h2><p class="post__p">Use HTTP POST to create a new environment with the CMA. </p><p class="post__p">Send a POST request to the following URL:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VJxRBcbNcX"
      aria-describedby="VJxRBcbNcX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VJxRBcbNcX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="VJxRBcbNcX" itemprop="text" content="https%3A%2F%2Fapi.contentful.com%2Fspaces%2F%7Bspace_id%7D%2Fenvironments">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">https</span><span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span>api<span class="token punctuation">.</span>contentful<span class="token punctuation">.</span>com<span class="token operator">/</span>spaces<span class="token operator">/</span><span class="token punctuation">{</span>space_id<span class="token punctuation">}</span><span class="token operator">/</span>environments</code></pre>
    </div>
  </div>

  <p class="post__p">With the following HTTP headers:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DsRlEVghuW"
      aria-describedby="DsRlEVghuW">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DsRlEVghuW">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="DsRlEVghuW" itemprop="text" content="Authorization%3A%20Bearer%20%3Ccma_token%3E%0A%22Content-Type%22%3A%20application%2Fvnd.contentful.management.v1%2Bjson">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">Authorization</span><span class="token operator">:</span> Bearer <span class="token operator">&lt;</span>cma_token<span class="token operator">></span><br><span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> application<span class="token operator">/</span>vnd<span class="token punctuation">.</span>contentful<span class="token punctuation">.</span>management<span class="token punctuation">.</span>v1<span class="token operator">+</span>json</code></pre>
    </div>
  </div>

  <p class="post__p">And the name of the environment you wish to create in the body of the request:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DuPAUbdUgG"
      aria-describedby="DuPAUbdUgG">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DuPAUbdUgG">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="DuPAUbdUgG" itemprop="text" content="body%3A%20%7B%0A%20%20%22name%22%3A%20%22My%20new%20environment%20name%22%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  <span class="token string-property property">"name"</span><span class="token operator">:</span> <span class="token string">"My new environment name"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">An HTTP POST request to this API URL creates a new environment with an auto-generated ID. While it’s perfectly fine to create an environment with an auto-generated ID, we recommend creating environments with a specified ID to have more control in your automation scripts. And the good news is, you can do this with an HTTP PUT request.</p><h2 class="post__h2">Creating or updating an environment with HTTP PUT</h2><p class="post__p">I like to think of the HTTP PUT method as literally putting more specific information into the data store, rather than sending off a request via POST for the API to create something for me.</p><p class="post__p">To create a new environment with a specified ID, send an HTTP PUT request to the following URL, where <code>{environment_id}</code> is the ID you wish to specify.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="NFwRtmzdZh"
      aria-describedby="NFwRtmzdZh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="NFwRtmzdZh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="NFwRtmzdZh" itemprop="text" content="https%3A%2F%2Fapi.contentful.com%2Fspaces%2F%7Bspace_id%7D%2Fenvironments%2F%7Benviroment_id%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">https</span><span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span>api<span class="token punctuation">.</span>contentful<span class="token punctuation">.</span>com<span class="token operator">/</span>spaces<span class="token operator">/</span><span class="token punctuation">{</span>space_id<span class="token punctuation">}</span><span class="token operator">/</span>environments<span class="token operator">/</span><span class="token punctuation">{</span>enviroment_id<span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Use the following HTTP headers:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="wMcHchCvlX"
      aria-describedby="wMcHchCvlX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="wMcHchCvlX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="wMcHchCvlX" itemprop="text" content="Authorization%3A%20Bearer%20%3Ccma_token%3E%0A%22Content-Type%22%3A%20application%2Fvnd.contentful.management.v1%2Bjson">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">Authorization</span><span class="token operator">:</span> Bearer <span class="token operator">&lt;</span>cma_token<span class="token operator">></span><br><span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> application<span class="token operator">/</span>vnd<span class="token punctuation">.</span>contentful<span class="token punctuation">.</span>management<span class="token punctuation">.</span>v1<span class="token operator">+</span>json</code></pre>
    </div>
  </div>

  <p class="post__p">And send the name of the environment you wish to create in the body of the request:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KSrLryLQqc"
      aria-describedby="KSrLryLQqc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KSrLryLQqc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="KSrLryLQqc" itemprop="text" content="body%3A%20%7B%0A%20%20%22name%22%3A%20%22My%20new%20environment%20name%22%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  <span class="token string-property property">"name"</span><span class="token operator">:</span> <span class="token string">"My new environment name"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">To update an environment, <b class="post__p--bold">make the same API call as above</b>, but with an additional HTTP header, specifying the last version of the environment you are updating, like so:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VIrZIvSTHv"
      aria-describedby="VIrZIvSTHv">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VIrZIvSTHv">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="VIrZIvSTHv" itemprop="text" content="X-Contentful-Version%3A%20%3Cexisting_version%3E">
      <pre class="language-javascript"><code class="language-javascript"><span class="token constant">X</span><span class="token operator">-</span>Contentful<span class="token operator">-</span>Version<span class="token operator">:</span> <span class="token operator">&lt;</span>existing_version<span class="token operator">></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Deleting an environment with HTTP DELETE</h2><p class="post__p">To delete a particular environment within a space, send an HTTP DELETE request to the following URL, where <code>{environment_id}</code> is the ID of the environment you wish to delete:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="QFYOgnmgdn"
      aria-describedby="QFYOgnmgdn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="QFYOgnmgdn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="QFYOgnmgdn" itemprop="text" content="https%3A%2F%2Fapi.contentful.com%2Fspaces%2F%7Bspace_id%7D%2Fenvironments%2F%7Benviroment_id%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">https</span><span class="token operator">:</span><span class="token operator">/</span><span class="token operator">/</span>api<span class="token punctuation">.</span>contentful<span class="token punctuation">.</span>com<span class="token operator">/</span>spaces<span class="token operator">/</span><span class="token punctuation">{</span>space_id<span class="token punctuation">}</span><span class="token operator">/</span>environments<span class="token operator">/</span><span class="token punctuation">{</span>enviroment_id<span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And use the following HTTP header:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fWCFbuPfTD"
      aria-describedby="fWCFbuPfTD">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fWCFbuPfTD">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="fWCFbuPfTD" itemprop="text" content="Authorization%3A%20Bearer%20%3Ccma_token%3E">
      <pre class="language-javascript"><code class="language-javascript"><span class="token literal-property property">Authorization</span><span class="token operator">:</span> Bearer <span class="token operator">&lt;</span>cma_token<span class="token operator">></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Now, here’s the fun bit!</h2><p class="post__p">Notice how the API URLs to create with a specified ID, read, update and delete an environment <u><b class="post__p--bold">are all exactly the same</b></u>! </p><p class="post__p">RESTful API calls are performed on a single URL representing the type of resource you wish to create or modify. The difference is that the read method uses an HTTP GET request, the create and update method uses HTTP PUT (with different HTTP headers depending on the action you want to perform), and the delete method uses HTTP DELETE.</p><p class="post__p">This is where RESTful architecture really shines. The combination of the HTTP methods coupled with the resource-based URLs and different HTTP headers makes navigating and working with RESTful APIs pretty intuitive and very well structured.</p><p class="post__p"><a href="https://www.contentful.com/developers/docs/references/content-management-api/#/reference/environments/environment" target="_blank">Click here to see the CMA environments documentation referenced above in full.</a></p><h2 class="post__h2">RESTful APIs are stateless</h2><p class="post__p">Stateless in this context means no information is passed between different interactions or API calls. In stateless architecture, each request to an API must be processed based only on the information sent with the request at the time. For example, if you make a call to the API with an authentication token, the API won’t remember that you’re authenticated in additional requests. If you wish to make more calls to the same API, you must send an authentication token in all subsequent requests. </p><p class="post__p">You’ll notice that in each API request above to the CMA, an authorization header was included with each request. Similarly, in the first GET request from the CDA, the authentication token was passed as a URL parameter.</p><h2 class="post__h2">In summary</h2><p class="post__p">An API is a way to communicate between different software services using code. REST is a set of rules and guidelines for creating a particular type of API, and not all APIs are RESTful APIs. </p><p class="post__p">REST stands for Representation State Transfer. RESTful APIs:</p><ul><li><p class="post__p">Have a predefined set of operations available to a user</p></li><li><p class="post__p">Are based on resources</p></li><li><p class="post__p">Usually allow CRUD operations on resources — and CRUD stands for <b class="post__p--bold">Create</b>, <b class="post__p--bold">Read</b>, <b class="post__p--bold">Update</b>, <b class="post__p--bold">Delete</b></p></li><li><p class="post__p">Are stateless</p></li></ul><p class="post__p">Explore the <a href="https://www.contentful.com/developers/docs/" target="_blank"><u>Contentful documentation</u></a> to solidify your understanding.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Ecommerce and Next.js on the Jamstack — with Colby Fayock</title>
          <description>I sat down with Colby Fayock to chat about Next.js and how all of its features help enable developers to build great experiences on the web.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/ecommerce-next-js-jamstack-colby-fayock/</link>
          <guid>https://whitep4nth3r.com/blog/ecommerce-next-js-jamstack-colby-fayock/</guid>
          <pubDate>Thu, 16 Sep 2021 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I sat down with <a href="https://twitter.com/colbyfayock" target="_blank">Colby Fayock</a> to talk about <a href="https://nextjs.org/docs" target="_blank">Next.js</a>, how all of its features help enable developers to build great experiences on the web, and how it can help solve the many challenges of building e-commerce sites on the Jamstack.</p><p class="post__p">In this video, we cover:</p><ul><li><p class="post__p">What is Next.js?</p></li><li><p class="post__p">Is Next.js a static site generator?</p></li><li><p class="post__p">What does server-side rendering mean?</p></li><li><p class="post__p">What makes Next.js a strong framework?</p></li><li><p class="post__p">What are some e-commerce challenges that Next.js helps to solve?</p></li><li><p class="post__p">How is performance important to e-commerce?</p></li><li><p class="post__p">How does Next.js help with performance?</p></li><li><p class="post__p">How is SEO important to e-commerce?</p></li><li><p class="post__p">How does Next.js help with SEO?</p></li><li><p class="post__p">What&#39;s the most overlooked, yet oddly difficult part of e-commerce?</p></li><li><p class="post__p">Accessibility, internationalisation and personalisation in e-commerce</p></li></ul><p class="post__p">Colby is a super-nice host and I can&#39;t wait to hang out with him again. Thanks for having me, Colby!</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/GyXyygeC2RE"
        title="Ecomm & Next.js with Salma AKA whitep4nth3r — Ecommerce on the Jamstack"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to build a personalized image social sharing app with Cloudinary and Next.js</title>
          <description>Encourage your event attendees to share their tickets for your next virtual event!</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/09/08/personalized-image-social-sharing-with-cloudinary-nextjs/</link>
          <guid>https://www.contentful.com/blog/2021/09/08/personalized-image-social-sharing-with-cloudinary-nextjs/</guid>
          <pubDate>Tue, 07 Sep 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Web Dev</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Have you seen <a href="https://tickets.contentful.com/fastforward2021?name=Salma&shared=true" target="_blank">Contentful’s event website that generates customized and shareable ticket images</a> that we released for our annual conference <a href="https://www.contentful.com/fast-forward" target="_blank">Fast Forward</a>?</p><img src="https://images.ctfassets.net/56dzm01z6lln/6sAmdgwpQze8AWyZuf2xDs/7510874600640738eb15a565c796d823/full_page_screenshot.png" alt="A screenshot from the Fast Forward ticket website with the text "Salma is going!", a personalised ticket image, and a sign up CTA." height="1076" width="2032" /><p class="post__p">As events continue to evolve in the digital landscape, you might have seen some fun and engaging personalized event tickets shared on social media for <a href="https://graphqlconf.org/ticket/salma-alam-naylor-002351" target="_blank">GraphQL Conf</a> and <a href="https://nextjs.org/conf/tickets/jun21/whitep4nth3r#room-mszfy" target="_blank">Next.js Conf</a> in 2021. I love this idea — not only for the fun factor. It also showcases just how many great low-cost services and capabilities exist in web development.</p><p class="post__p">In this post, we’re going to build a front-end application with <a href="https://nextjs.org/" target="_blank">Next.js</a> and <a href="https://cloudinary.com/" target="_blank">Cloudinary</a> that creates a personalized image of a ticket based on URL parameters to share on Twitter and LinkedIn.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7qSeksTTegk5vhQqjqKkEd/5a45ab8c594490944c95244e04565d98/ticket_montage.png" alt="A collage of GraphQL conf, Nextjs Conf and Fast Forward Conf social sharing event tickets in a range of desktop and mobile views." height="1839" width="1871" /><p class="post__p">We’ll also configure the app to behave differently for the ticket holder and anyone else who views the ticket. The beauty of this approach is that the possibilities for dynamic image sharing are endless. Previously, I wrote about <a href="https://whitep4nth3r.com/blog/puppeteer-node-open-graph-screenshot-for-socials/">3 ways to use Puppeteer to generate Open Graph images</a>. However, building this functionality with Cloudinary was so much simpler that I’m thinking of switching to this method for the Open Graph images on my blog! 🙈</p><p class="post__p">Here’s a sneak preview of what we’ll build. The name parameter in the URL provides a name to embed on the image itself via the Cloudinary API rather than being overlaid via HTML and CSS. We’ll also generate a random ticket number and configure the app to behave differently for viewers who aren’t ticket holders.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1iuZGdeCPTfR1e5pAsDiII/8e22c23a35e55fd103732ea027f0003b/param_to_image.png" alt="A screenshot of the Fast Forward ticket app, showing how the parameters from the URL translate to the content on the page. You can see that the name parameter from the URL is embedded onto the image of the ticket, and the random ticket number is also highlighted at the right of the ticket." height="1169" width="2032" /><p class="post__p">The only thing you’ll need for this tutorial is an image you’d like to personalize. Let’s get started!</p><h2 class="post__h2">Sign up for Cloudinary</h2><p class="post__p"><a href="https://cloudinary.com/" target="_blank">Cloudinary</a> is an image and video asset management service that provides an API for customizing your media on the fly. Add text to your images, style it with colors and custom fonts, crop, rotate, resize, recolour, detect faces… it&#39;s pretty powerful!</p><p class="post__p">Head on over to Cloudinary and click <a href="https://cloudinary.com/users/register/free" target="_blank">sign up for free</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/422JHuO1Bq8v9NLgUkGiZr/70e2212bdbdd71d3f67334b1ae92c95c/sign_up_cloudinary.png" alt="A screenshot of the Cloudinary sign up form." height="1133" width="690" /><p class="post__p">After you’ve confirmed your email address, log in to Cloudinary and you’ll be greeted with a welcome screen.</p><img src="https://images.ctfassets.net/56dzm01z6lln/23pLh17URJmG6XUvOA8BvX/29f774d71cbe2cafcd9510cdc5323fd7/cloudinary_welcome.png" alt="A screenshot of the Cloudinary welcome screen that you see after you log in to a fresh account." height="1169" width="1330" /><h2 class="post__h2">Upload your assets to Cloudinary</h2><p class="post__p">Click on the Media Library navigation item and click Upload in the top right corner. Select your template image and after a second or two, you’ll see the new image asset in your dashboard.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5n0oknEUxWDfILCZxLfvMI/d4a18f28b00a9b5026e167b19d0b37cb/cloudinary_image_uploaded.png" alt="A screenshot of the media upload area in Cloudinary, showing a blue border around the new ticket image I just uploaded." height="778" width="1176" /><p class="post__p">Your asset will be uploaded with an auto-generated suffix. Click on the asset to open it in the preview pane to change the file name so that it’s easier for you to recognize the image name in the code later on.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4BBZ1i7YMDQqBh21EDZcTU/104fb78418e2ee6752e4d227de9eb663/cloudinary_manage_asset.png" alt="A screenshot of the Cloudinary media upload screen, showing the new image asset just uploaded selected. On the right, an edit panel is revealed, showing that I edited the auto-generated image name for ease of use later." height="910" width="1021" /><p class="post__p">I also uploaded some custom fonts to Cloudinary to ensure the image personalizations were on brand for Contentful. Given that you can use a variety of Google fonts with the Cloudinary API, I won’t cover fonts in this post, but you can learn how to upload custom fonts via the Cloudinary media library from <a href="https://www.learnwithjason.dev/blog/upload-custom-font-cloudinary-media-library/" target="_blank">this post by Jason Lengstorf</a>.</p><p class="post__p">Now our image asset is stored safely in Cloudinary. Let’s get coding!</p><h2 class="post__h2">Create a new Next.js app</h2><p class="post__p">I chose Next.js for this application to harness the power of server-side rendering using URL parameters, which will power the image personalization.</p><p class="post__p">To spin up a new Next.js application, run the following command in your terminal:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="MwXmBSxJIm"
      aria-describedby="MwXmBSxJIm">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="MwXmBSxJIm">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="MwXmBSxJIm" itemprop="text" content="npx%20create-next-app%20ticket-app">
      <pre class="language-bash"><code class="language-bash">npx create-next-app ticket-app</code></pre>
    </div>
  </div>

  <p class="post__p">This command creates a new directory that includes all code to get started. The output below is what you should see after you run the command in your terminal. (I’ve truncated the output a little with ‘/* more things happen here */’ but what you’re looking for is ✨ Done!)</p><img src="https://images.ctfassets.net/56dzm01z6lln/q6kgKHoxWw92Flt2xdEr1/e635129d148fe3cde37c4b0149ba0e9a/create_next_app.png" alt="A screenshot of a terminal window showing the truncated output of the create next app command." height="1417" width="1072" /><p class="post__p">Navigate to the root of your project directory and start the development server:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ezIBuFxwib"
      aria-describedby="ezIBuFxwib">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ezIBuFxwib">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="ezIBuFxwib" itemprop="text" content="cd%20ticket-app%0Anpm%20run%20dev">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> ticket-app<br><span class="token function">npm</span> run dev</code></pre>
    </div>
  </div>

  <p class="post__p">Navigate to <a href="https://localhost:3000" target="_blank"><u>https://localhost:3000</u></a> in your browser and you’ll see your fresh Next.js app in action.</p><img src="https://images.ctfassets.net/56dzm01z6lln/t8xMuDNFpBLQX06n9BNQd/d36deb4c5f66852f8afd335158dd77a7/fresh_next_app.png" alt="A screenshot of the index page of a fresh Next.js app. The heading reads "Welcome to Next.js" and below are some links to documentation." height="1036" width="1347" /><p class="post__p">Let’s build our page for the ticket!</p><h2 class="post__h2">Build your page</h2><p class="post__p">In a Next.js application, any JavaScript file you add to the pages directory becomes a route on the front end. You can choose to work on <code>index.js</code> or create a new file in the pages directory. In the final implementation, I created <code>fast-forward.js</code> inside the pages directory to allow for the fact that the app will be used for future events. To account for this, I made sure all requests for the index were redirected to the current event page. For the purpose of this tutorial, we’ll work on index.js and serve the generated tickets under the root URL `/`.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3HzaLtfliqxTSBimtUeipL/ab9c933323c7cfabec463fbc70574668/index_js_file_tree.png" alt="A screenshot of the file tree in VS code. It shows a pages folder with index.js highlight inside it." height="545" width="676" /><h3 class="post__h3">Start with a blank slate</h3><p class="post__p">Delete most of the boilerplate code from index.js until you’re left with a nice blank canvas to work with:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="GXidGCVkoA"
      aria-describedby="GXidGCVkoA">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="GXidGCVkoA">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="GXidGCVkoA" itemprop="text" content="%2F%2F%20pages%2Findex.js%0A%0Aimport%20Head%20from%20%22next%2Fhead%22%3B%0A%0A%2F*%20We'll%20write%20some%20functions%20here!%20*%2F%0A%0Aexport%20default%20function%20Index()%20%7B%0A%20%20%2F*%20We'll%20configure%20the%20event%20data%20here!%20*%2F%0A%0A%20%20return%20(%0A%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%3CHead%3E%0A%20%20%20%20%20%20%20%20%3Ctitle%3EMy%20awesome%20event%3C%2Ftitle%3E%0A%20%20%20%20%20%20%3C%2FHead%3E%0A%0A%20%20%20%20%20%20%7B%2F*%20We'll%20build%20our%20page%20here!%20*%2F%7D%0A%20%20%20%20%3C%2Fmain%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// pages/index.js</span><br><br><span class="token keyword">import</span> Head <span class="token keyword">from</span> <span class="token string">"next/head"</span><span class="token punctuation">;</span><br><br><span class="token comment">/* We'll write some functions here! */</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Index</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* We'll configure the event data here! */</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>main<span class="token operator">></span><br>      <span class="token operator">&lt;</span>Head<span class="token operator">></span><br>        <span class="token operator">&lt;</span>title<span class="token operator">></span>My awesome event<span class="token operator">&lt;</span><span class="token operator">/</span>title<span class="token operator">></span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>Head<span class="token operator">></span><br><br>      <span class="token punctuation">{</span><span class="token comment">/* We'll build our page here! */</span><span class="token punctuation">}</span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>main<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Configure the server-side props</h3><p class="post__p">The image stored in Cloudinary will be personalized with the name of the ticket holder, grabbed from a URL parameter. Here’s the URL we’ll be working with in development.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="zmzWKitKJq"
      aria-describedby="zmzWKitKJq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="zmzWKitKJq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="zmzWKitKJq" itemprop="text" content="http%3A%2F%2Flocalhost%3A3000%3Fname%3Dwhitep4nth3r">
      <pre class="language-bash"><code class="language-bash">http://localhost:3000?name<span class="token operator">=</span>whitep4nth3r</code></pre>
    </div>
  </div>

  <p class="post__p">In a pure JavaScript application, you can process the URL parameter on the client-side to build the page content — but with Next.js we can use <code>getServerSideProps()</code> to render the page on the server using the value of the URL parameter. This prevents visitors to your page from seeing a flash of undefined content or having to show a loading state as the parameter is read by the browser. <a href="https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering" target="_blank">Read more about getServerSideProps() on the Next.js documentation</a>.</p><p class="post__p">Add the following <code>getServersideProps()</code> function to the bottom of your index.js file. This function will be called with a context object, from which we can destructure the query parameters. We’ll display the <code>name</code> query parameter on the ticket, and we’ll use the <code>isShared</code> parameter to configure how the page looks depending on whether the page has been visited by the ticket holder, or shared and visited via social media.</p><p class="post__p">Next, configure the Index component to take in the <code>name</code> and <code>isShared</code> props.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DGZCIDIkUf"
      aria-describedby="DGZCIDIkUf">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DGZCIDIkUf">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="DGZCIDIkUf" itemprop="text" content="%2F%2F%20pages%2Findex.js%0A%0Aimport%20Head%20from%20%22next%2Fhead%22%3B%0A%0A%2F*%20We'll%20write%20some%20functions%20here!%20*%2F%0A%0Aexport%20default%20function%20Index(%7B%20name%2C%20isShared%20%7D)%20%7B%0A%20%20%2F*%20We'll%20configure%20the%20event%20data%20here!%20*%2F%0A%0A%20%20return%20(%0A%20%20%20%20%2F*%20%E2%80%A6%20*%2F%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getServerSideProps(context)%20%7B%0A%20%20const%20%7B%20name%2C%20shared%20%7D%20%3D%20context.query%3B%0A%0A%20%20const%20isShared%20%3D%20shared%20!%3D%3D%20undefined%3B%0A%0A%20%20%2F%2F%20return%20the%20properties%20so%20they%20are%20available%20in%20the%20%60Index%60%20component%0A%20%20return%20%7B%0A%20%20%20%20props%3A%20%7B%0A%20%20%20%20%20%20name%3A%20decodeURI(name)%2C%0A%20%20%20%20%20%20isShared%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// pages/index.js</span><br><br><span class="token keyword">import</span> Head <span class="token keyword">from</span> <span class="token string">"next/head"</span><span class="token punctuation">;</span><br><br><span class="token comment">/* We'll write some functions here! */</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Index</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> name<span class="token punctuation">,</span> isShared <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* We'll configure the event data here! */</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token comment">/* … */</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getServerSideProps</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> name<span class="token punctuation">,</span> shared <span class="token punctuation">}</span> <span class="token operator">=</span> context<span class="token punctuation">.</span>query<span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> isShared <span class="token operator">=</span> shared <span class="token operator">!==</span> <span class="token keyword">undefined</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// return the properties so they are available in the `Index` component</span><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token function">decodeURI</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token punctuation">,</span><br>      isShared<span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, let’s set up a few event variables to reuse in a few places to avoid lots of copying and pasting.</p><h3 class="post__h3">Configure your event details</h3><p class="post__p">Set up the following variables inside your Index component: <code>eventName</code>, <code>ticketAppUrl</code>, <code>title</code> and <code>description</code>. We’ll use these values later.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="sdLOiMrZcw"
      aria-describedby="sdLOiMrZcw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="sdLOiMrZcw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="sdLOiMrZcw" itemprop="text" content="%2F%2F%20pages%2Findex.js%0A%0Aimport%20Head%20from%20%22next%2Fhead%22%3B%0A%0A%2F*%20We'll%20write%20some%20functions%20here!%20*%2F%0A%0Aexport%20default%20function%20Index(%7B%20name%2C%20isShared%20%7D)%20%7B%0A%20%20%2F*%20Event%20info%20config%20*%2F%0A%20%20const%20eventName%20%3D%20%22My%20awesome%20event%22%3B%0A%20%20const%20ticketAppUrl%20%3D%20%22https%3A%2F%2Fmy-awesome-ticket-app.dev%22%3B%0A%20%20const%20title%20%3D%20%60%24%7BdecodeURIComponent(name)%7D%20is%20Going!%20%7C%20%24%7BeventName%7D%60%3B%0A%20%20const%20description%20%3D%20%60Join%20%24%7Bname%7D%20at%20%24%7BeventName%7D.%20Grab%20your%20free%20ticket%20on%20%24%7BticketAppUrl%7D.%60%3B%0A%0A%20%20return%20(%0A%20%20%20%20%2F*%20...%20*%2F%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getServerSideProps(context)%20%7B%0A%20%2F*%20...%20*%2F%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// pages/index.js</span><br><br><span class="token keyword">import</span> Head <span class="token keyword">from</span> <span class="token string">"next/head"</span><span class="token punctuation">;</span><br><br><span class="token comment">/* We'll write some functions here! */</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Index</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> name<span class="token punctuation">,</span> isShared <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* Event info config */</span><br>  <span class="token keyword">const</span> eventName <span class="token operator">=</span> <span class="token string">"My awesome event"</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> ticketAppUrl <span class="token operator">=</span> <span class="token string">"https://my-awesome-ticket-app.dev"</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> title <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">decodeURIComponent</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> is Going! | </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>eventName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> description <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Join </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> at </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>eventName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">. Grab your free ticket on </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketAppUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token comment">/* ... */</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getServerSideProps</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token comment">/* ... */</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Optional: generate a ticket number (if you don’t have one)</h3><p class="post__p">I didn’t have access to legitimate ticket numbers for the Fast Forward 2021 event, but I still wanted to include a unique-<b class="post__p--italic">ish </b>ticket number in the design to make the personalized tickets look more official. The code in the final implementation generates a number from any given string, and the return value is prefixed with 000. Each unique string produces a unique number — the only caveat to this method being that if more than one person named “whitep4nth3r” receives a ticket to Fast Forward, then their ticket numbers will be the same. You get the gist. 🙈 </p><p class="post__p">For the purposes of this tutorial, we can use <code>Math.random()</code> to generate a ticket number.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="elUrPqPImB"
      aria-describedby="elUrPqPImB">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="elUrPqPImB">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="elUrPqPImB" itemprop="text" content="%2F%2F%20pages%2Findex.js%0A%0Aimport%20Head%20from%20%22next%2Fhead%22%3B%0A%0Aexport%20default%20function%20Index(%7B%20name%2C%20isShared%20%7D)%20%7B%0A%20%20%2F*%20Event%20info%20config...%20*%2F%0A%0A%20%20%2F*%20Generate%20a%20fake%20ticket%20number%20*%2F%0A%20%20const%20ticketNo%20%3D%20%60000%24%7BMath.random().toString().substr(2%2C%204)%7D%60%3B%0A%0A%20%20return%20(%0A%20%20%20%20%2F*%20...%20*%2F%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getServerSideProps(context)%20%7B%0A%20%20%2F*%20...%20*%2F%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// pages/index.js</span><br><br><span class="token keyword">import</span> Head <span class="token keyword">from</span> <span class="token string">"next/head"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Index</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> name<span class="token punctuation">,</span> isShared <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* Event info config... */</span><br><br>  <span class="token comment">/* Generate a fake ticket number */</span><br>  <span class="token keyword">const</span> ticketNo <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">000</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">substr</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token comment">/* ... */</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getServerSideProps</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* ... */</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Now that we’ve configured the data, we need to personalize the image using Cloudinary. Let’s get to the fun stuff!</p><h2 class="post__h2">Personalize your Cloudinary image</h2><p class="post__p">The Cloudinary API lets you make all sorts of image customizations via URL parameters. As an example, here’s the URL generated for my own Fast Forward ticket. Cloudinary accepts an image URL (`fastforward2021.png`) with prepended parameters separated by commas.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="jPxggoNQHN"
      aria-describedby="jPxggoNQHN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="jPxggoNQHN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="jPxggoNQHN" itemprop="text" content="https%3A%2F%2Fres.cloudinary.com%2Fdevrelcontentful%2Fimage%2Fupload%2Fw_831%2Ch_466%2Cc_fill%2Cq_auto%2Cf_auto%2Cr_20%2Fw_760%2Cc_fit%2Cco_rgb%3Affffff%2Cg_south_west%2Cx_46%2Cy_239%2Cl_text%3Aavenirdemi.otf_48%3Awhitep4nth3r%2Fw_760%2Cc_fit%2Cco_rgb%3A2a3039%2Ca_90%2Cg_north_east%2Cx_84%2Cy_100%2Cl_text%3Aavenirreg.otf_16%3ANO.%2Fw_760%2Cc_fit%2Cco_rgb%3A2a3039%2Ca_90%2Cg_north_east%2Cx_55%2Cy_140%2Cl_text%3Aavenirreg.otf_56%3A0007964%2Ffastforward2021.png">
      <pre class="language-bash"><code class="language-bash">https://res.cloudinary.com/devrelcontentful/image/upload/w_831,h_466,c_fill,q_auto,f_auto,r_20/w_760,c_fit,co_rgb:ffffff,g_south_west,x_46,y_239,l_text:avenirdemi.otf_48:whitep4nth3r/w_760,c_fit,co_rgb:2a3039,a_90,g_north_east,x_84,y_100,l_text:avenirreg.otf_16:NO./w_760,c_fit,co_rgb:2a3039,a_90,g_north_east,x_55,y_140,l_text:avenirreg.otf_56:0007964/fastforward2021.png</code></pre>
    </div>
  </div>

  <p class="post__p">The URL is built up of the following:</p><ul><li><p class="post__p">Cloudinary base URL — <b class="post__p--bold"><b class="post__p--italic">https://res.cloudinary.com</b></b></p></li><li><p class="post__p">Cloudinary cloud name — <b class="post__p--bold"><b class="post__p--italic">devrelcontentful</b></b></p></li><li><p class="post__p">Asset type —<b class="post__p--italic"> </b><b class="post__p--bold"><b class="post__p--italic">image/upload</b></b></p></li><li><p class="post__p">Width — <b class="post__p--bold"><b class="post__p--italic">w_831</b></b></p></li><li><p class="post__p">Height — <b class="post__p--bold"><b class="post__p--italic">h_466</b></b></p></li><li><p class="post__p">Crop mode — <b class="post__p--bold"><b class="post__p--italic">c_fill</b></b></p></li><li><p class="post__p">Automatic asset format selection for best browser experience — <b class="post__p--bold"><b class="post__p--italic">f_auto</b></b></p></li><li><p class="post__p">Rounded corners of 20px — <b class="post__p--bold"><b class="post__p--italic">r_20</b></b></p></li><li><p class="post__p">Text area width of 760px —<b class="post__p--italic"> </b><b class="post__p--bold"><b class="post__p--italic">w_760</b></b></p></li><li><p class="post__p">Name text area crop mode — <b class="post__p--bold"><b class="post__p--italic">c_fit</b></b></p></li><li><p class="post__p">Name text color (as a hex code without the #) — <b class="post__p--bold"><b class="post__p--italic">ffffff</b></b></p></li><li><p class="post__p">Name text gravity — <b class="post__p--bold"><b class="post__p--italic">g_south_west</b></b></p></li><li><p class="post__p">Name text position coordinates — <b class="post__p--bold"><b class="post__p--italic">x_46,y_239</b></b></p></li><li><p class="post__p">Name font and size — <b class="post__p--bold"><b class="post__p--italic">l_text:avenirdemi.otf_48</b></b></p></li><li><p class="post__p">Name text value — <b class="post__p--bold"><b class="post__p--italic">:whitep4nth3r</b></b></p></li><li><p class="post__p">The same is repeated for the ticket number text</p></li><li><p class="post__p">Finally, the URL ends with the name of the image as stored in Cloudinary — <b class="post__p--bold"><b class="post__p--italic">fastforward2021.png</b></b></p></li></ul><p class="post__p">Let’s take a look at some JavaScript code used to generate a URL like this. At first glance, it might look overwhelming. But, once you understand how it all pieces together, you’ll want to personalize images at every opportunity! Big thanks to Jason Lengstorf for <a href="https://github.com/jlengstorf/get-share-image" target="_blank">this repository</a>, which provided some inspiration and insight to some common gotchas when working with Cloudinary URLs. </p><p class="post__p">The function <code>generateImageUrl()</code> below takes a number of required and optional parameters to build up a Cloudinary image URL like we explored above, to generate a personalized image. Depending on your image and how you want it personalized, you’ll want to play around with the default input parameters of <code>generateImageUrl()</code>, most notably the offset values, colors, font sizes and gravity values. Note that I’ve used the font “Arial” instead of the custom font used in the URL above.</p><p class="post__p">For more information on how to configure these values, refer to the <a href="https://cloudinary.com/documentation/image_transformations" target="_blank">Cloudinary image transformations documentation</a>.</p><p class="post__p">Finally, add an <code>&lt;img /&gt;</code> tag to your Index component and add the <code>src</code> and <code>alt</code> attributes to render your personalized image. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="WDQwNgyfuo"
      aria-describedby="WDQwNgyfuo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="WDQwNgyfuo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="WDQwNgyfuo" itemprop="text" content="%2F%2F%20pages%2Findex.js%0A%0Aimport%20Head%20from%20%22next%2Fhead%22%3B%0A%0A%2F*%20Encode%20characters%20for%20Cloudinary%20URL%20*%2F%0Afunction%20cleanText(text)%20%7B%0A%20%20return%20encodeURIComponent(text).replace(%2F%25(23%7C2C%7C2F%7C3F%7C5C)%2Fg%2C%20%22%2525%241%22)%3B%0A%7D%0A%0A%2F*%20CONFIG%20vars%20*%2F%0Aconst%20CLOUD_NAME%20%3D%20%22the-claw%22%3B%0Aconst%20IMG_WIDTH%20%3D%20831%3B%0Aconst%20IMG_HEIGHT%20%3D%20466%3B%0A%0A%2F*%20Build%20the%20Cloudinary%20Image%20URL%20*%2F%0Afunction%20generateImageUrl(%7B%0A%20%20name%2C%0A%20%20ticketNo%2C%0A%20%20imagePublicID%2C%0A%20%20cloudinaryUrlBase%20%3D%20%22https%3A%2F%2Fres.cloudinary.com%22%2C%0A%20%20imageWidth%20%3D%20IMG_WIDTH%2C%0A%20%20imageHeight%20%3D%20IMG_HEIGHT%2C%0A%20%20textAreaWidth%20%3D%20760%2C%0A%0A%20%20ticketNoFont%20%3D%20%22Arial%22%2C%0A%20%20ticketNoGravity%20%3D%20%22north_east%22%2C%0A%20%20ticketNoLeftOffset%20%3D%2055%2C%0A%20%20ticketNoTopOffset%20%3D%20140%2C%0A%20%20ticketNoColor%20%3D%20%222a3039%22%2C%0A%20%20ticketNoFontSize%20%3D%2056%2C%0A%0A%20%20noFont%20%3D%20%22Arial%22%2C%0A%20%20noGravity%20%3D%20%22north_east%22%2C%0A%20%20noLeftOffset%20%3D%2084%2C%0A%20%20noTopOffset%20%3D%20100%2C%0A%20%20noColor%20%3D%20%222a3039%22%2C%0A%20%20noFontSize%20%3D%2016%2C%0A%0A%20%20nameFont%20%3D%20%22Arial%22%2C%0A%20%20nameGravity%20%3D%20%22south_west%22%2C%0A%20%20nameBottomOffset%20%3D%20239%2C%0A%20%20nameLeftOffset%20%3D%2046%2C%0A%20%20nameColor%20%3D%20%22ffffff%22%2C%0A%20%20nameFontSize%20%3D%2048%2C%0A%0A%20%20version%20%3D%20null%2C%0A%7D)%20%7B%0A%20%20%2F%2F%20configure%20social%20media%20image%20dimensions%2C%20quality%2C%20and%20format%0A%20%20const%20imageConfig%20%3D%20%5B%0A%20%20%20%20%60w_%24%7BimageWidth%7D%60%2C%0A%20%20%20%20%60h_%24%7BimageHeight%7D%60%2C%0A%20%20%20%20%22c_fill%22%2C%0A%20%20%20%20%22q_auto%22%2C%0A%20%20%20%20%22f_auto%22%2C%0A%20%20%20%20%22r_20%22%2C%0A%20%20%5D.join(%22%2C%22)%3B%0A%0A%20%20%2F%2F%20configure%20the%20name%20text%0A%20%20const%20nameConfig%20%3D%20%5B%0A%20%20%20%20%60w_%24%7BtextAreaWidth%7D%60%2C%0A%20%20%20%20%22c_fit%22%2C%0A%20%20%20%20%60co_rgb%3A%24%7BnameColor%20%7C%7C%20textColor%7D%60%2C%0A%20%20%20%20%60g_%24%7BnameGravity%7D%60%2C%0A%20%20%20%20%60x_%24%7BnameLeftOffset%7D%60%2C%0A%20%20%20%20%60y_%24%7BnameBottomOffset%7D%60%2C%0A%20%20%20%20%60l_text%3A%24%7BnameFont%7D_%24%7BnameFontSize%7D%3A%24%7BcleanText(name)%7D%60%2C%0A%20%20%5D.join(%22%2C%22)%3B%0A%0A%20%20%2F%2Fconfigure%20the%20%22NO.%22%20text%0A%20%20const%20noConfig%20%3D%20%5B%0A%20%20%20%20%5B%0A%20%20%20%20%20%20%60w_%24%7BtextAreaWidth%7D%60%2C%0A%20%20%20%20%20%20%22c_fit%22%2C%0A%20%20%20%20%20%20%60co_rgb%3A%24%7BnoColor%7D%60%2C%0A%20%20%20%20%20%20%60a_90%60%2C%0A%20%20%20%20%20%20%60g_%24%7BnoGravity%7D%60%2C%0A%20%20%20%20%20%20%60x_%24%7BnoLeftOffset%7D%60%2C%0A%20%20%20%20%20%20%60y_%24%7BnoTopOffset%7D%60%2C%0A%20%20%20%20%20%20%60l_text%3A%24%7BnoFont%7D_%24%7BnoFontSize%7D%3ANO.%60%2C%0A%20%20%20%20%5D.join(%22%2C%22)%2C%0A%20%20%5D%3B%0A%0A%20%20%2F%2F%20configure%20the%20ticketNo%20text%0A%20%20const%20ticketNoConfig%20%3D%20ticketNo%0A%20%20%20%20%3F%20%5B%0A%20%20%20%20%20%20%20%20%60w_%24%7BtextAreaWidth%7D%60%2C%0A%20%20%20%20%20%20%20%20%22c_fit%22%2C%0A%20%20%20%20%20%20%20%20%60co_rgb%3A%24%7BticketNoColor%7D%60%2C%0A%20%20%20%20%20%20%20%20%60a_90%60%2C%0A%20%20%20%20%20%20%20%20%60g_%24%7BticketNoGravity%7D%60%2C%0A%20%20%20%20%20%20%20%20%60x_%24%7BticketNoLeftOffset%7D%60%2C%0A%20%20%20%20%20%20%20%20%60y_%24%7BticketNoTopOffset%7D%60%2C%0A%20%20%20%20%20%20%20%20%60l_text%3A%24%7BticketNoFont%7D_%24%7BticketNoFontSize%7D%3A%24%7BcleanText(ticketNo)%7D%60%2C%0A%20%20%20%20%20%20%5D.join(%22%2C%22)%0A%20%20%20%20%3A%20undefined%3B%0A%0A%20%20%2F%2F%20combine%20all%20the%20pieces%20required%20to%20generate%20a%20Cloudinary%20URL%0A%20%20const%20urlParts%20%3D%20%5B%0A%20%20%20%20cloudinaryUrlBase%2C%0A%20%20%20%20CLOUD_NAME%2C%0A%20%20%20%20%22image%22%2C%0A%20%20%20%20%22upload%22%2C%0A%20%20%20%20imageConfig%2C%0A%20%20%20%20nameConfig%2C%0A%20%20%20%20noConfig%2C%0A%20%20%20%20ticketNoConfig%2C%0A%20%20%20%20version%2C%0A%20%20%20%20imagePublicID%2C%0A%20%20%5D%3B%0A%0A%20%20%2F%2F%20remove%20any%20falsy%20sections%20of%20the%20URL%20(e.g.%20an%20undefined%20version)%0A%20%20const%20validParts%20%3D%20urlParts.filter(Boolean)%3B%0A%0A%20%20%2F%2F%20join%20all%20the%20parts%20into%20a%20valid%20URL%20to%20the%20generated%20image%0A%20%20return%20validParts.join(%22%2F%22)%3B%0A%7D%0A%0Aexport%20default%20function%20Index(%7B%20name%2C%20isShared%20%7D)%20%7B%0A%20%20%2F*%20Event%20info%20config...%20*%2F%0A%20%20%2F*%20Generate%20a%20fake%20ticket%20number...%20*%2F%0A%0A%20%20%2F*%20Build%20the%20Cloudinary%20image%20URL%20*%2F%0A%20%20const%20imageUrl%20%3D%20generateImageUrl(%7B%0A%20%20%20%20name%3A%20name%2C%0A%20%20%20%20ticketNo%3A%20ticketNo%2C%0A%20%20%20%20imagePublicID%3A%20%22ticket_template.png%22%2C%0A%20%20%7D)%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%3CHead%3E%0A%20%20%20%20%20%20%20%20%7B%2F*%20%E2%80%A6%20*%2F%7D%0A%20%20%20%20%20%20%3C%2FHead%3E%0A%0A%20%20%20%20%20%20%3Cimg%20alt%3D%22My%20ticket%22%20src%3D%7BimageUrl%7D%20%2F%3E%0A%0A%20%20%20%20%3C%2Fmain%3E%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getServerSideProps(context)%20%7B%0A%20%20%20%20%2F*%20...%20*%2F%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// pages/index.js</span><br><br><span class="token keyword">import</span> Head <span class="token keyword">from</span> <span class="token string">"next/head"</span><span class="token punctuation">;</span><br><br><span class="token comment">/* Encode characters for Cloudinary URL */</span><br><span class="token keyword">function</span> <span class="token function">cleanText</span><span class="token punctuation">(</span><span class="token parameter">text</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token function">encodeURIComponent</span><span class="token punctuation">(</span>text<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">%(23|2C|2F|3F|5C)</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">"%25$1"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">/* CONFIG vars */</span><br><span class="token keyword">const</span> <span class="token constant">CLOUD_NAME</span> <span class="token operator">=</span> <span class="token string">"the-claw"</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> <span class="token constant">IMG_WIDTH</span> <span class="token operator">=</span> <span class="token number">831</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> <span class="token constant">IMG_HEIGHT</span> <span class="token operator">=</span> <span class="token number">466</span><span class="token punctuation">;</span><br><br><span class="token comment">/* Build the Cloudinary Image URL */</span><br><span class="token keyword">function</span> <span class="token function">generateImageUrl</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>  name<span class="token punctuation">,</span><br>  ticketNo<span class="token punctuation">,</span><br>  imagePublicID<span class="token punctuation">,</span><br>  cloudinaryUrlBase <span class="token operator">=</span> <span class="token string">"https://res.cloudinary.com"</span><span class="token punctuation">,</span><br>  imageWidth <span class="token operator">=</span> <span class="token constant">IMG_WIDTH</span><span class="token punctuation">,</span><br>  imageHeight <span class="token operator">=</span> <span class="token constant">IMG_HEIGHT</span><span class="token punctuation">,</span><br>  textAreaWidth <span class="token operator">=</span> <span class="token number">760</span><span class="token punctuation">,</span><br><br>  ticketNoFont <span class="token operator">=</span> <span class="token string">"Arial"</span><span class="token punctuation">,</span><br>  ticketNoGravity <span class="token operator">=</span> <span class="token string">"north_east"</span><span class="token punctuation">,</span><br>  ticketNoLeftOffset <span class="token operator">=</span> <span class="token number">55</span><span class="token punctuation">,</span><br>  ticketNoTopOffset <span class="token operator">=</span> <span class="token number">140</span><span class="token punctuation">,</span><br>  ticketNoColor <span class="token operator">=</span> <span class="token string">"2a3039"</span><span class="token punctuation">,</span><br>  ticketNoFontSize <span class="token operator">=</span> <span class="token number">56</span><span class="token punctuation">,</span><br><br>  noFont <span class="token operator">=</span> <span class="token string">"Arial"</span><span class="token punctuation">,</span><br>  noGravity <span class="token operator">=</span> <span class="token string">"north_east"</span><span class="token punctuation">,</span><br>  noLeftOffset <span class="token operator">=</span> <span class="token number">84</span><span class="token punctuation">,</span><br>  noTopOffset <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">,</span><br>  noColor <span class="token operator">=</span> <span class="token string">"2a3039"</span><span class="token punctuation">,</span><br>  noFontSize <span class="token operator">=</span> <span class="token number">16</span><span class="token punctuation">,</span><br><br>  nameFont <span class="token operator">=</span> <span class="token string">"Arial"</span><span class="token punctuation">,</span><br>  nameGravity <span class="token operator">=</span> <span class="token string">"south_west"</span><span class="token punctuation">,</span><br>  nameBottomOffset <span class="token operator">=</span> <span class="token number">239</span><span class="token punctuation">,</span><br>  nameLeftOffset <span class="token operator">=</span> <span class="token number">46</span><span class="token punctuation">,</span><br>  nameColor <span class="token operator">=</span> <span class="token string">"ffffff"</span><span class="token punctuation">,</span><br>  nameFontSize <span class="token operator">=</span> <span class="token number">48</span><span class="token punctuation">,</span><br><br>  version <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// configure social media image dimensions, quality, and format</span><br>  <span class="token keyword">const</span> imageConfig <span class="token operator">=</span> <span class="token punctuation">[</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">w_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>imageWidth<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">h_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>imageHeight<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token string">"c_fill"</span><span class="token punctuation">,</span><br>    <span class="token string">"q_auto"</span><span class="token punctuation">,</span><br>    <span class="token string">"f_auto"</span><span class="token punctuation">,</span><br>    <span class="token string">"r_20"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">","</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// configure the name text</span><br>  <span class="token keyword">const</span> nameConfig <span class="token operator">=</span> <span class="token punctuation">[</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">w_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>textAreaWidth<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token string">"c_fit"</span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">co_rgb:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nameColor <span class="token operator">||</span> textColor<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">g_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nameGravity<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">x_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nameLeftOffset<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">y_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nameBottomOffset<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">l_text:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nameFont<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>nameFontSize<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">cleanText</span><span class="token punctuation">(</span>name<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">","</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">//configure the "NO." text</span><br>  <span class="token keyword">const</span> noConfig <span class="token operator">=</span> <span class="token punctuation">[</span><br>    <span class="token punctuation">[</span><br>      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">w_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>textAreaWidth<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token string">"c_fit"</span><span class="token punctuation">,</span><br>      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">co_rgb:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>noColor<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">a_90</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">g_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>noGravity<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">x_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>noLeftOffset<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">y_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>noTopOffset<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">l_text:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>noFont<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>noFontSize<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:NO.</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">","</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// configure the ticketNo text</span><br>  <span class="token keyword">const</span> ticketNoConfig <span class="token operator">=</span> ticketNo<br>    <span class="token operator">?</span> <span class="token punctuation">[</span><br>        <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">w_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>textAreaWidth<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>        <span class="token string">"c_fit"</span><span class="token punctuation">,</span><br>        <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">co_rgb:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketNoColor<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>        <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">a_90</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>        <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">g_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketNoGravity<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>        <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">x_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketNoLeftOffset<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>        <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">y_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketNoTopOffset<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>        <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">l_text:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketNoFont<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketNoFontSize<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">cleanText</span><span class="token punctuation">(</span>ticketNo<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>      <span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">","</span><span class="token punctuation">)</span><br>    <span class="token operator">:</span> <span class="token keyword">undefined</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// combine all the pieces required to generate a Cloudinary URL</span><br>  <span class="token keyword">const</span> urlParts <span class="token operator">=</span> <span class="token punctuation">[</span><br>    cloudinaryUrlBase<span class="token punctuation">,</span><br>    <span class="token constant">CLOUD_NAME</span><span class="token punctuation">,</span><br>    <span class="token string">"image"</span><span class="token punctuation">,</span><br>    <span class="token string">"upload"</span><span class="token punctuation">,</span><br>    imageConfig<span class="token punctuation">,</span><br>    nameConfig<span class="token punctuation">,</span><br>    noConfig<span class="token punctuation">,</span><br>    ticketNoConfig<span class="token punctuation">,</span><br>    version<span class="token punctuation">,</span><br>    imagePublicID<span class="token punctuation">,</span><br>  <span class="token punctuation">]</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// remove any falsy sections of the URL (e.g. an undefined version)</span><br>  <span class="token keyword">const</span> validParts <span class="token operator">=</span> urlParts<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>Boolean<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token comment">// join all the parts into a valid URL to the generated image</span><br>  <span class="token keyword">return</span> validParts<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Index</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> name<span class="token punctuation">,</span> isShared <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* Event info config... */</span><br>  <span class="token comment">/* Generate a fake ticket number... */</span><br><br>  <span class="token comment">/* Build the Cloudinary image URL */</span><br>  <span class="token keyword">const</span> imageUrl <span class="token operator">=</span> <span class="token function">generateImageUrl</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">name</span><span class="token operator">:</span> name<span class="token punctuation">,</span><br>    <span class="token literal-property property">ticketNo</span><span class="token operator">:</span> ticketNo<span class="token punctuation">,</span><br>    <span class="token literal-property property">imagePublicID</span><span class="token operator">:</span> <span class="token string">"ticket_template.png"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>main<span class="token operator">></span><br>      <span class="token operator">&lt;</span>Head<span class="token operator">></span><br>        <span class="token punctuation">{</span><span class="token comment">/* … */</span><span class="token punctuation">}</span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>Head<span class="token operator">></span><br><br>      <span class="token operator">&lt;</span>img alt<span class="token operator">=</span><span class="token string">"My ticket"</span> src<span class="token operator">=</span><span class="token punctuation">{</span>imageUrl<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>main<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getServerSideProps</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">/* ... */</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Woo! We’ve got a personalized image via the Cloudinary API as a URL! Next, let’s use this to show a preview of the ticket when attendees share your event on social media.</p><h2 class="post__h2">Configure Open Graph meta for social sharing</h2><p class="post__p">The power behind those ticket previews you see on Twitter and LinkedIn is all down to the magic of the <a href="https://opengraphprotocol.org/" target="_blank">Open Graph protocol</a>. The Open Graph (OG) protocol was created at Facebook in 2010 to enable web page links to become rich objects with similar functionality and appearance to other content posted on Facebook. </p><p class="post__p">Open Graph meta tags are used in the <code>&lt;head&gt;</code> of an HTML page to expose information about web pages to social media platforms and other applications that unfurl URL metadata. OG meta tags are identified in the HTML by an attribute prefixed with <code>og</code>.  </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="WOHkEPvjdH"
      aria-describedby="WOHkEPvjdH">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="WOHkEPvjdH">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="WOHkEPvjdH" itemprop="text" content="%3Cmeta%20property%3D%22og%3Aimage%22%20content%3D%22https%3A%2F%2Fexample.com%2Fimage.png%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/image.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">OG meta tags can also be used to customize the appearance of your web pages according to the platform it’s shared on. For example, Twitter rolled out their own custom implementation of this, built on the OG protocol, and the following code tells Twitter to show the large image web page previews.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="lDBvLIkIbp"
      aria-describedby="lDBvLIkIbp">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="lDBvLIkIbp">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="lDBvLIkIbp" itemprop="text" content="%3Cmeta%20name%3D%22twitter%3Acard%22%20content%3D%22summary_large_image%22%20%2F%3E%0A%3Cmeta%0A%20%20%20%20name%3D%22twitter%3Aimage%22%0A%20%20%20%20content%3D%22https%3A%2F%2Fexample.com%2Fimage.png%22%0A%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:card<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>summary_large_image<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span><br>    <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:image<span class="token punctuation">"</span></span><br>    <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/image.png<span class="token punctuation">"</span></span><br><span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">The Next Head component — imported at the top of the file and rendered inside the Index component — will add the meta tags we define inside it to the head of the resulting HTML page. </p><p class="post__p">Define an <code>ogUrl</code> variable above the return statement of the Index component as <code>${ticketAppUrl}?name=${name}&amp;shared=true</code>. Notice that we’re adding a second URL parameter onto the end of the URL — <code>shared</code> — which we configured in <code>getSeverSideProps()</code> earlier. This will become important in the next couple of steps.</p><p class="post__p">Add the relevant OG meta tags inside the Next Head component tags to enable a fancy image preview with a title and description to show on Twitter and LinkedIn. You’ll notice we’re making good use of those event configuration variables we defined earlier.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="lAerGirKJL"
      aria-describedby="lAerGirKJL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="lAerGirKJL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="lAerGirKJL" itemprop="text" content="%2F%2F%20pages%2Findex.js%0A%0Aimport%20Head%20from%20%22next%2Fhead%22%3B%0A%0A%2F*%20...%20*%2F%0A%0Aexport%20default%20function%20Index(%7B%20name%2C%20isShared%20%7D)%20%7B%0A%20%20%2F*%20Event%20info%20config...%20*%2F%0A%20%20%2F*%20Generate%20a%20fake%20ticket%20number...%20*%2F%0A%20%20%2F*%20Build%20the%20Cloudinary%20image%20URL...%20*%2F%0A%0A%20%20%2F*%20Configure%20Open%20Graph%20URL%20*%2F%0A%20%20const%20ogUrl%20%3D%20%60%24%7BticketAppUrl%7D%3Fname%3D%24%7Bname%7D%26shared%3Dtrue%60%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%3CHead%3E%0A%20%20%20%20%20%20%20%20%3Ctitle%3E%7Btitle%7D%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20name%3D%22description%22%20content%3D%7Bdescription%7D%20%2F%3E%0A%0A%20%20%20%20%20%20%20%20%3Cmeta%20name%3D%22twitter%3Acard%22%20content%3D%22summary_large_image%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20name%3D%22twitter%3Asite%22%20content%3D%7BticketAppUrl%7D%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20name%3D%22twitter%3Acreator%22%20content%3D%22%40your_twitter_username%22%20%2F%3E%0A%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Aurl%22%20content%3D%7BogUrl%7D%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Atype%22%20content%3D%22website%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Atitle%22%20content%3D%7Btitle%7D%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Adescription%22%20content%3D%7Bdescription%7D%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Alocale%22%20content%3D%22en_US%22%20%2F%3E%0A%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Aimage%22%20content%3D%7BimageUrl%7D%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Aimage%3Aalt%22%20content%3D%7BeventName%7D%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Aimage%3Awidth%22%20content%3D%7BIMG_WIDTH%7D%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Aimage%3Aheight%22%20content%3D%7BIMG_HEIGHT%7D%20%2F%3E%0A%20%20%20%20%20%20%20%20%3Cmeta%20property%3D%22og%3Asite_name%22%20content%3D%7BeventName%7D%20%2F%3E%0A%20%20%20%20%20%20%3C%2FHead%3E%0A%0A%20%20%20%20%20%20%2F*%20...%20*%2F%0A%0A%20%20%20%20%3C%2Fmain%3E%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getServerSideProps(context)%20%7B%0A%20%20%2F*%20...%20*%2F%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// pages/index.js</span><br><br><span class="token keyword">import</span> Head <span class="token keyword">from</span> <span class="token string">"next/head"</span><span class="token punctuation">;</span><br><br><span class="token comment">/* ... */</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Index</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> name<span class="token punctuation">,</span> isShared <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* Event info config... */</span><br>  <span class="token comment">/* Generate a fake ticket number... */</span><br>  <span class="token comment">/* Build the Cloudinary image URL... */</span><br><br>  <span class="token comment">/* Configure Open Graph URL */</span><br>  <span class="token keyword">const</span> ogUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketAppUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">?name=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;shared=true</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>main<span class="token operator">></span><br>      <span class="token operator">&lt;</span>Head<span class="token operator">></span><br>        <span class="token operator">&lt;</span>title<span class="token operator">></span><span class="token punctuation">{</span>title<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>title<span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta name<span class="token operator">=</span><span class="token string">"description"</span> content<span class="token operator">=</span><span class="token punctuation">{</span>description<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br><br>        <span class="token operator">&lt;</span>meta name<span class="token operator">=</span><span class="token string">"twitter:card"</span> content<span class="token operator">=</span><span class="token string">"summary_large_image"</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta name<span class="token operator">=</span><span class="token string">"twitter:site"</span> content<span class="token operator">=</span><span class="token punctuation">{</span>ticketAppUrl<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta name<span class="token operator">=</span><span class="token string">"twitter:creator"</span> content<span class="token operator">=</span><span class="token string">"@your_twitter_username"</span> <span class="token operator">/</span><span class="token operator">></span><br><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:url"</span> content<span class="token operator">=</span><span class="token punctuation">{</span>ogUrl<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:type"</span> content<span class="token operator">=</span><span class="token string">"website"</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:title"</span> content<span class="token operator">=</span><span class="token punctuation">{</span>title<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:description"</span> content<span class="token operator">=</span><span class="token punctuation">{</span>description<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:locale"</span> content<span class="token operator">=</span><span class="token string">"en_US"</span> <span class="token operator">/</span><span class="token operator">></span><br><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:image"</span> content<span class="token operator">=</span><span class="token punctuation">{</span>imageUrl<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:image:alt"</span> content<span class="token operator">=</span><span class="token punctuation">{</span>eventName<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:image:width"</span> content<span class="token operator">=</span><span class="token punctuation">{</span><span class="token constant">IMG_WIDTH</span><span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:image:height"</span> content<span class="token operator">=</span><span class="token punctuation">{</span><span class="token constant">IMG_HEIGHT</span><span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:site_name"</span> content<span class="token operator">=</span><span class="token punctuation">{</span>eventName<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>Head<span class="token operator">></span><br><br>      <span class="token comment">/* ... */</span><br><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>main<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getServerSideProps</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* ... */</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Now, let’s create those social share links for your attendees to generate some excitement for your event!</p><h2 class="post__h2">Add Twitter and LinkedIn social sharing links</h2><p class="post__p">This is where all of the magic we conjured above comes together. We’re going to build a Twitter Web Intent URL and LinkedIn share URL that,  <b class="post__p--bold">when your website is live (and this is the important part!)</b>, will pull in the image you personalized via Cloudinary via the Open Graph <code>og:image</code> meta tag in your HTML <code>&lt;head&gt;</code>.</p><p class="post__p">The code below shows examples of how to create Twitter and LinkedIn share URLs. Things to bear in mind:</p><ul><li><p class="post__p">If you want to use line breaks (<code>/n</code>) in your tweet, make sure you wrap your tweet text in <code>encodeURIComponent()</code></p></li><li><p class="post__p">Ensure you include <code>&amp;shared=true</code> on your share URLs — you’ll see why in the next step!</p></li><li><p class="post__p">Make sure to convert all equals (=) symbols in the LinkedIn share URL to the HTML character code <code>%3D</code> — otherwise the link won’t work correctly</p></li></ul><p class="post__p">Finally, add anchor links to the Index component below the image tag, with your configured Twitter and LinkedIn share URLs. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZgSPbCJsWq"
      aria-describedby="ZgSPbCJsWq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZgSPbCJsWq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="ZgSPbCJsWq" itemprop="text" content="%2F%2F%20pages%2Findex.js%0A%0Aimport%20Head%20from%20%22next%2Fhead%22%3B%0A%0A%2F*%20...%20*%2F%0A%0Aexport%20default%20function%20Index(%7B%20name%2C%20isShared%20%7D)%20%7B%0A%20%20%2F*%20Event%20info%20config...%20*%2F%0A%20%20%2F*%20Generate%20a%20fake%20ticket%20number...%20*%2F%0A%20%20%2F*%20Build%20the%20Cloudinary%20image%20URL...%20*%2F%0A%20%20%2F*%20Configure%20Open%20Graph%20URL...%20*%2F%0A%0A%20%20%2F*%20Twitter%20Config%20*%2F%0A%20%20const%20tweetText%20%3D%20encodeURIComponent(%22I%20just%20got%20my%20ticket%20to%20an%20awesome%20event!%5Cn%5CnGrab%20your%20free%20ticket%20and%20join%20me!%5Cn%5Cn%22%2C%0A%20%20)%3B%0A%20%20const%20twitterShareUrl%20%3D%20encodeURIComponent(%60%24%7BticketAppUrl%7D%3Fname%3D%24%7Bname%7D%26shared%3Dtrue%60)%3B%0A%20%20const%20twitterShareHref%20%3D%20%60https%3A%2F%2Ftwitter.com%2Fintent%2Ftweet%3Furl%3D%24%7BtwitterShareUrl%7D%26text%3D%24%7BtweetText%7D%60%3B%0A%0A%20%2F*%20LinkedIn%20Config%20*%2F%0A%20const%20linkedInShareUrl%20%3D%20%60%24%7BticketAppUrl%7D%3Fname%253D%24%7Bname%7D%26shared%253Dtrue%60%3B%0A%20const%20linkedInShareHref%20%3D%20%60https%3A%2F%2Fwww.linkedin.com%2Fsharing%2Fshare-offsite%2F%3Furl%3D%24%7BlinkedInShareUrl%7D%60%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%3CHead%3E%0A%20%20%20%20%20%20%7B%2F*%20...%20*%2F%7D%0A%20%20%20%20%20%20%3C%2FHead%3E%0A%0A%20%20%20%20%20%20%3Cimg%20alt%3D%22My%20ticket%22%20src%3D%7BimageUrl%7D%20%2F%3E%0A%0A%20%20%20%20%20%20%3Ca%20href%3D%7BtwitterShareHref%7D%20target%3D%22_blank%22%20rel%3D%22noreferrer%22%3E%0A%20%20%20%20%20%20%20%20Share%20on%20Twitter%0A%20%20%20%20%20%20%3C%2Fa%3E%0A%20%20%20%20%20%20%3Ca%20href%3D%7BlinkedInShareHref%7D%20target%3D%22_blank%22%20rel%3D%22noreferrer%22%3E%0A%20%20%20%20%20%20%20%20Share%20on%20LinkedIn%0A%20%20%20%20%20%20%3C%2Fa%3E%0A%20%20%20%20%3C%2Fmain%3E%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getServerSideProps(context)%20%7B%0A%20%20%2F*%20...%20*%2F%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// pages/index.js</span><br><br><span class="token keyword">import</span> Head <span class="token keyword">from</span> <span class="token string">"next/head"</span><span class="token punctuation">;</span><br><br><span class="token comment">/* ... */</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Index</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> name<span class="token punctuation">,</span> isShared <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* Event info config... */</span><br>  <span class="token comment">/* Generate a fake ticket number... */</span><br>  <span class="token comment">/* Build the Cloudinary image URL... */</span><br>  <span class="token comment">/* Configure Open Graph URL... */</span><br><br>  <span class="token comment">/* Twitter Config */</span><br>  <span class="token keyword">const</span> tweetText <span class="token operator">=</span> <span class="token function">encodeURIComponent</span><span class="token punctuation">(</span><span class="token string">"I just got my ticket to an awesome event!\n\nGrab your free ticket and join me!\n\n"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> twitterShareUrl <span class="token operator">=</span> <span class="token function">encodeURIComponent</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketAppUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">?name=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;shared=true</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> twitterShareHref <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://twitter.com/intent/tweet?url=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>twitterShareUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;text=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>tweetText<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br> <span class="token comment">/* LinkedIn Config */</span><br> <span class="token keyword">const</span> linkedInShareUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>ticketAppUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">?name%3D</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;shared%3Dtrue</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br> <span class="token keyword">const</span> linkedInShareHref <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://www.linkedin.com/sharing/share-offsite/?url=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>linkedInShareUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>main<span class="token operator">></span><br>      <span class="token operator">&lt;</span>Head<span class="token operator">></span><br>      <span class="token punctuation">{</span><span class="token comment">/* ... */</span><span class="token punctuation">}</span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>Head<span class="token operator">></span><br><br>      <span class="token operator">&lt;</span>img alt<span class="token operator">=</span><span class="token string">"My ticket"</span> src<span class="token operator">=</span><span class="token punctuation">{</span>imageUrl<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br><br>      <span class="token operator">&lt;</span>a href<span class="token operator">=</span><span class="token punctuation">{</span>twitterShareHref<span class="token punctuation">}</span> target<span class="token operator">=</span><span class="token string">"_blank"</span> rel<span class="token operator">=</span><span class="token string">"noreferrer"</span><span class="token operator">></span><br>        Share on Twitter<br>      <span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><br>      <span class="token operator">&lt;</span>a href<span class="token operator">=</span><span class="token punctuation">{</span>linkedInShareHref<span class="token punctuation">}</span> target<span class="token operator">=</span><span class="token string">"_blank"</span> rel<span class="token operator">=</span><span class="token string">"noreferrer"</span><span class="token operator">></span><br>        Share on LinkedIn<br>      <span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>main<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getServerSideProps</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* ... */</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">There’s just one more step. Finally, let’s configure the web page for visitors to your site who clicked on a link from social media.</p><h2 class="post__h2">Configure your web page for social clicks</h2><p class="post__p">Remember the <code>isShared</code> prop we captured in <code>getServerSideProps()</code>? Here’s where it comes into play.</p><p class="post__p">Compare my Fast Forward ticket confirmation URL with the link shared on Twitter below.</p><h3 class="post__h3">My ticket confirmation</h3><p class="post__p">This is the full URL with a name parameter only: <a href="https://tickets.contentful.com/fastforward2021?name=Salma" target="_blank"><u>https://tickets.contentful.com/fastforward2021?name=Salma</u></a></p><img src="https://images.ctfassets.net/56dzm01z6lln/2tzlNajrqFO8AFo0zH7zUI/7ea0e47c479a2c2af3c3de99de64b555/my_ticket_confirmation.png" alt="A screenshot of my Fast Forward ticket confirmation with the headline "You're in!" and sharing CTAs to Twitter and LinkedIn, and an add to calendar button." height="1169" width="2032" /><h3 class="post__h3">What people see when they click on the link in my tweet</h3><p class="post__p">This is the full URL with a <code>name</code> parameter and <code>shared</code> parameter: <a href="https://tickets.contentful.com/fastforward2021?name=Salma&shared=true" target="_blank"><u>https://tickets.contentful.com/fastforward2021?name=Salma&amp;shared=true</u></a></p><img src="https://images.ctfassets.net/56dzm01z6lln/6sAmdgwpQze8AWyZuf2xDs/7510874600640738eb15a565c796d823/full_page_screenshot.png" alt="A screenshot from the Fast Forward ticket website with the text "Salma is going!", a personalised ticket image, and a sign up CTA." height="1076" width="2032" /><p class="post__p">Use the code below to configure a different headline and subtitle depending on the value of the <code>isShared</code> parameter received by the Index component. Furthermore, non-attendees of the event see a call to action to sign up for the event, rather than sharing it on social media.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vmfKmbAxuT"
      aria-describedby="vmfKmbAxuT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vmfKmbAxuT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="vmfKmbAxuT" itemprop="text" content="%2F%2F%20pages%2Findex.js%0A%0Aimport%20Head%20from%20%22next%2Fhead%22%3B%0A%0A%2F*%20...%20*%2F%0A%0Aexport%20default%20function%20Index(%7B%20name%2C%20isShared%20%7D)%20%7B%0A%20%20%2F*%20...%20*%2F%0A%0A%20%20%2F*%20Page%20text%20config%20*%2F%0A%20%20const%20headline%20%3D%20isShared%20%3F%20%60%24%7Bname%7D%20is%20going!%60%20%3A%20%22You're%20in!%22%3B%0A%20%20const%20subtitle%20%3D%20isShared%0A%20%20%20%20%3F%20%60Don't%20miss%20out!%20Sign%20up%20to%20register%20and%20join%20%24%7Bname%7D%20at%20%24%7BeventName%7D.%60%0A%20%20%20%20%3A%20%60Add%20the%20event%20to%20your%20calendar%20and%20invite%20your%20friends%20to%20join%20you%20at%20%24%7BeventName%7D.%60%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%3CHead%3E%0A%20%20%20%20%20%20%7B%2F*%20...%20*%2F%7D%0A%20%20%20%20%20%20%3C%2FHead%3E%0A%0A%20%20%20%20%20%20%20%20%3Ch1%3E%7Bheadline%7D%3C%2Fh1%3E%0A%20%20%20%20%20%20%20%20%3Cp%3E%7Bsubtitle%7D%3C%2Fp%3E%0A%0A%20%20%20%20%20%20%7BisShared%20%26%26%20%3Ca%20href%3D%22https%3A%2F%2Fmy-awesome-ticket-app.dev%2Fsign-up%22%3ESign%20up!%3C%2Fa%3E%7D%0A%0A%20%20%20%20%20%20%20%20%7B!isShared%20%26%26%20(%0A%20%20%20%20%20%20%20%20%20%20%3C%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Ca%20href%3D%7BtwitterShareHref%7D%20target%3D%22_blank%22%20rel%3D%22noreferrer%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20Share%20on%20Twitter%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fa%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Ca%20href%3D%7BlinkedInShareHref%7D%20target%3D%22_blank%22%20rel%3D%22noreferrer%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20Share%20on%20LinkedIn%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fa%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2F%3E%0A%20%20%20%20%20%20%20%20)%7D%0A%0A%20%20%20%20%20%20%7B%2F*%20...%20*%2F%7D%0A%20%20%20%20%3C%2Fmain%3E%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getServerSideProps(context)%20%7B%0A%20%20%2F*%20...%20*%2F%0A%7D%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// pages/index.js</span><br><br><span class="token keyword">import</span> Head <span class="token keyword">from</span> <span class="token string">"next/head"</span><span class="token punctuation">;</span><br><br><span class="token comment">/* ... */</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Index</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> name<span class="token punctuation">,</span> isShared <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* ... */</span><br><br>  <span class="token comment">/* Page text config */</span><br>  <span class="token keyword">const</span> headline <span class="token operator">=</span> isShared <span class="token operator">?</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> is going!</span><span class="token template-punctuation string">`</span></span> <span class="token operator">:</span> <span class="token string">"You're in!"</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> subtitle <span class="token operator">=</span> isShared<br>    <span class="token operator">?</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Don't miss out! Sign up to register and join </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> at </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>eventName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.</span><span class="token template-punctuation string">`</span></span><br>    <span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Add the event to your calendar and invite your friends to join you at </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>eventName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>main<span class="token operator">></span><br>      <span class="token operator">&lt;</span>Head<span class="token operator">></span><br>      <span class="token punctuation">{</span><span class="token comment">/* ... */</span><span class="token punctuation">}</span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>Head<span class="token operator">></span><br><br>        <span class="token operator">&lt;</span>h1<span class="token operator">></span><span class="token punctuation">{</span>headline<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>h1<span class="token operator">></span><br>        <span class="token operator">&lt;</span>p<span class="token operator">></span><span class="token punctuation">{</span>subtitle<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>p<span class="token operator">></span><br><br>      <span class="token punctuation">{</span>isShared <span class="token operator">&amp;&amp;</span> <span class="token operator">&lt;</span>a href<span class="token operator">=</span><span class="token string">"https://my-awesome-ticket-app.dev/sign-up"</span><span class="token operator">></span>Sign up<span class="token operator">!</span><span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><span class="token punctuation">}</span><br><br>        <span class="token punctuation">{</span><span class="token operator">!</span>isShared <span class="token operator">&amp;&amp;</span> <span class="token punctuation">(</span><br>          <span class="token operator">&lt;</span><span class="token operator">></span><br>            <span class="token operator">&lt;</span>a href<span class="token operator">=</span><span class="token punctuation">{</span>twitterShareHref<span class="token punctuation">}</span> target<span class="token operator">=</span><span class="token string">"_blank"</span> rel<span class="token operator">=</span><span class="token string">"noreferrer"</span><span class="token operator">></span><br>              Share on Twitter<br>            <span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><br>            <span class="token operator">&lt;</span>a href<span class="token operator">=</span><span class="token punctuation">{</span>linkedInShareHref<span class="token punctuation">}</span> target<span class="token operator">=</span><span class="token string">"_blank"</span> rel<span class="token operator">=</span><span class="token string">"noreferrer"</span><span class="token operator">></span><br>              Share on LinkedIn<br>            <span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><br>          <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><br>        <span class="token punctuation">)</span><span class="token punctuation">}</span><br><br>      <span class="token punctuation">{</span><span class="token comment">/* ... */</span><span class="token punctuation">}</span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>main<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getServerSideProps</span><span class="token punctuation">(</span><span class="token parameter">context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">/* ... */</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">That’s a wrap!</h2><p class="post__p">Don’t forget — if you want to make sure your Open Graph images work as expected — you’ll need to deploy your application to a live URL. Vercel makes it really easy to go live with your Next.js application in just a few seconds. <a href="https://vercel.com/signup" target="_blank">Sign up to Vercel</a> and connect your project via GitHub — and you’re away! </p><p class="post__p">Cloudinary is pretty magical, and I can’t wait to explore its possibilities even further. What’s more, I’m excited to build similar apps in the future for different events I might run for my Discord and streaming community. If you’d like to explore the code demonstrated in this post, <a href="https://github.com/whitep4nth3r/ticket-app-demo" target="_blank">check out the GitHub repository</a> here (it contains zero CSS so go wild with your designs!).</p><p class="post__p">And lastly, make sure to <a href="https://www.contentful.com/fast-forward" target="_blank"><u>sign up to Fast Forward 2021</u></a> to receive your free ticket! We’ve got three days of events dedicated to those building the next generation of digital experiences, from developers to architects, engineers, creatives and technology enthusiasts alike. Don’t forget to share your ticket on social media — it’ll be even more fun now you know how it works! 😉</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Why I love building with Next.js — a fireside chat with Cassidy Williams of Netlify</title>
          <description>I joined Cassidy Williams to talk about the performance benefits of Next.js, and how developers can make the most of the JavaScript framework.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/building-with-nextjs-cassidy-williams-netlify-fireside-chat/</link>
          <guid>https://whitep4nth3r.com/blog/building-with-nextjs-cassidy-williams-netlify-fireside-chat/</guid>
          <pubDate>Wed, 01 Sep 2021 23:00:00 GMT</pubDate>
          <category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In this fireside chat, I joined <a href="https://twitter.com/cassidoo" target="_blank">Cassidy Williams</a> to talk about the performance benefits of <a href="https://nextjs.org/docs" target="_blank">Next.js</a>, and how developers can make the most of the framework. </p><p class="post__p">Next.js has a lot of options when it comes to data fetching methods, which can be both a blessing and a curse. Learn about how to think about data fetching methods in Next.js to build performant static sites that create dynamic user experiences.

We also cover why we personally use Next.js for projects, the best way to style a Next.js app — and I decided to bring back the phrase “surf the web.”</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/BqQcgHEif5s?start=37"
        title="Building with Next.js | Cassidy Williams and Salma Alam-Naylor | Architecting with Next.js 2021"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>What is an API?</title>
          <description>Let's learn about application programming interfaces.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/08/12/what-is-an-api/</link>
          <guid>https://www.contentful.com/blog/2021/08/12/what-is-an-api/</guid>
          <pubDate>Wed, 11 Aug 2021 23:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you’ve read the <a href="https://docs.contentful.com/" target="_blank">Contentful docs</a>, you’ll have seen that we provide a REST API and a GraphQL API to access and manage your content. But what is an API?</p><p class="post__p">API stands for “Application Programming Interface,” which is a way to communicate between different software services. Different types of APIs are used in programming hardware and software, including operating system APIs, remote APIs and web APIs — like the APIs that Contentful provides. A web API is a set of tools that allow developers to send and receive instructions and data to and from web servers — usually in JSON format — to build applications.<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/" target="_blank"> Read more about JSON on MDN</a>.</p><h2 class="post__h2">APIs are everywhere</h2><p class="post__p">To look at how APIs work in ordinary life, let’s look at the APIs involved in posts. </p><ul><li><p class="post__p">You read a post on the internet</p></li><li><p class="post__p">When you land on the page, the web application contains instructions to request the data for the post via the API by a unique identifier — such as the URL slug of the post</p></li><li><p class="post__p">If the data requested exists, it is sent back to the web page from the database via the API as JSON (such as the title, the published date, the article text and so on)</p></li><li><p class="post__p">The data is then slotted into the appropriate HTML structure as programmed by the developer for you to read</p></li></ul><p class="post__p">This post will cover the fundamentals of what makes an API and how APIs communicate over the internet. But first, let’s take a look at the history of where it all began.</p><h2 class="post__h2">Where did it all start?</h2><p class="post__p">The concept of APIs has been around since the evolution of the first computers in 1940, when British computer scientists Marice Wilkes and David Wheeler worked on a software library for the Electronic Delay Storage Automatic Calculator (EDSAC). </p><p class="post__p">The EDSAC was programmed to accept a variety of instructions including add, subtract, print, load and store. This is similar to how modern web APIs interface with data, for example, “add blog post,” “delete blog post,” or “get blog post information.” The functionality of EDSAC was documented via a catalog of notes about its functionality and how to integrate it with other programs. This was the first example of the type of API documentation we know today!</p><h2 class="post__h2">What is a web API?</h2><p class="post__p">A web API is a set of tools that allow web developers to send and receive instructions and data over an internet connection to web servers. Many of the modern websites and web applications we use today are powered by APIs. Think Twitter, Instagram, Facebook and your favorite shopping websites.</p><p class="post__p">In modern web applications, front-end code does not interact with a database directly. Instead, the data is sent and received via an <b class="post__p--bold">API layer</b>. APIs act as a middle layer, or contract, between backend logic and database operations and the front-end application that a user interacts with. </p><p class="post__p">An API layer:</p><ul><li><p class="post__p">ensures that the web page is allowed to make the request to send or receive data</p></li><li><p class="post__p">confirms the request is in the correct format before sending it to the backend</p></li><li><p class="post__p">returns the data in the expected format, along with some additional information</p></li><li><p class="post__p">tells the web page if and why there is no data returned</p></li></ul><img src="https://images.ctfassets.net/56dzm01z6lln/2eSRtywd3EBJtKKbclXZth/c73c78caad087c97f68c1bd4861cc57d/what_is_an_api_diagram.png" alt="An illustration showing how an API works. On the left is an illustration of a computer, with the text "web app in browser" underneath. An arrow is drawn to the right representing the request to a wheel cog, with the word "API" underneath. Further to the right is an illustration of a web server, which has a line drawn connecting it to an illustration of a database. Under the arrow representing the request is an arrow moving in the opposite direction representing the response from the API sent back to the web browser." height="839" width="2560" /><p class="post__p">So far, we’ve discussed how an API provides a middle layer to send data back and forth between backend databases and front-end applications. But what powers this data exchange? Let’s take a look at the foundation of data exchange on the web: <b class="post__p--bold">HTTP</b>.</p><h2 class="post__h2">A look at HTTP</h2><p class="post__p">HTTP — an acronym for HyperText Transfer Protocol — is a protocol that allows fetching resources, such as HTML documents and JSON data, over an internet connection. A protocol is defined as a system of rules that defines how data is exchanged within or between computers. HTTP defines a set of request methods that perform different actions when called. The most frequently used HTTP request methods are GET and POST.</p><p class="post__p">As the verbs of the HTTP methods describe:</p><ul><li><p class="post__p">a GET request allows you to retrieve data via a URL</p></li><li><p class="post__p">a POST request allows you to send additional information with a request via a URL to perform certain actions</p></li></ul><p class="post__p">Other HTTP methods that we won’t explore in this post include PUT, DELETE, HEAD, CONNECT, OPTIONS, TRACE and PATCH. To learn more about HTTP in depth, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP" target="_blank"><u>check out this article on MDN</u></a>.</p><p class="post__p">Before we get started with looking into GET and POST HTTP methods, let’s go over some HTTP terminology.</p><h2 class="post__h2">Sending information over HTTP</h2><p class="post__p">When you ask for data from an API, you make a <b class="post__p--bold">request</b>. When you receive data from an API, you receive a <b class="post__p--bold">response</b>.</p><h3 class="post__h3">Request and response body</h3><p class="post__p">In some HTTP methods, you will be required to send additional data inside a <b class="post__p--bold">request body</b>.</p><p class="post__p">Successful HTTP responses will contain a <b class="post__p--bold">response body </b>that contains the data you requested via the API. </p><h3 class="post__h3">HTTP headers</h3><p class="post__p">In addition to using different HTTP methods, you will often be required to send specific HTTP headers with a request and you may find specific HTTP headers accompanying a response. HTTP headers let the front end (client) and the backend (server) pass additional information with an HTTP request or response. An HTTP header is formatted as a key value pair separated by a colon:</p><p class="post__p"><code>&quot;case-insensitive-header-name&quot;: &quot;value as a string&quot;</code></p><p class="post__p">When making a request to the <a href="https://graphql.contentful.com/" target="_blank"><u>Contentful GraphQL API</u></a>, you will be required to send an HTTP header with the request as follows:</p><p class="post__p"><code>authorization: &quot;Bearer _your_contentful_access_token_value_&quot;</code></p><p class="post__p">This header ensures that the web page is authorized to make the request to receive data from the API and is detailed in <a href="https://www.contentful.com/developers/docs/references/graphql/#/introduction/authentication" target="_blank"><u>the relevant documentation</u></a>.</p><h3 class="post__h3">HTTP response status codes</h3><p class="post__p">Have you ever seen a web page that says “404, page not found<b class="post__p--italic">”</b>? That code — 404 — is an HTTP response status code! In a browser, you’ll receive a 404 response status code if a URL is unrecognized by an application. From an API, you’ll receive a 404 response status code to indicate that the data you are requesting does not exist. You can find the HTTP response status codes returned to a browser in the browser network tab. </p><img src="https://images.ctfassets.net/56dzm01z6lln/2KJimiAPypGZjgccz6wRQj/d91c7e1654d58b53979b651e3f27aa45/404-not-found.png" alt="A screenshot of a browser window on a Contentful 404 page. The network tab is open and a yellow rectangle is highlighting the HTTP response status code of 404." height="1153" width="1383" /><p class="post__p">HTTP response status codes are delivered with HTTP responses as a number. When requesting data from the Contentful GraphQL API using fetch in JavaScript, the HTTP status code is available as a <code>status</code> property on your response. </p><p class="post__p">The example code below requests the total number of blog posts available on my <a href="https://nextjs-contentful-blog-starter.vercel.app/" target="_blank"><u>Next.js + Contentful starter blog</u></a>. Notice the HTTP headers and request body sent with the request. When you run the code below, you’ll receive an HTTP 200 response, and <code>200</code> will be logged to the console. A 200 HTTP response code means the request was successful — and everything is “Ok!”</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="pkLqnvZgqu"
      aria-describedby="pkLqnvZgqu">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="pkLqnvZgqu">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="pkLqnvZgqu" itemprop="text" content="const%20data%20%3D%20await%20fetch(%0A%20%20%22https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F84zl5qdw0ore%22%2C%0A%20%20%7B%0A%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20authorization%3A%20%22Bearer%20_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA%22%2C%0A%20%20%20%20%20%20%22content-type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%20%20body%3A%20'%7B%22query%22%3A%22%7B%20blogPostCollection%20%7B%20total%20%7D%20%7D%22%7D'%2C%0A%20%20%7D%2C%0A).then((response)%20%3D%3E%20%7B%0A%20%20console.log(response.status)%3B%20%2F%2F%20200%0A%20%20return%20response.json()%3B%0A%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><br>  <span class="token string">"https://graphql.contentful.com/content/v1/spaces/84zl5qdw0ore"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">{</span><br>    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">authorization</span><span class="token operator">:</span> <span class="token string">"Bearer _9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA"</span><span class="token punctuation">,</span><br>      <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">'{"query":"{ blogPostCollection { total } }"}'</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span>status<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// 200</span><br>  <span class="token keyword">return</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">There are five groups of HTTP response status codes that are categorized by the starting digit.</p><ol><li><p class="post__p">Informational responses (100–199)</p></li><li><p class="post__p">Successful responses (200–299)</p></li><li><p class="post__p">Redirects (300–399)</p></li><li><p class="post__p">Client errors (400–499)</p></li><li><p class="post__p">Server errors (500–599)</p></li></ol><p class="post__p">Open the network tab in your browser and look for the 200 HTTP response status code for this web page. And if you’d like to explore a full list of HTTP response codes (including the silly response code 418 🙈), <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" target="_blank"><u>check out this article on MDN</u></a>.</p><p class="post__p">Now we’ve explored how you can send and receive with HTTP methods, let’s look at the two most common HTTP methods — GET and POST — in more detail.</p><h2 class="post__h2">HTTP GET</h2><p class="post__p">An HTTP GET request allows you to retrieve data via a URL. GET requests do not send a request body with the call to the API.</p><p class="post__p">Here’s the Contentful Content Delivery API URL, which requests information about a single Contentful space. A Contentful space is like a bucket for your Contentful content, which has a name and unique ID. Notice the <code>/spaces/ </code>part of the URL, which defines that we’re asking for space information.</p><p class="post__p"><code>https://cdn.contentful.com/spaces/{space_id}?access_token={access_token}</code></p><p class="post__p">The URL requires two pieces of data. The <code>space_id</code>, which is part of the URL, is the unique identifier of the space we’d like to get information about from the database. The <code>access_token</code>, which is a URL query parameter prefixed with <code>?</code>, is an authentication token that tells the API that we’re allowed to make this request. This token will be verified by the backend when it receives the request. When you set up a Contentful space, you are instructed to generate an access token via the web app to communicate with the Contentful APIs. You may hear authentication tokens or similar credentials referred to as API Keys.</p><p class="post__p">See the Contentful Delivery API at work by navigating to this URL in your browser: <a href="https://cdn.contentful.com/spaces/84zl5qdw0ore?access_token=_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA" target="_blank"><u>https://cdn.contentful.com/spaces/84zl5qdw0ore?access_token=_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA</u></a><code></code></p><p class="post__p">This particular URL requests information about the Contentful space that powers the <a href="https://nextjs-contentful-blog-starter.vercel.app/" target="_blank"><u>Next.js starter blog</u></a> using the appropriate space ID and access token. You should see the following data returned to your browser. The API has successfully returned the data you requested via the URL as JSON.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="busTuzCtBL"
      aria-describedby="busTuzCtBL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="busTuzCtBL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="busTuzCtBL" itemprop="text" content="%7B%0A%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%22type%22%3A%20%22Space%22%2C%0A%20%20%20%20%22id%22%3A%20%2284zl5qdw0ore%22%0A%20%20%7D%2C%0A%20%20%22name%22%3A%20%22%5BLive%5D%20nextjs-blog-starter%22%2C%0A%20%20%22locales%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22code%22%3A%20%22en-US%22%2C%0A%20%20%20%20%20%20%22default%22%3A%20true%2C%0A%20%20%20%20%20%20%22name%22%3A%20%22English%20(United%20States)%22%2C%0A%20%20%20%20%20%20%22fallbackCode%22%3A%20null%0A%20%20%20%20%7D%2C%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22code%22%3A%20%22fr%22%2C%0A%20%20%20%20%20%20%22default%22%3A%20false%2C%0A%20%20%20%20%20%20%22name%22%3A%20%22French%22%2C%0A%20%20%20%20%20%20%22fallbackCode%22%3A%20%22en-US%22%0A%20%20%20%20%7D%0A%20%20%5D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Space"</span><span class="token punctuation">,</span><br>    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"84zl5qdw0ore"</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"[Live] nextjs-blog-starter"</span><span class="token punctuation">,</span><br>  <span class="token property">"locales"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      <span class="token property">"code"</span><span class="token operator">:</span> <span class="token string">"en-US"</span><span class="token punctuation">,</span><br>      <span class="token property">"default"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>      <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"English (United States)"</span><span class="token punctuation">,</span><br>      <span class="token property">"fallbackCode"</span><span class="token operator">:</span> <span class="token null keyword">null</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">{</span><br>      <span class="token property">"code"</span><span class="token operator">:</span> <span class="token string">"fr"</span><span class="token punctuation">,</span><br>      <span class="token property">"default"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br>      <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"French"</span><span class="token punctuation">,</span><br>      <span class="token property">"fallbackCode"</span><span class="token operator">:</span> <span class="token string">"en-US"</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Making an HTTP GET request using JavaScript</h3><p class="post__p">Here’s the same GET request above being made in JavaScript via fetch. GET is the default HTTP method in fetch, so it doesn’t need to be specified in the code.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="FSfWbGBCWC"
      aria-describedby="FSfWbGBCWC">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="FSfWbGBCWC">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="FSfWbGBCWC" itemprop="text" content="const%20response%20%3D%20await%20fetch(%20%22https%3A%2F%2Fcdn.contentful.com%2Fspaces%2F84zl5qdw0ore%3Faccess_token%3D_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA%22%2C%0A).then((data)%20%3D%3E%20data.json())%3B%0A%0Aconsole.log(response)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span> <span class="token string">"https://cdn.contentful.com/spaces/84zl5qdw0ore?access_token=_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA"</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token operator">=></span> data<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Here’s the output in the browser console, which matches what we saw in the browser above.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6io8y55XsfEiYYwgHP7dCG/1b923df6b581a36690c5f5d1b1756bb7/fetch_get_response.png" alt="A browser console log output showing the data space data received from the API." height="491" width="1562" /><p class="post__p">Just as in the early example of the EDSAC, a good API has great documentation. API documentation describes the functionality of each API URL (or endpoint), how to send data in the correct format, and what data to expect from the API in return.</p><p class="post__p">Here’s a screenshot from the Contentful Content Delivery API documentation, showing how to request information about a Contentful space.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1RZzFWbDlegbLXlBRcCOc8/c70287a39b2dc2639e08100125ff1e8b/space_docs_example.png" alt="A screenshot of the Contentful documentation, showing the code sample for a space GET request, the parameters required for the request, and a sample response." height="1970" width="2198" /><p class="post__p">Notice that the documentation specifies:</p><ul><li><p class="post__p">the parameters needed to perform a successful request </p></li><li><p class="post__p">the type of HTTP request (GET, POST, etc.)</p></li><li><p class="post__p">the URL for the request</p></li><li><p class="post__p">the expected response of a successful call</p></li></ul><p class="post__p">The documentation also provides you with examples of how requests should be formatted in different programming languages and using different Contentful tools (such as SDKs and client libraries). Use the dropdown on <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/spaces/space/get-a-space/console/curl" target="_blank"><u>this documentation page</u></a> to see your options.</p><h2 class="post__h2">HTTP POST</h2><p class="post__p">POST requests require data to be sent via the request body rather than as URL parameters. For this reason, POST requests cannot be made using the address bar in a browser. This makes POST requests more secure than GET requests, and should always be used for API operations that deal with sensitive data, such as sending a login request. </p><h3 class="post__h3">Making an HTTP POST request using JavaScript</h3><p class="post__p">Here’s an example of a POST request using fetch in JavaScript. This example posts a request to the Contentful GraphQL API, asking for the total number of blog posts in the Contentful space with space ID 84zl5qdw0ore. </p><p class="post__p">You may think it’s strange to request data by <b class="post__p--italic">sending data</b> — but GraphQL allows you to create, change and fetch data in a single HTTP call. That’s why the POST method is the prefered way because you definitely don’t want to update data with a GET request! This is also an example of sending extra HTTP headers with a request using JavaScript fetch (see authorization and content-type in the example below). </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="aWCAKNoron"
      aria-describedby="aWCAKNoron">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="aWCAKNoron">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="aWCAKNoron" itemprop="text" content="const%20response%20%3D%20await%20fetch(%0A%20%20%60https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F84zl5qdw0ore%60%2C%0A%20%20%7B%0A%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20authorization%3A%20%22Bearer%20_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA%22%2C%0A%20%20%20%20%20%20%22content-type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%20%20body%3A%20'%7B%22query%22%3A%22%7B%20blogPostCollection%20%7B%20total%20%7D%20%7D%22%7D'%2C%0A%20%20%7D%2C%0A).then((response)%20%3D%3E%20response.json())%3B%0A%0Aconsole.log(response)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><br>  <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://graphql.contentful.com/content/v1/spaces/84zl5qdw0ore</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>  <span class="token punctuation">{</span><br>    <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">authorization</span><span class="token operator">:</span> <span class="token string">"Bearer _9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA"</span><span class="token punctuation">,</span><br>      <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">'{"query":"{ blogPostCollection { total } }"}'</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">If you’d like more information on how GraphQL works (it’s pretty cool!) — <a href="https://www.contentful.com/developers/videos/learn-graphql/" target="_blank"><u>check out our GraphQL video course on our developer portal</u></a>. </p><h2 class="post__h2">That’s a wrap!</h2><p class="post__p">Now you know what an API is, how APIs work over HTTP and what you send and receive via GET and POST requests, have a go at putting your knowledge into practice by exploring the <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/" target="_blank"><u>Contentful REST API </u></a>and <a href="https://www.contentful.com/developers/docs/references/graphql/" target="_blank"><u>Contentful GraphQL API </u></a>using your preferred programming language. </p><p class="post__p">If you’ve got any questions, come and join the <a href="https://www.contentful.com/slack" target="_blank"><u>Contentful Community Slack, </u></a>where we’ll be happy to help you out. </p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>A Next.js complete beginner tutorial using the Spotify API</title>
          <description>I'm joined by Ebonie (metalandcoffee_) where we set up a new Next.js and Spotify API project to curate music recommendations.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/next-js-beginner-tutorial-using-spotify-api/</link>
          <guid>https://whitep4nth3r.com/blog/next-js-beginner-tutorial-using-spotify-api/</guid>
          <pubDate>Wed, 04 Aug 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In this video I&#39;m joined by Ebonie, otherwise known as <a href="https://twitter.com/metalandcoffee_" target="_blank">MetalAndCoffee_</a> — web developer, Metal DJ and Twitch streamer — to set up a new project in Next.js to curate music recommendations using the Spotify API.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/XPnBp0LwCFY"
        title="Next.js beginner tutorial using the Spotify API with @metalandcoffee_"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p">This video tutorial takes an in-depth look at the structure of a Next.js application for complete beginners. I also include a demonstration of some tips and tricks on how to make your project more accessible and how to improve the developer experience, including <a href="https://whitep4nth3r.com/blog/how-to-set-up-new-next-js-projects-with-bash-script/">How I set up my new Next.js projects with a handy bash script</a> and <a href="https://whitep4nth3r.com/blog/how-to-avoid-using-relative-path-imports-next-js/">How to avoid using relative path imports in Next.js</a>.</p><p class="post__p">In the project we use <a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation" target="_blank">getStaticProps()</a> to fetch data from Spotify at build-time to generate static pages that work without JavaScript on the client. We also use <a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation" target="_blank">getStaticPaths()</a> to generate over 100 dynamic routes at build time using data from the Spotify API. It works really well!</p><h2 class="post__h2">Source code</h2><p class="post__p"><a href="https://github.com/metalandcoffee/metal-music-curations" target="_blank">View the code for this project on GitHub</a>.</p><h2 class="post__h2">Video outline</h2><p class="post__p">Click on the links below to jump to the relevant sections of the video.</p><ul><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=31s" target="_blank">Project description</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=62s" target="_blank">Why use Next.js?</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=65s" target="_blank">Structuring a Next.js application with dynamic routes</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=180s" target="_blank">Exploring the Next.js app folder structure</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=196s" target="_blank">The Next.js pages directory</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=246s" target="_blank">The Next.js API directory and serverless functions</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=368s" target="_blank">The Next.js public directory</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=396s" target="_blank">The Next.js styles directory</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=410s" target="_blank">Adding a custom _document.js file to add a lang attribute on the HTML tag</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=493s" target="_blank">Adding a jsconfig.json to avoid relative path imports</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=532s" target="_blank">Getting started by editing pages/index.js</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=551s" target="_blank">The Next.js Head component</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=577s" target="_blank">What you get with CSS Modules</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=690s" target="_blank">The structure of a Next.js page file</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=718s" target="_blank">Data fetching with Next.js</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=742s" target="_blank">Setting up getStaticProps() and passing data to a page component</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=875s" target="_blank">Working with environment variables and .env files in Next.js</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=935s" target="_blank">Using JavaScript fetch to get a list of genres from Spotify in getStaticProps()</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=1091s" target="_blank">Setting up getStaticPaths() to generate dynamic routes with data from Spotify</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=1412s" target="_blank">Using dynamic route params to fetch track recommendations from Spotify in getStaticProps()</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=1506s" target="_blank">Using Next.js Link to enable client-side transitions between page routes</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=1677s" target="_blank">Creating a React component to render Spotify track information</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=1765s" target="_blank">Using Next Image to optimize images with lazy loading, source set and preventing cumulative layout shift</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=2020s" target="_blank">CSS Module file naming conventions and structuring your projects for scale</a></p></li><li><p class="post__p"><a href="https://www.youtube.com/watch?v=XPnBp0LwCFY&t=2147s" target="_blank">Rebuilding your site on the server when data changes using Incremental Static Regeneration (ISR)</a></p></li></ul><p class="post__p">This video was recorded <a href="https://twitch.tv/whitep4nth3r" target="_blank">live on Twitch </a>on Friday 30th July 2021.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to avoid using relative path imports in Next.js</title>
          <description>Say goodbye to ../really/long/and/silly/paths/to/components in your Next.js application and define absolute imports with a jsconfig.json file.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-avoid-using-relative-path-imports-next-js/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-avoid-using-relative-path-imports-next-js/</guid>
          <pubDate>Sun, 11 Jul 2021 23:00:00 GMT</pubDate>
          <category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Does this look familiar? 🤯</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="SnexHUZfXt"
      aria-describedby="SnexHUZfXt">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="SnexHUZfXt">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="SnexHUZfXt" itemprop="text" content="import%20MyComponent%20from%20%22..%2F..%2F..%2F..%2F..%2Fcomponents%2FMyComponent%22%3B%0Aimport%20ADifferentFile%20from%20%22..%2F..%2F..%2Fsome%2Fother%2Fdir%2FADifferentFile%22%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> MyComponent <span class="token keyword">from</span> <span class="token string">"../../../../../components/MyComponent"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> ADifferentFile <span class="token keyword">from</span> <span class="token string">"../../../some/other/dir/ADifferentFile"</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Relative import paths to files in any application can be tricky to manage. Often we rely on the intelligence of our IDEs to tell us how many <b class="post__p--italic">dot-dot-slashes</b> to type when we&#39;re importing files that are nested many directories deep. If you&#39;re working with Next.js — there&#39;s a better way!</p><p class="post__p">Define your base directories — or <b class="post__p--bold">module aliases</b> — in a jsconfig.json file at the root of your Next.js project.</p><p class="post__p">Here&#39;s the jsconfig.json file I use for the code that powers <a href="https://whitep4nth3r.com" target="_blank">whitep4nth3r.com.</a></p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="TkSKuZgLRF"
      aria-describedby="TkSKuZgLRF">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="TkSKuZgLRF">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="TkSKuZgLRF" itemprop="text" content="%7B%0A%20%20%22compilerOptions%22%3A%20%7B%0A%20%20%20%20%22baseUrl%22%3A%20%22.%2F%22%2C%0A%20%20%20%20%22paths%22%3A%20%7B%0A%20%20%20%20%20%20%22%40components%2F*%22%3A%20%5B%22components%2F*%22%5D%2C%0A%20%20%20%20%20%20%22%40contentful%2F*%22%3A%20%5B%22contentful%2F*%22%5D%2C%0A%20%20%20%20%20%20%22%40layouts%2F*%22%3A%20%5B%22layouts%2F*%22%5D%2C%0A%20%20%20%20%20%20%22%40styles%2F*%22%3A%20%5B%22styles%2F*%22%5D%2C%0A%20%20%20%20%20%20%22%40utils%2F*%22%3A%20%5B%22utils%2F*%22%5D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"compilerOptions"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"baseUrl"</span><span class="token operator">:</span> <span class="token string">"./"</span><span class="token punctuation">,</span><br>    <span class="token property">"paths"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token property">"@components/*"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"components/*"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>      <span class="token property">"@contentful/*"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"contentful/*"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>      <span class="token property">"@layouts/*"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"layouts/*"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>      <span class="token property">"@styles/*"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"styles/*"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>      <span class="token property">"@utils/*"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"utils/*"</span><span class="token punctuation">]</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Using module aliases, import paths at the top of files are self-documenting and easier to write, meaning you can focus on writing code rather than traversing spaghetti directories. It&#39;s beautiful.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mOPJTTfoxT"
      aria-describedby="mOPJTTfoxT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mOPJTTfoxT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="mOPJTTfoxT" itemprop="text" content="import%20PageMeta%20from%20%22%40components%2FPageMeta%22%3B%0Aimport%20RecentPostList%20from%20%22%40components%2FRecentPostList%22%3B%0Aimport%20SocialCards%20from%20%22%40components%2FSocialCards%22%3B%0A%0Aimport%20ContentfulBlogPost%20from%20%22%40contentful%2FBlogPost%22%3B%0A%0Aimport%20MainLayout%20from%20%22%40layouts%2Fmain%22%3B%0A%0Aimport%20Styles%20from%20%22%40styles%2FBaseStyles.module.css%22%3B%0A%0Aimport%20%7B%20Config%20%7D%20from%20%22%40utils%2FConfig%22%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> PageMeta <span class="token keyword">from</span> <span class="token string">"@components/PageMeta"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> RecentPostList <span class="token keyword">from</span> <span class="token string">"@components/RecentPostList"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> SocialCards <span class="token keyword">from</span> <span class="token string">"@components/SocialCards"</span><span class="token punctuation">;</span><br><br><span class="token keyword">import</span> ContentfulBlogPost <span class="token keyword">from</span> <span class="token string">"@contentful/BlogPost"</span><span class="token punctuation">;</span><br><br><span class="token keyword">import</span> MainLayout <span class="token keyword">from</span> <span class="token string">"@layouts/main"</span><span class="token punctuation">;</span><br><br><span class="token keyword">import</span> Styles <span class="token keyword">from</span> <span class="token string">"@styles/BaseStyles.module.css"</span><span class="token punctuation">;</span><br><br><span class="token keyword">import</span> <span class="token punctuation">{</span> Config <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@utils/Config"</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://nextjs.org/docs/advanced-features/module-path-aliases" target="_blank">Read more about absolute imports and module path aliases on the Next.js documentation</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How I set up my new Next.js projects with a handy bash script</title>
          <description>After I create a new Next.js application I run this bash script to prepare my app for development — just the way I like it. Give it a try!</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-set-up-new-next-js-projects-with-bash-script/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-set-up-new-next-js-projects-with-bash-script/</guid>
          <pubDate>Thu, 08 Jul 2021 23:00:00 GMT</pubDate>
          <category>Snippets</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you’ve watched my <a href="https://twitch.tv/whitep4nth3r" target="_blank">live streams on Twitch</a>, you’ll have seen me speed-build and YOLO-deploy quite a few websites with Next.js.</p><p class="post__p">After I bootstrap a new Next.js application with <code>npx create-next-app</code>, there are several things I do to prepare the app for development, including:</p><ul><li><p class="post__p">delete the vercel.svg file</p></li><li><p class="post__p">delete the Home.module.css file</p></li><li><p class="post__p">add my own CSS reset rules to globals.css</p></li><li><p class="post__p">add a custom pages/_document.js file to add a lang attribute to the HTML tag to improve accessibility</p></li><li><p class="post__p">delete lots of code from pages/index.js</p></li><li><p class="post__p">open the new project in VSCode</p></li></ul><p class="post__p">I’m a big fan of automation, and so I wrote a bash script to take care of these tasks for me — and to remind me to have a nice day 😎.</p><p class="post__p">If you’d like to do the same, run the script below with <code>—lang</code>, <code>—appname</code> and <code>—dir</code> flags — and have a nice day! You can also <a href="https://gist.github.com/whitep4nth3r/c178d99e8e3bd276c7d0047c3bd924da" target="_blank">bookmark the gist on GitHub</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="rfRGsUXgVy"
      aria-describedby="rfRGsUXgVy">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="rfRGsUXgVy">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="rfRGsUXgVy" itemprop="text" content="%23%20Input%20flags%0A%0ALANG%3D%22%22%0AAPP_NAME%3D%22%22%0A%0A%23%20The%20directory%20path%20must%20be%20relative%20to%20where%20the%20script%20lives%0ADIR%3D%22%22%0A%0A%23%20Loop%20through%20arguments%20and%20process%20them%0Afor%20arg%20in%20%22%24%40%22%0Ado%0A%20%20%20%20case%20%24arg%20in%0A%20%20%20%20%20%20%20%20-h%7C--help)%0A%20%20%20%20%20%20%20%20echo%20%22%E2%9A%A1%EF%B8%8F%20Example%20script%20usage%20%E2%9A%A1%EF%B8%8F%22%0A%20%20%20%20%20%20%20%20echo%20%22.%2Freset-next.sh%20--lang%3Den%20--appname%3D%5C%22my%20cool%20app%5C%22%20--dir%3Dthis-test%22%0A%20%20%20%20%20%20%20%20shift%0A%20%20%20%20%20%20%20%20exit%3B%0A%20%20%20%20%20%20%20%20%3B%3B%0A%20%20%20%20%20%20%20%20-l%3D*%7C--lang%3D*)%0A%20%20%20%20%20%20%20%20LANG%3D%22%24%7Barg%23*%3D%7D%22%0A%20%20%20%20%20%20%20%20shift%0A%20%20%20%20%20%20%20%20%3B%3B%0A%20%20%20%20%20%20%20%20-a%3D*%7C--appname%3D*)%0A%20%20%20%20%20%20%20%20APP_NAME%3D%22%24%7Barg%23*%3D%7D%22%0A%20%20%20%20%20%20%20%20shift%0A%20%20%20%20%20%20%20%20%3B%3B%0A%20%20%20%20%20%20%20%20-d%3D*%7C--dir%3D*)%0A%20%20%20%20%20%20%20%20DIR%3D%22%24%7Barg%23*%3D%7D%22%0A%20%20%20%20%20%20%20%20shift%0A%20%20%20%20%20%20%20%20%3B%3B%0A%20%20%20%20esac%0Adone%0A%0Achange_dir%20()%20%7B%0A%20%20echo%20%22%E2%9C%A8%20Changing%20directory%20to%20%241%22%0A%20%20cd%20%241%0A%7D%0A%0Adelete_vercel_svg%20()%20%7B%0A%20%20echo%20%22%E2%9D%8C%20Deleting%20vercel.svg%22%0A%20%20rm%20public%2Fvercel.svg%0A%7D%0A%0Adelete_home_css%20()%20%7B%0A%20%20echo%20%22%E2%9D%8C%20%20Deleting%20Home.module.css%22%0A%20%20rm%20styles%2FHome.module.css%0A%7D%0A%0Aadd_custom_document%20()%20%7B%0A%20%20echo%20%22%E2%9C%85%20Adding%20custom%20_document.js%20with%20lang%3D%24LANG%22%0A%20%20cd%20pages%0A%20%20echo%20'import%20Document%2C%20%7B%20Html%2C%20Head%2C%20Main%2C%20NextScript%20%7D%20from%20%22next%2Fdocument%22%3B%0A%20%20class%20MyDocument%20extends%20Document%20%7B%0A%20%20%20%20render()%20%7B%0A%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%3CHtml%20lang%3D%22'%24LANG'%22%3E%0A%20%20%20%20%20%20%20%20%20%20%3CHead%20%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cbody%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CMain%20%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CNextScript%20%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2Fbody%3E%0A%20%20%20%20%20%20%20%20%3C%2FHtml%3E%0A%20%20%20%20%20%20)%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%20%20export%20default%20MyDocument%3B'%20%3E%3E%20_document.js%0A%20%20cd%20..%0A%7D%0A%0Areplace_index%20()%20%7B%0A%20%20echo%20%22%E2%9C%85%20%20Replacing%20pages%2Findex.js%22%0A%20%20cd%20pages%0A%20%20rm%20index.js%0A%20%20echo%20'import%20Head%20from%20%22next%2Fhead%22%3B%0A%20%20export%20default%20function%20Home()%20%7B%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%3C%3E%0A%20%20%20%20%20%20%20%20%3CHead%3E%0A%20%20%20%20%20%20%20%20%20%20%3Ctitle%3E'%24APP_NAME'%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cmeta%20name%3D%22description%22%20content%3D%22Description%20for%20'%24APP_NAME'%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%3Clink%20rel%3D%22icon%22%20href%3D%22%2Ffavicon.ico%22%20%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FHead%3E%0A%20%20%20%20%20%20%20%20%3Cmain%3E%0A%20%20%20%20%20%20%20%20%20%20%3Ch1%3EThis%20new%20Next.js%20app%20has%20been%20reset!%3C%2Fh1%3E%0A%20%20%20%20%20%20%20%20%3C%2Fmain%3E%0A%20%20%20%20%20%20%3C%2F%3E%0A%20%20%20%20)%3B%0A%20%20%7D'%20%3E%3E%20index.js%0A%20%20cd%20..%0A%7D%0A%0Areplace_globals_css%20()%20%7B%0A%20%20echo%20%22%E2%9C%85%20%20Replacing%20styles%2Fglobals.css%22%0A%20%20cd%20styles%0A%20%20rm%20globals.css%0A%20%20echo%20'html%20%7B%0A%20%20font-size%3A%20100%25%3B%0A%7D%0Abody%20%7B%0A%20%20font-size%3A%201rem%3B%0A%20%20padding%3A%200%3B%0A%20%20margin%3A%200%3B%0A%20%20font-family%3A%20-apple-system%2C%20BlinkMacSystemFont%2C%20Segoe%20UI%2C%20Roboto%2C%20Oxygen%2C%20Ubuntu%2C%20Cantarell%2C%0A%20%20%20%20Fira%20Sans%2C%20Droid%20Sans%2C%20Helvetica%20Neue%2C%20sans-serif%3B%0A%7D%0A*%20%7B%0A%20%20box-sizing%3A%20border-box%3B%0A%7D%0A'%20%3E%3E%20globals.css%0A%20%20cd%20..%0A%7D%0A%0Aecho%20%22%F0%9F%94%A5%20Resetting%20Next.js%20app%20in%20%24DIR%22%0Aecho%20%22%E2%9C%A8%20Language%3A%20%24LANG%22%0Aecho%20%22%E2%9C%A8%20App%20name%3A%20%24APP_NAME%22%0A%0Achange_dir%20%24DIR%0Adelete_vercel_svg%0Adelete_home_css%0Aadd_custom_document%0Areplace_index%0Areplace_globals_css%0A%0Aecho%20%22%E2%9C%A8%20Opening%20project%20in%20VSCode%22%0Acode%20.%0A%0Aecho%20%22%F0%9F%93%A3%20DONE.%20Have%20a%20nice%20day!%22">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># Input flags</span><br><br><span class="token assign-left variable"><span class="token environment constant">LANG</span></span><span class="token operator">=</span><span class="token string">""</span><br><span class="token assign-left variable">APP_NAME</span><span class="token operator">=</span><span class="token string">""</span><br><br><span class="token comment"># The directory path must be relative to where the script lives</span><br><span class="token assign-left variable">DIR</span><span class="token operator">=</span><span class="token string">""</span><br><br><span class="token comment"># Loop through arguments and process them</span><br><span class="token keyword">for</span> <span class="token for-or-select variable">arg</span> <span class="token keyword">in</span> <span class="token string">"<span class="token variable">$@</span>"</span><br><span class="token keyword">do</span><br>    <span class="token keyword">case</span> <span class="token variable">$arg</span> <span class="token keyword">in</span><br>        -h<span class="token operator">|</span>--help<span class="token punctuation">)</span><br>        <span class="token builtin class-name">echo</span> <span class="token string">"⚡️ Example script usage ⚡️"</span><br>        <span class="token builtin class-name">echo</span> <span class="token string">"./reset-next.sh --lang=en --appname=<span class="token entity" title="\&quot;">\"</span>my cool app<span class="token entity" title="\&quot;">\"</span> --dir=this-test"</span><br>        <span class="token builtin class-name">shift</span><br>        <span class="token builtin class-name">exit</span><span class="token punctuation">;</span><br>        <span class="token punctuation">;</span><span class="token punctuation">;</span><br>        <span class="token parameter variable">-l</span><span class="token operator">=</span>*<span class="token operator">|</span>--lang<span class="token operator">=</span>*<span class="token punctuation">)</span><br>        <span class="token assign-left variable"><span class="token environment constant">LANG</span></span><span class="token operator">=</span><span class="token string">"<span class="token variable">${arg<span class="token operator">#</span>*=}</span>"</span><br>        <span class="token builtin class-name">shift</span><br>        <span class="token punctuation">;</span><span class="token punctuation">;</span><br>        <span class="token parameter variable">-a</span><span class="token operator">=</span>*<span class="token operator">|</span>--appname<span class="token operator">=</span>*<span class="token punctuation">)</span><br>        <span class="token assign-left variable">APP_NAME</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${arg<span class="token operator">#</span>*=}</span>"</span><br>        <span class="token builtin class-name">shift</span><br>        <span class="token punctuation">;</span><span class="token punctuation">;</span><br>        <span class="token parameter variable">-d</span><span class="token operator">=</span>*<span class="token operator">|</span>--dir<span class="token operator">=</span>*<span class="token punctuation">)</span><br>        <span class="token assign-left variable">DIR</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${arg<span class="token operator">#</span>*=}</span>"</span><br>        <span class="token builtin class-name">shift</span><br>        <span class="token punctuation">;</span><span class="token punctuation">;</span><br>    <span class="token keyword">esac</span><br><span class="token keyword">done</span><br><br><span class="token function-name function">change_dir</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"✨ Changing directory to <span class="token variable">$1</span>"</span><br>  <span class="token builtin class-name">cd</span> <span class="token variable">$1</span><br><span class="token punctuation">}</span><br><br><span class="token function-name function">delete_vercel_svg</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"❌ Deleting vercel.svg"</span><br>  <span class="token function">rm</span> public/vercel.svg<br><span class="token punctuation">}</span><br><br><span class="token function-name function">delete_home_css</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"❌  Deleting Home.module.css"</span><br>  <span class="token function">rm</span> styles/Home.module.css<br><span class="token punctuation">}</span><br><br><span class="token function-name function">add_custom_document</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"✅ Adding custom _document.js with lang=<span class="token environment constant">$LANG</span>"</span><br>  <span class="token builtin class-name">cd</span> pages<br>  <span class="token builtin class-name">echo</span> <span class="token string">'import Document, { Html, Head, Main, NextScript } from "next/document";<br>  class MyDocument extends Document {<br>    render() {<br>      return (<br>        &lt;Html lang="'</span><span class="token environment constant">$LANG</span><span class="token string">'"><br>          &lt;Head /><br>          &lt;body><br>            &lt;Main /><br>            &lt;NextScript /><br>          &lt;/body><br>        &lt;/Html><br>      );<br>    }<br>  }<br>  export default MyDocument;'</span> <span class="token operator">>></span> _document.js<br>  <span class="token builtin class-name">cd</span> <span class="token punctuation">..</span><br><span class="token punctuation">}</span><br><br><span class="token function-name function">replace_index</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"✅  Replacing pages/index.js"</span><br>  <span class="token builtin class-name">cd</span> pages<br>  <span class="token function">rm</span> index.js<br>  <span class="token builtin class-name">echo</span> <span class="token string">'import Head from "next/head";<br>  export default function Home() {<br>    return (<br>      &lt;><br>        &lt;Head><br>          &lt;title>'</span><span class="token variable">$APP_NAME</span><span class="token string">'&lt;/title><br>          &lt;meta name="description" content="Description for '</span><span class="token variable">$APP_NAME</span><span class="token string">'" /><br>          &lt;link rel="icon" href="/favicon.ico" /><br>        &lt;/Head><br>        &lt;main><br>          &lt;h1>This new Next.js app has been reset!&lt;/h1><br>        &lt;/main><br>      &lt;/><br>    );<br>  }'</span> <span class="token operator">>></span> index.js<br>  <span class="token builtin class-name">cd</span> <span class="token punctuation">..</span><br><span class="token punctuation">}</span><br><br><span class="token function-name function">replace_globals_css</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token builtin class-name">echo</span> <span class="token string">"✅  Replacing styles/globals.css"</span><br>  <span class="token builtin class-name">cd</span> styles<br>  <span class="token function">rm</span> globals.css<br>  <span class="token builtin class-name">echo</span> <span class="token string">'html {<br>  font-size: 100%;<br>}<br>body {<br>  font-size: 1rem;<br>  padding: 0;<br>  margin: 0;<br>  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,<br>    Fira Sans, Droid Sans, Helvetica Neue, sans-serif;<br>}<br>* {<br>  box-sizing: border-box;<br>}<br>'</span> <span class="token operator">>></span> globals.css<br>  <span class="token builtin class-name">cd</span> <span class="token punctuation">..</span><br><span class="token punctuation">}</span><br><br><span class="token builtin class-name">echo</span> <span class="token string">"🔥 Resetting Next.js app in <span class="token variable">$DIR</span>"</span><br><span class="token builtin class-name">echo</span> <span class="token string">"✨ Language: <span class="token environment constant">$LANG</span>"</span><br><span class="token builtin class-name">echo</span> <span class="token string">"✨ App name: <span class="token variable">$APP_NAME</span>"</span><br><br>change_dir <span class="token variable">$DIR</span><br>delete_vercel_svg<br>delete_home_css<br>add_custom_document<br>replace_index<br>replace_globals_css<br><br><span class="token builtin class-name">echo</span> <span class="token string">"✨ Opening project in VSCode"</span><br>code <span class="token builtin class-name">.</span><br><br><span class="token builtin class-name">echo</span> <span class="token string">"📣 DONE. Have a nice day!"</span></code></pre>
    </div>
  </div>

  <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to add Algolia InstantSearch to your Next.js application</title>
          <description>Every content website needs a search box.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/07/02/add-algolia-instantsearch-to-nextjs-app/</link>
          <guid>https://www.contentful.com/blog/2021/07/02/add-algolia-instantsearch-to-nextjs-app/</guid>
          <pubDate>Thu, 01 Jul 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>NodeJS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">By the time I had written 15 blog articles on <a href="https://whitep4nth3r.com/" target="_blank">my website</a>, it was getting a little tricky to find what I was looking for in a hurry! So I set out to implement search functionality on my blog.</p><p class="post__p">After researching my options, I decided to try out <a href="https://www.algolia.com/" target="_blank">Algolia</a>. Algolia is a flexible hosted search and discovery API that comes with a generous free community plan. It provides up to 10,000 search requests per month, pre-built UI libraries (which we’ll use in this tutorial), natural language processing and many other features. What’s more, the engineers at Algolia are wonderfully helpful! I’d especially like to extend a huge thank you to <a href="https://twitter.com/LukyVJ" target="_blank">LukyVJ</a>, who showed up while I was learning about Algolia live on Twitch and helped me navigate the docs for the UI library.</p><h2 class="post__h2">What we’ll do in this tutorial</h2><ol><li><p class="post__p">Set up Algolia to receive data to power search results on a web application</p></li><li><p class="post__p">Create a custom script to transform and send the data to Algolia</p></li><li><p class="post__p">Build out the search UI in a Next.js application using the Algolia React InstantSearch UI</p></li></ol><p class="post__p">While the content on my blog site is powered by Contentful, the following concepts apply to any data store or headless CMS out there — even if you store your blog content as markdown with your code. All you need is a Next.js application and some content!</p><img src="https://images.ctfassets.net/56dzm01z6lln/34RB7j18bWHjEZ5edmCKs8/4f15d24f41a4759e3b1accc043cd621f/search_example_on_blog.png" alt="A screenshot of my website showing the new search box in action." height="879" width="853" /><p class="post__p">Let’s get started!</p><h2 class="post__h2">Sign up for Algolia</h2><p class="post__p">Head on over to Algolia to <a href="https://www.algolia.com/users/sign_up" target="_blank">sign up</a>. You’re invited to a free standard trial for 14 days, after which the plan will be converted to the Community plan automatically.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7donKegoMvrws9X3BeQBKu/9b8937f651bb61b2c3e766973a4cd85c/sign_up.png" alt="A screenshot of the Algolia website sign up screen." height="855" width="1024" /><p class="post__p">Algolia does a really nice job of guiding you through the onboarding process. Follow the instructions until you land on the <b class="post__p--bold">Get started</b> screen!</p><img src="https://images.ctfassets.net/56dzm01z6lln/2XWmWq7DeocHoGKGxp2rhi/6bd40b2e15388ff7a27664bcfb4afe93/get_started.png" alt="A screenshot of the Get started screen after signing up to Algolia." height="887" width="1633" /><h2 class="post__h2">Create a new index</h2><p class="post__p">The first step in your search journey is to create a new index in Algolia. An index stores the data that you want to make searchable in Algolia. I like to think of it as a NoSQL document that stores JSON objects of your content. <a href="https://www.algolia.com/doc/api-client/methods/indexing/" target="_blank">Read more about this on the Algolia docs</a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1faaMfPTQT0YpSlNvQBpsH/1febfa6ffa010fde8fd2ac99ae64f5a0/create_index.png" alt="A screenshot of the first step in setting up Algolia. The CTA is "create a new index" and it provides an input field to name your index. I chose the name "my_awesome_content"." height="252" width="726" /><h2 class="post__h2">Grab your API keys</h2><p class="post__p">Next, you’ll need three API keys from your Algolia account. Navigate to the <b class="post__p--bold">API Keys</b> area via the sidebar menu.</p><p class="post__p">Find your <b class="post__p--bold">Application ID</b>, <b class="post__p--bold">Search-Only API Key</b> and <b class="post__p--bold">Admin API Key</b>. In your .env file in your Next.js application, add the following environment variables.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="sBsFNjEggS"
      aria-describedby="sBsFNjEggS">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="sBsFNjEggS">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="sBsFNjEggS" itemprop="text" content="NEXT_PUBLIC_ALGOLIA_APP_ID%3D%7BApplication%20ID%7D%0ANEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY%3D%7BSearch-Only%20API%20Key%7D%0AALGOLIA_SEARCH_ADMIN_KEY%3D%7BAdmin%20API%20Key%7D">
      <pre class="language-markup"><code class="language-markup">NEXT_PUBLIC_ALGOLIA_APP_ID={Application ID}<br>NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY={Search-Only API Key}<br>ALGOLIA_SEARCH_ADMIN_KEY={Admin API Key}</code></pre>
    </div>
  </div>

  <p class="post__p">To initialize InstantSearch on the front end, we need the Application ID and the Search API key to be publicly available on the client-side. Make sure to preface these two variables with <code>NEXT_PUBLIC_</code>. Just like the Contentful Content Delivery API keys, these keys provide <b class="post__p--bold">read-only access</b> to your search results, so it’s okay to expose them. </p><p class="post__p">We’re going to use the Admin API Key on the server-side only as part of the script to send data to the Algolia index. <b class="post__p--bold">This key provides write-access to your Algolia index.</b> <b class="post__p--bold">Be sure to keep the Admin API Key a secret and do not expose it to the client with the </b><code>NEXT_PUBLIC_ </code><b class="post__p--bold">prefix.</b></p><p class="post__p">That’s the setup! It’s done in just three steps! Now it’s time to write some code.</p><h2 class="post__h2">Write a custom script to build your data for your Algolia index</h2><p class="post__p">Let’s create a custom script to fetch our data and build up an array of objects to send to our Algolia index. I would recommend working in a script file that’s separate from the Next.js application architecture, which we can call with the postbuild command via the package.json scripts.</p><h3 class="post__h3">Create the script file</h3><p class="post__p">Create a directory called scripts and create a new file within it. I named my file build-search.js.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1N3ZBtvvkRa9zBobw73wUm/2f0552d3bd969562ed01197f84c7a007/create_build_search.png" alt="A screenshot of the file explorer in VSCode showing a scripts directory and a file named "build-search.js" inside it." height="977" width="1233" /><p class="post__p">To your package.json file, add the <code>postbuild</code> command to run the script. This will run <code>node build-search.js</code> in the build pipeline after the <code>build</code> command has completed.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="hkGmTrANsI"
      aria-describedby="hkGmTrANsI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="hkGmTrANsI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="hkGmTrANsI" itemprop="text" content="%2F%2F%20package.json%0A%0A%22scripts%22%3A%20%7B%0A%20%20%22dev%22%3A%20%22next%20dev%22%2C%0A%20%20%22build%22%3A%20%22next%20build%22%2C%0A%20%20%22postbuild%22%3A%20%22node%20.%2Fscripts%2Fbuild-search.js%22%2C%0A%20%20%22start%22%3A%20%22next%20start%22%0A%7D%2C">
      <pre class="language-json"><code class="language-json"><span class="token comment">// package.json</span><br><br><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  <span class="token property">"dev"</span><span class="token operator">:</span> <span class="token string">"next dev"</span><span class="token punctuation">,</span><br>  <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"next build"</span><span class="token punctuation">,</span><br>  <span class="token property">"postbuild"</span><span class="token operator">:</span> <span class="token string">"node ./scripts/build-search.js"</span><span class="token punctuation">,</span><br>  <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"next start"</span><br><span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Install dependencies</h3><p class="post__p">Let’s install the following dependencies from npm:</p><ul><li><p class="post__p"><b class="post__p--bold">algoliasearch</b> — to connect to the Algolia API</p></li><li><p class="post__p"><b class="post__p--bold">dotenv</b> — to access environment variables outside of the Next.js application</p></li></ul><p class="post__p">Run the following command in your terminal at the root of your project:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KBBBXzORzU"
      aria-describedby="KBBBXzORzU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KBBBXzORzU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="KBBBXzORzU" itemprop="text" content="npm%20install%20dotenv%20algoliasearch%20">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> dotenv algoliasearch</code></pre>
    </div>
  </div>

  <h3 class="post__h3">A note about Contentful Rich Text</h3><p class="post__p">The final implementation on my website handles adding a Contentful Rich Text field response to my search index as plain text. To reduce complexity, we won’t cover Rich Text in this post. But if you’re curious, <a href="https://github.com/whitep4nth3r/p4nth3rblog/blob/a32bf9e239b1d894b4e08e4c2445bd4b190a1c7a/scripts/build-search.js#L97" target="_blank">find the code to handle Rich Text on GitHub</a>.</p><h3 class="post__h3">Set up the script with an Immediately Invoked Function Expression</h3><p class="post__p">The script should perform several asynchronous operations, including fetching data from Contentful, transforming it and sending it to Algolia. To make the code more readable and to use async/await, we’re going to wrap everything in an async Immediately Invoked Function Expression (IIFE).</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DIMMuLaJNx"
      aria-describedby="DIMMuLaJNx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DIMMuLaJNx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="DIMMuLaJNx" itemprop="text" content="%2F%2F%20build-search.js%0Aconst%20dotenv%20%3D%20require(%22dotenv%22)%3B%0A%0A(async%20function%20()%20%7B%0A%20%20%2F%2F%20initialize%20environment%20variables%0A%20%20dotenv.config()%3B%0A%0A%20%20console.log(%22Schnitzel!%20Let's%20fetch%20some%20data!%22)%3B%0A%0A%7D)()%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// build-search.js</span><br><span class="token keyword">const</span> dotenv <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"dotenv"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// initialize environment variables</span><br>  dotenv<span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">"Schnitzel! Let's fetch some data!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Run your script from the root of the project on the command line to test it out:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ttoLUmxwIc"
      aria-describedby="ttoLUmxwIc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ttoLUmxwIc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="ttoLUmxwIc" itemprop="text" content="node%20.%2Fscripts%2Fbuild-search.js">
      <pre class="language-bash"><code class="language-bash"><span class="token function">node</span> ./scripts/build-search.js</code></pre>
    </div>
  </div>

  <h3 class="post__h3">Fetch your data</h3><p class="post__p">Fetch your data however you need to. <a href="https://github.com/whitep4nth3r/p4nth3rblog/blob/main/scripts/build-search.js" target="_blank">View the full build-search.js file on GitHub</a> to check out how I used the Contentful GraphQL API and node-fetch to grab my data for processing.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="wxlqeUjILo"
      aria-describedby="wxlqeUjILo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="wxlqeUjILo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="wxlqeUjILo" itemprop="text" content="%2F%2F%20build-search.js%0Aconst%20dotenv%20%3D%20require(%22dotenv%22)%3B%0A%0Aasync%20function%20getAllBlogPosts()%20%7B%0A%20%20%2F%2F%20write%20your%20code%20to%20fetch%20your%20data%0A%7D%0A%0A(async%20function%20()%20%7B%0A%20%20%2F%2F%20initialize%20environment%20variables%0A%20%20dotenv.config()%3B%0A%0A%20%20try%20%7B%0A%20%20%20%20%2F%2F%20fetch%20your%20data%0A%20%20%20%20const%20posts%20%3D%20await%20getAllBlogPosts()%3B%0A%0A%20%20%20%20%7D%0A%20%20%7D%20catch%20(error)%20%7B%0A%20%20%20%20console.log(error)%3B%0A%20%20%7D%0A%7D)()%3B%20">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// build-search.js</span><br><span class="token keyword">const</span> dotenv <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"dotenv"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getAllBlogPosts</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// write your code to fetch your data</span><br><span class="token punctuation">}</span><br><br><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// initialize environment variables</span><br>  dotenv<span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token comment">// fetch your data</span><br>    <span class="token keyword">const</span> posts <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getAllBlogPosts</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Transform your data for Algolia</h3><p class="post__p">Transforming your data for Algolia is as simple as creating an array of objects that contains the data you want to be searchable!</p><p class="post__p">Algolia search records are flexible and exist as objects of key-value pairs. Values can be added to the index as strings, booleans, numbers, arrays and objects. Attributes don’t have to respect a schema and can change from one object to another. For example, you could include a large recipe object or a smaller ingredient object in the same index! <a href="https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/" target="_blank">Read more on the Algolia docs about preparing your data for an index</a>.</p><p class="post__p">Here’s how I transformed my blog post data into an array of objects for Algolia. You can choose whether to provide an ID for each object, or have Algolia auto-generate an ID. Seeing as I had the <code>sys.id</code> from each blog post in Contentful, I chose to insert the posts with the IDs I had to hand.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="oRrjAeZbMy"
      aria-describedby="oRrjAeZbMy">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="oRrjAeZbMy">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="oRrjAeZbMy" itemprop="text" content="%2F%2F%20build-search.js%0Aconst%20dotenv%20%3D%20require(%22dotenv%22)%3B%0A%0Aasync%20function%20getAllBlogPosts()%20%7B%0A%20%20%2F%2F%20write%20your%20code%20to%20fetch%20your%20data%0A%7D%0A%0Afunction%20transformPostsToSearchObjects(posts)%20%7B%0A%20%20const%20transformed%20%3D%20posts.map((post)%20%3D%3E%20%7B%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20objectID%3A%20post.sys.id%2C%0A%20%20%20%20%20%20title%3A%20post.title%2C%0A%20%20%20%20%20%20excerpt%3A%20post.excerpt%2C%0A%20%20%20%20%20%20slug%3A%20post.slug%2C%0A%20%20%20%20%20%20topicsCollection%3A%20%7B%20items%3A%20post.topicsCollection.items%20%7D%2C%0A%20%20%20%20%20%20date%3A%20post.date%2C%0A%20%20%20%20%20%20readingTime%3A%20post.readingTime%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D)%3B%0A%0A%20%20return%20transformed%3B%0A%7D%0A%0A(async%20function%20()%20%7B%0A%20%20dotenv.config()%3B%0A%0A%20%20try%20%7B%0A%20%20%20%20const%20posts%20%3D%20await%20getAllBlogPosts()%3B%0A%20%20%20%20const%20transformed%20%3D%20transformPostsToSearchObjects(posts)%3B%0A%0A%20%20%20%20%2F%2F%20we%20have%20data%20ready%20for%20Algolia!%0A%20%20%20%20console.log(transformed)%3B%0A%20%20%7D%20catch%20(error)%20%7B%0A%20%20%20%20console.log(error)%3B%0A%20%20%7D%0A%7D)()%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// build-search.js</span><br><span class="token keyword">const</span> dotenv <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"dotenv"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getAllBlogPosts</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// write your code to fetch your data</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">transformPostsToSearchObjects</span><span class="token punctuation">(</span><span class="token parameter">posts</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> transformed <span class="token operator">=</span> posts<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">post</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">objectID</span><span class="token operator">:</span> post<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token punctuation">,</span><br>      <span class="token literal-property property">title</span><span class="token operator">:</span> post<span class="token punctuation">.</span>title<span class="token punctuation">,</span><br>      <span class="token literal-property property">excerpt</span><span class="token operator">:</span> post<span class="token punctuation">.</span>excerpt<span class="token punctuation">,</span><br>      <span class="token literal-property property">slug</span><span class="token operator">:</span> post<span class="token punctuation">.</span>slug<span class="token punctuation">,</span><br>      <span class="token literal-property property">topicsCollection</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">items</span><span class="token operator">:</span> post<span class="token punctuation">.</span>topicsCollection<span class="token punctuation">.</span>items <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">date</span><span class="token operator">:</span> post<span class="token punctuation">.</span>date<span class="token punctuation">,</span><br>      <span class="token literal-property property">readingTime</span><span class="token operator">:</span> post<span class="token punctuation">.</span>readingTime<span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> transformed<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  dotenv<span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> posts <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getAllBlogPosts</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> transformed <span class="token operator">=</span> <span class="token function">transformPostsToSearchObjects</span><span class="token punctuation">(</span>posts<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// we have data ready for Algolia!</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>transformed<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">I also included a little extra data in my search objects, such as <code>readingTime</code>, <code>topics</code> and <code>date</code> to display an already-existing UI component in my search results on the front end (we’ll look at this later). This is the beauty of the flexible schema of the search objects!</p><p class="post__p">Now we have our data records transformed for Algolia, let’s send them to the index!</p><h2 class="post__h2">Import your records programmatically to Algolia</h2><p class="post__p">After the content has been transformed, let’s initialize a new <code>algoliasearch</code> client with the environment variables we added earlier. Then, initialize the index with the name of the index you set up when you onboarded to Algolia, and call the <code>saveObjects</code> function with your transformed data. Make sure to import the <code>algoliasearch</code> dependency! Also, let’s log out the objectIDs from the response to make sure everything has gone smoothly.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZZEFLitqxW"
      aria-describedby="ZZEFLitqxW">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZZEFLitqxW">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="ZZEFLitqxW" itemprop="text" content="%2F%2F%20build-search.js%0Aconst%20dotenv%20%3D%20require(%22dotenv%22)%3B%0Aconst%20algoliasearch%20%3D%20require(%22algoliasearch%2Flite%22)%3B%0A%0Aasync%20function%20getAllBlogPosts()%20%7B%0A%20%20%2F%2F%20write%20your%20code%20to%20fetch%20your%20data%0A%7D%0A%0Afunction%20transformPostsToSearchObjects(posts)%20%7B%0A%20%20%2F%2F%20...%0A%7D%0A%0A(async%20function%20()%20%7B%0A%20%20dotenv.config()%3B%0A%0A%20%20try%20%7B%0A%20%20%20%20const%20posts%20%3D%20await%20getAllBlogPosts()%3B%0A%20%20%20%20const%20transformed%20%3D%20transformPostsToSearchObjects(posts)%3B%0A%0A%20%20%20%20%2F%2F%20initialize%20the%20client%20with%20your%20environment%20variables%0A%20%20%20%20const%20client%20%3D%20algoliasearch(%0A%20%20%20%20%20%20%20process.env.NEXT_PUBLIC_ALGOLIA_APP_ID%2C%0A%20%20%20%20%20%20%20process.env.ALGOLIA_SEARCH_ADMIN_KEY%2C%0A%20%20%20%20%20)%3B%0A%0A%20%20%20%20%20%2F%2F%20initialize%20the%20index%20with%20your%20index%20name%0A%20%20%20%20%20const%20index%20%3D%20client.initIndex(%22my_awesome_content%22)%3B%0A%0A%20%20%20%20%20%2F%2F%20save%20the%20objects!%0A%20%20%20%20%20const%20algoliaResponse%20%3D%20await%20index.saveObjects(transformed)%3B%0A%0A%20%20%20%20%20%2F%2F%20check%20the%20output%20of%20the%20response%20in%20the%20console%0A%20%20%20%20%20console.log(%0A%20%20%20%20%20%20%20%60%F0%9F%8E%89%20Sucessfully%20added%20%24%7BalgoliaResponse.objectIDs.length%7D%20records%20to%20Algolia%20search.%20Object%20IDs%3A%5Cn%24%7BalgoliaResponse.objectIDs.join(%0A%20%20%20%20%20%20%20%20%20%22%5Cn%22%2C%0A%20%20%20%20%20%20%20)%7D%60%2C%0A%20%20%20%20%20)%3B%0A%20%20%7D%20catch%20(error)%20%7B%0A%20%20%20%20console.log(error)%3B%0A%20%20%7D%0A%7D)()%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// build-search.js</span><br><span class="token keyword">const</span> dotenv <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"dotenv"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> algoliasearch <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"algoliasearch/lite"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getAllBlogPosts</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// write your code to fetch your data</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">transformPostsToSearchObjects</span><span class="token punctuation">(</span><span class="token parameter">posts</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// ...</span><br><span class="token punctuation">}</span><br><br><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  dotenv<span class="token punctuation">.</span><span class="token function">config</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> posts <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getAllBlogPosts</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> transformed <span class="token operator">=</span> <span class="token function">transformPostsToSearchObjects</span><span class="token punctuation">(</span>posts<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// initialize the client with your environment variables</span><br>    <span class="token keyword">const</span> client <span class="token operator">=</span> <span class="token function">algoliasearch</span><span class="token punctuation">(</span><br>       process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NEXT_PUBLIC_ALGOLIA_APP_ID</span><span class="token punctuation">,</span><br>       process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">ALGOLIA_SEARCH_ADMIN_KEY</span><span class="token punctuation">,</span><br>     <span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>     <span class="token comment">// initialize the index with your index name</span><br>     <span class="token keyword">const</span> index <span class="token operator">=</span> client<span class="token punctuation">.</span><span class="token function">initIndex</span><span class="token punctuation">(</span><span class="token string">"my_awesome_content"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>     <span class="token comment">// save the objects!</span><br>     <span class="token keyword">const</span> algoliaResponse <span class="token operator">=</span> <span class="token keyword">await</span> index<span class="token punctuation">.</span><span class="token function">saveObjects</span><span class="token punctuation">(</span>transformed<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>     <span class="token comment">// check the output of the response in the console</span><br>     console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><br>       <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">🎉 Sucessfully added </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>algoliaResponse<span class="token punctuation">.</span>objectIDs<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> records to Algolia search. Object IDs:\n</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>algoliaResponse<span class="token punctuation">.</span>objectIDs<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><br>         <span class="token string">"\n"</span><span class="token punctuation">,</span><br>       <span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>     <span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">After the script has executed successfully, head on over to your Algolia dashboard, and you’ll see your index populated with your search objects. 🎉  You can also preview the results of the search algorithm — right there in the UI!</p><img src="https://images.ctfassets.net/56dzm01z6lln/58xJbWftzcteorwdOHtIEq/008b869a258bc85e29a64b2e831b1e34/search_preview.png" alt="A screenshot of the Algolia Indices dashboard, showing a preview of the results as I search for "graphQL tutorial"." height="793" width="1021" /><p class="post__p">Given that you added the <code>postbuild</code> command to your package.json file, you are safe to commit these changes to your project. If your project is live and hosted on a hosting provider like Vercel, check out the build console to confirm the search results are sent to Algolia after your project has been built.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5skRd5HAs20UcjxIsUbgoU/332c7692e7958531edc3e9f66f36cc63/vercel_output.png" alt="A screenshot of the output of build-search.js in the Vercel logs output UI." height="512" width="748" /><p class="post__p">Now our search data records are safe in Algolia, let’s look at how we can use the React InstantSearch UI library to search records in our Next.js application. </p><h2 class="post__h2">Install InstantSearch dependencies</h2><p class="post__p">InstantSearch is Algolia’s front-end library. I always thought it was just a search box — but it’s so much more! It provides a library of pre-built and customizable components to build up a full-page UI on your front end — complete with super-fast filtering. <a href="https://codesandbox.io/embed/github/algolia/doc-code-samples/tree/master/React+InstantSearch/getting-started" target="_blank">Check out this React InstantSearch demo from Algolia</a> on CodeSandbox.</p><p class="post__p">In this tutorial, we’re going to use the React InstantSearch DOM library to build a simple search box that displays search results when a search term is provided. We’re also going to use some of the provided higher-order components from the library to allow us to build some custom UI components. </p><p class="post__p">Here’s a breakdown of the components we’ll be using and customizing.</p><img src="https://images.ctfassets.net/56dzm01z6lln/VsQ9rQ16qv8U83KqXRj10/aa133a1049aad62f4b8ca2bb48bf0592/component_breakdown.png" alt="A colour-coded diagram showing how the Algolia InstantSearch, SearchBox and Hits components work together." height="621" width="951" /><p class="post__p">Let’s get started by installing the dependencies. We’ll need <code>algoliasearch</code> that we installed earlier and <code>react-instantsearch-dom</code>. Run the following command in your terminal at the root of your project.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="zPVIAbPUaw"
      aria-describedby="zPVIAbPUaw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="zPVIAbPUaw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="zPVIAbPUaw" itemprop="text" content="npm%20install%20react-instantsearch-dom%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> react-instantsearch-dom</code></pre>
    </div>
  </div>

  <h2 class="post__h2">Using the default InstantSearch components</h2><p class="post__p">Create a new component file for the InstantSearch code and import the <code>algoliasearch</code> dependency.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="NyBzFIZcMX"
      aria-describedby="NyBzFIZcMX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="NyBzFIZcMX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="NyBzFIZcMX" itemprop="text" content="%2F%2F%20.%2Fcomponents%2FSearch%2Findex.js%20%0A%0A%2F%2F%20%E2%80%9Calgoliasearch%2Flite%E2%80%9D%20is%20the%20search-only%20version%20of%20the%20API%20client%20%E2%80%94%20optimized%20for%20size%20and%20search%0Aimport%20algoliasearch%20from%20%22algoliasearch%2Flite%22%3B%0A%0Aexport%20default%20function%20Search()%20%7B%0A%20%20return%20(%0A%20%20%20%20%2F%2F%20Our%20search%20components%20will%20go%20here!%0A%20%20)%0A%7D%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// ./components/Search/index.js </span><br><br><span class="token comment">// “algoliasearch/lite” is the search-only version of the API client — optimized for size and search</span><br><span class="token keyword">import</span> algoliasearch <span class="token keyword">from</span> <span class="token string">"algoliasearch/lite"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Search</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token comment">// Our search components will go here!</span><br>  <span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">InstantSearch works nicely with server-side rendering so we’re safe to use the new component on Next.js page files out of the box. Import the new component to your existing blog index page.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vHAywYArKx"
      aria-describedby="vHAywYArKx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vHAywYArKx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="vHAywYArKx" itemprop="text" content="%2F%2F%20.%2Fpages%2Fblog%2Findex.js%0A%0Aimport%20ContentfulApi%20from%20%22.%2Flib%2FContentfulApi%22%3B%0Aimport%20PostList%20from%20%22.%2Fcomponents%2FPostList%22%3B%0Aimport%20Search%20from%20%22.%2Fcomponents%2FSearch%22%3B%0A%0Aexport%20default%20function%20BlogIndex(%7B%20posts%20%7D)%20%7B%0A%20%20return%20(%0A%20%20%20%20%3C%3E%0A%20%20%20%20%20%20%20%20%3CSearch%20%2F%3E%0A%20%20%20%20%20%20%20%20%3CPostList%20posts%3D%7Bposts%7D%20%2F%3E%0A%20%20%20%20%3C%2F%3E%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getStaticProps()%20%7B%0A%20%20const%20posts%20%3D%20await%20ContentfulApi.getPostSummaries()%3B%0A%0A%20%20return%20%7B%0A%20%20%20%20props%3A%20%7B%0A%20%20%20%20%20%20posts%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%3B%0A%7D%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// ./pages/blog/index.js</span><br><br><span class="token keyword">import</span> ContentfulApi <span class="token keyword">from</span> <span class="token string">"./lib/ContentfulApi"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> PostList <span class="token keyword">from</span> <span class="token string">"./components/PostList"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> Search <span class="token keyword">from</span> <span class="token string">"./components/Search"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">BlogIndex</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> posts <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>Search <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>PostList posts<span class="token operator">=</span><span class="token punctuation">{</span>posts<span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getStaticProps</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> posts <span class="token operator">=</span> <span class="token keyword">await</span> ContentfulApi<span class="token punctuation">.</span><span class="token function">getPostSummaries</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      posts<span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">In your new search component, initialise a new <code>algoliasearch</code> client with the public environment variables you set up earlier.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="HFiBSyzIFF"
      aria-describedby="HFiBSyzIFF">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="HFiBSyzIFF">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="HFiBSyzIFF" itemprop="text" content="%2F%2F%20.components%2FSearch%2Findex.js%0A%0Aimport%20algoliasearch%20from%20%22algoliasearch%2Flite%22%3B%0A%0Aconst%20searchClient%20%3D%20algoliasearch(%0A%20%20process.env.NEXT_PUBLIC_ALGOLIA_APP_ID%2C%0A%20%20process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY%2C%0A)%3B%0A%0Aexport%20default%20function%20Search()%20%7B%0A%20%20return%20(%0A%20%20%20%2F%2F%20Our%20search%20components%20will%20go%20here!%0A%20%20)%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// .components/Search/index.js</span><br><br><span class="token keyword">import</span> algoliasearch <span class="token keyword">from</span> <span class="token string">"algoliasearch/lite"</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> searchClient <span class="token operator">=</span> <span class="token function">algoliasearch</span><span class="token punctuation">(</span><br>  process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NEXT_PUBLIC_ALGOLIA_APP_ID</span><span class="token punctuation">,</span><br>  process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Search</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>   <span class="token comment">// Our search components will go here!</span><br>  <span class="token punctuation">)</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Import the InstantSearch, SearchBox and Hits UI components and render them in the component as follows. Pass the <code>searchClient</code> and the <code>indexName</code> you set up with Algolia as props into the InstantSearch component.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="HVZaTigEBl"
      aria-describedby="HVZaTigEBl">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="HVZaTigEBl">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="HVZaTigEBl" itemprop="text" content="%2F%2F%20.components%2FSearch%2Findex.js%0A%0Aimport%20algoliasearch%20from%20%22algoliasearch%2Flite%22%3B%0Aimport%20%7B%20InstantSearch%2C%20SearchBox%2C%20Hits%20%7D%20from%20%22react-instantsearch-dom%22%3B%0A%0Aconst%20searchClient%20%3D%20algoliasearch(%0A%20%20process.env.NEXT_PUBLIC_ALGOLIA_APP_ID%2C%0A%20%20process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY%2C%0A)%3B%0A%0Aexport%20default%20function%20Search()%20%7B%0A%20%20return%20(%0A%20%20%20%20%3C%3E%0A%20%20%20%20%20%20%3CInstantSearch%20%0A%20%20%20%20%20%20%20%20searchClient%3D%7BsearchClient%7D%20%0A%20%20%20%20%20%20%20%20indexName%3D%22my_awesome_content%22%3E%0A%20%20%20%20%20%20%20%20%3CSearchBox%20%2F%3E%0A%20%20%20%20%20%20%20%20%3CHits%20%2F%3E%0A%20%20%20%20%20%20%3C%2FInstantSearch%3E%0A%20%20%20%20%3C%2F%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// .components/Search/index.js</span><br><br><span class="token keyword">import</span> algoliasearch <span class="token keyword">from</span> <span class="token string">"algoliasearch/lite"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> InstantSearch<span class="token punctuation">,</span> SearchBox<span class="token punctuation">,</span> Hits <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-instantsearch-dom"</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> searchClient <span class="token operator">=</span> <span class="token function">algoliasearch</span><span class="token punctuation">(</span><br>  process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NEXT_PUBLIC_ALGOLIA_APP_ID</span><span class="token punctuation">,</span><br>  process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Search</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span>InstantSearch <br>        searchClient<span class="token operator">=</span><span class="token punctuation">{</span>searchClient<span class="token punctuation">}</span> <br>        indexName<span class="token operator">=</span><span class="token string">"my_awesome_content"</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>SearchBox <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>Hits <span class="token operator">/</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>InstantSearch<span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">You’ll now see something like this on your blog index page. Type into the search box to watch your InstantSearch results update — instantly!</p><img src="https://images.ctfassets.net/56dzm01z6lln/12Kqto6hZZYiZSMiVQPlbL/923d40d0b32549c05530cfd0a0c06cc5/default_instant_search.png" alt="A screenshot of the default InstantSearch unstyled UI, available after doing the minimal setup." height="447" width="806" /><p class="post__p"></p><p class="post__p">That’s InstantSearch connected to our Algolia index, displaying and updating search results in real-time. Now, let’s look at creating some custom components to give us more control over the UI and CSS, and to only render the search results when there’s a search query present in the input field. </p><h2 class="post__h2">Create your custom components</h2><h3 class="post__h3">CustomSearchBox.js</h3><p class="post__p">Create a new file inside your Search component folder called CustomSearchBox.js. This will be a new custom form that will perform the search. </p><ul><li><p class="post__p">Import the <code>connectSearchBox</code> higher-order component from <code>react-instant-search-dom</code> — this is the function that will connect the custom search box to the InstantSearch client. <a href="https://reactjs.org/docs/higher-order-components.html" target="_blank">Read more about higher-order components in React</a>.</p></li><li><p class="post__p">Build your HTML form using the available <code>refine</code> prop to manage the <code>onChange</code> of the input field. I chose to add a label element alongside the input field for accessibility reasons.</p></li><li><p class="post__p">Export your custom component wrapped with <code>connectSearchBox</code>.</p></li><li><p class="post__p">You’re free to style the form with standard CSS classes, CSS Modules, Styled Components and so on.</p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CwQrMNMwan"
      aria-describedby="CwQrMNMwan">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CwQrMNMwan">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="CwQrMNMwan" itemprop="text" content="%2F%2F%20.components%2FSearch%2FCustomSearchBox.js%0A%0Aimport%20%7B%20connectSearchBox%20%7D%20from%20%22react-instantsearch-dom%22%3B%0A%0Afunction%20SearchBox(%7B%20refine%20%7D)%20%7B%0A%20%20return%20(%0A%20%20%20%20%3Cform%20action%3D%22%22%20role%3D%22search%22%3E%0A%20%20%20%20%20%20%3Clabel%20htmlFor%3D%22algolia_search%22%3ESearch%20articles%3C%2Flabel%3E%0A%20%20%20%20%20%20%3Cinput%0A%20%20%20%20%20%20%20%20id%3D%22algolia_search%22%0A%20%20%20%20%20%20%20%20type%3D%22search%22%0A%20%20%20%20%20%20%20%20placeholder%3D%22javascript%20tutorial%22%0A%20%20%20%20%20%20%20%20onChange%3D%7B(e)%20%3D%3E%20refine(e.currentTarget.value)%7D%0A%20%20%20%20%20%20%2F%3E%0A%20%20%20%20%3C%2Fform%3E%0A%20%20)%3B%0A%7D%0A%0Aexport%20default%20connectSearchBox(SearchBox)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// .components/Search/CustomSearchBox.js</span><br><br><span class="token keyword">import</span> <span class="token punctuation">{</span> connectSearchBox <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-instantsearch-dom"</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">SearchBox</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> refine <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>form action<span class="token operator">=</span><span class="token string">""</span> role<span class="token operator">=</span><span class="token string">"search"</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span>label htmlFor<span class="token operator">=</span><span class="token string">"algolia_search"</span><span class="token operator">></span>Search articles<span class="token operator">&lt;</span><span class="token operator">/</span>label<span class="token operator">></span><br>      <span class="token operator">&lt;</span>input<br>        id<span class="token operator">=</span><span class="token string">"algolia_search"</span><br>        type<span class="token operator">=</span><span class="token string">"search"</span><br>        placeholder<span class="token operator">=</span><span class="token string">"javascript tutorial"</span><br>        onChange<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">refine</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>currentTarget<span class="token punctuation">.</span>value<span class="token punctuation">)</span><span class="token punctuation">}</span><br>      <span class="token operator">/</span><span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>form<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">connectSearchBox</span><span class="token punctuation">(</span>SearchBox<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Import and render the CustomSearchBox component as a child of the InstantSearch component, like so.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="AAnfzVyCwA"
      aria-describedby="AAnfzVyCwA">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="AAnfzVyCwA">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="AAnfzVyCwA" itemprop="text" content="%2F%2F%20.components%2FSearch%2Findex.js%0A%0Aimport%20algoliasearch%20from%20%22algoliasearch%2Flite%22%3B%0Aimport%20%7B%20InstantSearch%2C%20Hits%20%7D%20from%20%22react-instantsearch-dom%22%3B%0Aimport%20CustomSearchBox%20from%20%22.%2FCustomSearchBox%22%3B%0A%0Aconst%20searchClient%20%3D%20algoliasearch(...)%3B%0A%0Aexport%20default%20function%20Search()%20%7B%0A%20%20return%20(%0A%20%20%20%20%3C%3E%0A%20%20%20%20%20%20%3CInstantSearch%20searchClient%3D%7BsearchClient%7D%20indexName%3D%22p4nth3rblog%22%3E%0A%20%20%20%20%20%20%20%20%3CCustomSearchBox%20%2F%3E%0A%20%20%20%20%20%20%20%20%3CHits%20%2F%3E%0A%20%20%20%20%20%20%3C%2FInstantSearch%3E%0A%20%20%20%20%3C%2F%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// .components/Search/index.js</span><br><br><span class="token keyword">import</span> algoliasearch <span class="token keyword">from</span> <span class="token string">"algoliasearch/lite"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> InstantSearch<span class="token punctuation">,</span> Hits <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-instantsearch-dom"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> CustomSearchBox <span class="token keyword">from</span> <span class="token string">"./CustomSearchBox"</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> searchClient <span class="token operator">=</span> <span class="token function">algoliasearch</span><span class="token punctuation">(</span><span class="token operator">...</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Search</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span>InstantSearch searchClient<span class="token operator">=</span><span class="token punctuation">{</span>searchClient<span class="token punctuation">}</span> indexName<span class="token operator">=</span><span class="token string">"p4nth3rblog"</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>CustomSearchBox <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>Hits <span class="token operator">/</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>InstantSearch<span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, onto the custom hits component.</p><h3 class="post__h3">CustomHits.js</h3><p class="post__p">Create a new file inside your Search component folder called CustomHits.js. This will be the component that processes the logic to only show our search results when a search query is present in the input field.</p><ul><li><p class="post__p">Import the <code>connectStateResults</code> higher-order component from <code>react-instant-search-dom</code> — this is the function that will connect the custom hits to the InstantSearch client.</p></li><li><p class="post__p">Capture <code>searchState</code> and <code>searchResults</code> as props in the component function declaration.</p></li><li><p class="post__p">Build your HTML output using the available <code>searchResults</code> prop to manage the <code>onChange</code> of the input field.</p></li><li><p class="post__p">Export your custom component wrapped with <code>connectStateResults</code>.</p></li><li><p class="post__p">You’re free to style the form with standard CSS classes, CSS module styles, Styled Components and so on.</p></li><li><p class="post__p">You’re free to render another custom component to display the <code>searchResults.hits</code>. I used the same component that displays my recent blog posts on my home page!</p></li><li><p class="post__p"><b class="post__p--bold">Optional</b>: use <code>searchState.query</code> to process some logic to only render results to the DOM if the length of the search query is greater than or equal to three characters in length. </p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YLuqGwdzMC"
      aria-describedby="YLuqGwdzMC">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YLuqGwdzMC">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="YLuqGwdzMC" itemprop="text" content="%2F%2F%20.%2Fcomponents%2FSearch%2FCustomHits.js%0Aimport%20%7B%20connectStateResults%20%7D%20from%20%22react-instantsearch-dom%22%3B%0A%0Afunction%20Hits(%7B%20searchState%2C%20searchResults%20%7D)%20%7B%0A%20%20const%20validQuery%20%3D%20searchState.query%3F.length%20%3E%3D%203%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3C%3E%0A%20%20%20%20%20%20%7BsearchResults%3F.hits.length%20%3D%3D%3D%200%20%26%26%20validQuery%20%26%26%20(%0A%20%20%20%20%20%20%20%20%3Cp%3EAw%20snap!%20No%20search%20results%20were%20found.%3C%2Fp%3E%0A%20%20%20%20%20%20)%7D%0A%20%20%20%20%20%20%7BsearchResults%3F.hits.length%20%3E%200%20%26%26%20validQuery%20%26%26%20(%0A%20%20%20%20%20%20%20%20%3Col%3E%0A%20%20%20%20%20%20%20%20%20%20%7BsearchResults.hits.map((hit)%20%3D%3E%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cli%20key%3D%7Bhit.objectID%7D%3E%7Bhit.title%7D%3C%2Fli%3E%0A%20%20%20%20%20%20%20%20%20%20))%7D%0A%20%20%20%20%20%20%20%20%3C%2Fol%3E%0A%20%20%20%20%20%20)%7D%0A%20%20%20%20%3C%2F%3E%0A%20%20)%3B%0A%7D%0A%0Aexport%20default%20connectStateResults(Hits)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// ./components/Search/CustomHits.js</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> connectStateResults <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-instantsearch-dom"</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">Hits</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> searchState<span class="token punctuation">,</span> searchResults <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> validQuery <span class="token operator">=</span> searchState<span class="token punctuation">.</span>query<span class="token operator">?.</span>length <span class="token operator">>=</span> <span class="token number">3</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span><span class="token operator">></span><br>      <span class="token punctuation">{</span>searchResults<span class="token operator">?.</span>hits<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span> <span class="token operator">&amp;&amp;</span> validQuery <span class="token operator">&amp;&amp;</span> <span class="token punctuation">(</span><br>        <span class="token operator">&lt;</span>p<span class="token operator">></span>Aw snap<span class="token operator">!</span> No search results were found<span class="token punctuation">.</span><span class="token operator">&lt;</span><span class="token operator">/</span>p<span class="token operator">></span><br>      <span class="token punctuation">)</span><span class="token punctuation">}</span><br>      <span class="token punctuation">{</span>searchResults<span class="token operator">?.</span>hits<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span> <span class="token operator">&amp;&amp;</span> validQuery <span class="token operator">&amp;&amp;</span> <span class="token punctuation">(</span><br>        <span class="token operator">&lt;</span>ol<span class="token operator">></span><br>          <span class="token punctuation">{</span>searchResults<span class="token punctuation">.</span>hits<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">hit</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><br>            <span class="token operator">&lt;</span>li key<span class="token operator">=</span><span class="token punctuation">{</span>hit<span class="token punctuation">.</span>objectID<span class="token punctuation">}</span><span class="token operator">></span><span class="token punctuation">{</span>hit<span class="token punctuation">.</span>title<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>li<span class="token operator">></span><br>          <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><br>        <span class="token operator">&lt;</span><span class="token operator">/</span>ol<span class="token operator">></span><br>      <span class="token punctuation">)</span><span class="token punctuation">}</span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token function">connectStateResults</span><span class="token punctuation">(</span>Hits<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Import and render the CustomHits component as a child of the InstantSearch component.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="EGEQwLvXoU"
      aria-describedby="EGEQwLvXoU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="EGEQwLvXoU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="EGEQwLvXoU" itemprop="text" content="%2F%2F%20.components%2FSearch%2Findex.js%0A%0Aimport%20algoliasearch%20from%20%22algoliasearch%2Flite%22%3B%0Aimport%20%7B%20InstantSearch%20%7D%20from%20%22react-instantsearch-dom%22%3B%0Aimport%20CustomSearchBox%20from%20%22.%2FCustomSearchBox%22%3B%0Aimport%20CustomHits%20from%20%22.%2FCustomHits%22%3B%0A%0Aconst%20searchClient%20%3D%20algoliasearch(%0A%20%20process.env.NEXT_PUBLIC_ALGOLIA_APP_ID%2C%0A%20%20process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY%2C%0A)%3B%0A%0Aexport%20default%20function%20Search()%20%7B%0A%20%20return%20(%0A%20%20%20%20%3C%3E%0A%20%20%20%20%20%20%3CInstantSearch%20searchClient%3D%7BsearchClient%7D%20indexName%3D%22p4nth3rblog%22%3E%0A%20%20%20%20%20%20%20%20%3CCustomSearchBox%20%2F%3E%0A%20%20%20%20%20%20%20%20%3CCustomHits%20%2F%3E%0A%20%20%20%20%20%20%3C%2FInstantSearch%3E%0A%20%20%20%20%3C%2F%3E%0A%20%20)%3B%0A%7D%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// .components/Search/index.js</span><br><br><span class="token keyword">import</span> algoliasearch <span class="token keyword">from</span> <span class="token string">"algoliasearch/lite"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> InstantSearch <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react-instantsearch-dom"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> CustomSearchBox <span class="token keyword">from</span> <span class="token string">"./CustomSearchBox"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> CustomHits <span class="token keyword">from</span> <span class="token string">"./CustomHits"</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> searchClient <span class="token operator">=</span> <span class="token function">algoliasearch</span><span class="token punctuation">(</span><br>  process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NEXT_PUBLIC_ALGOLIA_APP_ID</span><span class="token punctuation">,</span><br>  process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY</span><span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Search</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span>InstantSearch searchClient<span class="token operator">=</span><span class="token punctuation">{</span>searchClient<span class="token punctuation">}</span> indexName<span class="token operator">=</span><span class="token string">"p4nth3rblog"</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>CustomSearchBox <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token operator">&lt;</span>CustomHits <span class="token operator">/</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>InstantSearch<span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And there you have it! Now you’ve got InstantSearch hooked up with your custom components, you’re now free to style them up to your heart’s content!</p><p class="post__p"><a href="https://github.com/whitep4nth3r/p4nth3rblog/commit/f852d14efb0bc33fd0dc4e44dcdc79855bed4d95" target="_blank">Click here to see the full code example</a>, complete with styles using CSS Modules.</p><p class="post__p">Is there something you’d like to learn more about to get the most out of Contentful? Come and let us know in the <a href="https://www.contentful.com/slack" target="_blank">Community Slack</a>. We love meeting new developers!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to filter entries by linked references in GraphQL</title>
          <description>Check out this quick guide that shows you how to get the data you need using the linkedFrom field in your query.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/06/15/filter-entries-by-linked-references-in-graphql-api/</link>
          <guid>https://www.contentful.com/blog/2021/06/15/filter-entries-by-linked-references-in-graphql-api/</guid>
          <pubDate>Mon, 14 Jun 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>GraphQL</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Have you ever been frustrated by not being able to filter a collection by a linked entry value using the GraphQL API? For example, do you want to filter your blog posts by linked topics such as “javascript” or “tutorial”? Check out this quick guide that shows you how to get the data you need using the linkedFrom field in your query — it’s pretty nice!</p><p class="post__p">Let’s say you’re making great progress on building your new blog site. You’re using Contentful’s GraphQL API and it’s a good time. You’ve written some great blog posts — and now you want a way to categorize them in your front-end application. For example, you want to show posts tagged with “JavaScript” on a single URL. Nice.</p><p class="post__p">You create a new “Topic” content type, which contains text fields for <code>name</code> and <code>slug</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5Y4C93NS1LMB1Op9micjec/9f1eaa8f80f6a1d4b2d7c06ab05710bf/topic_content_type.png" alt="Screenshot of a Topic content type in Contentful, showing name and slug fields." height="298" width="661" /><p class="post__p">You update your blog post content model with a new field called <code>topics</code>, which is an array of linked references (remember type: Array&lt;Link&gt; for later).</p><img src="https://images.ctfassets.net/56dzm01z6lln/2svPh7yU6XqalpUQpMH3vo/078295de10d7fce90e1c5b037ccef064/topics_field_on_blog_post.png" alt="A screenshot of a Blog Post content type in Contentful showing a Topic field which is an array of linked references." height="321" width="658" /><p class="post__p">You add the relevant topics as links to each blog post entry. Beautiful.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3lMA0AJJMdP4K79wMaEpy3/2b906097222416d62fff37ea4867a50f/topics_on_blog_post_entry.png" alt="A screenshot of the Topics field on a Blog Post entry, showing the linked topics Jamstack, Tutorials, Contentful and JavaScript." height="587" width="661" /><p class="post__p">Here’s how the content model might look, visualized by <a href="https://contentmodel.io/" target="_blank">contentmodel.io</a>. The blog post has a field <code>topics</code>, which is a reference to many (n) “Topic” content types, which have fields <code>name</code> and <code>slug</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6cG0lUxmMUhYy8ob3NUCml/b5bdc8a3fdaeebeb9f4b0da8fd38c728/content_model.png" alt="Screenshot of Contentful content model as visualised on contentmodel.io." height="1054" width="1453" /><p class="post__p">Here’s how you might display collections of blog posts per topic in your web application.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4BgQTsZag16AoSD4nsIz1e/5a89f006202b376aa38e3b71e1019e9f/javascript_topics.png" alt="A screenshot of a JavaScript topic page on my blog site." height="894" width="780" /><p class="post__p">Now it’s time to construct the GraphQL query. Let’s search the docs to see how we can <b class="post__p--bold">query the blog post collection and filter by linked topic</b>!</p><img src="https://images.ctfassets.net/56dzm01z6lln/3i20kCUbRIDyVdX3TzCKT4/3a4dafd5b91f17d03353a21ad5dee624/collection_filters_docs.png" alt="Screenshot of collection filter Contentful docs." height="411" width="1084" /><p class="post__p">But what’s this? <a href="https://www.contentful.com/developers/docs/references/graphql/#/reference/collection-filters/limitations" target="_blank">The documentation</a> states that it is <b class="post__p--italic">not possible to filter on fields of Type Array&lt;Link&gt;</b>! <b class="post__p--bold">So, what do we do?</b></p><img src="https://images.ctfassets.net/56dzm01z6lln/1D6mN82Msfh0CVESUqxmvD/81cbe02b90de8615ee7c7408173e0675/filter_limitations.png" alt="Screenshot of Contentful GraphQL filter limitations." height="105" width="1026" /><h3 class="post__h3">There’s no need to rage quit! </h3><p class="post__p">This type of query is made possible with the <b class="post__p--bold">linkedFrom</b> field! Let’s take a look at how it works.</p><h2 class="post__h2">Install the GraphQL Playground App</h2><p class="post__p">Let’s investigate how we can build this query in the Contentful GraphQL Playground App within the Contentful web app. Install the app by navigating to Apps and follow the instructions for installation. Authorize the app to access your Contentful account and provide it with a Content Preview API key. Click save.</p><img src="https://images.ctfassets.net/56dzm01z6lln/13SVxza8Pf8TGtYrjwEGuf/3ed4226674c1a1cacdcfe20bfe3ca4b3/install_gql_playground.png" alt="Screenshot showing how to install GraphQL playground app in Contentful." height="622" width="790" /><p class="post__p">After installing the GraphQL Playground App, navigate to it via Apps in the navigation.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6Yufmaty8tLfbUwdDcIHuI/f9de10b25706c3d6db7415c2e8b7bf83/app_link_nav.png" alt="Screenshot of Contentful app navigation showing the GraphQL playground link." height="196" width="280" /><p class="post__p">What I love about GraphQL is that it’s self-documenting. Tools such as GraphQL Playground allow you to explore what data and types of queries are available on your content model. You can read the provided in-app documentation or use nifty auto-completion (we’ll do that later in the post). All this functionality is made possible by GraphQL Introspection Queries. <a href="https://www.youtube.com/watch?v=udou5eV9QFw" target="_blank">Check out this video from Shy on Introspection Queries</a> to learn more. It’s definitely worth a watch — I learned a lot!</p><h2 class="post__h2">How to build a linkedFrom query</h2><p class="post__p">I like to think of a linkedFrom query as “reversing” the query you actually want to make. Instead of querying blog posts and filtering the results by linked topic, we’re going to query a single topic and fetch the blog posts that contain that topic as a linked reference.</p><p class="post__p">Let’s start by querying our topicCollection. If you’ve set up the topic content type as described above with the fields <code>name</code> and <code>slug</code>, paste the query below into the GraphQL Playground App.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="iydMSRHZfW"
      aria-describedby="iydMSRHZfW">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="iydMSRHZfW">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="iydMSRHZfW" itemprop="text" content="query%20%7B%0A%20%20topicCollection%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20slug%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token keyword">query</span> <span class="token punctuation">{</span><br>  <span class="token object">topicCollection</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token property">name</span><br>      <span class="token property">slug</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">The response will include an array of items containing the name and slug properties as requested.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2Mhzr5EulNbaFoC7Du6v8s/1ed080f8c1403560b8fd99b1c8e13b0c/topic_collection_query_1.png" alt="Screenshot of the topic collection query in the GraphQL playground app." height="507" width="1120" /><p class="post__p">Let’s update the query to fetch a single topic by slug — <code>javascript</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="tCUsylcalV"
      aria-describedby="tCUsylcalV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="tCUsylcalV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="tCUsylcalV" itemprop="text" content="query%20%7B%0A%20%20topicCollection(where%3A%20%7B%20slug%3A%20%22javascript%22%20%7D%2C%20limit%3A%201)%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%20%20slug%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token keyword">query</span> <span class="token punctuation">{</span><br>  <span class="token property-query">topicCollection</span><span class="token punctuation">(</span><span class="token attr-name">where</span><span class="token punctuation">:</span> <span class="token punctuation">{</span> <span class="token attr-name">slug</span><span class="token punctuation">:</span> <span class="token string">"javascript"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token attr-name">limit</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token property">name</span><br>      <span class="token property">slug</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Here’s the updated response.</p><img src="https://images.ctfassets.net/56dzm01z6lln/62arhfjYeLFhWOmsvkR5qJ/f4a034869eb0e48ae61709bf2edb5e9e/topic_collection_query_2.png" alt="A screenshot of a topicCollection query by slug javascript in the GraphQL playground." height="410" width="1278" /><p class="post__p">What we really want to return in our query is a list of blog posts that contain the referenced topic <code>javascript</code>. Here’s where we can use <b class="post__p--bold">linkedFrom</b> — and thanks to Introspection Queries — the GraphQL Playground gives us some hints!</p><img src="https://images.ctfassets.net/56dzm01z6lln/7qWZLW7AHcFQxwiwBv7twH/d14dc53660e471ecc38cb4483b090175/linked_from_popup.png" alt="Screenshot of linkedfrom popup helper in the GraphQL playground." height="200" width="585" /><p class="post__p">Let’s request some fields from the <code>blogPostCollection</code> items that reference the <code>javascript</code> topic using the query below:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="cRygCDmLkh"
      aria-describedby="cRygCDmLkh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="cRygCDmLkh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="cRygCDmLkh" itemprop="text" content="query%20%7B%0A%20%20topicCollection(where%3A%20%7B%20slug%3A%20%22javascript%22%20%7D%2C%20limit%3A%201)%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%09linkedFrom%20%7B%0A%20%20%20%20%20%20%20%20blogPostCollection%20%7B%0A%20%20%20%20%20%20%20%20%20%20total%0A%20%20%20%20%20%20%20%20%20%20items%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20slug%0A%20%20%20%20%20%20%20%20%20%20%20%20date%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token keyword">query</span> <span class="token punctuation">{</span><br>  <span class="token property-query">topicCollection</span><span class="token punctuation">(</span><span class="token attr-name">where</span><span class="token punctuation">:</span> <span class="token punctuation">{</span> <span class="token attr-name">slug</span><span class="token punctuation">:</span> <span class="token string">"javascript"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token attr-name">limit</span><span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>    	<span class="token object">linkedFrom</span> <span class="token punctuation">{</span><br>        <span class="token object">blogPostCollection</span> <span class="token punctuation">{</span><br>          <span class="token property">total</span><br>          <span class="token object">items</span> <span class="token punctuation">{</span><br>            <span class="token property">title</span><br>            <span class="token property">slug</span><br>            <span class="token property">date</span><br>          <span class="token punctuation">}</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s the response:</p><img src="https://images.ctfassets.net/56dzm01z6lln/3yQPtk4IcjOwlNKtCbZLEx/3179cb3833b7943203621d51068c477b/topic_collection_query_3.png" alt="A screenshot of a linkedFrom query in the GraphQL playground, fetching blog posts that have linked tags of JavaScript." height="705" width="1279" /><p class="post__p">And there you have it! </p><p class="post__p">Using the linkedFrom field is an effective way to request all entries that reference entries of Type Array&lt;Link&gt;.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to use GitHub actions and Contentful webhooks to show your latest blog posts on your GitHub README</title>
          <description>Want to show your latest blog posts on your GitHub README? Here's how I do it using the power of GitHub actions and webhooks in Contentful.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-github-actions-contentful-webhooks-to-show-latest-blog-posts-readme/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-github-actions-contentful-webhooks-to-show-latest-blog-posts-readme/</guid>
          <pubDate>Thu, 27 May 2021 00:00:00 GMT</pubDate>
          <category>Tutorials</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you push code to GitHub and share it with the public, chances are that someone will stumble across your GitHub profile. Since the launch of the <a href="https://docs.github.com/en/github/setting-up-and-managing-your-github-profile/customizing-your-profile/managing-your-profile-readme" target="_blank">GitHub profile README</a> in 2020, more and more people are finding creative ways to showcase their personal brand through this developer-centric medium. </p><p class="post__p">My own GitHub profile README is constantly evolving and I always like to find new ways to showcase what I&#39;m doing on other channels. In this post, I&#39;ll show you how I generate a list of recent posts from whitep4nth3r.com on my GitHub profile README with <a href="https://github.com/features/actions" target="_blank">GitHub Actions</a>, and how I keep it up to date each time I publish a new post with a <a href="https://www.contentful.com/developers/docs/concepts/webhooks/" target="_blank">webhook in Contentful</a>.</p><p class="post__p">If you want to jump straight to the code, <a href="https://github.com/whitep4nth3r/whitep4nth3r/blob/main/.github/workflows/build-readme.yml" target="_blank">view my GitHub actions workflow on GitHub</a>.</p><p class="post__p"><b class="post__p--italic">This post assumes you&#39;ve already added a GitHub profile README to your GitHub account — </b><b class="post__p--italic"><b class="post__p--bold">and your blog has a public RSS feed</b></b><b class="post__p--italic">. </b></p><h2 class="post__h2">Setting up the GitHub Action</h2><p class="post__p">If you&#39;re new to GitHub Actions, the good news is that the GitHub UI offers you a handy way to set up your first workflow right from your browser.</p><p class="post__p">Navigate to your README profile repository and click on the &#39;Actions&#39; button.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2sIb9Oo9OktLbLK42fhoYk/efa5aa8bfd8256c7fb51451b93efcc5f/github_actions_button.png" alt="A screenshot of the GitHub repository UI highlighting the position of the Actions button." height="126" width="656" /><p class="post__p">GitHub will offer you some template options for workflows, but we&#39;re going to start afresh.</p><p class="post__p">If this is your first GitHub action, click on &#39;set up a workflow yourself&#39;. </p><img src="https://images.ctfassets.net/56dzm01z6lln/6io8eRbkFqy81JV17Ol3SW/abad630ababce62ab95e548c5b805040/set_up_workflow.png" alt="A screenshot of the GitHub UI showing a link to set up a workflow yourself after clicking on the Actions tab in the UI." height="536" width="1062" /><p class="post__p">This will prepare a commit to add the necessary directories (.github/workflows) and the action <code>.yml</code> file to your README repository.</p><img src="https://images.ctfassets.net/56dzm01z6lln/J8lBYfX0UGmTlWrECj93d/b0a915f8e388e227e689123eca83415a/name_your_file.png" alt="A screenshot of the GitHub action UI, showing the directories and file added for the action." height="62" width="651" /><p class="post__p">Name your <code>.yml</code> file anything you like. I chose <b class="post__p--bold">build-readme</b>. The next step is to replace the boilerplate code you see in the editor below with the following:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="MuFExwczXC"
      aria-describedby="MuFExwczXC">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="MuFExwczXC">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="yaml">
      <meta data-code-id="MuFExwczXC" itemprop="text" content="name%3A%20Latest%20blog%20post%20workflow%0Aon%3A%0A%20%20repository_dispatch%3A%0A%20%20%20%20types%3A%20%5Bcontentful.deploy%5D%0Ajobs%3A%0A%20%20update-readme-with-blog%3A%0A%20%20%20%20name%3A%20Update%20this%20repo's%20README%20with%20latest%20blog%20posts%0A%20%20%20%20runs-on%3A%20ubuntu-latest%0A%20%20%20%20steps%3A%0A%20%20%20%20%20%20-%20uses%3A%20actions%2Fcheckout%40v2%0A%20%20%20%20%20%20-%20uses%3A%20jakejarvis%2Fwait-action%40master%0A%20%20%20%20%20%20%20%20with%3A%0A%20%20%20%20%20%20%20%20%20%20time%3A%20%225m%22%0A%20%20%20%20%20%20-%20uses%3A%20gautamkrishnar%2Fblog-post-workflow%40master%0A%20%20%20%20%20%20%20%20with%3A%0A%20%20%20%20%20%20%20%20%20%20feed_list%3A%20%22https%3A%2F%2Fwhitep4nth3r.com%2Ffeed.xml%22%0A%20%20%20%20%20%20%20%20%20%20commit_message%3A%20%22Updated%20README%20with%20latest%20blog%20posts%22">
      <pre class="language-yaml"><code class="language-yaml"><span class="token key atrule">name</span><span class="token punctuation">:</span> Latest blog post workflow<br><span class="token key atrule">on</span><span class="token punctuation">:</span><br>  <span class="token key atrule">repository_dispatch</span><span class="token punctuation">:</span><br>    <span class="token key atrule">types</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>contentful.deploy<span class="token punctuation">]</span><br><span class="token key atrule">jobs</span><span class="token punctuation">:</span><br>  <span class="token key atrule">update-readme-with-blog</span><span class="token punctuation">:</span><br>    <span class="token key atrule">name</span><span class="token punctuation">:</span> Update this repo's README with latest blog posts<br>    <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest<br>    <span class="token key atrule">steps</span><span class="token punctuation">:</span><br>      <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v2<br>      <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> jakejarvis/wait<span class="token punctuation">-</span>action@master<br>        <span class="token key atrule">with</span><span class="token punctuation">:</span><br>          <span class="token key atrule">time</span><span class="token punctuation">:</span> <span class="token string">"5m"</span><br>      <span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> gautamkrishnar/blog<span class="token punctuation">-</span>post<span class="token punctuation">-</span>workflow@master<br>        <span class="token key atrule">with</span><span class="token punctuation">:</span><br>          <span class="token key atrule">feed_list</span><span class="token punctuation">:</span> <span class="token string">"https://whitep4nth3r.com/feed.xml"</span><br>          <span class="token key atrule">commit_message</span><span class="token punctuation">:</span> <span class="token string">"Updated README with latest blog posts"</span></code></pre>
    </div>
  </div>

  <p class="post__p">Let&#39;s take a look at what&#39;s happening here.</p><ul><li><p class="post__p"><code>name:</code> this is the name of the action which will appear in the GitHub Actions UI</p></li><li><p class="post__p"><code>on:</code> this is the parameter that specifies the list of conditions under which the action will be triggered</p></li><li><p class="post__p"><code>repository_dispatch:</code> this is a way to send named events to your GitHub repository with a data payload — we&#39;ll configure this in the Contentful webhook</p></li><li><p class="post__p"><code>types:</code> this array contains the names of the webhooks that the GitHub action will listen for (you can add more than one!)</p></li><li><p class="post__p"><code>jobs:</code> this contains a list of jobs that the action will perform</p></li><li><p class="post__p"><code>update-readme-with-blog:</code> this is the action we&#39;re building</p></li><li><p class="post__p"><code>runs-on:</code> this specifies which Docker image is needed for the action to run in the container on GitHub</p></li><li><p class="post__p"><code>steps:</code> a list of instructions to perform the job</p></li><li><p class="post__p"><code>uses:</code> the action code repository required for the following steps</p></li><li><p class="post__p"><code>with:</code> the parameters required for the code pulled in via the <code>uses:</code> parameter above</p></li></ul><h3 class="post__h3">Waiting for 5 minutes?</h3><p class="post__p">I host my blog on Netlify, which is pre-rendered into static HTML via a webhook in Contentful — each time I publish a new post. To ensure that the blog and RSS feed finish building before the blog post list is generated, I am using <a href="https://github.com/jakejarvis/wait-action" target="_blank">jakejarvis/wait-action</a> to wait for an arbitrary length of five minutes. This is a completely optional step for you. It might seem a bit hacky — but if it works, it works!</p><h3 class="post__h3">Building the feed list</h3><p class="post__p">The action code repository used to generate the recent blog post feed is by <a href="https://github.com/gautamkrishnar" target="_blank">gautamkrishnar</a> (<code>uses: gautamkrishnar/blog-post-workflow@master</code>). If you&#39;re curious, you can <a href="https://github.com/gautamkrishnar/blog-post-workflow" target="_blank">view the code and full setup instructions here</a>!</p><p class="post__p">Make sure to replace the <code>feed_list</code> parameter with the full URL to your own RSS feed. </p><p class="post__p">Commit your new <code>.yml</code> file in the UI.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7oFsBiJxe1fWaeM8d5nBoE/7c745e09a62a0db9672e592da1f8e852/commit_yml.png" alt="A screenshot showing the commit name and message for the new .yml file in the GitHub UI." height="549" width="557" /><p class="post__p">Next, add the following comment lines into your README.md file. This is what the <b class="post__p--bold">blog-post-workflow</b> action will look for in your README in order to insert the list generated from the RSS feed.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="HXEdJntLMC"
      aria-describedby="HXEdJntLMC">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="HXEdJntLMC">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="HXEdJntLMC" itemprop="text" content="%3C!--%20BLOG-POST-LIST%3ASTART%20--%3E%0A%3C!--%20BLOG-POST-LIST%3AEND%20--%3E">
      <pre class="language-markup"><code class="language-markup"><span class="token comment">&lt;!-- BLOG-POST-LIST:START --></span><br><span class="token comment">&lt;!-- BLOG-POST-LIST:END --></span></code></pre>
    </div>
  </div>

  <p class="post__p">Commit this change to the repository. The GitHub action is ready to go! </p><p class="post__p">Now, let&#39;s take a look at how we trigger the webhook in Contentful each time we publish a new blog post.</p><h2 class="post__h2">Creating the Contentful webhook</h2><p class="post__p">In your Contentful space, navigate to <b class="post__p--bold">Settings &gt; Webhooks</b> and click on <b class="post__p--bold">Add Webhook</b>.</p><p class="post__p">Input the following settings:</p><ul><li><p class="post__p">Choose a name for the webhook (I chose <b class="post__p--bold">GH Action</b>)</p></li><li><p class="post__p">Set the URL to <b class="post__p--bold">POST</b> and enter the following URL, replacing <code>{your_user_name}/{repository_name} with the correct values:</code></p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YYHWekiuXx"
      aria-describedby="YYHWekiuXx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YYHWekiuXx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="YYHWekiuXx" itemprop="text" content="https%3A%2F%2Fapi.github.com%2Frepos%2F%7Byour_user_name%7D%2F%7Brepository_name%7D%2Fdispatches">
      <pre class="language-markdown"><code class="language-markdown">https://api.github.com/repos/{your_user_name}/{repository_name}/dispatches</code></pre>
    </div>
  </div>

  <ul><li><p class="post__p">Choose when you&#39;d like to trigger the webhook and GitHub action. I selected to trigger the webhook each time I <b class="post__p--bold">publish, unpublish and delete</b> an entry in Contentful.</p></li></ul><img src="https://images.ctfassets.net/56dzm01z6lln/4QZdl5OfAc45rQREE9f39U/d397a35551dccea464230ff1aba60a97/webhook_1.png" alt="A screenshot of the new webhook settings showing the name, POST URL, and triggers." height="872" width="915" /><ul><li><p class="post__p">Add the following <b class="post__p--bold">Custom Headers:</b></p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="scwdbdvkAw"
      aria-describedby="scwdbdvkAw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="scwdbdvkAw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="scwdbdvkAw" itemprop="text" content="Accept%3A%20application%2Fvnd.github.v3%2Bjson%0AUser-Agent%3A%20Contentful%20Webhook">
      <pre class="language-markup"><code class="language-markup">Accept: application/vnd.github.v3+json<br>User-Agent: Contentful Webhook</code></pre>
    </div>
  </div>

  <ul><li><p class="post__p">Generate a <a href="https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token" target="_blank">GitHub personal access token</a> and add the following <b class="post__p--bold">Secret Header</b>:</p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="PCuLBlTZQX"
      aria-describedby="PCuLBlTZQX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="PCuLBlTZQX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="PCuLBlTZQX" itemprop="text" content="Authorization%3A%20Bearer%20%7Byour_github_personal_access_token%7D">
      <pre class="language-markup"><code class="language-markup">Authorization: Bearer {your_github_personal_access_token}</code></pre>
    </div>
  </div>

  <img src="https://images.ctfassets.net/56dzm01z6lln/DydZVvW011Rw8OGvGNiXv/2591560086ff9884e7183514765b810a/webhook_2.png" alt="A screenshot showing the custom and secret headers for the webhook settings." height="523" width="914" /><ul><li><p class="post__p">Select the &#39;<b class="post__p--bold">Customize the webhook payload&#39;</b> option below and enter the following into the code editor:</p></li></ul>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="UjcYplULzz"
      aria-describedby="UjcYplULzz">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="UjcYplULzz">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="UjcYplULzz" itemprop="text" content="%7B%0A%20%20%22event_type%22%3A%20%22contentful.deploy%22%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"event_type"</span><span class="token operator">:</span> <span class="token string">"contentful.deploy"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <img src="https://images.ctfassets.net/56dzm01z6lln/7cdFprMidF2p0gPqpTHlYP/399feb2ff816de746ee2323632a0aaae/webhook_3.png" alt="A screenshot showing the custom payload to send in the webhook." height="553" width="915" /><p class="post__p">Note how the <code>&quot;event_type&quot;</code> is named the same as the <code>repository_dispatch &gt; types</code> value in the build-readme.yml file.</p><p class="post__p">Click save!</p><p class="post__p"><b class="post__p--italic">Huge thanks to </b><a href="https://nharox.com/blog/trigger-github-actions-with-contentful-webhook/" target="_blank"><b class="post__p--italic">nharox</b></a><b class="post__p--italic"> for writing about triggering a GitHub action via Contentful webhooks  before I did!</b></p><h2 class="post__h2">Test it out!</h2><p class="post__p">Publish, unpublish or delete an entry in your Contentful space and watch your GitHub Action trigger automagically via the webhook. To check if your webhook fired, click on the <b class="post__p--bold">Activity log</b> tab via the webhook settings.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3fLujTWdsm7jyNybIw827I/348015852449d904d5281f79a5b97a78/webhook_log.png" alt="A screenshot of the webhook activity log showing it being triggered a number of times, with the timestamp, HTTP status codes and links to view details." height="398" width="1193" /><p class="post__p">You&#39;ll now see an automatically generated list of your last five blog posts in your GitHub profile README. 🎉</p><img src="https://images.ctfassets.net/56dzm01z6lln/65TFLgy66L66x9Qs9nbRfM/060b0ed38d74bf73ce88b0a4ce4a8b50/generated_list.png" alt="A screenshot from my GitHub profile README, showing the last five blog posts in a list." height="249" width="667" /><p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to make your font sizes accessible with CSS</title>
          <description>Here's how to make sure your website respects font size preferences specified in browser settings using two important CSS concepts.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-make-your-font-sizes-accessible-with-css/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-make-your-font-sizes-accessible-with-css/</guid>
          <pubDate>Sun, 16 May 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Accessibility</category><category>CSS</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Everyone has personal preferences when it comes to browsing the web. Light mode, dark mode, window size and zoom percentage are only a few ways in which folks can optimise the quality of their reading experience — whether they are visually impaired or not. For example, when I <a href="https://twitch.tv/whitep4nth3r" target="_blank">live stream on Twitch</a>, I increase the zoom value in browser pages and browser dev tools to ensure that my viewers can see what I&#39;m explaining clearly — especially if they&#39;re viewing the stream on a mobile device. </p><p class="post__p">Fun fact — I&#39;m writing this blog post in the Contentful Web App zoomed in to 110%. </p><p class="post__p">Ensuring that people who use your website can <b class="post__p--italic">override text spacing</b> without affecting the readability of the content, is one of the most positive impacts you can have on the quality of a person&#39;s reading experience. Guidance on text spacing from <a href="https://www.w3.org/WAI/WCAG21/Understanding/text-spacing.html" target="_blank">W3C Web Accessibility Initiative (WAI)</a> states the following:</p><blockquote class="post__blockquote"><p class="post__p">...ensure that people can override author specified text spacing to improve their reading experience</p></blockquote><blockquote class="post__blockquote"><p class="post__p">...ensuring users can override author settings for spacing also significantly increases the likelihood other style preferences can be set by the user. For example, a user may need to change to a wider font family than the author has set in order to effectively read text.</p></blockquote><p class="post__p">At the time of writing this blog post, I attempted to find data on the percentage of people who browse the web at the non-default font size settings — and I concluded that the <b class="post__p--bold">statistics aren&#39;t important</b>. What&#39;s more important is that as web developers, we consider people&#39;s personal preferences and needs — and build applications that respect those needs.</p><h2 class="post__h2">Setting the preferred browser font size</h2><p class="post__p">Browser font size settings are usually configured in browser settings &gt; appearance settings. In Brave browser (Chromium-based), you can find the browser font settings in settings &gt; appearance &gt; font size.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1qKU39gNlNs8s4Wq6NDGWi/c7c058d1f26a845e7e4b735c58fb3a0e/font_settings_preferences.gif" alt="A looping GIF of changing the font preference settings in Brave browser, switching from medium, to very large, to very small, and back to medium, showing how the font sizes in the browser settings respond accordingly." height="259" width="640" /><p class="post__p">Now we know how to change font size settings to check how our websites respond to user-specified settings, let&#39;s take a look at two CSS rules we can use to ensure a website responds to those settings.</p><h2 class="post__h2">Set font-size: 100% on the html tag</h2><p class="post__p">Setting <code>font-size: 100%</code> on the <code>&lt;html&gt;</code> tag ensures that your CSS applies an initial font-size value to the whole document that is equal to the font size set by the user in the browser. More on why this is important later.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YhcnoUgCzr"
      aria-describedby="YhcnoUgCzr">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YhcnoUgCzr">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="YhcnoUgCzr" itemprop="text" content="html%20%7B%0A%20%20font-size%3A%20100%25%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">html</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 100%<br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Use rem units for font sizes</h2><p class="post__p">A <code>rem</code> unit in CSS stands for <b class="post__p--italic">root em.</b> </p><p class="post__p">An <code>em</code> unit is equal to the computed value of the property of the parent element on which it is used. For example:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ERbDuGAOMG"
      aria-describedby="ERbDuGAOMG">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ERbDuGAOMG">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="ERbDuGAOMG" itemprop="text" content=".parent%20%7B%0A%20%20font-size%3A%2016px%3B%0A%7D%0A%0A%2F%2F%201em%20%3D%2016px%2C%20inherited%20from%20the%20parent%20element%0A%0A.parent%20.child%20%7B%0A%20%20font-size%3A%201em%3B%0A%7D%0A%0A%2F%2F%202em%20%3D%2032px%2C%20inherited%20from%20the%20parent%20element%0A%0A.parent%20.anotherChild%20%7B%0A%20%20font-size%3A%202em%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.parent</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 16px<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">// 1em = 16px, inherited from the parent element<br><br>.parent .child</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 1em<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">// 2em = 32px, inherited from the parent element<br><br>.parent .anotherChild</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 2em<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">A <code>rem</code> unit is calculated relatively to the <b class="post__p--bold">computed value on the </b><b class="post__p--bold"><b class="post__p--italic">root</b></b><b class="post__p--bold"> element</b> (this bit is super important), which is usually the <code>&lt;html&gt;</code> element. For example:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BzKqTCxjFO"
      aria-describedby="BzKqTCxjFO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BzKqTCxjFO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="BzKqTCxjFO" itemprop="text" content="html%20%7B%0A%20%20font-size%3A%2016px%3B%0A%7D%0A%0A%2F%2F%201rem%20is%20equal%20to%20the%20font-size%20specified%20on%20the%20html%20root%20element%20%0A%2F%2F%20In%20this%20case%2C%20it%20is%20calculated%20at%2016px%0A%0A.anyOtherElement%20%7B%0A%20%20font-size%3A%201rem%3B%0A%7D%0A%0A%2F%2F%20The%20computed%20value%20of%202rem%20is%2032px%0A%0A.anotherElement%20%7B%0A%20%20font-size%3A%202rem%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">html</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 16px<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">// 1rem is equal to the font-size specified on the html root element <br>// In this case, it is calculated at 16px<br><br>.anyOtherElement</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">// The computed value of 2rem is 32px<br><br>.anotherElement</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 2rem<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">If we modify the example above to use <code>font-size: 100%</code> on the root (<code>&lt;html&gt;</code>) element, <b class="post__p--italic">we automatically set the initial font size of the document to 100% of the size specified in the browser — this is the computed value — </b>and all child elements that use rems as a unit of measurement will be <b class="post__p--italic">calculated relative to the computed value.</b></p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mEkcycudoC"
      aria-describedby="mEkcycudoC">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mEkcycudoC">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="mEkcycudoC" itemprop="text" content="%2F%2F%20Set%20font-size%3A%20100%25%20on%20the%20root%20element%20to%20pick%20up%20the%20browser%20setting%0A%0Ahtml%20%7B%0A%20%20font-size%3A%20100%25%3B%0A%7D%0A%0A%2F%2F%201rem%20%3D%201%20x%20the%20default%20font-size%20set%20by%20the%20user%20in%20the%20browser%0A%0A.anElement%20%7B%0A%20%20font-size%3A%201rem%3B%0A%7D%0A%0A%2F%2F%202rem%20%3D%202%20x%20the%20default%20font-size%20set%20by%20the%20user%20in%20the%20browser%0A%0A.anElement%20%7B%0A%20%20font-size%3A%202rem%3B%0A%7D%0A%0A%2F%2F%20and%20so%20on...">
      <pre class="language-css"><code class="language-css"><span class="token selector">// Set font-size: 100% on the root element to pick up the browser setting<br><br>html</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 100%<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">// 1rem = 1 x the default font-size set by the user in the browser<br><br>.anElement</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">// 2rem = 2 x the default font-size set by the user in the browser<br><br>.anElement</span> <span class="token punctuation">{</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 2rem<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br>// and so on...</code></pre>
    </div>
  </div>

  <h2 class="post__h2">Watch it in action</h2><p class="post__p">Inspect the DOM on <a href="https://whitep4nth3r.com" target="_blank">whitep4nth3r.com</a> for a live example of how using <code>font-size: 100%</code> on the <code>&lt;html&gt;</code> element and <code>rem</code> units for all font-size declarations (and more) allows the web page content to respond to browser appearance settings.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2jghRiL21S6nSno2WnFKNf/70b4220210c148e4f27247d52169a91e/font_size_changing_website.gif" alt="A looping GIF showing how this website respects the font size browser preferences set by the user." height="181" width="320" /><p class="post__p">And there you have it — two CSS rules to live by to ensure people who use your website can <b class="post__p--italic">override text spacing</b> without affecting the readability of the content as specified by the <a href="https://www.w3.org/WAI/WCAG21/Understanding/text-spacing.html" target="_blank">W3C WAI Guidelines</a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>What is BEM in CSS?</title>
          <description>Did you change CSS somewhere and something unexpected happened somewhere else? I have the solution for you.
</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/what-is-bem-in-css/</link>
          <guid>https://whitep4nth3r.com/blog/what-is-bem-in-css/</guid>
          <pubDate>Mon, 10 May 2021 23:00:00 GMT</pubDate>
          <category>CSS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Have you ever worked on an application with one huge CSS file and found that when you changed a style rule in one place, something unexpected happened somewhere else? I had this problem <b class="post__p--italic">a lot</b> in my early days of front end development. It was frustrating! So what can you do to stop this from happening? </p><h2 class="post__h2">You need to scope your style rules!</h2><p class="post__p">To <b class="post__p--bold">scope your CSS</b> means to encapsulate style rules in a systematic way so that they apply to one particular chunk of HTML only. CSS-in-JS solutions such as <a href="https://styled-components.com/" target="_blank">Styled Components</a> or <a href="https://github.com/css-modules/css-modules" target="_blank">CSS modules</a> that ship with front end frameworks have largely solved this problem by scoping styles to your component templates as standard. This means you don&#39;t need to worry about classes in one component affecting the styling of another component — even if you use the same class name. Nice!</p><p class="post__p">But what if you&#39;re just starting out, and you want to focus on building out pure CSS in a systematic way without getting bogged down with CSS-in-JS?</p><h2 class="post__h2">Working in pure CSS</h2><p class="post__p">In order to scope your styles in pure CSS, the aim is to declare your CSS classes specifically and solely for individual HTML components. Style rules should be purposefully verbose and self-documenting, without relying on inheritance or default browser behaviour. This type of system discourages the use of utility classes reused across multiple components because this is where you can run into the problems described at the beginning of the post. If you change the style properties of a utility class used in multiple components, <b class="post__p--italic">it could affect the layout of your whole application</b> — sometimes with very undesirable results! </p><p class="post__p">Let&#39;s take a look at how we can harness the power of a system like <b class="post__p--bold">BEM</b>.</p><h2 class="post__h2">What does BEM stand for?</h2><p class="post__p">BEM stands for <b class="post__p--bold">block, element, modifier</b>, and it&#39;s a super-handy system to help you scope CSS style properties to blocks of HTML. What&#39;s more, it encourages you to make your HTML and CSS descriptive and self-documenting — helping identify the purpose and intended function of the CSS classes in the code itself.</p><p class="post__p">Other class naming conventions exist alongside BEM to help you scope styles when writing HTML and CSS — such as OOCSS and SMACSS. You can even roll your own system! But the most important thing to remember is to use a system, stick to that system, and <b class="post__p--italic">make it work for you</b>.</p><p class="post__p">So, how do we work with BEM?</p><h2 class="post__h2">Block, element, modifier</h2><p class="post__p">Let&#39;s take a look at the building blocks of BEM.</p><h3 class="post__h3">Block: a chunk of HTML to which you want to scope styles</h3>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="jparUwrCYT"
      aria-describedby="jparUwrCYT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="jparUwrCYT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="jparUwrCYT" itemprop="text" content=".block%20%7B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.block</span> <span class="token punctuation">{</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Element: any element inside that block, name-spaced with your block name</h3>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="yJHhReLnYW"
      aria-describedby="yJHhReLnYW">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="yJHhReLnYW">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="yJHhReLnYW" itemprop="text" content=".block__elementOne%20%7B%0A%7D%0A%0A.block__elementTwo%20%7B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.block__elementOne</span> <span class="token punctuation">{</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.block__elementTwo</span> <span class="token punctuation">{</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Modifier: a flag to modify styles of an existing element, without creating a separate CSS class</h3>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="gkiaHQSAhN"
      aria-describedby="gkiaHQSAhN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="gkiaHQSAhN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="gkiaHQSAhN" itemprop="text" content=".block__elementOne--modifier%20%7B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.block__elementOne--modifier</span> <span class="token punctuation">{</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">BEM syntax conventions</h3><ul><li><p class="post__p">Use one or two underscores to separate the block name from the element name</p></li><li><p class="post__p">Use one or two dashes to separate the element name and its modifier</p></li><li><p class="post__p">Use descriptive class names in <b class="post__p--bold">camelCase</b></p></li></ul><h2 class="post__h2">BEM in context</h2><p class="post__p">In context, your HTML using the above class names might look like this:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="oSJiQWAGOd"
      aria-describedby="oSJiQWAGOd">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="oSJiQWAGOd">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="oSJiQWAGOd" itemprop="text" content="%3Csection%20class%3D%22block%22%3E%0A%20%20%3Cp%20class%3D%22block__elementOne%22%3EThis%20is%20an%20element%20inside%20a%20block.%3C%2Fp%3E%0A%20%20%3Cp%20class%3D%22block__elementOne%20block__elementOne--modifier%22%3EThis%20is%20an%20element%20inside%20a%20block%2C%20with%20a%20modifier.%3C%2Fp%3E%0A%3C%2Fsection%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>block<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>block__elementOne<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>This is an element inside a block.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>block__elementOne block__elementOne--modifier<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>This is an element inside a block, with a modifier.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">In a real-life example, with more realistic class names, this might look like:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ngEvdUpkEw"
      aria-describedby="ngEvdUpkEw">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ngEvdUpkEw">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="ngEvdUpkEw" itemprop="text" content="%3Csection%20class%3D%22container%22%3E%0A%20%20%3Cp%20class%3D%22container__paragraph%22%3EThis%20is%20a%20paragraph%20inside%20a%20container.%3C%2Fp%3E%0A%20%20%3Cp%20class%3D%22container__paragraph%20container__paragraph--bold%22%3E%0A%20%20%20%20This%20is%20a%20paragraph%20inside%20a%20container%2C%20with%20a%20modifier%20that%20adds%20bold%20styling.%0A%20%20%3C%2Fp%3E%0A%3C%2Fsection%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>section</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>container<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>container__paragraph<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>This is a paragraph inside a container.<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>container__paragraph container__paragraph--bold<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    This is a paragraph inside a container, with a modifier that adds bold styling.<br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>p</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>section</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Using the fully-declarative approach, where you don&#39;t rely on inheritance or default browser styles, your CSS classes might look like this:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fJUdKmemuA"
      aria-describedby="fJUdKmemuA">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fJUdKmemuA">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="fJUdKmemuA" itemprop="text" content=".container%20%7B%0A%20%20display%3A%20block%3B%0A%20%20margin%3A%201rem%20auto%3B%0A%20%20padding%3A%201rem%3B%0A%20%20box-sizing%3A%20border-box%3B%0A%7D%0A%0A.container__paragraph%20%7B%0A%20%20color%3A%20%23000000%3B%0A%20%20font-family%3A%20Arial%2C%20Helvetica%2C%20sans-serif%3B%0A%20%20font-size%3A%201rem%3B%0A%20%20font-weight%3A%20normal%3B%0A%20%20line-height%3A%201.2%3B%0A%20%20margin%3A%200%200%201rem%200%3B%0A%7D%0A%0A.container__paragraph--bold%20%7B%0A%20%20font-weight%3A%20bold%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">.container</span> <span class="token punctuation">{</span><br>  <span class="token property">display</span><span class="token punctuation">:</span> block<span class="token punctuation">;</span><br>  <span class="token property">margin</span><span class="token punctuation">:</span> 1rem auto<span class="token punctuation">;</span><br>  <span class="token property">padding</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><br>  <span class="token property">box-sizing</span><span class="token punctuation">:</span> border-box<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.container__paragraph</span> <span class="token punctuation">{</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> #000000<span class="token punctuation">;</span><br>  <span class="token property">font-family</span><span class="token punctuation">:</span> Arial<span class="token punctuation">,</span> Helvetica<span class="token punctuation">,</span> sans-serif<span class="token punctuation">;</span><br>  <span class="token property">font-size</span><span class="token punctuation">:</span> 1rem<span class="token punctuation">;</span><br>  <span class="token property">font-weight</span><span class="token punctuation">:</span> normal<span class="token punctuation">;</span><br>  <span class="token property">line-height</span><span class="token punctuation">:</span> 1.2<span class="token punctuation">;</span><br>  <span class="token property">margin</span><span class="token punctuation">:</span> 0 0 1rem 0<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.container__paragraph--bold</span> <span class="token punctuation">{</span><br>  <span class="token property">font-weight</span><span class="token punctuation">:</span> bold<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Notice how any default browser behaviour we might take for granted has been declared in the above classes — such as <code>display: block</code> on the <code>.container &lt;div&gt;</code> element. This is an extremely useful way to ensure that if you need to switch up the HTML elements in your components — for example swapping the <code>&lt;div&gt;</code> (default <code>display: block</code>) for a <code>&lt;span&gt;</code> (default <code>display: inline</code>) in the above example — the resulting styles of your components are not affected.</p><h2 class="post__h2">Wrapping up</h2><p class="post__p">Using BEM is not going to solve <b class="post__p--italic">all</b> your CSS problems (good luck with centring those <code>&lt;div&gt;</code> elements in your layouts!), but it can help you take a step in the right direction to make your CSS readable, descriptive, and safe from any unexpected results. Again, the most important thing to remember is to use a system, stick to that system, and <b class="post__p--italic">make it work for you</b>.</p><p class="post__p">Check out my latest YouTube video that supports this post. Subscribe for more regular front end web development tips!</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/SND9SDdY0UM"
        title="What is BEM in CSS?"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p"></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>When to use aria-labels in your HTML</title>
          <description>This is one of the most important ways to use aria-labels so your code provides contextual information to screen-readers and assistive tech.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/when-to-use-aria-labels-in-your-html/</link>
          <guid>https://whitep4nth3r.com/blog/when-to-use-aria-labels-in-your-html/</guid>
          <pubDate>Mon, 03 May 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Accessibility</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <h2 class="post__h2">TL;DR</h2><p class="post__p"><b class="post__p--italic">It&#39;s common to display multiple links with the same text on a web page — such as &#39;read more&#39; in a list of blog posts. To improve the web page experience for people who use screen readers and accessibility tools, add an aria-label to the anchor element with text that differentiates the content of the onward links.</b></p><p class="post__p">Check the results in Chromium Dev Tools &gt; Accessibility. Voila!</p><img src="https://images.ctfassets.net/56dzm01z6lln/3VDMXp15sXiboalgvpbfQO/96f6b319d55f20aed2b03539334c0f26/aria_label.png" alt="A screenshot of my website, showing how aria labels enrich the context of the HTML in the accessibility tree in Chromium dev tools." height="941" width="1908" /><h2 class="post__h2">Checking for accessibility</h2><p class="post__p">In a previous post, <a href="https://whitep4nth3r.com/blog/how-to-make-your-code-blocks-accessible-on-your-website/">How to make your code blocks accessible on your website</a>, I listed many of the tools I use to check accessibility on my websites as part of my workflow, including <a href="https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd" target="_blank">axe Dev Tools</a>.</p><p class="post__p">Here&#39;s a section of my website home page that displays a list of recent blog posts. Observe six links with the text &#39;read more&#39;, below.</p><img src="https://images.ctfassets.net/56dzm01z6lln/bv7aJV66P8RQHDoPSgaT6/d6d1c1292c6d59f1675fedf4a954fc0a/blog_post_list.png" alt="A screenshot of my website showing my recent post list, with six links using the text 'read more'." height="946" width="1916" /><p class="post__p">The code for each &#39;read more&#39; link (without an aria-label) might look like this:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mdjPQLDivo"
      aria-describedby="mdjPQLDivo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mdjPQLDivo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="mdjPQLDivo" itemprop="text" content="%3Ca%20href%3D%22https%3A%2F%2Fwhitep4nth3r.com%2Fblog%2Fhow-to-make-your-code-blocks-accessible-on-your-website%22%3ERead%20more%3C%2Fa%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://whitep4nth3r.com/blog/how-to-make-your-code-blocks-accessible-on-your-website<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Read more<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">If we scan the code above with <b class="post__p--bold">axe</b>, we receive the following warning:</p><blockquote class="post__blockquote"><p class="post__p">Ensure that links with the same accessible name serve a similar purpose</p></blockquote><img src="https://images.ctfassets.net/56dzm01z6lln/19V8LeAm1bKG7oySEc3z9N/3a398b4f64119cca6952f0fb0e4457fb/axe_dev_tools_need_aria_labels.png" alt="A screenshot of my website and Axe Dev Tools console, showing the issue description 'Ensure that links with the same accessible name serve a similar purpose', as all of the links have the text 'Read more'." height="946" width="1916" /><p class="post__p">You can use the accessibility tree in Chromium Dev Tools to check how screen readers and assistive technologies understand your web page. Find the accessibility tab far to the right of the styles tab.</p><img src="https://images.ctfassets.net/56dzm01z6lln/npPM4qm2M1AIvLCgqqFyf/1796f05f4a5df0564c10e6471c36c6cd/accessibility_tree_read_more.png" alt="A screenshot of my website showing the accessibility tree in Chromium dev tools, where a link is described with the text 'Read more'. This is the same for all links in that section." height="946" width="1916" /><p class="post__p">If you were using a screen reader to select the link to a specific blog post, and all the information you had was the text &#39;read more&#39; — six times over — you&#39;d be pretty frustrated! So how do we fix it?</p><h2 class="post__h2">Add aria-labels to provide descriptive information</h2><p class="post__p">I always recommend using a variety of tools to check accessibility on your website, and also to use your judgement. Here&#39;s what Axe suggests to check in the above example:</p><blockquote class="post__blockquote"><p class="post__p">Check that links have the same purpose, or are intentionally ambiguous</p></blockquote><p class="post__p">I checked, and I confirmed. These links do <b class="post__p--bold"><b class="post__p--italic">not</b></b><b class="post__p--bold"> </b>have the same purpose — they link to different blog posts or onward journeys. Here&#39;s where the power of aria-labels comes into play.</p><p class="post__p">Here&#39;s the same anchor link with an aria-label attribute added:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fJkabkKVCD"
      aria-describedby="fJkabkKVCD">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fJkabkKVCD">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="fJkabkKVCD" itemprop="text" content="%3Ca%20href%3D%22https%3A%2F%2Fwhitep4nth3r.com%2Fblog%2Fhow-to-make-your-code-blocks-accessible-on-your-website%22%20aria-label%3D%22Read%20How%20to%20make%20your%20code%20blocks%20accessible%20on%20your%20website%22%3ERead%20more%3C%2Fa%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://whitep4nth3r.com/blog/how-to-make-your-code-blocks-accessible-on-your-website<span class="token punctuation">"</span></span> <span class="token attr-name">aria-label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Read How to make your code blocks accessible on your website<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Read more<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">When we re-scan the page with <b class="post__p--bold">axe</b> with the aria-labels in place, everything looks good! We also see that the links have distinct titles in the accessibility tree, allowing screen readers and assistive technologies to read out a more descriptive link when the element is in focus.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4c6AmxEQ9SBI3zQL9DHsVy/eeb0b567d7c7c2c45222d97d1ba09247/aria_labels_in_accessibility_tree.png" alt="A screenshot of my website, showing that when adding aria-labels to the links, the accessibility tree has better context of the onward journey." height="946" width="1916" /><h2 class="post__h2">The edge case</h2><p class="post__p">The downside to <b class="post__p--italic">relying</b> on aria-label is that the content of the attribute may not automatically be translated when using automatic translation tools. Adrian Roselli tells us more in this blog post — <a href="https://adrianroselli.com/2019/11/aria-label-does-not-translate.html" target="_blank">aria-label Does Not Translate</a>.</p><h2 class="post__h2">7 web dev accessibility tools to help you build better websites</h2><p class="post__p">To learn more about the different accessibility tools I use on a daily basis, check out my very first YouTube video — <b class="post__p--italic">7 web dev accessibility tools to help you build better websites</b>.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/VjbYTdR-NYE"
        title="7 web dev accessibility tools to help you build better websites"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p">Make accessibility part of your web dev workflow from the moment you write that first line of code, and you&#39;ll automatically create better and more inclusive experiences for everyone as you build stuff, learn things, and love what you do.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Paginating your Contentful blog posts in Next.js with the GraphQL API</title>
          <description>In this post, we’re going to build a set of article list pages that display a number of blog post summaries per page.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/04/23/paginating-contentful-blogposts-with-nextjs-graphql-api/</link>
          <guid>https://www.contentful.com/blog/2021/04/23/paginating-contentful-blogposts-with-nextjs-graphql-api/</guid>
          <pubDate>Thu, 22 Apr 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>GraphQL</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In this post, we’re going to build a set of article list pages that display a number of blog post summaries per page — fetched from the Contentful GraphQL API at build time. We’ll also include navigation to next and previous pages. What’s great about this approach is that it requires no client-side state. All article-list pages are pre-rendered into static HTML at build time. This requires a lot less code than you might think!</p><h2 class="post__h2">The benefits of Static Site Generation</h2><p class="post__p">Next.js is a powerful framework that offers Static Site Generation (SSG) for React applications. Static Site Generation is where your website pages are pre-rendered as static files using data fetched at <b class="post__p--bold">build time </b>(on the server), rather than running JavaScript to build the page in the browser (on the client) or on the server at the time someone visits your website (run time).</p><p class="post__p">Some of the benefits of SSG:</p><ul><li><p class="post__p">Speed. Entire pages are loaded at first request rather than having to wait for client-side requests to fetch the data required.</p></li><li><p class="post__p">Accessibility. Pages load without JavaScript.</p></li><li><p class="post__p">Convenience. Host the files on your static hoster of choice (Netlify, Vercel or even good old GitHub pages) and call it a day!</p></li><li><p class="post__p">It’s scalable, fast and secure.</p></li></ul><p class="post__p">Here’s how it looks in a complete Next.js starter. <a href="https://nextjs-contentful-blog-starter.vercel.app/blog" target="_blank">Click here to view a live demo of the code referenced in this post.</a></p><img src="https://images.ctfassets.net/56dzm01z6lln/7Dvj1VssGFdwTjzBQJE0uv/d0744de226023942cbc06814c648d298/blog_index_screenshot.png" alt="Screenshot of the blog index from the Next.js and Contentful starter example website" height="1288" width="999" /><p class="post__p">To build the article list pagination, we’re going to harness the power of Static Site Generation provided by Next.js via the following two asynchronous functions:</p><ul><li><p class="post__p"><code>getStaticProps</code>: Fetch data at build time</p></li><li><p class="post__p"><code>getStaticPaths</code>: Specify dynamic routes to pre-render pages based on data</p></li></ul><p class="post__p"><a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation" target="_blank">If you’re new to Next.js, check out the documentation on Static Generation here.</a></p><h2 class="post__h2">Getting set up</h2><p class="post__p">I created a Next.js + Contentful blog starter repository that contains the completed code for statically generated article list pages described in this post. If you’d like to explore the code before getting started with the tutorial, you can <a href="https://github.com/whitep4nth3r/nextjs-contentful-blog-starter" target="_blank">fork the repository on GitHub here.</a></p><p class="post__p">We’re going to create a fresh Next.js application and build up the functionality to understand how it all fits together.</p><p class="post__p">For the purposes of this tutorial, you don’t need a Contentful account or any of your own blog posts. We’re going to connect to an example Contentful space that contains all of the data we need to build the article list pages and pagination. That being said, if you have an existing Contentful account and blog posts, you can connect your new Next.js application to your Contentful space with your own space ID and Contentful Delivery API access token. Just be sure to use the correct content type fields in your GraphQL queries if they are different to the example.</p><p class="post__p">To spin up a new Next.js application, run the following command in your terminal:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fKqujRaPmm"
      aria-describedby="fKqujRaPmm">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fKqujRaPmm">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="fKqujRaPmm" itemprop="text" content="npx%20create-next-app%20nextjs-contentful-pagination-tutorial%0A">
      <pre class="language-bash"><code class="language-bash">npx create-next-app nextjs-contentful-pagination-tutorial</code></pre>
    </div>
  </div>

  <p class="post__p">This command creates a new directory that includes all code to get started. This is what you should see after you run the command in your terminal window. (I’ve truncated the output a little with ‘...’ but what you’re looking for is ✨ Done!)</p><img src="https://images.ctfassets.net/56dzm01z6lln/JMxA6gIyX6voYaLfeGKmG/f1c0418dd2b669af80d49a23321b3404/terminal_output.png" alt="Screenshot of terminal output after running npx create-next-app" height="1015" width="997" /><p class="post__p">Navigate to the root of your project directory to view the files created for you.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KltrgpuyNQ"
      aria-describedby="KltrgpuyNQ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KltrgpuyNQ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="KltrgpuyNQ" itemprop="text" content="cd%20nextjs-contentful-pagination-tutorial%0Als%20-la">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> nextjs-contentful-pagination-tutorial<br><span class="token function">ls</span> <span class="token parameter variable">-la</span></code></pre>
    </div>
  </div>

  <p class="post__p">If this is what you see, you’re good to go!</p><img src="https://images.ctfassets.net/56dzm01z6lln/64hiNNr3SA68fJ58NqMCT1/194cab41a1b8992b53f4451ef0d66b85/ls_la_output.png" alt="Screenshot of terminal output after running npx create-next-app and then ls -la" height="417" width="1028" /><p class="post__p">You now have a fresh Next.js application with all dependencies installed. But what data are we going to use to build the article list pages?</p><h3 class="post__h3">Retrieving example data</h3><p class="post__p">I created an example Contentful space that provides the data for the <a href="https://nextjs-contentful-blog-starter.vercel.app/" target="_blank">Next.js Contentful Blog Starter.</a> It contains the content model we need and three blog posts so we can build out the pagination.</p><p class="post__p">In the root of your project directory, create an <code>.env.local</code> file.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="zyuLwKyxZd"
      aria-describedby="zyuLwKyxZd">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="zyuLwKyxZd">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="zyuLwKyxZd" itemprop="text" content="touch%20.env.local">
      <pre class="language-bash"><code class="language-bash"><span class="token function">touch</span> .env.local</code></pre>
    </div>
  </div>

  <p class="post__p">Copy and paste the following into the <code>.env.local</code> file:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="GZzcWSUlte"
      aria-describedby="GZzcWSUlte">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="GZzcWSUlte">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markup">
      <meta data-code-id="GZzcWSUlte" itemprop="text" content="CONTENTFUL_SPACE_ID%3D84zl5qdw0ore%0ACONTENTFUL_ACCESS_TOKEN%3D_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA">
      <pre class="language-markup"><code class="language-markup">CONTENTFUL_SPACE_ID=84zl5qdw0ore<br>CONTENTFUL_ACCESS_TOKEN=_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA</code></pre>
    </div>
  </div>

  <p class="post__p">These credentials will connect the application to the example Contentful space to provide you with some data to build out the functionality.</p><p class="post__p">We will use the following fields on the <code>blogPost</code> content type in our GraphQL queries to build the paginated article list:</p><ul><li><p class="post__p">Date (date &amp; time)</p></li><li><p class="post__p">Title (short text)</p></li><li><p class="post__p">Slug (short text)</p></li><li><p class="post__p">Tags (short text, list)</p></li><li><p class="post__p">Excerpt (long text, presented in a markdown editor)</p></li></ul><img src="https://images.ctfassets.net/56dzm01z6lln/5Fr3TY7mJhYlRIFjlGGfLi/c117b7b441a1713ce60f5e6a563ce190/basic_content_type_fields.png" alt="Screenshot of basic content type fields used in this tutorial" height="472" width="656" /><p class="post__p">You’re good to go if you have:</p><ul><li><p class="post__p">a fresh Next.js application</p></li><li><p class="post__p">an .env.local file with the example credentials provided above</p></li></ul><p class="post__p">To run the application, navigate to the root of your project directory and run:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="rBtqBWOalX"
      aria-describedby="rBtqBWOalX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="rBtqBWOalX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="rBtqBWOalX" itemprop="text" content="npm%20run%20dev">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> run dev</code></pre>
    </div>
  </div>

  <p class="post__p">You’ll need to stop and start your development server each time you add a new file to the application.</p><p class="post__p">So, we’ve got a Next.js application and credentials that we can use to connect us to a Contentful space! What files do we need in our application to implement a paginated blog?</p><h2 class="post__h2">Building the routes</h2><p class="post__p">We’re going to pre-render the following routes at build time, which will call out to the Contentful GraphQL API to get the data for each article list page:</p><ul><li><p class="post__p">/blog</p></li><li><p class="post__p">/blog/page/2</p></li><li><p class="post__p">/blog/page/3</p></li><li><p class="post__p">etc</p></li></ul><p class="post__p">In the pages directory, create a new directory and name it <code>blog</code>. Add a file called <code>index.js</code> — this will be the /blog route.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="RueyRythJm"
      aria-describedby="RueyRythJm">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="RueyRythJm">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="RueyRythJm" itemprop="text" content="cd%20my-blog%2Fpages%0Amkdir%20blog%20%0Acd%20blog%0Atouch%20index.js">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> my-blog/pages<br><span class="token function">mkdir</span> blog <br><span class="token builtin class-name">cd</span> blog<br><span class="token function">touch</span> index.js</code></pre>
    </div>
  </div>

  <p class="post__p">Next, inside the blog directory, create a new directory and name it <code>page</code>. Create a new file inside that directory, and name it <code>[page].js</code> — this will be our dynamic route, which will build the routes <code>/blog/page/{pageNumber}</code>. <a href="https://nextjs.org/docs/routing/dynamic-routes" target="_blank">Read more about dynamic routes on the Next.js docs</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="RYyEgWkuvl"
      aria-describedby="RYyEgWkuvl">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="RYyEgWkuvl">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="RYyEgWkuvl" itemprop="text" content="cd%20my-blog%2Fpages%2Fblog%0Amkdir%20page%0Acd%20page%0Atouch%20%5Bpage%5D.js">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> my-blog/pages/blog<br><span class="token function">mkdir</span> page<br><span class="token builtin class-name">cd</span> page<br><span class="token function">touch</span> <span class="token punctuation">[</span>page<span class="token punctuation">]</span>.js</code></pre>
    </div>
  </div>

  <p class="post__p">Here’s what your file and folder structure should look like:</p><img src="https://images.ctfassets.net/56dzm01z6lln/6NPcKEyL8j5rzdotOJkZ1V/039ffff1931682cd25b7f55ef49c35a5/folder_structure.png" alt="Screenshot of the folder structure in VSCode after creating the required files" height="185" width="293" /><p class="post__p">That’s all it takes to set up the routes <code>/blog/</code> and <code>/blog/page/{pageNumber}</code>, but they’re not doing anything yet. Let’s get some data from Contentful.</p><h2 class="post__h2">Setting up the calls to the Contentful GraphQL API</h2><p class="post__p">To fill the pages with data, we need to make API calls. I prefer to define API calls in a dedicated file, so that they can be reused easily across the application. In this example, I created a <a href="https://github.com/whitep4nth3r/nextjs-contentful-blog-starter/blob/main/utils/ContentfulApi.js" target="_blank">ContentfulApi.js</a> class, which can be found in the <code>utils</code> directory of the starter repository. We need to make two requests to the API to build our article list pages. </p><p class="post__p">Create a <code>utils</code> directory at the root of your project, and create a new file named <code>ContentfulApi.js</code>. </p><img src="https://images.ctfassets.net/56dzm01z6lln/4Vsnsu8v8sM7zQXH0Wun6s/ef996ef5ae0bb72d644d77d21e80f2a7/contentful_api_file.png" alt="Screenshot of ContentfulApi.js file created inside a utils directory in VSCode" height="123" width="471" /><p class="post__p">Before we start building the needed GraphQL queries, let’s set up an asynchronous call to the Contentful GraphQL API that takes in a string parameter named <code>query</code>. We’ll use this twice later to request data from Contentful.</p><p class="post__p">If you’d like to learn more about GraphQL, check out <a href="https://www.contentful.com/blog/author/stefan-judis/" target="_blank">Stefan Judis</a>’s <a href="https://www.youtube.com/watch?v=3fkwOtybh2A&list=PLAaQpb7XfX3BkRC5LUscTOz8ub9GL0v5Q" target="_blank">free GraphQL course on YouTube</a>.</p><p class="post__p">To explore the GraphQL queries in this post using the Contentful GraphiQL playground, navigate to the following URL and paste any of the queries below into the explorer (without the <code>const</code> and <code>=</code>). The space ID and access token in the URL will connect you to the same Contentful space that you connected to via the <code>.env.local</code> file.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vlnARctzLP"
      aria-describedby="vlnARctzLP">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vlnARctzLP">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="vlnARctzLP" itemprop="text" content="https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F84zl5qdw0ore%2Fexplore%3Faccess_token%3D_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA">
      <pre class="language-bash"><code class="language-bash">https://graphql.contentful.com/content/v1/spaces/84zl5qdw0ore/explore?access_token<span class="token operator">=</span>_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA</code></pre>
    </div>
  </div>

  <p class="post__p">Add the following code to <code>/utils/ContentfulApi.js</code>. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="dnDiXDPyRx"
      aria-describedby="dnDiXDPyRx">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="dnDiXDPyRx">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="dnDiXDPyRx" itemprop="text" content="%2F%2F%20%2Futils%2FContentfulApi.js%0A%0Aexport%20default%20class%20ContentfulApi%20%7B%0A%0A%20%20static%20async%20callContentful(query)%20%7B%0A%20%20%20%20const%20fetchUrl%20%3D%20%60https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%24%7Bprocess.env.CONTENTFUL_SPACE_ID%7D%60%3B%0A%0A%20%20%20%20const%20fetchOptions%20%3D%20%7B%0A%20%20%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20%20%20Authorization%3A%20%60Bearer%20%24%7Bprocess.env.CONTENTFUL_ACCESS_TOKEN%7D%60%2C%0A%20%20%20%20%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20body%3A%20JSON.stringify(%7B%20query%20%7D)%2C%0A%20%20%20%20%7D%3B%0A%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20const%20data%20%3D%20await%20fetch(fetchUrl%2C%20fetchOptions).then((response)%20%3D%3E%0A%20%20%20%20%20%20%20%20response.json()%2C%0A%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20return%20data%3B%0A%20%20%20%20%7D%20catch%20(error)%20%7B%0A%20%20%20%20%20%20throw%20new%20Error(%22Could%20not%20fetch%20data%20from%20Contentful!%22)%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /utils/ContentfulApi.js</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">ContentfulApi</span> <span class="token punctuation">{</span><br><br>  <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">callContentful</span><span class="token punctuation">(</span><span class="token parameter">query</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> fetchUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://graphql.contentful.com/content/v1/spaces/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CONTENTFUL_SPACE_ID</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> fetchOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">CONTENTFUL_ACCESS_TOKEN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>        <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">try</span> <span class="token punctuation">{</span><br>      <span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>fetchUrl<span class="token punctuation">,</span> fetchOptions<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span><br>        response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br>      <span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token keyword">return</span> data<span class="token punctuation">;</span><br>    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Could not fetch data from Contentful!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">We’ve got our API call set up! Now, let’s fetch some data.</p><h3 class="post__h3">Querying the total number of posts</h3><p class="post__p">In order to calculate how many dynamic page routes we need to build and statically generate on <code>/blog/page/[page].js</code>, we need to work out how many blog posts we have, and divide that by the number of posts we want to show on each page.</p><p class="post__p"><b class="post__p--italic">numberOfPages = totalNumberOfPosts / howManyPostsToDisplayOnEachPage</b></p><p class="post__p">For this, it’s useful to define how many posts you want to show on each page in a global variable or configuration object. We’ll need to use it in a few different places.</p><p class="post__p">For that reason, the <a href="https://github.com/whitep4nth3r/nextjs-contentful-blog-starter" target="_blank">Next.js + Contentful blog starter</a> contains a <a href="https://github.com/whitep4nth3r/nextjs-contentful-blog-starter/blob/main/utils/Config.js" target="_blank">Config.js</a> file in the <code>utils</code> directory. We’ll use the exported <code>Config</code> object in our API calls.</p><p class="post__p">Feel free to skip this step and use a hard-coded number if you’re just exploring.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BeNPrCpTzH"
      aria-describedby="BeNPrCpTzH">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BeNPrCpTzH">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="BeNPrCpTzH" itemprop="text" content="%2F%2F%20%2Futils%2FConfig.js%0A%0Aexport%20const%20Config%20%3D%20%7B%0A%20%20%2F%2F...%0A%20%20pagination%3A%20%7B%0A%20%20%20%20pageSize%3A%202%2C%0A%20%20%7D%2C%0A%7D%3B%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /utils/Config.js</span><br><br><span class="token keyword">export</span> <span class="token keyword">const</span> Config <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token comment">//...</span><br>  <span class="token literal-property property">pagination</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">pageSize</span><span class="token operator">:</span> <span class="token number">2</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">In the same <code>ContentfulApi</code> class, let’s create a new asynchronous method that will query and return the total blog number of blog posts.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="MZCbPSCwyn"
      aria-describedby="MZCbPSCwyn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="MZCbPSCwyn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="MZCbPSCwyn" itemprop="text" content="%2F%2F%20%2Futils%2FContentfulApi.js%0A%0Aexport%20default%20class%20ContentfulApi%20%7B%0A%0A%20%20static%20async%20callContentful(query)%20%7B%20%2F*%20GQL%20call%20described%20above%20*%2F%20%7D%0A%0A%20%20static%20async%20getTotalPostsNumber()%20%7B%0A%20%20%20%20%2F%2F%20Build%20the%20query%0A%20%20%20%20const%20query%20%3D%20%60%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20blogPostCollection%20%7B%0A%20%20%20%20%20%20%20%20%20%20total%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%60%3B%0A%0A%20%20%20%20%2F%2F%20Call%20out%20to%20the%20API%0A%20%20%20%20const%20response%20%3D%20await%20this.callContentful(query)%3B%0A%20%20%20%20const%20totalPosts%20%3D%20response.data.blogPostCollection.total%0A%20%20%20%20%20%20%3F%20response.data.blogPostCollection.total%0A%20%20%20%20%20%20%3A%200%3B%0A%0A%20%20%20%20return%20totalPosts%3B%0A%20%20%7D%0A%7D%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /utils/ContentfulApi.js</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">ContentfulApi</span> <span class="token punctuation">{</span><br><br>  <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">callContentful</span><span class="token punctuation">(</span><span class="token parameter">query</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">/* GQL call described above */</span> <span class="token punctuation">}</span><br><br>  <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">getTotalPostsNumber</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token comment">// Build the query</span><br>    <span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br>      {<br>        blogPostCollection {<br>          total<br>        }<br>      }<br>    </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>    <span class="token comment">// Call out to the API</span><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">callContentful</span><span class="token punctuation">(</span>query<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> totalPosts <span class="token operator">=</span> response<span class="token punctuation">.</span>data<span class="token punctuation">.</span>blogPostCollection<span class="token punctuation">.</span>total<br>      <span class="token operator">?</span> response<span class="token punctuation">.</span>data<span class="token punctuation">.</span>blogPostCollection<span class="token punctuation">.</span>total<br>      <span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">return</span> totalPosts<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">We’ve successfully retrieved our total number of blog posts. What’s next?</p><h3 class="post__h3">Querying the post summaries by page number</h3><p class="post__p">Let’s create a final asynchronous method that requests the number of blog post summaries we defined in <code>Config.pagination.pageSize</code>, by page number.</p><p class="post__p">We also request the total number of blog posts in this query. We’ll need this later, and it saves us having to make two API calls when generating the <code>/blog</code> route.</p><p class="post__p">Here’s the code. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LZfqEkidbM"
      aria-describedby="LZfqEkidbM">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LZfqEkidbM">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="LZfqEkidbM" itemprop="text" content="%2F%2F%20%2Futils%2FContentfulApi.js%0A%0Aexport%20default%20class%20ContentfulApi%20%7B%0A%0A%20%20static%20async%20callContentful(query)%20%7B%20%2F*%20GQL%20call%20described%20above%20*%2F%20%7D%0A%0A%20%20static%20async%20getTotalPostsNumber()%20%7B%20%2F*%20method%20described%20above%20*%2F%20%7D%0A%0A%20%20static%20async%20getPaginatedPostSummaries(page)%20%7B%0A%20%20%20%20const%20skipMultiplier%20%3D%20page%20%3D%3D%3D%201%20%3F%200%20%3A%20page%20-%201%3B%0A%20%20%20%20const%20skip%20%3D%0A%20%20%20%20%20%20skipMultiplier%20%3E%200%20%3F%20Config.pagination.pageSize%20*%20skipMultiplier%20%3A%200%3B%0A%0A%20%20%20%20const%20query%20%3D%20%60%7B%0A%20%20%20%20%20%20%20%20blogPostCollection(limit%3A%20%24%7BConfig.pagination.pageSize%7D%2C%20skip%3A%20%24%7Bskip%7D%2C%20order%3A%20date_DESC)%20%7B%0A%20%20%20%20%20%20%20%20%20%20total%0A%20%20%20%20%20%20%20%20%20%20items%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20date%0A%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20slug%0A%20%20%20%20%20%20%20%20%20%20%20%20excerpt%0A%20%20%20%20%20%20%20%20%20%20%20%20tags%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%60%3B%0A%0A%20%20%20%20%2F%2F%20Call%20out%20to%20the%20API%0A%20%20%20%20const%20response%20%3D%20await%20this.callContentful(query)%3B%0A%0A%20%20%20%20const%20paginatedPostSummaries%20%3D%20response.data.blogPostCollection%0A%20%20%20%20%20%20%3F%20response.data.blogPostCollection%0A%20%20%20%20%20%20%3A%20%7B%20total%3A%200%2C%20items%3A%20%5B%5D%20%7D%3B%0A%0A%20%20%20%20return%20paginatedPostSummaries%3B%0A%20%20%7D%0A%20%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /utils/ContentfulApi.js</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">class</span> <span class="token class-name">ContentfulApi</span> <span class="token punctuation">{</span><br><br>  <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">callContentful</span><span class="token punctuation">(</span><span class="token parameter">query</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">/* GQL call described above */</span> <span class="token punctuation">}</span><br><br>  <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">getTotalPostsNumber</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">/* method described above */</span> <span class="token punctuation">}</span><br><br>  <span class="token keyword">static</span> <span class="token keyword">async</span> <span class="token function">getPaginatedPostSummaries</span><span class="token punctuation">(</span><span class="token parameter">page</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">const</span> skipMultiplier <span class="token operator">=</span> page <span class="token operator">===</span> <span class="token number">1</span> <span class="token operator">?</span> <span class="token number">0</span> <span class="token operator">:</span> page <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> skip <span class="token operator">=</span><br>      skipMultiplier <span class="token operator">></span> <span class="token number">0</span> <span class="token operator">?</span> Config<span class="token punctuation">.</span>pagination<span class="token punctuation">.</span>pageSize <span class="token operator">*</span> skipMultiplier <span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">{<br>        blogPostCollection(limit: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>Config<span class="token punctuation">.</span>pagination<span class="token punctuation">.</span>pageSize<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, skip: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>skip<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, order: date_DESC) {<br>          total<br>          items {<br>            sys {<br>              id<br>            }<br>            date<br>            title<br>            slug<br>            excerpt<br>            tags<br>          }<br>        }<br>      }</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>    <span class="token comment">// Call out to the API</span><br>    <span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">callContentful</span><span class="token punctuation">(</span>query<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> paginatedPostSummaries <span class="token operator">=</span> response<span class="token punctuation">.</span>data<span class="token punctuation">.</span>blogPostCollection<br>      <span class="token operator">?</span> response<span class="token punctuation">.</span>data<span class="token punctuation">.</span>blogPostCollection<br>      <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">total</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token literal-property property">items</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">return</span> paginatedPostSummaries<span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br> <span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Observe that we are querying for the five fields referenced at the top of this post: date, title, slug, tags and excerpt — plus the <code>sys.id</code>. This will be useful when rendering our data to the DOM.</p><p class="post__p">The <code>skip</code> parameter in the GraphQL query is what does all the magic for us here. We calculate the skip parameter for the query based on the incoming <code>page</code> number parameter. For example, if we want to fetch the posts for page two, the skip parameter would be calculated as 1 x <code>Config.pagination.pageSize</code>, therefore skipping the results of page one.</p><p class="post__p">If we want to fetch the posts for page six, the skip parameter would be calculated as 5 x <code>Config.pagination.pageSize</code>, and so on. When all your code is set up in your application, play around with the <code>Config.pagination.pageSize</code> to see this magic in action.</p><p class="post__p">We’ve now set up all the API calls we need to get our data to pre-render our blog page routes at build time. Let’s fetch our data for page one on /blog.</p><h2 class="post__h2">Building the blog index with getStaticProps</h2><p class="post__p">The blog index will be available on <code>/blog</code> and will serve page one of our blog post summaries. For this reason, we can safely hardcode the number “1” in this file. This is great for readability — think self-documenting code!</p><p class="post__p">Let’s pre-render this page at build time by exporting an <code>async</code> function called <code>getStaticProps</code>. <a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation" target="_blank">Read more about getStaticProps on the Next.js documentation</a>.</p><p class="post__p">Add the following code to <code>pages/blog/index.js</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="THsWDmmPof"
      aria-describedby="THsWDmmPof">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="THsWDmmPof">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="THsWDmmPof" itemprop="text" content="%2F%2F%20%2Fpages%2Fblog%2Findex.js%0A%0Aimport%20ContentfulApi%20from%20%22%40utils%2FContentfulApi%22%3B%0Aimport%20%7B%20Config%20%7D%20from%20%22%40utils%2FConfig%22%3B%0A%0Aexport%20default%20function%20BlogIndex(props)%20%7B%0A%20%20const%20%7B%20postSummaries%2C%20currentPage%2C%20totalPages%20%7D%20%3D%20props%3B%0A%0A%20%20return%20(%0A%20%20%20%20%2F%2F%20We%E2%80%99ll%20build%20the%20post%20list%20component%20later%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getStaticProps()%20%7B%0A%20%20const%20postSummaries%20%3D%20await%20ContentfulApi.getPaginatedPostSummaries(1)%3B%0A%20%20const%20totalPages%20%3D%20Math.ceil(postSummaries.total%20%2F%20Config.pagination.pageSize)%3B%0A%0A%20%20return%20%7B%0A%20%20%20%20props%3A%20%7B%0A%20%20%20%20%20%20postSummaries%3A%20postSummaries.items%2C%0A%20%20%20%20%20%20totalPages%2C%0A%20%20%20%20%20%20currentPage%3A%20%221%22%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /pages/blog/index.js</span><br><br><span class="token keyword">import</span> ContentfulApi <span class="token keyword">from</span> <span class="token string">"@utils/ContentfulApi"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> Config <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@utils/Config"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">BlogIndex</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> postSummaries<span class="token punctuation">,</span> currentPage<span class="token punctuation">,</span> totalPages <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token comment">// We’ll build the post list component later</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getStaticProps</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> postSummaries <span class="token operator">=</span> <span class="token keyword">await</span> ContentfulApi<span class="token punctuation">.</span><span class="token function">getPaginatedPostSummaries</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> totalPages <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">ceil</span><span class="token punctuation">(</span>postSummaries<span class="token punctuation">.</span>total <span class="token operator">/</span> Config<span class="token punctuation">.</span>pagination<span class="token punctuation">.</span>pageSize<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">postSummaries</span><span class="token operator">:</span> postSummaries<span class="token punctuation">.</span>items<span class="token punctuation">,</span><br>      totalPages<span class="token punctuation">,</span><br>      <span class="token literal-property property">currentPage</span><span class="token operator">:</span> <span class="token string">"1"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">We’re using <code>getStaticProps()</code> to:</p><ul><li><p class="post__p">Request the post summaries for page one and total number of posts from the API.</p></li><li><p class="post__p">Calculate the total number of pages based on the number of posts and <code>Config.pagination.pageSize</code>.</p></li><li><p class="post__p">Return the <code>postSummaries.items</code>, <code>totalPages</code>, and <code>currentPage</code> as props to the <code>BlogIndex</code> component.</p></li></ul><h3 class="post__h3">Bonus content!</h3><p class="post__p">You’ll notice that the file imports from the <code>utils</code> directory in this example are imported using absolute paths via a module alias using <code>@</code>. This is a really neat way to avoid long relative path imports (../../../../..) in your Next.js application, which increases code readability.</p><p class="post__p">You can define module aliases in a <code>jsconfig.json</code> file at the root of your project. Here’s the <code>jsconfig.json</code> file used in the Next.js Contentful blog starter:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="NZPzUUltVT"
      aria-describedby="NZPzUUltVT">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="NZPzUUltVT">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="NZPzUUltVT" itemprop="text" content="%2F%2F%20jsconfig.json%0A%0A%7B%0A%20%20%22compilerOptions%22%3A%20%7B%0A%20%20%20%20%22baseUrl%22%3A%20%22.%2F%22%2C%0A%20%20%20%20%22paths%22%3A%20%7B%0A%20%20%20%20%20%20%22%40components%2F*%22%3A%20%5B%22components%2F*%22%5D%2C%0A%20%20%20%20%20%20%22%40utils%2F*%22%3A%20%5B%22utils%2F*%22%5D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token comment">// jsconfig.json</span><br><br><span class="token punctuation">{</span><br>  <span class="token property">"compilerOptions"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"baseUrl"</span><span class="token operator">:</span> <span class="token string">"./"</span><span class="token punctuation">,</span><br>    <span class="token property">"paths"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token property">"@components/*"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"components/*"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>      <span class="token property">"@utils/*"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"utils/*"</span><span class="token punctuation">]</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://nextjs.org/docs/advanced-features/module-path-aliases" target="_blank">Read more on the official documentation</a>.</p><p class="post__p">We’re going to be creating a <code>components</code> directory later on in this post, so I recommend adding this <code>jsconfig.json</code> file to your project to make file imports super easy. Make sure you stop and start your development server after adding this new file to enable Next.js to pick up the changes.</p><p class="post__p">So that’s fetching data for page one done! But how do we build the dynamic routes at build time based on how many blog posts we have, and how many posts we want to show per page?</p><h2 class="post__h2">Building the dynamic article list pages with getStaticPaths</h2><p class="post__p">The article list pages will be available on <code>/blog/page/{pageNumber}</code> starting with the second page (<code>/blog/</code> is page one). This is where we need to use <code>getStaticPaths()</code> to define a list of paths that will be rendered to HTML at build time.The rendered paths are based on the total number of blog posts, and how many posts we want to show per page. </p><p class="post__p">Let’s tell Next.js which paths we want to statically render by exporting an <code>async</code> function called <code>getStaticPaths</code>. <a href="https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation" target="_blank">Read more about getStaticPaths on the Next.js documentation</a>.</p><p class="post__p">Add the following code to <code>pages/blog/page/[page].js</code>:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xFlfJmHJHO"
      aria-describedby="xFlfJmHJHO">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xFlfJmHJHO">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="xFlfJmHJHO" itemprop="text" content="%2F%2F%20%2Fpages%2Fblog%2Fpages%2F%5Bpage%5D.js%0A%0Aimport%20ContentfulApi%20from%20%22%40utils%2FContentfulApi%22%3B%0Aimport%20%7B%20Config%20%7D%20from%20%22%40utils%2FConfig%22%3B%0A%0Aexport%20default%20function%20BlogIndexPage(props)%20%7B%0A%20%20const%20%7B%20postSummaries%2C%20totalPages%2C%20currentPage%20%7D%20%3D%20props%3B%0A%0A%20%20return%20(%0A%20%20%20%20%2F%2F%20We%E2%80%99ll%20build%20the%20post%20list%20component%20later%0A%20%20)%3B%0A%7D%0A%0Aexport%20async%20function%20getStaticPaths()%20%7B%0A%20%20const%20totalPosts%20%3D%20await%20ContentfulApi.getTotalPostsNumber()%3B%0A%20%20const%20totalPages%20%3D%20Math.ceil(totalPosts%20%2F%20Config.pagination.pageSize)%3B%0A%0A%20%20const%20paths%20%3D%20%5B%5D%3B%0A%0A%20%20%2F**%0A%20%20%20*%20Start%20from%20page%202%2C%20so%20we%20don't%20replicate%20%2Fblog%0A%20%20%20*%20which%20is%20page%201%0A%20%20%20*%2F%0A%20%20for%20(let%20page%20%3D%202%3B%20page%20%3C%3D%20totalPages%3B%20page%2B%2B)%20%7B%0A%20%20%20%20paths.push(%7B%20params%3A%20%7B%20page%3A%20page.toString()%20%7D%20%7D)%3B%0A%20%20%7D%0A%0A%20%20return%20%7B%0A%20%20%20%20paths%2C%0A%20%20%20%20fallback%3A%20false%2C%0A%20%20%7D%3B%0A%7D%0A%0Aexport%20async%20function%20getStaticProps(%7B%20params%20%7D)%20%7B%0A%20%20const%20postSummaries%20%3D%20await%20ContentfulApi.getPaginatedPostSummaries(%0A%20%20%20%20params.page%2C%0A%20%20)%3B%0A%20%20const%20totalPages%20%3D%20Math.ceil(postSummaries.total%20%2F%20Config.pagination.pageSize)%3B%0A%0A%20%20return%20%7B%0A%20%20%20%20props%3A%20%7B%0A%20%20%20%20%20%20postSummaries%3A%20postSummaries.items%2C%0A%20%20%20%20%20%20totalPages%2C%0A%20%20%20%20%20%20currentPage%3A%20params.page%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /pages/blog/pages/[page].js</span><br><br><span class="token keyword">import</span> ContentfulApi <span class="token keyword">from</span> <span class="token string">"@utils/ContentfulApi"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> Config <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@utils/Config"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">BlogIndexPage</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> postSummaries<span class="token punctuation">,</span> totalPages<span class="token punctuation">,</span> currentPage <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token comment">// We’ll build the post list component later</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getStaticPaths</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> totalPosts <span class="token operator">=</span> <span class="token keyword">await</span> ContentfulApi<span class="token punctuation">.</span><span class="token function">getTotalPostsNumber</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> totalPages <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">ceil</span><span class="token punctuation">(</span>totalPosts <span class="token operator">/</span> Config<span class="token punctuation">.</span>pagination<span class="token punctuation">.</span>pageSize<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> paths <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><br>  <span class="token comment">/**<br>   * Start from page 2, so we don't replicate /blog<br>   * which is page 1<br>   */</span><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> page <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">;</span> page <span class="token operator">&lt;=</span> totalPages<span class="token punctuation">;</span> page<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    paths<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">params</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">page</span><span class="token operator">:</span> page<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    paths<span class="token punctuation">,</span><br>    <span class="token literal-property property">fallback</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">export</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getStaticProps</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> params <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> postSummaries <span class="token operator">=</span> <span class="token keyword">await</span> ContentfulApi<span class="token punctuation">.</span><span class="token function">getPaginatedPostSummaries</span><span class="token punctuation">(</span><br>    params<span class="token punctuation">.</span>page<span class="token punctuation">,</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> totalPages <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">ceil</span><span class="token punctuation">(</span>postSummaries<span class="token punctuation">.</span>total <span class="token operator">/</span> Config<span class="token punctuation">.</span>pagination<span class="token punctuation">.</span>pageSize<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">postSummaries</span><span class="token operator">:</span> postSummaries<span class="token punctuation">.</span>items<span class="token punctuation">,</span><br>      totalPages<span class="token punctuation">,</span><br>      <span class="token literal-property property">currentPage</span><span class="token operator">:</span> params<span class="token punctuation">.</span>page<span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">We’re using <code>getStaticPaths()</code> to:</p><ul><li><p class="post__p">Request the total number of posts from the Contentful API.</p></li><li><p class="post__p">Calculate the total number of pages we need to build depending on the page size we defined.</p></li><li><p class="post__p">Build a <code>paths</code> array that starts from page two (blog/page/2) and ends at the total number of pages we calculated.</p></li><li><p class="post__p">Return the paths array to <code>getStaticProps</code> so that for each path, Next.js will request the data for the dynamic page number — <code>params.page</code> at build time.</p></li><li><p class="post__p">We’re using <code>fallback: false</code> because we always want to statically generate these paths at build time. If we add more blog posts which changes the number of pages we need to render, we’d want to build the site again. This is usually done with <a href="https://www.contentful.com/developers/docs/concepts/webhooks/" target="_blank">webhooks</a> that Contentful sends to your hosting platform of choice each time you publish a new change. <a href="https://nextjs.org/docs/basic-features/data-fetching#the-fallback-key-required" target="_blank">Read more about the fallback key here</a>.</p></li></ul><p class="post__p">In our dynamic route, we’re using <code>getStaticProps()</code> in a similar way to <code>/blog</code>, with the only difference being that we’re using <code>params.page</code> in the calls to the Contentful API instead of hardcoding page number “1.”</p><p class="post__p">Now we have our blog post summary data from Contentful, requested at build time and passed to our blog index and dynamic blog pages. Great! Let’s build a component to display our posts on the front end.</p><h2 class="post__h2">Building the post list component</h2><p class="post__p">Let’s build a <code>PostList</code> component that we will use on the blog index and our dynamic routes.</p><p class="post__p">Create a <code>components</code> directory at the route of your project, create a new directory inside that called <code>PostList</code>, and add a new file inside that directory called <code>index.js</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7fvYBZtVZ6fTL2SM18YA46/ee7efd8fd5b5227536a7d17e122f2d53/postlist_screenshot.png" alt="Screenshot of the PostList component in the components folder in VSCode" height="150" width="464" /><p class="post__p">The <code>PostList</code> renders an ordered list (<code>&lt;ol&gt;</code>) of <code>article</code> elements that display the date, title, tags and excerpt of the post via the JavaScript <code>map()</code> function. We use <code>next/link</code> to enable a <a href="https://nextjs.org/docs/api-reference/next/link" target="_blank">client-side transition</a> to the blog post itself. Also notice that we’re using the <code>post.sys.id</code> on the <code>&lt;li&gt;</code> element to ensure each element in the map has a unique key. <a href="https://reactjs.org/docs/lists-and-keys.html#keys" target="_blank">Read more about keys in React.</a></p><p class="post__p">This example uses <code>react-markdown</code> to render the markdown of the excerpt field. This package is an optional dependency. Using it depends on the amount of flexibility you require for displaying formatted text in the blog-post excerpt. If you’re curious, you can view the <a href="https://github.com/whitep4nth3r/nextjs-contentful-blog-starter/blob/main/utils/ReactMarkdownRenderers.js" target="_blank">ReactMarkdownRenderers.js</a> file in the example project repository. This is used to add CSS classes and formatting to the markdown returned from the API. </p><p class="post__p">If you’d like to use <code>react-markdown</code> with the renderer options provided in the example project, <a href="https://www.npmjs.com/package/react-markdown" target="_blank">install the package via npm following the given instructions</a>.</p><p class="post__p">I’ve also included a couple of date-formatting functions for the HTML <code>&lt;time&gt;</code> element referenced below in <a href="https://github.com/whitep4nth3r/nextjs-contentful-blog-starter/blob/main/utils/Date.js" target="_blank">this file on GitHub</a> to help you out.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="cZTHZGLRoe"
      aria-describedby="cZTHZGLRoe">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="cZTHZGLRoe">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="cZTHZGLRoe" itemprop="text" content="%2F%2F%20%2Fcomponents%2FPostList%2Findex.js%0A%0Aimport%20Link%20from%20%22next%2Flink%22%3B%0Aimport%20ReactMarkdown%20from%20%22react-markdown%22%3B%0Aimport%20ReactMarkdownRenderers%20from%20%22%40utils%2FReactMarkdownRenderers%22%3B%0Aimport%20%7B%0A%20%20formatPublishedDateForDateTime%2C%0A%20%20formatPublishedDateForDisplay%2C%0A%7D%20from%20%22%40utils%2FDate%22%3B%0A%0Aexport%20default%20function%20PostList(props)%20%7B%0A%20%20const%20%7B%20posts%20%7D%20%3D%20props%3B%0A%0A%20%20return%20(%0A%20%20%20%20%20%20%3Col%3E%0A%20%20%20%20%20%20%20%20%7Bposts.map((post)%20%3D%3E%20(%0A%20%20%20%20%20%20%20%20%20%20%3Cli%20key%3D%7Bpost.sys.id%7D%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Carticle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ctime%20dateTime%3D%7BformatPublishedDateForDateTime(date)%7D%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7BformatPublishedDateForDisplay(date)%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Ftime%3E%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3CLink%20href%3D%7B%60blog%2F%24%7Bpost.slug%7D%60%7D%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ca%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ch2%3E%7Bpost.title%7D%3C%2Fh2%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fa%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2FLink%3E%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cul%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Btags.map((tag)%20%3D%3E%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Cli%20key%3D%7Btag%7D%3E%7Btag%7D%3C%2Fli%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20))%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Ful%3E%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3CReactMarkdown%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20children%3D%7Bpost.excerpt%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20renderers%3D%7BReactMarkdownRenderers(post.excerpt)%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Farticle%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2Fli%3E%0A%20%20%20%20%20%20%20%20))%7D%0A%20%20%20%20%20%20%3C%2Fol%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /components/PostList/index.js</span><br><br><span class="token keyword">import</span> Link <span class="token keyword">from</span> <span class="token string">"next/link"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> ReactMarkdown <span class="token keyword">from</span> <span class="token string">"react-markdown"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> ReactMarkdownRenderers <span class="token keyword">from</span> <span class="token string">"@utils/ReactMarkdownRenderers"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span><br>  formatPublishedDateForDateTime<span class="token punctuation">,</span><br>  formatPublishedDateForDisplay<span class="token punctuation">,</span><br><span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@utils/Date"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">PostList</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> posts <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>      <span class="token operator">&lt;</span>ol<span class="token operator">></span><br>        <span class="token punctuation">{</span>posts<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">post</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><br>          <span class="token operator">&lt;</span>li key<span class="token operator">=</span><span class="token punctuation">{</span>post<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token punctuation">}</span><span class="token operator">></span><br>            <span class="token operator">&lt;</span>article<span class="token operator">></span><br>              <span class="token operator">&lt;</span>time dateTime<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">formatPublishedDateForDateTime</span><span class="token punctuation">(</span>date<span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">></span><br>                <span class="token punctuation">{</span><span class="token function">formatPublishedDateForDisplay</span><span class="token punctuation">(</span>date<span class="token punctuation">)</span><span class="token punctuation">}</span><br>              <span class="token operator">&lt;</span><span class="token operator">/</span>time<span class="token operator">></span><br><br>              <span class="token operator">&lt;</span>Link href<span class="token operator">=</span><span class="token punctuation">{</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">blog/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>post<span class="token punctuation">.</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">}</span><span class="token operator">></span><br>                <span class="token operator">&lt;</span>a<span class="token operator">></span><br>                  <span class="token operator">&lt;</span>h2<span class="token operator">></span><span class="token punctuation">{</span>post<span class="token punctuation">.</span>title<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>h2<span class="token operator">></span><br>                <span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><br>              <span class="token operator">&lt;</span><span class="token operator">/</span>Link<span class="token operator">></span><br><br>              <span class="token operator">&lt;</span>ul<span class="token operator">></span><br>                <span class="token punctuation">{</span>tags<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">tag</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">(</span><br>                  <span class="token operator">&lt;</span>li key<span class="token operator">=</span><span class="token punctuation">{</span>tag<span class="token punctuation">}</span><span class="token operator">></span><span class="token punctuation">{</span>tag<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>li<span class="token operator">></span><br>                <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><br>              <span class="token operator">&lt;</span><span class="token operator">/</span>ul<span class="token operator">></span><br><br>              <span class="token operator">&lt;</span>ReactMarkdown<br>                children<span class="token operator">=</span><span class="token punctuation">{</span>post<span class="token punctuation">.</span>excerpt<span class="token punctuation">}</span><br>                renderers<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">ReactMarkdownRenderers</span><span class="token punctuation">(</span>post<span class="token punctuation">.</span>excerpt<span class="token punctuation">)</span><span class="token punctuation">}</span><br>              <span class="token operator">/</span><span class="token operator">></span><br>            <span class="token operator">&lt;</span><span class="token operator">/</span>article<span class="token operator">></span><br>          <span class="token operator">&lt;</span><span class="token operator">/</span>li<span class="token operator">></span><br>        <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>ol<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Render your <code>postList</code> in your <code>BlogIndex</code> and <code>BlogIndexPage</code> components like so. Pass the <code>totalPages</code> and <code>currentPage</code> props in, too, as we’ll be using them in the final part of this guide.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="QKUzAflDoE"
      aria-describedby="QKUzAflDoE">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="QKUzAflDoE">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="QKUzAflDoE" itemprop="text" content="%2F%2F%20%2Fpages%2Fblog%2Findex.js%0A%2F%2F%20Do%20the%20same%20for%20%2Fpages%2Fblog%2Fpage%2F%5Bpage%5D.js%0A%0Aimport%20PostList%20from%20%22%40components%2FPostList%22%3B%0A%0Aexport%20default%20function%20BlogIndex(props)%20%7B%0A%20%20const%20%7B%20postSummaries%2C%20currentPage%2C%20totalPages%20%7D%20%3D%20props%3B%0A%0A%20%20return%20(%0A%20%20%20%20%20%20%20%20%3CPostList%20%0A%20%20%20%20%20%20%20%20%20%20%20%20posts%3D%7BpostSummaries%7D%20%0A%20%20%20%20%20%20%20%20%20%20%20%20totalPages%3D%7BtotalPages%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20currentPage%3D%7BcurrentPage%7D%0A%20%20%20%20%20%20%20%2F%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /pages/blog/index.js</span><br><span class="token comment">// Do the same for /pages/blog/page/[page].js</span><br><br><span class="token keyword">import</span> PostList <span class="token keyword">from</span> <span class="token string">"@components/PostList"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">BlogIndex</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> postSummaries<span class="token punctuation">,</span> currentPage<span class="token punctuation">,</span> totalPages <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>        <span class="token operator">&lt;</span>PostList <br>            posts<span class="token operator">=</span><span class="token punctuation">{</span>postSummaries<span class="token punctuation">}</span> <br>            totalPages<span class="token operator">=</span><span class="token punctuation">{</span>totalPages<span class="token punctuation">}</span><br>            currentPage<span class="token operator">=</span><span class="token punctuation">{</span>currentPage<span class="token punctuation">}</span><br>       <span class="token operator">/</span><span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">You should now have your post list rendering on <code>/blog</code> and <code>/blog/page/2</code>. There’s one more piece to the puzzle! Let’s build a component to navigate back and forth in our pagination.</p><h2 class="post__h2">Building the pagination component</h2><p class="post__p">We’re going to make our lives really easy here! To ensure that our application can scale nicely and that we don’t have to battle with displaying or truncating a million page numbers when we’ve written a bazillion blog posts, we will be rendering only three UI elements inside our pagination component:</p><ul><li><p class="post__p">A “previous page” link</p></li><li><p class="post__p">A current page / total pages indicator</p></li><li><p class="post__p">A “next page” link</p></li></ul><p class="post__p">Inside <code>components/PostList</code>, add a new directory called <code>Pagination</code>. Inside that directory, add a new file called <code>index.js</code>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/QShpFM6zTLoe3Vu3sqWvY/c18d3f1b1d8088941c9f89df45ed9645/pagination_screenshot.png" alt="Screenshot of the pagination component created in the components directory in VSCode" height="224" width="459" /><p class="post__p">Add the following code to <code>index.js</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="AmADhBQYZe"
      aria-describedby="AmADhBQYZe">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="AmADhBQYZe">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="AmADhBQYZe" itemprop="text" content="%2F%2F%20%2Fcomponents%2FPostList%2FPagination%2Findex.js%0A%0Aimport%20Link%20from%20%22next%2Flink%22%3B%0A%0Aexport%20default%20function%20Pagination(props)%20%7B%0A%20%20const%20%7B%20totalPages%2C%20currentPage%2C%20prevDisabled%2C%20nextDisabled%20%7D%20%3D%20props%3B%0A%0A%20%20const%20prevPageUrl%20%3D%0A%20%20%20%20currentPage%20%3D%3D%3D%20%222%22%0A%20%20%20%20%20%20%3F%20%22%2Fblog%22%0A%20%20%20%20%20%20%3A%20%60%2Fblog%2Fpage%2F%24%7BparseInt(currentPage%2C%2010)%20-%201%7D%60%3B%0A%0A%20%20const%20nextPageUrl%20%3D%20%60%2Fblog%2Fpage%2F%24%7BparseInt(currentPage%2C%2010)%20%2B%201%7D%60%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3Col%3E%0A%20%20%20%20%20%20%3Cli%3E%0A%20%20%20%20%20%20%20%20%7BprevDisabled%20%26%26%20%3Cspan%3EPrevious%20page%3C%2Fspan%3E%7D%0A%20%20%20%20%20%20%20%20%7B!prevDisabled%20%26%26%20(%0A%20%20%20%20%20%20%20%20%20%20%3CLink%20href%3D%7BprevPageUrl%7D%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Ca%3EPrevious%20page%3C%2Fa%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2FLink%3E%0A%20%20%20%20%20%20%20%20)%7D%0A%20%20%20%20%20%20%3C%2Fli%3E%0A%20%20%20%20%20%20%3Cli%3E%0A%20%20%20%20%20%20%20%20Page%20%7BcurrentPage%7D%20of%20%7BtotalPages%7D%0A%20%20%20%20%20%20%3C%2Fli%3E%0A%20%20%20%20%20%20%3Cli%3E%0A%20%20%20%20%20%20%20%20%7BnextDisabled%20%26%26%20%3Cspan%3ENext%20page%3C%2Fspan%3E%7D%0A%20%20%20%20%20%20%20%20%7B!nextDisabled%20%26%26%20(%0A%20%20%20%20%20%20%20%20%20%20%3CLink%20href%3D%7BnextPageUrl%7D%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Ca%3ENext%20page%3C%2Fa%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2FLink%3E%0A%20%20%20%20%20%20%20%20)%7D%0A%20%20%20%20%20%20%3C%2Fli%3E%0A%20%20%20%20%3C%2Fol%3E%0A%20%20)%3B%0A%7D%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /components/PostList/Pagination/index.js</span><br><br><span class="token keyword">import</span> Link <span class="token keyword">from</span> <span class="token string">"next/link"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">Pagination</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> totalPages<span class="token punctuation">,</span> currentPage<span class="token punctuation">,</span> prevDisabled<span class="token punctuation">,</span> nextDisabled <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> prevPageUrl <span class="token operator">=</span><br>    currentPage <span class="token operator">===</span> <span class="token string">"2"</span><br>      <span class="token operator">?</span> <span class="token string">"/blog"</span><br>      <span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/blog/page/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">parseInt</span><span class="token punctuation">(</span>currentPage<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> nextPageUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/blog/page/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">parseInt</span><span class="token punctuation">(</span>currentPage<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>ol<span class="token operator">></span><br>      <span class="token operator">&lt;</span>li<span class="token operator">></span><br>        <span class="token punctuation">{</span>prevDisabled <span class="token operator">&amp;&amp;</span> <span class="token operator">&lt;</span>span<span class="token operator">></span>Previous page<span class="token operator">&lt;</span><span class="token operator">/</span>span<span class="token operator">></span><span class="token punctuation">}</span><br>        <span class="token punctuation">{</span><span class="token operator">!</span>prevDisabled <span class="token operator">&amp;&amp;</span> <span class="token punctuation">(</span><br>          <span class="token operator">&lt;</span>Link href<span class="token operator">=</span><span class="token punctuation">{</span>prevPageUrl<span class="token punctuation">}</span><span class="token operator">></span><br>            <span class="token operator">&lt;</span>a<span class="token operator">></span>Previous page<span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><br>          <span class="token operator">&lt;</span><span class="token operator">/</span>Link<span class="token operator">></span><br>        <span class="token punctuation">)</span><span class="token punctuation">}</span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>li<span class="token operator">></span><br>      <span class="token operator">&lt;</span>li<span class="token operator">></span><br>        Page <span class="token punctuation">{</span>currentPage<span class="token punctuation">}</span> <span class="token keyword">of</span> <span class="token punctuation">{</span>totalPages<span class="token punctuation">}</span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>li<span class="token operator">></span><br>      <span class="token operator">&lt;</span>li<span class="token operator">></span><br>        <span class="token punctuation">{</span>nextDisabled <span class="token operator">&amp;&amp;</span> <span class="token operator">&lt;</span>span<span class="token operator">></span>Next page<span class="token operator">&lt;</span><span class="token operator">/</span>span<span class="token operator">></span><span class="token punctuation">}</span><br>        <span class="token punctuation">{</span><span class="token operator">!</span>nextDisabled <span class="token operator">&amp;&amp;</span> <span class="token punctuation">(</span><br>          <span class="token operator">&lt;</span>Link href<span class="token operator">=</span><span class="token punctuation">{</span>nextPageUrl<span class="token punctuation">}</span><span class="token operator">></span><br>            <span class="token operator">&lt;</span>a<span class="token operator">></span>Next page<span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><br>          <span class="token operator">&lt;</span><span class="token operator">/</span>Link<span class="token operator">></span><br>        <span class="token punctuation">)</span><span class="token punctuation">}</span><br>      <span class="token operator">&lt;</span><span class="token operator">/</span>li<span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>ol<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">We’re using the <code>next/link</code> component to make use of client-side routing, and we’re calculating the links to the next and previous pages based on the <code>currentPage</code> prop.</p><p class="post__p">Import the <code>Pagination</code> component at the top of the <code>PostList</code>file, and add it at the end of the template rendering the HTML. Pass in the <code>totalPages</code> and <code>currentPages</code> props.</p><p class="post__p">Next, calculate the <code>nextDisabled</code> and <code>prevDisabled</code> variables based on the <code>currentPage</code> and <code>totalPages</code>:</p><ul><li><p class="post__p">If we’re on the page one, <code>prevDisabled</code> = true</p></li><li><p class="post__p">If we’re on the last page, <code>nextDisabled</code> = true</p></li></ul><p class="post__p">Finally, pass these two props to the <code>Pagination</code> component.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ypenipeTJX"
      aria-describedby="ypenipeTJX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ypenipeTJX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="ypenipeTJX" itemprop="text" content="%2F%2F%20%2Fcomponents%2FPostList%2Findex.js%0A%0Aimport%20Pagination%20from%20%22%40components%2FPostList%2FPagination%22%3B%0A%0Aexport%20default%20function%20PostList(props)%20%7B%0A%20%2F%2F%20Remember%20to%20take%20the%20currentPage%20and%20totalPages%20from%20props%20passed%0A%20%2F%2F%20from%20the%20BlogIndex%20and%20BlogIndexPage%20components%0A%20%20const%20%7B%20posts%2C%20currentPage%2C%20totalPages%20%7D%20%3D%20props%3B%0A%0A%20%2F%2F%20Calculate%20the%20disabled%20states%20of%20the%20next%20and%20previous%20links%0A%20%20const%20nextDisabled%20%3D%20parseInt(currentPage%2C%2010)%20%3D%3D%3D%20parseInt(totalPages%2C%2010)%3B%0A%20%20const%20prevDisabled%20%3D%20parseInt(currentPage%2C%2010)%20%3D%3D%3D%201%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3C%3E%0A%0A%20%20%20%20%20%20%2F%2F%20Post%20list%20%3Col%3E...%0A%0A%20%20%20%20%20%20%3CPagination%0A%20%20%20%20%20%20%20%20totalPages%3D%7BtotalPages%7D%0A%20%20%20%20%20%20%20%20currentPage%3D%7BcurrentPage%7D%0A%20%20%20%20%20%20%20%20nextDisabled%3D%7BnextDisabled%7D%0A%20%20%20%20%20%20%20%20prevDisabled%3D%7BprevDisabled%7D%0A%20%20%20%20%20%20%2F%3E%0A%20%20%20%20%3C%2F%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// /components/PostList/index.js</span><br><br><span class="token keyword">import</span> Pagination <span class="token keyword">from</span> <span class="token string">"@components/PostList/Pagination"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">PostList</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br> <span class="token comment">// Remember to take the currentPage and totalPages from props passed</span><br> <span class="token comment">// from the BlogIndex and BlogIndexPage components</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> posts<span class="token punctuation">,</span> currentPage<span class="token punctuation">,</span> totalPages <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br> <span class="token comment">// Calculate the disabled states of the next and previous links</span><br>  <span class="token keyword">const</span> nextDisabled <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>currentPage<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>totalPages<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">const</span> prevDisabled <span class="token operator">=</span> <span class="token function">parseInt</span><span class="token punctuation">(</span>currentPage<span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token number">1</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span><span class="token operator">></span><br><br>      <span class="token comment">// Post list &lt;ol>...</span><br><br>      <span class="token operator">&lt;</span>Pagination<br>        totalPages<span class="token operator">=</span><span class="token punctuation">{</span>totalPages<span class="token punctuation">}</span><br>        currentPage<span class="token operator">=</span><span class="token punctuation">{</span>currentPage<span class="token punctuation">}</span><br>        nextDisabled<span class="token operator">=</span><span class="token punctuation">{</span>nextDisabled<span class="token punctuation">}</span><br>        prevDisabled<span class="token operator">=</span><span class="token punctuation">{</span>prevDisabled<span class="token punctuation">}</span><br>      <span class="token operator">/</span><span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And that’s it! You’ve built statically generated article list pages based on the number of blog posts in the example Contentful space and how many posts you’d like to show per article list page. </p><h2 class="post__h2">The finished product</h2><p class="post__p">In this tutorial we built statically generated article list pagination using data from Contentful in a fresh Next.js application. You can find the final styled result <a href="https://nextjs-contentful-blog-starter.vercel.app/blog" target="_blank">here</a>, and here’s how it looks.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7Dvj1VssGFdwTjzBQJE0uv/d0744de226023942cbc06814c648d298/blog_index_screenshot.png" alt="Screenshot of the blog index from the Next.js and Contentful starter example website" height="1288" width="999" /><p class="post__p">If you want to look at how the demo site is styled with CSS, <a href="https://github.com/whitep4nth3r/nextjs-contentful-blog-starter/tree/main/styles" target="_blank">take a look at these files on GitHub</a>.</p><p class="post__p">If you’ve set up a <a href="https://www.contentful.com/developers/docs/concepts/webhooks/" target="_blank">webhook inside Contentful</a> to trigger a build every time you publish a change, your article list pages will be rebuilt, and continue to generate the <code>/blog/page/{pageNumber}</code> routes dynamically based on how many blog post entries you have!</p><p class="post__p">If you’ve found this guide useful, I’d love for you to come and say hi on <a href="https://twitch.tv/whitep4nth3r" target="_blank">Twitch</a>, where I code live three times a week. I built this code on stream!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Exploring linked entries and assets in Contentful with JavaScript via REST and GraphQL</title>
          <description>An investigation into the inner workings of the Contentful REST API and GraphQL API.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/04/14/exploring-linked-entries-assets-contentful/</link>
          <guid>https://www.contentful.com/blog/2021/04/14/exploring-linked-entries-assets-contentful/</guid>
          <pubDate>Tue, 13 Apr 2021 23:00:00 GMT</pubDate>
          <category>GraphQL</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">The most frequently asked questions in DevRel when I started with Contentful were about how to display links or linked entries and assets inside the <a href="https://www.contentful.com/developers/docs/concepts/rich-text/" target="_blank">Contentful Rich Text</a> field on the front end. It’s no secret that those of you who tuned in to my <a href="https://www.twitch.tv/whitep4nth3r" target="_blank">Twitch streams</a> after I had started at Contentful saw me struggle with the concept of links as well! So, I set out to explore and investigate the inner workings of the Contentful REST API and GraphQL API in terms of linking assets and entries on a content type in order to understand how we can render links inside Contentful Rich Text fields.</p><h2 class="post__h2">What are links in Contentful?</h2><p class="post__p">If you’re looking for information on how to render linked assets and entries returned as part of the Contentful Rich Text field response using REST or GraphQL in JavaScript, <a href="https://www.contentful.com/blog/2021/04/14/rendering-linked-assets-entries-in-contentful/" target="_blank">check out this post</a>.</p><p class="post__p">Links are Contentful’s way of modelling relationships between content types and entries. Entries in Contentful can contain link fields that point to other assets or entries, and those entries can link to other assets or entries, and so on. For example:</p><ul><li><p class="post__p">A blog post can have an author </p></li><li><p class="post__p">A team can have many authors</p></li><li><p class="post__p">A company can have many teams</p></li></ul><p class="post__p">You can liken this to working with relational databases, where you would define one to one or one to many relationships within your data structures or models. For more information on the concept of links in Contentful, <a href="https://www.contentful.com/developers/docs/concepts/links/" target="_blank">visit the documentation</a>.</p><p class="post__p">Here’s the content model that we’ll be working with in this article. The screenshot of the blog post content model below shows that the Author field is a Reference field type, which is a link.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7aZSO2RGG7r9r7ry0L7i0W/37a956aff21615d65928df5f91291789/blog_post_content_model.png" alt="A screenshot of a blog post content model in the Contentful Web App" height="837" width="830" /><h2 class="post__h2">TL;DR:</h2><p class="post__p">If you’re using the <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/" target="_blank">Content Delivery</a> and <a href="https://www.contentful.com/developers/docs/references/content-preview-api/" target="_blank">Content Preview</a> REST API, Contentful provides a number of SDKs (Software Development Kits) in the most popular programming languages. These will resolve your linked entries and assets for you. In this example, we’ll be taking a look at the <a href="https://www.contentful.com/developers/docs/javascript/sdks/" target="_blank">JavaScript SDK</a>. </p><p class="post__p">If you’re using the GraphQL API, you control how your entries are resolved in the construction of your GraphQL query. And by understanding how the REST API works and how the SDKs resolve links, you’ll be all set.</p><p class="post__p">Let’s take a look!</p><h2 class="post__h2">Requesting data from Contentful</h2><p class="post__p">The following examples focus on using the JavaScript ecosystem to query data from <a href="https://nextjs-contentful-blog-starter.vercel.app/blog/the-power-of-the-contentful-rich-text-field" target="_blank">this example blog post</a>. The example blog post is served on an application built with Next.js — but we won’t be going into Next.js in this post.</p><h2 class="post__h2">Using the REST API</h2><p class="post__p">Take this example request URL.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="QMIaKOYCGc"
      aria-describedby="QMIaKOYCGc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="QMIaKOYCGc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="QMIaKOYCGc" itemprop="text" content="https%3A%2F%2Fcdn.contentful.com%2Fspaces%2F%7B%7BspaceId%7D%7D%2Fenvironments%2Fmaster%2Fentries%3Faccess_token%3D%7B%7BaccessToken%7D%7D%26content_type%3DblogPost%26fields.slug%3Dthe-power-of-the-contentful-rich-text-field%26include%3D10">
      <pre class="language-bash"><code class="language-bash">https://cdn.contentful.com/spaces/<span class="token punctuation">{</span><span class="token punctuation">{</span>spaceId<span class="token punctuation">}</span><span class="token punctuation">}</span>/environments/master/entries?access_token<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">{</span>accessToken<span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&amp;</span><span class="token assign-left variable">content_type</span><span class="token operator">=</span>blogPost<span class="token operator">&amp;</span><span class="token assign-left variable">fields.slug</span><span class="token operator">=</span>the-power-of-the-contentful-rich-text-field<span class="token operator">&amp;</span><span class="token assign-left variable">include</span><span class="token operator">=</span><span class="token number">10</span></code></pre>
    </div>
  </div>

  <p class="post__p">It is querying the Contentful Delivery API with the following parameters:</p><ul><li><p class="post__p"><b class="post__p--bold">spaceId</b>: Our space ID</p></li><li><p class="post__p"><b class="post__p--bold">accessToken</b>: Our access token for the Content Delivery API</p></li><li><p class="post__p"><b class="post__p--bold">content_type</b>: blogPost</p></li><li><p class="post__p"><b class="post__p--bold">fields.slug</b>: the-power-of-the-contentful-rich-text-field (return the blogPost entry that has this slug)</p></li><li><p class="post__p"><b class="post__p--bold">include</b>: 10 (return linked entries and assets up to 10 levels deep (this is the maximum <code>include</code> parameter value on the Content Delivery API) - <b class="post__p--bold">we will unpack this later!)</b></p></li></ul><h3 class="post__h3">The REST API response</h3><p class="post__p">The raw JSON response from the request above contains the following top level properties and nodes in a flat structure.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="IRbfkxVJiX"
      aria-describedby="IRbfkxVJiX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="IRbfkxVJiX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="IRbfkxVJiX" itemprop="text" content="%7B%0A%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%22type%22%3A%20%22Array%22%0A%20%20%7D%2C%0A%20%20%22total%22%3A%201%2C%0A%20%20%22skip%22%3A%200%2C%0A%20%20%22limit%22%3A%20100%2C%0A%20%20%22items%22%3A%20%5B...%5D%2C%0A%20%20%22includes%3A%20%7B...%7D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Array"</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token property">"total"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br>  <span class="token property">"skip"</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span><br>  <span class="token property">"limit"</span><span class="token operator">:</span> <span class="token number">100</span><span class="token punctuation">,</span><br>  <span class="token property">"items"</span><span class="token operator">:</span> <span class="token punctuation">[</span>...<span class="token punctuation">]</span><span class="token punctuation">,</span><br>  "includes<span class="token operator">:</span> <span class="token punctuation">{</span>...<span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">The items array</h3><p class="post__p"><code>items</code> contains the requested entries (the entry with the matching slug in this case). Each entry contains a subset of the <code>fields</code> defined on the content type of this entry and some internal system information (<code>sys</code>). Notice how the linked <code>author</code> entry is missing the <code>fields</code> property. It only holds the <code>sys</code> information including the <code>linkType</code> and <code>id</code>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="uYQNizhRaz"
      aria-describedby="uYQNizhRaz">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="uYQNizhRaz">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="uYQNizhRaz" itemprop="text" content="%22items%22%3A%20%5B%0A%20%20%7B%0A%20%20%20%20%22sys%22%3A%20%7B...%7D%2C%0A%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%22title%22%3A%20%22...%22%2C%0A%20%20%20%20%20%20%22slug%22%3A%20%22the-power-of-the-contentful-rich-text-field%22%2C%0A%20%20%20%20%20%20%22author%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%23%20This%20is%20a%20%22Link%22%0A%20%20%20%20%20%20%20%20%23%20and%20contains%20only%20a%20reference%20to%20the%20Author%20entry%0A%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22Entry%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22123456789%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%7D%0A%20%20%7D%0A%5D">
      <pre class="language-json"><code class="language-json"><span class="token property">"items"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>  <span class="token punctuation">{</span><br>    <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span>...<span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"..."</span><span class="token punctuation">,</span><br>      <span class="token property">"slug"</span><span class="token operator">:</span> <span class="token string">"the-power-of-the-contentful-rich-text-field"</span><span class="token punctuation">,</span><br>      <span class="token property">"author"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        # This is a <span class="token string">"Link"</span><br>        # and contains only a reference to the Author entry<br>        <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>          <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>          <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"123456789"</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">]</span></code></pre>
    </div>
  </div>

  <p class="post__p">Where are the author fields? Let’s find out!</p><h3 class="post__h3">The includes object</h3><p class="post__p">The <code>includes</code> object contains two array nodes:</p><ol><li><p class="post__p"><code>&quot;Entry&quot;</code> for all referenced <b class="post__p--bold">entries</b> in <code>items</code> (such as the the blog post author which we saw returned as a <code>“type”: “Link”</code> in the response above)</p></li><li><p class="post__p"><code>&quot;Asset&quot;</code> for all referenced <b class="post__p--bold">assets</b> in <code>items</code> (such as images, which might be a featured image on a blog post, for example)</p></li></ol><p class="post__p">In the case of the <code>author</code>, which is a linked entry on our <code>blogPost</code>, we see the full author object returned in <code>includes.Entry[0]</code> — including another link to an image asset.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="zigShlhIcF"
      aria-describedby="zigShlhIcF">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="zigShlhIcF">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="zigShlhIcF" itemprop="text" content="%22includes%22%3A%20%7B%0A%20%22Entry%22%3A%20%5B%0A%20%20%7B%0A%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%22space%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%20%2F%2F...%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%22id%22%3A%20%22123456789%22%2C%0A%20%20%20%20%20%20%22type%22%3A%20%22Entry%22%2C%0A%20%20%20%20%20%20%22createdAt%22%3A%20%22...%22%2C%0A%20%20%20%20%20%20%22updatedAt%22%3A%20%22...%22%2C%0A%20%20%20%20%20%20%22environment%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%20%2F%2F...%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%22revision%22%3A%201%2C%0A%20%20%20%20%20%20%22contentType%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22ContentType%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22person%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%22locale%22%3A%20%22en-US%22%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%22image%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%23%20Here%E2%80%99s%20another%20link%20that%20we%20didn%E2%80%99t%20find%20in%20the%20items%20array%0A%20%20%20%20%20%20%20%20%20%20%23%20due%20to%20it%20being%20nested%20deeper%20than%201%20level%20in%20the%20object%20tree%0A%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22Asset%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22555555555%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%22name%22%3A%20%22Salma%20Alam-Naylor%22%2C%0A%20%20%20%20%20%20%22description%22%3A%20%22This%20is%20the%20author%20description%22%2C%0A%20%20%20%20%20%20%22twitterUsername%22%3A%20%22whitep4nth3r%22%2C%0A%20%20%20%20%20%20%22gitHubUsername%22%3A%20%22whitep4nth3r%22%2C%0A%20%20%20%20%20%20%22twitchUsername%22%3A%20%22whitep4nth3r%22%2C%0A%20%20%20%20%20%20%22websiteUrl%22%3A%20%22https%3A%2F%2Fwhitep4nth3r.com%22%0A%20%20%20%20%7D%0A%20%20%7D%2C%0A%20%5D%0A%7D%0A">
      <pre class="language-json"><code class="language-json"><span class="token property">"includes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br> <span class="token property">"Entry"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>  <span class="token punctuation">{</span><br>    <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token property">"space"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">//... }</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"123456789"</span><span class="token punctuation">,</span><br>      <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>      <span class="token property">"createdAt"</span><span class="token operator">:</span> <span class="token string">"..."</span><span class="token punctuation">,</span><br>      <span class="token property">"updatedAt"</span><span class="token operator">:</span> <span class="token string">"..."</span><span class="token punctuation">,</span><br>      <span class="token property">"environment"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">//... }</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token property">"revision"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br>      <span class="token property">"contentType"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>          <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"ContentType"</span><span class="token punctuation">,</span><br>          <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"person"</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token property">"locale"</span><span class="token operator">:</span> <span class="token string">"en-US"</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token property">"image"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          # Here’s another link that we didn’t find in the items array<br>          # due to it being nested deeper than <span class="token number">1</span> level in the object tree<br>          <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>          <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"Asset"</span><span class="token punctuation">,</span><br>          <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"555555555"</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Salma Alam-Naylor"</span><span class="token punctuation">,</span><br>      <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"This is the author description"</span><span class="token punctuation">,</span><br>      <span class="token property">"twitterUsername"</span><span class="token operator">:</span> <span class="token string">"whitep4nth3r"</span><span class="token punctuation">,</span><br>      <span class="token property">"gitHubUsername"</span><span class="token operator">:</span> <span class="token string">"whitep4nth3r"</span><span class="token punctuation">,</span><br>      <span class="token property">"twitchUsername"</span><span class="token operator">:</span> <span class="token string">"whitep4nth3r"</span><span class="token punctuation">,</span><br>      <span class="token property">"websiteUrl"</span><span class="token operator">:</span> <span class="token string">"https://whitep4nth3r.com"</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br> <span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">The response includes all the data that you need to render the blog post to the front end. However, the data is spread across <code>items</code> and <code>includes</code>, and you — as a developer — would expect all that data to be returned as one object, right? 🤯 </p><p class="post__p">For example, in React, you might want to do something like this to show the author’s name on the front end:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="caDSNvuTZj"
      aria-describedby="caDSNvuTZj">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="caDSNvuTZj">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="caDSNvuTZj" itemprop="text" content="export%20default%20function%20BlogPost(props)%20%7B%0A%20%20const%20%7B%20blogPost%20%7D%20%3D%20props%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3Cdiv%3E%0A%20%20%20%20%20%20%3Ch1%3E%7BblogPost.fields.title%7D%3C%2Fh1%3E%0A%20%20%20%20%20%20%3Ch2%3EBy%20%7BblogPost.fields.author.name%7D%3C%2Fh2%3E%0A%20%20%20%20%3C%2Fdiv%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">BlogPost</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> blogPost <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>div<span class="token operator">></span><br>      <span class="token operator">&lt;</span>h1<span class="token operator">></span><span class="token punctuation">{</span>blogPost<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>title<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>h1<span class="token operator">></span><br>      <span class="token operator">&lt;</span>h2<span class="token operator">></span>By <span class="token punctuation">{</span>blogPost<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>author<span class="token punctuation">.</span>name<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>h2<span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>div<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">However, we need to do some more work before we can make this happen — we need to <b class="post__p--bold">resolve the linked entries </b>— and this is where we can use the Contentful JavaScript SDK.</p><p class="post__p">Currently, the blogPost item references the author by <code>sys.id</code>:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="rkvJVFsqVX"
      aria-describedby="rkvJVFsqVX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="rkvJVFsqVX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="rkvJVFsqVX" itemprop="text" content="%22author%22%3A%20%7B%0A%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%22linkType%22%3A%20%22Entry%22%2C%0A%20%20%20%20%22id%22%3A%20%22123456789%22%0A%20%20%7D%0A%7D%2C">
      <pre class="language-json"><code class="language-json"><span class="token property">"author"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>    <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"123456789"</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre>
    </div>
  </div>

  <p class="post__p">You <b class="post__p--italic">could</b> cross-reference the <code>items[0].fields.author.sys.id</code> with the <code>includes.Entry</code> array, find the item in the array that has the <code>id</code> that matches, and resolve the data from there. It sounds pretty straightforward in this example, but when your content model gets more complex with many entries linking to other entries, it could get unwieldy.</p><p class="post__p">Let’s look at how the <a href="https://www.contentful.com/developers/docs/javascript/sdks/" target="_blank">JavaScript SDK</a> can help us out.</p><p class="post__p">Under the hood, the JavaScript SDK uses the <a href="https://github.com/contentful/contentful-resolve-response" target="_blank">contentful-resolve-response</a> package, which converts the raw nodes into a rich tree of data. <b class="post__p--italic">The one limitation of the Contentful Delivery API to bear in mind is that it will only return linked entries up to a maximum of 10 levels deep that can be resolved.</b></p><img src="https://images.ctfassets.net/56dzm01z6lln/4LE3OwpLzzbSr6aTUPUvKE/ed0c3a32473358043a62c708d72ead04/TransformedContentTree.png" alt="Graph representing how the rest API response feeds into the transformed content tree" height="2011" width="2560" /><h3 class="post__h3">Unpacking the include request parameter</h3><p class="post__p">Specify the depth of the resolved tree using the <code>include</code> parameter in the request to the API, either as a parameter on the GET request URL, like this:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="fetiuHQjax"
      aria-describedby="fetiuHQjax">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="fetiuHQjax">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="fetiuHQjax" itemprop="text" content="https%3A%2F%2Fcdn.contentful.com%2Fspaces%2F%7B%7BspaceId%7D%7D%2Fenvironments%2Fmaster%2Fentries%3Faccess_token%3D%7B%7BaccessToken%7D%7D%26content_type%3DblogPost%26fields.slug%3Dthe-power-of-the-contentful-rich-text-field%26limit%3D1%26include%3D10">
      <pre class="language-bash"><code class="language-bash">https://cdn.contentful.com/spaces/<span class="token punctuation">{</span><span class="token punctuation">{</span>spaceId<span class="token punctuation">}</span><span class="token punctuation">}</span>/environments/master/entries?access_token<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">{</span>accessToken<span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&amp;</span><span class="token assign-left variable">content_type</span><span class="token operator">=</span>blogPost<span class="token operator">&amp;</span><span class="token assign-left variable">fields.slug</span><span class="token operator">=</span>the-power-of-the-contentful-rich-text-field<span class="token operator">&amp;</span><span class="token assign-left variable">limit</span><span class="token operator">=</span><span class="token number">1</span><span class="token operator">&amp;</span><span class="token assign-left variable">include</span><span class="token operator">=</span><span class="token number">10</span></code></pre>
    </div>
  </div>

  <p class="post__p">or via a call to the JavaScript SDK:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="aLgrEQnSrD"
      aria-describedby="aLgrEQnSrD">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="aLgrEQnSrD">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="aLgrEQnSrD" itemprop="text" content="const%20post%20%3D%20await%20client%0A%20%20.getEntries(%7B%0A%20%20%20%20content_type%3A%20%22blogPost%22%2C%0A%20%20%20%20limit%3A%201%2C%0A%20%20%20%20include%3A%2010%2C%0A%20%20%20%20%22fields.slug%22%3A%20%22the-power-of-the-contentful-rich-text-field%22%2C%0A%20%20%7D)%0A%20%20.then((entry)%20%3D%3E%20entry)%0A%20%20.catch(console.error)%3B%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> post <span class="token operator">=</span> <span class="token keyword">await</span> client<br>  <span class="token punctuation">.</span><span class="token function">getEntries</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">content_type</span><span class="token operator">:</span> <span class="token string">"blogPost"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">limit</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">include</span><span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"fields.slug"</span><span class="token operator">:</span> <span class="token string">"the-power-of-the-contentful-rich-text-field"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">entry</span><span class="token punctuation">)</span> <span class="token operator">=></span> entry<span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span>console<span class="token punctuation">.</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">Both examples above make the same request to the Contentful API — except the SDK example is resolving your linked entries as part of the process using <a href="https://github.com/contentful/contentful-resolve-response" target="_blank">contentful-resolve-response</a>. Neat!</p><h3 class="post__h3">How the include parameter affects the length of the includes response</h3><p class="post__p">Say you have a blog post, which has a reference to an author, which has a reference to a team.</p><p class="post__p">To visualise this in an object graph:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VAXDAdybei"
      aria-describedby="VAXDAdybei">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VAXDAdybei">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="VAXDAdybei" itemprop="text" content="%7B%0A%20%20%22blogPost%22%3A%20%7B%0A%20%20%20%20%2F%2F...%0A%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%22author%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F...%0A%20%20%20%20%20%20%20%20%20%20%22team%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F...%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"blogPost"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token comment">//...</span><br>    <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>       <span class="token property">"author"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token comment">//...</span><br>          <span class="token property">"team"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token comment">//...</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">If you specify <code>includes=1</code> in your request, your <code>includes</code> array on the response will contain one item in this example, the <code>author</code> object (1 level deep).</p><p class="post__p">If you specify <code>includes=2</code> in your request, your <code>includes</code> array on the response will contain two items, the <code>author</code> object and the <code>team</code> object. (2 levels deep).</p><p class="post__p">If your <code>blogPost</code> had another top level reference, say a <code>heroBanner</code>, <code>includes=1</code> would return both the <code>author</code> and <code>heroBanner</code> inside the <code>includes</code> array.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="NmDZLmQpjg"
      aria-describedby="NmDZLmQpjg">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="NmDZLmQpjg">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="NmDZLmQpjg" itemprop="text" content="%7B%0A%20%20%22blogPost%22%3A%20%7B%0A%20%20%20%20%2F%2F...%0A%0A%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%2F%2F...%0A%0A%20%20%20%20%20%20%22heroBanner%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F...%0A%20%20%20%20%20%20%7D%2C%0A%0A%20%20%20%20%20%20%22author%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F...%0A%0A%20%20%20%20%20%20%20%20%22team%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%2F%2F...%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"blogPost"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token comment">//...</span><br><br>    <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token comment">//...</span><br><br>      <span class="token property">"heroBanner"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token comment">//...</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>      <span class="token property">"author"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token comment">//...</span><br><br>        <span class="token property">"team"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token comment">//...</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Regardless of the <code>include</code> depth you specify — the SDK — which uses the <a href="https://github.com/contentful/contentful-resolve-response" target="_blank">contentful-resolve-response</a> package, will link all available and responded entries and assets that are returned in the <code>includes</code> response.</p><p class="post__p"><a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/links" target="_blank">Read more about the include param on the Contentful docs</a>.</p><h2 class="post__h2">Using the GraphQL API</h2><p class="post__p"><a href="https://www.contentful.com/developers/docs/references/graphql/" target="_blank">The Contentful GraphQL API</a> doesn’t require an SDK to handle linked entries — but understanding the concepts covered previously helps us out here.</p><h3 class="post__h3">The main differences between the REST API and GraphQL API</h3><ul><li><p class="post__p">The response from the GraphQL API gives you a rich object graph as standard (so you won’t find <code>includes</code> in the response).</p></li><li><p class="post__p">With GraphQL you specify the equivalent depth of the <code>includes</code> response <b class="post__p--italic">through the construction of your query</b>. The only limit here is the complexity of your GraphQL query. Technically, if you construct your query cleverly, you can reach data hundreds of levels deep! <a href="https://www.contentful.com/developers/docs/references/graphql/#/introduction/query-complexity-limits" target="_blank">Read more about GraphQL complexity limits here</a>.</p></li></ul><p class="post__p">Here’s the GraphQL query that we would use to fetch the same blog post data with the author name and image as referenced in the first example:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="XkmgAeItrY"
      aria-describedby="XkmgAeItrY">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="XkmgAeItrY">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="XkmgAeItrY" itemprop="text" content="const%20query%20%3D%20%60%7B%0A%20%20%20%20blogPostCollection(limit%3A%201%2C%20where%3A%20%7Bslug%3A%20%22the-power-of-the-contentful-rich-text-field%22%7D)%20%7B%0A%20%20%20%20%20%20items%20%7B%0A%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20slug%0A%20%20%20%20%20%20%20%20author%20%7B%0A%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%23%20more%20author%20fields%20%E2%80%A6%20%0A%20%20%20%20%20%20%20%20%20%20image%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20more%20image%20fields%20...%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%60%3B%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">{<br>    blogPostCollection(limit: 1, where: {slug: "the-power-of-the-contentful-rich-text-field"}) {<br>      items {<br>        sys {<br>          id<br>        }<br>        title<br>        slug<br>        author {<br>          name<br>          # more author fields … <br>          image {<br>            sys {<br>              id<br>            }<br>            url<br>            # more image fields ...<br>          }<br>        }<br>      }<br>    }<br>  }</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s how we can query the Contentful GraphQL API using fetch:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ReohOvxMui"
      aria-describedby="ReohOvxMui">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ReohOvxMui">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="ReohOvxMui" itemprop="text" content="const%20fetchOptions%20%3D%20%7B%0A%20%20method%3A%20%22POST%22%2C%0A%20%20headers%3A%20%7B%0A%20%20%20%20Authorization%3A%20%60Bearer%20%24%7BACCESS_TOKEN%7D%60%2C%0A%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%7D%2C%0A%20%20body%3A%20JSON.stringify(%7B%20query%20%7D)%2C%0A%7D%3B%0A%0Aconst%20response%20%3D%20await%20fetch(%60https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%7BSPACE_ID%7D%60%2C%20fetchOptions).then((response)%20%3D%3E%20response.json())%3B%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> fetchOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Bearer </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">ACCESS_TOKEN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span><br>    <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://graphql.contentful.com/content/v1/spaces/{SPACE_ID}</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> fetchOptions<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">To compare this query to the <code>include</code> levels in the REST API:</p><ul><li><p class="post__p">Level 1 (blogPost)</p></li><li><p class="post__p">Level 2 (blogPost.author)</p></li><li><p class="post__p">Level 3 (blogPost.author.image)</p></li></ul><h3 class="post__h3">The GraphQL API response</h3><p class="post__p">Due to how we constructed our GraphQL query to fetch the linked entries and assets, the raw response from the GraphQL contains the data for the linked assets and entries in the nodes we would expect — at a content type level only.</p><p class="post__p">Here’s the response for the above query from the GraphQL API:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="pJDxQrSHZt"
      aria-describedby="pJDxQrSHZt">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="pJDxQrSHZt">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="pJDxQrSHZt" itemprop="text" content="%7B%0A%20%20%22data%22%3A%20%7B%0A%20%20%20%20%22blogPostCollection%22%3A%20%7B%0A%20%20%20%20%20%20%22items%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2253PLFh5VLIotcvMqR6VsnO%22%0A%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22The%20power%20of%20the%20Contentful%20Rich%20Text%20field%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22slug%22%3A%20%22the-power-of-the-contentful-rich-text-field%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22author%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22name%22%3A%20%22Salma%20Alam-Naylor%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22image%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22rImaN1nOhnl7aJ4OYwbOp%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22url%22%3A%20%22https%3A%2F%2Fimages.ctfassets.net%2F...%2Fimage.png%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%5D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token punctuation">{</span><br>  <span class="token string-property property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token string-property property">"blogPostCollection"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token string-property property">"items"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>        <span class="token punctuation">{</span><br>          <span class="token string-property property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>            <span class="token string-property property">"id"</span><span class="token operator">:</span> <span class="token string">"53PLFh5VLIotcvMqR6VsnO"</span><br>          <span class="token punctuation">}</span><span class="token punctuation">,</span><br>          <span class="token string-property property">"title"</span><span class="token operator">:</span> <span class="token string">"The power of the Contentful Rich Text field"</span><span class="token punctuation">,</span><br>          <span class="token string-property property">"slug"</span><span class="token operator">:</span> <span class="token string">"the-power-of-the-contentful-rich-text-field"</span><span class="token punctuation">,</span><br>          <span class="token string-property property">"author"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>            <span class="token string-property property">"name"</span><span class="token operator">:</span> <span class="token string">"Salma Alam-Naylor"</span><span class="token punctuation">,</span><br>            <span class="token string-property property">"image"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>              <span class="token string-property property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                <span class="token string-property property">"id"</span><span class="token operator">:</span> <span class="token string">"rImaN1nOhnl7aJ4OYwbOp"</span><br>              <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token string-property property">"url"</span><span class="token operator">:</span> <span class="token string">"https://images.ctfassets.net/.../image.png"</span><span class="token punctuation">,</span><br>             <span class="token punctuation">}</span><br>          <span class="token punctuation">}</span><span class="token punctuation">,</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">]</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">In the response above, the data for <code>author</code> appeared in the node tree exactly where we expected it, and we can access the name on the front end — for example, via <code>data.blogPostCollection.items[0].author.name</code> — without having to use an SDK to resolve the entries.</p><h3 class="post__h3">The include depth is inferred by the construction of your GraphQL query</h3><p class="post__p">In comparison to the REST API, where you usually fetch the blog post data and link the entries after the fact, a GraphQL API query is entirely flexible to your needs. There’s always the caveat, however, that a complex GraphQL query with many nested link assets and entries might surpass the maximum complexity permitted on the GraphQL API. <a href="https://www.contentful.com/developers/docs/references/graphql/#/introduction/query-complexity-limits" target="_blank">Read more about GraphQL complexity limits here</a>.</p><h2 class="post__h2">In conclusion</h2><p class="post__p">Understanding the structure of the data responses from Contentful and how linked assets are returned and then resolved via the Contentful SDKs, empowers you to choose which APIs and methods are best suited to your applications. And, hey, if you want to resolve the linked assets and entries yourself, then you’re well equipped.</p><p class="post__p">Check out some further reading on <a href="https://www.contentful.com/blog/2021/04/14/rendering-linked-assets-entries-in-contentful/" target="_blank">how you can resolve linked assets and entries from the Contentful Rich Text field response in both the REST API and GraphQL API</a>.</p><p class="post__p">And remember, build stuff, learn things and love what you do.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Rendering linked assets and entries in the Contentful Rich Text field</title>
          <description>Take a deep dive into rendering linked assets and entries in the Contentful Rich Text field using both the REST and GraphQL APIs.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/04/14/rendering-linked-assets-entries-in-contentful/</link>
          <guid>https://www.contentful.com/blog/2021/04/14/rendering-linked-assets-entries-in-contentful/</guid>
          <pubDate>Tue, 13 Apr 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">In order to understand how to render <a href="mailto:test@test.com" >linked</a> assets and entries inside the Contentful Rich Text field on the front end, it is crucial to understand how linked assets and entries work across Contentful as a whole. This post focuses on working with Contentful in a JavaScript ecosystem, but the principals and <a href="mailto:thing@thing.com" >methods</a> are the same across the tech stack and can be applied to your favorite programming language, too.</p><p class="post__p"><b class="post__p--bold">Before you get started, you might want to check out this blog post — </b><a href="https://whitep4nth3r.com/blog/exploring-linked-entries-assets-contentful/">Exploring linked entries and assets in Contentful with JavaScript via REST and GraphQL</a><b class="post__p--bold"> —  to take a deep dive into the inner workings of the Contentful REST API and GraphQL API, how our links are returned in the response and how they can be resolved into a rich object graph.</b></p><h2 class="post__h2">Exploring the Rich Text field editor</h2><p class="post__p">Now that we’re familiar with how Contentful returns items and their linked entries and assets, and how we can resolve the links either manually (through a lot of hard work!) or with the <a href="https://www.contentful.com/developers/docs/javascript/sdks/" target="_blank">JavaScript SDK</a> (that’s nice and easy), let’s look at how it works with<b class="post__p--bold"> </b><b class="post__p--italic"><b class="post__p--bold">links inside the Rich Text field</b></b>.</p><p class="post__p">Rich Text is a field type that enables authors to create rich text content, similar to traditional “what you see is what you get” (WYSIWYG) editors. The key difference here is that the Contentful Rich Text field response is returned as pure JSON rather than HTML. Additionally, it allows entries and assets within our Contentful space to be linked dynamically and embedded within the flow of the text. It offers common text formatting options such as paragraphs, lists and all that good stuff, but allows us to embed and link other references, too.</p><p class="post__p"><a href="https://www.contentful.com/developers/docs/concepts/rich-text/" target="_blank">Read more about the Rich Text field here</a>. </p><p class="post__p">Find an example of the Rich Text field editor in the Contentful UI below. It includes several paragraphs but also links a video embed entry, an image asset and a code block entry.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2aHKeD4JTtXECtcuBnUvGY/2f0e2c319986be95c224b2c4ef9611f3/rich_text_field_with_links.png" alt="Screenshot of rich text field embedded entries" height="1138" width="747" /><p class="post__p">If you want to code along with the post, you can create the following content types in your Contentful space, which we will use in our examples:</p><p class="post__p">The code block entry contains the following fields:</p><ul><li><p class="post__p">Description (short text)</p></li><li><p class="post__p">Language (short text)</p></li><li><p class="post__p">Code (long text displayed as a markdown field)</p></li></ul><p class="post__p">The video embed entry contains the following fields:</p><ul><li><p class="post__p">Title (short text)</p></li><li><p class="post__p">Embed URL (short text)</p></li></ul><p class="post__p">That’s the visual structure of the Rich Text field, but how is the content — and especially the references — represented in the JSON response? What are the differences between the REST and GraphQL API responses? Let’s take a look.</p><h2 class="post__h2">Rendering Rich Text references using the REST API</h2><p class="post__p">The following examples use JavaScript to fetch data from <a href="https://nextjs-contentful-blog-starter.vercel.app/blog/the-power-of-the-contentful-rich-text-field" target="_blank"><u>this example blog post</u></a>. The blog post is served on an application built with Next.js — but we won’t be going into Next.js in this post.</p><p class="post__p">We can request the data via this URL:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="KeowpEedPj"
      aria-describedby="KeowpEedPj">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="KeowpEedPj">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="KeowpEedPj" itemprop="text" content="https%3A%2F%2Fcdn.contentful.com%2Fspaces%2F%7B%7BspaceId%7D%7D%2Fenvironments%2Fmaster%2Fentries%3Faccess_token%3D%7B%7BaccessToken%7D%7D%26content_type%3DblogPost%26fields.slug%3Dthe-power-of-the-contentful-rich-text-field%26limit%3D1%26include%3D10">
      <pre class="language-bash"><code class="language-bash">https://cdn.contentful.com/spaces/<span class="token punctuation">{</span><span class="token punctuation">{</span>spaceId<span class="token punctuation">}</span><span class="token punctuation">}</span>/environments/master/entries?access_token<span class="token operator">=</span><span class="token punctuation">{</span><span class="token punctuation">{</span>accessToken<span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token operator">&amp;</span><span class="token assign-left variable">content_type</span><span class="token operator">=</span>blogPost<span class="token operator">&amp;</span><span class="token assign-left variable">fields.slug</span><span class="token operator">=</span>the-power-of-the-contentful-rich-text-field<span class="token operator">&amp;</span><span class="token assign-left variable">limit</span><span class="token operator">=</span><span class="token number">1</span><span class="token operator">&amp;</span><span class="token assign-left variable">include</span><span class="token operator">=</span><span class="token number">10</span></code></pre>
    </div>
  </div>

  <p class="post__p">It returns this raw response from the REST API. This is trimmed down to show only the fields we are concerned with in this example:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="MKaPQVYKZp"
      aria-describedby="MKaPQVYKZp">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="MKaPQVYKZp">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="MKaPQVYKZp" itemprop="text" content="%7B%0A%20%20%22items%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22title%22%3A%20%22The%20power%20of%20the%20Contentful%20Rich%20Text%20field%22%2C%0A%20%20%20%20%20%20%20%20%22slug%22%3A%20%22the-power-of-the-contentful-rich-text-field%22%2C%0A%20%20%20%20%20%20%20%20%22body%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22text%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%3A%20%22Here%20is%20an%20inline%20entry%20that%20links%20to%20another%20blog%20post%3A%20%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22marks%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22embedded-entry-inline%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22target%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22999888%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22Entry%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%3A%20%22Here%20is%20a%20video%20entry%20embedded%20as%20an%20entry%20in%20the%20Rich%20Text%20field%20editor.%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22text%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22paragraph%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22target%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2212345%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22Entry%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22embedded-entry-block%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%3A%20%22Here%20is%20an%20image%20asset.%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22text%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22paragraph%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22target%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2267890%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22Asset%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22embedded-asset-block%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%3A%20%22And%20here%20is%20a%20code%20block%20entry.%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22text%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22paragraph%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22target%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2299999%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22Entry%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22embedded-entry-block%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%5D%2C%0A%20%20%22includes%22%3A%20%7B%0A%20%20%20%20%22Entry%22%3A%20%5B%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%22id%22%3A%20%22999888%22%2C%0A%20%20%20%20%20%20%20%20%22type%22%3A%20%22Entry%22%2C%0A%20%20%20%20%20%20%20%20%22contentType%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22ContentType%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22blogPost%22%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22This%20blog%20comes%20complete%20with%20an%20RSS%20feed%20that's%20generated%20at%20build%20time%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22slug%22%3A%20%22this-blog-comes-complete-with-an-rss-feed-thats-generated-at-build-time%22%2C%0A%20%20%20%20%20%20%20%20%20%20%2F%2F%20More%20blog%20post%20fields...%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2212345%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Entry%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22contentType%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22ContentType%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22videoEmbed%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22Example%20video%20embed%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22embedUrl%22%3A%20%22https%3A%2F%2Fwww.youtube.com%2Fembed%2F97Hg0OYFC0w%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2299999%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Entry%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22contentType%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22ContentType%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22codeBlock%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22description%22%3A%20%22Example%20code%20block%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22language%22%3A%20%22javascript%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22code%22%3A%20%22export%20function%20formatPublishedDateForDisplay(dateString)%20%7B%5Cn%20%20const%20timestamp%20%3D%20Date.parse(dateString)%3B%5Cn%20%20const%20date%20%3D%20new%20Date(timestamp)%3B%5Cn%20%20return%20%60%24%7Bdate.getDate()%7D%20%24%7BgetMonthStringFromInt(%5Cn%20%20%20%20date.getMonth()%2C%5Cn%20%20)%7D%20%24%7Bdate.getFullYear()%7D%60%3B%5Cn%7D%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%5D%2C%0A%20%20%20%20%22Asset%22%3A%20%5B%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2267890%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Asset%22%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22colourful-galaxy%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22description%22%3A%20%22Blue%20and%20purple%20galaxy%20digital%20wallpaper%22%2C%0A%20%20%20%20%20%20%20%20%20%20%22file%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22url%22%3A%20%22%2F%2Fimages.ctfassets.net%2F...%2Fexample.jpg%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22fileName%22%3A%20%22example.jpg%22%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%5D%0A%20%20%7D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"items"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"The power of the Contentful Rich Text field"</span><span class="token punctuation">,</span><br>        <span class="token property">"slug"</span><span class="token operator">:</span> <span class="token string">"the-power-of-the-contentful-rich-text-field"</span><span class="token punctuation">,</span><br>        <span class="token property">"body"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>            <span class="token punctuation">{</span><br>               <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"text"</span><span class="token punctuation">,</span><br>                <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"Here is an inline entry that links to another blog post: "</span><span class="token punctuation">,</span><br>                <span class="token property">"marks"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>                <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br>             <span class="token punctuation">}</span><span class="token punctuation">,</span><br>             <span class="token punctuation">{</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"embedded-entry-inline"</span><span class="token punctuation">,</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span>  <br>                <span class="token property">"target"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"999888"</span><span class="token punctuation">,</span><br>                    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>                    <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><br>                  <span class="token punctuation">}</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">}</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>                <span class="token punctuation">{</span><br>                  <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"Here is a video entry embedded as an entry in the Rich Text field editor."</span><span class="token punctuation">,</span><br>                  <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"text"</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"paragraph"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                <span class="token property">"target"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"12345"</span><span class="token punctuation">,</span><br>                    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>                    <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><br>                  <span class="token punctuation">}</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"embedded-entry-block"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>                <span class="token punctuation">{</span><br>                  <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"Here is an image asset."</span><span class="token punctuation">,</span><br>                  <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"text"</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"paragraph"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                <span class="token property">"target"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"67890"</span><span class="token punctuation">,</span><br>                    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>                    <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"Asset"</span><br>                  <span class="token punctuation">}</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"embedded-asset-block"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>                <span class="token punctuation">{</span><br>                  <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"And here is a code block entry."</span><span class="token punctuation">,</span><br>                  <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"text"</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"paragraph"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                <span class="token property">"target"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"99999"</span><span class="token punctuation">,</span><br>                    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>                    <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><br>                  <span class="token punctuation">}</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"embedded-entry-block"</span><br>            <span class="token punctuation">}</span><br>          <span class="token punctuation">]</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token property">"includes"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"Entry"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>      <span class="token punctuation">{</span><br>       <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"999888"</span><span class="token punctuation">,</span><br>        <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>        <span class="token property">"contentType"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>            <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>            <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"ContentType"</span><span class="token punctuation">,</span><br>            <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"blogPost"</span><br>          <span class="token punctuation">}</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>        <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"This blog comes complete with an RSS feed that's generated at build time"</span><span class="token punctuation">,</span><br>          <span class="token property">"slug"</span><span class="token operator">:</span> <span class="token string">"this-blog-comes-complete-with-an-rss-feed-thats-generated-at-build-time"</span><span class="token punctuation">,</span><br>          <span class="token comment">// More blog post fields...</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">{</span><br>        <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"12345"</span><span class="token punctuation">,</span><br>          <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>          <span class="token property">"contentType"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>            <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>              <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>              <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"ContentType"</span><span class="token punctuation">,</span><br>              <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"videoEmbed"</span><br>            <span class="token punctuation">}</span><br>          <span class="token punctuation">}</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>        <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"Example video embed"</span><span class="token punctuation">,</span><br>          <span class="token property">"embedUrl"</span><span class="token operator">:</span> <span class="token string">"https://www.youtube.com/embed/97Hg0OYFC0w"</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">{</span><br>        <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"99999"</span><span class="token punctuation">,</span><br>          <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>          <span class="token property">"contentType"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>            <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>              <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>              <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"ContentType"</span><span class="token punctuation">,</span><br>              <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"codeBlock"</span><br>            <span class="token punctuation">}</span><br>          <span class="token punctuation">}</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>        <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"Example code block"</span><span class="token punctuation">,</span><br>          <span class="token property">"language"</span><span class="token operator">:</span> <span class="token string">"javascript"</span><span class="token punctuation">,</span><br>          <span class="token property">"code"</span><span class="token operator">:</span> <span class="token string">"export function formatPublishedDateForDisplay(dateString) {\n  const timestamp = Date.parse(dateString);\n  const date = new Date(timestamp);\n  return `${date.getDate()} ${getMonthStringFromInt(\n    date.getMonth(),\n  )} ${date.getFullYear()}`;\n}"</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">]</span><span class="token punctuation">,</span><br>    <span class="token property">"Asset"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>      <span class="token punctuation">{</span><br>        <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"67890"</span><span class="token punctuation">,</span><br>          <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Asset"</span><br>        <span class="token punctuation">}</span><span class="token punctuation">,</span><br>        <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"colourful-galaxy"</span><span class="token punctuation">,</span><br>          <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"Blue and purple galaxy digital wallpaper"</span><span class="token punctuation">,</span><br>          <span class="token property">"file"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>            <span class="token property">"url"</span><span class="token operator">:</span> <span class="token string">"//images.ctfassets.net/.../example.jpg"</span><span class="token punctuation">,</span><br>            <span class="token property">"fileName"</span><span class="token operator">:</span> <span class="token string">"example.jpg"</span><br>          <span class="token punctuation">}</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">]</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">We can see that the entry response contains two top-level nodes: <code>items</code> and <code>includes</code>.</p><p class="post__p">Inspecting the Rich Text <code>body</code> field, we observe that:</p><ul><li><p class="post__p"><code>items[0].fields.body.content</code> contains a number of nodes — text nodes (with <code>nodeType: &quot;paragraph&quot;</code>) and additional nodes with the property <code>data.target.type: &quot;Link&quot;</code> and <code>nodetype: &quot;embedded-entry-block&quot;, nodetype: &quot;embedded-entry-inline&quot;</code>, and <code>nodeType: &quot;embedded-asset-block&quot;</code> — <b class="post__p--bold">with empty content nodes</b></p></li></ul><p class="post__p">Wait — the linked entries inside <code>items[0].fields.body.content</code> are empty! Where is our data?</p><p class="post__p">The <b class="post__p--italic">actual data</b> for the linked entries referenced in the <code>body.content</code> field are in the <code>includes</code> object, returned alongside the top-level <code>items</code> array:</p><ul><li><p class="post__p"><code>includes.Entry</code> contains the data for the two linked entries (the code block and the video embed)</p></li><li><p class="post__p"><code>includes.Asset</code> includes the data for the linked asset (the image)</p></li></ul><p class="post__p">What do we do now? How do we link all the data together so we can access it inside of the <code>body</code> node as we would expect?</p><h3 class="post__h3">Using the Contentful JavaScript SDK</h3><p class="post__p">The good news is, that if we’re using a Contentful SDK to make a call to the Contentful API, those linked assets and entries in the Rich Text field will be resolved for you.</p><p class="post__p">As discussed in <a href="https://www.contentful.com/blog/2021/04/14/exploring-linked-entries-assets-contentful/" target="_blank">this blog post</a>, under the hood, the JavaScript SDK uses the <a href="https://github.com/contentful/contentful-resolve-response" target="_blank">contentful-resolve-response</a> package, which converts the flat nodes into a rich tree of data. <b class="post__p--bold">The one limitation of the Contentful API to remember is that it will only return linked entries up to a maximum of 10 levels deep that can be resolved</b>. However, given that our Rich Text field contains embedded entries and assets only one level deep in this example, we’re good to go.</p><p class="post__p">The linked entries that are returned from the API are determined by the <code>include</code> parameter on the request to the API. Read more about <a href="https://www.contentful.com/blog/2021/04/14/exploring-linked-entries-assets-contentful/" target="_blank">the include parameter here</a>.</p><p class="post__p">Make the same call to fetch an entry including a Rich Text field via the <a href="https://www.contentful.com/developers/docs/javascript/sdks/" target="_blank">JavaScript SDK</a>:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="utfYLaLWCU"
      aria-describedby="utfYLaLWCU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="utfYLaLWCU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="utfYLaLWCU" itemprop="text" content="const%20post%20%3D%20await%20client%0A%20%20.getEntries(%7B%0A%20%20%20%20content_type%3A%20%22blogPost%22%2C%0A%20%20%20%20limit%3A%201%2C%0A%20%20%20%20include%3A%2010%2C%0A%20%20%20%20%22fields.slug%22%3A%20%22the-power-of-the-contentful-rich-text-field%22%2C%0A%20%20%7D)%0A%20%20.then((entry)%20%3D%3E%20entry)%0A%20%20.catch(console.error)%3B%0A">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> post <span class="token operator">=</span> <span class="token keyword">await</span> client<br>  <span class="token punctuation">.</span><span class="token function">getEntries</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>    <span class="token literal-property property">content_type</span><span class="token operator">:</span> <span class="token string">"blogPost"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">limit</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">include</span><span class="token operator">:</span> <span class="token number">10</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"fields.slug"</span><span class="token operator">:</span> <span class="token string">"the-power-of-the-contentful-rich-text-field"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">entry</span><span class="token punctuation">)</span> <span class="token operator">=></span> entry<span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span>console<span class="token punctuation">.</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s the processed JavaScript object returned from the API call via the SDK, containing the data we need for each node in the Rich Text response:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="WRaDCgeCZb"
      aria-describedby="WRaDCgeCZb">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="WRaDCgeCZb">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="WRaDCgeCZb" itemprop="text" content="%7B%0A%20%20%22items%22%3A%20%5B%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22title%22%3A%20%22The%20power%20of%20the%20Contentful%20Rich%20Text%20field%22%2C%0A%20%20%20%20%20%20%20%20%22slug%22%3A%20%22the-power-of-the-contentful-rich-text-field%22%2C%0A%20%20%20%20%20%20%20%20%22body%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22text%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%3A%20%22Here%20is%20an%20inline%20entry%20that%20links%20to%20another%20blog%20post%3A%20%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22paragraph%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22target%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22999888%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Entry%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22contentType%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22ContentType%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22blogPost%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22This%20blog%20comes%20complete%20with%20an%20RSS%20feed%20that's%20generated%20at%20build%20time%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22slug%22%3A%20%22this-blog-comes-complete-with-an-rss-feed-thats-generated-at-build-time%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20More%20blog%20post%20fields%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22embedded-entry-inline%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%3A%20%22Here%20is%20a%20video%20entry%20embedded%20as%20an%20entry%20in%20the%20Rich%20Text%20field%20editor.%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22text%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22paragraph%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22target%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2212345%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Entry%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22contentType%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22ContentType%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22videoEmbed%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22Example%20video%20embed%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22embedUrl%22%3A%20%22https%3A%2F%2Fwww.youtube.com%2Fembed%2F97Hg0OYFC0w%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22embedded-entry-block%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%3A%20%22Here%20is%20an%20image%20asset.%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22text%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22paragraph%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22target%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2267890%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Asset%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22colourful-galaxy%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22description%22%3A%20%22Blue%20and%20purple%20galaxy%20digital%20wallpaper%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22file%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22url%22%3A%20%22%2F%2Fimages.ctfassets.net%2F...%2Fexample.jpg%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22fileName%22%3A%20%22example.jpg%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22embedded-asset-block%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%3A%20%22And%20here%20is%20a%20code%20block%20entry.%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22text%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22paragraph%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22data%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22target%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%2299999%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Entry%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22contentType%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22sys%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22type%22%3A%20%22Link%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22linkType%22%3A%20%22ContentType%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22id%22%3A%20%22codeBlock%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22fields%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22description%22%3A%20%22Example%20code%20block%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22language%22%3A%20%22javascript%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22code%22%3A%20%22export%20function%20formatPublishedDateForDisplay(dateString)%20%7B%5Cn%20%20const%20timestamp%20%3D%20Date.parse(dateString)%3B%5Cn%20%20const%20date%20%3D%20new%20Date(timestamp)%3B%5Cn%20%20return%20%60%24%7Bdate.getDate()%7D%20%24%7BgetMonthStringFromInt(%5Cn%20%20%20%20date.getMonth()%2C%5Cn%20%20)%7D%20%24%7Bdate.getFullYear()%7D%60%3B%5Cn%7D%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22content%22%3A%20%5B%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22embedded-entry-block%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%22nodeType%22%3A%20%22document%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%5D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"items"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token punctuation">{</span><br>      <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"The power of the Contentful Rich Text field"</span><span class="token punctuation">,</span><br>        <span class="token property">"slug"</span><span class="token operator">:</span> <span class="token string">"the-power-of-the-contentful-rich-text-field"</span><span class="token punctuation">,</span><br>        <span class="token property">"body"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>          <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>                  <span class="token punctuation">{</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"text"</span><span class="token punctuation">,</span><br>              <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"Here is an inline entry that links to another blog post: "</span><span class="token punctuation">,</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"paragraph"</span><span class="token punctuation">,</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                <span class="token property">"target"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"999888"</span><span class="token punctuation">,</span><br>                    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>                    <span class="token property">"contentType"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                      <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                        <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>                        <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"ContentType"</span><span class="token punctuation">,</span><br>                        <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"blogPost"</span><br>                      <span class="token punctuation">}</span><br>                    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>                  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>                  <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"This blog comes complete with an RSS feed that's generated at build time"</span><span class="token punctuation">,</span><br>                    <span class="token property">"slug"</span><span class="token operator">:</span> <span class="token string">"this-blog-comes-complete-with-an-rss-feed-thats-generated-at-build-time"</span><span class="token punctuation">,</span><br>                    <span class="token comment">// More blog post fields</span><br>                  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>                <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"embedded-entry-inline"</span><span class="token punctuation">,</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>                <span class="token punctuation">{</span><br>                  <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"Here is a video entry embedded as an entry in the Rich Text field editor."</span><span class="token punctuation">,</span><br>                  <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"text"</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"paragraph"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                <span class="token property">"target"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"12345"</span><span class="token punctuation">,</span><br>                    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>                    <span class="token property">"contentType"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                      <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                        <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>                        <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"ContentType"</span><span class="token punctuation">,</span><br>                        <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"videoEmbed"</span><br>                      <span class="token punctuation">}</span><br>                    <span class="token punctuation">}</span><br>                  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>                  <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"Example video embed"</span><span class="token punctuation">,</span><br>                    <span class="token property">"embedUrl"</span><span class="token operator">:</span> <span class="token string">"https://www.youtube.com/embed/97Hg0OYFC0w"</span><br>                  <span class="token punctuation">}</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"embedded-entry-block"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>                <span class="token punctuation">{</span><br>                  <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"Here is an image asset."</span><span class="token punctuation">,</span><br>                  <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"text"</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"paragraph"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                <span class="token property">"target"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"67890"</span><span class="token punctuation">,</span><br>                    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Asset"</span><br>                  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>                  <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"colourful-galaxy"</span><span class="token punctuation">,</span><br>                    <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"Blue and purple galaxy digital wallpaper"</span><span class="token punctuation">,</span><br>                    <span class="token property">"file"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                      <span class="token property">"url"</span><span class="token operator">:</span> <span class="token string">"//images.ctfassets.net/.../example.jpg"</span><span class="token punctuation">,</span><br>                      <span class="token property">"fileName"</span><span class="token operator">:</span> <span class="token string">"example.jpg"</span><br>                    <span class="token punctuation">}</span><br>                  <span class="token punctuation">}</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"embedded-asset-block"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>                <span class="token punctuation">{</span><br>                  <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"And here is a code block entry."</span><span class="token punctuation">,</span><br>                  <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"text"</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"paragraph"</span><br>            <span class="token punctuation">}</span><span class="token punctuation">,</span><br>            <span class="token punctuation">{</span><br>              <span class="token property">"data"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                <span class="token property">"target"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                  <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"99999"</span><span class="token punctuation">,</span><br>                    <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Entry"</span><span class="token punctuation">,</span><br>                    <span class="token property">"contentType"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                      <span class="token property">"sys"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                        <span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"Link"</span><span class="token punctuation">,</span><br>                        <span class="token property">"linkType"</span><span class="token operator">:</span> <span class="token string">"ContentType"</span><span class="token punctuation">,</span><br>                        <span class="token property">"id"</span><span class="token operator">:</span> <span class="token string">"codeBlock"</span><br>                      <span class="token punctuation">}</span><br>                    <span class="token punctuation">}</span><br>                  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>                  <span class="token property">"fields"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>                    <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"Example code block"</span><span class="token punctuation">,</span><br>                    <span class="token property">"language"</span><span class="token operator">:</span> <span class="token string">"javascript"</span><span class="token punctuation">,</span><br>                    <span class="token property">"code"</span><span class="token operator">:</span> <span class="token string">"export function formatPublishedDateForDisplay(dateString) {\n  const timestamp = Date.parse(dateString);\n  const date = new Date(timestamp);\n  return `${date.getDate()} ${getMonthStringFromInt(\n    date.getMonth(),\n  )} ${date.getFullYear()}`;\n}"</span><br>                  <span class="token punctuation">}</span><br>                <span class="token punctuation">}</span><br>              <span class="token punctuation">}</span><span class="token punctuation">,</span><br>              <span class="token property">"content"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>              <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"embedded-entry-block"</span><br>            <span class="token punctuation">}</span><br>          <span class="token punctuation">]</span><span class="token punctuation">,</span><br>          <span class="token property">"nodeType"</span><span class="token operator">:</span> <span class="token string">"document"</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Notice how all the data that was previously contained in a separate <code>includes </code>node from the raw REST API response is now inserted beautifully into the Rich Text field response — where we would expect.</p><p class="post__p">Now that we have our links and their data inside the Rich Text field where we need it in a nicely packaged JavaScript object, how do we render the HTML for each node?</p><h3 class="post__h3">Rendering the Rich Text response from REST with linked assets and entries on the front end</h3><p class="post__p">Contentful provides you with tools to speed up your workflow on the front end and to allow you to work with the Rich Text field data and render the nodes into HTML — Rich Text field renderers. For this example, we are going to be using the <a href="https://www.npmjs.com/package/@contentful/rich-text-react-renderer" target="_blank">@contentful/rich-text-react-renderer</a> to demonstrate the concepts in <b class="post__p--bold">JavaScript </b>and<b class="post__p--bold"> React</b>.</p><p class="post__p">There are a number of Rich Text field renderer packages available for your favorite programming languages and frameworks — <a href="https://github.com/contentful/rich-text" target="_blank">check them out on GitHub here</a>.</p><p class="post__p">Let’s return to the example Rich Text field with two embedded links — a code block entry and a video embed entry — and an image asset. Most likely, we will want to display the data from these entries in particular ways for the front end, such as by using specific HTML elements, adding CSS classes, or rendering custom React components.</p><p class="post__p">With the response from the REST API processed by the JavaScript SDK — which has linked the <a href="https://www.contentful.com/blog/2021/04/14/exploring-linked-entries-assets-contentful/" target="_blank">entries and assets for us</a> — we can call <code>documentToReactComponents</code> with an optional <code>options</code> parameter, allowing us control over how our data is displayed on the page.</p><p class="post__p">Notice below, that for each node in the Rich Text response, the SDK has resolved the links for us. We can access the type of entry or asset using <code>node.data.target.contentType.sys.id</code>, and access the fields using <code>node.data.target.fields</code> and so on. </p><p class="post__p">This is where the link resolution magic of the SDK comes into play.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="BMDNzVLcyB"
      aria-describedby="BMDNzVLcyB">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="BMDNzVLcyB">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="BMDNzVLcyB" itemprop="text" content="import%20%7B%20documentToReactComponents%20%7D%20from%20%22%40contentful%2Frich-text-react-renderer%22%3B%0Aimport%20%7B%20BLOCKS%2C%20INLINES%20%7D%20from%20%22%40contentful%2Frich-text-types%22%3B%0A%0A%2F%2F%20Create%20a%20bespoke%20renderOptions%20object%20to%20target%20BLOCKS.EMBEDDED_ENTRY%20(linked%20block%20entries%20e.g.%20code%20blocks)%0A%2F%2F%20INLINES.EMBEDDED_ENTRY%20(linked%20inline%20entries%20e.g.%20a%20reference%20to%20another%20blog%20post)%0A%2F%2F%20and%20BLOCKS.EMBEDDED_ASSET%20(linked%20assets%20e.g.%20images)%0A%0Aconst%20renderOptions%20%3D%20%7B%0A%20%20renderNode%3A%20%7B%0A%20%20%20%20%5BINLINES.EMBEDDED_ENTRY%5D%3A%20(node%2C%20children)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%2F%2F%20target%20the%20contentType%20of%20the%20EMBEDDED_ENTRY%20to%20display%20as%20you%20need%0A%20%20%20%20%20%20if%20(node.data.target.sys.contentType.sys.id%20%3D%3D%3D%20%22blogPost%22)%20%7B%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%3Ca%20href%3D%7B%60%2Fblog%2F%24%7Bnode.data.target.fields.slug%7D%60%7D%3E%20%20%20%20%20%20%20%20%20%20%20%20%7Bnode.data.target.fields.title%7D%0A%20%20%20%20%20%20%20%20%20%20%3C%2Fa%3E%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%2C%0A%20%20%20%20%5BBLOCKS.EMBEDDED_ENTRY%5D%3A%20(node%2C%20children)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%2F%2F%20target%20the%20contentType%20of%20the%20EMBEDDED_ENTRY%20to%20display%20as%20you%20need%0A%20%20%20%20%20%20if%20(node.data.target.sys.contentType.sys.id%20%3D%3D%3D%20%22codeBlock%22)%20%7B%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%3Cpre%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Ccode%3E%7Bnode.data.target.fields.code%7D%3C%2Fcode%3E%0A%20%20%20%20%20%20%20%20%20%20%3C%2Fpre%3E%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20if%20(node.data.target.sys.contentType.sys.id%20%3D%3D%3D%20%22videoEmbed%22)%20%7B%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%3Ciframe%0A%20%20%20%20%20%20%20%20%20%20%20%20src%3D%7Bnode.data.target.fields.embedUrl%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20height%3D%22100%25%22%0A%20%20%20%20%20%20%20%20%20%20%20%20width%3D%22100%25%22%0A%20%20%20%20%20%20%20%20%20%20%20%20frameBorder%3D%220%22%0A%20%20%20%20%20%20%20%20%20%20%20%20scrolling%3D%22no%22%0A%20%20%20%20%20%20%20%20%20%20%20%20title%3D%7Bnode.data.target.fields.title%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20allowFullScreen%3D%7Btrue%7D%0A%20%20%20%20%20%20%20%20%20%20%2F%3E%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%2C%0A%0A%20%20%20%20%5BBLOCKS.EMBEDDED_ASSET%5D%3A%20(node%2C%20children)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%2F%2F%20render%20the%20EMBEDDED_ASSET%20as%20you%20need%0A%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%3Cimg%0A%20%20%20%20%20%20%20%20%20%20src%3D%7B%60https%3A%2F%2F%24%7Bnode.data.target.fields.file.url%7D%60%7D%0A%20%20%20%20%20%20%20%20%20%20height%3D%7Bnode.data.target.fields.file.details.image.height%7D%0A%20%20%20%20%20%20%20%20%20%20width%3D%7Bnode.data.target.fields.file.details.image.width%7D%0A%20%20%20%20%20%20%20%20%20%20alt%3D%7Bnode.data.target.fields.description%7D%0A%20%20%20%20%20%20%20%20%2F%3E%0A%20%20%20%20%20%20)%3B%0A%20%20%20%20%7D%2C%0A%20%20%7D%2C%0A%7D%3B%0A%0Aexport%20default%20function%20BlogPost(props)%20%7B%0A%20%20const%20%7B%20post%20%7D%20%3D%20props%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3C%3E%0A%20%20%20%20%20%20%20%7BdocumentToReactComponents(post.fields.body%2C%20renderOptions)%7D%0A%20%20%20%20%3C%2F%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> documentToReactComponents <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@contentful/rich-text-react-renderer"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> <span class="token constant">BLOCKS</span><span class="token punctuation">,</span> <span class="token constant">INLINES</span> <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@contentful/rich-text-types"</span><span class="token punctuation">;</span><br><br><span class="token comment">// Create a bespoke renderOptions object to target BLOCKS.EMBEDDED_ENTRY (linked block entries e.g. code blocks)</span><br><span class="token comment">// INLINES.EMBEDDED_ENTRY (linked inline entries e.g. a reference to another blog post)</span><br><span class="token comment">// and BLOCKS.EMBEDDED_ASSET (linked assets e.g. images)</span><br><br><span class="token keyword">const</span> renderOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">renderNode</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token punctuation">[</span><span class="token constant">INLINES</span><span class="token punctuation">.</span><span class="token constant">EMBEDDED_ENTRY</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">node<span class="token punctuation">,</span> children</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token comment">// target the contentType of the EMBEDDED_ENTRY to display as you need</span><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>contentType<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id <span class="token operator">===</span> <span class="token string">"blogPost"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">return</span> <span class="token punctuation">(</span><br>          <span class="token operator">&lt;</span>a href<span class="token operator">=</span><span class="token punctuation">{</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/blog/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">}</span><span class="token operator">></span>            <span class="token punctuation">{</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>title<span class="token punctuation">}</span><br>          <span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><br>        <span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">[</span><span class="token constant">BLOCKS</span><span class="token punctuation">.</span><span class="token constant">EMBEDDED_ENTRY</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">node<span class="token punctuation">,</span> children</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token comment">// target the contentType of the EMBEDDED_ENTRY to display as you need</span><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>contentType<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id <span class="token operator">===</span> <span class="token string">"codeBlock"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">return</span> <span class="token punctuation">(</span><br>          <span class="token operator">&lt;</span>pre<span class="token operator">></span><br>            <span class="token operator">&lt;</span>code<span class="token operator">></span><span class="token punctuation">{</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>code<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>code<span class="token operator">></span><br>          <span class="token operator">&lt;</span><span class="token operator">/</span>pre<span class="token operator">></span><br>        <span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><br><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>contentType<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id <span class="token operator">===</span> <span class="token string">"videoEmbed"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">return</span> <span class="token punctuation">(</span><br>          <span class="token operator">&lt;</span>iframe<br>            src<span class="token operator">=</span><span class="token punctuation">{</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>embedUrl<span class="token punctuation">}</span><br>            height<span class="token operator">=</span><span class="token string">"100%"</span><br>            width<span class="token operator">=</span><span class="token string">"100%"</span><br>            frameBorder<span class="token operator">=</span><span class="token string">"0"</span><br>            scrolling<span class="token operator">=</span><span class="token string">"no"</span><br>            title<span class="token operator">=</span><span class="token punctuation">{</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>title<span class="token punctuation">}</span><br>            allowFullScreen<span class="token operator">=</span><span class="token punctuation">{</span><span class="token boolean">true</span><span class="token punctuation">}</span><br>          <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br><br>    <span class="token punctuation">[</span><span class="token constant">BLOCKS</span><span class="token punctuation">.</span><span class="token constant">EMBEDDED_ASSET</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">node<span class="token punctuation">,</span> children</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token comment">// render the EMBEDDED_ASSET as you need</span><br>      <span class="token keyword">return</span> <span class="token punctuation">(</span><br>        <span class="token operator">&lt;</span>img<br>          src<span class="token operator">=</span><span class="token punctuation">{</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>file<span class="token punctuation">.</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">}</span><br>          height<span class="token operator">=</span><span class="token punctuation">{</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>file<span class="token punctuation">.</span>details<span class="token punctuation">.</span>image<span class="token punctuation">.</span>height<span class="token punctuation">}</span><br>          width<span class="token operator">=</span><span class="token punctuation">{</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>file<span class="token punctuation">.</span>details<span class="token punctuation">.</span>image<span class="token punctuation">.</span>width<span class="token punctuation">}</span><br>          alt<span class="token operator">=</span><span class="token punctuation">{</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>description<span class="token punctuation">}</span><br>        <span class="token operator">/</span><span class="token operator">></span><br>      <span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">BlogPost</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> post <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span><span class="token operator">></span><br>       <span class="token punctuation">{</span><span class="token function">documentToReactComponents</span><span class="token punctuation">(</span>post<span class="token punctuation">.</span>fields<span class="token punctuation">.</span>body<span class="token punctuation">,</span> renderOptions<span class="token punctuation">)</span><span class="token punctuation">}</span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">TL;DR: Don’t worry about links if you’re using an SDK and the contentful-rich-text-react-renderer! For each node in the Rich Text response, access the type of entry or asset using data.target.contentType.sys.id, and access the fields using data.target.fields and so on.</h3><p class="post__p">The SDK and the renderer package handles linked entries and assets beautifully for us. But how does it work when using the GraphQL API?</p><h2 class="post__h2">Rendering Rich Text references using the GraphQL API</h2><p class="post__p"><a href="https://www.contentful.com/developers/docs/references/graphql/" target="_blank">The Contentful GraphQL API</a> doesn’t require an SDK to handle linked entries. Understanding the concepts of links covered in <a href="https://www.contentful.com/blog/2021/04/14/exploring-linked-entries-assets-contentful/" target="_blank">this blog post</a> helps us out massively. </p><p class="post__p">To explore the GraphQL query in this example, navigate to the following URL and paste the query below into the explorer (without the <code>const</code> and <code>=</code>):</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="yUKKyJvDMy"
      aria-describedby="yUKKyJvDMy">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="yUKKyJvDMy">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="yUKKyJvDMy" itemprop="text" content="https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F84zl5qdw0ore%2Fexplore%3Faccess_token%3D_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA">
      <pre class="language-bash"><code class="language-bash">https://graphql.contentful.com/content/v1/spaces/84zl5qdw0ore/explore?access_token<span class="token operator">=</span>_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA</code></pre>
    </div>
  </div>

  <p class="post__p">The Rich Text field response from the GraphQL API is different and contains two top-level nodes<b class="post__p--italic"><b class="post__p--bold">.</b></b></p><p class="post__p">Here’s the GraphQL query for our blog post:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="VBZaslDECL"
      aria-describedby="VBZaslDECL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="VBZaslDECL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="VBZaslDECL" itemprop="text" content="const%20query%20%3D%20%60%7B%0A%20%20%20%20blogPostCollection(limit%3A%201%2C%20where%3A%20%7Bslug%3A%20%22the-power-of-the-contentful-rich-text-field%22%7D)%20%7B%0A%20%20%20%20%20%20items%20%7B%0A%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%23%20For%20this%20example%2C%20we%E2%80%99ll%20focus%20on%20the%20Rich%20Text%20field%20query%20below%0A%20%20%20%20%20%20%20%20%23%20and%20omit%20the%20rest%20of%20the%20blog%20post%20fields%0A%20%20%20%20%20%20%20%20body%20%7B%0A%20%20%20%20%20%20%20%20%20%20json%0A%20%20%20%20%20%20%20%20%20%20links%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20entries%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20inline%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20__typename%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20...%20on%20BlogPost%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20slug%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20block%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20__typename%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20...%20on%20CodeBlock%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20language%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20code%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20...%20on%20VideoEmbed%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20embedUrl%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20assets%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20block%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20width%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20height%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%60%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">{<br>    blogPostCollection(limit: 1, where: {slug: "the-power-of-the-contentful-rich-text-field"}) {<br>      items {<br>        sys {<br>          id<br>        }<br>        # For this example, we’ll focus on the Rich Text field query below<br>        # and omit the rest of the blog post fields<br>        body {<br>          json<br>          links {<br>            entries {<br>              inline {<br>                sys {<br>                  id<br>                }<br>                __typename<br>                ... on BlogPost {<br>                  title<br>                  slug<br>                }<br>              }<br>              block {<br>                sys {<br>                  id<br>                }<br>                __typename<br>                ... on CodeBlock {<br>                  description<br>                  language<br>                  code<br>                }<br>                ... on VideoEmbed {<br>                  embedUrl<br>                  title<br>                }<br>              }<br>            }<br>            assets {<br>              block {<br>                sys {<br>                  id<br>                }<br>                url<br>                title<br>                width<br>                height<br>                description<br>              }<br>            }<br>          }<br>        }<br>      }<br>    }<br>  }</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">And here’s how we can query the Contentful GraphQL API using fetch:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YaZehoDQFN"
      aria-describedby="YaZehoDQFN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YaZehoDQFN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="YaZehoDQFN" itemprop="text" content="const%20fetchOptions%20%3D%20%7B%0A%20%20method%3A%20%22POST%22%2C%0A%20%20headers%3A%20%7B%0A%20%20%20%20Authorization%3A%20%22Bearer%20%22%20%2B%20%7BACCESS_TOKEN%7D%2C%0A%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%7D%2C%0A%20%20body%3A%20JSON.stringify(%7B%20query%20%7D)%2C%0A%7D%3B%0A%0Aconst%20response%20%3D%20await%20fetch(%60https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%7BSPACE_ID%7D%60%2C%20fetchOptions).then((response)%20%3D%3E%20console.log(response.json()))%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> fetchOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token string">"Bearer "</span> <span class="token operator">+</span> <span class="token punctuation">{</span><span class="token constant">ACCESS_TOKEN</span><span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span><br><br><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://graphql.contentful.com/content/v1/spaces/{SPACE_ID}</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> fetchOptions<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">The Rich Text field response (<code>blogPost.body</code>) contains the following two top-level nodes — <code>json</code> and <code>links</code>. <code>json</code> includes the Rich Text JSON tree representing whatever people put into the editor. It is to point out that this JSON structure only includes ids to possibly linked references. These references can then be queried using the <code>links</code> node.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xFzvbnVmng"
      aria-describedby="xFzvbnVmng">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xFzvbnVmng">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="xFzvbnVmng" itemprop="text" content="%22body%22%3A%20%7B%0A%20%20%23%20JSON%20structure%20of%20the%20Rich%20Text%20field%0A%20%20%22json%22%3A%20%7B%0A%20%20%20%20%23%20...%0A%20%20%7D%0A%20%20%23%20all%20referenced%20assets%2Fentries%0A%20%20%22links%22%3A%20%7B%0A%20%20%20%20%23%20...%0A%20%20%7D%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token property">"body"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  # JSON structure of the Rich Text field<br>  <span class="token property">"json"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    # ...<br>  <span class="token punctuation">}</span><br>  # all referenced assets/entries<br>  <span class="token property">"links"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    # ...<br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">The references are not automatically resolved inside of the Rich Text JSON. This means we have to take a different approach to render and resolve links when using GraphQL.</p><h3 class="post__h3">Rendering the Rich Text response from GraphQL with linked assets and entries on the front end</h3><p class="post__p">We can still use <code>documentToReactComponents</code> to render our Rich Text field data to the DOM, but instead of passing in an options object, we’ll need to construct the object using a custom function to process a bit of logic to resolve our links.</p><p class="post__p">In order to target asset and entry data when rendering <code>BLOCKS.EMBEDDED_ENTRY</code> and <code>BLOCKS.EMBEDDED_ASSET</code> with <code>documentToReactComponents</code>, we can create an <code>assetMap</code> (id: asset) and <code>entryMap</code> (id: entry) to store data we can reference by ID.</p><p class="post__p">When the <code>renderOptions</code> reaches the entry and asset types, we can access the data from the maps we created at the top of the function, and render it accordingly. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="xfmmTMTUrg"
      aria-describedby="xfmmTMTUrg">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="xfmmTMTUrg">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="xfmmTMTUrg" itemprop="text" content="import%20%7B%20documentToReactComponents%20%7D%20from%20%22%40contentful%2Frich-text-react-renderer%22%3B%0Aimport%20%7B%20BLOCKS%2C%20INLINES%20%7D%20from%20%22%40contentful%2Frich-text-types%22%3B%0A%0A%2F%2F%20Create%20a%20bespoke%20renderOptions%20object%20to%20target%20BLOCKS.EMBEDDED_ENTRY%20(linked%20block%20entries%20e.g.%20code%20blocks)%0A%2F%2F%20INLINES.EMBEDDED_ENTRY%20(linked%20inline%20entries%20e.g.%20a%20reference%20to%20another%20blog%20post)%0A%2F%2F%20and%20BLOCKS.EMBEDDED_ASSET%20(linked%20assets%20e.g.%20images)%0A%0Afunction%20renderOptions(links)%20%7B%0A%20%20%2F%2F%20create%20an%20asset%20map%0A%20%20const%20assetMap%20%3D%20new%20Map()%3B%0A%20%20%2F%2F%20loop%20through%20the%20assets%20and%20add%20them%20to%20the%20map%0A%20%20for%20(const%20asset%20of%20links.assets.block)%20%7B%0A%20%20%20%20assetMap.set(asset.sys.id%2C%20asset)%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20create%20an%20entry%20map%0A%20%20const%20entryMap%20%3D%20new%20Map()%3B%0A%20%20%2F%2F%20loop%20through%20the%20block%20linked%20entries%20and%20add%20them%20to%20the%20map%0A%20%20for%20(const%20entry%20of%20links.entries.block)%20%7B%0A%20%20%20%20entryMap.set(entry.sys.id%2C%20entry)%3B%0A%20%20%7D%0A%0A%20%20%20%2F%2F%20loop%20through%20the%20inline%20linked%20entries%20and%20add%20them%20to%20the%20map%0A%20%20for%20(const%20entry%20of%20links.entries.inline)%20%7B%0A%20%20%20%20entryMap.set(entry.sys.id%2C%20entry)%3B%0A%20%20%7D%0A%0A%20%20return%20%7B%0A%20%20%20%20%2F%2F%20other%20options...%0A%0A%20%20%20%20renderNode%3A%20%7B%0A%20%20%20%20%20%20%2F%2F%20other%20options...%0A%20%20%20%20%20%20%20%5BINLINES.EMBEDDED_ENTRY%5D%3A%20(node%2C%20children)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20find%20the%20entry%20in%20the%20entryMap%20by%20ID%0A%20%20%20%20%20%20%20%20const%20entry%20%3D%20entryMap.get(node.data.target.sys.id)%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20render%20the%20entries%20as%20needed%0A%20%20%20%20%20%20%20%20if%20(entry.__typename%20%3D%3D%3D%20%22BlogPost%22)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%20%3Ca%20href%3D%7B%60%2Fblog%2F%24%7Bentry.slug%7D%60%7D%3E%7Bentry.title%7D%3C%2Fa%3E%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%5BBLOCKS.EMBEDDED_ENTRY%5D%3A%20(node%2C%20children)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20find%20the%20entry%20in%20the%20entryMap%20by%20ID%0A%20%20%20%20%20%20%20%20const%20entry%20%3D%20entryBlockMap.get(node.data.target.sys.id)%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20render%20the%20entries%20as%20needed%20by%20looking%20at%20the%20__typename%20%0A%20%20%20%20%20%20%20%20%2F%2F%20referenced%20in%20the%20GraphQL%20query%0A%20%20%20%20%20%20%20%20if%20(entry.__typename%20%3D%3D%3D%20%22CodeBlock%22)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpre%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3Ccode%3E%7Bentry.code%7D%3C%2Fcode%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2Fpre%3E%0A%20%20%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20if%20(entry.__typename%20%3D%3D%3D%20%22VideoEmbed%22)%20%7B%0A%20%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Ciframe%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20src%3D%7Bentry.embedUrl%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20height%3D%22100%25%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20width%3D%22100%25%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20frameBorder%3D%220%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20scrolling%3D%22no%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%3D%7Bentry.title%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20allowFullScreen%3D%7Btrue%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%3E%0A%20%20%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%5BBLOCKS.EMBEDDED_ASSET%5D%3A%20(node%2C%20next)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20find%20the%20asset%20in%20the%20assetMap%20by%20ID%0A%20%20%20%20%20%20%20%20const%20asset%20%3D%20assetMap.get(node.data.target.sys.id)%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20render%20the%20asset%20accordingly%0A%20%20%20%20%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20%20%20%3Cimg%20src%3D%7Basset.url%7D%20alt%3D%22My%20image%20alt%20text%22%20%2F%3E%0A%20%20%20%20%20%20%20%20)%3B%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%7D%2C%0A%20%20%7D%3B%0A%7D%0A%0A%2F%2F%20Render%20post.body.json%20to%20the%20DOM%20using%0A%2F%2F%20documentToReactComponents%20from%20%22%40contentful%2Frich-text-react-renderer%22%0A%0Aexport%20default%20function%20BlogPost(props)%20%7B%0A%20%20const%20%7B%20post%20%7D%20%3D%20props%3B%0A%0A%20%20return%20%3C%3E%7BdocumentToReactComponents(post.body.json%2C%20renderOptions(post.body.links))%7D%3C%2F%3E%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> documentToReactComponents <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@contentful/rich-text-react-renderer"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> <span class="token constant">BLOCKS</span><span class="token punctuation">,</span> <span class="token constant">INLINES</span> <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@contentful/rich-text-types"</span><span class="token punctuation">;</span><br><br><span class="token comment">// Create a bespoke renderOptions object to target BLOCKS.EMBEDDED_ENTRY (linked block entries e.g. code blocks)</span><br><span class="token comment">// INLINES.EMBEDDED_ENTRY (linked inline entries e.g. a reference to another blog post)</span><br><span class="token comment">// and BLOCKS.EMBEDDED_ASSET (linked assets e.g. images)</span><br><br><span class="token keyword">function</span> <span class="token function">renderOptions</span><span class="token punctuation">(</span><span class="token parameter">links</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// create an asset map</span><br>  <span class="token keyword">const</span> assetMap <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Map</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token comment">// loop through the assets and add them to the map</span><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> asset <span class="token keyword">of</span> links<span class="token punctuation">.</span>assets<span class="token punctuation">.</span>block<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    assetMap<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>asset<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token punctuation">,</span> asset<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token comment">// create an entry map</span><br>  <span class="token keyword">const</span> entryMap <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Map</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token comment">// loop through the block linked entries and add them to the map</span><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> entry <span class="token keyword">of</span> links<span class="token punctuation">.</span>entries<span class="token punctuation">.</span>block<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    entryMap<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>entry<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token punctuation">,</span> entry<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>   <span class="token comment">// loop through the inline linked entries and add them to the map</span><br>  <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> entry <span class="token keyword">of</span> links<span class="token punctuation">.</span>entries<span class="token punctuation">.</span>inline<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    entryMap<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>entry<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token punctuation">,</span> entry<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token comment">// other options...</span><br><br>    <span class="token literal-property property">renderNode</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>      <span class="token comment">// other options...</span><br>       <span class="token punctuation">[</span><span class="token constant">INLINES</span><span class="token punctuation">.</span><span class="token constant">EMBEDDED_ENTRY</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">node<span class="token punctuation">,</span> children</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token comment">// find the entry in the entryMap by ID</span><br>        <span class="token keyword">const</span> entry <span class="token operator">=</span> entryMap<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// render the entries as needed</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>entry<span class="token punctuation">.</span>__typename <span class="token operator">===</span> <span class="token string">"BlogPost"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          <span class="token keyword">return</span> <span class="token operator">&lt;</span>a href<span class="token operator">=</span><span class="token punctuation">{</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/blog/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>entry<span class="token punctuation">.</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">}</span><span class="token operator">></span><span class="token punctuation">{</span>entry<span class="token punctuation">.</span>title<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>a<span class="token operator">></span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">[</span><span class="token constant">BLOCKS</span><span class="token punctuation">.</span><span class="token constant">EMBEDDED_ENTRY</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">node<span class="token punctuation">,</span> children</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token comment">// find the entry in the entryMap by ID</span><br>        <span class="token keyword">const</span> entry <span class="token operator">=</span> entryBlockMap<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// render the entries as needed by looking at the __typename </span><br>        <span class="token comment">// referenced in the GraphQL query</span><br>        <span class="token keyword">if</span> <span class="token punctuation">(</span>entry<span class="token punctuation">.</span>__typename <span class="token operator">===</span> <span class="token string">"CodeBlock"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>          <span class="token keyword">return</span> <span class="token punctuation">(</span><br>            <span class="token operator">&lt;</span>pre<span class="token operator">></span><br>              <span class="token operator">&lt;</span>code<span class="token operator">></span><span class="token punctuation">{</span>entry<span class="token punctuation">.</span>code<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>code<span class="token operator">></span><br>            <span class="token operator">&lt;</span><span class="token operator">/</span>pre<span class="token operator">></span><br>          <span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br><br>       <span class="token keyword">if</span> <span class="token punctuation">(</span>entry<span class="token punctuation">.</span>__typename <span class="token operator">===</span> <span class="token string">"VideoEmbed"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>         <span class="token keyword">return</span> <span class="token punctuation">(</span><br>            <span class="token operator">&lt;</span>iframe<br>              src<span class="token operator">=</span><span class="token punctuation">{</span>entry<span class="token punctuation">.</span>embedUrl<span class="token punctuation">}</span><br>              height<span class="token operator">=</span><span class="token string">"100%"</span><br>              width<span class="token operator">=</span><span class="token string">"100%"</span><br>              frameBorder<span class="token operator">=</span><span class="token string">"0"</span><br>              scrolling<span class="token operator">=</span><span class="token string">"no"</span><br>              title<span class="token operator">=</span><span class="token punctuation">{</span>entry<span class="token punctuation">.</span>title<span class="token punctuation">}</span><br>              allowFullScreen<span class="token operator">=</span><span class="token punctuation">{</span><span class="token boolean">true</span><span class="token punctuation">}</span><br>            <span class="token operator">/</span><span class="token operator">></span><br>          <span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token punctuation">}</span><br><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>      <span class="token punctuation">[</span><span class="token constant">BLOCKS</span><span class="token punctuation">.</span><span class="token constant">EMBEDDED_ASSET</span><span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">node<span class="token punctuation">,</span> next</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token comment">// find the asset in the assetMap by ID</span><br>        <span class="token keyword">const</span> asset <span class="token operator">=</span> assetMap<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span>node<span class="token punctuation">.</span>data<span class="token punctuation">.</span>target<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// render the asset accordingly</span><br>        <span class="token keyword">return</span> <span class="token punctuation">(</span><br>          <span class="token operator">&lt;</span>img src<span class="token operator">=</span><span class="token punctuation">{</span>asset<span class="token punctuation">.</span>url<span class="token punctuation">}</span> alt<span class="token operator">=</span><span class="token string">"My image alt text"</span> <span class="token operator">/</span><span class="token operator">></span><br>        <span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Render post.body.json to the DOM using</span><br><span class="token comment">// documentToReactComponents from "@contentful/rich-text-react-renderer"</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">BlogPost</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> post <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token operator">&lt;</span><span class="token operator">></span><span class="token punctuation">{</span><span class="token function">documentToReactComponents</span><span class="token punctuation">(</span>post<span class="token punctuation">.</span>body<span class="token punctuation">.</span>json<span class="token punctuation">,</span> <span class="token function">renderOptions</span><span class="token punctuation">(</span>post<span class="token punctuation">.</span>body<span class="token punctuation">.</span>links<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span><span class="token operator">></span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">And there we have it! It’s a little more work to render our links with GraphQL, but if we understand how the SDK works, its magic and how links work across Contentful as a whole, we’re all set.</p><p class="post__p">Take a look at this <a href="https://github.com/whitep4nth3r/contentful-graphql-vs-rest" target="_blank"><u>example repository on GitHub,</u></a> which is a demo Next.js application that contains all of the example code in this post, and demonstrates how we can fetch this data and render the Rich Text field linked assets and entries using both the REST API with JavaScript SDK and the GraphQL API. </p><h3 class="post__h3">To wrap up</h3><p class="post__p"><b class="post__p--bold">Using the JavaScript SDK with the REST API and the contentful-rich-text-react-renderer we can define our renderOptions without worrying about having to resolve our links. All the data we need is available via node.data.target.</b></p><p class="post__p"><b class="post__p--bold">Using the GraphQL API and the contentful-rich-text-react-renderer, we have to perform the mapping of the linked entries ourselves, which we can do when defining our renderOptions and passing in the links as an additional parameter.</b></p><p class="post__p">The power of the Contentful Rich Text field is that it is stored in pure JSON data. With the knowledge of how linked assets and entries are referenced at a content type level, you’re empowered to render the contents of your Contentful Rich Text fields, with or without SDKs or other supporting packages. Go forth and build stuff!</p><p class="post__p">If you’ve got any questions about linked assets and entries in Contentful, come and join the <a href="https://www.contentful.com/slack/" target="_blank">Contentful Community Slack</a>, where we’ll be more than happy to help!</p><p class="post__p">And remember, build stuff, learn things and love what you do.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to build, test and release a node module in ES6</title>
          <description>If you Google "build test release npm module" this is the top result. Cool, huh?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-build-test-and-release-node-module-es6/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-build-test-and-release-node-module-es6/</guid>
          <pubDate>Sat, 10 Apr 2021 23:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>NodeJS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I had never actually built or released a node module before <a href="https://www.npmjs.com/package/@whitep4nth3r/random-code" target="_blank"><u>@whitep4nth3r/random-code</u></a>, and <a href="https://medium.com/swlh/how-to-publish-an-es6-module-to-npm-43dda8aabbf" target="_blank"><u>this blog post by Alec Mather</u></a> was super helpful in understanding the concepts. The most important thing I learned was that <b class="post__p--bold">if we want to write the code for the node module in ES6, we need a way to transpile the code from ES6 to ES5 so that it’s compatible with anyone’s code</b>.</p><p class="post__p">I don’t intend to recreate the tutorial in this post, but instead, write out the process for future me. I can see myself harnessing the power of building and releasing node modules<b class="post__p--bold"> much more</b> as time goes on!</p><p class="post__p">If you’d prefer to watch this process from start to finish, check out this five-minute quick fire-video on YouTube that shows me learning how to create my first node module and publish it to npm — live on Twitch!</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/GCjqV8InkBc"
        title="How to build, test and publish and npm package in es6"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p">Let’s go through step by step how to create, build and publish a package to npm that’s written in ES6. </p><h2 class="post__h2">Prerequisites</h2><p class="post__p">Ensure you’ve installed Node.js and npm on your machine.</p><h2 class="post__h2">Create an account on npm</h2><p class="post__p">You’ll need this to be able to publish your package. <a href="https://www.npmjs.com/signup" target="_blank"><u>Sign up here</u></a>.</p><h2 class="post__h2">Login to npm via your terminal</h2><p class="post__p">Run <code>npm login</code> in your terminal and enter your username, password and email. This will ensure you can publish your package later via the CLI.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2LaGBjAFuLOE5Doud4ho1X/debefa2ffce3c7708c655e08a6e50290/npm_login.png" alt="A screenshot of the output in a terminal after running npm login" height="694" width="2352" /><h2 class="post__h2">Set up your project</h2><p class="post__p">Create a new directory for your project that will contain the code for your npm package. Navigate to that directory. For unscoped modules (without the @scope-name prefix), run <code>npm init</code>. This will create the package.json file for your project. </p><p class="post__p">For my first node module, I decided to create a <b class="post__p--bold">scoped public package</b>, so that it would include my brand name. To initialise a scoped module, run <code>npm init --scope=@scope-name</code>. Follow the instructions in the terminal to configure your project. <a href="https://docs.npmjs.com/creating-and-publishing-scoped-public-packages#publishing-scoped-public-packages" target="_blank"><u>Read more about scoped public packages here</u></a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="miPajoEAvG"
      aria-describedby="miPajoEAvG">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="miPajoEAvG">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="miPajoEAvG" itemprop="text" content="mkdir%20my-new-npm-package%0Acd%20my-new-npm-package%0Anpm%20init%0A%23%20or%20for%20scoped%20packages%0Anpm%20init%20--scope%3D%40scope-name">
      <pre class="language-bash"><code class="language-bash"><span class="token function">mkdir</span> my-new-npm-package<br><span class="token builtin class-name">cd</span> my-new-npm-package<br><span class="token function">npm</span> init<br><span class="token comment"># or for scoped packages</span><br><span class="token function">npm</span> init <span class="token parameter variable">--scope</span><span class="token operator">=</span>@scope-name</code></pre>
    </div>
  </div>

  <p class="post__p">Here’s the package.json file that was created via the CLI for @whitep4nth3r/random-code.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ADqfxMXgph"
      aria-describedby="ADqfxMXgph">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ADqfxMXgph">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="ADqfxMXgph" itemprop="text" content="%7B%0A%20%20%22name%22%3A%20%22%40whitep4nth3r%2Frandom-code%22%2C%0A%20%20%22version%22%3A%20%221.0.0%22%2C%0A%20%20%22description%22%3A%20%22Need%20some%20code%20for%20your%20project%3F%20We've%20got%20you%20covered.%20Choose%20your%20language.%20Choose%20how%20much%20code.%20B%C3%84M!%20You%20got%20code.%22%2C%0A%20%20%22main%22%3A%20%22index.js%22%2C%0A%20%20%22scripts%22%3A%20%7B%0A%20%20%20%20%22test%22%3A%20%22echo%20%5C%22Error%3A%20no%20test%20specified%5C%22%20%26%26%20exit%201%22%0A%20%20%7D%2C%0A%20%20%22keywords%22%3A%20%5B%0A%20%20%20%20%22random%22%0A%20%20%5D%2C%0A%20%20%22author%22%3A%20%22whitep4nth3r%22%2C%0A%20%20%22license%22%3A%20%22MIT%22%0A%7D">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"@whitep4nth3r/random-code"</span><span class="token punctuation">,</span><br>  <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"1.0.0"</span><span class="token punctuation">,</span><br>  <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"Need some code for your project? We've got you covered. Choose your language. Choose how much code. BÄM! You got code."</span><span class="token punctuation">,</span><br>  <span class="token property">"main"</span><span class="token operator">:</span> <span class="token string">"index.js"</span><span class="token punctuation">,</span><br>  <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token property">"test"</span><span class="token operator">:</span> <span class="token string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token property">"keywords"</span><span class="token operator">:</span> <span class="token punctuation">[</span><br>    <span class="token string">"random"</span><br>  <span class="token punctuation">]</span><span class="token punctuation">,</span><br>  <span class="token property">"author"</span><span class="token operator">:</span> <span class="token string">"whitep4nth3r"</span><span class="token punctuation">,</span><br>  <span class="token property">"license"</span><span class="token operator">:</span> <span class="token string">"MIT"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">At the root of your new project, create the following directories:</p><ul><li><p class="post__p"> <code>src</code> — this is where we’ll store our ES6 code files</p></li><li><p class="post__p"> <code>dist</code> — this is where we’ll store the transpiled ES5 code</p></li></ul><p class="post__p">Inside the <code>src</code> folder, create an <code>index.js</code> file. This is the file that will export all of your ES6 code from this directory.</p><p class="post__p">Finally, at the root of the project, create an <code>index.js</code> file, and add this line of code.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="wFLsuwqwCh"
      aria-describedby="wFLsuwqwCh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="wFLsuwqwCh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="wFLsuwqwCh" itemprop="text" content="module.exports%20%3D%20require(%22.%2Fdist%22)%3B">
      <pre class="language-javascript"><code class="language-javascript">module<span class="token punctuation">.</span>exports <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"./dist"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">This is the entry point to our application, as specified in the <code>main</code> field in the package.json file. This instructs whatever code is consuming the node module to load all of the contents of the <code>dist</code> directory, where our transpiled ES5 code will be.</p><p class="post__p">Here’s how your project structure should look so far:</p><img src="https://images.ctfassets.net/56dzm01z6lln/4pIbMqfR6ltok2SRUEyPGn/a5d9ab4f652d4a6b306049894a895a03/folder_structure.png" alt="A screenshot of the folder tree in VS code showing the dist directory, src directory with index.js inside, and inside.js and package.json at the root" height="492" width="762" /><h2 class="post__h2">Write some ES6 code in the src directory</h2><p class="post__p">I can’t help you with this bit — but go wild! Ensure that each function you want to export from the <code>src</code> directory in <code>index.js</code> is prefixed with <code>export</code>. <a href="https://github.com/whitep4nth3r/random-code/blob/main/src/index.js" target="_blank"><u>Check out the equivalent file for the random-code node module on GitHub</u></a>.</p><p class="post__p">At this point you’ll probably want to create a git repository for your node module package to ensure your hard work is version-controlled.</p><h2 class="post__h2">Transpile your ES6 code to ES5 using babel</h2><p class="post__p">We need to install two dev dependencies to transpile the ES6 code to ES5.</p><p class="post__p">In your terminal, run:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="FVALhnAkmX"
      aria-describedby="FVALhnAkmX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="FVALhnAkmX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="FVALhnAkmX" itemprop="text" content="npm%20install%20--save-dev%20%40babel%2Fcli%20%40babel%2Fcore%20%40babel%2Fpreset-env">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> --save-dev @babel/cli @babel/core @babel/preset-env</code></pre>
    </div>
  </div>

  <p class="post__p">Next, at the root of your project, add a <code>.babelrc</code> file, and add the following lines:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="jvzkyNPiXZ"
      aria-describedby="jvzkyNPiXZ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="jvzkyNPiXZ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="jvzkyNPiXZ" itemprop="text" content="%7B%0A%20%20%22presets%22%3A%20%5B%22%40babel%2Fpreset-env%22%5D%0A%7D%0A">
      <pre class="language-json"><code class="language-json"><span class="token punctuation">{</span><br>  <span class="token property">"presets"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"@babel/preset-env"</span><span class="token punctuation">]</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Next, add the following build command to your package.json file.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="lPdgQXxKIU"
      aria-describedby="lPdgQXxKIU">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="lPdgQXxKIU">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="json">
      <meta data-code-id="lPdgQXxKIU" itemprop="text" content="%22scripts%22%3A%20%7B%0A%20%20%22build%22%3A%20%22babel%20src%20-d%20dist%22%0A%7D%0A">
      <pre class="language-json"><code class="language-json"><span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>  <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"babel src -d dist"</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Now, when you run <code>npm run build</code>, babel will transpile all of the code from the <code>src</code> directory from ES6 to ES5, and place it in <code>dist</code>. Make sure you run <code>npm run build</code> each time you want to test your code locally in a different directory or project.</p><p class="post__p">Wait, I can test my npm package locally? You sure can! Here’s how.</p><h2 class="post__h2">Test your node module before you publish</h2><p class="post__p">We can use <code>npm link</code> to test out the functionality of an npm package without publishing it.</p><p class="post__p">In your node module project directory, run the following command:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="GjCeuCEran"
      aria-describedby="GjCeuCEran">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="GjCeuCEran">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="GjCeuCEran" itemprop="text" content="npm%20link%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">link</span></code></pre>
    </div>
  </div>

  <p class="post__p">In an existing project directory, or a new directory where you wish to test this npm package (assuming the project already has a package.json), run the following command:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mofJYCiMzV"
      aria-describedby="mofJYCiMzV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mofJYCiMzV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="mofJYCiMzV" itemprop="text" content="npm%20link%20my-new-npm-package%0A%23%20or%20for%20scoped%20projects%0Anpm%20link%20%40scope-name%2Fmy-new-npm-package%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">link</span> my-new-npm-package<br><span class="token comment"># or for scoped projects</span><br><span class="token function">npm</span> <span class="token function">link</span> @scope-name/my-new-npm-package</code></pre>
    </div>
  </div>

  <p class="post__p">You can now import the node module as you would if it was published to npm, for example:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="YfQLYECTNy"
      aria-describedby="YfQLYECTNy">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="YfQLYECTNy">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="YfQLYECTNy" itemprop="text" content="import%20%7B%20getLanguages%2C%20generateRandomCode%20%7D%20from%20%22%40whitep4nth3r%2Frandom-code%22%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> <span class="token punctuation">{</span> getLanguages<span class="token punctuation">,</span> generateRandomCode <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"@whitep4nth3r/random-code"</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Publish your new node module</h2><p class="post__p">When you’ve tested your new node module and you’re happy with the results, it’s ready to be published!</p><p class="post__p">At the root of your npm package directory, make sure you are logged into npm via the CLI as described above, and run the following command in your terminal:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="XCjPhzEhMy"
      aria-describedby="XCjPhzEhMy">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="XCjPhzEhMy">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="XCjPhzEhMy" itemprop="text" content="%23%20for%20unscoped%20packages%0Anpm%20publish%0A%23%20for%20scoped%20packages%0Anpm%20publish%20--access%20public%0A">
      <pre class="language-bash"><code class="language-bash"><span class="token comment"># for unscoped packages</span><br><span class="token function">npm</span> publish<br><span class="token comment"># for scoped packages</span><br><span class="token function">npm</span> publish <span class="token parameter variable">--access</span> public</code></pre>
    </div>
  </div>

  <p class="post__p"><b class="post__p--italic">Ensure that you increment the version number in package.json each time you want to publish.</b></p><p class="post__p">And there you have it! </p><p class="post__p"><a href="https://www.npmjs.com/package/@whitep4nth3r/random-code" target="_blank"><u>View the @whitep4nther/random-code node module on npm.</u></a></p><p class="post__p">The npm ecosystem is a great way to distribute useful blocks of reusable code. Through learning how to build and publish packages to npm, I feel like I’ve really levelled-up my web dev skills. Good luck in creating your first npm package!</p><p class="post__p">If you&#39;ve tried out the random-code package on npm, I&#39;d love to hear about it! <a href="https://twitch.tv/whitep4nth3r" target="_blank">Come and say hi on Twitch</a>!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>3 ways to use Puppeteer to generate Open Graph images</title>
          <description>Take screenshots of browser pages and generate dynamic images to share on your social media accounts.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/03/17/puppeteer-node-open-graph-screenshot-for-socials/</link>
          <guid>https://www.contentful.com/blog/2021/03/17/puppeteer-node-open-graph-screenshot-for-socials/</guid>
          <pubDate>Wed, 17 Mar 2021 15:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Serverless</category><category>NodeJS</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p"></p><p class="post__p">It’s no secret that you drive more engagement when you share beautiful images with links on social media. But generating fresh images each time you publish a new blog post can be incredibly time-consuming. The good news is, we can harness the power of a tool called Puppeteer to take screenshots of browser pages and generate dynamic images to share on your social media accounts. Let’s take a look!</p><h2 class="post__h2">What is Open Graph?</h2><p class="post__p">The Open Graph (OG) protocol was created at Facebook in 2010 to enable web page links to become rich objects with similar functionality and appearance to other content posted on Facebook. If you’ve shared a link on social media and have seen the platform automatically show you a large image, title, description and URL for the link you want to share even before you’ve clicked Post — you’ve seen the OG protocol at work.</p><p class="post__p">Open Graph meta tags are used in the <code>&lt;head&gt;</code> of an HTML page to expose information about web pages to social media platforms and other applications that unfurl URL metadata. OG meta tags are identified in the HTML by a property attribute prefixed with <code>og</code>.  </p><p class="post__p">This is an example of an Open Graph meta tag. It provides a url to an image that is used to represent the web page.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ilAbOsPCfL"
      aria-describedby="ilAbOsPCfL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ilAbOsPCfL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="ilAbOsPCfL" itemprop="text" content="%3Cmeta%20property%3D%22og%3Aimage%22%20content%3D%22https%3A%2F%2Fexample.com%2Fimage.png%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">property</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>og:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/image.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">OG meta tags can also be used to customize the appearance of your web pages according to the platform it’s shared on. For example, Twitter rolled out their own custom implementation of this, built on the OG protocol, and the following code tells Twitter to show the large image web page previews.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="iXAtqeDLFV"
      aria-describedby="iXAtqeDLFV">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="iXAtqeDLFV">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="iXAtqeDLFV" itemprop="text" content="%3Cmeta%20name%3D%22twitter%3Acard%22%20content%3D%22summary_large_image%22%20%2F%3E%0A%3Cmeta%20name%3D%22twitter%3Aimage%22%20content%3D%22https%3A%2F%2Fexample.com%2Fimage.png%22%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:card<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>summary_large_image<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>twitter:image<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com/image.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p"><a href="https://ogp.me/" target="_blank">Read more about the Open Graph protocol here.</a></p><p class="post__p">A basic — yet useful — implementation of an Open Graph image tag on your web pages should point to a static image. But in a world of sensory overload, how do you ensure your OG images provide useful context to your audience for the link you are sharing? How do you create automatically generated dynamic OG images for your web pages without having to open up Photoshop each time you want to promote a new post?</p><p class="post__p">Enter Puppeteer!</p><p class="post__p">Puppeteer is a Node library that provides a high-level API to control headless Chrome or Chromium. “Headless” browsers allow you to automate your interactions with a browser-like environment via a command-line interface. Most things that you can do manually in the browser can be done using Puppeteer.</p><p class="post__p"><a href="https://developers.google.com/web/tools/puppeteer/get-started" target="_blank">Read the Puppeteer documentation.</a></p><p class="post__p">A great way to elevate your Open-Graph-image game is to harness the power of Puppeteer by providing a link in your OG image tags that calls out to a serverless function that generates a screenshot of a browser page.</p><p class="post__p">Other uses for the automation of generating screenshots of web pages might include build pipeline tools that check for web page regressions during new feature releases, or to provide richer experiences in the front end for your audience. Vercel had a great example of this. They show a preview of your application in your deployment dashboard.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1OnBL11fUnIGdU9qhpZ4ae/f589b5b5e2e81c2785578cd250099f00/vercel_dashboard.png" alt="A screenshot of the deployment dashboard in Vercel which shows a screenshot of the deployed website" height="650" width="1050" /><p class="post__p">If you want to dive straight into some example code, <a href="https://github.com/whitep4nth3r/puppeteer-demo" target="_blank">this repository demonstrates how you can get started with Puppeteer to generate screenshots of web page pages</a>. </p><h3 class="post__h3">I can take screenshots of webpages with code? This sounds pretty magical! How does it work?</h3><p class="post__p">This <a href="https://github.com/whitep4nth3r/puppeteer-demo" target="_blank">puppeteer-demo repository</a> contains a <a href="https://vercel.com/docs/serverless-functions/introduction" target="_blank">Vercel serverless function</a> that runs in the browser and accepts a URL parameter of <code>page</code>. This would be a URL of a web page you want to screenshot, including <code>https://</code>.</p><p class="post__p">Give it a try by visiting this URL: <a href="https://puppeteer-screenshot-demo.vercel.app/api/screenshot?page=https://whitep4nth3r.com" target="_blank">https://puppeteer-screenshot-demo.vercel.app/api/screenshot?page=https://whitep4nth3r.com</a></p><p class="post__p">Here’s what you’ll see in the browser.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2F657x0IjrGZd26MRZZ9qe/35c0b663569275f673604e72158f8b84/function_screenshot.png" alt="A screenshot of Chrome browser with the Vercel function URL in the address bar, showing the generated screenshot from running the serverless function" height="799" width="1275" /><p class="post__p">Switch out the page parameter for a different URL to watch it in action!</p><h3 class="post__h3">Let’s take a look at what just happened when you called the URL in the browser.</h3><ol><li><p class="post__p">You called out to a serverless function hosted on Vercel</p></li><li><p class="post__p">The instance of the function installed Puppeteer and spun up a Chrome headless browser in memory</p></li><li><p class="post__p">The installation of Chrome launched, and visited the provided URL with a viewport size specified of 1920 x 1080</p></li><li><p class="post__p">Chrome took a screenshot of the page as a .png file in the viewport and the function saved the file in memory</p></li><li><p class="post__p">The instance of Chrome closed itself</p></li><li><p class="post__p">The function returned the file to the browser with a Content-Type of image/png</p></li></ol><p class="post__p">That’s pretty cool, right?</p><p class="post__p">If this excites you as much as it excited me when I first discovered the capabilities of Puppeteer, read on to see three ways in which you can use Puppeteer to take screenshots of web pages: on the command line, as a serverless function in the background or in a front-end application.</p><h3 class="post__h3">1. Generating screenshots on the command line</h3><p class="post__p">First, clone the repo to your local machine.</p><p class="post__p">Using git HTTPS:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="JDdoWnUqhI"
      aria-describedby="JDdoWnUqhI">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="JDdoWnUqhI">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="JDdoWnUqhI" itemprop="text" content="git%20clone%20https%3A%2F%2Fgithub.com%2Fwhitep4nth3r%2Fpuppeteer-demo.git">
      <pre class="language-bash"><code class="language-bash"><span class="token function">git</span> clone https://github.com/whitep4nth3r/puppeteer-demo.git</code></pre>
    </div>
  </div>

  <p class="post__p">Or, using the GitHub CLI:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vrBMyqijKj"
      aria-describedby="vrBMyqijKj">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vrBMyqijKj">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="vrBMyqijKj" itemprop="text" content="gh%20repo%20clone%20whitep4nth3r%2Fpuppeteer-demo">
      <pre class="language-bash"><code class="language-bash">gh repo clone whitep4nth3r/puppeteer-demo</code></pre>
    </div>
  </div>

  <p class="post__p">Install dependencies:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DJFhbNYpiZ"
      aria-describedby="DJFhbNYpiZ">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DJFhbNYpiZ">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="DJFhbNYpiZ" itemprop="text" content="cd%20puppeteer-demo%0Anpm%20install%0A%23or%0Ayarn%20install">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> puppeteer-demo<br><span class="token function">npm</span> <span class="token function">install</span><br><span class="token comment">#or</span><br><span class="token function">yarn</span> <span class="token function">install</span></code></pre>
    </div>
  </div>

  <p class="post__p">Run the following command in your terminal at the root of the project directory. You can also pass in multiple URLs separated by a space.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="oWJdRNhUDm"
      aria-describedby="oWJdRNhUDm">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="oWJdRNhUDm">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="oWJdRNhUDm" itemprop="text" content="cd%20puppeteer-demo%0Anode%20demo.js%20https%3A%2F%2Fwhitep4nth3r.com%0A%23or%20e.g.%0Anode%20demo.js%20https%3A%2F%2Fwhitep4nth3r.com%20https%3A%2F%2Fstefanjudis.com">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> puppeteer-demo<br><span class="token function">node</span> demo.js https://whitep4nth3r.com<br><span class="token comment">#or e.g.</span><br><span class="token function">node</span> demo.js https://whitep4nth3r.com https://stefanjudis.com</code></pre>
    </div>
  </div>

  <p class="post__p">You’ll notice that a new .png file (or files) will be created in the <code>screenshots</code> directory from the URL provided.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2CgHfpNEGLoQAYl3hZw1Hl/41569b90c55ca6c49a8d26525a1f31bf/file_tree_screenshots.png" alt="A screenshot of the file tree in VS Code showing the newly created png files" height="223" width="670" /><p class="post__p">Let’s look at the code.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="vEZABQoWEo"
      aria-describedby="vEZABQoWEo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="vEZABQoWEo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="vEZABQoWEo" itemprop="text" content="const%20puppeteer%20%3D%20require(%22puppeteer%22)%3B%0A%0A(async%20()%20%3D%3E%20%7B%0A%20%20%2F%2F%20Take%20the%20urls%20from%20the%20command%20line%0A%20%20var%20args%20%3D%20process.argv.slice(2)%3B%0A%0A%20%20try%20%7B%0A%20%20%20%20%2F%2F%20launch%20a%20new%20headless%20browser%0A%20%20%20%20const%20browser%20%3D%20await%20puppeteer.launch()%3B%0A%0A%20%20%20%20%2F%2F%20loop%20over%20the%20urls%0A%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20args.length%3B%20i%2B%2B)%20%7B%0A%0A%20%20%20%20%20%20%2F%2F%20check%20for%20https%20for%20safety!%0A%20%20%20%20%20%20if%20(args%5Bi%5D.includes(%22https%3A%2F%2F%22))%20%7B%0A%20%20%20%20%20%20%20%20const%20page%20%3D%20await%20browser.newPage()%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20set%20the%20viewport%20size%0A%20%20%20%20%20%20%20%20await%20page.setViewport(%7B%0A%20%20%20%20%20%20%20%20%20%20width%3A%201920%2C%0A%20%20%20%20%20%20%20%20%20%20height%3A%201080%2C%0A%20%20%20%20%20%20%20%20%20%20deviceScaleFactor%3A%201%2C%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20tell%20the%20page%20to%20visit%20the%20url%0A%20%20%20%20%20%20%20%20await%20page.goto(args%5Bi%5D)%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20take%20a%20screenshot%20and%20save%20it%20in%20the%20screenshots%20directory%0A%0A%20%20%20%20%20%20%20%20await%20page.screenshot(%7B%20%0A%20%20%20%20%20%20%20%20%20%20path%3A%20%60.%2Fscreenshots%2F%24%7Bargs%5Bi%5D.replace(%22https%3A%2F%2F%22%2C%20%22%22)%7D.png%60%20%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%20%20%20%20%2F%2F%20done!%0A%20%20%20%20%20%20%20%20console.log(%60%E2%9C%85%20Screenshot%20of%20%24%7Bargs%5Bi%5D%7D%20saved!%60)%3B%0A%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20console.error(%60%E2%9D%8C%20Could%20not%20save%20screenshot%20of%20%24%7Bargs%5Bi%5D%7D!%60)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20close%20the%20browser%0A%20%20%20%20await%20browser.close()%3B%0A%20%20%7D%20catch%20(error)%20%7B%0A%20%20%20%20console.log(error)%3B%0A%20%20%7D%0A%7D)()%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> puppeteer <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"puppeteer"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token comment">// Take the urls from the command line</span><br>  <span class="token keyword">var</span> args <span class="token operator">=</span> process<span class="token punctuation">.</span>argv<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token comment">// launch a new headless browser</span><br>    <span class="token keyword">const</span> browser <span class="token operator">=</span> <span class="token keyword">await</span> puppeteer<span class="token punctuation">.</span><span class="token function">launch</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// loop over the urls</span><br>    <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator">&lt;</span> args<span class="token punctuation">.</span>length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br>      <span class="token comment">// check for https for safety!</span><br>      <span class="token keyword">if</span> <span class="token punctuation">(</span>args<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">"https://"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>        <span class="token keyword">const</span> page <span class="token operator">=</span> <span class="token keyword">await</span> browser<span class="token punctuation">.</span><span class="token function">newPage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// set the viewport size</span><br>        <span class="token keyword">await</span> page<span class="token punctuation">.</span><span class="token function">setViewport</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>          <span class="token literal-property property">width</span><span class="token operator">:</span> <span class="token number">1920</span><span class="token punctuation">,</span><br>          <span class="token literal-property property">height</span><span class="token operator">:</span> <span class="token number">1080</span><span class="token punctuation">,</span><br>          <span class="token literal-property property">deviceScaleFactor</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br>        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// tell the page to visit the url</span><br>        <span class="token keyword">await</span> page<span class="token punctuation">.</span><span class="token function">goto</span><span class="token punctuation">(</span>args<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// take a screenshot and save it in the screenshots directory</span><br><br>        <span class="token keyword">await</span> page<span class="token punctuation">.</span><span class="token function">screenshot</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <br>          <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./screenshots/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>args<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">"https://"</span><span class="token punctuation">,</span> <span class="token string">""</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.png</span><span class="token template-punctuation string">`</span></span> <br>        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>        <span class="token comment">// done!</span><br>        console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">✅ Screenshot of </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>args<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> saved!</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>        console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">❌ Could not save screenshot of </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>args<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">!</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><br>    <span class="token punctuation">}</span><br><br>    <span class="token comment">// close the browser</span><br>    <span class="token keyword">await</span> browser<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">2. Running a serverless function in the browser</h2><p class="post__p">Vercel serverless functions can have a maximum size of 50MB. The code on the server (<a href="https://github.com/whitep4nth3r/puppeteer-demo/blob/main/api/screenshot.js" target="_blank">api/screenshot.js</a>) uses the <code>puppeteer-core</code> package (which comes without any headless browser installed) instead of the full <code>puppeteer</code> installation. To take the screenshot, we install a light version of Chrome via <code>chrome-aws-lambda</code> on the server. This keeps the function size down.</p><p class="post__p">Additionally, we configure the <code>puppeteer.launch()</code> options differently for development and production. For safety on the server, we only accept one URL.</p><p class="post__p">To run the serverless function locally, you’ll need to install the <a href="https://vercel.com/download" target="_blank"><u>Vercel CLI</u></a>. </p><p class="post__p">To install the Vercel CLI globally via npm, run:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="CktZzDghkX"
      aria-describedby="CktZzDghkX">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="CktZzDghkX">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="CktZzDghkX" itemprop="text" content="npm%20i%20-g%20vercel">
      <pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> i <span class="token parameter variable">-g</span> vercel</code></pre>
    </div>
  </div>

  <p class="post__p">To start the development server, open your terminal and run:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="mwRujksUop"
      aria-describedby="mwRujksUop">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="mwRujksUop">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="mwRujksUop" itemprop="text" content="cd%20puppeteer-demo%0Avercel%20dev">
      <pre class="language-bash"><code class="language-bash"><span class="token builtin class-name">cd</span> puppeteer-demo<br>vercel dev</code></pre>
    </div>
  </div>

  <p class="post__p">To ensure Puppeteer is given the correct options for the development environment, pass the query parameter <code>isDev=true</code> to the function. Here’s an example of how to run the serverless function in your local development environment.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="qYQcNLBVRh"
      aria-describedby="qYQcNLBVRh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="qYQcNLBVRh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="bash">
      <meta data-code-id="qYQcNLBVRh" itemprop="text" content="http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fscreenshot%3Fpage%3Dhttps%3A%2F%2Fwhitep4nth3r.com%26isDev%3Dtrue%0A">
      <pre class="language-bash"><code class="language-bash">http://localhost:3000/api/screenshot?page<span class="token operator">=</span>https://whitep4nth3r.com<span class="token operator">&amp;</span><span class="token assign-left variable">isDev</span><span class="token operator">=</span>true</code></pre>
    </div>
  </div>

  <p class="post__p">Here’s the serverless function adapted from the code which runs on the local command line.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="EUqXVkSmyM"
      aria-describedby="EUqXVkSmyM">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="EUqXVkSmyM">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="EUqXVkSmyM" itemprop="text" content="const%20puppeteer%20%3D%20require(%22puppeteer-core%22)%3B%0Aconst%20chrome%20%3D%20require(%22chrome-aws-lambda%22)%3B%0A%0A%2F**%20The%20code%20below%20determines%20the%20executable%20location%20for%20Chrome%20to%0A%20*%20start%20up%20and%20take%20the%20screenshot%20when%20running%20a%20local%20development%20environment.%0A%20*%0A%20*%20If%20the%20code%20is%20running%20on%20Windows%2C%20find%20chrome.exe%20in%20the%20default%20location.%0A%20*%20If%20the%20code%20is%20running%20on%20Linux%2C%20find%20the%20Chrome%20installation%20in%20the%20default%20location.%0A%20*%20If%20the%20code%20is%20running%20on%20MacOS%2C%20find%20the%20Chrome%20installation%20in%20the%20default%20location.%0A%20*%20You%20may%20need%20to%20update%20this%20code%20when%20running%20it%20locally%20depending%20on%20the%20location%20of%0A%20*%20your%20Chrome%20installation%20on%20your%20operating%20system.%0A%20*%2F%0A%0Aconst%20exePath%20%3D%0A%20%20process.platform%20%3D%3D%3D%20%22win32%22%0A%20%20%20%20%3F%20%22C%3A%5C%5CProgram%20Files%20(x86)%5C%5CGoogle%5C%5CChrome%5C%5CApplication%5C%5Cchrome.exe%22%0A%20%20%20%20%3A%20process.platform%20%3D%3D%3D%20%22linux%22%0A%20%20%20%20%3F%20%22%2Fusr%2Fbin%2Fgoogle-chrome%22%0A%20%20%20%20%3A%20%22%2FApplications%2FGoogle%20Chrome.app%2FContents%2FMacOS%2FGoogle%20Chrome%22%3B%0A%0Aasync%20function%20getOptions(isDev)%20%7B%0A%20%20let%20options%3B%0A%20%20if%20(isDev)%20%7B%0A%20%20%20%20options%20%3D%20%7B%0A%20%20%20%20%20%20args%3A%20%5B%5D%2C%0A%20%20%20%20%20%20executablePath%3A%20exePath%2C%0A%20%20%20%20%20%20headless%3A%20true%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%20else%20%7B%0A%20%20%20%20options%20%3D%20%7B%0A%20%20%20%20%20%20args%3A%20chrome.args%2C%0A%20%20%20%20%20%20executablePath%3A%20await%20chrome.executablePath%2C%0A%20%20%20%20%20%20headless%3A%20chrome.headless%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%0A%20%20return%20options%3B%0A%7D%0A%0Amodule.exports%20%3D%20async%20(req%2C%20res)%20%3D%3E%20%7B%0A%20%20const%20pageToScreenshot%20%3D%20req.query.page%3B%0A%0A%20%20%2F%2F%20pass%20in%20the%20isDev%3Dtrue%20parameter%20if%20you%20are%20developing%20locally%0A%20%20%2F%2F%20to%20ensure%20puppeteer%20picks%20up%20your%20machine%20installation%20of%0A%20%20%2F%2F%20Chrome%20via%20the%20configurable%20options%0A%20%20const%20isDev%20%3D%20req.query.isDev%20%3D%3D%3D%20%22true%22%3B%0A%0A%20%20try%20%7B%0A%20%20%20%20%2F%2F%20check%20for%20https%20for%20safety!%0A%20%20%20%20if%20(!pageToScreenshot.includes(%22https%3A%2F%2F%22))%20%7B%0A%20%20%20%20%20%20res.statusCode%20%3D%20404%3B%0A%20%20%20%20%20%20res.json(%7B%0A%20%20%20%20%20%20%20%20body%3A%20%22Sorry%2C%20we%20couldn't%20screenshot%20that%20page.%20Did%20you%20include%20https%3A%2F%2F%3F%22%2C%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20get%20options%20for%20browser%0A%20%20%20%20const%20options%20%3D%20await%20getOptions(isDev)%3B%0A%0A%20%20%20%20%2F%2F%20launch%20a%20new%20headless%20browser%20with%20dev%20%2F%20prod%20options%0A%20%20%20%20const%20browser%20%3D%20await%20puppeteer.launch(options)%3B%0A%20%20%20%20const%20page%20%3D%20await%20browser.newPage()%3B%0A%0A%20%20%20%20%2F%2F%20set%20the%20viewport%20size%0A%20%20%20%20await%20page.setViewport(%7B%0A%20%20%20%20%20%20width%3A%201920%2C%0A%20%20%20%20%20%20height%3A%201080%2C%0A%20%20%20%20%20%20deviceScaleFactor%3A%201%2C%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%2F%2F%20tell%20the%20page%20to%20visit%20the%20url%0A%20%20%20%20await%20page.goto(pageToScreenshot)%3B%0A%0A%20%20%20%20%2F%2F%20take%20a%20screenshot%0A%20%20%20%20const%20file%20%3D%20await%20page.screenshot(%7B%0A%20%20%20%20%20%20type%3A%20%22png%22%2C%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%2F%2F%20close%20the%20browser%0A%20%20%20%20await%20browser.close()%3B%0A%0A%20%20%20%20res.statusCode%20%3D%20200%3B%0A%20%20%20%20res.setHeader(%22Content-Type%22%2C%20%60image%2Fpng%60)%3B%0A%0A%20%20%20%20%2F%2F%20return%20the%20file!%0A%20%20%20%20res.end(file)%3B%0A%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20res.statusCode%20%3D%20500%3B%0A%20%20%20%20res.json(%7B%0A%20%20%20%20%20%20body%3A%20%22Sorry%2C%20Something%20went%20wrong!%22%2C%0A%20%20%20%20%7D)%3B%0A%20%20%7D%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> puppeteer <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"puppeteer-core"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> chrome <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"chrome-aws-lambda"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token comment">/** The code below determines the executable location for Chrome to<br> * start up and take the screenshot when running a local development environment.<br> *<br> * If the code is running on Windows, find chrome.exe in the default location.<br> * If the code is running on Linux, find the Chrome installation in the default location.<br> * If the code is running on MacOS, find the Chrome installation in the default location.<br> * You may need to update this code when running it locally depending on the location of<br> * your Chrome installation on your operating system.<br> */</span><br><br><span class="token keyword">const</span> exePath <span class="token operator">=</span><br>  process<span class="token punctuation">.</span>platform <span class="token operator">===</span> <span class="token string">"win32"</span><br>    <span class="token operator">?</span> <span class="token string">"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"</span><br>    <span class="token operator">:</span> process<span class="token punctuation">.</span>platform <span class="token operator">===</span> <span class="token string">"linux"</span><br>    <span class="token operator">?</span> <span class="token string">"/usr/bin/google-chrome"</span><br>    <span class="token operator">:</span> <span class="token string">"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"</span><span class="token punctuation">;</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getOptions</span><span class="token punctuation">(</span><span class="token parameter">isDev</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">let</span> options<span class="token punctuation">;</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>isDev<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    options <span class="token operator">=</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">args</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">executablePath</span><span class="token operator">:</span> exePath<span class="token punctuation">,</span><br>      <span class="token literal-property property">headless</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span><br>    options <span class="token operator">=</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">args</span><span class="token operator">:</span> chrome<span class="token punctuation">.</span>args<span class="token punctuation">,</span><br>      <span class="token literal-property property">executablePath</span><span class="token operator">:</span> <span class="token keyword">await</span> chrome<span class="token punctuation">.</span>executablePath<span class="token punctuation">,</span><br>      <span class="token literal-property property">headless</span><span class="token operator">:</span> chrome<span class="token punctuation">.</span>headless<span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br>  <span class="token keyword">return</span> options<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br>module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">req<span class="token punctuation">,</span> res</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> pageToScreenshot <span class="token operator">=</span> req<span class="token punctuation">.</span>query<span class="token punctuation">.</span>page<span class="token punctuation">;</span><br><br>  <span class="token comment">// pass in the isDev=true parameter if you are developing locally</span><br>  <span class="token comment">// to ensure puppeteer picks up your machine installation of</span><br>  <span class="token comment">// Chrome via the configurable options</span><br>  <span class="token keyword">const</span> isDev <span class="token operator">=</span> req<span class="token punctuation">.</span>query<span class="token punctuation">.</span>isDev <span class="token operator">===</span> <span class="token string">"true"</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">try</span> <span class="token punctuation">{</span><br>    <span class="token comment">// check for https for safety!</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>pageToScreenshot<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">"https://"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      res<span class="token punctuation">.</span>statusCode <span class="token operator">=</span> <span class="token number">404</span><span class="token punctuation">;</span><br>      res<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>        <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">"Sorry, we couldn't screenshot that page. Did you include https://?"</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br><br>    <span class="token comment">// get options for browser</span><br>    <span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">getOptions</span><span class="token punctuation">(</span>isDev<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// launch a new headless browser with dev / prod options</span><br>    <span class="token keyword">const</span> browser <span class="token operator">=</span> <span class="token keyword">await</span> puppeteer<span class="token punctuation">.</span><span class="token function">launch</span><span class="token punctuation">(</span>options<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token keyword">const</span> page <span class="token operator">=</span> <span class="token keyword">await</span> browser<span class="token punctuation">.</span><span class="token function">newPage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// set the viewport size</span><br>    <span class="token keyword">await</span> page<span class="token punctuation">.</span><span class="token function">setViewport</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      <span class="token literal-property property">width</span><span class="token operator">:</span> <span class="token number">1920</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">height</span><span class="token operator">:</span> <span class="token number">1080</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">deviceScaleFactor</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// tell the page to visit the url</span><br>    <span class="token keyword">await</span> page<span class="token punctuation">.</span><span class="token function">goto</span><span class="token punctuation">(</span>pageToScreenshot<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// take a screenshot</span><br>    <span class="token keyword">const</span> file <span class="token operator">=</span> <span class="token keyword">await</span> page<span class="token punctuation">.</span><span class="token function">screenshot</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">"png"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// close the browser</span><br>    <span class="token keyword">await</span> browser<span class="token punctuation">.</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    res<span class="token punctuation">.</span>statusCode <span class="token operator">=</span> <span class="token number">200</span><span class="token punctuation">;</span><br>    res<span class="token punctuation">.</span><span class="token function">setHeader</span><span class="token punctuation">(</span><span class="token string">"Content-Type"</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">image/png</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// return the file!</span><br>    res<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span>file<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    res<span class="token punctuation">.</span>statusCode <span class="token operator">=</span> <span class="token number">500</span><span class="token punctuation">;</span><br>    res<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br>      <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token string">"Sorry, Something went wrong!"</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">3. Building a slightly fancier front end to run the serverless function</h2><p class="post__p">Finally, we can call out to the serverless function in a web page application that returns the screenshot to us in the browser. Call the function URL using fetch, <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob" target="_blank">construct a blob from the response</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL" target="_blank">create an object URL</a> from the blob to be able to display it in the HTML.</p><p class="post__p">Here’s the functional front-end code built with vanilla JavaScript.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="tXVqDGurpL"
      aria-describedby="tXVqDGurpL">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="tXVqDGurpL">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="tXVqDGurpL" itemprop="text" content="%2F%2F%20Change%20this%20to%20%22true%22%20if%20you%20are%20developing%20locally%0Aconst%20isDev%20%3D%20%22true%22%3B%0A%0A%2F%2F%20Query%20the%20elements%20we%20need%20from%20the%20DOM%0Aconst%20form%20%3D%20document.querySelector(%22form%22)%3B%0Aconst%20urlInput%20%3D%20document.querySelector(%22%5Bdata-url-input%5D%22)%3B%0Aconst%20imageHolder%20%3D%20document.querySelector(%22%5Bdata-image-holder%5D%22)%3B%0Aconst%20imageHolderTitle%20%3D%20document.querySelector(%22%5Bdata-image-holder-title%5D%22)%3B%0Aconst%20loader%20%3D%20document.querySelector(%22%5Bdata-loader%5D%22)%3B%0A%0Afunction%20buildImageElement(url)%20%7B%0A%20%20const%20imageEl%20%3D%20document.createElement(%22img%22)%3B%0A%20%20imageEl.setAttribute(%22src%22%2C%20url)%3B%0A%20%20imageEl.setAttribute(%22id%22%2C%20%22generatedImage%22)%3B%0A%20%20return%20imageEl%3B%0A%7D%0A%0Afunction%20clearImageHolder()%20%7B%0A%20%20const%20imageEl%20%3D%20document.getElementById(%22generatedImage%22)%3B%0A%20%20if%20(imageEl)%20%7B%0A%20%20%20%20imageHolderTitle.style.display%20%3D%20%22none%22%3B%0A%20%20%20%20imageEl.remove()%3B%0A%20%20%7D%0A%7D%0A%0Afunction%20showLoader()%20%7B%0A%20%20loader.style.display%20%3D%20%22block%22%3B%0A%7D%0A%0Afunction%20hideLoader()%20%7B%0A%20%20loader.style.display%20%3D%20%22none%22%3B%0A%7D%0A%0A%2F%2F%20Call%20out%20to%20the%20serverless%20function%20on%20form%20submit%0Aform.addEventListener(%22submit%22%2C%20async%20(event)%20%3D%3E%20%7B%0A%20%20event.preventDefault()%3B%0A%20%20clearImageHolder()%3B%0A%20%20showLoader()%3B%0A%0A%20%20await%20fetch(%60%2Fapi%2Fscreenshot%3Fpage%3D%24%7BurlInput.value%7D%26isDev%3D%24%7BisDev%7D%60)%0A%20%20%20%20.then((response)%20%3D%3E%20response.blob())%0A%20%20%20%20.then((blob)%20%3D%3E%20%7B%0A%20%20%20%20%20%20const%20url%20%3D%20URL.createObjectURL(blob)%3B%0A%0A%20%20%20%20%20%20%2F%2F%20build%20up%20the%20image%20element%20with%20the%20url%0A%20%20%20%20%20%20const%20newImageEl%20%3D%20buildImageElement(url)%3B%0A%20%20%20%20%20%20imageHolderTitle.style.display%20%3D%20%22block%22%3B%0A%0A%20%20%20%20%20%20%2F%2F%20add%20the%20new%20element%20to%20the%20DOM%0A%20%20%20%20%20%20imageHolder.appendChild(newImageEl)%3B%0A%20%20%20%20%20%20hideLoader()%3B%0A%20%20%20%20%7D)%3B%0A%7D)%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// Change this to "true" if you are developing locally</span><br><span class="token keyword">const</span> isDev <span class="token operator">=</span> <span class="token string">"true"</span><span class="token punctuation">;</span><br><br><span class="token comment">// Query the elements we need from the DOM</span><br><span class="token keyword">const</span> form <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"form"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> urlInput <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-url-input]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> imageHolder <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-image-holder]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> imageHolderTitle <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-image-holder-title]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token keyword">const</span> loader <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">"[data-loader]"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">function</span> <span class="token function">buildImageElement</span><span class="token punctuation">(</span><span class="token parameter">url</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> imageEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"img"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  imageEl<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"src"</span><span class="token punctuation">,</span> url<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  imageEl<span class="token punctuation">.</span><span class="token function">setAttribute</span><span class="token punctuation">(</span><span class="token string">"id"</span><span class="token punctuation">,</span> <span class="token string">"generatedImage"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">return</span> imageEl<span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">clearImageHolder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> imageEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">"generatedImage"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token keyword">if</span> <span class="token punctuation">(</span>imageEl<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    imageHolderTitle<span class="token punctuation">.</span>style<span class="token punctuation">.</span>display <span class="token operator">=</span> <span class="token string">"none"</span><span class="token punctuation">;</span><br>    imageEl<span class="token punctuation">.</span><span class="token function">remove</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">showLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  loader<span class="token punctuation">.</span>style<span class="token punctuation">.</span>display <span class="token operator">=</span> <span class="token string">"block"</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">hideLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  loader<span class="token punctuation">.</span>style<span class="token punctuation">.</span>display <span class="token operator">=</span> <span class="token string">"none"</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Call out to the serverless function on form submit</span><br>form<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"submit"</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">event</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  event<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token function">clearImageHolder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token function">showLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">/api/screenshot?page=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>urlInput<span class="token punctuation">.</span>value<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;isDev=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>isDev<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">response</span><span class="token punctuation">)</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">blob</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">blob</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token constant">URL</span><span class="token punctuation">.</span><span class="token function">createObjectURL</span><span class="token punctuation">(</span>blob<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>      <span class="token comment">// build up the image element with the url</span><br>      <span class="token keyword">const</span> newImageEl <span class="token operator">=</span> <span class="token function">buildImageElement</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      imageHolderTitle<span class="token punctuation">.</span>style<span class="token punctuation">.</span>display <span class="token operator">=</span> <span class="token string">"block"</span><span class="token punctuation">;</span><br><br>      <span class="token comment">// add the new element to the DOM</span><br>      imageHolder<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>newImageEl<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token function">hideLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <img src="https://images.ctfassets.net/56dzm01z6lln/PENCtCpXTtFctI2L9vmFa/2edae6f2bcf10707d372fcec03c8f02a/gen_screenshot.gif" alt="An animated GIF of the puppeteer demo front end in action" height="944" width="672" /><p class="post__p"><a href="https://puppeteer-screenshot-demo.vercel.app/" target="_blank">View the live demo application here.</a></p><h2 class="post__h2">How I generate my dynamic Open Graph images for whitep4nth3r.com</h2><p class="post__p">To create dynamic Open Graph images for <a href="https://whitep4nth3r.com/" target="_blank">my personal blog site</a> built with Next.js and Contentful, I forked <a href="https://github.com/vercel/og-image" target="_blank">this repository from Vercel</a> that uses Puppeteer in the same way as described above, and created <a href="https://github.com/whitep4nth3r/p4nth3rblog-og-image" target="_blank">my own custom service</a> which I host on Vercel.</p><p class="post__p"><a href="https://p4nth3rblog-og-image.vercel.app/" target="_blank">View the front-end application here</a>.</p><p class="post__p">The application renders an HTML page based on the title of a blog post or page, which then uses Puppeteer to screenshot the page, and cache it.</p><p class="post__p">The URL parameters in the request hold the key to the magic being performed here. Here’s a look at the code that generates my dynamic Open Graph images in the head of my web pages, <a href="https://github.com/whitep4nth3r/p4nth3rblog/blob/main/utils/OpenGraph.js" target="_blank">which you can find in full here</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="QBUxSWIalm"
      aria-describedby="QBUxSWIalm">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="QBUxSWIalm">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="QBUxSWIalm" itemprop="text" content="%2F%2F%20Get%20a%20random%20light%2Fdark%20theme%20to%20switch%20things%20up%0Afunction%20getRandomTheme()%20%7B%0A%20%20const%20themes%20%3D%20%5B%22light%22%2C%20%22dark%22%5D%3B%0A%20%20return%20themes%5BMath.floor(Math.random()%20*%20themes.length)%5D%3B%0A%7D%0A%0A%2F%2F%20Get%20a%20random%20panther%20SVG%20to%20switch%20things%20up%20even%20more%0Afunction%20getRandomPanther()%20%7B%0A%20%20const%20panthers%20%3D%20%5B%22cool%22%2C%20%22dolla%22%2C%20%22fire%22%2C%20%22heart%22%2C%20...%5D%3B%0A%20%20return%20panthers%5BMath.floor(Math.random()%20*%20panthers.length)%5D%3B%0A%7D%0A%0A%2F%2F%20Get%20the%20base%20panther%20image%20uri%20-%20it's%20important%20to%20use%20encodeURIComponent()%20here%20to%20ensure%20all%20characters%20of%20the%20image%20url%20are%20encoded%0A%20function%20getBaseImageEncodedUri()%20%7B%0A%20%20return%20encodeURIComponent(%0A%20%20%20%20%22https%3A%2F%2Fexamplebaseurl.com%2Fdir%2Fto%2Fimages%22%2C%0A%20%20)%3B%0A%7D%0A%0A%2F%2F%20And%20the%20magic%20happens%20here%0Afunction%20generateImageUrl(title)%20%7B%0A%20%20%20%20return%20%60https%3A%2F%2Furl.to.the.service%2F%24%7BencodeURI(%0A%20%20%20%20%20%20title%2C%0A)%7D.png%3Ftheme%3D%24%7BgetRandomTheme()%7D%26md%3D0fontSize%3D80px%26images%3D%24%7BgetBaseImageEncodedUri()%7D%24%7BgetRandomPanther()%7D.svg%60%3B%0A%7D%0A%0A%2F%2F%20And%20this%20code%20calls%20the%20generateImageUrl%20function%20in%20the%20head%20of%20my%20blog%20pages%0A%20%3Cmeta%20property%3D%22og%3Aimage%22%20content%3D%7BgenerateImageUrl(title)%7D%20%2F%3E">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// Get a random light/dark theme to switch things up</span><br><span class="token keyword">function</span> <span class="token function">getRandomTheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> themes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"light"</span><span class="token punctuation">,</span> <span class="token string">"dark"</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>  <span class="token keyword">return</span> themes<span class="token punctuation">[</span>Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> themes<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Get a random panther SVG to switch things up even more</span><br><span class="token keyword">function</span> <span class="token function">getRandomPanther</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">const</span> panthers <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"cool"</span><span class="token punctuation">,</span> <span class="token string">"dolla"</span><span class="token punctuation">,</span> <span class="token string">"fire"</span><span class="token punctuation">,</span> <span class="token string">"heart"</span><span class="token punctuation">,</span> <span class="token operator">...</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br>  <span class="token keyword">return</span> panthers<span class="token punctuation">[</span>Math<span class="token punctuation">.</span><span class="token function">floor</span><span class="token punctuation">(</span>Math<span class="token punctuation">.</span><span class="token function">random</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">*</span> panthers<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Get the base panther image uri - it's important to use encodeURIComponent() here to ensure all characters of the image url are encoded</span><br> <span class="token keyword">function</span> <span class="token function">getBaseImageEncodedUri</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token function">encodeURIComponent</span><span class="token punctuation">(</span><br>    <span class="token string">"https://examplebaseurl.com/dir/to/images"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// And the magic happens here</span><br><span class="token keyword">function</span> <span class="token function">generateImageUrl</span><span class="token punctuation">(</span><span class="token parameter">title</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>    <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://url.to.the.service/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">encodeURI</span><span class="token punctuation">(</span><br>      title<span class="token punctuation">,</span><br><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.png?theme=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">getRandomTheme</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&amp;md=0fontSize=80px&amp;images=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">getBaseImageEncodedUri</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">getRandomPanther</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.svg</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// And this code calls the generateImageUrl function in the head of my blog pages</span><br> <span class="token operator">&lt;</span>meta property<span class="token operator">=</span><span class="token string">"og:image"</span> content<span class="token operator">=</span><span class="token punctuation">{</span><span class="token function">generateImageUrl</span><span class="token punctuation">(</span>title<span class="token punctuation">)</span><span class="token punctuation">}</span> <span class="token operator">/</span><span class="token operator">></span></code></pre>
    </div>
  </div>

  <p class="post__p">Harness the magic of Puppeteer in three ways: on the command line, as a serverless function in the background, or in a front-end application. Explore and fork this <a href="https://github.com/whitep4nth3r/puppeteer-demo" target="_blank">puppeteer-demo repository </a>to see Puppeteer in action — and have fun creating dynamic Open Graph images for your websites or finding new ways to use Puppeteer!</p><p class="post__p">If you found this helpful, check out the rest of <a href="https://www.contentful.com/blog/author/salma-alam-naylor/" target="_blank">my guides on the Contentful blog</a>. </p><p class="post__p">And remember, build stuff, learn things and love what you do.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>My Twitch live coding setup in OBS</title>
          <description>I often receive questions about how I set up OBS to stream live coding on Twitch — so let’s take a look!</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/twitch-live-coding-setup-obs/</link>
          <guid>https://whitep4nth3r.com/blog/twitch-live-coding-setup-obs/</guid>
          <pubDate>Sat, 13 Mar 2021 20:00:00 GMT</pubDate>
          <category>Streaming</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">When <a href="https://twitch.tv/whitep4nth3r" target="_blank">streaming live coding on Twitch</a>, I code on a MacBook Pro and stream via <a href="https://obsproject.com/" target="_blank">OBS</a> on a <a href="https://whitep4nth3r.com/uses/#streaming" target="_blank">custom PC build</a>. Given the variety of wild overlays I have added to my stream over time, I often receive questions about how I set up OBS to stream, so let’s take a look!</p><p class="post__p"><b class="post__p--italic">This article does not intend to be a tutorial on how to use OBS, but rather a window into the backstage area at p4nth3rHQ to answer common questions. </b></p><p class="post__p"><b class="post__p--italic">This article will also help future me in the event my streaming PC explodes 🔥.</b></p><h2 class="post__h2">How I monitor my stream when I’m live</h2><img src="https://images.ctfassets.net/56dzm01z6lln/7l79I3mLArFSFcE2upAVg8/31bfcbbddd459570420895afb078f331/what_i_see.png" alt="A screenshot of what I see in OBS when I am streaming live on Twitch" height="1042" width="1920" /><p class="post__p">I monitor my stream through a combination of UI docks in OBS and the Twitch Stream Manager — which I view on my MacBook Pro screen. Monitoring activity on two screens means I am less likely to miss activity in chat or items in the request queue. </p><p class="post__p">The most important part of what I monitor in Twitch Stream Manager is what my viewers are seeing (which is always slightly delayed compared to what I see in OBS). This ensures I can time raids at the end of my stream accurately, and alerts me to any visual/framerate issues as well.</p><p class="post__p">I use a variety of configurable UI docks available in OBS, including:</p><ul><li><p class="post__p">Twitch chat</p></li><li><p class="post__p">Twitch stats</p></li><li><p class="post__p">Twitch request queue</p></li><li><p class="post__p">Twitch activity feed</p></li><li><p class="post__p">Captions monitor (powered by the Google Cloud Captions plugin)</p></li></ul><h2 class="post__h2">Component and Composite Scenes</h2><p class="post__p">I organise my scene collection in OBS into a set of component scenes to enable easy reuse across composite scenes.</p><p class="post__p">I prefix all scene names with labels for quick visual identification, such as <b class="post__p--bold">[SCENE]</b> for a composite scene that will be shown on stream, <b class="post__p--bold">[C]</b> for component and so on. I also separate the list of component scenes into categories with blank scenes named “-------”.</p><img src="https://images.ctfassets.net/56dzm01z6lln/H1WFiUEWTiPmIJ4TKQakq/ac231311af842c3017fd415e7160e9b8/scene_list.png.PNG" alt="A screenshot of the full list of component and composite scenes I use in OBS" height="613" width="284" /><h2 class="post__h2">The composite scenes I use on stream</h2><ul><li><p class="post__p"><a href="#pre-roll" >Pre-roll</a></p></li><li><p class="post__p"><a href="#chatting" >Chatting</a></p></li><li><p class="post__p"><a href="#coding" >Coding</a></p></li><li><p class="post__p"><a href="#intermission" >Intermission</a></p></li><li><p class="post__p"><a href="#other-scenes" >Other scenes</a></p></li></ul><h2 class="post__h2">Pre-roll</h2><img src="https://images.ctfassets.net/56dzm01z6lln/eJEyp9f6oGT6nh9eUMiKN/5482a8987339630a9f9e679716c8c6cf/starting_soon_small.gif" alt="An animated gif of the starting soon screen I use when live streaming on Twitch" height="225" width="400" /><p class="post__p">This is the scene you’ll see when I’ve just pressed the go-live button. I leave this scene on for around five minutes, whilst I welcome people to the stream by voice. I don’t usually have my webcam capture visible for this scene, but sometimes I surprise the audience with a cameo appearance now and then.</p><p class="post__p">Here’s how it looks in OBS:</p><img src="https://images.ctfassets.net/56dzm01z6lln/5Xgp6H6ebIDEgDkGHZHGYE/0f155887f3e7d50150d3793f2ae7379e/pre_roll_obs.png" alt="A screenshot of the pre-roll composite scene components in OBS" height="209" width="442" /><p class="post__p">The <b class="post__p--bold">[C] essentials</b> component scene is a collection of browser sources that appear on every composite scene except the <b class="post__p--bold">end-transmission</b> scene, comprising:</p><ul><li><p class="post__p">Streamlabs alert box (which I only use for host alerts)</p></li><li><p class="post__p">Giveaway Friday overlay</p></li><li><p class="post__p">Footer overlay</p></li><li><p class="post__p">Chat overlay</p></li><li><p class="post__p">p4nth3rdrop overlay</p></li><li><p class="post__p">troll alert component scene (comprising a number of .mov files)</p></li></ul><img src="https://images.ctfassets.net/56dzm01z6lln/6nZ5rVFkihnugD7xbR9cC0/fe50b6b877d9b61d3a5951306a3aa274/essentials.png" alt="A screenshot of my essentials component scene that I add to every composite scene" height="129" width="406" /><p class="post__p">The giveaway, footer and chat overlays are powered by a React app, whilst <a href="https://github.com/whitep4nth3r/p4nth3rdrop" target="_blank">p4nth3rdrop</a> — the emote rain powered by Twitch chat commands — is a self-contained application.</p><p class="post__p">The <b class="post__p--bold">[C] panther static overlay</b> is a component scene containing the two large semi-transparent panthers you see in the background of the scene. The <b class="post__p--bold">[banner] starting-soon</b> and <b class="post__p--bold">[panther] majick</b> are at the centre of the screen.</p><p class="post__p">The <b class="post__p--bold">Geo Loop</b> is the dynamic geometric transparent-ish background — this is something I tried out once and it stuck — and I haven’t needed to put it in its own component scene yet.</p><p class="post__p">The p4nth3rlabs alerts browser source is added as a separate component on each composite scene. Even though I want to show the alerts on every scene, the browser source remains separate from the <b class="post__p--bold">essentials</b> due to it needing to be positioned differently across scenes. This is also the case for the <a href="https://github.com/whitep4nth3r/p4nth3rball" target="_blank">p4nth3rball</a>, which is another self-contained application.</p><p class="post__p">Depending on my mood, I’ll use the <b class="post__p--bold">[BG] black</b> or <b class="post__p--bold">[BG] red</b> patterned backgrounds. I leave both components in the scene for ease of switching — I do this type of thing in a lot of scenes. </p><h2 class="post__h2">Chatting</h2><img src="https://images.ctfassets.net/56dzm01z6lln/2Vda6U0ZgRSg3HrsBEkKLh/a2fd1b49c1ed4fc61383d2e510c75c04/chatting_small.gif" alt="An animated gif of a loop from my live Twitch stream that shows me in my chatting scene" height="225" width="400" /><p class="post__p">I mostly use this scene at the beginning and end of streams. It contains all of the essentials and separate components mentioned in the pre-roll scene, plus a <b class="post__p--bold">Credits</b> browser source from Streamlabs, which I use to show my custom-styled credits at the end of a stream. Here&#39;s how it looks in OBS:</p><img src="https://images.ctfassets.net/56dzm01z6lln/3Fvbkvlsknwtr1gGrv8ZhC/4a0458d1ddb1e8215b4f46c07ead9f04/chatting_obs.png" alt="A screenshot of my chatting composite scene details from OBS" height="206" width="443" /><h2 class="post__h2">Coding</h2><img src="https://images.ctfassets.net/56dzm01z6lln/5A0ZcwF9xnT17RoJWe6P7p/b726bdb61f6a7d128ef1c0d048601382/coding_small.gif" alt="An animated gif of a short part of a live Twitch stream where I am coding and receiving a subscription alert" height="225" width="400" /><p class="post__p">This is the scene I’ll show for most of a stream. This scene contains all of my <b class="post__p--bold">essentials</b>, the p4nth3rlabs alerts, and two <b class="post__p--bold">[Capture]</b> source component scenes. I use the <a href="https://whitep4nth3r.com/uses/#streaming" target="_blank">Elgato HD60 S Capture Card 1080p</a> to send the output of my main monitor (which is plugged in to my MacBook Pro) to the streaming PC.</p><p class="post__p">The <b class="post__p--bold">[Capture] Main</b> component scene is the full capture of my main 1080p monitor that I view directly in front of me when I stream.</p><p class="post__p">The <b class="post__p--bold">[Capture] Pixelated screen</b> component is my current privacy screen, which allows for some dynamic movement behind a privacy filter when I need to conceal my screen. This is a separate component scene containing another capture of the main monitor I code on, with a scaling/aspect ratio filter applied. <a href="https://www.lucecarter.co.uk/blog/how-to-achieve-a-pixellated-secrets-scene-in-obs-for-windows" target="_blank">Read this post by Luce Carter</a> to see how I set this up.</p><p class="post__p">Ordering of the component scenes inside composite scenes is something I make sure to consider carefully. For example, in the coding scene it’s important that the <b class="post__p--bold">[Cam] Main</b> is above <b class="post__p--bold">[C] alerts</b>, so that the alert banner doesn’t cover my face. </p><img src="https://images.ctfassets.net/56dzm01z6lln/38zAjoO3oyXC0jQf2IzePI/72a3c6f011e85670b22989b5e110f9a2/privacy_small.gif" alt="An animated gif of a shot from my live Twitch stream that shows my pixelated privacy screen" height="225" width="400" /><p class="post__p">Here’s how it looks in OBS:</p><img src="https://images.ctfassets.net/56dzm01z6lln/6tqLLOiQo2dyr1Xe4vhJ52/90d9f9bffdf1cb53d452cd611a69e0e3/coding_obs.png" alt="A screenshot showing the components that make up my coding composite scene in OBS" height="149" width="443" /><h2 class="post__h2">Intermission</h2><img src="https://images.ctfassets.net/56dzm01z6lln/6GHOLv1J8NqhXF5Wkz9Vhg/b858b56c5b2fb3ffa8f85cffa90fa7ee/intermission_small.gif" alt="An animated gif showing the intermission scene that I use during my Twitch live streams" height="225" width="400" /><p class="post__p">The <b class="post__p--bold">intermission</b> scene is almost identical to the <b class="post__p--bold">pre-roll</b> scene, with the banner and panther switched out. Here’s how it looks in OBS:</p><img src="https://images.ctfassets.net/56dzm01z6lln/6h7obTERCtgSYjd1QcZ1RV/dbc7c6d2055e839ae8043ae6f5853f1d/intermission_obs.png" alt="A screenshot of my intermission composite scene I use whilst live streaming on Twitch in OBS" height="224" width="443" /><h2 class="post__h2">Other scenes</h2><ul><li><p class="post__p"><b class="post__p--bold">[SCENE] end-transmission</b> is for when the stream ends — you’ll usually only see this for a few seconds before a raid</p></li><li><p class="post__p">I use <b class="post__p--bold">[SCENE] coding CAMR</b> if I need to move my face to the left side of the screen (next to chat) for any reason</p></li><li><p class="post__p"><b class="post__p--bold">[SCENE] coding CAMSMALL</b> is used to make my webcam capture smaller should I need to show viewers something behind my head — it’s a direct copy of <b class="post__p--bold">[SCENE] coding</b> but with the webcam component made much smaller</p></li><li><p class="post__p"><b class="post__p--bold">[SCENE] coding GUEST</b> is a flexible scene for when I have a guest on stream, which is mostly a duplicate of <b class="post__p--bold">[SCENE] coding</b></p></li><li><p class="post__p"><b class="post__p--bold">[SCENE] hacking</b> used to be my privacy scene that I tend not to use these days — but I still keep it around</p></li></ul><h2 class="post__h2">Stinger</h2><img src="https://images.ctfassets.net/56dzm01z6lln/2ybmLT3PVj7MB3gKQ7Tnlv/3962612493971053b7617c0646c9723f/stinger.gif" alt="A gif of the stinger I used for my scene transitions in OBS" height="1080" width="1920" /><p class="post__p">I recently made a stinger using <a href="https://www.youtube.com/watch?v=wGntVBSnA8Q" target="_blank">this tutorial from Gaming Careers</a> that I use to transition between all of my scenes. Here are the transition settings in OBS:</p><img src="https://images.ctfassets.net/56dzm01z6lln/5h6TUSt7uXWjJpVEKgdXLA/842d4ed22b7bf78ab731fc6e63b64133/stinger_settings.png" alt="A screenshot of the transition settings I use for my stream scenes in OBS" height="778" width="743" /><h2 class="post__h2">Audio</h2><p class="post__p">I use the Wave Link software that comes with my <a href="https://whitep4nth3r.com/uses/#streaming" target="_blank">Elgato Wave 1 USB Condenser Mic</a> to control most of the audio in OBS. I set up Wave Link to send audio inputs from multiple channels (mic, system sounds, voice chat in Discord, and the audio capture from my Mac) to one output for the stream — via the Wave Link Stream output — or WLS. </p><p class="post__p">Here are the two audio channels added to the main <b class="post__p--bold">[A] Audio mix</b> component scene, which is added to every streaming scene, except <b class="post__p--bold">end-transmission</b>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6Swe4yPahiNXRqSeDYulkt/abd868c1dd5eccc2c8311f9a808afd33/audio_component.png" alt="A screenshot of the audio component scene in OBS which I add to every composite scene" height="244" width="570" /><p class="post__p">Here are the Wave Link software settings. The Wave Link microphone acts as an audio interface, and this software allows the control of audio channel levels individually. My headphones are plugged into the microphone, and the interface allows me to control the levels of what I hear in my headphones independently of what audio levels are output to the stream.</p><img src="https://images.ctfassets.net/56dzm01z6lln/61jvzxJQGYhvwLfMA4ZwXf/d9bd73b29bd7a1fd64c0c79deae8810c/wave_link_settings.png" alt="A screenshot of the Wave Link audio interface software showing the channels I add to my main stream audio channel" height="540" width="929" /><p class="post__p">The music played via the <a href="https://whitep4nth3r.com/uses/#streaming" target="_blank">Pretzel</a> application is added as a separate audio channel, so I can lower the level of the music automatically whilst I am speaking via a compression filter — this is called audio ducking.</p><p class="post__p">If you’re playing music in the background whilst you speak, audio ducking is a really powerful feature that can greatly improve the production quality of your streams. I add a compression filter to the <b class="post__p--bold">Music</b> audio channel with the following settings. This means that whenever audio is output on the WLS channel, the Music channel will lower in volume.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4NdKAxshzbKu1QRnsaCmGz/887eeff4815ea7010957eef93cf6b681/ducking_settings.png" alt="A screenshot of the settings I use for audio ducking in OBS" height="551" width="779" /><p class="post__p">I also use a compression filter on the WLS channel, to filter out some background noise from the microphone input.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2bzrrqRc8TYUed3oBHgyXL/607b9d9a735e108dc991d781c85f43e7/wls_compressor_settings.png" alt="A screenshot showing the compression filter settings I use on the main audio channel of the stream" height="525" width="765" /><p class="post__p">As I use the Wave Link software to manage my audio channels, I disable all global audio devices in OBS, and set the monitoring device to the headphones plugged into my microphone/audio interface.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6h8YTxXGt3nhAEAugqFlyJ/2df145b6afe75da5c8caf2a02fc3c1d6/obs_audio_settings.png" alt="A screenshot showing the global audio settings I use in OBS" height="726" width="1009" /><p class="post__p">In the advanced audio settings, I set three audio sources that are generated by component scenes (browser sources) to <b class="post__p--bold">Monitor and Output</b>, so that I can monitor what my viewers are hearing on stream — such as follower alerts and chat-controlled overlay changes.</p><img src="https://images.ctfassets.net/56dzm01z6lln/6JmnEG6W6L35cQjuq59iPn/78a378d074c3430893ecec617000da28/advanced_audio_settings.png.PNG" alt="A screenshot showing the advanced audio mixer settings in OBS" height="517" width="1100" /><h2 class="post__h2">Stream Settings</h2><p class="post__p">Currently I stream at 2500kpbs, in 1080p at 60fps.</p><img src="https://images.ctfassets.net/56dzm01z6lln/4ojPdmbCGz2AAMQUDBgYq0/6f903983934627f98f77dfec2de31ab2/output_settings.png" alt="A screenshot of the output settings I use in OBS" height="598" width="980" /><img src="https://images.ctfassets.net/56dzm01z6lln/4DVzf7woUYtmnvy6mOt1hK/932c8b110d185560b906c56297e58a29/video_settings.png" alt="A screenshot of the video settings I use in OBS" height="599" width="980" /><h2 class="post__h2">OBS Plugins</h2><h3 class="post__h3">Cloud Closed Captions</h3><p class="post__p">I use the <a href="https://obsproject.com/forum/resources/closed-captioning-via-google-speech-recognition.833/" target="_blank">Cloud Closed Captions plugin</a> to provide captions to my viewers on stream. I have found it to be really reliable, and I like the fact that I can monitor the generated captions whilst I stream in a UI dock.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7ciRXN5iyHHoocx5BPcIXF/838fe2fd32fce028b8bf85fcaf50f6db/captions_settings.png" alt="A screenshot showing the captions settings I use in OBS" height="835" width="420" /><h3 class="post__h3">Elgato Remote Control</h3><p class="post__p">I use this plugin with my <a href="https://whitep4nth3r.com/uses/#streaming" target="_blank">Elgato Stream Deck XL</a>.</p><hr class="post__hr" /><p class="post__p">If you want to chat about or get some advice on live coding streaming setups, come and join a fantastic group of tech streamers in <a href="https://discord.gg/GQbXUVCneJ" target="_blank">The Claw Discord</a> server. We’d love to have you!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to generate an RSS feed for your blog with JavaScript and Netlify functions</title>
          <description>On demand RSS feeds — a neat use for serverless functions.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/03/05/generate-blog-rss-feed-with-javascript-and-netlify/</link>
          <guid>https://www.contentful.com/blog/2021/03/05/generate-blog-rss-feed-with-javascript-and-netlify/</guid>
          <pubDate>Mon, 08 Mar 2021 09:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>Serverless</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">After I built my first project with Contentful, I had no idea people would actually want to follow my content using their favorite RSS reader (thanks, <a href="https://stefanjudis.com" target="_blank">Stefan Judis</a>!). So I set out to learn how to generate an RSS feed for my microblog that’s built with no front-end frameworks.</p><h2 class="post__h2">RSS, explained</h2><p class="post__p">RSS (RDF Site Summary or Really Simple Syndication) was first released in March 1999. It allows people to subscribe to newly published content via an RSS reader so that they don’t need to manually check websites or channels. The myriad ways to consume content in 2021 can lead to content and sensory overload, so RSS feeds are great — Stefan Judis highlights the modern benefits of RSS in a <a href="https://www.stefanjudis.com/blog/web-weekly-5/#why-i-still-use-rss" target="_blank">recent Web Weekly newsletter</a>.</p><blockquote class="post__blockquote"><p class="post__p">”[With RSS] there’s no algorithm “curating” the articles, and I can catch up in my own time. It’s beautiful!” – Stefan Judis</p></blockquote><p class="post__p">An RSS feed takes the form of a standard XML (Extensible Markup Language) file, built of content and tags that define the content, and it looks a bit like HTML. XML is both human and machine readable. Its goal is to emphasize simplicity and usability across the World Wide Web. <a href="https://www.contentful.com/blog/feed.xml" target="_blank">Check out the Contentful blog’s RSS feed here</a> — it’s full of content!</p><h3 class="post__h3">Let’s build one!</h3><p class="post__p">There are a variety of plugins available for different platforms and frameworks that generate RSS feeds from your content. In order to stay true to the no-frameworks philosophy of my first Contentful project — <a href="https://thingoftheday.netlify.app/" target="_blank">thingoftheday.xyz</a> — I wanted to explore building the functionality myself.</p><p class="post__p"><b class="post__p--italic">Note: This guide assumes you are hosting your microblog on Netlify and can make use of </b><a href="https://www.netlify.com/products/functions/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=nil-functions" target="_blank"><b class="post__p--italic">Netlify functions</b></a><b class="post__p--italic">. </b></p><p class="post__p">Thingoftheday is a static client-side application, which means the page is populated with data at the time of the request. To keep things simple, I opted for the same approach with the RSS feed and populated the XML file at run time. Rather than setting up unnecessary routing in my single page application, I decided to build a Netlify function that runs on a specified URL at the time of the request to generate and serve the XML file to the browser or RSS reader.</p><h3 class="post__h3">The building blocks of an RSS XML file</h3><p class="post__p">An RSS feed must contain a channel tag (which must contain a title, link and description) and item tags (which, at a minimum, must contain a title or description). We used <a href="https://cyber.harvard.edu/rss/rss.html" target="_blank">this article from cyber.harvard.edu </a>as a guide to what we could include.</p><p class="post__p">Here’s the simplest form an RSS feed can take:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="EpAdIZjlzc"
      aria-describedby="EpAdIZjlzc">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="EpAdIZjlzc">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="xml">
      <meta data-code-id="EpAdIZjlzc" itemprop="text" content="%3Crss%20version%3D%222.0%22%3E%0A%20%20%3Cchannel%3E%0A%20%20%20%20%3Ctitle%3Ethingoftheday.xyz%3C%2Ftitle%3E%0A%20%20%20%20%3Clink%3Ehttps%3A%2F%2Fthingoftheday.xyz%3C%2Flink%3E%0A%20%20%20%20%3Cdescription%3Ethingoftheday%20is%20a%20lightweight%20microblogging%20site%20powered%20by%20Contentful%20and%20vanilla%20HTML%2C%20CSS%20and%20JavaScript.%0A%20%20%20%20%3C%2Fdescription%3E%0A%20%20%3C%2Fchannel%3E%0A%0A%20%20%3Citem%3E%0A%20%20%20%20%3Ctitle%3EThis%20is%20my%20RSS%20feed!%3C%2Ftitle%3E%0A%20%20%3C%2Fitem%3E%0A%3C%2Frss%3E">
      <pre class="language-xml"><code class="language-xml"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>rss</span> <span class="token attr-name">version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>2.0<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>channel</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>thingoftheday.xyz<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span><span class="token punctuation">></span></span>https://thingoftheday.xyz<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>link</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>description</span><span class="token punctuation">></span></span>thingoftheday is a lightweight microblogging site powered by Contentful and vanilla HTML, CSS and JavaScript.<br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>description</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>channel</span><span class="token punctuation">></span></span><br><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>item</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>This is my RSS feed!<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>item</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>rss</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">Building the RSS feed</h2><p class="post__p">If you want to go straight to the finished code, <a href="https://github.com/whitep4nth3r/thingoftheday/blob/main/functions/rss.js" target="_blank">click here.</a></p><p class="post__p">I was fortunate to work with Shy on our <a href="http://ctfl.io/generating-RSS-feeds-via-contentful" >first <b class="post__p--italic">ever</b> Contentful live stream together</a> to learn about RSS and decide on the approach. In the framework-free spirit of the application, I stubbornly set out to use Node’s native HTTP functionality in the Netlify function code, which would generate the XML file. Despite our brains being reduced to mush while not being able to fetch the data before the stream ended, I moved forward undeterred the next day, and I learned a few things in the process!</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/oMLUjaKXvS0"
        title="Generating RSS feeds via Contentful Live Stream"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <h3 class="post__h3">Setting up the files to enable the Netlify function</h3><p class="post__p">At the root of your project, create a functions directory, and add to it a new file called rss.js.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5uCBMQna3GDZUTgXc5SVi5/1b3dd26b29bc6bfc41639b557718bcb3/set_up_netlify_functions_dir.png" alt="A screenshot of the Netlify functions folder structure in an IDE" height="377" width="739" /><p class="post__p">If you’d like to override the directory where you store your functions, you can do so with a netlify.toml file at the root of your project, but we’ll go with the default here. <a href="https://docs.netlify.com/functions/configure-and-deploy/?utm_campaign=devex-san&utm_source=whitep4nth3r-blog&utm_content=func-config-deploy" target="_blank">Read more about Netlify functions</a>.</p><p class="post__p">When those files are pushed to your repository, you’ll be able to navigate to `https://{hostname}/.netlify/functions/rss` to run the function. This is also where you’ll find the generated XML file later. </p><h3 class="post__h3">Constructing the XML document in JavaScript</h3><p class="post__p">RSS is XML. From within a serverless function in Node.js we are going to build up a string, which we will return to the browser as “text/xml” content-type. Check it out:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ycNeLBzcgq"
      aria-describedby="ycNeLBzcgq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ycNeLBzcgq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="ycNeLBzcgq" itemprop="text" content="%2F%2F%20Netlify%20functions%20require%20the%20exports.handler%20function%20wrapper%0Aexports.handler%20%3D%20async%20function%20(event%2C%20context)%20%7B%0A%0A%20%20%2F%2F%20Construct%20the%20required%20building%20blocks%0A%20%20const%20rssFeed%20%3D%20%60%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%20%20%3Crss%20version%3D%222.0%22%20xmlns%3Aatom%3D%22http%3A%2F%2Fwww.w3.org%2F2005%2FAtom%22%3E%0A%20%20%3Cchannel%3E%0A%20%20%20%20%3Ctitle%3Ethingoftheday.xyz%3C%2Ftitle%3E%0A%20%20%20%20%3Clink%3Ehttps%3A%2F%2Fthingoftheday.xyz%3C%2Flink%3E%0A%20%20%20%20%3Cdescription%3Ethingoftheday%20is%20a%20lightweight%20microblogging%20site%20powered%20by%20Contentful%20and%20vanilla%20HTML%2C%20CSS%20and%20JavaScript.%3C%2Fdescription%3E%0A%0A%2F%2F%20We%E2%80%99ll%20get%20to%20this%20bit%20later!%0A%20%20%24%7BbuildRssItems(await%20getPosts())%7D%0A%0A%20%3C%2Fchannel%3E%0A%3C%2Frss%3E%60%3B%0A%0A%20%20%2F%2F%20Return%20the%20string%20in%20the%20body%20as%20type%20%E2%80%9Ctext%2Fxml%E2%80%9D%0A%20%20return%20%7B%0A%20%20%20%20statusCode%3A%20200%2C%0A%20%20%20%20contentType%3A%20%22text%2Fxml%22%2C%0A%20%20%20%20body%3A%20rssFeed%2C%0A%20%20%7D%3B%0A%7D%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">// Netlify functions require the exports.handler function wrapper</span><br>exports<span class="token punctuation">.</span><span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br><br>  <span class="token comment">// Construct the required building blocks</span><br>  <span class="token keyword">const</span> rssFeed <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">&lt;?xml version="1.0"?><br>  &lt;rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><br>  &lt;channel><br>    &lt;title>thingoftheday.xyz&lt;/title><br>    &lt;link>https://thingoftheday.xyz&lt;/link><br>    &lt;description>thingoftheday is a lightweight microblogging site powered by Contentful and vanilla HTML, CSS and JavaScript.&lt;/description><br><br>// We’ll get to this bit later!<br>  </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">buildRssItems</span><span class="token punctuation">(</span><span class="token keyword">await</span> <span class="token function">getPosts</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"><br><br> &lt;/channel><br>&lt;/rss></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>  <span class="token comment">// Return the string in the body as type “text/xml”</span><br>  <span class="token keyword">return</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">contentType</span><span class="token operator">:</span> <span class="token string">"text/xml"</span><span class="token punctuation">,</span><br>    <span class="token literal-property property">body</span><span class="token operator">:</span> rssFeed<span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Fetching the microblog data with Node https</h3><p class="post__p">Here’s a function that fetches the microblogs using the Contentful GraphQL API, requesting only the data that we need for the feed. </p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="GjUzxmKwYn"
      aria-describedby="GjUzxmKwYn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="GjUzxmKwYn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="GjUzxmKwYn" itemprop="text" content="const%20https%20%3D%20require(%22https%22)%3B%0A%0Aasync%20function%20getPosts()%20%7B%0A%20%20return%20new%20Promise((resolve%2C%20reject)%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F%20Copy%20the%20GraphQL%20query%20from%20the%20main%20application%20code%0A%20%20%20%20%2F%2F%20Remove%20irrelevant%20data%0A%0A%20%20%20%20const%20query%20%3D%20%60%0A%20%20%20%20query%20%7B%0A%20%20%20%20%20%20microblogCollection%20%7B%0A%20%20%20%20%20%20%20%20items%20%7B%0A%20%20%20%20%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20firstPublishedAt%0A%20%20%20%20%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20text%0A%20%20%20%20%20%20%20%20%20%20link%0A%20%20%20%20%20%20%20%20%20%20linkText%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20%60%3B%0A%20%20%20%20%2F%2F%20Construct%20https%20options%0A%0A%20%20%20%20const%20options%20%3D%20%7B%0A%20%20%20%20%20%20protocol%3A%20%22https%3A%22%2C%0A%20%20%20%20%20%20hostname%3A%20%22graphql.contentful.com%22%2C%0A%20%20%20%20%20%20path%3A%20%22%2Fcontent%2Fv1%2Fspaces%2F%7BSPACE_ID%7D%22%2C%20%2F%2Fadd%20your%20space%20ID%0A%20%20%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20%20%20Authorization%3A%20%22Bearer%20%7BACCESS_TOKEN%7D%22%2C%20%2F%2Fadd%20your%20access%20token%0A%20%20%20%20%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%7D%3B%0A%0A%20%20%20%20let%20posts%20%3D%20%22%22%3B%0A%0A%20%20%20%20const%20req%20%3D%20https.request(options%2C%20(res)%20%3D%3E%20%7B%0A%20%20%20%20%20%20res.on(%22data%22%2C%20(data)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20posts%20%2B%3D%20data%3B%0A%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%20%20res.on(%22end%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20const%20parsedPosts%20%3D%20JSON.parse(posts)%3B%0A%20%20%20%20%20%20%20%20resolve(parsedPosts.data.microblogCollection.items)%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20req.on(%22error%22%2C%20(e)%20%3D%3E%20%7B%0A%20%20%20%20%20%20console.error(e)%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20req.write(JSON.stringify(%7B%20query%20%7D))%3B%0A%20%20%20%20req.end()%3B%0A%20%20%7D)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> https <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"https"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getPosts</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve<span class="token punctuation">,</span> reject</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token comment">// Copy the GraphQL query from the main application code</span><br>    <span class="token comment">// Remove irrelevant data</span><br><br>    <span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br>    query {<br>      microblogCollection {<br>        items {<br>          sys {<br>            firstPublishedAt<br>            id<br>          }<br>          text<br>          link<br>          linkText<br>        }<br>      }<br>    }<br>    </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>    <span class="token comment">// Construct https options</span><br><br>    <span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span><br>      <span class="token literal-property property">protocol</span><span class="token operator">:</span> <span class="token string">"https:"</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">hostname</span><span class="token operator">:</span> <span class="token string">"graphql.contentful.com"</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">"/content/v1/spaces/{SPACE_ID}"</span><span class="token punctuation">,</span> <span class="token comment">//add your space ID</span><br>      <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>      <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>        <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token string">"Bearer {ACCESS_TOKEN}"</span><span class="token punctuation">,</span> <span class="token comment">//add your access token</span><br>        <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>      <span class="token punctuation">}</span><span class="token punctuation">,</span><br>    <span class="token punctuation">}</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">let</span> posts <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">;</span><br><br>    <span class="token keyword">const</span> req <span class="token operator">=</span> https<span class="token punctuation">.</span><span class="token function">request</span><span class="token punctuation">(</span>options<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">res</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      res<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"data"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        posts <span class="token operator">+=</span> data<span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>      res<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"end"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>        <span class="token keyword">const</span> parsedPosts <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>posts<span class="token punctuation">)</span><span class="token punctuation">;</span><br>        <span class="token function">resolve</span><span class="token punctuation">(</span>parsedPosts<span class="token punctuation">.</span>data<span class="token punctuation">.</span>microblogCollection<span class="token punctuation">.</span>items<span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    req<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">"error"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    req<span class="token punctuation">.</span><span class="token function">write</span><span class="token punctuation">(</span><span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    req<span class="token punctuation">.</span><span class="token function">end</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Building the RSS items</h3><p class="post__p">Once the data had been fetched, we iterated over the posts to build the XML items, joined them all together as one string and inserted them inside the string we created in the <code>exports.handler</code> function.</p><p class="post__p">As mentioned before, the only required piece of data in an item is either a title or description. We chose to add an <code>author</code>, <code>link</code>, <code>pubDate</code> and <code>guid</code>, as well.</p><h3 class="post__h3">2022 Update:</h3><p class="post__p">When I initially published this blog post, the <code>pubDate</code> tags were invalid! The code in the GitHub repository has been updated, and a small comment has been made on the code snippet below. For detailed information on valid RFC-822 dates in RSS feeds and how to build them with plain JavaScript, check out this post: <a href="https://whitep4nth3r.com/blog/how-to-format-dates-for-rss-feeds-rfc-822/">How to format dates for RSS feeds (RFC-822)</a>.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="MwBhfUEzKe"
      aria-describedby="MwBhfUEzKe">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="MwBhfUEzKe">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="MwBhfUEzKe" itemprop="text" content="function%20buildRFC822Date(dateString)%20%7B%0A%20%20%2F%2F%20See%20GitHub%20for%20full%20code%0A%7D%0A%0Afunction%20buildRssItems(items)%20%7B%0A%20%20return%20items%0A%20%20%20%20.map((item)%20%3D%3E%20%7B%0A%20%20%20%20%20%20return%20%60%0A%20%20%20%20%20%20%20%20%3Citem%3E%0A%20%20%20%20%20%20%20%20%20%20%3Ctitle%3E%24%7Bitem.text%7D%3C%2Ftitle%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cauthor%3Ewhitep4nth3r%40gmail.com%20(whitep4nth3r)%3C%2Fauthor%3E%0A%20%20%20%20%20%20%20%20%20%20%3Clink%3Ehttps%3A%2F%2Fthingoftheday.xyz%23%24%7Bitem.sys.id%7D%3C%2Flink%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cguid%3Ehttps%3A%2F%2Fthingoftheday.xyz%23%24%7Bitem.sys.id%7D%3C%2Fguid%3E%0A%20%20%20%20%20%20%20%20%20%20%3CpubDate%3E%24%7BbuildRFC822Date(item.sys.firstPublishedAt)%7D%3C%2FpubDate%3E%0A%20%20%20%20%20%20%20%20%3C%2Fitem%3E%0A%20%20%20%20%20%20%20%20%60%3B%0A%20%20%20%20%7D)%0A%20%20%20%20.join(%22%22)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">buildRFC822Date</span><span class="token punctuation">(</span><span class="token parameter">dateString</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token comment">// See GitHub for full code</span><br><span class="token punctuation">}</span><br><br><span class="token keyword">function</span> <span class="token function">buildRssItems</span><span class="token punctuation">(</span><span class="token parameter">items</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token keyword">return</span> items<br>    <span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">item</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>      <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><br>        &lt;item><br>          &lt;title></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token punctuation">.</span>text<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/title><br>          &lt;author>whitep4nth3r@gmail.com (whitep4nth3r)&lt;/author><br>          &lt;link>https://thingoftheday.xyz#</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/link><br>          &lt;guid>https://thingoftheday.xyz#</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>id<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/guid><br>          &lt;pubDate></span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token function">buildRFC822Date</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span>sys<span class="token punctuation">.</span>firstPublishedAt<span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&lt;/pubDate><br>        &lt;/item><br>        </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><span class="token punctuation">)</span><br>    <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">""</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Viewing the feed in the browser</h3><p class="post__p">Once you’ve fetched your data, built the string, created your XML document and sent it as “text/xml” to the browser, you can test out your Netlify function in two ways.</p><ol><li><p class="post__p">Push the code to your repository, give it a minute to deploy to Netlify and hit the URL to your function in the browser (<code>https://{hostname}/.netlify/functions/rss</code>).</p></li><li><p class="post__p">If you have the Netlify CLI installed, run <code>netlify dev</code> in your terminal at the root of your project. This will start up a development server where you can run your Netlify function at e.g. <code>http://localhost:8888/.netlify/functions/rss</code>.</p></li></ol><p class="post__p">And here’s what the feed looks like in the browser:</p><img src="https://images.ctfassets.net/56dzm01z6lln/5UNURTERYi9NFAnE0yPrOB/2afb3d8bb3343aae2b09c244590d084a/rss_in_browser.png" alt="A screenshot of the XML feed for thingoftheday.xyz in the browser" height="722" width="954" /><h3 class="post__h3">Distribute your RSS feed link!</h3><p class="post__p">You can now keep people happy who want to follow you via an RSS reader by giving out the link to the URL that generates the XML file. <a href="https://thingoftheday.netlify.app/.netlify/functions/rss" target="_blank">Check out the RSS feed for thingoftheday here.</a> Finally, here’s what the thingoftheday RSS feed looks like in the Feedly RSS reader.</p><img src="https://images.ctfassets.net/56dzm01z6lln/58bqUY1kqPfk1pOT273WUW/bc00cb720a828f7382f6bdaaedcba5a4/view_in_reader.png" alt="A screenshot of the thingoftheday.xyz RSS feed in an RSS reader" height="943" width="1275" /><p class="post__p">Remember, RSS feeds, sitemaps or any other files can be generated with data returned by APIs such as Contentful’s. Fetch the data, build a string and serve the document at request time or write the file to disk. You’re in control.</p><h3 class="post__h3">Bonus content: Make your RSS feed auto-discoverable</h3><p class="post__p">RSS auto-discovery means that people who want to subscribe to you via an RSS feeder can enter your website URL into their RSS reader, rather than having to find the exact link to the XML file.</p><p class="post__p">To enable auto discovery for your RSS feed, you can add a small snippet of code to the <code>&lt;head&gt;</code> of your index.html file, which directs readers to your feed URL, like so:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DKLmJBOBQN"
      aria-describedby="DKLmJBOBQN">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DKLmJBOBQN">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="DKLmJBOBQN" itemprop="text" content="%3Clink%0A%20%20%20%20%20%20rel%3D%22alternate%22%0A%20%20%20%20%20%20type%3D%22application%2Frss%2Bxml%22%0A%20%20%20%20%20%20title%3D%22RSS%20Feed%20for%20thingoftheday.xyz%22%0A%20%20%20%20%20%20href%3D%22https%3A%2F%2Fthingoftheday.xyz%2F.netlify%2Ffunctions%2Frss%22%0A%20%2F%3E">
      <pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span><br>      <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>alternate<span class="token punctuation">"</span></span><br>      <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>application/rss+xml<span class="token punctuation">"</span></span><br>      <span class="token attr-name">title</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>RSS Feed for thingoftheday.xyz<span class="token punctuation">"</span></span><br>      <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://thingoftheday.xyz/.netlify/functions/rss<span class="token punctuation">"</span></span><br> <span class="token punctuation">/></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Do I think RSS is worth it? Most definitely. If I can enable more people to access my content in whatever way they choose, then RSS is a winner. I’ll definitely be adding RSS feeds to all my future content projects — thanks again for showing me the way, Stefan! </p><p class="post__p"><a href="https://github.com/whitep4nth3r/thingoftheday/blob/main/functions/rss.js" target="_blank">Check out the full Netlify function file on GitHub.</a></p><p class="post__p">And remember, build stuff, learn things, and love what you do.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to make your code blocks accessible on your website</title>
          <description>How do you ensure your code blocks adhere to Web Content Accessibility Guidelines (WCAG) standards?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/how-to-make-your-code-blocks-accessible-on-your-website/</link>
          <guid>https://whitep4nth3r.com/blog/how-to-make-your-code-blocks-accessible-on-your-website/</guid>
          <pubDate>Fri, 26 Feb 2021 00:00:00 GMT</pubDate>
          <category>Tutorials</category><category>Accessibility</category><category>CSS</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">If you’re a developer or technical writer who publishes content on the internet, you’ll want to make sure your code examples are presented beautifully for your audience to consume. </p><p class="post__p">The good news is, there are plenty of tools available to adorn your website with fancy-looking code blocks in your blog posts, that mimic current and trendy IDE themes. For example, this site uses the <b class="post__p--bold">Okaidia</b> theme from <a href="https://prismjs.com/" target="_blank">Prism.js</a> to present code like this:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="TxNHeTuKBo"
      aria-describedby="TxNHeTuKBo">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="TxNHeTuKBo">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="TxNHeTuKBo" itemprop="text" content="import%20CodeBlockStyles%20from%20%22.%2FCodeBlock.module.css%22%3B%0Aimport%20Prism%20from%20%22prismjs%22%3B%0Aimport%20%7B%20useEffect%20%7D%20from%20%22react%22%3B%0A%0Aexport%20default%20function%20CodeBlock(props)%20%7B%0A%20%20useEffect(()%20%3D%3E%20%7B%0A%20%20%20%20Prism.highlightAll()%3B%0A%20%20%7D%2C%20%5B%5D)%3B%0A%0A%20%20const%20%7B%20language%2C%20code%20%7D%20%3D%20props%3B%0A%0A%20%20return%20(%0A%20%20%20%20%3Cpre%20className%3D%7B%60%24%7BCodeBlockStyles.codeBlock%7D%20language-%24%7Blanguage%7D%60%7D%3E%0A%20%20%20%20%20%20%3Ccode%20className%3D%7BCodeBlockStyles.codeBlock__inner%7D%3E%7Bcode%7D%3C%2Fcode%3E%0A%20%20%20%20%3C%2Fpre%3E%0A%20%20)%3B%0A%7D">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> CodeBlockStyles <span class="token keyword">from</span> <span class="token string">"./CodeBlock.module.css"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> Prism <span class="token keyword">from</span> <span class="token string">"prismjs"</span><span class="token punctuation">;</span><br><span class="token keyword">import</span> <span class="token punctuation">{</span> useEffect <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"react"</span><span class="token punctuation">;</span><br><br><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">function</span> <span class="token function">CodeBlock</span><span class="token punctuation">(</span><span class="token parameter">props</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br>  <span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    Prism<span class="token punctuation">.</span><span class="token function">highlightAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>  <span class="token keyword">const</span> <span class="token punctuation">{</span> language<span class="token punctuation">,</span> code <span class="token punctuation">}</span> <span class="token operator">=</span> props<span class="token punctuation">;</span><br><br>  <span class="token keyword">return</span> <span class="token punctuation">(</span><br>    <span class="token operator">&lt;</span>pre className<span class="token operator">=</span><span class="token punctuation">{</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>CodeBlockStyles<span class="token punctuation">.</span>codeBlock<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> language-</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>language<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">}</span><span class="token operator">></span><br>      <span class="token operator">&lt;</span>code className<span class="token operator">=</span><span class="token punctuation">{</span>CodeBlockStyles<span class="token punctuation">.</span>codeBlock__inner<span class="token punctuation">}</span><span class="token operator">></span><span class="token punctuation">{</span>code<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>code<span class="token operator">></span><br>    <span class="token operator">&lt;</span><span class="token operator">/</span>pre<span class="token operator">></span><br>  <span class="token punctuation">)</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">The bad news is, most prebuilt themes (especially those with darker colour palettes) come loaded with accessibility issues such as <b class="post__p--bold">failing colour contrast checks</b>, meaning your audience may have difficulties consuming your code examples. Additionally, your website will be penalised in web search results for failing accessibility checks.</p><p class="post__p">This means we need to do a little more work in CSS to make our code blocks accessible. Let’s take a look.</p><p class="post__p"><a href="#fixing-code-block-accessibility-issues" >Jump straight to the code examples.</a></p><h2 class="post__h2">How to check accessibility on your website</h2><p class="post__p">Accessibility standards are defined by the <a href="https://www.w3.org/WAI/standards-guidelines/wcag/" target="_blank">Web Content Accessibility Guidelines (WCAG)</a>. There are a number of free tools you can use to check for accessibility on your website. Not all tools will catch everything and some tools may catch false positives. For this reason I always recommend using a combination of tools to ensure you can create the best experience possible for your audience.</p><h3 class="post__h3">Google Lighthouse</h3><p class="post__p">If you’re developing in Google Chrome or Microsoft Edge, you can access the <a href="https://developers.google.com/web/tools/lighthouse#devtools" target="_blank"><u>Google Lighthouse tools from your developer console</u></a>. </p><p class="post__p"><b class="post__p--italic">Quick tip — some Chrome plugins can affect the way Lighthouse runs in your browser — so it’s always recommended to run these checks in an incognito window without any plugins activated.</b></p><img src="https://images.ctfassets.net/56dzm01z6lln/15wBp7QhJEhV6Gxo3C4rl6/fd1b6be731ee8dee927d7a39605865ec/google_lighthouse_accessibility_check.png" alt="A screenshot showing the Google Lighthouse dev tools tab with the Accessibility checkbox checked" height="585" width="1274" /><p class="post__p">You can use Google Lighthouse to run a variety of reports on your web pages. Check the &#39;Accessibility&#39; checkbox and click ‘Generate report’.</p><img src="https://images.ctfassets.net/56dzm01z6lln/1dsOJmWA3jwtb9jyTHE400/c6998b70ab32cbdf24ca33ee047d73c3/google_lighthouse_accessibility_report.png" alt="A screenshot showing a 100% Accessibility score on the Google Lighthouse dev tools tab" height="727" width="1272" /><p class="post__p">Here’s an accessibility check for <a href="https://whitep4nth3r.com/blog/how-to-build-a-lightweight-blog/" target="_blank"><u>this blog post</u></a> (including code blocks) which produces a satisfying score of 100%! 🎉  But remember — as the report shows — there are always areas of your website that automated tools might not be able to assess correctly — so you should always carry out manual checks as well.</p><h3 class="post__h3">axe — Web Accessibility Testing</h3><p class="post__p">I’ve been using <a href="https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd" target="_blank"><u>this Chrome plugin</u></a> for a number of years, and it has helped me incredibly in improving my knowledge of accessibility.</p><p class="post__p">Install the plugin, open your Chrome dev tools, and navigate to the ‘axe’ tab. </p><img src="https://images.ctfassets.net/56dzm01z6lln/6DzX1VunnJUC4yLPZFxklp/188c1c465c400ef42228a2db779af994/axe_screenshot.png" alt="A screenshot of the axe accessibility panel in Chrome dev tools with options to scan all of my page and scan part of my page" height="736" width="1276" /><p class="post__p">What’s great about axe is that it shows the HTML code for any issues that it has detected. For the same blog post we scanned with Lighthouse above, axe finds 11 issues. </p><p class="post__p">That&#39;s not so good! But let’s investigate.</p><img src="https://images.ctfassets.net/56dzm01z6lln/7mY07makct51qA3J4Fc40d/e683b599758d8915a93b720be946992c/axe_iframe_issue.png" alt="A screenshot showing an axe accessibility tool issue description of failing colour contrast in a YouTube iframe embed" height="511" width="1273" /><p class="post__p">Axe is describing colour contrast issues inside a YouTube iframe embed. It’s not related to any CSS that&#39;s bundled with the web page, and it’s not ideal. We might be able to fix it if we can target the iframe CSS effectively, or use a different thumbnail on the YouTube video. But for now, it&#39;s good to know!</p><h3 class="post__h3">WAVE Evaluation Tool</h3><p class="post__p">The third tool that is integral to my accessibility workflow is the <a href="https://chrome.google.com/webstore/detail/wave-evaluation-tool/jbbplnpkjmmeebjpijfedlgcdilocofh" target="_blank"><u>Wave Evaluation Tool Chrome extension</u></a>. This tool is great for highlighting semantic HTML and ensuring you’ve included the necessary aria labels.</p><p class="post__p">Here’s an example of Wave in action.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5vdeBAoGL2fICfJSK2sben/282504d1c4aca5f3bc2ecf7f473456d6/wave_screenshot.png" alt="A screenshot showing a Wave Evaluation Tool summary tab overlaid on the home page of whitep4nth3r.com" height="741" width="957" /><p class="post__p">Click on ‘View details’ to see a detailed breakdown of the summary.</p><img src="https://images.ctfassets.net/56dzm01z6lln/5zdxY4hoX6ewJaF7lbnQib/28527d25f9add7137beeec54e77e55af/wave_screenshot_details.png" alt="A screenshot showing a Wave Evaluation Tool details tab overlaid on the home page of whitep4nth3r.com" height="884" width="958" /><h2 class="post__h2">Fixing code block accessibility issues</h2><p class="post__p">There are two main issues to look for in your code blocks, both of which axe picked up for me.</p><h3 class="post__h3">Ensure the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds</h3><p class="post__p">The theme I chose to use from Prism.js is beautiful, but some of the colours failed the colour contrast checks.</p><p class="post__p">Here is a code block that failed a colour contrast check:</p><img src="https://images.ctfassets.net/56dzm01z6lln/183DUhKK2VqLZ41bzLHKoZ/2ee139341717a5550bcf6386c199f5dc/failing_cc_code_block.png" alt="A screenshot of a code block that fails colour contrast" height="204" width="630" /><p class="post__p">This is described in axe alongside the underlying HTML:</p><img src="https://images.ctfassets.net/56dzm01z6lln/7v4h8mj3ieAwMts1aQSw9Z/0ec711f76de4718350a7e2bfd37ccb1c/axe_code_block_cc.png" alt="A screenshot of the axe accessibility panel in Chrome dev tools showing a failing colour contrast check" height="702" width="955" /><h3 class="post__h3">Finding accessible colours</h3><p class="post__p">Another neat tool I use is the <a href="https://colourcontrast.cc/" target="_blank"><u>Colour Contrast checker</u></a> that’s also available as a <a href="https://chrome.google.com/webstore/detail/colour-contrast-checker/nmmjeclfkgjdomacpcflgdkgpphpmnfe?hl=en-GB" target="_blank"><u>Chrome extension</u></a>. Activate the extension, and use the eyedropper to select background and foreground colours to view whether it passes <a href="https://www.w3.org/WAI/standards-guidelines/wcag/" target="_blank"><u>Web Content Accessibility Guidelines (WCAG)</u></a>.</p><img src="https://images.ctfassets.net/56dzm01z6lln/2uLUS8OwLYayW8bxfvblez/f483413cb5dc03ad77927d13aa5aa559/cc_checker_fail.png" alt="A screenshot of the Colour Contrast Checker Chrome extension showing a failing colour contrast check" height="529" width="1041" /><p class="post__p">The beautiful thing about this tool is you can move the hue, saturation and light sliders to find colours that pass accessibility guidelines without having to leave your browser page. Using the slider tools, I was able to find colours that passed colour contrast checks, and added the following overrides to my global CSS file:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZCJhIbolzn"
      aria-describedby="ZCJhIbolzn">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZCJhIbolzn">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="ZCJhIbolzn" itemprop="text" content="%2F*%20accessibility%20fixes%20for%20prismjs%20*%2F%0A%0A.token.comment%20%7B%0A%20%20color%3A%20%23adb8c2%20!important%3B%0A%7D%0A%0A.token.important%20%7B%0A%20%20color%3A%20%23f3a344%20!important%3B%0A%7D%0A%0A.token.tag%2C%0A.token.property%2C%0A.token.constant%20%7B%0A%20%20color%3A%20%23fc92b6%20!important%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token comment">/* accessibility fixes for prismjs */</span><br><br><span class="token selector">.token.comment</span> <span class="token punctuation">{</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> #adb8c2 <span class="token important">!important</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.token.important</span> <span class="token punctuation">{</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> #f3a344 <span class="token important">!important</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span><br><br><span class="token selector">.token.tag,<br>.token.property,<br>.token.constant</span> <span class="token punctuation">{</span><br>  <span class="token property">color</span><span class="token punctuation">:</span> #fc92b6 <span class="token important">!important</span><span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h3 class="post__h3">Elements that have scrollable content should be accessible by keyboard</h3><p class="post__p">This accessibility violation was a little trickier to solve, and is the primary reason for writing this blog post.</p><img src="https://images.ctfassets.net/56dzm01z6lln/78KRDXY1Mu0tA1miQZUj3t/3094b9c3dc12cbf3365e3f1bbe28b661/axe_scrollable_fail.png" alt="A screenshot showing an axe accessibility tool issue description of scrollable content failure" height="450" width="1042" /><p class="post__p">Prism.js ships with the following code that caused this violation, which causes overflow text to scroll rather than wrap:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="awFvOppBXi"
      aria-describedby="awFvOppBXi">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="awFvOppBXi">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="awFvOppBXi" itemprop="text" content="pre%5Bclass*%3D%22language-%22%5D%20%7B%0A%20%20%20padding%3A%201em%3B%0A%20%20%20overflow%3A%20auto%3B%0A%20%20%20border-radius%3A%200.3em%3B%0A%7D">
      <pre class="language-css"><code class="language-css"><span class="token selector">pre[class*="language-"]</span> <span class="token punctuation">{</span><br>   <span class="token property">padding</span><span class="token punctuation">:</span> 1em<span class="token punctuation">;</span><br>   <span class="token property">overflow</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span><br>   <span class="token property">border-radius</span><span class="token punctuation">:</span> 0.3em<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">Code blocks are not focusable by a keyboard (whereas anchor tags <b class="post__p--italic">are</b> focusable, for example). This means that if your audience is using only a keyboard to navigate the site, they will be unable to access the content that has overflowed the container, which would usually be scrolled with a mouse.</p><p class="post__p">The way to fix this accessibility issue is to ensure the text will wrap when necessary inside the <code>&lt;code&gt;</code> tag.</p><p class="post__p">Here’s the code I used to prevent the scrolling behaviour and make the text wrap onto a new line:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="DJwbpgNDUs"
      aria-describedby="DJwbpgNDUs">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="DJwbpgNDUs">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="css">
      <meta data-code-id="DJwbpgNDUs" itemprop="text" content="code%5Bclass*%3D%22language-%22%5D%20%7B%0A%20%20white-space%3A%20pre-wrap%3B%0A%20%20word-break%3A%20break-all%3B%0A%7D%0A">
      <pre class="language-css"><code class="language-css"><span class="token selector">code[class*="language-"]</span> <span class="token punctuation">{</span><br>  <span class="token property">white-space</span><span class="token punctuation">:</span> pre-wrap<span class="token punctuation">;</span><br>  <span class="token property">word-break</span><span class="token punctuation">:</span> break-all<span class="token punctuation">;</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <h2 class="post__h2">In conclusion</h2><p class="post__p">Prebuilt IDE-like code block themes like the ones available from Prism.js are beautiful, but if you want to use one, ensure you check for accessibility issues such as colour contrast and scrolling behaviour. This will ensure your audience can use your site effectively, and your site ranks well in search engines for accessibility. </p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to build a lightweight microblogging site with Contentful, vanilla HTML, CSS and JavaScript</title>
          <description>Learn how to build a lightweight microblog with Contentful and vanilla HTML, CSS and JavaScript.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/02/05/how-to-build-a-lightweight-blog/</link>
          <guid>https://www.contentful.com/blog/2021/02/05/how-to-build-a-lightweight-blog/</guid>
          <pubDate>Fri, 05 Feb 2021 00:00:00 GMT</pubDate>
          <category>Tutorials</category><category>JavaScript</category><category>GraphQL</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I’m a big believer in the KISS principle in web development. KISS is an acronym for the phrase “keep it simple, stupid” — and while I don’t advocate for calling anyone “stupid” 🙈 — I definitely advocate for keeping things very simple when learning to work with new technology.</p><p class="post__p">Before joining Contentful in January 2021, I purposefully avoided using Contentful in my front-end projects so that I could come to it with fresh eyes and explore it with purpose and meaning.</p><p class="post__p">So, with this being my first-ever app built using Contentful (🎉), I wanted to make sure I focused my efforts on learning this single technology, rather than getting bogged down with other front-end frameworks and technologies such as Angular, React, Vue or Svelte (which are all great, by the way!).</p><h2 class="post__h2">What do you mean, no front-end frameworks?</h2><p class="post__p"><b class="post__p--italic">thingoftheday</b> is a simple static site built entirely with vanilla HTML, CSS and JavaScript; there are no packages to install, no build commands to run, and no framework patterns to follow.  </p><p class="post__p">Sidenote: It was really fun to get back to basics using native JavaScript functionality such as <code>document.querySelector </code>and <code>document.createElement</code> in this app. As an added bonus, the single-page application is super fast and is deployed instantly (to Netlify) whenever the repository receives new changes.</p><h3 class="post__h3">Sounds great! How do I get started?</h3><p class="post__p">If you’re the type who likes to fork a repository and get stuck into the code straight away (I know I am!), I’ve included a <a href="https://github.com/whitep4nth3r/thingoftheday#thingoftheday" target="_blank"><u>quick-start guide on GitHub here </u></a>where you can import the content type and example content defined in this tutorial using the Contentful CLI in a matter of minutes. You can also get started by watching this quick tutorial video.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/97Hg0OYFC0w"
        title="Getting started with thingoftheday: a microblog powered by Contentful, vanilla HTML, CSS and JS"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p">Have fun!</p><p class="post__p">Here’s what you’ll need to be able to build a lightweight microblogging site the old-school way with Contentful.</p><h3 class="post__h3">A Contentful account</h3><p class="post__p">You can <a href="https://www.contentful.com/sign-up/" target="_blank"><u>sign up for a free Contentful account</u></a> here.</p><h3 class="post__h3">Optional: Prepare your development environment</h3><p class="post__p">If you want to use es6 syntax in your vanilla JavaScript file, you’ll need to serve your module file to the browser over an http-server in your local development environment. You can do this with <a href="https://www.npmjs.com/package/http-server" target="_blank"><u>this nifty npm package</u></a>, aptly titled “http-server.”</p><p class="post__p">That’s everything you need!</p><h2 class="post__h2">Let’s get building!</h2><h3 class="post__h3">Create your space in Contentful</h3><p class="post__p">Call it whatever you like. We’ll go with <b class="post__p--italic">thingoftheday.</b></p><img src="https://images.ctfassets.net/56dzm01z6lln/2exWoZe8GObFe4gX0GRNbd/18564f547fb9f2157a474d5e43316618/step1.gif" alt="An animated GIF showing how to create a Contentful space" height="378" width="640" /><h3 class="post__h3">Create and define your content type</h3><p class="post__p">Before we write any code, we’re going to create our content type<b class="post__p--italic"> </b>inside our space<b class="post__p--italic">. </b></p><p class="post__p">What’s great about Contentful is that it encourages you to approach your applications with a data-first mindset. I have always advocated for building applications from the “middle out,”  which means defining your data structures and how they might be extended before beginning any development on a project. With this method, both backend and front-end development teams can get to work simultaneously. This is how agile teams work effectively, especially in large organizations. </p><p class="post__p">Your content is the most important building block of your applications. Without its data, your application doesn’t have a voice.</p><h3 class="post__h3">Wait, what’s a content type?</h3><p class="post__p">A content type is a collection of bits of data you’d like to group together. Think of it like an object in JavaScript. </p><p class="post__p">For example, you might have a <a href="https://www.contentful.com/help/contentful-glossary/#content-type" target="_blank">content type</a> of a <code>cat</code>. Your <code>cat</code> will have different fields that are used to describe it (or properties in JavaScript object land), which might be <code>breed</code>, <code>numberOfLegs</code> or <code>favoriteBrandOfCatFood</code>. These fields will be of certain types (think text, date, media). If you’re familiar with strongly typed programming languages such as TypeScript, this might feel nice and familiar.</p><p class="post__p">The purpose of <b class="post__p--italic">thingoftheday</b> is to capture a small image/snippet/tweet/link/thing each day that I’ve learned, or to record proud achievements that I wanted to remember forever that won’t get lost in a Twitter timeline.</p><p class="post__p">The basic requirements for the microblog are to display the following per post:</p><ul><li><p class="post__p">Some text </p></li><li><p class="post__p">An image</p></li><li><p class="post__p">An external link</p></li><li><p class="post__p">Some text for the link </p></li><li><p class="post__p">A panther emote that represented my mood on the day</p></li></ul><p class="post__p">And this is what we’ll use to build our content type!</p><p class="post__p">In case you’re wondering about the panther emote — or any of the panthers that you’ll see if you follow me online — let me explain. It started off as a bit of a tongue-in-cheek thing. My handle, @whitep4nth3r, is what I would call myself if I were a member of FSociety. (If you haven’t watched Mr. Robot, you need to.) The panthers just expanded from there. For example, if you watch <a href="https://www.twitch.tv/whitep4nth3r" target="_blank"><u>my Twitch stream</u></a>, you can express yourself with a plethora of panther emotes and drop different flavors of panther rain on me. I use these panthers to express my current mood at the time on my <a href="https://twitch.tv/whitep4nth3r" target="_blank"><u>Twitch stream</u></a>. The panthers have just become a “thing” — so they will be a “thing” for <b class="post__p--italic">thingoftheday</b>!</p><p class="post__p">Here’s a quick video tutorial on how to set up the content type for our microblog.</p>
    <div class="videoEmbed">
      <iframe
        class="videoEmbed__iframe"
        width="560"
        height="315"
        src="https://www.youtube.com/embed/5Irq8gHNVr8"
        title="Getting started with thingoftheday: setting up the content type for your microblog"
        frameborder="0"
        loading="lazy"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
      </iframe>
    </div>
    <p class="post__p">We’ve now created our content type for our blog posts. 🎉</p><p class="post__p">Here’s what the UI should contain when you click on the content model navigation tab.</p><img src="https://images.ctfassets.net/56dzm01z6lln/gwgSGpfZRWAdQUKJnfPnr/99348b37e586b1a7c94d870228a4f895/finished_content_model.png" alt="A screenshot of a finished content model for thingoftheday" height="414" width="831" /><h3 class="post__h3">Now, for the content!</h3><p class="post__p">Let’s add our first microblog post!</p><p class="post__p">Navigate to the content tab and click on add microblog.</p><p class="post__p">Add some fun stuff, then click the big green publish button. </p><img src="https://images.ctfassets.net/56dzm01z6lln/6X6GQQKGaHtRFE0pGIfbOi/9039a74f7ce3550163fe20c5d3f5fc9f/step15.gif" alt="An animated GIF showing the creation of a blog post" height="422" width="640" /><p class="post__p">Navigate back to the content tab, and you’ll find your shiny new microblog post in the list. ✨</p><p class="post__p">The text field has been used for the entry title in the name column as we specified earlier.</p><img src="https://images.ctfassets.net/56dzm01z6lln/eyUj0FprQbir41wvrmNB5/86e125ae7f1fbe5c01f40459f079549e/step16.png" alt="Image showing the published piece of content" height="206" width="872" /><p class="post__p">Congrats! You’ve created your first post with your very own content type in Contentful. 😎</p><h3 class="post__h3">Next up, fetch your content</h3><p class="post__p">In order to fetch your content, you’ll need two pieces of data. Your space ID and an access token. <a href="https://www.contentful.com/help/personal-access-tokens/" target="_blank"><u>You can read more about personal access tokens here.</u></a></p><p class="post__p">Head on over to the settings tab, click general settings, and find your space ID at the top of the page.</p><img src="https://images.ctfassets.net/56dzm01z6lln/3hKjH57LQCiiSRVN8vOkTK/1a56dd57bae72a3bd6f9253e31b1ee8c/step17.png" alt="Screenshot showing where to find the Contentful space ID" height="264" width="740" /><p class="post__p">Click on the settings tab again, and click on API keys.</p><p class="post__p">Click on the big blue add API key button, configure what you need and copy the content delivery API - access token.</p><img src="https://images.ctfassets.net/56dzm01z6lln/9m8EcQ3afoHeirlmA6s51/d18fc1eccce5dbe626c614206d38ac5e/step18.png" alt="A screenshot showing where to find the Contentful Delivery API access token" height="96" width="799" /><p class="post__p">Click save. Now we’re ready to rock and roll!</p><h3 class="post__h3">The Content Delivery API</h3><p class="post__p">Contentful offers a few ways to fetch content, including <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/" target="_blank"><u>the REST API</u></a> and the <a href="https://www.contentful.com/developers/docs/references/graphql/" target="_blank"><u>GraphQL API</u></a>. We’re going to use the GraphQL API for this application, because there’s no need to download any packages or SDKs, and, most importantly, it’s a fantastic way to request only the data you need for the front end. No need to deal with huge complex payloads with lots of irrelevant system information!</p><p class="post__p">Remember — keep it simple, stupid! 🙈</p><p class="post__p">Sidenote: If JavaScript isn’t your thing, <a href="https://www.contentful.com/blog/2021/01/14/GraphQL-via-HTTP-in-five-ways/" target="_blank"><u>check out how you can also use the GraphQL API with cURL, Python, Ruby and PHP here</u></a>. Thanks Shy!</p><p class="post__p">Let’s head over to Contentful’s GraphiQL interface using the following URL configured with your space ID and access token:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="LTqbDnsfOt"
      aria-describedby="LTqbDnsfOt">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="LTqbDnsfOt">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="markdown">
      <meta data-code-id="LTqbDnsfOt" itemprop="text" content="https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2F%7BYOUR_SPACE_ID%7D%2Fexplore%3Faccess_token%3D%7BYOUR_ACCESS_TOKEN%7D">
      <pre class="language-markdown"><code class="language-markdown">https://graphql.contentful.com/content/v1/spaces/{YOUR_SPACE_ID}/explore?access_token={YOUR_ACCESS_TOKEN}</code></pre>
    </div>
  </div>

  <p class="post__p">Here’s where we’ll construct our GraphQL query to use in our application code.</p><p class="post__p">You might need to play around a bit with the format of your query depending on your content type fields (and whether or not you chose to include panthers!), but here’s some example code to get you started.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="EHmhVzVcLA"
      aria-describedby="EHmhVzVcLA">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="EHmhVzVcLA">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="graphql">
      <meta data-code-id="EHmhVzVcLA" itemprop="text" content="%7B%0A%20%20microblogCollection%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20firstPublishedAt%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20text%0A%20%20%20%20%20%20image%20%7B%0A%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20width%0A%20%20%20%20%20%20%20%20height%0A%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20panther%0A%20%20%20%20%20%20link%0A%20%20%20%20%20%20linkText%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D">
      <pre class="language-graphql"><code class="language-graphql"><span class="token punctuation">{</span><br>  <span class="token object">microblogCollection</span> <span class="token punctuation">{</span><br>    <span class="token object">items</span> <span class="token punctuation">{</span><br>      <span class="token object">sys</span> <span class="token punctuation">{</span><br>        <span class="token property">firstPublishedAt</span><br>      <span class="token punctuation">}</span><br>      <span class="token property">text</span><br>      <span class="token object">image</span> <span class="token punctuation">{</span><br>        <span class="token property">url</span><br>        <span class="token property">title</span><br>        <span class="token property">width</span><br>        <span class="token property">height</span><br>        <span class="token property">description</span><br>      <span class="token punctuation">}</span><br>      <span class="token property">panther</span><br>      <span class="token property">link</span><br>      <span class="token property">linkText</span><br>    <span class="token punctuation">}</span><br>  <span class="token punctuation">}</span><br><span class="token punctuation">}</span></code></pre>
    </div>
  </div>

  <p class="post__p">A key thing to note is that the name of your content type (i.e. microblog) will need to be queried as a collection, e.g. microblogCollection, with a property of items.</p><p class="post__p">Notice that the query is constructed from the automatically generated field names we defined when we set up our content type. Give it a go!</p><p class="post__p">You’ll also notice that I’m requesting some system information in this query (sys.firstPublishedAt). You’re free to add a separate date field to your microblog content type if you wish, instead of using the system information. Moreover, if you’re using the Contentful REST API, sys.created and sys.updated will be available for you to consume.</p><h3 class="post__h3">Finally, let’s write some vanilla front-end code!</h3><p class="post__p">Let’s set up a basic HTML boilerplate file and add links to our CSS file and JS file.</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="AaZfbUMVBq"
      aria-describedby="AaZfbUMVBq">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="AaZfbUMVBq">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="html">
      <meta data-code-id="AaZfbUMVBq" itemprop="text" content="%3C!DOCTYPE%20html%3E%0A%3Chtml%20lang%3D%22en%22%3E%0A%20%20%3Chead%3E%0A%20%20%20%20%3Cmeta%20charset%3D%22utf-8%22%3E%0A%20%20%20%20%3Ctitle%3Ethingoftheday%20by%20%40whitep4nth3r%3C%2Ftitle%3E%0A%20%20%20%20%3Cmeta%20name%3D%22description%22%20content%3D%22A%20wonderful%20description%22%3E%0A%20%20%20%20%3Cmeta%20name%3D%22viewport%22%20content%3D%22width%3Ddevice-width%2C%20initial-scale%3D1%22%3E%0A%20%20%20%20%3Clink%20rel%3D%22stylesheet%22%20href%3D%22style.css%22%3E%0A%20%20%3C%2Fhead%3E%0A%20%20%3Cbody%3E%0A%20%20%20%20%3Cscript%20type%3D%22module%22%20src%3D%22app.js%22%3E%3C%2Fscript%3E%0A%20%20%3C%2Fbody%3E%0A%3C%2Fhtml%3E">
      <pre class="language-html"><code class="language-html"><span class="token doctype"><span class="token punctuation">&lt;!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>utf-8<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>title</span><span class="token punctuation">></span></span>thingoftheday by @whitep4nth3r<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>title</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>description<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>A wonderful description<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>style.css<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">></span></span><br>    <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>app.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><br>  <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">></span></span><br><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">></span></span></code></pre>
    </div>
  </div>

  <p class="post__p">Since we’re using vanilla JavaScript, we’re going to use the native fetch<b class="post__p--italic"> </b>api in our code.</p><p class="post__p">The aim of this next part is to fetch our content from Contentful in our app.js file, and log it to the browser console.</p><p class="post__p">Use the query you constructed in the GraphiQL interface, construct your fetch options and fetch that data!</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="XYCziiMACg"
      aria-describedby="XYCziiMACg">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="XYCziiMACg">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="XYCziiMACg" itemprop="text" content="%2F%2FHere's%20our%20query%20we%20constructed%20in%20the%20GraphiQL%20interface%0Aconst%20query%20%3D%20%60%7B%0A%20%20microblogCollection%20%7B%0A%20%20%20%20items%20%7B%0A%20%20%20%20%20%20sys%20%7B%0A%20%20%20%20%20%20%20%20firstPublishedAt%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20text%0A%20%20%20%20%20%20image%20%7B%0A%20%20%20%20%20%20%20%20url%0A%20%20%20%20%20%20%20%20title%0A%20%20%20%20%20%20%20%20width%0A%20%20%20%20%20%20%20%20height%0A%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20panther%0A%20%20%20%20%20%20link%0A%20%20%20%20%20%20linkText%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%60%0A%0A%2F%2F%20Here%20are%20our%20options%20to%20use%20with%20fetch%0Aconst%20fetchOptions%20%3D%20%7B%0A%20%20spaceID%3A%20%22yourSpaceId%22%2C%0A%20%20accessToken%3A%20%22yourAccessToken%22%2C%0A%20%20endpoint%3A%20%22https%3A%2F%2Fgraphql.contentful.com%2Fcontent%2Fv1%2Fspaces%2FyourSpaceID%22%2C%0A%20%20method%3A%20%22POST%22%2C%0A%20%20headers%3A%20%7B%0A%20%20%20%20Authorization%3A%20%22Bearer%20yourAccessToken%22%2C%0A%20%20%20%20%22Content-Type%22%3A%20%22application%2Fjson%22%2C%0A%20%20%7D%2C%0A%20%20body%3A%20JSON.stringify(%7B%20query%20%7D)%0A%7D%0A%0A%2F%2F%20Let's%20fetch%20the%20data%20-%20check%20out%20the%20browser%20console!%0Afetch(endpoint%2C%20fetchOptions)%0A%20%20.then(response%20%3D%3E%20response.json())%0A%20%20.then(data%20%3D%3E%20console.log(data))%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token comment">//Here's our query we constructed in the GraphiQL interface</span><br><span class="token keyword">const</span> query <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">{<br>  microblogCollection {<br>    items {<br>      sys {<br>        firstPublishedAt<br>      }<br>      text<br>      image {<br>        url<br>        title<br>        width<br>        height<br>        description<br>      }<br>      panther<br>      link<br>      linkText<br>    }<br>  }<br>}</span><span class="token template-punctuation string">`</span></span><br><br><span class="token comment">// Here are our options to use with fetch</span><br><span class="token keyword">const</span> fetchOptions <span class="token operator">=</span> <span class="token punctuation">{</span><br>  <span class="token literal-property property">spaceID</span><span class="token operator">:</span> <span class="token string">"yourSpaceId"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">accessToken</span><span class="token operator">:</span> <span class="token string">"yourAccessToken"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">endpoint</span><span class="token operator">:</span> <span class="token string">"https://graphql.contentful.com/content/v1/spaces/yourSpaceID"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">"POST"</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span><br>    <span class="token literal-property property">Authorization</span><span class="token operator">:</span> <span class="token string">"Bearer yourAccessToken"</span><span class="token punctuation">,</span><br>    <span class="token string-property property">"Content-Type"</span><span class="token operator">:</span> <span class="token string">"application/json"</span><span class="token punctuation">,</span><br>  <span class="token punctuation">}</span><span class="token punctuation">,</span><br>  <span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span> query <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Let's fetch the data - check out the browser console!</span><br><span class="token function">fetch</span><span class="token punctuation">(</span>endpoint<span class="token punctuation">,</span> fetchOptions<span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">data</span> <span class="token operator">=></span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p">You might be worried that you’re using sensitive data (space ID and access token) in your code directly rather than using environment variables (as you would usually do in a framework like React), but don’t fret!</p><p class="post__p">Contentful’s Content Delivery API (or CDA) is a read-only API. If you’re using Contentful to fetch public data, there’s nothing to worry about. If you would prefer to keep your Contentful credentials private, I would recommend proxying the data fetched from Contentful to your client via your own API server application.</p><p class="post__p">Start your http-server and check out the console in your browser. </p><p class="post__p">We have content! 🎉</p><img src="https://images.ctfassets.net/56dzm01z6lln/1e0KDObYleURkUEKC3cXfD/0335836b213f0d2f17e71d7a268a686b/step19.png" alt="Checking the console in the browser for the fetched content" height="482" width="811" /><h3 class="post__h3">Let’s add our content to the DOM!</h3><p class="post__p">For each bit of data of each item in the returned array, we’ll want to create an HTML element and append it to the DOM.</p><p class="post__p">Here’s a example function to get you started:</p>
  <div class="post__codeBlock">
    <div class="post__codeBlockBar">
      <span class="post__codeBlockDots">
        <img src="/img/snippet_buttons.svg" alt="" />
      </span>
      <button type="button" class="post__codeBlockCopyButton" data-copy="ZMVbHDXhYh"
      aria-describedby="ZMVbHDXhYh">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 384 512"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM112 192l160 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-160 0c-8.8 0-16-7.2-16-16s7.2-16 16-16z" fill="currentColor" /></svg>
      <span>
        Copy
      </span>
      </button>
    </div>
    <div data-code-block itemscope itemtype="https://schema.org/SoftwareSourceCode" id="ZMVbHDXhYh">
      <meta itemprop="codeSampleType" content="snippet">
      <meta itemprop="programmingLanguage" content="javascript">
      <meta data-code-id="ZMVbHDXhYh" itemprop="text" content="const%20addContentToDom%20%3D%20(items)%20%3D%3E%20%7B%0A%20%20items.forEach(item%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F%20Create%20the%20article%20element%20to%20hold%20all%20post%20data%20elements%0A%20%20%20%20const%20newItemEl%20%3D%20document.createElement(%22article%22)%3B%0A%0A%20%20%20%20%2F%2F%20The%20panther%20field%20is%20required%20in%20our%20content%20type%0A%20%20%20%20%2F%2F%20We'll%20always%20have%20a%20panther!%0A%20%20%20%20const%20newPantherEl%20%3D%20document.createElement(%22img%22)%3B%0A%20%20%20%20newPantherEl.src%20%3D%20%60.%2Fpanthers%2F%24%7Bitem.panther%7D.svg%60%3B%0A%20%20%20%20newPantherEl.alt%20%3D%20%60%24%7Bitem.panther%7D%20panther%20emote%60%3B%0A%0A%20%20%20%20%2F%2F%20Append%20the%20panther%20element%20to%20the%20article%20element%0A%20%20%20%20newItemEl.appendChild(newPantherEl)%3B%0A%0A%20%20%20%20%2F%2F%20Let's%20check%20if%20we%20have%20an%20image%0A%20%20%20%20if%20(item.image)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Create%20an%20image%20element%0A%20%20%20%20%20%20const%20newImgEl%20%3D%20document.createElement(%22img%22)%3B%0A%20%20%20%20%20%20%2F%2F%20Populate%20with%20data%0A%20%20%20%20%20%20newImgEl.src%20%3D%20%60%24%7Bitem.image.url%7D%3Fw%3D500%60%3B%0A%20%20%20%20%20%20newImgEl.alt%20%3D%20item.image.description%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Add%20the%20image%20element%20to%20the%20article%20element%0A%20%20%20%20%20%20newItemEl.appendChild(newImgEl)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Let's%20check%20if%20we%20have%20some%20text%0A%20%20%20%20if%20(item.text)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Create%20an%20h2%20element%0A%20%20%20%20%20%20const%20newTextEl%20%3D%20document.createElement(%22h2%22)%3B%0A%20%20%20%20%20%20%2F%2F%20Populate%20with%20data%0A%20%20%20%20%20%20newTextEl.innerText%20%3D%20item.text%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Add%20the%20text%20element%20to%20the%20article%20element%0A%20%20%20%20%20%20newItemEl.appendChild(newTextEl)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Let's%20check%20if%20we%20have%20a%20link%0A%20%20%20%20if%20(item.link)%20%7B%0A%20%20%20%20%20%20%2F%2F%20Create%20an%20anchor%20element%0A%20%20%20%20%20%20const%20newLinkEl%20%3D%20document.createElement(%22a%22)%3B%0A%20%20%20%20%20%20%2F%2F%20Populate%20with%20data%20%20%20%20%20%20%0A%20%20%20%20%20%20newLinkEl.href%20%3D%20item.link%3B%0A%20%20%20%20%20%20newLinkEl.innerText%20%3D%20item.linkText%20%7C%7C%20%22View%20more%22%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Add%20the%20link%20element%20to%20the%20article%20element%0A%20%20%20%20%20%20newItemEl.appendChild(newLinkEl)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Let's%20append%20the%20new%20article%20element%20to%20the%20DOM!%0A%20%20%20%20document.body.appendChild(newItemEl)%3B%0A%20%20%7D)%0A%7D%0A%0A%2F%2F%20Don%E2%80%99t%20forget%20to%20pass%20the%20data%20from%20the%20fetch%20response%20correctly%0A%2F%2F%20to%20your%20function!%0Afetch(endpoint%2C%20fetchOptions)%0A%20%20.then(response%20%3D%3E%20response.json())%0A%20%20.then(data%20%3D%3E%20addContentToDom(data.data.microblogCollection.items))%3B">
      <pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token function-variable function">addContentToDom</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">items</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>  items<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token parameter">item</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br>    <span class="token comment">// Create the article element to hold all post data elements</span><br>    <span class="token keyword">const</span> newItemEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"article"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// The panther field is required in our content type</span><br>    <span class="token comment">// We'll always have a panther!</span><br>    <span class="token keyword">const</span> newPantherEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"img"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>    newPantherEl<span class="token punctuation">.</span>src <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./panthers/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token punctuation">.</span>panther<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.svg</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>    newPantherEl<span class="token punctuation">.</span>alt <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token punctuation">.</span>panther<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> panther emote</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br><br>    <span class="token comment">// Append the panther element to the article element</span><br>    newItemEl<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>newPantherEl<span class="token punctuation">)</span><span class="token punctuation">;</span><br><br>    <span class="token comment">// Let's check if we have an image</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>item<span class="token punctuation">.</span>image<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token comment">// Create an image element</span><br>      <span class="token keyword">const</span> newImgEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"img"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token comment">// Populate with data</span><br>      newImgEl<span class="token punctuation">.</span>src <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token punctuation">.</span>image<span class="token punctuation">.</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">?w=500</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br>      newImgEl<span class="token punctuation">.</span>alt <span class="token operator">=</span> item<span class="token punctuation">.</span>image<span class="token punctuation">.</span>description<span class="token punctuation">;</span><br><br>      <span class="token comment">// Add the image element to the article element</span><br>      newItemEl<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>newImgEl<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br><br>    <span class="token comment">// Let's check if we have some text</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>item<span class="token punctuation">.</span>text<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token comment">// Create an h2 element</span><br>      <span class="token keyword">const</span> newTextEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"h2"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token comment">// Populate with data</span><br>      newTextEl<span class="token punctuation">.</span>innerText <span class="token operator">=</span> item<span class="token punctuation">.</span>text<span class="token punctuation">;</span><br><br>      <span class="token comment">// Add the text element to the article element</span><br>      newItemEl<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>newTextEl<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br><br>    <span class="token comment">// Let's check if we have a link</span><br>    <span class="token keyword">if</span> <span class="token punctuation">(</span>item<span class="token punctuation">.</span>link<span class="token punctuation">)</span> <span class="token punctuation">{</span><br>      <span class="token comment">// Create an anchor element</span><br>      <span class="token keyword">const</span> newLinkEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">"a"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br>      <span class="token comment">// Populate with data      </span><br>      newLinkEl<span class="token punctuation">.</span>href <span class="token operator">=</span> item<span class="token punctuation">.</span>link<span class="token punctuation">;</span><br>      newLinkEl<span class="token punctuation">.</span>innerText <span class="token operator">=</span> item<span class="token punctuation">.</span>linkText <span class="token operator">||</span> <span class="token string">"View more"</span><span class="token punctuation">;</span><br><br>      <span class="token comment">// Add the link element to the article element</span><br>      newItemEl<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>newLinkEl<span class="token punctuation">)</span><span class="token punctuation">;</span><br>    <span class="token punctuation">}</span><br><br>    <span class="token comment">// Let's append the new article element to the DOM!</span><br>    document<span class="token punctuation">.</span>body<span class="token punctuation">.</span><span class="token function">appendChild</span><span class="token punctuation">(</span>newItemEl<span class="token punctuation">)</span><span class="token punctuation">;</span><br>  <span class="token punctuation">}</span><span class="token punctuation">)</span><br><span class="token punctuation">}</span><br><br><span class="token comment">// Don’t forget to pass the data from the fetch response correctly</span><br><span class="token comment">// to your function!</span><br><span class="token function">fetch</span><span class="token punctuation">(</span>endpoint<span class="token punctuation">,</span> fetchOptions<span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">response</span> <span class="token operator">=></span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br>  <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">data</span> <span class="token operator">=></span> <span class="token function">addContentToDom</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>data<span class="token punctuation">.</span>microblogCollection<span class="token punctuation">.</span>items<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
    </div>
  </div>

  <p class="post__p"> You’ll now find your posts as unstyled HTML in the browser!</p><img src="https://images.ctfassets.net/56dzm01z6lln/5xYP2vs332a8kxJqeXljuD/38ea8702daf2109a0f4c06018c62373b/step20.png" alt="A screenshot of the HTML written to the DOM in the browser source code" height="315" width="771" /><p class="post__p">With a little bit of date formatting, adding some classes to the HTML and some vanilla CSS magic, you’ll end up with something like this in no time. Now all you need to do is remember to add a daily post!</p><img src="https://images.ctfassets.net/56dzm01z6lln/20BbwezR56teLt5U0XoFjL/f9e3266315c3dbb4d295ad6e724430e7/step21.png" alt="A screenshot of the styled microblog page" height="677" width="626" /><p class="post__p"><a href="https://github.com/whitep4nth3r/thingoftheday" target="_blank"><u>You can view the code for the application on GitHub here</u></a>, and you can view <a href="https://thingoftheday.netlify.app/" target="_blank"><u>my new microblogging site at thingoftheday.xyz</u></a>. Feel free to fork it, play around with it and make it your own with your own content. I’ve also included a quick setup guide in the <a href="https://github.com/whitep4nth3r/thingoftheday/blob/main/README.md" target="_blank"><u>README file</u></a>, where you can import the content type and example content defined in this tutorial using the Contentful CLI in a matter of minutes. </p><p class="post__p">If you’ve used this tutorial to help you get started with a lightweight microblogging site using Contentful, I’d love to check it out! <a href="https://twitch.tv/whitep4nth3r" target="_blank"><u>Come and say hi on Twitch</u></a>.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>My non-traditional journey into tech and DevRel</title>
          <description>Learn about what led Salma to DevRel and how she accidentally increased engagement in the Contentful Community by 200% on her first day.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.contentful.com/blog/2021/02/03/salma's-journey-into-tech-and-devrel/</link>
          <guid>https://www.contentful.com/blog/2021/02/03/salma's-journey-into-tech-and-devrel/</guid>
          <pubDate>Wed, 03 Feb 2021 00:00:00 GMT</pubDate>
          <category>Career</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">After navigating a varied career path — including teaching music and performing musical comedy — Salma, or @whitep4nth3r as she has come to be known across the vast interwebs, joined our DevRel team at Contentful in January 2021. 🎉 We managed to catch Salma amidst her busy onboarding schedule to learn about what led her to developer relations and how she accidentally increased engagement in the<a href="https://www.contentful.com/slack/" target="_blank"> Contentful Community Slack</a> by 200% on her first day at Contentful.</p><h2 class="post__h2">Hello, Salma! We’re thrilled to have you on the team! How did you get into tech?</h2><p class="post__p">There are four chapters to my journey in tech. </p><p class="post__p">I’ll start with the story of my earliest memory in front of a computer. There was an old Commodore 64 machine kicking around my house in the 1980s — you know, the type of thing where you loaded a cassette tape into a strange contraption hooked up to a chunky computer keyboard. After pressing play on the cassette player, you had to literally go outside and play because it would take about an hour before the game was actually playable.</p><p class="post__p">I remember there was a four-inch thick programming manual that came with this C64, and during my early days of learning to read, I decided to explore it.</p><p class="post__p">I was around six years old at the time, and to cut an increasingly (an unnecessarily) long story short, I remember sitting in front of that machine for hours at a time, meticulously copying symbols from the manual (only recently did I learn I was writing BASIC), character by character, into a fascinating blue terminal screen. </p><p class="post__p">And MAGIC happened! I made a ball bounce across the screen! I printed patterns! I made coloured stripes! I could do ANYTHING! I was hooked.</p><p class="post__p">But this was 1991, and the internet wasn’t a thing for most people yet. I didn’t even know how to articulate that I really, really, <b class="post__p--italic">really</b> wanted to explore this. I probably didn’t even have the word “programming” in my vocabulary yet; and, I had no idea what I was doing. But I did know that I was incredibly entranced by the whole experience.</p><h2 class="post__h2">What happened after you mastered programming in BASIC at the age of six?</h2><p class="post__p">At this point in my life I had already gotten heavily into learning music, so this took over.  I don’t think it’s a coincidence that my degree is in music composition. There are so many similarities between the way I write music and code. Both are motif based. Short, reusable snippets of code are like short, reusable musical motifs. </p><p class="post__p">The second chapter of my journey into tech happened in 1998 at the dawn of the dial-up home internet connection. The internet was just as fascinating to me as writing BASIC on a C64. Information was everywhere, communication with friends was no longer confined to a landline telephone conversation and I discovered the joy of building websites with the now-defunct GeoCities.</p><p class="post__p">My first website had all the bells and whistles of a website built in the 90s: a visitor counter, a guest book, terribly inaccessible animations, an abundance of #0000FF (also known as “internet blue”) and lots of personality. In fact, I am so fond of the nostalgia of the internet in the 90s that I recently completed a frontend challenge where <a href="https://stylestage.dev/styles/geocities/" target="_blank"><u>I recreated the look and feel of a GeoCities website entirely in CSS</u></a>. The good thing about this version is that it’s responsive and accessible! </p><p class="post__p">With limited resources to really come to grips with programming at the time, I continued to pursue music, and went to study music composition at the Royal Northern College of Music in Manchester. </p><p class="post__p">But my tech pursuits didn’t stop there. As I got more and more into music technology and production, I moved away from Windows and embraced Macs. And, back then, iMacs came bundled with another deprecated piece of software: iWeb. </p><p class="post__p">At this point, I started experimenting with iWeb to build websites. iWeb was a drag-and-drop website builder. If you used images for most of the elements on the page, and iframes that sourced text files for dynamic content, you could end up with a pretty snazzy looking result! (If you ignored the fact that the page wasn’t accessible or responsive.) This is also how I gained some skills in graphic design.</p><h2 class="post__h2">What happened after you graduated?</h2><p class="post__p">Being a recent music graduate, paid work was few and far between. So I made a little bit of a living designing album artwork and complementary websites (using iWeb) for my musician friends, which were hosted in strange places and edited by uploading file changes via an FTP file server — all while I recorded and released an album on Spotify with my folk band. </p><p class="post__p">You would never believe that, at the time of this interview, <a href="http://www.thesterlingtrio.com/thesterlingtrio.com/Home.html" ><u>this website I built using iWeb</u></a> is still live on the internet. </p><p class="post__p">I haven’t inspected the code to see how it compares to modern web standards. I’m too afraid.Tell us about your first job in development</p><p class="post__p">I’ll skip over the few years that involved becoming a qualified music teacher, teaching kids how to play in rock bands and gigging as a musical comedian, to when I abruptly decided to quit teaching altogether and found myself working in a call center. </p><p class="post__p">Through a series of serendipitous events, my graphic design experience led me to what became my first development role for a small magazine website, which operated above a furniture shop in a tiny northern UK village.</p><p class="post__p">The three previous chapters in tech had given me some limited experience, but I managed to convince the one “IT guy” there (he was responsible for the infrastructure, backend, frontend and CMS) to take me under his wing. I was really fortunate to be able to learn on the job.</p><p class="post__p">It’s so important to have a mentor. Whether you’re learning on the job or learning on your own, having someone to talk with about programming problems is absolutely invaluable. After all, programming isn’t necessarily about writing code. It’s about solving problems and building things for real people to use. </p><h2 class="post__h2">How did your career in tech evolve from there?</h2><p class="post__p">I’ve worked on a variety of different software products, including global ecommerce platforms, SaaS startups, mobile apps and, most recently, at a product agency in Manchester, UK. </p><p class="post__p">I think the experience I had in graphic design greatly influenced my choice to specialize in frontend technologies. Due to my teaching and leadership experience before my final journey chapter, I moved on to working as a tech lead quite early on in my tech career. I’ve been involved in a lot of backend technologies and architecture for some time, too.</p><p class="post__p">2020 was a strange year for everyone. And in that strange year, I discovered that I quite enjoyed <a href="https://twitch.tv/whitep4nth3r" target="_blank"><u>streaming live coding on Twitch</u></a>. Through building a community around my Twitch channel, I was fortunate to be introduced to the world of DevRel. I didn’t even know it existed until a few months ago. </p><p class="post__p">I feel very lucky to have found my first DevRel role at such a welcoming company that builds a product that makes frontend applications so easy to build — something I have always advocated for.</p><h2 class="post__h2">Finally — how did you increase engagement in the Contentful Community Slack by 200% on your first day?</h2><p class="post__p">I’m a growth hacker! An innovator! </p><p class="post__p">… I pressed the wrong button and accidentally enabled two-factor authentication for all c. 5,000 members of the Contentful Community Slack. And now I am fondly known as the team security champion. 🚨</p><h2 class="post__h2">Great work! Any final words?</h2><p class="post__p">If you’d like to stay up to date with what’s happening in Salma’s world, find her on <a href="https://twitch.tv/whitep4nther" target="_blank"><u>Twitch</u></a> and in the rainy suburbs of Manchester, UK. Or join the <a href="https://www.contentful.com/slack/" target="_blank"><u>Contentful Community</u></a><a href="https://www.contentful.com/slack/" target="_blank"><u> Slack</u></a> —  just make sure you’re on top of your security. 😉</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>5 tips for leading teams with an empathy-first approach</title>
          <description>Here's me trying to sum up my time as an engineering team lead as I move into DevRel in 2021.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://www.codecomputerlove.com/blog/5-tips-for-leading/</link>
          <guid>https://www.codecomputerlove.com/blog/5-tips-for-leading/</guid>
          <pubDate>Mon, 23 Nov 2020 00:00:00 GMT</pubDate>
          <category>Career</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p"><b class="post__p--italic">As my time as an engineering team lead comes to an end and I move into DevRel in 2021 - I wrote about how I prioritise empathy in tech teams over everything - something that will continue to stay with me firmly as my career continues to evolve.</b></p><hr class="post__hr" /><p class="post__p">I’ve been working as a technical lead for almost six years. During this time I’ve had the opportunity to work with diverse groups of people to build a variety of different products including e-commerce platforms, energy switching services, brochure websites and personal gifting experiences to name but a few.</p><p class="post__p">Regardless of the product, in-house company or client, external factors or urgency of deadlines – these very different experiences have one thing in common: every single team has been an incredibly high-performing and collaborative group of people.</p><h2 class="post__h2">What’s my secret?</h2><p class="post__p">For those that have worked with me, you might attribute this to the rigorous weekly processes that I support the team to put in place: prioritisation sessions, team retrospectives, Wednesday planning sessions (the day here is very important!), morning stand-ups and afternoon catch-ups.</p><p class="post__p">You might also credit success to the unrelenting autonomy that I encourage within teams, ensuring that people are given the freedom to be creative, experiment, and own the way that products are built and delivered.</p><p class="post__p">But these are all just ways of working. And what I want to talk about today is a way of <b class="post__p--italic">being</b>.</p><h2 class="post__h2">Being human beings</h2><p class="post__p">Human beings are complicated. We are a bag of experiences, emotions, fears, hopes, dreams, preferences and choices. Some of us have big families, some of us live alone, some of us care for others. Some of us are extroverted, introverted, somewhere in between. We learn in different ways, we respond intuitively to music, language, colours, weather, seasons, other people.</p><p class="post__p">We’re all so incredibly different, yet all so incredibly interlinked by our human need to be <b class="post__p--italic">heard and understood.</b></p><h2 class="post__h2">Good leaders make you feel safe</h2><p class="post__p">Empathy is defined as the ability to <b class="post__p--italic">understand and share the feelings of another</b>. It sounds incredibly easy to be empathetic in theory, but whilst the stresses of the 2020 global pandemic and beyond keeps us firmly in survival mode, it’s increasingly tricky, but also increasingly important to practise empathy with our colleagues.</p><p class="post__p">As human beings, we need psychological safety now more than ever. Now is the time to come together to lift each other up and out of survival mode, so we are free to explore, create and experiment, in order to reach our full potential at work.</p><p class="post__p">Here are my top five guiding principles that help me prioritise leading teams through an empathy-first approach.</p><h2 class="post__h2">1. I am not a manager</h2><p class="post__p">The most prevalent way to develop an empathetic relationship with your team is to be part of the team. Notice that I never refer to team leads as ‘managers’. Your role as a team lead is to provide direction, support and protection – not to ‘manage’ or ‘take charge of’ in the traditional sense.</p><p class="post__p">If you, as a team lead are integrated with your team, are involved in (but not necessarily leading) team ceremonies, pair-programming with developers, and architecting roadmaps with product owners – you are much more likely to <b class="post__p--italic">understand and share their feelings</b> as a result of being in the same shared situations.</p><h2 class="post__h2">2. You are not a resource</h2><p class="post__p">This is a tricky subject when working at a product studio that adopts an agency model, where clients are often billed by the hour for people’s time. The most impactful thing you can do is humanise your team and refer to people as people – not resources.</p><h2 class="post__h2">3. You can only go as fast as you can go</h2><p class="post__p"><b class="post__p--italic">We have to go faster! We must increase velocity! We should deliver at pace! Do this ASAP!</b></p><p class="post__p">I think Basecamp (<a href="https://basecamp.com/guides/how-we-communicate" target="_blank">Guide to Internal Communication, the Basecamp Way</a>) said it best: “Urgency is overrated, ASAP is poison.”</p><p class="post__p">Until every part of a developer’s job is automated (I give it until the year 2100), human beings can only go as fast as human beings can go. We can only define requirements for a product so quickly, only prioritise the roadmap so efficiently, only type on our keyboards so fast, and release changes when they’re fully tested and ready.</p><p class="post__p">Without delving into a completely different topic on the toxicity of arbitrary deadlines – you, the team and its individuals can only go as fast as you can go. Ensure that as a team lead you are offering your team this level of protection from external pressures. Allow your team the space to be creative, grow, explore, and factor this into estimates, roadmaps and time planning.</p><h2 class="post__h2">4. You are never ‘too’ emotional</h2><p class="post__p">Your team members are not machines. They are human beings with complex emotions and hormones and chemicals and feelings and thoughts and a life to live outside of work. Sometimes days are more challenging than others – and that’s okay. Encourage your team members to take care of themselves and to take time and space away from their computer screens.</p><p class="post__p">Lend your team members a compassionate ear when they need it. Encourage them to explore their challenges and successes with you and get to know them as people. In doing this you’ll be able to build up a clear picture of how your team will respond to certain situations, so you’ll be able to proactively prepare to protect them if the need should arise.</p><h2 class="post__h2">5. This is just a job</h2><p class="post__p">The most important lesson I’ve learned from Steve Parker, Product Owner and one of my most valued colleagues at Code – is despite how passionate you are about your craft, don’t put too much pressure on yourself at work.</p><p class="post__p">After all, it’s just a job. (I can hear this in his voice, now). There are more important things to be worrying about.</p><p class="post__p">No one’s going to die (at least at Code).</p><p class="post__p"><b class="post__p--italic"></b></p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>6 things I learned about streaming live coding in my first 6 weeks on Twitch</title>
          <description>The most important part of your stream is YOU.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/blog/6-things-i-learned-about-streaming-live-coding-in-my-first-6-weeks-on-twitch/</link>
          <guid>https://whitep4nth3r.com/blog/6-things-i-learned-about-streaming-live-coding-in-my-first-6-weeks-on-twitch/</guid>
          <pubDate>Sun, 16 Aug 2020 00:00:00 GMT</pubDate>
          <category>Streaming</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">I hesitantly pressed the go-live button on Twitch for the very first time on Thursday 25th June. Within seven days I had reached Affiliate status, and had collected a core group of regular viewers who continued (for some reason) to return to my streams.</p><p class="post__p">For anyone who&#39;s thinking about getting started with streaming live coding, here&#39;s six valuable lessons I learned in my first six weeks on Twitch, and hopefully it makes you a little less hesitant about going live yourself.</p><h2 class="post__h2">1. You don&#39;t need a pro set up to get started</h2><p class="post__p">Don&#39;t invest in expensive kit when you&#39;re just starting out. In fact, I would highly recommend that you buy <b class="post__p--bold">absolutely nothing</b> in preparation for your first streams. You don&#39;t really know what you&#39;re going to need until your content starts evolving.</p><p class="post__p">All you need to get started is a laptop with an in-built microphone and webcam, some free streaming software such as <a href="https://obsproject.com/download" target="_blank">OBS</a>, and something to do on your stream. Anything else is overkill and will certainly give you a lot more to worry about as you go live for the first time. More kit definitely means more problems. Start slow, and adapt as you go.</p><p class="post__p">Once you&#39;ve found your rhythm, developed your style, and your community is starting to grow, then is the time to figure out how to increase the production value of your stream — <b class="post__p--italic">whatever that may mean for you.</b></p><p class="post__p">Here&#39;s what my streams looked like when I started out.</p><img src="https://images.ctfassets.net/56dzm01z6lln/bb0BjRGjW5ebTjENKYOKd/e28b5ed20d67726fa23f639982464c1e/original-stream-setup.png" alt="A screenshot of whitep4nth3r's simple stream overlays from the early days of streaming in 2020." height="561" width="1000" /><h2 class="post__h2">2. You will have tech issues!</h2><p class="post__p">When the time comes for you to work on the production quality of your stream — you will have technical issues. <b class="post__p--bold">Dropped frames, audio out of sync with the webcam image, &quot;Minecraft style resolution&quot;, white balance issues, flickering webcam image and lack of processing power</b> — these are just <b class="post__p--italic">some</b> of the issues I faced as I started to evolve my setup.</p><p class="post__p">Viewers <b class="post__p--bold">will</b> point out your technical issues. Whilst this is stressful whilst you&#39;re trying to stay on top of your content and engage with the chat — this feedback is a gift. It shows that your viewers are engaged and want to see you succeed.</p><p class="post__p">If it&#39;s not possible to tweak those issues as they happen, experiment with different settings and approaches to solving your problems off-stream, and always crowdsource advice.</p><p class="post__p">Here are some of the key things I&#39;ve learned about the optimum technical setup:</p><h3 class="post__h3">1. Do not stream using WiFi</h3><p class="post__p">If you can&#39;t deal with a 50m long ethernet cable trailing up or down your stairs, try a powerline adaptor.</p><h3 class="post__h3">2. If your programming is CPU-intensive, try a dual machine setup</h3><p class="post__p">I know this isn&#39;t the most cost-effective solution, but I was lucky to have a PC sitting idly below my desk. I now use a PC to run OBS and stream via powerline, whilst I code on my Macbook that is connected to the PC via a capture card.</p><h3 class="post__h3">3. If your shiny new camera flickers randomly, use default Windows drivers and USB 2.0</h3><p class="post__p">After a few weeks of streaming with my MacBook webcam, I switched to using a Logitech Streamcam, but I had nightmare after nightmare trying to work out why the camera was intermittently flickering. After I tried <b class="post__p--italic">everything</b> and almost rage-quit streaming altogether, I found the solution was software-based. I switched to using Windows default drivers rather than the Logitech drivers, and purchased an adaptor to plug the proprietary USB C connector into USB 2.0. <b class="post__p--bold">And my problem was solved!</b></p><p class="post__p">(This might have been a unique problem to me due to the first generation USB C hardware on the motherboard in the PC, but if I can help at least one other person with this advice, then it&#39;s definitely worth mentioning!)</p><p class="post__p">Read this post from 2021 that describes my streaming setup in detail: <a href="https://whitep4nth3r.com/blog/twitch-live-coding-setup-obs/">My Twitch live coding setup in OBS</a></p><h2 class="post__h2">3. Engage with your community</h2><p class="post__p">I love hanging out on streams where the streamer is engaged with the chat. It feels really sociable, like you&#39;re hanging out with a group of friends, sharing the same interests and experience. I try my best to replicate this in my streams, and ensure that I create an inclusive and welcoming atmosphere.</p><p class="post__p">A lot of viewers ask similar questions about getting started in web development, and I make an effort to engage with everyone. It&#39;s these inquisitive viewers that I notice coming back again and again to be part of the conversation.</p><p class="post__p">As your community grows, make sure you build up a core team of mods to support your whilst you&#39;re streaming. They are invaluable to keeping your stream running smoothly. Thank you to all my mods, <b class="post__p--bold">@Steffi128</b> and <b class="post__p--bold">@thatn00b__</b> in particular!</p><h2 class="post__h2">4. Let the look of your stream evolve organically</h2><p class="post__p">When you&#39;re starting out, you can feel like your stream <b class="post__p--italic">doesn&#39;t look good enough</b> compared to other channels.</p><p class="post__p">The options for the look and feel of your stream are endless, and whilst there are a myriad tools out there to help you get set up relatively quickly, stream overlays and artwork can consume your time to the point where you feel like you don&#39;t have any time to actually stream!</p><p class="post__p">You don&#39;t need to achieve <b class="post__p--italic">the perfect stream</b> branding before you go live. In fact, developing your stream overlays is perfect stream <b class="post__p--italic">content</b> — especially where programming is involved. Let your stream look and feel evolve with your personality and content. Plus, it&#39;s exciting for your viewers to return to your stream and find a new interactive feature they can play with!</p><h2 class="post__h2">5. Stream the content that motivates you</h2><p class="post__p">If you try to stream content that doesn&#39;t motivate you, or content you&#39;re not invested in, you simply won&#39;t enjoy it. I stream content centred around front end development, accessibility, and user experience. When I first started out, I noticed that this kind of content was pretty rare on Twitch. And so it got me questioning.</p><p class="post__p"><b class="post__p--italic">Did people really want to watch me make pretty, accessible websites? Or were they more interested in deep-diving into C#, Rust and Python?</b></p><p class="post__p">I experimented with streaming some very different content for a little while (for example, Rust) — and whilst it certainly brought some different viewers to my channel (and some have certainly stayed), I didn&#39;t feel as comfortable with this content.</p><h2 class="post__h2">6. The most valuable element to your stream is you</h2><p class="post__p">The content you are streaming is only <b class="post__p--bold">part</b> of what you bring to the community. Viewers don&#39;t necessarily return to your streams time and time again to learn about what you are coding — they come to hang out with <b class="post__p--bold">you</b>, chat with others, and to feel connected to a community.</p><p class="post__p">If you haven&#39;t pressed the go-live button just yet, if you really want to get started on Twitch, and if you&#39;re not feeling confident about your content — <b class="post__p--italic"><b class="post__p--bold">remember that the most valuable element to your stream is you</b></b> — and there <b class="post__p--bold">will</b> be people out there who will enjoy watching your streams, regardless of what you do.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>An Introduction to the World Wide Web for Very Senior Programmers</title>
          <description>As we witness the much-anticipated release of HTML 2.0, there is no better time to surf the World Wide Web.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/talks/an-introduction-to-the-world-wide-web-for-very-senior-programmers/</link>
          <guid>https://whitep4nth3r.com/talks/an-introduction-to-the-world-wide-web-for-very-senior-programmers/</guid>
          <pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Welcome to an introduction to the worldwide web. Who here is a very senior programmer? I need some more noise. Yes, thank you. Very good. So this talk is for you. It is December 15th, 1995. Now it is unseasonably warm for December, but the scientists have reassured us that climate change will be fixed by the next millennium. And talking of the next millennium, please give me a cheer if you are ready and you have sorted out your millennium bug problems. Thank you very much. I like to hear it. Of course, because you&#39;re all very very senior programmers. Very good. </p><p class="post__p">All right. So, um, why are we here today? We are in a dot comt boom. There are websites for everything these days. There are bookshops, auction sites, dating sites. There&#39;s a guy Craig who made a list. We are truly in innovative times and it is all powered by the worldwide web and HTML. And I want to talk about HTML today and languages in general. </p><p class="post__p">Please here give me a cheer if you are a Cobol programmer. Thank you very much. Very good. And what about C++? Not that many of you. What about early adopters in the audience? Anyone here writing Java? Very good. PHP? Yes, that came out the this year, didn&#39;t it? How many personal homepages have you made, Ilija? &quot;Too many.&quot; Too many. That&#39;s a lot. What I like to hear. </p><p class="post__p">Okay, so we we&#39;ve we&#39;ve got a lot happening on the world wide web and HTML 2.0 has just been released just a few months ago and it is changing the way we look at the world wide web. But I also want to talk to you today about how we build stuff for the world wide web. And I want to plant a question in your head. Why are we still writing HTML using text and text editors, if this is the way the worldwide web is going? So, I&#39;m going to tell you all about HTML, all about the new advancements, and everything&#39;s going to be wonderful. Let&#39;s go. Oh, Ilija, were you on the phone again? I mean, it happens. It&#39;s fine. I&#39;m sure you&#39;ll get a second phone line at some point. We&#39;ll just wait. Won&#39;t take too long. Excellent. Okay. Very lucky that it connected first time, right?</p><p class="post__p">So just a quick primer because I know most of you here are working on servers and and programming and the back end and databases and stuff. So let&#39;s go and have a look at the world wide web. Quick primer. So, the world wide web, invented by my mate Tim Berners-Lee in 1989 and it was released publicly just four years ago in 1991. And so much has happened over the last few years, but it has exponentially increased this year in 1995.</p><p class="post__p">So quickly, now, I know what you&#39;re seeing here is probably blowing your mind right now but please, I will tell you all about it in due course. Let&#39;s focus on this for now. HyperText markup language, that&#39;s what HTML stands for again, invented by Tim Berners-Lee just two years ago in 1993. And it is machine readable. It defines the content of everything that we see on our CRT monitors in front of us. And it&#39;s the new programming language of the worldwide web. It is a fully-fledged programming language.</p><p class="post__p">Surfing the web is fresh this year in 1995. You can buy books about the internet and you can even buy books from a website. If you can&#39;t get down to your local bookstore, there&#39;s a really small independent homepage called amazon.com, and you can purchase this and it will ship to your house in 15 to 30 business days. And I really want you to to be encouraged to support your local online websites, your local stores. I have no doubt that amazon.com will continue to be a beautiful and healthy community-based business.</p><p class="post__p">All right. So, what will the web look like in 30 years? That&#39;s the question we&#39;re talking about today. This is an artist&#39;s impression of what the year 2020 will look like. 2020 is going to be a great year. We&#39;re going to be in our homes surrounded by devices doing meetings, family quizzes, dating probably, and shopping, all of the things, working in our homes trapped by our devices. It&#39;s going to be a brilliant year and I I cannot wait to have the internet glued to my eyes.</p><p class="post__p">But some people, however, do not believe in this brilliant future. Some people think that next year in 1996, the internet will completely collapse. Don&#39;t like this guy, Bob. He&#39;s not on my team. But you in here, you can prevent the collapse of the internet by adopting HTML 2.0 today and building amazing homepages, growing the world wide web for everyone as a community, and we will all profit inside our hearts.</p><p class="post__p">So today&#39;s agenda is this. I&#39;m going to be talking about images. Remember that thing that blew your mind just now? We&#39;re going to be talking about images on the world wide web. We&#39;re going to be talking about new advancements and proposals for how we might present HTML on a page to our users, how we might style it. And then we might also talk about, and we WILL also talk about going beyond the read only experience that the world wide web has been for the last four years. We&#39;re going to talk about bringing CD ROM rich media and interactivity to our browsers. And we&#39;re going to talk about how visual tools are going to bring it all together. And they&#39;re beginning to bring it all together. And then I&#39;m going to give you my prediction for what all of this is going to look like, how we are going to be building homepages in the year 2025 if we have all managed to survive the millennium bug.</p><p class="post__p">So, HTML 2.0  here. Look at this again. It&#39;s glorious. HTML 2 was released on 22nd of September 1995, just a few months ago. And the biggest innovation in HTML 2.0 is this I M G tag which stands for image. This is an image, I M G. It&#39;s fascinating, isn&#39;t it? And also HTML 2.0 is incredible because it sets out the first set of real standards for the world wide web and the guys all formed the World Wide Web Consortium to kind of govern those standards and push us forward to a new and bright future on the web. </p><p class="post__p">So this is all about the new I M G tag, image tag. This is how you use it. It comes with two attributes. You write them in capital letters in HTML 2.0. We got the source which is where your image is on the server. And then we&#39;ve got this alt. Now this alt is providing alternative text for the image. Now some browsers right now cannot render images. They need to be inside an anchor link. It&#39;s all very convoluted and silly. But this alternative text ensures that even if your users are using an inferior browser, they can still access this image, such as this baby. I think this is going to be really big. This is this is going to go places. There are three types of images you can use right now. I&#39;m sure you&#39;re all familiar with XBM. It&#39;s just a monochrome bitmap. We don&#39;t need to talk about that. And then this one, no one&#39;s really sure how to pronounce it. I&#39;m going to go with GIF, like giraffe. So GIFs, 256 colors in a GIF. lossless compression as well. And it moves. This is a GIF. This is a little baby GIF. And it&#39;s incredible. And then we&#39;ve got JPEGs. Now, JPEGs are what photos are on your computer. They have 16 million colors. The compression&#39;s a little bit lossy, but I am really, really looking forward to receiving many unsolicited JPEGs and unsolicited GIF pics in my email inbox as the internet grows and progresses. It&#39;s extremely exciting, images on the web. </p><p class="post__p">You can make your homepages really pop with all of these. Look at this. My favorite is this skeleton. Look at this creepy little guy. I love it. There&#39;s a growing library that you can access. You can just take these, use them. They&#39;re all free. Use Creative Commons. Put them on your homepage. Make sure to use this one when you are working in progress because people need to know. It&#39;s very exciting. Look at this thing. Look at it. Look at it go. I mean, the possibilities are endless. </p><p class="post__p">You can also level up even further with something called an image map. This is how you use it. This is the source of your image map. And what happens here with this anchor tag and this image, when someone clicks on your page, the client will send the x and y coordinates of that to the back end. And depending on which coordinates were clicked, you can then send someone somewhere. So without needing to push the designs of HTML and styling HTML any further, you can just put a full beautiful image on your page like what Microsoft does on their world wide web server. Look, it&#39;s an image. You can click here. You can go and explore the FTP server if you want. Find out about Windows socket. The Microsoft network, finds the facts about the server. It&#39;s incredible, isn&#39;t it? Um this is this is an image on the web. There are other people who are using image maps too, which is very exciting. Hong Kong Polytechnic University are using this. They even tell you that the above is an image map. So, they&#39;re educating you about the web, through their their content and of course it&#39;s under construction as well. So, it&#39;s going to improve as time goes on. I&#39;ll tell you who else is using an image map. The White House. Look at this incredible design. You can sign a guest book. You can read some welcome messages. I am so excited about this one because how groundbreaking and truly incredible to have access to the unfiltered thoughts and feelings of the President of the United States of America [RAPTUROUS APPLAUSE]. Yes, that&#39;s what I&#39;m talking about.</p><p class="post__p">On a more serious note though, HTML is a machine readable visual experience, right? So, we&#39;re getting that built in with our images. Even if the alt attributes are used primarily in your heart for those of those people with older browsers, screen readers are now shipping with Windows 95. And that means those screen readers can read what your images are all about should you provide that alt text. And I know you are all very very senior programmers and therefore you will all be following those rules. And so what&#39;s really really exciting if we survive the millennium bug that the future of the world wide web is guaranteed to be accessible for all. No question. </p><p class="post__p">But I&#39;m going to talk about presenting HTML. That&#39;s what we&#39;re calling it here. What happens now is that you I&#39;m sure you&#39;re all aware if you have a browser you can change the fonts, the colors. You can do that on your own browser, on your own machine, to affect the way you view the world wide web as you&#39;re browsing through those documents. But this isn&#39;t very future proof. It&#39;s only static on your machine. You can&#39;t share it with anyone. You can&#39;t share it between machines should you be using Windows and Macintosh. And also probably your configuration is messing with those documents that you&#39;re reading and the author will never know and so you&#39;ll just think the author&#39;s terrible at HTML, but it&#39;s your fault. So there is a proposal that was presented at the web conference in Chicago last year. Give me a cheer if you were there. [CHEERS] Yes. Oh you guy in the Windows 95. I remember you. I had to get security to escort you out. Y wouldn&#39;t stop talking at me about your Windows 3.1 server. So this cascading HTML stylesheets proposal is extremely exciting. You define an ordered list or cascade of stylesheets to set styles for your HTML 2.0 tags and you define the influence of those styles as a percentage. So, for example, here you&#39;ve got a font size for the H1 at 24 points, 50% influence. Somewhere further down in the stylesheet, you&#39;ll have an H1 font size of 32 points with a 70% influence. It makes perfect sense, doesn&#39;t it?[LAUGHS] Of course, no conflicts whatsoever. </p><p class="post__p">You can also compute things in this cascading HTML stylesheets proposal. So just like HTML, this will be a proper programming language as well. So say for example, the age of a page that&#39;s been open in your browser is greater than 3 days. Maybe you want to decay the website a little bit by turning the background yellow. You also might want to load a different stylesheet depending on the display height of the CRT monitor. And you can also use wild cards, again fully functional programming language. And you can also optimize for different media. So at the moment we&#39;re really focused, we&#39;re really stuck in this whole kind of document replication on the web. But what about printing? What about speech for screen readers? And what about braille? So you could use cascading HTML stylesheets to change the volume of the speech for an emphasis tag and you can just change anything you want if someone wanted to print out your website. </p><p class="post__p">Now, of course, in order to get around all of those extra configurations that users have done to their browsers, you probably need to add a common default fallback kind of thing to reset what that browser configuration has done. But, you can deal with that. And you can also probably change the window settings. So say for example your website has got a dark background, you could access the window and change that too to really like make the the page and the browser experience a seamless kind of experience. It&#39;s going to be really really exciting.</p><p class="post__p">So next I want to talk about how things are moving and how what&#39;s happened in in the last week that is really changing things. So, I&#39;ve been talking a lot about how HTML is read only, mimics print media, it&#39;s static, and it&#39;s becoming more visual with the introduction of images in HTML 2.0. But I don&#39;t know whether you&#39;ve heard that as of last week, this is all about to change. A new programming language has been made, invented, released. It&#39;s called JavaScript. Now, it&#39;s not to be confused with Java. It&#39;s nothing to do with Java. And it was released last week, invented by Brendan Eich at Netscape. And does anyone know what JavaScript does? I&#39;ve got a story for you. It brings interactivity to the world wide web, to the internet.</p><p class="post__p">Now, I probably know that you&#39;re like, &quot;Well, what am I going to do? How am I going to make the web interactive? I just read websites.&quot; Well, let me show you this. Imagine the possibilities. This is a function that executes in the browser. It&#39;s called hello. And it prints a greeting. And it&#39;s called when the page has loaded and it&#39;s ready. And look, there&#39;s no static typing. You can do whatever you want. You can track your visitor activity. You know those kind of fake guest book counters you&#39;ve been creating you&#39;ve been kind of making? You can do that real time you can you can you can increment that number every time someone visits your page you can show dynamic advertisements cookies, you can use client-side cookies now. I&#39;m legitimately interested in those! You can create all sorts of connections between third parties and sell people&#39;s data and it won&#39;t slow down your homepages whatsoever and all features will be supported in all browsers. The future really is here and you can all profit from it.</p><p class="post__p">Recap: HTML 2.0, I M G tag, unsolicited GIF pics, cascading HTML stylesheets, JavaScript for interactivity and I&#39;m going to give you a little demo about that JavaScript later. But there is even more. If you can handle it. This year, multimedia tools for the world wide web have emerged. [APPLAUSE STARTS] Yes, you can round of applause that. [APPLAUSE CONTINUES] Yes, we&#39;ve got two: Macromedia Shockwave Player and FutureWave FutureSplash Animator. And they&#39;re changing the game. Right.</p><p class="post__p">This is Macromedia Shockwave. This is not Macromedia Shockwave player, this is Chip&#39;s challenge. And I&#39;m stuck on this level. If anyone wants to help me later, if you can find me outside, but you can use Macromedia Director and publish stuff like this on the internet. Imagine bringing all of that good stuff from Encarta 95 onto the internet in a web browser using Macromedia. You can make games. Now, full disclosure, you do need an internet speed of 28.8 kilobytes to make use of this. Please upgrade your modems now because this is the future because Intel is paving the way for this innovation and is using Shockwave to celebrate the 25th anniversary of the microprocessor. What&#39;s really great about this website is they have an HTML version and a Shockwave version. So just in case your internet isn&#39;t fast enough, Intel have thought about you. Really innovative, really exciting. Now you don&#39;t need a CD ROM to use Shockwave. You can download it from the internet. Now on a 56 KB mode it&#39;s 6 minutes. I think on a 28 it might be about 2 hours but it&#39;s worth it. Honestly it&#39;s absolutely worth it. </p><p class="post__p">We&#39;ve also got FutureWave FutureSplash Animator. Now originally Future Wave started off in the pen computing industry. They made SmartSketch for PenOS. Now we know as a collective as a society no one wants to touch screens. No one wants to interact with little things in their hands. We want to be sat at desks with our CRT monitors, our mice, collection of balls on the side, our beautiful beige keyboards. We don&#39;t want that. And FutureWave knew this. So they ported SmartSketch to Windows and Macintosh and they re-released it as FutureSplash Animator. It&#39;s quite similar to Macromedia. And this is what it looks like. Look at that. This is not a GIF. This is FutureWave FutureSplash in the browser. I think this is a GIF though. But look at this. You can click onto these things and it will open up a big portal into their demo site. </p><p class="post__p">Now, there have been some rumors that because of the competition, Macromedia are probably going to try to acquire FutureWave. But I think those rumors are just a flash in the pan [LAUGHS]. So we can ignore them.</p><p class="post__p">So what&#39;s this showing us is that the enduser experience is evolving. We got the I M G tag, we&#39;ve got stylesheets, JavaScript for interactivity and multimedia experiences. But what about us? What about us? Those people who are crafting these homepages, these experiences on the world wide web. Give me a cheer if you&#39;re still using MS DOS editor. Oh, that&#39;s a sad cheer, wasn&#39;t it? What about Notepad on Windows 95? [WHOOPING] Yes. And command line editors. Who&#39;s still there? Yeah. Yeah. That&#39;s that was really telling that all the backend devs are just &quot;Yeah. Yeah. I&#39;m on the command line. You can&#39;t take me into a notepad!&quot; But why why are you still there? Why are you still writing text? Text is slow. Text is rubbish. And Brett Victor in 1973 stood on stage and said that he believes in 40 years we will not be writing code in text files and that is in 2013. So we can start that future now. We have no choice because the future is GUIs: graphical user interfaces. You can do anything with GUIs. You don&#39;t need to use text editors. Text input is holding you back.</p><p class="post__p">Now I&#39;m sure you&#39;re thinking, well what the hell do I use, Salma? What am I doing? I don&#39;t know anything but the command line. Well, here are your options. There is good news because GUIs are already here. We&#39;ve got GUIs for native apps. We&#39;ve got Visual Basic. You can build Windows apps super fast. You can do anything in Visual Basic. You can even build a GUI in Visual Basic to track the hacker&#39;s IP address. Okay, that was a reference that none of you really understood. Google it. Um, Google does exist now, by the way. You can interact with databases and software components. Visual basic is everything. You can also build in Visual Basic a game engine. This is Unreal Engine released this year. You can build great games. It&#39;s built in Visual Basic. Visual programming is the future, but it&#39;s here now. We are living in the future. And the worldwide web also has GUIs!</p><p class="post__p">All right. The future is here. It is now time to choose your GUI of choice and specialize so you do not get left behind. And I&#39;m going to show you your options right now. We have something called Beverly Hills Internet. It was founded last year by my mates David and John. It&#39;s free. It&#39;s in the browser. You can make your homepages using a GUI in the browser. And what&#39;s great is that you get 2 megabytes of storage. Imagine being able to fill up 2 megabytes of storage. You&#39;ll be going into the next millennium. And the great thing about Beverly Hills Internet is that it&#39;s a web directory. It really helps us build community. You put your homepage in a category and you can find other like-minded people who are in the same category and browse through like a kind of a ring browse kind of style. But actually just the other day, Beverly Hills Internet rebranded to Geocities. It&#39;s very exciting. Same thing, a bit of a brand new look. I think they&#39;re also previewing cascading HTML stylesheets on this somehow. But it&#39;s incredible and it&#39;s a good one. </p><p class="post__p">We&#39;ve also go FrontPage 1.0 developed by Vermeer Technologies. It&#39;s a full GUI to build HTML and put your websites online. You can upload them via FTP directly in the interface. And what you see here is what you get on the internet. Now again, in this technology world, everyone wants to acquire everyone else. There is talk that Microsoft&#39;s going to get their hands on FrontPage, maybe even next year. I&#39;m not convinced. We&#39;ll have to see how that goes because I&#39;m sure even if Microsoft do acquire it, they will make sure that all of the markup created in FrontPage will work as equally well in every browser and not just internet explorer.</p><p class="post__p">We&#39;ve also got iBand Backstage Designer, another full GUI to build HTML. Again, people are talking about an acquisition, talking about Macromedia, taking it over. But again, whoever&#39;s starting these rumors are just trying to weave their own dreams into the conversation.</p><p class="post__p">Recap: images, stylesheets, JavaScript, GUIs, and now we have come to the innovation point in this talk. We have now come to my prediction of how we&#39;re going to be building websites. taking all of this into account in 30 years in 2025. Now, unfortunately, I&#39;m not a designer. So, I had to mock up my vision of the future in Microsoft Paint. Here we go. And so, we&#39;ve got our HTML on the side here, images. We&#39;ve got our cascading HTML stylesheets and their influences here. And we&#39;ve got what you see is what you get right in the middle of here. And we&#39;ve also got JavaScript. So when the mouse moves, you show a little greeting [LAUGHS].  It&#39;s very exciting. Constant popups, constant greetings. So joyful. Please do this. I want to visit your site and be greeted incessantly. It looks like this. Look at that. Greetings. And when you click it, it pops up again. I made it. It&#39;s great. And we&#39;ve also got multimedia experiences inside this GUI, this fictional GUI called nordcraft.com. We&#39;ve got this animation thing here. We&#39;re rotating me. It&#39;s very exciting. Everything is brought together in this visual interface. So that that&#39;s my prediction. It&#39;s a little bit crude, but I think you&#39;re all very senior programmers in here. So, you can take this idea and you can run with it. And in 30 years, you can build your own visual framework that takes all of these ideas into account. </p><p class="post__p">And so as well in 30 years all of you will continue to have your own personal homepage whether it&#39;s using Beverly Hills internet, Geocities, one of these GUIS, you&#39;ll be you&#39;ll be using the GUIs, you&#39;ll be using image tags and image maps across your websites you will of course master the very intuitive influence of cascading HTML stylesheets, and you&#39;re probably going to use JavaScript for everything because let&#39;s face it if you can use JavaScript to dynamically pop up those greetings every time, you can probably use JavaScript to create HTML and cascading HTML stylesheets. So everything can be JavaScript because there&#39;s no typing. So you can just do whatever you want. And of course, you&#39;re going to be using GUIs. And what&#39;s more, all browsers are going to be on board, right? Every new feature for JavaScript release, especially regarding dates, will be supported by every single browser, every single cascading HTML stylesheet feature will be supported. And that is my vision of the future everybody. </p><p class="post__p">That was the introduction to the world wide web. You can find me on the world wide web everywhere. My homepage is built with a GUI. And you can also visit my live mockup wink wink of nordcraft.com. Thank you very much for listening everyone. I am Salma and I can&#39;t wait to talk to you about 1995 at the rest of the conference. Thank you so much.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Entertainment as Code: Finale</title>
          <description>Imagine if we could blur the lines between a streamer being live and offline on Twitch.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/talks/entertainment-as-code-finale/</link>
          <guid>https://whitep4nth3r.com/talks/entertainment-as-code-finale/</guid>
          <pubDate>Fri, 11 Oct 2024 00:00:00 GMT</pubDate>
          <category>Streaming</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">[Jason]: Next speaker is Salma Alam-Naylor. Salma is a senior developer relations Advocate at Sentry uh she also writes code for your entertainment which is why we&#39;re all here, she is active in the developer Community, she is a Microsoft MVP for developer Technologies, winner of the Jamstack Community Creator award in 2021, and partnered twitch streamer where she builds weird websites and silly projects roasts your code and chats about the tech industry every week for your entertainment. Please welcome to the stage Salma. [Applause]</p><p class="post__p">[Salma] In the late 1900s I wrote my first lines of BASIC code on a Commodore 64. At the dawn of the 21st century I built my first geocities website and in 2005 I uploaded my first HTML file via FTP. In 2009 I used HTML and CSS on my MySpace page, and in 2014 I released a mobile first responsive website using an obscure PHP framework and bootstrap CSS. And I hated every single minute of it. I hate coding. But I found a way to hate coding a little less. By building stupid projects in front of a live audience. and this is entertainment as code. [Applause]</p><p class="post__p">When I graduated from music College in 2008, I was an entertainer. I wanted to be a film composer, I was in a band, I was making terrible websites for my friends, I was also a music teacher and a musical comedian. Here is the oldest clip you can find of me on the internet. </p><p class="post__p">[Video] My name&#39;s Salma, and I&#39;m one of lead tutors at the School of Rock and Pop. The School of Rock and Pop combines professional music tuition with the opportunity to perform. Each center holds gigs at recognized rock music venues. It&#39;s a chance to meet like-minded teenagers and really rock out in a way that can&#39;t be done at home. </p><p class="post__p">[Salma] I couldn&#39;t play the drums then. I can play the drums now. But in 2010 I set up the School of Rock and pop in Manchester in the UK, and I was teaching children how to be in rock bands. And it was about helping them put music into a real tangible context, rather than practicing alone on their instruments in their rooms. Fast forward to 2012, I got my teaching qualification and I worked as a music teacher in high schools for a few years. Fast forward again, it&#39;s a very long story, but in 2014 I quit teaching and I got my first job in tech and this led to many more jobs in Tech. </p><p class="post__p">And in 2020: everything changed. And in these unprecedented times, everyone stayed inside and found new ways to spend their time. Anyone remember family Zoom quizzes? Worst time of my life. Anyway, here&#39;s where Twitch comes onto the scene for me. Twitch is a live streaming platform originally centered on gaming, but now creators are streaming music, arts and crafts, 24/7 live feeds of farm animals and earthquakes, and much more. 17 billion hours were spent on Twitch in 2020, which was 83% higher than in 2019. I started watching Twitch in April 2020, and the most exciting category I discovered was software and game development. Nerds were writing code live on stream and I was sucked in. </p><p class="post__p">And I didn&#39;t just watch Twitch streams. I took it one step further. In June 2020 I went live on Twitch for the first time under the username white panther, which was a joke at the time but I&#39;m stuck with it now, but here is the first clip ever taken from one of my streams back then. </p><p class="post__p">[Video] So now standard and drop D is working but DADGAD is not. What the hell? [laughing]. </p><p class="post__p">[Salma] As usual my code wasn&#39;t working, but this will be relevant I promise you. In this stream I was mixing my music and tech experience through building an app called The Fretonator. It&#39;s an app to learn scales and modes on the guitar. You can still go to it today: fretonator.com. On The Fretonator, you choose a starting note, you pick a mode or a scale, and you use the guitar fretboard to learn it. You can also learn more about the theory behind it if you want but the most important thing, is that every scale comes with a backing track for you to practice with. So rather than just like trying to learn the scale on your own in your bedroom on a single guitar, without any musical context, The Fretonator is about putting theory into practice, much like the mission of the School of Rock and Pop. And this concept has inspired everything I do live on Twitch. </p><p class="post__p">Through live streaming coding, I was discovering that demonstrating and practically applying technical concepts in a real world context, with a live audience, was actually making tech more accessible, more human, more entertaining, exactly like when you play and perform music with others. I still hate coding though. </p><p class="post__p">[Video] I quit I- I- just- I quit I quit. I&#39;m going to do something else. I can&#39;t I can&#39;t make this. Every step of the way is just an absolute piece of [ __ ].</p><p class="post__p">[Salma] But I hate it a little less when things go right. </p><p class="post__p">[Video] If it does work I will feel sick that I have succeeded. Look! I did it I did it I did it. </p><p class="post__p">[Salma] Genius. I learned a lot in my first six weeks of streaming, and the most important thing I learned is that Twitch streams are live shows. People go to Twitch and watch streams to be entertained, and I&#39;m an Entertainer I always have been. So I wanted to make my live coding streams about entertainment. Not to build the next disruptive SaaS or JavaScript framework, or take myself too seriously. I wanted to build stuff that made people laugh, that brought people together and got them excited about the infinite possibilities of just a few lines of silly code. </p><p class="post__p">So at the center of all of this is my Twitch bot, which we lovingly call the mainframe, and I&#39;ve been building this on stream with other things for over four years. And the Mainframe powers most of the interactive features on my stream. And what I discovered through building this is that building stuff for your stream, live on stream, with viewers, for viewers is the best way to make people feel part of something, and enable them to create their own entertainment. </p><p class="post__p">I&#39;ve got an architecture diagram for you. I&#39;m going to show you everything I use to power my stream, it&#39;s extremely over engineered, that sounded like a pun didn&#39;t it? I&#39;ve never realized that before, after all of this time doing this talk, but I don&#39;t recommend what I&#39;m about to show you. Right, we start with the backend. Pantherbot is an Express app, it uses typescript, it stores some stuff in MongoDB, and sends lots and lots of events over a websocket connection. iIt uses the Twitch API and a third party wrapper around the Twitch API called TMI.JS. There are two React apps listening for websocket events, and there&#39;s one other front-end app built with Nuxt, which I&#39;ll reveal towards the end of this talk. There&#39;s a types package that both the backend and front-end apps use, and it also uses a Discord API to send some stuff to my Discord server. And all of this is brought together in OBS, which stands for Open Broadcaster Software, which I use to stream, and I also use one other app called Aitum, which allows me to control OBS directly via Twitch. Did I pass the whiteboarding interview? Let&#39;s check this out. </p><p class="post__p">So, follows, subscriptions, and cheering: these events are core parts of the Twitch experience. When a viewer presses a button on their device which makes something happen on your stream, it creates a sense of joy and wonder and of being part of something bigger than yourself. Or maybe it&#39;s just dopamine, I don&#39;t know, but when I get a new follower, I make a big deal about it. Here is the code. On each new follow, the Twitch event sub API sends the mainframe a notification, and when it receives that notification, we find the Twitch user who followed by ID and then we build up a follow event to send over the websocket connection. Most importantly, the event contains the follower name and their profile image which is how we&#39;ll make the viewer get flooded with dopamine by making them feel part of the live show. </p><p class="post__p">This is the frontend React component that displays all alerts. There&#39;s a queuing system, which you definitely need if your stream is busy, and each type of alert shows a different image and text depending on what&#39;s sent by the backend. Sound is also important for a live show. Each alert plays a different custom guitar riff. Here is a follow Alert in action. </p><p class="post__p">[Video] Amonteirom! Thank you for the follow, welcome. </p><p class="post__p">[Salma] So we got the follower alert, and uh there were two other things that happened. First, the panther logo is changed at random, and plays another custom sound And we have panthers of various sizes raining down. You see, even though white panther was a joke I went with the whole panther thing. It kind of worked. As you watch a stream on Twitch, you accumulate channel currency and you can choose to spend it on fun stuff set up by the streamer. So I made it so viewers can also trigger that logo change manually via channel point redemptions, and chat can also make the emotes rain down by typing different weather words in chat. </p><p class="post__p">This has of course suffered from many bugs throughout the years, such as this one that happened when someone gave me 50 gifts subscriptions at once. We were testing in production, which you often have to do on Twitch, and it didn&#39;t scale! aAd yes we heard that guitar riff 50 times. But that&#39;s entertainment, right? Writing the code to make that happen live on Twitch was entertainment. Viewers being able to cause unexpected chaos on my stream is entertainment.</p><p class="post__p">Another source of entertainment is showing the chat feed on the stream so people can see their words on the screen and again, feel part of the live show. Chat is added to the stream via another browser URL from the React app in OBS, and each chat message is represented by this very large chat message data object. And this incorporates a lot of features that viewers specifically asked for, because viewers love it when you build their ideas — more dopamine — and they are great QA for all of these features ,such as being able to use an HTML Marquee tag, and numeronymn mode. </p><p class="post__p">Now if you don&#39;t know what a numeronym is, it&#39;s a number based word, commonly used in Tech to shorten long words such as accessibility, internationalization. Here is the code for numeronymn mode. Very silly code, it uh splits the full message by space and for each word it returns the first character, the count of the characters in the middle, and the last character. It&#39;s pointless really, but for some reason I published it to npm! And it has two weekly downloads! It&#39;s going very well! But when streaming is considered, things always don&#39;t go this well. </p><p class="post__p">So I&#39;m talking about back seaters. Back seaters are everywhere in software and game dev streams. What is back seating? It&#39;s when new viewers come into the stream and they just tell me what to do, with no context about the problem that we&#39;re solving. They paste code snippets in chat, ask me why I&#39;m not using Tailwind in my backend code or try to start a fight about something irrelevant like Next.js, and it&#39;s the worst: like backseat driving. And one of my viewers thought it would be a great idea to put back seaters in a literal car, and while I was streaming he sent me some car parts he created in Microsoft Paint. </p><p class="post__p">[Video] Layer in front of me... [laughing]... okay... [laughing] </p><p class="post__p">[Salma] I got it set up eventually. </p><p class="post__p">[Video] One second, um, two Taco Bells please, uh with sparkling water thank you. I&#39;ve never had Taco Bell in my life, but the car has evolved of course and can be triggered by viewers if they want to put someone in the back seat. Here&#39;s how it works. The back end listens for a command in chat: !bs, and what bs stands for is open to interpretation, and we grab the username from the message, and if the targeted back seater is present in chat, we send an event over the websocket. Again, making sure to send the profile image URL. And today the car looks like this. I want people to send me loads of code in Twitch chat. So that person, it&#39;s a shame that this person didn&#39;t have like a proper face as their Avatar, but they just sent me like a 100 lines of code in chat and they got really aggressive when I showed them that it didn&#39;t work and they said good luck with your insecurities, and I&#39;m wondering whether it&#39;s um their insecurities instead. </p><p class="post__p">It&#39;s chaos isn&#39;t it? And chaos is entertainment, and I&#39;ve now got so many channel point redemptions, interactions and triggers that break each other through edge cases and conflicts and I haven&#39;t fix them on purpose, because as a viewer it&#39;s fun to break a stream. It&#39;s fun to cause chaos in a live show, and it&#39;s entertaining to watch me struggle and shout at things. </p><p class="post__p">[Video] My redemptions are too cheap so they go too far this is [laughter] fine wow it&#39;s broken, you&#39;re going to have to endure about 40 more alerts and- there&#39;s Zephyr, and Luca-person as well, thank you oh the car didn&#39;t work okay oh we got a raid! Welcome in glitchbyte! How are you- [interrupted] </p><p class="post__p">[Salma] That&#39;s a channel Point Redemption and it lasts for 30 seconds, and people get very confused when they see that. That&#39;s a very brief glimpse into my live show that&#39;s been evolving over the years, but now I want to show you something I&#39;ve been building this year which is even more chaotic and entertaining than all of this. </p><p class="post__p">Imagine... Leo imagine! [Leo remembers to dim the lights] if we could blur the lines between a streamer being live and offline on Twitch. And I started thinking, what if I could create a world where people could interact with my stream not just when I&#39;m live, but when I&#39;m offline as well. What if I could create a game that people could play on my stream, and what if that game game didn&#39;t stop when the stream ends? This is Pantherworld. [a single wooo in the background] Thank you, Hillary! [laughing] </p><p class="post__p">Pantherworld is a multiplayer text-based game that you play in my Twitch chat. And when events happen on the stream, a random world item will spawn in a random world zone and the objective in Panther world is to claim items and fill up your inventory and that objective has changed as the game has evolved. The game is played via chat commands. You can get a list of available zones, move to a Zone. Also as a new follower you&#39;re automatically dropped into a random zone. Check your location, claim a spawned or dropped item, drop an item. You can also cook a variety of recipes with varying results, gift an item to a specific or random player, list your inventory total, check the population of the world, and view information on how to play. As world leader I can release unclaimed items in a random zone where players have 60 seconds to claim them. </p><p class="post__p">Now, the only way I can truly demonstrate this wonderful world thing to you is uh let&#39;s play it together. So if you have a phone, uh this will take you to my Twitch chat, so if you&#39;re logged in via Twitch you can um play, or you can just watch what happens. And I need to do a little bit of finangling to get over to here, okay. Now uh I oh thank you for the follow chaachi, it&#39;s like I&#39;m streaming. Okay you can&#39;t see the thing I need to do hang on I need to do this need to go over there no no no there right okay okay okay. Right. I just spawned loads of things for you. Where&#39;s the sound? Where where hang on uh right so you can actually play the game now that should make some sound as well, this demo never goes right. Anyway, all these things have spawned. So if you um, you need to be in pantherworld to get them but I should have had um a lot of people ready in the chat to play the demo but look, Lazar claim something man! You&#39;ve got to be in the right zone. Anyway, oh look someone- oh that&#39;s another spawn. All right look b-rob there, locate, oh you&#39;re not in Pantherworld yet. You got to- I mean- and I know I went through the commands really really quickly, um but look yeah, Sarah&#39;s moved to the beach. There&#39;s someone in the city, oh look a wooden bowl spawned in the mountain! Sometimes moves spawn some things as well. I want someone to claim something, someone claim, and then we can stop this nonsense. Claim, no you have to claim the name of the item you see you can&#39;t you have to scroll up to see what spawned. It&#39;s a very difficult game obviously clearly, but hi hi everyone uh in the chat, anyway okay, that didn&#39;t work, &#39;cause my normal people who were there to show how it&#39;s played were not there. I think the 60 seconds is up. Right we we&#39;ll get back, we&#39;ll get back here. It&#39;s all good. Thank you for trying to play the game. </p><p class="post__p">Um so um, as you as you see, I did a manual little demo thing there. But whenever an event happens on the stream, um something that triggers an event, that triggers a spawn, right, and these events happen while I&#39;m live and also while I&#39;m offline. So remember the code from earlier that handled follow alerts? Now whenever someone follows, the spawn function is called, and it does eight things, and we&#39;re going to very swiftly step through each. The first thing we do is calculate the event rarity. Items have rarities. The higher the rarity, the less likely it will be to spawn, and the more valuable it is in Pantherworld. I did not mean to create a capitalist society but I will tell you more on how I&#39;m working on fixing that later. </p><p class="post__p">Each event type has a particular piece of input data associated with it. For a cheer it&#39;s the number of bits that were sent, for a sub and a follower it&#39;s the username, and for the raid it&#39;s the number of raiders and so on. The calculate event rarity function takes that input data and returns it as a number. This will be the first seed for the calculate eligible items function. Next we&#39;ll get a random zone from the currently available zones. Next we&#39;ll calculate the eligible items for spawning based on the rarity seed number and the zone. First we attempt to select the items based on the zone because there are certain items that only currently spawn in the swamp or the beach for example. In the swamp you&#39;ve got toxic waste, rotten egg, irradiated croissant, and so on. And then we attempt to filter the items based on the exact rarity of the seed provided. For examplem if someone cheers 10 bits so the rarity seed number is 10 and the spawn zone is the swamp, we&#39;ll return the irradiated avocado and the irradiated croissant. However, if there not if there are no exact matches for the rarity seed number we&#39;ll attempt to select higher or lower rarity items. There&#39;s currently a 20% chance to spawn higher rarity items, 80% chance to spawn lower. And then we filter the items depending on whether we&#39;re going higher or lower and return them. So now we have an array of eligible items to spawn, we can select a random one, save it in the database, and then construct a message to notify players, which is sent to the Twitch chat, and a special channel in the Discord. And finally, the event is sent over the websocket connection to appear on that overlay you saw before. There it is. </p><p class="post__p">So the initial plan for claiming items was that any player who was the first to claim an item kept it in their inventory. The only caveat was that there was a time limit of 60 seconds to claim that item, otherwise it remained unclaimed. However one day it became apparent that this was not the case, and a player reported an item that they thought they had claimed was no longer in their inventory. Chat had found another bug. Players were able to steal items from each other within the time limit of 60 seconds. But all was not lost, and in fact there was more to be gained, because this marked the moment of the creation of the first rule of Pantherworld, where bugs are actually features. So let&#39;s look at the claim function and I&#39;ll show you where the bug came from. </p><p class="post__p">To claim a player types !claim with the name of the item they want to grab. We first check to see if the world leader (me) has released a set of items. If there is a release in progress, we find all items that have a user ID of null, meaning they are previously unclaimed. If I did not release items, this means the player is trying to claim a freshly spawned or dropped item. This database query looks for items that have a created date of 60 seconds ago or higher or have been dropped. Who can see the bug? Who can see why players could steal items from each other here? [user in audience suggests that I didn&#39;t check for user ID equals null] Yes, correct, very good. Give this man a round of applause. I was not checking for a null user ID, which meant that anyone could get anything if it was created 60 seconds ago or earlier or later. If we find no items we do nothing. If we do find items, we try to find a player. If there&#39;s no player, they are not in Pantherworld, so they get some feedback on how to play. You saw that in the demo. If we do find a player, we need to make sure the player is in the same zone as the item they are trying to claim. If the player is not in the correct zone, they are told to move to try and claim it. We don&#39;t tell them the correct zone &#39;cause that would be too easy. If the player is in the correct zone, the item becomes theirs, until someone steals it within the time limit. And then we send a message to the Twitch chat, the Discord, and over the web socket. </p><p class="post__p">So who can remember the first rule of Pantherworld? Bugs are features, very good, like um, this little bug here. Look! This is a good one. All items were locked after a release, but TNT was still able to claim. There was clearly some kind of race condition that I didn&#39;t care to look into, uh, I&#39;m not going to fix it. It&#39;s a good bug. You never know when you might be able to claim an item! Um, I&#39;m going to show you another bug. This, and this one became a very special feature. </p><p class="post__p">[Video] There are 12 front-facing baby chicks in the database, they have spawned. There are no front facing baby chicks in the item list. Obviously front facing baby chick, and there&#39;s none of these in the item list. Now my hypothesis was that chicken egg was somehow being changed to the front-facing baby chick, but there are chicken eggs in the database. And at what point has a string been converted to an emoji? </p><p class="post__p">[Salma] This was a very good bug that was caused by another one terrible line of code, and I&#39;ll reveal it in a moment but let&#39;s look at how dropping items works. The throw command comes in with the name of the item to drop. First we attempt to find the player and their zone and if we find a player, we find an item with that user ID and item name, and update the user ID to null so it doesn&#39;t belong to anyone. We change the zone of the item to the player&#39;s current zone where it was dropped, we set dropped to true so it can be claimable, and then we send all the messages. </p><p class="post__p">So as I was writing the throw feature, chat experimented with dropping random things to stress test the system. There were clearly 12 front-facing baby chicks dropped during the stress test, and there was one problem with the original database query that updated the dropped item. I&#39;d left upsert &#39;cause I copied and pasted which meant at that point players could drop anything and it would create it in Pantherworld. Players unknowingly had the ability to control spawns, and yes I had to clean up the database quite substantially after that, but for the purposes of the game lore, we kept the original trick chicks, and because no front-facing baby chick will ever spawn again, they currently have the highest spawn rarity value of the game which is 10,000. </p><p class="post__p">Now, finally the original objective of the game. I have a minute left, and I did not expect that. Filling up your inventory, remember this pink mystery app? That&#39;s Pantherworld. It&#39;s a standalone front-end companion app where you can view your inventory, and there&#39;s more to come but, um in Pantherworld you log in via Twitch and you can view your inventory items and their rarities, and in which zone you&#39;re located. There&#39;s me in the beach, there&#39;s Matty in the city there&#39;s JWalter in the forest, and there&#39;s Yoshi on the beach. Yoshi has even made a community Wiki to keep track of item spawns and rarities how the Pantherworld map works, and some world lore. And another viewer, Boomslang, also built a separate companion app, where you can choose to complete daily quests by gifting the panther quest bot particular combinations of items. And it warms my heart that people from the community are enjoying this game so much that they&#39;re building things to extend the game and enhance their playing experience. And it soon became obvious who the really hardcore players were, and these players were finding ways to like, game the game. </p><p class="post__p">So Yoshi wrote how, um wrote about how she made a Chrome extension which launches the Twitch chat and the Discord feed in in different Windows to keep track of spawns. She even figured out that there&#39;s a lower latency on spawn event messages sent to the Discord so she keeps both the Discord open and chat open and she gets notified in Discord and then claims items via Twitch before the messages even appear in the Twitch chat window. So did Yoshi go too far or did I go too far? And I want to hear from another hardcore player Matty. </p><p class="post__p">[Matty]: For some time now there&#39;s been a few people collecting a lot of items, like me, and these players have accumulated such a large amount of items and wealth, that it&#39;s become unfair. So a wealth adjustment initiative was introduced. When you go to claim an item, if you are holding a certain value of items or higher, then you are at risk of losing items while you claim. Think of it like this: you&#39;re holding a giant bag of loot and you go to pick up loot on the ground, and you drop 10 of them on the floor. And they&#39;re instantly claimable by other people, so other
people try and swoop in and claim these items. </p><p class="post__p">[Salma]: On 12th of July 2024 the Wealth Distribution Act was passed in Pantherworld. Now when claiming there is a probability you may drop between one item and 10% of your inventory total, and the probability of dropping is calculated using a wealth index and the following nonlinear function, [laughter] where your wealth index is a sum of all inventory items multiplied by their rarity. Basically, once your wealth index reaches 500,000 there is a 100% probability that you will drop at least one item each time you claim a new one. This was the first time it happened: Yoshi lost 10 items including a super rare ruby when she claimed fresh basil. [laughter] </p><p class="post__p">Now, Yoshi currently has a wealth index of over 2 million [laughter] and that&#39;s the result of a bug in the cook function
where the rarity of a live lobster was used to calculate the final rarity of a cook even though it wasn&#39;t part of the recipe. That&#39;s my fault. But Yoshi could drop that item but she&#39;s chosen to keep it, and so now she&#39;s just going to drop every time she claims. But when Matty had accumulated so much wealth that his risk of getting taxed was near 100%, he brought a new tactic into the game.</p><p class="post__p">[Matty]: And I thought to myself maybe I could reduce this risk. Maybe I could pull a Bezos and put all of my items offshore into a second Twitch account so that I&#39;m not holding those items when I try to claim something. So if I do get taxed, because I&#39;ll be over that threshold, the only items I&#39;ll lose are low value items. So I took all of my high value items and I shoved them into my second Twitch account Matty_nshoes and, the way that I did that is very simple: there is a gifting mechanism. I didn&#39;t even need to drop the items first and then pick them up very quickly with my other account. You don&#39;t need to be a data analyst to see that this is a massive anomaly and that of course uh someone with 35 items having 53,000 wealth is probably an offshore bank account. Whitepanther has actually created the perfect mechanism for laundering items offshore, to not be taxed. You have literally recreated capitalism. So whose fault is it really that I am using the system, doing the thing that is most natural, which is avoiding getting taxed? </p><p class="post__p">[Salma]: So here&#39;s some historic proof on the leaderboard of Matty&#39;s continued offshore antics, although he suggested some fixes to this capitalism, taxing the rich at unspecified intervals. I have so far implemented an intermediary solution to this by attempting wealth distribution on the gift recipients so that when Matty_nshoes
receives those gifted items, there is a probability that that that offshore account will get wealth taxed. So what&#39;s really exciting about the whole process is the game is evolving in response to how people choose to play it.I&#39;m sorry I&#39;m going over but there&#39;s just some very quick final words from Matty. </p><p class="post__p">[Matty]: The game is incredible, the game is so much fun to play. It&#39;s extremely addictive. It&#39;s so addictive that I&#39;m a little bit concerned, because I have it open while I&#39;m streaming. But I do absolutely want the irradiated stick of butter. I&#39;m hoping I don&#39;t get wealth taxed.</p><p class="post__p">[Salma]: So Pantherworld is always revolving as a result of how the game is being played, whether I like it or not, and one day what I really want to make is some kind of map where you can visualize your location, and the location of your friends, and watch items spawn in real time. And I&#39;ve kind of crossed over to game dev and I have no idea what I&#39;m doing, but you can come and join me on Twitch to watch me struggle while I build this. It will be my pleasure to provide you with entertainment as code, you can uh find me on the internet everywhere as whitep4nth3r, and hopefully I&#39;ll find you in Pantherworld [Applause]. Thank you. </p><p class="post__p">[Outro sequence]: It&#39;s chaos up in here here. [Music] Pro streamer. </p><p class="post__p">[Jason]: I have to wait, okay now it caught up. All right. That was amazing. Um, so, we get on it we need more questions, but I&#39;ve got one um. How did you manage to work your way to doing something so much fun every
day? </p><p class="post__p">[Salma] Um, through necessity and survival, because the tech industry is often very repetitive and boring so you have to do fun things in order to not go a little bit insane. </p><p class="post__p">[Jason]: That&#39;s fair, that&#39;s fair. Having been in the tech industry for a long time, I&#39;ve gone through some very long periods of boredom as well um, so what&#39;s what&#39;s the next game? </p><p class="post__p">[Salma]: This isn&#39;t finished yet. </p><p class="post__p">[Jason]: Is anything ever? </p><p class="post__p">[Salma]: No, but like that there are so many features that the players want, like I&#39;m not even started, like there&#39;s all sorts, so you can cook stuff um from mainly edible things right now, but people want to be able to craft things &#39;cause there&#39;s some items like a wooden bowl, and um uh there&#39;s wood there&#39;s drift wood, there&#39;s there&#39;s lots of raw
materials in Pantherworld right, and so the the aim is that you collect all these items to then build things with them. Cooking was like the proof of concept for that and cooking generates some very large earities so the wealth tax is an issue there, um but people want to build houses in the in the zones, and you know some features like that are coming up um, for example, sand isn&#39;t available yet but when sand spawns you should need a bucket and things like that. I don&#39;t know what&#39;s happening, but I don&#39;t know whether to just like stop this or do it for the rest of my life. I don&#39;t think there&#39;s any in between. </p><p class="post__p">[Jason]: That seems like a false choice to be honest. Uh, speaking speaking of false choices um sorry James this just really kind of led right into this, how do you balance being productive on a stream while also entertaining? </p><p class="post__p">[Salma]: Any streamer knows that you can&#39;t really be productive on stream. You have to fake it, it&#39;s all about looking
like you&#39;re doing good stuff, um but you never really do. Um ,I mean obviously yeah, I&#39;ve built stuff on stream, it&#39;s just part of the process, like, part of the being distracted, part of the questions that you get, it&#39;s all part of the show. It&#39;s a live show, isn&#39;t it? Like, I don&#39;t go and stream and build Pantherworld to get stuff done. I go to put on a live show for people and and for them to give me ideas and for me to build their ideas, and then they get happy and all the dopamine and everyone is happy and then they hang out in the Discord, and they continue to play the game offline. Um, so there&#39;s no balance and that&#39;s the point. [Jason]: Excellent, so uh, I there&#39;s okay, maybe this is, um we should we should ask one of the one of the more practical questions. Are you using Sentry for error or feature tracking? [</p><p class="post__p">Salma]: Yes, of course. Sentry is, like, the best error monitoring platform there is and performance monitoring, and and there are so many cool things. And actually, I mean MongoDB support um is has just come out for Sentry and I&#39;m I&#39;m going to add it to Pantherworld live on stream &#39;cause I use Mongo, um so yeah if you&#39;re not using Sentry, why not? Shamess plug hashtag. </p><p class="post__p">[Jason]: That&#39;s great, um so I want to come back to the the creation and and where the ideas come from, um if you had to guess, where does it uh what&#39;s the balance between things that are inspiring you coming from the players versus things that you&#39;ve been thinking about and and want to try and explore?</p><p class="post__p">[Salma]: There&#39;s no balance. I will do things when I want to do them, and I will sometimes make it look like the things I want to do are the things that the chat want to do. Um, it&#39;s all my show right? So I like I- it doesn&#39;t it... maybe it doesn&#39;t look like I&#39;m in control, but I&#39;m always in control. Um, saying that out loud sounds like a lie, but um, I decide and, and if it&#39;s not fun then we just don&#39;t do it and we don&#39;t stop it. If it&#39;s if it&#39;s not fun then it&#39;s it&#39;s there&#39;s no it&#39;s it&#39;s meaningless right? It&#39;s meant to be fun and um, so there is, there is a skill in, in balancing what I want and what the chat wants, I can&#39;t lie, um but it&#39;s top secret. </p><p class="post__p">[Jason]: That is totally fair. Um, I have been up on stages enough to know that we are the entertainment and it does need to be sort of dictated by what people want to learn. Um, so uh, the wealth tax. There&#39;s a big question, now we
keep getting questions from Anonymous and I really I&#39;m doing my best to not imagine that we&#39;re being trolled this whole conference by a hacker group but it actually kind of fits in this case I think that would be kind of cool, um, all right, wealth tax uh be a white panther that roams the world and attacks them with storage for food, maybe is that a nice evolution of how the tax might work?</p><p class="post__p">[Salma]: That&#39;s evil, see I&#39;m trying to make a better world, right, in Pantherworld. I&#39;m not trying- I&#39;m trying to make it fair. Um, it was meant to be a socialist community where everyone had access to anything they needed, and um I guess human nature dictates otherwise, right? </p><p class="post__p">[Jason]: All right so, so here&#39;s another bit of the the human nature and I think um, I think for anybody that creates a lot, um some days you feel it some days you don&#39;t. Um, do you maintain, uh like any particular strict schedule, or or does it just when the spirit moves you, how does that how does that work for you? </p><p class="post__p">[Salma]: When um my life is not in a mess um, and I have things going on in my life, yes I do have a strict schedule. I think that&#39;s very important for when you&#39;re streaming and you&#39;re building a community. So my strict schedule over the past year has been Thursdays and Fridays for three or four hours in the afternoon in the uh UK time zone. I think a schedule is very important for um maintaining expectations for the people who want to to see you, and watch you and be entertained by you. Um, I find that when you do random streams here and there, like it&#39;s different if I do a Saturday stream in the evening, it&#39;s like the wild west. There&#39;s all these people who have no idea what&#39;s going on, but when um, my my core audience is like a bunch of people who work from home and um they put my stream on in the background, and when you have those knowns as well it makes it more enjoyable to go live when you know what you&#39;re going to expect as a streamer. </p><p class="post__p">[Jason]: All right so uh we&#39;re just about out of time um I&#39;ve been saving this one for last, your favorite twitch emote? </p><p class="post__p">[Salma]: No comment. I don&#39;t know I don&#39;t know, that&#39;s a strange question, that&#39;s like saying what&#39;s my favorite database query. </p><p class="post__p">[Jason]: Some people would have one. </p><p class="post__p">[Salma]: Yeah I know, I know and that&#39;s why I try to make my streams fun and entertaining so we don&#39;t get nonsense like that. [laughter] </p><p class="post__p">[Jason]: Uh well I did dodge the whole question about how data science would fit into what you&#39;re doing but - </p><p class="post__p">[Salma]: Oh no it does! Matty has done a whole big analysis on the items that have spawned and their rarities and it&#39;s fascinating and it made me make the game better by putting- because all the rarities are based on vibes, I don&#39;t, like, have a system, but he made me, like um put in a better system, so that uh- like a million olives wouldn&#39;t
just spawn [laughter], it was good good data. </p><p class="post__p">[Jason]: All right, well good we we have a little bit of science
in everything. Um, Salma thank you so much </p><p class="post__p">[Salma]: Thank you. </p><p class="post__p">[Jason]: That was thoroughly entertaining. </p><p class="post__p">[Salma]: Thank you very much. [Applause]</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Entertainment as Code: Premier</title>
          <description>Learn about streaming live coding, and how writing silly code in front of a live audience is a powerful (and hilarious) way to build community.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/talks/entertainment-as-code-premier/</link>
          <guid>https://whitep4nth3r.com/talks/entertainment-as-code-premier/</guid>
          <pubDate>Fri, 10 Nov 2023 00:00:00 GMT</pubDate>
          <category>Streaming</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">My name is Salma Alam-Naylor. I had to double-barrel my name when I got married otherwise I would have been a disease. And this is entertainment as code! [Heavy rock music]. That&#39;s where you clap! [Applause]. </p><p class="post__p">I write code for your entertainment. I&#39;m a live streamer on Twitch, software engineer, and developer educator. I got Covid for the first time a few weeks ago, when I started writing this talk, and I change my hair all the time. This hair cut is still six years (I meant days) old. Still not sure about it! </p><p class="post__p">Let&#39;s travel back to 2008, when I graduated from the Royal Northern College of Music in Manchester with a degree in composition. I wanted to be a film composer for your entertainment, and, at the time, I was also in a folk band. We released an album, played weddings and festivals for your entertainment, and I was also making terrible websites, graphics, and album art for my musician friends. But as a musician, in order to pay the bills, you usually end up being a teacher. </p><blockquote class="post__blockquote"><p class="post__p">My name&#39;s Salma, one of the lead tutors here at the School of Rock and Pop. We combine the possibility to perform, each centre holds two gigs at recognised rock music venues, a chance to meet like-minded teenagers and rock out in a way that can&#39;t be done at home. </p></blockquote><p class="post__p">I actually started learning the drums properly like eight weeks ago. So it is a lot of fun. Didn&#39;t know how to play the drums then. </p><p class="post__p">Anyway, in 2010, I set up the School of Rock and Pop in Manchester, teaching children how to be in rock bands for your entertainment. It was about helping them put music into a real tangible context rather than practising alone in their rooms. And I really liked this whole teaching thing, so I got a teaching qualification, and I worked as a music teacher in secondary schools and sixth forms for a few years. Around this time, I was also a musical comedian, for your entertainment. We played some good gigs, won some competitions, and we went viral on the internet a few times. Unfortunately, you can&#39;t watch our music videos now because, unlike HTML, JavaScript, and CSS, some comity is not evergreen. </p><p class="post__p">What is funny, though, is that whenever I&#39;m on stage with a mic, I instinctively try to tell jokes. I need to, like, stop myself. I&#39;m not going to tell any jokes today, but I do just want to take a brief intermission and talk about a new business venture that I&#39;m working on now I&#39;m on the stage, now, because I&#39;ve got your attention. I love tech. As I&#39;m sure all of you do here. And breakfast is the most important meal of the day. You all love breakfast more than tech. So I want to merge the two. And open a tech-themed breakfast and brunch bar. Tech-themed breakfast and brunch bar called TechFast, and there will be things on the menu like Full Stack of Pancakes. Eggs and JSON. POP3 Tarts. ChatGPTea. Serial Numbers. And it&#39;ll all be served in little containers. Microservings. And the service will be Blazing Fast. And if there are any investors in the audience today, I&#39;m trying to raise the chia seed round right now. Please talk to me later at the social. No more jokes! It&#39;s time to be serious, Salma, which is often the feedback I get on my PRs. </p><p class="post__p">Let&#39;s fast-forward to 2014 when I quit teaching and I got my first job in tech. I don&#39;t have the time to tell the full story today. It&#39;s on my YouTube channel if you&#39;re interested, but this led to many more jobs in tech. As a front-end developer and a tech lead at product agencies, start-ups, global eCommerce brands, you know, boring, boring same old, but in 2020, as a lot of other people have alluded today, everything changed. The pandemic descended, everyone stayed inside, and we all found new ways to spend our time. And here&#39;s where Twitch comes into my life. </p><p class="post__p">The main focus and the largest categories on life streaming platform Twitch have always been centred around gaming, but as time went on, creators started streaming music, arts and crafts, talk shows, charity events, 24/7 live feeds of farm animals — anything you could probably ever dream of. That was not a joke. In 2020, for a short time, I was also glued to a behind-the-counter live stream of the comings and goings of a fish-and-chip shop. I think it might have been in Brighton. I&#39;m not sure. I&#39;m probably making that up. </p><p class="post__p">Twitch ended 2020 with its biggest numbers ever, 17 billion hours were spent on Twitch in 2020 which was 83% higher than in 2019. I was part of that. I started watching Twitch in the first lockdown of 2020, and the category that caught my attention was software and game development. People were writing code, live on stream, and people were watching. It sounds like madness! In the last year, over 14 million hours of software and game development streams have been watched. The category has around 1.6k average viewers at any one time, and there are always around 100 live software and game dev channels at any one time. It is a small category compared to gaming but with that comes a big opportunity. On Thursday 25th June 2020, I went live on Twitch for the first time. Under the user name whitep4nth3r, I don&#39;t have time to tell the story about this user name, ask me later if you&#39;re interested. It didn&#39;t make sense, but it was quite unintentional that panthers would become integral to everything, and you will see later. Here is the earliest stream clip I have. It is not the first, but it&#39;s very early. </p><blockquote class="post__blockquote"><p class="post__p">So standard and DROP D is working, but DADGAD is not.</p></blockquote><p class="post__p">Typical stream, typical development, everything goes wrong all the time. In this clip, I was building my first open source app called the Fretonator. It is true. That&#39;s not a joke! It exists now. You can go to see it. The Fretonator. And I was having problems. People in the chat were helping me. </p><p class="post__p">The Fretonator is a guitar app to help you learn scales and modes, and you can use it in the browser. So why did I do this first of all? Other than boredom and Boris not letting me out? The channels I saw were mainly back-end development, you know, C Sharp, Rust — boring stuff! They were being streamed by mainly men. I thought maybe I could fill a gap in the market. I&#39;m not a man, and I&#39;m a front-end developer, that&#39;s my specialism. Also, I started building the Fretonator a few months before because I wanted to learn Angular, and I learn best by building stuff, and putting things into real context. I was also building this app for my husband. He wanted to learn scale and mode theory on the guitar, but my traditional music-teaching methods why failing him and he desired a way to learn interactively on his own. I show what I&#39;m building. It does exist. You choose a starting note, you pick a mode that you want to learn, and you use the guitar fretboard to play scale. It uses the web audio API as well. You can dive deeper into the theory of modes and scales if you want, but most importantly, every scale for every note comes with a backing track for you to practise with. 160 jam tracks for every note and scale combination were painstakingly manually source (not using ChatGPT) for your entertainment. </p><p class="post__p">So the Fretonator is about rather than just playing the scale on your own without any more musical context, it&#39;s about putting the theory into practice in tangible and meaningful ways much like the mission of the School of Rock and Pop. This concept of putting theory into practice, of making tangible things has underpinned everything I do in tech and on my stream, for your entertainment. </p><p class="post__p">My original cheesy tag line, build stuff, learn things, love what you do, put the focus on building. Which is representative of how I learn. I actually retired this tag line this year because I had the realisation that it was too similar to &quot;Live, laugh, love&quot;. [Laughter]. </p><p class="post__p">You can still buy this, though, if you want to enjoy living, laughing, loving. But through streaming on Twitch during the pandemic, I was starting to discover that demonstrating and practically applying technical concepts in front of a live audience was making tech more accessible. It was making it more human. Pretty much exactly like when you perform music with others. It was also at times like pair programming with 100 people all at once. You know how like camaraderie developments through top experiences, and especially through the inevitable pain and suffering of software development, like this ... </p><blockquote class="post__blockquote"><p class="post__p">I quit. I quit! I&#39;m going to do something else. I can&#39;t make this. Every step of the way is just an absolute piece of shit. [Laughter]. </p></blockquote><p class="post__p">So the documentation was telling me that I needed to provision an SSL certificate in order to make an API call on my local machine. And I was just done. But when you have problem-solved as a team with you and the viewers, and your code finally works, you feel good, right? Those brain chemicals, they bring you closer to, they form those human relationships even more. </p><blockquote class="post__blockquote"><p class="post__p">If it does work, I will feel sick that I have succeeded. Argh! Look! [Laughs]. I did it! I did it! I did it! </p></blockquote><p class="post__p">So as I was working out what this whole streaming thing was about, people were coming, they were watching me, they were helping me, and returning back to my stream days later to see progress. They were pair-programming with me, and wanted to see me succeed. Without even setting out to do this, there was a community forming around me and my stream. And in just seven days from that first day that I went live, I had streamed enough and had enough average concurrent viewers to gain affiliate status on Twitch which meant I could make use of community engagement tools such as, VIP status for viewers, channel-point redemptions, and people could throw Twitch currency at me in the form of subs and cheers. So I wrote about what I learned in those early days on my blog, and the most important thing I took away from this is that the most valuable element to your stream, whatever you stream, is you. The code you are writing is only a small part of the experience. Viewers don&#39;t actually watch you to learn how to code. That is just a side effect. They come to hang out with you to feel safe in the presence of people with similar interests, and to be entertained. </p><p class="post__p">Now, I haven&#39;t rehearsed this slide because I added it after a conversation with Maggie at lunch. But, in the before times, right, before there was software and game development, the one category that programmers streamed in was Science and Technology. And science and Technology became overrun with 24/7 live feeds of farm animals and earthquake detectors, and just nonsense, and so we raised an issue on Twitch User Voice for a particular category for us — Software and Game Development. It was going well, and we were really looking forward to the new category launch but Twitch decided to release a new category before our category which was &quot;Hot Tubs, Pools and Beaches&quot;. It was basically a soft porn category designed to take away all the people who want to sit in a hot tub from Just Chatting and to put them in their own category, presumably to hide them, but they&#39;re just more easily discoverable now, but the core theme of Hot Tubs, Pools and Beaches is that people follow or sub, or donate in order to get anywhere name written on the streamer&#39;s body, so they can feel like they&#39;re part of something. They are closer to the streamer, and they are in the stream. </p><p class="post__p">Now, Hot Tubs, Pools and Beaches became a meme in the programming community, people put hot tubs in their overlays, writing names of their followers on their faces. The conclusion of all of this deep down at the root of everything, everyone just wants to be part of a stream on Twitch. Whether it is getting your name written on someone&#39;s leg, or having your avatar and user name rain down on the stream in an exciting way, everyone wants to be part of it. And this is going to become a running theme later on. </p><p class="post__p">So I didn&#39;t start streaming from a hot tub, but the community was forming around me, and to support that, I created The Claw Discord on 30th July 2020, apparently a claw is a group of Panthers, but I&#39;ve only seen it referenced once, so I don&#39;t know whether I got that wrong. But people started gathering here when I was offline, relationships started to form, and now we have a co-working channel where remote workers from around the world log into the channel, they have their cameras on, and they just keep each other company while they&#39;re working from their bedrooms all day, and it is pretty nice to see. </p><p class="post__p">So Twitch viewers are not passive like YouTube viewers, they&#39;re active participants in the experience. And so I started thinking about how I could build on this, and how I could bring people further into this the stream make them feel even more involved with what they were experiencing, without having to write their name on my face. </p><p class="post__p">So I built a Twitch bot, and this is an application built using the Twitch API and other third-party tools, to allow viewers to make things happen on the screen via chat commands and channel-point redemptions to level up their participation in the stream. </p><p class="post__p">There are pre-existing tools that exist to bring interactivity to your streams if you can&#39;t build your own, such as NightBot which I use as well. Making a Twitch bot is not a new and innovative thing, but here I had the opportunity to build a Twitch bot that reflected my personality and the entertainment value of the stream I wanted to build on. Plus, building stuff for your stream live on stream with viewers for viewers is the best way to test functionality, get the QA going, crowd-source ideas, and again make people feel part of that process, part of the product, and part of the stream. </p><p class="post__p">Here is some probably very over engineered architecture for your entertainment. This is everything I used to currently power my stream interactions, and everything that happens. We start with p4nth3rb0t, storing stuff in MongoDB, it sends events over a WebSocket connection, the back-end. It uses the Twitch API and a third-party wrapper around the Twitch API called tmi.js. We have two apps listening for WebSocket events. There is a types package that both the front-end and back-end applications use, and it also uses a Discord API and all of this is brought together in OBS, which stands for &quot;open broadcaster software&quot; which I use to stream, where each piece of functionality is added to my stream scenes as what we call &quot;browser source URLs&quot;. If you can get a browser URL in the browser, that does some stuff, you can easily put that into OBS so it can do the stuff there. I also use another tool called Aitum that allows me to control OBS itself Twitch stuff. That&#39;s it. I&#39;ve been building this over three years. I want to show you some of the key functionality of my Twitch bot. The whole thing did used to be open source but it got to bespoke, far too silly, now it is private, but I have core maintainers in my community who help out with bugs and features, and general fun stuff. </p><p class="post__p">So follows, subscriptions, and cheering are core part of the Twitch experience. And as a viewer myself, I like to sneak into other viewers&#39; streams and follow streamers to see how much I become part of the stream when I press the follow button. If they don&#39;t notice me, I will bounce right out, because making your viewers feel welcome and part of something is key to getting them to stick around, especially in this smaller more intimate category of software and game development. So here&#39;s how my alerts started out. </p><blockquote class="post__blockquote"><p class="post__p">PewWTD, thanks for the follow. Pew Pew. I need a Pew Pew Panther. </p></blockquote><p class="post__p">So that was using StreamLabs alerts, added as a browser source to OBS. I did make a Pew Pew Panther. It&#39;s actually my most used emote because my bot uses that the most. The bot uses the event sub API from Twitch which lets your application subscribe to events that happen on Twitch. When an event occurs for a subscription or follows, it sends my Twitch app a notification. When I receive a follow event, in order to protect against viewers spamming alerts on my screen, we first check for an existing follower in the database. If we don&#39;t find one, we find the Twitch user, I&#39;ve got a wrapper around the Twitch API here, and send a follow event over the WebSocket connection. Most importantly, the event contains the follower name and their profile image which is how we will put the viewer into the stream. So there are a number of front-end URLs which are listening for WebSocket events from the back-end. Here is a little React component that powers all of the alerts. There&#39;s a queuing system because you want to account for a lot of things happening at once especially with a substantial amount of viewers, and each type of alert shows a different image and subtitle depending on what is sent. Each alert plays a different alert sound which both me and the viewer here when I&#39;m streaming, all sounds are custom sounds written and recorded by me and my husband, and I do have a funny story about custom sounds. </p><p class="post__p">When the repo was open source, I stored the MP3 files for alerts in my Google Drive and used environment variables to access them when needed so people couldn&#39;t steam the files from the open source repo. At random times, the alert sounds would just stop working, and it took me and my viewers months to work out why. It turns out that too many alerts at any one time would DDoS the connection to Google Drive and deny the bot access. So my Twitch viewers DDoSed Google, so that was fun. </p><blockquote class="post__blockquote"><p class="post__p">Thank you for the follow!</p></blockquote><p class="post__p">This is when I accidentally coloured my hair to match my stream background. [Laughter]. Total accident. And I am yearning for that one now. Also, notice two other events that happened. One that changes the Panther logo overlaid on my camera at random, and one that rains down my Twitch emotes, all on separate browsers, listening to WebSocket events in the back-end. As you watch the stream, you accumulate channel currency, and you can choose to spend it on fun stuff set up by the streamer. I enable streamers to trigger the logo change manually by redemptions whenever they want, triggers a sound per Panther, and viewers can trigger the emote rain by rain, hail, snow, blizzard by writing in chat. It&#39;s gone through a lot of iterations throughout the years, including needing to limit the amount of emotes dropped at any one time because this was just ridiculous. </p><p class="post__p">RIP my frame rate. That is when someone decided to spend stupid money on 50 gift subscriptions to my channel all at once. It broke everything, kept on coming, and, yes, we heard that guitar riff 50 times. That is entertainment. Writing code to make that happen live on Twitch was entertainment. Viewers making happening anything on my stream from anywhere in the world is entertainment. What sometimes happens unexpectedly as a result of writing that code is also entertainment. </p><p class="post__p">So you probably also noticed in my screen clips that I show chat messages on stream. Chat is another custom browser overlay powered by the central Twitch bot which listens to message events from tmi.js and sends events over the WebSocket connection — extremely over-engineered. A lot of information is sent over the WebSocket to power chat messages, and viewers have been eager to act at QA for chat and suggest ideas for new features, such allowing the use of a marquee tag which is an undocumented Easter egg, and numberonym mode: a numeronym is a number-based word, numeronyms are used in tech to shorten long words such as accessibility, internationalisation, localisation, Kubernetes, et cetera. And here is the code for numeronym mode. </p><p class="post__p">The makeNumeronym function splits the full message by space, and for each word it returns the first character, the count of characters in the middle, and the last. For some reason I published this to NPM so I could use it in multiple places and people are downloading it. Why? So that is fun. Because I&#39;ve never used it in any other project, but go off. I also built a giveaway mechanism into my Twitch bot which are powered by typing commands in Twitch chat. To enter, viewers write exclamation mark win in chat. tmi.js listens for commands, so when the command is found, we run the handler for that command. If it reads !in, we enter the viewer into the giveaway. To draw, I type !drawga in chat, and a random winner is selected from entrants who are still present in the chat. We send the event over the WebSocket, browser overlays, and OBS, and the key thing here is to send the user&#39;s profile image URL over the WebSocket to put them in the stream. Here is a clip of the giveaway in action. </p><blockquote class="post__blockquote"><p class="post__p">I like giving away things. Mo gets a T-shirt! </p></blockquote><p class="post__p">And someone else gets a T-shirt today! Congratulations! [Applause]. Well done! I seriously, just did something to my back! Right, so I&#39;ve given away things like stickers, merchandise, JetBrains licences, FFConf T-shirts. The postage costs for sending things people around the world have got a bit much so I haven&#39;t given away much recently and it is very difficult and very non-inclusive to say UK people only! But it is lots of fun, and I got to build some cool stuff. </p><p class="post__p">Back-seating has always been a thing. It is like an meme now. New viewers often come into the stream and tell me what to do with no context about the problem we are solving, and it is the worst thing ever, like back-seat driving. </p><blockquote class="post__blockquote"><p class="post__p">Why aren&#39;t you doing this, I thought you should use this. Blah blah blah blah. Back-seating. </p></blockquote><p class="post__p">One of my viewers thought it would be a good idea to put back seaters in a literal car. While I was streaming, sent me these very lo-fi three drawn layers of a car that I added to the scene in OBS on the fly.</p><blockquote class="post__blockquote"><p class="post__p">A layer in front of me. [Laughter]. </p></blockquote><p class="post__p">The car has evolved and is now a permanent fixture in my stream and can be triggered my viewers.</p><blockquote class="post__blockquote"><p class="post__p">One second ... two Taco Bells, please, with sparkling water. Thank you.</p></blockquote><p class="post__p">I&#39;ve never had Taco Bell in my life. I don&#39;t know what it tastes like. Sparkling water is the superior water. Here is how the car works. Again, tmi.js listens for a command in chat. Exclamation mark BS. What BS stands for is open to interpretation. We grab the user name from the message and only put people in the back seat who are actually viewing the stream. The point of the car now is to be as offensively obtrusive as possible like back-seaters, and it looks like this. </p><blockquote class="post__blockquote"><p class="post__p">Hey there! </p></blockquote><p class="post__p">There is all sorts of magic, like Aitum, filter-changing, and some tough. It&#39;s probably going to get worse. </p><p class="post__p">Now I want to show you other silly things I built on stream that are not part of my stream, and how it brought people together, what we learned along the way, and just how much fun it was.</p><p class="post__p">GeoCities, big up, GeoCities! Something so close to my heart. The original website builder of the 1990s. And in GCSE IT, while people were making spreadsheets and text documents, I was making websites with GeoCities. I don&#39;t know how I got away with it, to be honest. This is the first home page of GeoCities from 22nd October 1996. Left-aligned content, Times New Roman, full of links and gifs, and table-based layouts. Isn&#39;t that beautiful? The semantic HTML in there is pretty wild, description lists and description term, but it&#39;s not followed by a description definition. So I&#39;m very disappointed. I&#39;m very disappointed in GeoCities right there. I&#39;m going to call them out on Twitter! </p><p class="post__p">This is the Style Stage project. Style Stage, we have already have Stephanie Eccles mentioned today, maintained by Stephanie, about showcasing the capabilities of modern CSS. Your challenge with Style Stage is to completely change the look and feel of this HTML page with CSS only. So it forces you to be really creative, and learn more about CSS in the process. So I wanted to make Style Stage look like a GeoCities website. </p><p class="post__p">There is a lot going on here, isn&#39;t there? If you&#39;ve got internet been you can scan the QR code to go to this. It is responsive, right? And I know that is not authentic. But I couldn&#39;t not make it responsive. Now, I do know what you&#39;re thinking. Salma, how did you pull off getting those iconic dancing baby gifs in there without editing the HTML? I&#39;m glad you asked. Base 64 encoded data URLs added as pseudo elements to existing HTML elements on the page, and assigned to CSS custom properties to be able to use those data URLs easily, because those string are long. It was a complete nightmare, that CSS file, because only more than one I&#39;ve done of this, isn&#39;t there? </p><p class="post__p">The fake visitor counter is also powered by a CSS animated pseudo element attached to the h2 above it, and, yes, I did manually specify 100 key frames, live on stream, much of the frustration of my viewers, as they were forced to watch me do it! I do what I want! </p><p class="post__p">Other highlights include this stunning marquee using the papyrus font added by another convoluted pseudo element, powered by a CSS animation using transform: translate. It is all in the name of entertainment. And these globes! Which are powered by the CSS list style property, and a custom property of a data URL of another Base 64 encoded gif. I learned loads about CSS and how you can stretch the possibilities, and me and my Twitch viewers bonded over our love of the web of yesterday. </p><p class="post__p">Speaking of 1990s internet nostalgia, I made a web ring. A web ring is a collection of websites linked together in a circular structure, usually organised around a specific theme, such as technology. And they were popular in the 1990s and early 2000s, particularly among amateur websites, so all of our websites in here. If you were a member of a web ring, you would display that on your website. So the aim of the claw web ring was to make it as easy as possible to display the web ring widget on any website built with any or no framework. I wanted to make sure that updates, for example, when new members joined the ring, would be automatically pushed out to all sites without a redeploy. </p><p class="post__p">So what you see here is a web component, and clicking &quot;join&quot; takes you to the GitHub repo. You can navigate back and forth between the ring and click to go to a random site in the ring. To add yourself to the web ring, you open a PR to the Claw Web ring Repo and add an entry with your name and site URL. After the original release, I had 26 more people add themselves to the web ring, and for some, this was their first open source contribution.</p><p class="post__p"> When your PR is merged, this will make your site data available on a public URL which is fetched by the web component at runtime which means that every instance on every site of the web component stays up to date. To show the web ring widget your site, add the custom element tag and you include some optional fallback content in case JavaScript isn&#39;t available, or you don&#39;t want to use JavaScript at all which looks like this, progressive enhancement, and you can configure light and dark mode, whether or not to show the full scrollable member list, and is also a hidden Easter egg, of course. Just disappears. Why not? </p><p class="post__p">And now, for the biggest meme of my streaming career, I built a random code generator, and it was the funniest thing I did on stream. This probably won&#39;t be funny today, but it was a kind of you had to be there. Suppose I needed JavaScript for my project, I select the options, I scroll down, and bam, perfect JavaScript for me to copy and paste into my project! Perfect. This is how it started.</p><blockquote class="post__blockquote"><p class="post__p">Quality memes! </p></blockquote><p class="post__p">Chat&#39;s loving it.</p><blockquote class="post__blockquote"><p class="post__p">I love it. I love it! I need to write more code!</p></blockquote><p class="post__p">How does this work? Don&#39;t even know any more! But here&#39;s the JavaScript example. It is a bunch of methods that return a random thing. Some methods return an array which will be processed by a helper function to return a random value from that array. Some methods mash together values. Here are some helper functions, get random noun, verb, single character, which was often used for nonsense variable assignments, and here are the verbs and nouns array. So when the random code is generated, we will always start with a function name which mashes a verb and noun together. For example, initialise array, and you can go through all of them. That is what writing code is, right? It goes on like that. This is the full — it is not the full, there are millions of lines of this. I&#39;m not going to explain this, it&#39;s stupid. If you want to generate random code, maybe show it on your website, what I&#39;m coding right now, similar to what I&#39;m listening to right now on Spotify. You can, because I published it to NPM! Again, no idea why people are downloading this! What are you doing with it! There were 105 downloads in a week in September. But this was actually the first package I published to NPM live on stream, of course, and I wrote about it, and the blog post was really popular. If you search for &quot;build, test, release node module&quot; on Google, it is the top result with a featured snippet. It is like one of my biggest achievements that came out of writing stupid, pointless silly nonsense code in front of a live audience on Twitch. It doesn&#39;t matter what you do. The algorithm will decide. [Laughter]. </p><p class="post__p">This led me to publish a few other nonsense get random NPM modules and I did start building a random website generator, and a get random headline and get random tech business name but it wasn&#39;t funny enough. I received 75 pull requests to the original random code generator site before I turned it into an NPM module. People loved adding their own favourite programming language to the catalogue of random code generators, and again for some people, it was their first contribution to open source, and I&#39;m warning you now, I&#39;m going over, but not over the time I&#39;m allotted in the schedule. </p><p class="post__p">In August 2021, I achieved Twitch parter status, to be honest, it&#39;s not that special. There are many, many Twitch partners these days, but being a Twitch partner meant I could form an official stream team on Twitch, bringing the community closer together, the Claw currently at 28 members across multiple time zones streaming software and game development in a variety of formats and programming languages who are welcoming and inclusive to all, and, yes, I need to update the live, laugh, love banner on the top of that page. </p><p class="post__p">I used this opportunity to add more functionality to my Twitch bot including go live announcements. Powered by the Twitch bot and  the Twitch events API, so when a member goes live, an announcement is published in Discord, alerting all members who have opted in to the Discord role, bringing everyone closer together. We were not far now. For a few more miscellaneous examples, I want to show you some smaller projects. </p><p class="post__p">So last year, I discovered a streaming channel called ZengoWeb, a development agency in Hungary, live streaming their work day from the office. It&#39;s great to have on in the background. There is a co-working vibe but it was difficult to remember their Twitch user names and who was who in the office. I decided to build something for them, another browser overlay to display each person&#39;s Twitch user name on the stream. Here&#39;s the moment when it first went live. </p><blockquote class="post__blockquote"><p class="post__p">It works! There is a house! All right. Great! </p></blockquote><p class="post__p">So how does it work? It is a plain HTML page hosted on Netlify. In order  to give the team overlay without the need to edit the code, it uses an edge function which is a serverless function that runs the closest server location to the request, using an URL query parameter to show team members working from home whenever they need to, by intercepting the HTTP request for the index page parsing the wfh query parameters, and modifying the response accordingly, and returning it, and the ZengoWeb office has a tradition of eating pancakes, so client-side JS that runs on page load that assigns a random team member a pancake emoji, so they know it&#39;s their turn to make, purchase, or source pancakes. The team have also submitted new PRs. I was excited to merge them all. </p><p class="post__p">I&#39;ve also experimented with creating some fun educational resources. One example of this is Game of Codes. It is an HTTP status code quiz and I think it might be a good fame for you, Amber. Here is the QR code if you have the internet. Here is a very sped-up video of me completing the quiz. I did it in one take. I didn&#39;t like try to think too hard or do it over and over again to get ten out of ten. This is my current HTTP knowledge. When the quiz is complete, it gives you a summary of what you got right and wrong, correct answers, so you can learn and try again. Game of Codes is written in plain JavaScript, and with Game of Codes, I also wanted to sow what is possible without JavaScript, without a JavaScript framework, or any dependencies, so instead of using a state management library, we are using good old JavaScript object to keep track of the game state, and the whole thing is written with a functional programming approach, so it is easily testable. There are no tests. And you might think ... [Laughter]. This is easy. Salma has used plain JavaScript. The HTML must have an identifier for the correct answer so JavaScript knows what I&#39;ve selected to determine the outcome so I can do a cheeky, inspect the DOM, check the answer. Surely, I can game the system? I did think about that, of course. Each answer generated is given a string of random numbers to identify it by. One of them is correct, the game state knows which one, but there is no other way to know in the HTML. You could use the browser debugger but you would be like an mega nerd. I did have plans for competitions, a leaderboard, but at this point I was learning some projects don&#39;t need to be finished and can exist just as they are. Almost finished, I promise! </p><p class="post__p">As well as sharing my love for GeoCities, sharing my love for the most enchanting, intriguing website of all time. I&#39;ve been recreating a website that was burned into my brain since I first encountered it in 1999. It is hell.com.</p><blockquote class="post__blockquote"><p class="post__p">This is my favourite part of the internet. Access prohibited. This is a private web. You&#39;re being rerouted to the public information site. I love it! </p></blockquote><p class="post__p">That is a random dialogue prompt thing programmed to come up, and the public information site is just Google. But when this happened to me as a teenager, I believed it. You can read up on the lore on Wikipedia, but the tl;dr is hell.com existed from 1995 to 2009, and I wanted to recreate that mystery on the modern web. There are too many Easter eggs and things to share, but I used the a combination of the page pages and user journeys described in Wikipedia, and the ones I found on the Wayback machine to create it before I&#39;m clicking around. It is built with Astro and jQuery, because I wanted to preserve the nostalgia of an earlier web, even though it is the most popular JavaScript library used on the web today. It was difficult to get jQuery working in Astro, so I wrote about it. </p><p class="post__p">If you like to blog, or want to write about it, the best things that are difficult and the problems you solved. The chances are someone else has experienced this too, and you can help people and yourself when you&#39;ve forgotten how to do something. If I have a closing piece of advice to you, it is to ship your silly side projects. Have fun, like this stupid piece of website! It tells you whether I&#39;m live on Twitch or not. It takes more inspiration from GeoCities, plays with CSS, the fake marquee makes another appearance. I learned a lot from making it, because silly things stick. </p><p class="post__p">I remember vividly in primary school when we were learning about the effects of alcohol, and instead of just standing at the front of the class and telling us how dangerous alcohol was, our teacher spent the lesson wobbling around the classroom, slurring his speech, and generally being silly, making us laugh, but we learned. And I&#39;ve never forgotten this lesson, and I&#39;ve always drawn on this approach for inspiration when I was a music teacher, and it is one. Core driving principles of entertainment as code. I&#39;m Salma, I write code for your entertainment. Find me opt internet everywhere as whitep4nth3r, and I will see you on Twitch! [Rock music]. 
[Cheering and applause]. </p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>We actually need more JavaScript frameworks</title>
          <description>Join me as I share my weird and winding journey through the JavaScript framework landscape.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/talks/we-need-more-javascript-frameworks/</link>
          <guid>https://whitep4nth3r.com/talks/we-need-more-javascript-frameworks/</guid>
          <pubDate>Thu, 26 Jan 2023 00:00:00 GMT</pubDate>
          <category>JavaScript</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Hello and welcome to the most controversial talk at thejam.dev 2023. In June last year, I yolo deployed a blog post. I was feeling frustrated and I was overwhelmed at the state of the JavaScript ecosystem. There was simply too much choice. And it&#39;s a meme, right? One JavaScript framework gets released every minute!</p><p class="post__p">Now, this blog post links to a stupid website I built: should-i-write-a-new-javascript-framework.lol. And at the time the answer was no. I did not recommend it. But this blog post and this website, the reactions to it and the conversations I had along the way started me on a journey, resulting in an update to that website where I said that, okay, maybe you should write a new JavaScript framework. And I wrote about it in depth on my blog. And I&#39;m gonna tell you the story here because yes, we actually do need more JavaScript frameworks. You can quote me on that. </p><p class="post__p">My name is Salma Alam-Naylor and I write code for your entertainment, and I am going to take you on a journey and show you why I changed my mind.</p><p class="post__p">Now, a common question I get asked in my Twitch streams, in Discord servers, I see it all over the internet. I&#39;m starting a new project. What JavaScript framework should I use? And so to help answer this question, I built another website, What the Framework? And it was intended to be a tool to help people choose.</p><p class="post__p">Now the answer of course is always, it depends, but I wanted to encourage developers to drill into those dependencies and make a decision about a framework to use based on the type of site they wanted to build. No hype, no bias. And I tried to achieve this by asking just two question. Silly, I know, it&#39;s far more complicated than that, but let&#39;s take a look.</p><p class="post__p">Question one, what type of website are you building? Now without biasing the answer with too much technical terminology, this tries to determine if you&#39;re building a static site, which is a website that&#39;s packaged up and served as static HTML files and assets from a content delivery network. These sites are good for content-based sites that don&#39;t change often. Or are you building a site that needs to be server-side rendered, where each HTML page is built freshly on the server at request time. These are good for dynamic sites where each page needs to provide a custom experience for each user. Or are you building a combination of the two, some static pages, some dynamic pages, and I&#39;ll refer to this mode as hybrid mode later.</p><p class="post__p">Question two was originally, do you want to maintain state across multiple pages? But you can achieve that in more than one way. So it was a bit of a weird question. So I gave more technical transparency in this question, and I asked, do you want to build a single-page application in JavaScript? A single-page application or SPA is a JavaScript application that builds HTML for different pages locally in the browser. It needs client-side JavaScript to run. Alternatively, you can build a multi-page application or an MPA where every route has its own HTML file that&#39;s delivered from the server and doesn&#39;t rely on client-side JavaScript to load initial content. </p><p class="post__p">Now here we have a list of frameworks. We&#39;ve chosen a dynamic site that&#39;s a single-page application. Now these are all filtered based on the features available in the frameworks. Simple, right? It&#39;s actually not that simple. There are only 23 JavaScript frameworks listed on What the Framework. The criteria to include these frameworks in the list were that the code is actively maintained and the project has published a stable version one release.</p><p class="post__p">But, say my project needs to serve both static content and server-rendered pages, so hybrid mode, and I want to build a multi-page application. Here we have five options. Eleventy, RedwoodJS, Next.js, Nuxt, and Gatsby. Plenty of options, right? Not really when we look at what the frameworks do. </p><p class="post__p">Next.js or Gatsby aren&#39;t entirely appropriate for my use case because they are SPAs by default. They use React. Now, there are ways to generate static sites with Next.js or Gatsby to turn the site into a multi-page application. But you&#39;d need to do a few workarounds and also static builds using these frameworks actually can&#39;t make use of server-side rendering functionality, at least at the time I wrote this talk, so it&#39;s a no-go for my requirements.</p><p class="post__p">Eleventy is a good option, but the server-side rendering on the edge functionality is currently experimental and it only works on Netlify and I don&#39;t like vendor lock-in. </p><p class="post__p">So we&#39;re left with two options, Nuxt or RedwoodJS. Now, Nuxt 3 specifically provides a hybrid combination of static and server-side rendered pages as a multi-page application, but I don&#39;t know Vue right now, and I&#39;m not sure I want to learn a new framework in a new project.</p><p class="post__p">RedwoodJS is a full-stack framework, which could be a great option in theory, but it comes with loads of overheads and integrations that I&#39;m not sure I need just yet. Given what the web is capable of in 2023, there are simply not enough options. I&#39;ve presented only one problem here, my hybrid MPA, but there are many more nuances.</p><p class="post__p">Zach Leatherman, the creator of Eleventy points out that there are also many ways to define server-side rendering. So how do I decide which framework to use if I don&#39;t know which type of server-side rendering I need, or if I even need server-side rendering at all. And as the web continues to evolve, the choices we have to optimize for performance are growing.</p><p class="post__p">Ben Holmes, core maintainer of Astro, ran some experiments using caching and server-side rendering, and he discovered that server-side rendering can still match your static site&#39;s speed. This means you can do less pre-building of static pages and do more server-side rendering and still have a fast site. </p><p class="post__p">So servers could give me better performance, but there are different types of server-side rendering. I don&#39;t know what I need, but should we even be considering static sites anymore? There are certainly real world problems that could be solved with new frameworks. And we&#39;re developers we build to solve problems, right? </p><p class="post__p">As Ryan Carniato, creator of SolidJS says, &quot;We need new frameworks. We need innovation right now.&quot; Ryan is committed to bringing framework authors together to work on building a better web ecosystem together, avoiding the framework wars and building a better web. And I talked to him in depth about all of these issues when I released that blog post on stupid website. </p><p class="post__p">So I recently researched into the history of the tech that powers Twitter, and it&#39;s an interesting story.</p><p class="post__p">To summarize, in 2010, Twitter began implementing a new architecture almost entirely in JavaScript. One of the primary goals was to make page navigation easier and faster by providing a rich web application that behaves like a traditional website. That sounds like a single-page application to me. In 2010, three years before React  released.</p><p class="post__p">In 2012, Twitter announced that to take back control of their front end performance, they decided to move much of the rendering from the client back to the server. And in 2013, just nine days after React was released, Twitter released a JavaScript framework — Flight — which they were using in production. It was an interesting alternative to React given it didn&#39;t lock you into templating languages, and it allowed you to render HTML on both the client and the server. This was in 2013, 10 years ago. </p><p class="post__p">In 2017, Twitter released Twitter Light, aimed at improving the mobile Twitter experience by minimizing data usage, loading quicker on slow connections, and taking up less than one megabyte of device space. You can still install that now. It&#39;s a Progressive Web App, aiming to replicate the native app-like experience of a single-page application.</p><p class="post__p">The story goes on. You can read more about it in depth on my blog, but the bottom line is the tech that powers Twitter and so many projects and products go on a journey, much like I did last year. </p><p class="post__p">Let&#39;s render everything on the server. Web 1.0. Now let&#39;s do everything in JavaScript in the browser. Long live the single-page application. Actually, let&#39;s move everything back to the server because it&#39;s quicker. It&#39;s fine, it&#39;s good. We&#39;ve got the power now. And now this isn&#39;t a bad thing. It&#39;s the evolution of the web and technology. But we can take two things away from it. And the first is that tech is always cyclical. Server-side rendering is quite a new thing for front end embedded into JavaScript frameworks. But this is how Web 1.0 worked and how a lot of the web still works today. And number two, Twitter adapted and evolved tech in response to how people were using the platform. Most notably, by prioritizing mobile adoption in 2017. </p><p class="post__p">The technical choices you make, such as your JavaScript framework, don&#39;t just depend on the features your product will need. These choices are also massively influenced by how people will use your product. I will encourage you to ask meaningful questions when you are choosing your tech. Who is your audience? How fast is their internet connection? What devices do they use? Do they use more than one device frequently? How do they actually use the product?</p><p class="post__p">And because of all this and more, What the Framework is a pretty much entirely useless tool. There&#39;s just no one answer. As always, it depends. So if you are building something, if you can&#39;t make it work with existing tech and there&#39;s a problem you want to solve, build that new JavaScript framework because there is no perfect solution and there probably never will be. And it&#39;ll change and evolve even more. </p><p class="post__p">And because we&#39;ll never solve every problem, for every use case, for every product, we actually do need more JavaScript frameworks. Thank you.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>We're all living on it. But what exactly is The Edge?</title>
          <description>What is The Edge? What are Edge Functions? And what impact does this new technology have on the Jamstack and the web?</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/talks/were-all-living-on-it-but-what-exactly-is-the-edge/</link>
          <guid>https://whitep4nth3r.com/talks/were-all-living-on-it-but-what-exactly-is-the-edge/</guid>
          <pubDate>Wed, 10 Aug 2022 23:00:00 GMT</pubDate>
          <category>JavaScript</category><category>Serverless</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">Hi everybody and welcome to Moar Serverless. I&#39;m really excited to be the first speaker of the day. I&#39;m really honoured to be here, thank you so much!</p><p class="post__p">The title of my talk is &quot;We&#39;re all living on it. But what exactly is The Edge?&quot; But before we get to that, a little bit about me. I&#39;m Salma. If you&#39;ve seen me around the internet before you might know me as whitepanther, or if you use a screen reader white-p4-enth-3r. I&#39;m still trying to find ways to make screen readers read out it as whitepanther! And I help developers build stuff learn things and love what they do. I&#39;m a Staff Developer Experience Engineer at Netlify, and if you haven&#39;t heard of Netlify it&#39;s a modern web development platform that offers you a huge range of developer tools to build, test and ship your websites projects and products really really quickly.</p><p class="post__p">I work with developers at all experience levels to enable and empower everyone to build a better web, and I do that by writing guides and tutorials for the Netlify blog, speaking at events and conferences like this, speaking on podcasts and all that fun stuff. And I also stream live coding on Twitch. I stream front end development, CSS, JavaScript, Jamstack, accessibility and YOLO deploying unfinished websites. I also maintain a variety of open source projects from fun stuff to serious stuff, and you can find them on my GitHub profile and get involved if that&#39;s your bag.</p><p class="post__p">So, back to today. We&#39;re all living on it. But what exactly is The Edge? More and more cloud hosting providers and software as a service platforms are offering serverless functions — at The Edge. But what is The Edge? What are Edge Functions? And why does it matter?</p><p class="post__p">We&#39;re going to answer these questions today by looking at serverless functions,
serverless and Jamstack, The Edge, and finally Edge Functions on Netlify. But — first question to set the scene. What is a serverless function?</p><p class="post__p">Now, I like to break things down as much as possible when getting technical. So the actual first question we&#39;re going to answer is what is a function? Now, a function is a bit of code that does something. hHere is a small JavaScript function. it&#39;s called print message, and it simply prints out the message “I am a function” to the logs, the terminal etc.</p><p class="post__p">So that&#39;s a function. But what about serverless? What do we mean by serverless? However, we can&#39;t talk about serverless without answering — what is a server? So a server is a computer that can store and run bits of code. And those bits of code are the functions we talked about just now. Servers can be physical servers — that&#39;s hardware existing in time and space — or virtual servers — software on a physical machine — and these mimic the functionality of physical servers.</p><p class="post__p">So when we say serverLESS do we actually mean no servers? No, we don&#39;t! When we talk about traditional servers, we talk about managed servers — like this. Here is a server room storing a bunch of physical servers, and despite being “managed”, you might notice that the cable management is poor.</p><p class="post__p">Back end code, for example code that reads and writes information to a database, needs a running server to run. And before the dawn of serverless and “The Cloud” in around 2008 to 2010, in order to run any code that required a server, you had to store and run that code on your own physical servers or computers, like in this image, and maintain manage and scale them yourself. For small projects, you could get away with hosting your code in your bedroom or in a server room like this, but this doesn&#39;t scale when you have thousands of website visitors around the world, generating potentially millions of requests. And what do we mean by scale? More requests equals more resources, which means more servers.</p><p class="post__p">To get around this, larger businesses required a group of networked — or connected — physical servers to ensure a fast experience for users. And the cost of running these physical servers could get expensive very quickly. Think about popular times for online shopping such as Christmas. E-commerce websites that run on physical servers need to respond to demand. If there aren&#39;t enough servers to handle all that traffic, the site will become slow unresponsive and people won&#39;t be able to check out. I&#39;ve actually seen some websites operate a virtual queuing system in times like this where you actually can&#39;t access the website until there&#39;s enough server resource available. And that&#39;s going to lose you and your business some sales.</p><p class="post__p">So the solution to all of that un-ideal nonsense has moved us from relying on managed physical servers to embracing cloud servers. Serverless doesn&#39;t mean no servers. It means you don&#39;t manage the servers yourself. You use servers as a service. And here&#39;s what they might look like. And yes! The cloud servers are still physical servers somewhere in the world, in a fixed location. You just don&#39;t need to worry about them. They&#39;re managed by somebody else.</p><p class="post__p">AWS provides serverless functions, also known as AWS Lambda. And if you&#39;ve used
AWS in the past you may be aware that you can choose which location or region
to run your serverless functions from, such as US West, US East, Paris or Ireland. And we&#39;ll come back to why locations of servers are important later when we start to talk about The Edge.</p><p class="post__p">So why is serverless good? Serverless functions are backend as a service. On-demand servers have removed limitations and constraints from the traditional managed server model allowing you as developers to build and scale applications by only thinking about the bits of code that you need to run. This means that everyone from hobby developers to large businesses can execute back-end code on demand without having to manage maintain and scale their own servers and manage their own cables.</p><p class="post__p">With serverless there&#39;s less stress. No security and software upgrades. You don&#39;t need any physical space and you can outsource the overheads and focus on building your product. Serverless is cost effective. When you run a serverless function, you pay only for the processing required whilst the code is running — not for persistently running servers and the maintenance that comes with it. You also don&#39;t need to worry about scaling, which means increasing the number of servers available when your website traffic grows. It&#39;s all taken care of for you by the service provider. And what I love the most is that most cloud providers come with generous free plans so you can usually run hobby projects that use serverless functions at little to no enough financial cost. So you can try things out before you start investing in your next big idea.</p><p class="post__p">Now let&#39;s talk about serverless and Jamstack. It might feel like a little side step but it will lead us right on to The Edge. Serverless is a complementary technology to Jamstack, meaning that the technologies work really nicely together and help enhance and enrich your projects. They go hand in hand as the emoji suggests. Let&#39;s look at another definition: what is Jamstack? You might know Jamstack as using static site generators to ship static sites. But as the web continues to evolve and make more and more things possible the definition of Jamstack is also evolving. My colleague Phil Hawksworth recently released a post on the Netlify blog in which he discussed a refreshed Jamstack definition.</p><p class="post__p">The key takeaway is that Jamstack is an architectural approach that decouples or separates the web experience layer — the front end — from data and business logic — all the back-end stuff. Using serverless functions with your front-end code is a perfect example of this architecture, and one of the key technologies that powered Jamstack architecture from the beginning was the CDN, or content delivery network. A CDN is a network of servers distributed around the world, working together to serve cached static and pre-built content to users from the closest server location possible. Keep this in mind and it will become important in a moment.</p><p class="post__p">Traditionally requests for web pages were processed by a managed server, which we talked about earlier and this is an a fixed location. A user makes a request from a web browser. The server receives that request runs the business logic, builds and bundles the HTML documents and all the resources in real-time, and sends it back to the browser. There are several factors that can make this slow, resulting in a bad user experience. If the business logic is complex, if the return bundle is large, and if the request is being made far away from the physical server in a fixed location, for example, halfway around the world, that&#39;s going to make the experience slow. In contrast, when using a static site generator combined with Netlify, your HTML pages are served as pre-built static assets from the content delivery network. This removes the build and bundling process saving you a lot of time.</p><p class="post__p">A CDN is also known as an edge network, referring to the servers that sits on the edge of multiple networks, which act as bridges to route traffic between networks across the CDN. When somebody visits your pre-generated site on Netlify they get back the pre-built pages straight away from the closest CDN server node location, and there&#39;s no build to run. As a result, people around the world get a fast experience on sites served by a CDN.</p><p class="post__p">Serverless functions have historically been restricted to running on servers in specific locations, take for example AWS US West. For me, in the UK, making a request to US West takes longer than making a request to one of the European regions, due to the physical distance the request has to travel. If you&#39;re using serverless functions in your product, in order to make your website fast for everyone globally, you need high availability on servers around the world coupled with an intelligent network which knows how to route requests to the closest location to the user. And this is now possible with edge functions.</p><p class="post__p">Edge functions are serverless functions powered by the edge network, which means that you can run serverless functions at the closest location to a request anywhere in the world. You might also hear this technology referred to as edge computing and with this, you as developers can collectively make the web faster and more accessible to more people, which could potentially change lives. And with the wide adoption of edge computing, we may also begin to see a reduction in carbon emissions as a result of data being served quicker over a shorter physical distance. And as someone currently surviving another heat wave in the Uk who doesn&#39;t have air conditioning — I would appreciate that very much!</p><p class="post__p">In April this year, Netlify released Edge Functions, and you can now add edge functions to any new or existing project on Netlify. Netlify Edge Functions are very much like serverless functions. You can write them using JavaScript or TypeScript, but instead of using Node.js under the hood, they are powered by Deno — an open source runtime built on web standards. With Netlify Edge Functions, you can transform HTTP requests and responses, stream server-rendered content, and even run full server-side rendered applications at the edge.</p><p class="post__p">So what does an edge function look like? Here is a hello world example. An edge
function is a function — just a bit of code that does something, and this hello world example is written in JavaScript, but you can also use TypeScript if you prefer. Here we export a default async function and we return a new HTTP response with a string “Hello, world,”.</p><p class="post__p">So you might be thinking well Salma, that edge function code looks suspiciously similar to a serverless function. Why would I use edge functions in production instead of using standard serverless functions on Netlify? So whilst Netlify serverless functions are great for providing API endpoints and serving responses, the super cool thing about Netlify Edge Functions is that you have the power to modify requests and responses on the fly before they are returned to the front end. Edge functions are also faster than serverless functions. Compared to Node environments, Deno starts up and is ready to run code much quicker. And all this happens at the edge  — at the closest location to the user. And you&#39;ll see how this becomes very powerful when we go through some examples. The TL;DR is that you can do a lot of stuff on the edge that you would usually do with client-side javascript, and if you know me you know that i love to ship as little client-side javascript to the browser as possible.</p><p class="post__p">So let&#39;s take a look at how you can add edge functions to your projects on Netlify in just three steps. Step one is to add edge function files to your projects. Step two is to associate those edge functions with URLs, and step three — you just deploy your project to netlify as usual.</p><p class="post__p">So step one. Let&#39;s add an edge function file to an existing project. Heere&#39;s an existing project that you might have on Netlify. If you&#39;re already using Netlify functions, you&#39;ll have a Netlify directory, and inside that a functions directory. Edge functions live in an edge functions directory right next to functions. And I don&#39;t know how many more times I can say functions today and say it correctly! Here&#39;s the edge function we wrote earlier —  hello world — and there it is living in the edge functions directory in our existing project.</p><p class="post__p">So step two is to associate your functions with URLs. If you&#39;ve got a project on Netlify,
you&#39;ve most probably got a netlify.toml file, and the netlify.toml file is a configuration file that specifies how Netlify builds and deploys your site, including redirects, branch settings and more. It&#39;s version-controlled and allows new team members to get a setup with no config required. Here&#39;s a netlify.toml file that&#39;s already got a redirect set up, redirecting from a nice clean short url to a terrible url that is too long. And to associate edge function files with URLs, we add edge functions entries to the netlify.toml file, and each edge function will have a separate entry. So you add the path that you&#39;d like the edge function to run on — this one is just slash hello — and then we point to the name of the file inside the edge functions directory, which is hello edge.</p><p class="post__p">So step three is to deploy your project to Netlify as usual. If you&#39;ve set up automatic deploys via git, you can use git push to have Netlify build and deploy your site automatically, or you can build your project locally and then deploy it with Netlify using the Netlify CLI. And here&#39;s our edge function live, and if you&#39;ve got keen fingers you can actually go to this URL right now in your browser to see it in action. And I&#39;ll tell you more about that website in a moment.</p><p class="post__p">So there you have it! Edge functions on Netlify in three steps. But just now I mentioned the Netlify CLI. So what&#39;s that? In order to make edge functions worth it for developers, we need good ways to develop and test locally for the edge. We can&#39;t YOLO deploy this. And that&#39;s exactly what you get with the Netlify CLI. Install the Netlify CLI globally via npm, and then you can either associate an existing local project on netlify using netlify link in the project directory, or you can create a new project in your Netlify account using Netlify init. Finally, you can run Netlify dev to spin up a local development environment, and your edge functions will run locally —even with geolocation data, and we&#39;ll get to that in a moment. And top tip! You can also save some keystrokes and use ntl, which is a built-in alias. I love telling people about that, it&#39;s my favourite.</p><p class="post__p">Okay, so that&#39;s hello world covered. So let&#39;s look at some more real-world examples of how you can use Netlify Edge Functions. Let&#39;s return to our hello world edge function. This is a small function that returns a response object. Now, if you&#39;re familiar with HTTP you&#39;ll be pleased to know that this response is the standard web API response, and when working with edge functions you&#39;re building with web standards and open APIs because of Deno, and there&#39;s relatively little new syntax to learn, and you can work with the response object in the usual ways such as setting the content type headers and more. Edge functions receive two arguments which you can make use of in your code. The first argument is the request object, which again is the standard request object representing the incoming HTTP request, and you can use all the native web API functionality on the request object, such as setting request headers and parsing the request URL for parameters.</p><p class="post__p">Here we modify the response based on the event parameter in a URL. So we get the URL object from the request, and if we find the query parameter event equals “moar” in the URL, we return “Hello, Moar serverless!” or we return “Hello, everyone else!” if we don&#39;t find it. This is a perfect example of being able to modify an incoming request before you send the response back to the user without needing to use client-side javascript to parse the URL in the browser after the fact.</p><p class="post__p">The second argument is the context object, which is specific to netlify and has some pretty powerful features. The context object exposes Netlify-specific APIs, and currently you can work with cookies, access geolocation and IP information, return data as json, write to logs, the next method will serve the next requests in the HTTP chain, and you can also rewrite incoming requests to another same site URL and get site information.</p><p class="post__p">So let&#39;s use the geolocation API to modify a returned response depending on your location. So let&#39;s make sure we add the context as a second argument of the edge function, and let&#39;s grab the location information from context.geo. Let&#39;s be ruthless and say you&#39;re only allowed to access this content this hello message from the UK, like so. If the  location.country.code equals GB, and we can even modify the HTTP status code to return when the request is not coming from the UK to tell the browser that this content is not available for legal reasons. And hopefully if you take anything away from this talk today is that you remember what HTTP status code 451 means. I like that one for some reason.</p><p class="post__p">As a front-end developer I don&#39;t think I&#39;ve ever had these capabilities so readily accessible before, and I would have no idea how to configure physical servers and traditional backends to modify a response at the server level and based on the user&#39;s location. With Netlify Edge Functions it&#39;s right in front of me, and it&#39;s using the web APIs I&#39;m already familiar, with plus some extra magic from the Netlify context.</p><p class="post__p">Now for some more magic. Let&#39;s take a look at how we can use cookies to create a personalized experience without JavaScript. Let&#39;s say you have an online shop where you sell two categories of products — corgis and food — and you want to modify what someone sees based on what they&#39;re most interested in, to hopefully encourage people to buy more things that they like. Here&#39;s a really basic example. Say someone clicks on an item from the food category which navigates to a URL that contains the string food. Here&#39;s some lovely pizza, notice food in the URL. </p><p class="post__p">With edge functions we can parse the URL, and set a cookie based on what the user clicked on. So here we grab the url from the request. If the path name contains food we set the prefers cookie to food, and if the path name contains corgi we set the prefers cookie to corgis. Next, when we load the home page, we can read the prefers cookie and either modify the products that the user sees at the top of the page by sorting the data, or by using context.rewrite to redirect to a special food homepage or a special corgi home page. So I clicked on a pizza, the prefers food cookie was set, and now when I navigate to the homepage, the URL is rewritten to the home page for food, and all I see is food. Now I&#39;m hungry,  sorry if you are too.</p><p class="post__p">This no-js personalization idea is from Jason Lengstord, who was the creator of the food and corgis website. He goes one step further and sets the cookies based on a scoring and waiting system, but go check out the code on GitHub, and try out the personalization on the site. Personalization without JavaScript is a dream and that&#39;s just one of the things you can do with edge functions.</p><p class="post__p">If you&#39;re excited and curious there are so many more examples to look at over on edge functions examples dot Netlify dot app, which is the site that I showed you hello on earlier. We&#39;ve got loads of examples — it&#39;s a growing list. There&#39;s inspiration for how you might put things into your products, there&#39;s atomic examples without trying to sway you any way with opinionated code, and you can also look at the code examples on GitHub as well as on the site, and you can also deploy the whole site to your Netlify account to iterate and experiment.</p><p class="post__p">And there&#39;s more! You can use edge functions in any project whether you&#39;re using a front-end framework or not and actually my favourite way to use edge functions is without a framework at all, and Netlify also uses edge functions to power some of the more advanced features in a growing list of popular front-end frameworks including Astro, Eleventy, Next.js, SvelteKit, Remix and more. Remember when I talked about rendering full server-side rendered applications at the edge? You can do that now. You can also find more information in the Netlify documentation about the frameworks that you can run on the edge.</p><p class="post__p">So let&#39;s recap. Serverless functions are bits of back-end code, run on servers in a fixed location that you don&#39;t have to worry about managing. An edge network is a group of connected servers that sit on the edge of multiple networks to route traffic between networks across a content delivery network or CDN. Combine serverless functions with an edge network and you get edge functions — serverless functions at the edge, and they give you the power to run code on demand at the closest location to a request, and intercept requests anywhere in the world. This is making the web faster and more accessible to more people around the world and is empowering developers to create more performant front ends by moving computation from the client to the Netlify Edge Functions today in your existing projects on Netlfy. Edge functions run on Deno, which is open source and they use standard web APIs and the Netlify context API. It just takes three steps to get started with Netlify Edge Functions. Add files, associate your files with URLs, and deploy your project as usual. We&#39;ve got a library of examples for you to fork deploy and experiment with, and you can use the Netlify CLI to develop and test edge functions locally.</p><p class="post__p">We&#39;ve got loads more information on the docs about the apis and how you can get started and I also have a video on youtube where I explored edge functions during the launch in April and kind of did a lot of what I&#39;m doing here today. So my name&#39;s Salma or white panther or white p4-enth-3r, and I help developers build stuff, learn things and love what they do. If you want to find out more about me and see what i do you can visit <a href="https://whitep4nth3r.com" target="_blank">whitep4nth3r.com</a> to learn more. Thank you for watching and have a great day at Moar Serverless!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>Contentful's Fast Forward 2021 Day 2 Keynote</title>
          <description>Join Salma and Stefan Judis on a journey through time where they explore how content management and software development has evolved.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/talks/contentful-fast-forward-2021-day-2-keynote/</link>
          <guid>https://whitep4nth3r.com/talks/contentful-fast-forward-2021-day-2-keynote/</guid>
          <pubDate>Thu, 04 Nov 2021 00:00:00 GMT</pubDate>
          <category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <h3 class="post__h3">Salma</h3><p class="post__p">So it is 2021 we are coming to you live from Berlin in 2021 and web development today is an extremely powerful ecosystem of tools frameworks and services that means I, personally, get to build really cool stuff all day, every day and put stuff live and update it whenever I want.</p><p class="post__p">And we at Contentful are part of that ecosystem. But it wasn&#39;t always like this. So Stefan, how did we get here?</p><h3 class="post__h3">Stefan</h3><p class="post__p">That is a good question! So for Contentful everything started with a single question. And the question was, &quot;Why is there no content management system for mobile applications?&quot; And I did my homework, and I did a little bit of research, and I browsed internet and web archive and I found this Hacker News entry. And this user entry is from June 3rd 2011, and what you basically see there is a person that is called &quot;sashthebash&quot;, who turns out to be Contentful&#39;s co-founder Sascha Konietzke and he was kind of testing the waters if there is a market for a JSON-based API-first CMS.</p><p class="post__p">And what he was building at the time was storageroomapp.com — which still redirects today to Contentful, because at the time there was no flexible content management solution available. So Salma, 10 years ago what were you doing?</p><h3 class="post__h3">Salma</h3><p class="post__p">Funny story! There&#39;s still a website live that I released in 2010, and at the time I was building websites for my musician friends in a piece of software called iWeb which was an image-based website creator that used to ship with older iMacs. And even then, in 2010, I had to develop a solution to the content management problem. My clients wanted to be able to update their website without me and getting a developer involved. So what you see on this screenshot in that purple area in the latest news is actually an iframe taking content from a text file that I taught my clients how to upload via FTP and I&#39;m sure they broke it a lot of times. So this was 2010 — an example of a small website that was trying to solve the problem of content management. But 10 years ago this problem also existed at scale in large product companies.</p><h3 class="post__h3">Stefan</h3><p class="post__p">So 10 years ago, websites and products were built entirely in-house and Salma and I — we have a big love for front-end development — but at the time there was no serious front-end development yet. And if there was, front-end and back-end teams were divided. And the pain point of this time was that everyone was forced to reinvent the wheel all the time. I cannot tell you how many times I had to build a login form, a search form or a content management solution in one way or another. It was repetition, repetition, repetition. And all that led to the sad reality that development was hard, expensive, and slow.</p><h3 class="post__h3">Salma</h3><p class="post__p">But around the time I was developing my own content management solutions with iframe text files, things were starting to change. In 2010, specialist front end developers were starting to build the beginnings of front-end frameworks as we know them today. In tandem the development of angularjs and backbone.js were starting to make front end development more serious. And it&#39;s really interesting that the first releases of these two frameworks were just a week apart. </p><p class="post__p">Fast forward to 2013, and whilst angularjs and backbone.js aren&#39;t really in use anymore, React came to the scene, and was released to the public. And if you&#39;re a front-end developer you&#39;ve most probably heard of React, and you&#39;ve maybe used it to build stuff — so it stood the test of time.</p><p class="post__p">So 10 years ago we can see with angular and backbone and react, front-end development really started to evolve. And content requirements changed, as we saw from Sascha&#39;s post on hacker news. And what&#39;s more, at the same time — which we&#39;ll get into later — cloud computing arrived on the scene.</p><h3 class="post__h3">Stefan</h3><p class="post__p">So with storage room, Sascha was looking for a solution to build mobile apps faster, and he wanted to do that with the ease of maintenance of websites, and at the time but basically, well, that was just the beginning. Because today we know that content needs to go anywhere. And it goes from watches, fridges, cars smart somethings, to also different use cases. Because when you just build a website today there is a high chance that you maybe want to reuse the content for a different project or different app. The core challenge was integrating content in dynamic applications that are ready for change and iteration. And in today&#39;s world you better be fast. So what does fast actually mean? </p><h3 class="post__h3">Salma</h3><p class="post__p">Fast means fast development, fast to build. How fast can you get your product to market? How fast can you iterate on your products and improve them? How fast can you react to change in the world and what your needs users need? A typical example is when Covid came. And how fast can you pursue new opportunities to keep your products ahead of the competition?</p><p class="post__p">Faster also means fast and cutting edge products. You need to deliver speedy performance and a fast and responsive user experience on the web, because Google tells us and ranks by the faster your site is, the more people are going to stick around. And finally, making things fast to develop and fast to use is not actually possible without innovation. And you actually need to be able to innovate fast as well. </p><h3 class="post__h3">Stefan</h3><p class="post__p">Today builders and technology companies are changing the game. And we do have a few other examples of that. So Ty Magnin from UiPath told us that since they&#39;re using Contentful, they&#39;re able to ship and develop their software stacks faster, but also they&#39;re resulting in way better and faster products because this leads to a better user experience. And as Salma told us, Google pretty much ranks speed as a rating factor. </p><h3 class="post__h3">Salma</h3><p class="post__p">Claudia Kim, vice president and global brand director of Shiseido Professional says that since using Contentful, as a business today they are more connected. They work faster, and everything they do can be updated instantly.</p><h3 class="post__h3">Stefan</h3><p class="post__p">Candace Green O&#39;Leary, director of marketing at Rangle says that Contentful allows them to build pages in hours rather than days. </p><h3 class="post__h3">Salma</h3><p class="post__p">So what makes these businesses fast? It&#39;s a combination of how they manage their content, and the technology they use to manage it. So Stefan, what were the problems that these businesses were facing with their content that was slowing them down before they discovered the power of Contentful? </p><h3 class="post__h3">Stefan</h3><p class="post__p">So before using a content system like Contentful, the problem is that your content is not ready to be reused in different use cases on several devices. And this is why we at Contentful bet 100% on the API-first approach. Sascha was looking for a solution to ship content to mobile applications but basically this is unlocking content to go anywhere. And if you use an old school or legacy content management system and HTML is in your content — well good luck with that one or porting that into a different platform. </p><p class="post__p">We bet on RESTful and GraphQP APIs, but as a side effect we also discovered that structured and scalable content are the core too. So in a past in a previous life I was using HTML-based &quot;what you see is what you get&quot; editors, and they just gave me a pretty pretty hard time. This is why we 100% bet on JSON, and for example, our Rich Text JSON editing experience. But also we figured that probably the use cases are not around pages and posts all the time. So what you can do in Contentful is you can set up posts and pages, but also you can set up events, authors, recipes — you name it. You define the content structures and content metal models and are in charge.</p><h3 class="post__h3">Salma</h3><p class="post__p">You can then fetch and manage your content using our open source tooling. We provide libraries in a variety of different programming languages to help you build front ends with our APIs even faster still. We also provide data migration tooling and content management tooling on the command line (and as a developer I especially love that) but sometimes it&#39;s not only about fetching your content to display it. Sometimes you need to do more. And for that reason we provide an open source app framework, with its own design system, Forma 36, which allows you to build your own integrations inside Contentful to help you manage your own specific content and business needs. We&#39;ll talk about that in a little bit more detail later. And next up after us you&#39;ve also got an AMA later on with the app framework team. </p><h3 class="post__h3">Stefan</h3><p class="post__p">So 10 years ago I had two choices when I was dealing with content. Either I was, I had to use a pre-built old-school content management system that messed with my content, or I was forced to build and run my own. But nowadays you need a way more than a content management system. </p><h3 class="post__h3">Salma</h3><p class="post__p">You need a content management platform. And that&#39;s what we want to do for you at Contentful. As Paulo said yesterday, a platform enables business specific workflows. A platform lets you easily add new capabilities, and a platform brings teams and systems together as we saw in the case studies before. </p><p class="post__p">So now we&#39;ve talked about untangling your content in order to go faster, let&#39;s take a look at how the developments in technology over the last 10 years have helped us as front-end developers to go even faster still.</p><h3 class="post__h3">Stefan</h3><p class="post__p">So in my previous life, what I had to deal with is server setups, hosting and database things. And I just like to build beautiful UIs and do cool things. Then when I was dealing with customers and clients and friends and family to build things for the web, I had to find a content management solution which I had to set up, maintain, and learn in the first place. And then if I had bigger customers, I had to find a solution to scale that up in case the traffic um rose roses rose? So I had to scale that up and then — true story — I was several times at airports going somewhere and I had customers and clients giving me a phone call telling me &quot;Hey Stefan! We&#39;re down! Because I copy and pasted a random snippet from the internet into my content management system!&quot; This is what I was dealing with 10 years ago as a front-end developer and it was just time to break free and go faster. </p><h3 class="post__h3">Salma</h3><p class="post__p">And so we come to 2015 with the dawn of the Jamstack, and the Jamstack started to emerge as a front-end architectural approach in response to a lot of these problems that Stefan was experiencing, personally. So what was the jam stack in 2015? </p><p class="post__p">The Jamstack comprised JavaScript, APIs and markup — hence the name JAM. And the main principles were that you would use client-side javascript connect them, to services and AOUs decoupled from the monolithic CMSs which Stefan hated so much, to get your data. All of this would be built into markup, static HTML, and served from CDNs around the globe. And as a front-end developer you would profit and move at the speed of light.</p><p class="post__p">Mpdern Web Development on the Jamstack from O&#39;Reilly and Netlify says this brought a proper software architecture to front-end workflows and empowered front-end developers like us to iterate much farther faster eehhh. </p><h3 class="post__h3">Stefan</h3><p class="post__p">And Salma and I — we&#39;ve been there. So I built my first Jamstack site with Jekyll and GitHub pages and was it was a pretty decent experience. So we would love to hear from you so please drop it in chat, raise your hand if you use this exact technology to build your first Jamstack sites, but also please provide us some URLS, we would love to see some retro looking Jamstack sites from 2015. </p><p class="post__p">This was mine! JavaScript and the Jamstack was perfect for creating personal projects and small blog sites that front-end developers could build on their own without the overheads of back-end systems and databases and server scaling and all that jazz that I really didn&#39;t want to pay attention to. But since then the ecosystem changed quite dramatically. Today we have over 300 static site generators in use. They range from Next.js to Gatsby to Nuxt to 11ty, and there&#39;s a vibrant and thriving community around the world. around Jamstack today. </p><h3 class="post__h3">Salma</h3><p class="post__p">And we can see this in the results from the Jamstack 2021 community survey. We see that the Jamstack now is not just personal blog sites and small projects anymore. The adoption is far wider. If we look at the top five types of websites that are built on the jam stack they now include e-commerce, B2B software and consumer software. That&#39;s pretty big stuff just for JavaScript, APIs and markup, right? Because things started to change again. Today Jamstack is more than just static site generators. because as the needs of the developers became more complex, the Jamstack had to innovate again. So how has the Jamstack evolved? How was it possible that industries like e-commerce started to adopt the Jamstack? </p><h3 class="post__h3">Stefan</h3><p class="post__p">So it all started with cloud computing. When that kicked off when I started web development, I was using my first CDNs to distribute content all globally — but that was mainly static content. And that all changed with the evolution of serverless functions. Today any developer can build and develop their own APIs with a serverless functions and as you see it&#39;s literally just six lines of code for me to enhance and enrich my website and the products that I&#39;m building, and I don&#39;t need a full back-end hosted application to do so. </p><p class="post__p">And what we see now is that there are modern hosting providers that are tying together frameworks and provides serverless functions and serverless experiences right out of the box. Fast and cheap development is now possible because I can develop things without having to, without the need to know lots of back-end solutions. There&#39;s a very low barrier of entry and we don&#39;t have services to maintain. And the beauty of this entire architecture is that this is scaling up if I&#39;m getting a hacker news hit or if I write a very popular app. </p><p class="post__p">Small front-end teams today can take large projects with little or no operations and back-end support, and this is where we at Contentful come into play. If you need a content management solution, set up your content types, set up your content model and get it into your products. </p><h3 class="post__h3">Salma</h3><p class="post__p">And so now with the arrival of cloud computing and serverless functions, Jamstack is way more than just JavaScript, APIs and static HTML. And so to reflect that it needed a name change. So at some point between 2015 and now it changed from capital JAMstack to just Jamstack to reflect that shift. </p><p class="post__p">So we went from JavaScript, APIs and markup to pre-rendering, serverless functions, software as a service, git integration, automated releases, preview URLs, infrastructure, CDNs at the edge — and probably so much more because of all the innovation in just a few years. But let me tell you what really excites me about the Jamstack as a front end developer today. </p><p class="post__p">So we now have four types of rendering available. So back 10 years ago we started at server-side rendering — building the HTML every time at every request. Now this is still available on the Jamstack now and is great for things such as personalization in e-commerce. So right at the beginning of the Jamstack it introduced static site generation where everything was bundled up and served from a CDN at build time, static, doesn&#39;t change, great. But now there&#39;s some fancy stuff going on. </p><p class="post__p">We&#39;ve got incremental static regeneration which generates static HTML but your data is revalidated at intervals, which then is regenerated in the background using serverless functions, and still being served statically. We also have distributed persistent rendering. Say for example you have a huge website with thousands and thousands of pages which is going to make your build process take hours and hours and hours. With DPR you can choose what to render at build time — and if you don&#39;t render a page at build time, when somebody visits your site you will get that page generated at request time, and it will be cached at the edge for future requests. So you&#39;re building up this static site as you go.</p><p class="post__p">So we&#39;ve moved away from building only static pages with static data on the Jamstack to a truly flexible model, where you can choose when your page is rendered depending on the type of data you&#39;re serving to your visitors and how up-to-date it needs to be — and I love it! </p><h3 class="post__h3">Stefan</h3><p class="post__p">I couldn&#39;t agree more, because I love this new fancy revalidation stuff putting things on the CDNs and we at Contentful — we also love the Jamstack. So as an example we use our own product as much as possible and of course one obvious example here is contentful.com. </p><p class="post__p">So what you see here is a typical Jamstack setup, Contentful.com runs on Contentful, Gatsby, we integrate with segmentio, and we deploy everything to Netlify. But while this is a classical marketing website, content can go in various places, and we took the opportunity the last summer to build something like an internal people directory. You could call it an internal Facebook Metabook, Metaverse or whatever, whatever is the the thing right now. So we built this with Contentful, Next.js, we integrated with Okta, and with Vercel. And I hear you. You might say, Stefan you built you built an internal company Facebook, what&#39;s the deal here, right? The deal is that we did that in two weeks. We were just able to develop, sit down and roll with it.</p><p class="post__p">Thanks to the innovators and the innovation that Salma mentioned, the amount of work that is possible by a single developer is now astronomical.</p><h3 class="post__h3">Salma</h3><p class="post__p">It&#39;s all about speed and how fast you can go. So we started with JSON CMS for mobile apps in 2011, and the journey that web development took us on as developers coupled with all that good innovation means that content was unlocked with new technology. Speed was unlocked with new technology. More innovation was unlocked with new technology, and especially skills were unlocked with new technology. Never in my wildest dreams in 2011 would I have thought I would be releasing all this stuff with serverless functions and CDNs at the edge and blah blah blah, micro services and I had no idea how powerful I could be.</p><h3 class="post__h3">Stefan</h3><p class="post__p">And Salma is an excellent example here because it&#39;s just beautiful to be a developer today. She is YOLO-deploying new projects every day and we reach the state where one developer can literally — and that is a big statement — but one developer can literally change the world. But you won&#39;t change the world by building everything yourself. So let me ask you the question — would you make your own bricks to build a new house? Probably not. Or you probably also don&#39;t want to build your own CMS — I&#39;ve been there — don&#39;t go there! Or you don&#39;t want to own and build your own payment gateway, or your own analytics tools. </p><p class="post__p">There are now software as a service vendors that specialise on doing one thing and they do that one thing very very well. And these vendors go from authorisation to payment to databases and — yes of course — to content management. Because you have to consider that when you&#39;re building your sites or your products the developers,  Salma and I, or the developers in your teams should be focusing on building your core product — and not on setting up or maintaining or scaling, for example a content management system. </p><p class="post__p">Trust the experts, because these companies, like we at Contentful, we probably — we&#39;re focusing on that, and if you want to build that on your own, that will take a long time and you will hit a lot of problems on the way.</p><h3 class="post__h3">Salma</h3><p class="post__p">Over 10 years! </p><h3 class="post__h3">Stefan</h3><p class="post__p">Over 10 years! So when we consider all these software as a service vendors, how do you connect your content with these technologies?</p><h3 class="post__h3">Salma</h3><p class="post__p">And for this as I mentioned before, we love the Contentful app framework. And the app framework is low code tools for developers to build apps for content creation and orchestration. Because you know that content is at the centre of every company and their products that they&#39;re building. Content communicates with your customers, content is your brand, your message, your offering and your value. But it is surrounded by way more. </p><p class="post__p">The app framework allows you to integrate with your ecommerce solutions, your analytics platforms, your warehousing tools and much more to keep your content at the centre where it belongs. </p><h3 class="post__h3">Stefan</h3><p class="post__p">So we at Contentful are heavily invested in providing you apps built on the app framework to integrate with other services and service providers. If you are interested to see what we already provide out of the box you can browse our marketplace and probably find a ready-to-use integration already, but the app framework is also about building your own custom solutions. So for example you can extend the UIs with sidebar apps, with full page apps — you can edit and change the entire editing experience. And if you have the need for content automation, content workflows, you can set up backend apps, too. With the app framework the sky is the limit and we can&#39;t wait to see how you innovate and what you build. </p><h3 class="post__h3">Salma</h3><p class="post__p">So what we&#39;ve seen is that the bottlenecks that came with old school traditional software development, like me and Stefan hated, and building iframes with text files is a thing of the past. And by adopting a platform agnostic JSON-based and structured approach to managing your content, and coupling that with the innovative technology on the Jamstack — in an ecosystem of software as a service, and when you are a builder who can focus on building core products and not reinventing the wheel, you can go faster and faster and faster and innovate even more. </p><p class="post__p">And if all of this has been possible as a result of the changes in web development over the last 10 years, who knows what will happen and what will be possible for us as developers in 10 years from now. </p><h3 class="post__h3">Stefan</h3><p class="post__p">I cannot even imagine what will be possible in the next 10 years but let&#39;s stick to the present for one moment. So if you&#39;re curious to see and to learn what&#39;s happening at Contentful, what are the new features, what are Salma and I doing and where&#39;s the Contentful community,  you can head over to contentful.com/developers. It&#39;s always up to date, so you can go over there and if you want to have a chat with us too, after Fast Forward — not in the next eight hours! You can go to contentful.com/slack — it is an open Slack community, we hang out there a lot we answer questions and we would also love to learn what you are building and how life in general is. </p><p class="post__p">And with this, we made it through the opening thank you everybody for listening and we see you during the day!</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>This box will change your life</title>
          <description>Are you struggling with margins, paddings and layout in web development? Understanding the CSS box model will change your life.</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/talks/this-box-will-change-your-life/</link>
          <guid>https://whitep4nth3r.com/talks/this-box-will-change-your-life/</guid>
          <pubDate>Thu, 23 Sep 2021 23:00:00 GMT</pubDate>
          <category>CSS</category><category>Web Dev</category>
          
  <content:encoded><![CDATA[ 
    <h2 class="post__h2">This box will change your life</h2><p class="post__p">Hi friends, I&#39;m Salma and I help developers build stuff, learn things, and love what they do. If you&#39;ve seen me on the internet before you might know me as white anther or white P-4-enth-3-R if you use a screen-reader. I work as a developer advocate for Contentful. And if you&#39;ve never heard of Contentful, it&#39;s a headless content management platform that offers you a GraphQL and REST API, and many other tools to help you build some really great stuff.</p><p class="post__p">This is a box. This is another box. Here are some more boxes. Here are some tiny boxes. Here are some more boxes. Let&#39;s add some more boxes. And as you see this evolve, you might start to realize that it&#39;s starting to look incredibly like the layout of a webpage. And these are the boxes we&#39;re going to be talking about today.</p><h2 class="post__h2">But first, what is CSS? </h2><p class="post__p">You might know CSS is the language that is used to style your web pages and make them look really cool. CSS stands for cascading style sheet and the three most important things to know about CSS are the cascade, specificity, and inheritance. </p><p class="post__p">Cascade is an algorithm that defines how to combine CSS property values from different sources. Specificity is how browsers decide which CSS property values are the most relevant to an element, and therefore will be applied. And inheritance is where elements inherit CSS properties from their container elements or parent elements, much like how parents pass characteristics down to their children in real life. And this is one of the most important things we&#39;re going to be looking at today. </p><p class="post__p">But first — you versus CSS. In the past, you&#39;ve probably had a bit of a fight. Let&#39;s take a look at one of the most common fights you might&#39;ve had. </p><p class="post__p">Let&#39;s set up an HTML div element with class equals container. Let&#39;s give that container a max width of 640 pixels. Let&#39;s give it a margin of auto, to centre it on the page, and let&#39;s give it a big fat red border. Everything here is as we expect, there is no content inside that box yet. And so the border will collapse on itself. </p><p class="post__p">Let&#39;s add a box inside that container. Let&#39;s give it a yellow background and a big fat blue border. Again, this is how we expect. There&#39;s no content inside that box yet. And therefore the border has collapsed. </p><p class="post__p">Let&#39;s give the box a padding of 20 pixels, and we are beginning to see what that box might look like. Let&#39;s add some content inside that box. Let&#39;s add a paragraph tag and we&#39;re starting to see some results.</p><p class="post__p">Now let&#39;s add width 100% to that inner yellow box. Of course, it&#39;s just going to be 100% of the width of the parent container with the red border, except this is what happens. This is currently a box that is ruining my life and how do I fix it? </p><p class="post__p">We can fix it with one CSS property box sizing, border box, and then we start to see something more like we want.</p><p class="post__p">And this is the fundamental principle of the CSS box model. And this is the box that will change your life. </p><h2 class="post__h2">What is the CSS box model? </h2><p class="post__p">The CSS box model is the fundamental property of how to work with design and layout in CSS. If we think back to all of these boxes on this webpage layout, and if we look inside those boxes, we see a model that we can work with if we understand it. </p><p class="post__p">At the centre of the box is a box of content. Around that content box is a box of padding. Around the box of padding is a box of border. And on the outside of this box is a box of margin. And you&#39;ll see that these boxes tessellate and work like this together in layouts on a webpage. Let&#39;s take a look at a box on my website.</p><p class="post__p">Here is the content. We have some padding that separates the content from the border, and we have some margin around the box that centres it on the page. We can visualise this in Chromium dev tools, where we see colour-coded parts of the CSS box model to help us understand our layout. And if you hover over those areas on the box, you&#39;ll begin to see how your page is comprised.</p><p class="post__p">So box sizing, border box fixed everything for us, but what is box sizing and how does that property work? The box sizing property defines how the width and height of an element are calculated. If we go back to this image of the CSS box model from Chromium dev tools, we&#39;ll see the numbers on each of the sections are the widths of those areas calculated in pixels. </p><p class="post__p">With the box sizing property, you can choose to include padding and borders in the width and height calculation with box sizing border box, or you can choose to not include padding and borders in the width and height calculation with box sizing content box. And what&#39;s very interesting to note is box sizing content box is the browser default. </p><p class="post__p">If we take a look at this box class again with no box sizing specified, the default is content box. And this means that the calculated width of that black box inside the container with the red border is 100% plus 20 pixels of padding all around, plus 10 pixels of border all around. If we take a look at that same box with box sizing, border box, the calculated width is 100% taking into consideration the padding and the border as well.</p><p class="post__p">Now you might have seen ways to mitigate this problem with CSS resets. And there is a big problem with CSS resets when you&#39;re working with complicated layouts and just CSS in general. </p><h2 class="post__h2">But first of all, what is a CSS reset? </h2><p class="post__p">A CSS reset is a set of CSS rules that resets the styling of all HTML elements to a consistent baseline — with one caveat that it&#39;s probably opinionated. There is a vast array of CSS reset packages available that have been in use and in development since the early two thousands. And most of them — if not all — apply a box siding border box to star, which is every single element on your page. If you want to find out more about CSS resets, there&#39;s a <a href="https://css-tricks.com/reboot-resets-reasoning/" target="_blank">great article from Chris Coyier on CSS tricks</a> linked here.</p><p class="post__p">But what I really want to ask you to do is to understand the behaviour of your boxes, understand what you want to achieve depending on what box sizing property you choose. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing" target="_blank">MDN has a great box sizing playground</a> to let you experiment with content box and border box and understand how the child will respond to the parent element.</p><p class="post__p">And they also provide some really good advice. It&#39;s often useful to set box sizing to border box to layout elements. This makes dealing with the sizes of elements, much easier, and generally eliminates a number of pitfalls you can stumble on which we found in the example before. On the other hand, when using position relative or position absolute the use of box sizing content box allows the positioning of values to be relative to the content and independent of changes to border and padding sizes which you might want sometimes. Know what you want to achieve and don&#39;t go blanket resetting everything. </p><h2 class="post__h2">Let&#39;s take a look at understanding browser defaults. </h2><p class="post__p">You versus browser defaults. You&#39;ve probably had a similar fight as what we saw before. Now, what are browser defaults? You might know of them as the user agent style sheet. A user agent is any web browser or any device that you can view an HTML webpage on.</p><p class="post__p">And where do we find those browser defaults or the user agent style sheet? In Chromium, you can see them in the styles inspector in italics labelled with user agent style sheet. And this is in browsers such as Brave, Edge, Chrome and Opera. In Firefox, if you want to see the user agent style sheet, you need to go to the options and check the show browser styles option. And you&#39;ll see them in the same place. What you might notice already, there&#39;s a discrepancy between the Firefox default styles and the Chromium default styles already. And in Safari, you can see them in a similar way to Chromium. </p><p class="post__p">Let&#39;s go back to our box that we used before, which was a div element, an HTML div element. What do you think is going to happen if we change that element to a span? Our box breaks again. Now, why is this? If we go into the styles inspector, we can see actually there are no default user agent styles applied to this span, obviously. If we go to the computed tab, we can see actually that the span is display inline, whereas the div was display block. If we change this span and give it a display property of block or inline block, the result we get is what we expected. </p><p class="post__p">And so this comes to different types of boxes. There are three different types of boxes to work with in CSS. </p><p class="post__p">You have block boxes which could be block, flex or grid. You have inline boxes. And you have an intermediary between block and inline, which can be inline, inline flex, or inline grid. Block boxes take the full width of the page, stack on top of each other, and their margins apply around them vertically and horizontally. It&#39;s worth mentioning that block, flex and grid containers behave the same way, but they affect how their child elements behave depending on the property that you choose.</p><p class="post__p">You have inline boxes, which are stacked together horizontally. Horizontal margins do apply to inline elements, but vertical margins do not. If you want vertical margins to apply around an inline element, you need to change those to inline block, inline flex or inline grid. Here&#39;s a quick summary table to see how the different box types behave.</p><p class="post__p">You can see that the inline star boxes are that intermediary between how inline boxes and block boxes behave. Let&#39;s take a look at a quick example. Here is a set of inline elements. If I apply margin left or margin right to those elements, it applies on the page. If I add margin bottom and margin top, they do not apply. As soon as I change that display property to inline block, inline flex or inline grid, then all of the margins around vertically and horizontally apply. </p><p class="post__p">So I have one simple trick for you to solve all of your CSS problems. Always declare the display property. Don&#39;t rely on the browser defaults. This means you can easily switch between different HTML elements without breaking your layouts.</p><p class="post__p">And in fact, I would encourage you to go one step further and always declare everything. Don&#39;t rely on that inheritance, especially when you&#39;re switching around your HTML tags. </p><h2 class="post__h2">Wrapping up</h2><p class="post__p">The CSS box model and understanding it will change your life. It changed mine. You can use the CSS box model concepts to understand how your layouts are really built and how they&#39;ll work.</p><p class="post__p">Think inside the box. Think about how you want your content and your padding and your border to behave. Think outside the box. Think about how you want your margin to be applied. Think about the box. Is it a block box or is it an inline box and how do you need it to stack on the page? The CSS box model, when you get to grips with it, I guarantee you will change your life. </p><p class="post__p">Come and say hi on my code newbie profile, and you can find me as whitep4nth3r and let&#39;s continue the discussion of this talk. And I want to know how you&#39;ve found it and has it helped you, and will it change your life. You can find me on the internet as white panther or white P-4-enth-3-R everywhere.</p><p class="post__p">I stream regularly on Twitch and I create YouTube content as well. Build stuff, learn things, love what you do. And I hope you have a great time at CodeLand. Thank you very much.</p>
  ]]></content:encoded>
        </item>
        
        <item>
          <title>How to prevent the collapse of society by building an accessible web</title>
          <description>It’s 2031. Society, as we knew it in 2021, has officially collapsed. How do you ensure your website is accessible amidst a raging apocalypse?
</description>
          <author>whitep4nth3r@gmail.com (Salma Alam-Naylor)</author>
          <link>https://whitep4nth3r.com/talks/how-to-prevent-the-collapse-of-society-by-building-an-accessible-web/</link>
          <guid>https://whitep4nth3r.com/talks/how-to-prevent-the-collapse-of-society-by-building-an-accessible-web/</guid>
          <pubDate>Tue, 22 Jun 2021 23:00:00 GMT</pubDate>
          <category>Accessibility</category><category>CSS</category><category>JavaScript</category>
          
  <content:encoded><![CDATA[ 
    <p class="post__p">How to prevent the collapse of society by building an accessible web. Hi friends, I&#39;m Salma. I help developers build stuff, learn things, and love what they do.  If you&#39;ve seen me on the internet already, you might know me as whitep4nth3r or white-p-4-nth-3-r if you use a screenreader. I am a Developer Evangelist for Contentful, and if you&#39;ve never heard of Contentful, it&#39;s a headless CMS with a REST API, GraphQL API, generous free community plan limits, and some great CLI tooling and other packages to help you build great stuff.</p><p class="post__p">Stephen Hawking said, &quot;I believe that life on earth is at an ever-increasing risk of being wiped out by a disaster, such as a sudden nuclear war or genetically engineered virus or other dangers.&quot; </p><p class="post__p">It&#39;s 2031. The apocalypse has arrived. The internet is running on a slow 3G connection, and display screens are broken. And this is what people are now Googling for — <b class="post__p--italic">how to build a fallout shelter</b>. And accessibility on the web is paramount to the survival of the human race. So, how do we prepare the web for the apocalypse of 2031? And why is this important now?</p><h2 class="post__h2">Let&#39;s first talk about accessibility for slow connections </h2><p class="post__p">There are almost two billion websites in the world, and 95% of those websites use JavaScript. And why is this bad news for the apocalypse? </p><p class="post__p">Let&#39;s take a look at the network tab of mobile.twitter.com. On the first request, it loads 37 javascript files worth 1.44 megabytes (compressed), 27 other files and nine image files. If we look at Lighthouse Dev Tools, we&#39;ll see that the time-to-interactive is 5.8 seconds and on the original trace of the page you see absolutely nothing on that first load. That&#39;s a lot of requests for JavaScript. And they&#39;re pretty expensive. It takes time to load — plus — it might fail.</p><p class="post__p">Here&#39;s a tweet from 2018 where &quot;our monitoring tells us that around one percent of requests for JavaScript on BuzzFeed time out, and that&#39;s around 13 million requests per month.&quot; That&#39;s 156 million failed requests for JavaScript each year for BuzzFeed alone. And that&#39;s potentially that many failed JavaScript requests on the web each year. (Assuming that all websites that use JS have the same traffic as BuzzFeed.)</p><p class="post__p">In the apocalypse, time-to-interactive is the difference between life and death. People need to know how to fight off those zombies as quickly as possible. </p><h3 class="post__h3">So how can we serve critical content faster? Do we abandon JavaScript?</h3><p class="post__p">The Jamstack has gone some way to try and solve this problem. The Jamstack is an architecture designed to make the web faster, more secure, and easier to scale. The key feature of the Jamstack is pre-rendering — where the entire front end is pre-built into highly optimised static pages and assets during a build process, and then enhanced with JavaScript. You might have heard of some of the most popular static site generators — Nuxt.js which uses Vue, Universal which uses Angular, and Next.js which uses React. But does modern Jamstack really solve this problem that we saw with Twitter?</p><p class="post__p">Here&#39;s my website and the network tab. It&#39;s built with Next.js and hosted on Vercel. On a normal load of this site in a browser, it loads more than 30 javascript files on the client. The first document to load is the HTML document. But why do we need all that JavaScript when the document has already loaded, and the Jamstack is telling us that our pages are pre-rendered and served statically?</p><p class="post__p">Let&#39;s do an experiment and let&#39;s turn off JavaScript on my website. Here it is. You can see the blocked requests in the network tab — JavaScript is disabled. But as you can see it still loads. It still works! Let&#39;s compare this site with JavaScript disabled and JavaScript enabled. The load time for the JavaScript disabled site is 259 milliseconds compared to 793 milliseconds. And that is on a normal speedy connection in the UK. What if we switched that to slow 3G, which the apocalypse will be using? The load times are incredibly different without JavaScript — 8.67 seconds and with JavaScript over 25 seconds. And the sizes of these pages are phenomenally different. 300 kilobytes for no JavaScript versus 1.4 megabytes using JavaScript. And that&#39;s compressed! The uncompressed sizes are even greater in difference. 3.6 megabytes to load the same site with JavaScript enabled.</p><p class="post__p">I did another experiment. To test this theory I wanted to see how low we can get the size of the page and the load time of the page without JavaScript. So the first thing is I recreated my blog article list fetching data at build time, statically generated on Next.js. 105kb loaded in just under 5 seconds. I created the same page but loaded all the data on the client without JavaScript. We obviously get no content because there&#39;s no JavaScript to load it — and it&#39;s only slightly less than 92.7kb, loads in just about five seconds again. So what if we could turn this into a static HTML page, no framework? HTML and CSS, no JavaScript, 100 kb in 4.71 seconds. </p><p class="post__p">So what&#39;s very interesting is that the statically generated Next.js version s actually very very comparable to my static HTML page — although my static HTML page does kind of win. So these Jamstack frameworks are pretty good. </p><p class="post__p">And so in terms of accessibility for slow connections, you can remove as much JavaScript as possible. Harness these Jamstack frameworks — but always use your judgment. I was still able to get the load time and the page weight down. So what I recommend for accessibility for slow connections: websites must work without JavaScript in the apocalypse. Decrease your load time, decrease your page weight. Because there is no progressive enhancement in the apocalypse. There&#39;s just simply no time.</p><h2 class="post__h2">Accessibility for broken screens</h2><p class="post__p">In the apocalypse, screens are broken. Everything is a wasteland. You don&#39;t see fancy website designs and animations anymore.</p><p class="post__p">There are almost two billion websites in the world, and 96% of those websites use CSS. And why is this bad news for the apocalypse? developers.google.com tells us that by default, CSS is treated as a render-blocking resource, meaning that the browser won&#39;t render any processed content until the CSS object model is constructed. This page recommends that we keep our CSS lean, deliver it as quickly as possible, and use media types and queries to unblock rendering. I say in the apocalypse let&#39;s not do that. Because in the apocalypse, time-to-interactive is the difference between life and death. </p><h3 class="post__h3">How can we serve critical content faster? Do we also abandon CSS?</h3><p class="post__p">You might have noticed I like tattoos. There&#39;s a big saying in the tattoo industry — <b class="post__p--italic">go big or go home</b>. In this case, I say go small or go die. So let&#39;s take that static HMTL page and make it as small as possible.</p><p class="post__p">Let&#39;s remove all CSS and class names. Every byte counts. Let&#39;s remove useless font imports. We can&#39;t see them on broken screens. Let&#39;s remove SVGs — we don&#39;t need those bytes. Let&#39;s turn these links into just really simple links that don&#39;t even need any aria labels or anything like that. And a little trick I found as well. I&#39;m going to replace the favicon with a sneaky zero kilobyte favicon. You might notice that in the network tab if you don&#39;t have a favicon on your site — the browser will throw a 404. I found a little way to make a base64 encoded string masquerade as a favicon. It&#39;s zero kilobytes. We just put that string into our head and there we go.</p><p class="post__p">So here&#39;s what my page looks like now. It actually looks pretty well laid out. We&#39;ve got links, we&#39;ve got sections, we&#39;ve got lists — and it actually looks very much like this very iconic website. It&#39;s lightweight. It&#39;s responsive. And it just works. So how does my no-CSS page compare to my HTML with CSS? So here we go — 100kb versus 4.6kb — and on slow 3G — 4.7 seconds versus 2.1. And if we look at the Lighthouse performance on web dev measure — everything is 100. That&#39;s going to rank really well in Google search results — and we&#39;re going to get to that later.</p><h3 class="post__h3">In 2021 WebAim surveyed one million home pages for accessibility</h3><p class="post__p">And across those one million home pages, over 51 million distinct errors were detected. That&#39;s an average of 51.4 errors per page. So what&#39;s going wrong? It seems pretty broken!

Here&#39;s a breakdown of the most common failures from this report, and let&#39;s look at these, one by one, and see what&#39;s going on. </p><h3 class="post__h3">Low contrast text</h3><p class="post__p">86.4% of home pages — and what&#39;s that caused by? CSS — and we already know we&#39;re going to abandon that, but let&#39;s have a look. I looked at some of the top 50 websites in the world to try and find these errors and they were present on quite a few of them. We&#39;ve got youtube.com, and this violation appears in the little copyright disclaimer on the sidebar. Now, you may think this doesn&#39;t matter really, but let&#39;s have a look. We&#39;ve got the HTML here, and the colour contrast error is caused by this CSS rule. The background and the foreground colours do not offer an accessible solution. Using one of my favorite colour contrast checker tools (actually built by an ex-colleague of mine) you can see here that on three out of four of the criteria that color contrast fails. If we remove CSS, we don&#39;t even need to think about this.</p><h3 class="post__h3">Next, missing alternative text for images</h3><p class="post__p">On 60.6 of homepages. What&#39;s this caused by? The HTML. Now you&#39;re going to see a running theme as I look through all of these failures and we&#39;re going to start with AliExpress. Now, on AliExpress there is a rotating carousel on their home page full of lots and lots of images and links. If we look at the accessibility tree in Chromium dev tools, you&#39;ll see that it&#39;s just an image, and you get no supporting information in that accessibility tree. Here&#39;s the HTML, div, div, div, a link with no information and an image with no alternative text. And what&#39;s very, very interesting is that AliExpress has chosen to hide the whole carousel for screen readers, maybe with the assumption that if it&#39;s hidden — it doesn&#39;t matter. And I don&#39;t agree with that. </p><p class="post__p">accessibilitydeveloperguide.com says, &quot;Whenever you think about hiding something from any audience, better ask yourself whether this is really a good solution. Most of the time it&#39;s better to create a solution that works the same way for everybody, which does not need any shaky aria.&quot; Let&#39;s look at the fix for this violation. On AliExpress, add an alt tag — &quot;This is a great image description,&quot; and we&#39;ll get to this later — but if you want to avoid cumulative layout shift, add a height and width on the image as well.</p><h3 class="post__h3">Missing form input labels </h3><p class="post__p">On 54.4 of home pages, again caused by incorrect HTML. I spent a long time trying to find an example of this, and what&#39;s interesting, is that if a placeholder is present in the input field, screenreaders and accessibility trees kind of get around that and give you some information. However, I did put a call out on Twitter to ask if anybody had any examples of HTML forms without labels and placeholders. A wonderful member of the community, Alex, said he had a gimmicky one on his 404 page to do redirects. And this was like gold dust to me. This was an absolutely abysmal experience for screenreaders, and let&#39;s look at what&#39;s happening.</p><p class="post__p">It looks pretty cool. There&#39;s some ASCII art. The instructions are to press enter to continue, which redirects you to Alex&#39;s home page. Here is what the HTML looks like. We have a form, we have an input field. We have no labels, we have no placeholders, and in the accessibility tree we have no information. And furthermore, when you focus on this element with VoiceOver on Mac, it reads out to you — &quot;edit text blank main.&quot; It&#39;s not really what you&#39;re expecting when you land on a 404. So my question is, does this page really need a form? Is this HTML giving meaning to what needs to be done on that page? Furthermore, a fun little easter egg — if you focus on the ASCII art in its current state with VoiceOver, it reads this out to you: &quot;<b class="post__p--italic">underscore new line forward slashs pace underscore space back slash space undercore underscore underscore underscore etc</b>,&quot; and there is a good fix for that, where you can use aria labels and roles to allow the screenreader to know what that is. You add role=image and an aria label. And when you focus on that now with a screen reader, you getmore contextual information about what&#39;s on the page. It&#39;s a pretty fancy page for a 404. I think I&#39;d prefer my 404s to be functional, especially when we&#39;re in an apocalypse.</p><h3 class="post__h3">Now for a moment let&#39;s talk about placeholders </h3><p class="post__p">As I mentioned earlier, placeholders actually are not labels. w3schools.com says, &quot;The placeholder attribute specifies a short hint that describes the expected value of an input field e.g a sample value or a short description of the expected format.&quot;</p><p class="post__p">Let&#39;s go back to AliExpress. When you refresh the page on AliExpress you get a new contextual placeholder. It told me to search for true love, Starbucks, fancy underwear and car accessories. In the accessibility tree, the placeholder says &quot;true love&quot; — which is fair enough. But there is no label on the form. If I focus on that input with a screen reader, I don&#39;t actually know what that form is for — especially when the placeholder is changing on every load of the page. Without a label that placeholder is meaningless.</p><p class="post__p">So what does a good form input look like? We have a label and we tell the HTML, the DOM, what input that label is for using the for and the id attributes. We give the input a contextual placeholder — here it&#39;s first name — and that is going to really help you out when you can&#39;t see that screen.</p><h3 class="post__h3">Let&#39;s go on to empty links </h3><p class="post__p">51.3 of home pages — again caused by HTML. And we go back to AliExpress. In the footer of AliExpress there are a set of social links. They are empty. They are full of non-breaking spaces — and the accessibility tree tells us no information, apart from &quot;space&quot;. When we focus on those links as a screen reader user — as someone without a working display — I&#39;m going to ask myself what will happen if I click on this link?</p><p class="post__p">And it&#39;s not just empty links that are problematic. Let&#39;s take a look at the basket icon on AliExpress. Again, there is a text of &quot;zero&quot; inside that, when I&#39;m hovering over that with a screen reader — when I&#39;m focused on it. I&#39;ve gone through the input field that says &quot;true love,&quot; I go to the basket icon and it just reads out &quot;zero&quot;. Here&#39;s the HTML. A link, an icon and &quot;zero&quot;. And when I focus on it with a screen reader, it reads out to me: &quot;link, zero.&quot; I don&#39;t know where that link is going — I&#39;m not going to want to click on it. I&#39;m going to be asking myself again — what even is this?</p><p class="post__p">Let&#39;s look at the fix. Add an aria label onto the link. Make it descriptive. How about, &quot;You have zero items in your basket. Go to basket.&quot; Tell the user what they&#39;re going to do if they click on that link.</p><h3 class="post__h3">Missing document language attribute</h3><p class="post__p">28.9 of home pages, again caused by the very opening tag of your HTML. accessibilitydeveloperguide.com says, &quot;In the example of a screen reader, the synthesiser needs to know which language the content is, in order to pronounce the output correctly. Otherwise it will be hard to understand, even if you know the other language.&quot;</p><p class="post__p">We go back to AliExpress and here you can see Axe Dev tools, a great free Chromium extension to test accessibility. There is no language attribute on the HTML, and you can see here AliExpress have prioritised merging the Facebook and OpenGraph schema into their HTML before they&#39;ve thought about language.</p><p class="post__p">The fix? Very simple. Add lang equals whatever your language is on the HTML. </p><h3 class="post__h3">Finally, we come to empty buttons </h3><p class="post__p">26.9 of home pages — again caused by HTML, but in this case — it&#39;s also caused by CSS (which we&#39;re going to abandon). We go to a product page of AliExpress and there are two buttons on this page that presumably increment and decrement the amount of items you would like to add to your basket. When using VoiceOver, when you focus on this element it just reads out &quot;button&quot; — no one is going to click that if they don&#39;t know what it&#39;s for. Here is the HTML. It&#39;s a button element with an icon element inside it. So how is that plus and minus added? With CSS pseudo elements. And what&#39;s really interesting is this character is not actually supported by a lot of browsers, and also my VSCode. I presume they mean a plus, but literally in the CSS inspector the character is unrecognised. And I had to bring the WAT duck out for that.</p><p class="post__p">The fix? Give a fallback to your button. Put a plus in there. Give it an aria label that tells the users what they&#39;re going to do when they click on that button. Give it some meaning.</p><p class="post__p">So when we have a look at all of these most common failures, they&#39;re caused by incorrect CSS and HTML. They are not built for accessibility. </p><h3 class="post__h3">So what do we do for accessibility for broken screens in the apocalypse?</h3><p class="post__p">Don&#39;t fake content with css. Put it there in the DOM. Use semantic HTML. Use forms where it&#39;s appropriate. Don&#39;t break the web. Because if the web is unusable in the apocalypse — everyone dies.</p><p class="post__p">So we&#39;re talking about the apocalypse. 2031 is 10 years off — but why is all of this important now?</p><h2 class="post__h2">Let&#39;s quickly recap </h2><p class="post__p">How do we prepare the web for the apocalypse of 2031? we start with semantic HTML. We use minimal CSS, and we use as little JavaScript as possible. And why is this important now?</p><h3 class="post__h3">Worldwide, the internet is slow</h3><p class="post__p">worldpopulationreview.com did a survey on internet speeds by country in 2021 and the average speed in the world is 55.13 megabits per second. If we look at the code from Chromium Dev Tools, the slow 3G simulated connection is 51.2 megabits per second. These are pretty much the same. So all of the results you&#39;ve seen when I had slow 3G in my dev tools — loading that page with JavaScript — the results are going to be very similar for a lot of people around the world. They are not going to survive if pages take that long to load. </p><h3 class="post__h3">Furthermore, globally at least 2.2 billion people have a near or distance vision impairment</h3><p class="post__p">I&#39;m one of them. I have really bad eyes — I can&#39;t see. Those of you who have seen me live-streaming know that I miss placeholders all the time when they are really low contrast. And also, not all users of screen readers have vision impairments. Here&#39;s a survey by WebAim in  2017, and it shows here that not everyone is blind who answered this survey. Some people have cognitive disabilities, some people are deaf, some people have motor disabilities. So it&#39;s not just blind people who use these — there are a lot more people you&#39;re affecting if you don&#39;t build accessible websites.</p><h3 class="post__h3">And lastly, as of this month, Google now uses Core Web Vitals as a ranking factor in search results</h3><p class="post__p">So what are the Core Web Vitals? We&#39;ve got the largest contentful paint, which means how quickly does the page render. We&#39;ve got the first input delay — how quickly does the page respond to user input. And we&#39;ve got cumulative layout shift — how stable is the page layout. So in order to make the most out of search rankings and Google Core Web Vitals, what do we need to do?</p><p class="post__p">We need to get our content to our users as fast as possible. We need to make our web applications usable as soon as possible, and we need to make our web applications as stable as possible — or we risk being forgotten in the apocalypse.</p><p class="post__p">Billions of people use the internet on a slow connection, and millions of people use the internet via a screen reader. People won&#39;t find your website if it&#39;s not performant or accessible.</p><p class="post__p">The apocalypse of 2031 might not happen but if it does? Hopefully, with this information, we&#39;ll be prepared.</p><p class="post__p">My name is Salma or whitep4nth3r. I encourage developers to build stuff, learn things and love what they do. You can find me on all of these social platforms as whitep4nth3r. Thank you very much I&#39;ve had a lovely time!</p>
  ]]></content:encoded>
        </item>
        
    </channel>
    </rss>
  