Matt Wing I build sites with WordPress, React, Django — honestly whatever's lying around at the time. Sometimes I post here too! 2024-09-11T02:00:59Z https://wingmatt.dev Matt Wing Jackson Fire Info 2020-09-09T00:00:00Z https://wingmatt.dev/projects/jackson-fire-info/ <p><em>Jackson County's lo-fi firewatch. Created during Southern Oregon's September 2020 fires.</em></p> <p>In September of 2020 I was evacuated from my home, along with the rest of my neighborhood, due to the approach of the Almeda fire. Within a couple hours of the fire’s first spark, I was driving to a friend’s house with my dog, cat, and my most precious belongings.</p> <p>As the days passed, everyone in my valley were stuck with the same two questions:</p> <ul> <li>Will we need to evacuate again?</li> <li>When can we return to our homes?</li> </ul> <p>I collected a list of Twitter accounts that had timely, well-sourced situation updates, and bookmarked a frequently-updated GIS map of evacuation zones from the county.</p> <p>Then I realized I could patch it all together on a webpage and stop switching between the same 6 bookmarks every half an hour. I deployed a single link tree-type Github Page with a couple dozen lines of HTML and a few lines of CSS.</p> <p>After that, I created a second page for those unconcerned about data caps: The actual evacuation map was embedded at the top, with the most recent news tweets listed below it.</p> <p>Built with <strong>HTML</strong>, <strong>Jekyll</strong>, <strong>Twitter API</strong>, and <strong>GIS APIs</strong>. It was an interesting time.</p> WooCommerce Global Variation Templates 2021-01-01T00:00:00Z https://wingmatt.dev/projects/global-variations-plugin-design/ <p><em>Interface and architecture for a time-saving plugin for photography print sellers.</em></p> <p>I designed the admin interface, and assisted with architectural design, for a custom plugin that managed price lists for standardized sets of product attributes.</p> <p>This allows shop managers to update price lists for all similar product attribute combinations at the same time. For example, if a photographer sells prints of their photos, they can sell every photo print in a certain size for the same price, and update that pricing for all photos with one click.</p> <p>Built with <strong>PHP</strong>, <strong>JavaScript</strong>, and <strong>CSS</strong> using <strong>WordPress</strong> plugin development standards.</p> Subscription Streaming Platform 2021-05-05T00:00:00Z https://wingmatt.dev/projects/streaming-platform/ <p><em>Nonprofit streaming platform to deliver live entertainment amidst a pandemic.</em></p> <p>I built a streaming subscription platform so audience members to safely enjoy live performances and support a local nonprofit theatre.</p> <p>The NextJS web app showed a Wowza livestream for anyone logged in with an active subscription.</p> <p>Auth0 handled account management, and retained user's Stripe subscription information. Stripe webhooks updated subscription statuses in real-time so audience members immediately received access to the live stream. Audience members could use Stripe's built-in subscription management page to update cards on file or cancel their subscription.</p> <p>The theatre's marketing website, built on WordPress, had custom fields added so website admins could add custom Call to Action buttons underneath the livestream window. This feature, which used WPGraphQL to fetch the data, allowed the theatre to add custom tip buttons and other calls to action depending on the show.</p> <p>Built with <strong>Next.JS</strong>, <strong>Auth0</strong>, <strong>Stripe Subscription API</strong>, <strong>GraphQL</strong>, and <strong>Wowza Streaming</strong></p> Pantry & Grocery List Manager 2021-11-11T00:00:00Z https://wingmatt.dev/projects/pantry-manager/ <p><em>Your kitchen's inventory screen. Add ingredients to your shopping list with a click.</em></p> <p>I got tired of forgetting what’s in the pantry, so I made a whole app about it.</p> <p><a href="https://kitchensyn.cc/">Kitchen Syncc</a> keeps track of what's in your pantry and on your shopping list. When something is checked off of the shopping list, it gets moved to the pantry, and when something gets used up in the pantry, it gets moved to the shopping list (or just removed for one-off ingredients).</p> <p>I also wrote a recipe parser that takes a recipe URL and spits out the list of ingredients in the recipe. Integration with the shopping list Coming Soon™!</p> <p>Built with <strong>React</strong>, <strong>Next.JS</strong>, <strong>GraphQL</strong>, and <strong>AWS Amplify</strong>. Free to sign up if you want to check it out!</p> Blatherbrush 2023-11-15T00:00:00Z https://wingmatt.dev/projects/blatherbrush/ <p><em>Infrastructure, architecture, design and development for a social image generation experience!</em></p> <p>My friend hosted a casual art exhibition, and I wanted to participate but I have all the artistic ability of a half-eaten ham sandwich.</p> <p>So I built this thing, which lets folks toss words into a secret, partially filled-out prompt without knowing the context. Once all the blanks are filled in, the prompt gets sent over to OepnAI's DALL-E while participants marvel at the prompt they created together. A gloriously chaotic image is then delivered at last for all to appreciate, or at least contemplate.</p> <p><a href="https://blatherbrush.com/">Take a look here!</a></p> <p>Heads up that you need an account to host a game, and each account only has a few credits for free before you have to pay for more. This is required to cover the cost of generating images each round with DALL-E. Blatherbrush is under active development, so play at your own risk!</p> <p>Built with <strong>Next.JS</strong>, <strong>React</strong>, <strong>TypeScript</strong>, <strong>Postgresql</strong>, <strong>OpenAI</strong>, and <strong>Stripe</strong>.</p> Posts 2024-09-11T02:00:59Z https://wingmatt.dev/posts/ <ul class="tdbc-column-container"><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/" class="tdbc-card__title">Parsing Schema Data with node-html-parser</a> <p>How to get useful data from pasting a URL</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/posts/most-effecient-self-taught-learning/" class="tdbc-card__title">The Most Efficient Self-Taught Learning is Routine</a> <p>Make the most of your time by making time</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/posts/managing-side-projects/" class="tdbc-card__title">Managing Side Projects to Account for Life</a> <p>How to make the most of projects that don't get finished</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/posts/background-blend-mode-safari/" class="tdbc-card__title">Background-blend-mode and Safari</a> <p>AKA why your thing with a background looks wrong on your iPhone</p> </div> </li></ul> Background-blend-mode and Safari 2024-09-11T02:00:59Z https://wingmatt.dev/posts/background-blend-mode-safari/ <p>I've been dabbling with <a href="https://www.toptal.com/designers/subtlepatterns/">Subtle Patterns</a> for years to break up big swaths of negative space in my website designs.</p> <p>Everything was rosy and magical... <em>until now.</em></p> <p>I used the Funky Lines subtle pattern for <a href="https://blatherbrush.com/">Blatherbrush</a>, which has a color scheme based on your device's light mode preference.</p> <p>To handle dark mode, I set up a good ol' <code>background-blend-mode: multiply</code> so the white parts of the pattern would become dark gray in dark mode. It worked on my machine so I deployed it and continued on my merry way.</p> <p>It wasn't until I popped the site open in my iPhone that I realized everything was actually terrible. Check this out:</p> <p class="codepen" data-height="300" data-default-tab="css,result" data-slug-hash="wvOKYPL" data-user="wingmatt" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/wingmatt/pen/wvOKYPL"> Uh Oh: Background-Blend-Mode + Big Repeating BG Image in Safari/Webkit</a> by Matt Wing (<a href="https://codepen.io/wingmatt">@wingmatt</a>) on <a href="https://codepen.io/">CodePen</a>.</span> </p> <p>If you're on most browsers, that's blue... But on iOS Safari, it's white. It will be white if you open the embedded codepen in another tab, too. In these cases, the <code>background-blend-mode</code> rule doesn't affect the <code>background-image</code>.</p> <p>Reproducing this quite the adventure as I couldn't figure out what ruleset was causing this. After barking up several wrong trees, I eventually learned that Safari has a hard time with <code>background-blend-mode</code> when the background in question has a repeating background tile over 256px in either dimension. Thanks to Damon and the Cassidoo Discord for helping me figure out what was actually going on here!</p> <p>Take a look at this example with the same CSS, just with the background tile's size shrunk down a bit. This should work as expected <em><strong>everywhere</strong></em>:</p> <p class="codepen" data-height="300" data-default-tab="css,result" data-slug-hash="BaEzpKz" data-user="wingmatt" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/wingmatt/pen/BaEzpKz"> All Good: Background-Blend-Mode + 256px Repeating BG Image in Safari/Webkit</a> by Matt Wing (<a href="https://codepen.io/wingmatt">@wingmatt</a>) on <a href="https://codepen.io/">CodePen</a>.</span> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p><a href="https://bugs.webkit.org/show_bug.cgi?id=259081">Here's the Webkit bug tracker for this issue in case you want to see it &quot;fixed&quot; in &quot;real time&quot;!</a></p> <p>I'm hoping this post becomes obsolete soon and you, dear reader, are baffled at the similarity between the examples above despite my claims. But as of March 2024, you can't use images over 256px in size for your repeating <code>background-image</code> tile if you're gonna be doing a <code>background-blend-mode</code> with them!</p> Managing Side Projects to Account for Life 2024-09-11T02:00:59Z https://wingmatt.dev/posts/managing-side-projects/ <p>Everyone loves a good side project! It's your chance to actually work with the shiny new technology you've read 12 tutorials about, to add some real code to your resume to show your proficiency with the shiniest new technologies, and re-learn just how difficult it can be to step outside your coding comfort zone.</p> <p>It's common for coders to have a big barrel full of abandoned side projects. Each one is a milestone in a way. I personally have some concepts I've started from scratch on 3 or 4 times.</p> <p>Much like the first few levels in World of Warcraft, the first few hours of a new project are full of excitement. Anything involving a package manager gives you those first moments where you watch things fly by in the console and feel like a master hacker as you sip on your beverage of choice. Then you plug in the example code and see it work like it did in the example! Just like that! Wow!</p> <p>Eventually the joy ride ends as you come to terms with the vast gulf that spans between where your project is now, and its completed state. Invoking the magic words &quot;mimimum viable product&quot; can only do so much to reduce the scope of your dream when you're hoping to do something amazing. The rational part of your brain knows that spending an hour a day on this will get you to the finish line eventually.</p> <p>That would be all we need for motivation if humans ran entirely on rational thought! Alas, most side projects do eventually fizzle out as we tire of using our spare time to bang our head against the wall in frustration. Even with a mild concussion, the dream that sparked the side project's creation never dies.</p> <p>When you're ready to dive back into things, it's easy to feel overwhelmed by re-acquainting yourself with your old code. It's like loading up a game you almost beat six months ago and trying to remember what you were supposed to be doing.</p> <p>How do we make the most of our fizzled-out projects to make the most of these bursts of renewed motivation? Here are a few suggestions from someone who has plenty of stalled projects to experiment with:</p> <h2 id="document-the-heck-out-of-them" tabindex="-1">Document the heck out of them<a class="tdbc-anchor" href="https://wingmatt.dev/posts/managing-side-projects/#document-the-heck-out-of-them" aria-hidden="true">#</a></h2> <p>Write up rough design documents. Map out what classes or functions you think you'll need to make things happen. You could even write tests for them that roughly describe how you expect them to work. These don't need to be detailed or fancy - they're just for you.</p> <p>This does a few things for you:</p> <ul> <li>Just like in &quot;real&quot; software projects, you'll be able to reconsider architecture and design decisions before you're halfway through implementing your first guess at how things should be. It's so much easier to draw a new diagram than it is to refactor your app from the ground up.</li> <li>If (when?) your motivation for working on the project dries up, you can take a break and easily jump back in when you're ready. If you kept track of your progress too, you can just jump back in from where you left off.</li> <li>If you want to start from scratch, your planning docs won't be tightly tied to specific code, so you can use a fancy new framework instead of the <em>old</em> framework you were excited about the last time you started things up.</li> </ul> <p>I just did this in the aftermath of my most recent project hiatus. I wrote out a rough outline of the type of data the most complex objects would need to hold onto, some pseudocode for the most complicated functions, and a few basic unit tests. I found that it's easier for me to come up with unit testing ideas on paper than in the IDE - maybe because I don't get hung up on getting the syntax perfect as I'm trying to come up with them. What will you find easier to plan on paper? Only one way to find out!</p> <h2 id="use-what-youve-learned-for-next-time" tabindex="-1">Use what you've learned for next time<a class="tdbc-anchor" href="https://wingmatt.dev/posts/managing-side-projects/#use-what-youve-learned-for-next-time" aria-hidden="true">#</a></h2> <p>So... you didn't write up any design documents. That's okay! You moved fast, broke things, and got out of there. We've all done the same. Chances are good that you got stuck on a hard problem. In web development, running into one of those usually means one of three things:</p> <ul> <li>The real problem is that you don't fully understand what's going on, which makes a small problem seem like a huge problem.</li> <li>You're going to learn that someone solved that problem for you in a library - thanks, open source software!</li> <li>You are a pioneer staking your claim on something that, truly, no one has done before. If you solve this, you can become the hero in the last scenario.</li> </ul> <p>The first two situations are pretty common, at least in my experience. I've come back to projects before and found that the biggest problems had melted away.</p> <p>The third scenario is why people say things like &quot;the things worth doing in life are hard.&quot; If you run into a truly unique problem and keep puttering away at it, you'll become a master of that problem's domain before you know it.</p> <p>No matter what stopped you in your tracks, your experience taught you something that no tutorial was going to show you. Maybe you learned the limits of the shiny new technology that looked so effortless in the demos. Maybe you found the edges of a library's helpfulness, or a bug you can report. No matter what happened, you learned something from it, so the next time you start a side project, you'll be that much more knowledgeable about how to make it work.</p> <p>Like I said before, these are the practices I'm working on for myself in order to make the most of my personal programming time. What do you do to improve your chances of taking a side project to the finish line?</p> The Most Efficient Self-Taught Learning is Routine 2024-09-11T02:00:59Z https://wingmatt.dev/posts/most-effecient-self-taught-learning/ <p>I love to learn, but I hate finding time for it.</p> <p>We all only have 24 hours in a day. Between work, sleep, and everything else, you're probably juggling a lot already! The challenge for anyone doing self-taught learning is finding out how much time you can set aside <strong>consistently</strong> — fitting it into your regular routine, the cadence of your daily life.</p> <p>Taking two hours to dive into something every other Saturday is a great supplement to your regular cadence. But if that's all you can do, you're gonna spend a good chunk of each time block just trying to remember where you left off.</p> <p>If you can set aside even a half hour every weekday, or even just three days a week, you will feel the progress. And any big blocks of time you <em>can</em> snag will be infinitely more productive!</p> <p>Fair warning that, best case, you'll be taking this time from your solo free time. Yes, that still sucks. But it's still better than depriving yourself of sleep, or neglecting your relationships.</p> <p>It's common for people trying to learn new skills to be drained by the time they get time for it. I found a way around this in my schedule by doing it first thing in the morning, before I hop into any other work. This works for me because I'm already awake a couple hours before my job typically starts.</p> <p>Everyone's schedule is different. As long as you're making the time and protecting it wherever it lands on your schedule, you're moving forward along your path. It doesn't have to be a huge long session each time, and it doesn't even have to be every day. It just needs to be <strong>consistent</strong>!</p> Parsing Schema Data with node-html-parser 2024-09-11T02:00:59Z https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/ <p>Did you know that there's a whole JSON object schema for providing machine-readable information about the contents of your website? Google uses the data in these objects to fill out search results and build rich snippets.</p> <p>Here's a secret - it can power other stuff too. For example, I'm building a Node JS web app that includes the ability to plug a URL in and get a list of that recipe's ingredients.</p> <p>Want to start parsing data yourself? Read on!</p> <h2 id="challenges" tabindex="-1">Challenges<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#challenges" aria-hidden="true">#</a></h2> <ul> <li>Fetching the raw HTML</li> <li>Making the Raw HTML Parse-able</li> <li>Finding the right Schema object out of all of the ones on the page</li> <li>Grabbing the right data out of that schema object</li> </ul> <h2 id="fetching-the-raw-html" tabindex="-1">Fetching the raw HTML<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#fetching-the-raw-html" aria-hidden="true">#</a></h2> <p>First things first — we want to be able to fetch the HTML code of whatever link we end up pasting into our app.</p> <p>There are a lot of ways to do this in Node JS. For this tutorial, we'll be using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">native JavaScript <code>fetch</code> API</a>.</p> <p>With that in mind, here's how to make <code>fetch</code> happen:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token comment">// Use an async function so we can wait for the fetch to complete</span><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getHtmlStringFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// responseHtml is a huge string containing the entire web page HTML.</span><br /> <span class="token comment">// In the next section, we'll process it into something we can work with</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> <h2 id="making-the-raw-html-parse-able" tabindex="-1">Making the Raw HTML Parse-able<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#making-the-raw-html-parse-able" aria-hidden="true">#</a></h2> <p>When we first fetch a URL and grab the response body, it's one enormous text string. There's HTML in there, but we can't really work with it yet. We need to plug this string into an HTML parser that will let us use DOM selectors to pick out the useful bits.</p> <p>node-html-parser is my personal choice for this. It lets us use all the usual JavaScript DOM selector methods, and it's pretty fast too. Add it to your project with this terminal command:</p> <p><code>yarn add node-html-parser</code></p> <p>Then import the parse command from the package into the JS file where you'll be using it:</p> <p><code>import { parse } from &quot;node-html-parser&quot;;</code></p> <p>Now we can take the response body string, plug it into our parser, and get to the real fun:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getHtmlDocumentFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// parse the HTML string into a DOM-like object we can navigate</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<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></code></pre> <p>That's all we need to get the HTML into something we can sift through! The returned object has all the same methods as a typical document object, such as querySelector, getElementByID, and so on.</p> <p>So, how do we work it to find the structured data we're looking for?</p> <h2 id="finding-the-right-schema-objects" tabindex="-1">Finding the right Schema object(s)<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#finding-the-right-schema-objects" aria-hidden="true">#</a></h2> <p>The nice thing about working with structured data is that you can make some assumptions about the data you're processing, because it <em>has</em> to be structured in a way that web crawlers can understand to be useful.</p> <p>The structured data Schema objects we're looking for are going to be found within <code>ld+json</code> script tags. Now that we've DOMified the HTML, we can run queries on it like this:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getSchemaNodeListFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Create a NodeList of elements containing the page's structured data JSON. So close to useful!</span><br /> <span class="token keyword">const</span> structuredData <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">'script[type="application/ld+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 /><span class="token punctuation">}</span></code></pre> <p>That will give us a NodeList of all the matching elements. That's close to perfect, but it's not a true array and could give us errors if we try to treat it like one (which we will soon). So let's turn it into an array:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getSchemaArrayFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Create an ARRAY of elements containing the page's structured data JSON. Just one more step!</span><br /> <span class="token keyword">const</span> structuredData <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'script[type="application/ld+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 punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre> <p>Now we have an array of structured data nodes. In a way, we're back to square one with data that is <em>so close</em> to being useful. To make it useful, we need to grab the innerHTML of each node, which will come out as a big string. Then we can parse that into ✨real JSON!✨</p> <pre class="language-jsx"><code class="language-jsx"><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getJsonFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> structuredData <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'script[type="application/ld+json"]'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Get an array containing the contents of each structured data element on the page. This is the ✨useful stuff✨</span><br /> <span class="token keyword">const</span> structuredDataJson <span class="token operator">=</span> structuredData<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">node</span><span class="token punctuation">)</span> <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>node<span class="token punctuation">.</span>innerHTML<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">flat</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// We also flatten the array with .flat() to handle how some sites structure their schema data. See epilogue for more info</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> <p>Whoa, look at us. We've got the real, actual JSON object now. If you log structuredDataJson to your console, you'll see an array of structured data objects! Huzzah 🎉</p> <p>But of course, we're not done yet! There is likely to be a ton of data you don't need in this array, in addition to whatever you're actually looking for.</p> <h2 id="grabbing-the-right-data-out-of-that-schema-object" tabindex="-1">Grabbing the right data out of that schema object<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#grabbing-the-right-data-out-of-that-schema-object" aria-hidden="true">#</a></h2> <p>You're looking for some sort of specific data out of these objects. In my case, I'm looking for the list of <a href="https://schema.org/recipeIngredient">ingredients</a> within the <a href="https://schema.org/Recipe">Recipe object</a>. So, now that we have actual JSON, we can view certain properties and use it to whittle our array down to a single, useful, piece of data:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getIngredientsFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> structuredData <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'script[type="application/ld+json"]'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> structuredDataJson <span class="token operator">=</span> structuredData<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">node</span><span class="token punctuation">)</span> <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>node<span class="token punctuation">.</span>innerHTML<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">flat</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Look for a Recipe schema and return its ingredients if it exists </span><br /> <span class="token keyword">const</span> recipeData <span class="token operator">=</span> structuredDataJson<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">schema</span><span class="token punctuation">)</span> <span class="token operator">=></span> schema<span class="token punctuation">[</span><span class="token string">"@type"</span><span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token string">"Recipe"</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>recipeData<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> recipeData<span class="token punctuation">.</span>recipeIngredient<br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">return</span> <span class="token keyword">null</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> <p>If one of the structured data objects is for a Recipe, we'll get the array of ingredients we're looking for. If not, the function will return <code>null</code> so we know it failed to find what we were looking for.</p> <p>That's it! We've parsed the HTML into JSON into the actual thing we need 🎉</p> <h2 id="conclusion" tabindex="-1">Conclusion<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#conclusion" aria-hidden="true">#</a></h2> <p>At this point, you have a function that takes a URL and returns an array of whatever information you're looking for. This general process can be used to do a whole lot of interesting stuff depending on what you're grabbing. <a href="https://codesandbox.io/s/nextjs-recipe-schema-data-parsing-example-tblx7">Here's an example I put together to grab the ingredients within a recipe page.</a></p> <p><a href="https://schema.org/docs/schemas.html">Here are some of the most common schemas out there</a> for inspiration. In my case, I'm parsing recipe ingredients so I can see if they're in my pantry, and add them to my shopping list if they're not.</p> <p>How about you? If you end up using this process to parse website data in your web app, let me know what you're doing!</p> <h2 id="epilogue-handling-edge-cases-with-the-flat-method" tabindex="-1">Epilogue: Handling Edge Cases with the flat() method<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#epilogue-handling-edge-cases-with-the-flat-method" aria-hidden="true">#</a></h2> <p>As mentioned earlier, structured data has to be readable by web crawlers to be useful, so we can make some assumptions about what it will look like. Still, we're ultimately trusting people to build their websites according to a certain convention, so you still might run into some issues across different websites and pages.</p> <p>When I was testing my recipe parser, I ran into a few websites that structured their data in non-standard ways, which caused some trouble early on. The most common issue I found was that some sites would wrap their schema JSON within an array. This prevented my array.find() method from finding any of the data within the nested array.</p> <p>In my production code, I handle this by flattening the parsed JSON to remove any nested arrays before I start looking for specific data. Here's what that looks like with the example code we've been using:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getHtmlFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> structuredData <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'script[type="application/ld+json"]'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Adding .flat() to the line below handles the most common edge cases I've found so far! </span><br /> <span class="token keyword">const</span> structuredDataJson <span class="token operator">=</span> structuredData<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">node</span><span class="token punctuation">)</span> <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>node<span class="token punctuation">.</span>innerHTML<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">flat</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> recipeData <span class="token operator">=</span> structuredDataJson<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">schema</span><span class="token punctuation">)</span> <span class="token operator">=></span> schema<span class="token punctuation">[</span><span class="token string">"@type"</span><span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token string">"Recipe"</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>recipeData<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> recipeData<span class="token punctuation">.</span>recipeIngredient<br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">return</span> <span class="token keyword">null</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> Projects 2024-09-11T02:00:59Z https://wingmatt.dev/projects/ <ul class="tdbc-column-container"><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/blatherbrush/" class="tdbc-card__title">Blatherbrush</a> <p>Multiplayer AI art image generation game</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/pantry-manager/" class="tdbc-card__title">Pantry & Grocery List Manager</a> <p>Your kitchen's inventory screen. Add ingredients to your shopping list with a click.</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/streaming-platform/" class="tdbc-card__title">Subscription Streaming Platform</a> <p>Nonprofit streaming platform to deliver live entertainment amidst a pandemic.</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/global-variations-plugin-design/" class="tdbc-card__title">WooCommerce Global Variation Templates</a> <p>Interface and architecture for a time-saving plugin for photography print sellers.</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/jackson-fire-info/" class="tdbc-card__title">Jackson Fire Info</a> <p>Jackson County's lo-fi firewatch. Created during Southern Oregon's September 2020 fires. </p> </div> </li></ul>
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Matt Wing</title>
<subtitle>I build sites with WordPress, React, Django — honestly whatever's lying around at the time. Sometimes I post here too!</subtitle>
<link href="https://wingmatt.dev/feed/" rel="self"/>
<link href="https://wingmatt.dev"/>
<updated>2024-09-11T02:00:59Z</updated>
<id>https://wingmatt.dev</id>
<author>
<name>Matt Wing</name>
</author>
<entry>
<title>Jackson Fire Info</title>
<link href="https://wingmatt.dev/projects/jackson-fire-info/"/>
<updated>2020-09-09T00:00:00Z</updated>
<id>https://wingmatt.dev/projects/jackson-fire-info/</id>
<content type="html"><p><em>Jackson County's lo-fi firewatch. Created during Southern Oregon's September 2020 fires.</em></p> <p>In September of 2020 I was evacuated from my home, along with the rest of my neighborhood, due to the approach of the Almeda fire. Within a couple hours of the fire’s first spark, I was driving to a friend’s house with my dog, cat, and my most precious belongings.</p> <p>As the days passed, everyone in my valley were stuck with the same two questions:</p> <ul> <li>Will we need to evacuate again?</li> <li>When can we return to our homes?</li> </ul> <p>I collected a list of Twitter accounts that had timely, well-sourced situation updates, and bookmarked a frequently-updated GIS map of evacuation zones from the county.</p> <p>Then I realized I could patch it all together on a webpage and stop switching between the same 6 bookmarks every half an hour. I deployed a single link tree-type Github Page with a couple dozen lines of HTML and a few lines of CSS.</p> <p>After that, I created a second page for those unconcerned about data caps: The actual evacuation map was embedded at the top, with the most recent news tweets listed below it.</p> <p>Built with <strong>HTML</strong>, <strong>Jekyll</strong>, <strong>Twitter API</strong>, and <strong>GIS APIs</strong>. It was an interesting time.</p> </content>
</entry>
<entry>
<title>WooCommerce Global Variation Templates</title>
<link href="https://wingmatt.dev/projects/global-variations-plugin-design/"/>
<updated>2021-01-01T00:00:00Z</updated>
<id>https://wingmatt.dev/projects/global-variations-plugin-design/</id>
<content type="html"><p><em>Interface and architecture for a time-saving plugin for photography print sellers.</em></p> <p>I designed the admin interface, and assisted with architectural design, for a custom plugin that managed price lists for standardized sets of product attributes.</p> <p>This allows shop managers to update price lists for all similar product attribute combinations at the same time. For example, if a photographer sells prints of their photos, they can sell every photo print in a certain size for the same price, and update that pricing for all photos with one click.</p> <p>Built with <strong>PHP</strong>, <strong>JavaScript</strong>, and <strong>CSS</strong> using <strong>WordPress</strong> plugin development standards.</p> </content>
</entry>
<entry>
<title>Subscription Streaming Platform</title>
<link href="https://wingmatt.dev/projects/streaming-platform/"/>
<updated>2021-05-05T00:00:00Z</updated>
<id>https://wingmatt.dev/projects/streaming-platform/</id>
<content type="html"><p><em>Nonprofit streaming platform to deliver live entertainment amidst a pandemic.</em></p> <p>I built a streaming subscription platform so audience members to safely enjoy live performances and support a local nonprofit theatre.</p> <p>The NextJS web app showed a Wowza livestream for anyone logged in with an active subscription.</p> <p>Auth0 handled account management, and retained user's Stripe subscription information. Stripe webhooks updated subscription statuses in real-time so audience members immediately received access to the live stream. Audience members could use Stripe's built-in subscription management page to update cards on file or cancel their subscription.</p> <p>The theatre's marketing website, built on WordPress, had custom fields added so website admins could add custom Call to Action buttons underneath the livestream window. This feature, which used WPGraphQL to fetch the data, allowed the theatre to add custom tip buttons and other calls to action depending on the show.</p> <p>Built with <strong>Next.JS</strong>, <strong>Auth0</strong>, <strong>Stripe Subscription API</strong>, <strong>GraphQL</strong>, and <strong>Wowza Streaming</strong></p> </content>
</entry>
<entry>
<title>Pantry & Grocery List Manager</title>
<link href="https://wingmatt.dev/projects/pantry-manager/"/>
<updated>2021-11-11T00:00:00Z</updated>
<id>https://wingmatt.dev/projects/pantry-manager/</id>
<content type="html"><p><em>Your kitchen's inventory screen. Add ingredients to your shopping list with a click.</em></p> <p>I got tired of forgetting what’s in the pantry, so I made a whole app about it.</p> <p><a href="https://kitchensyn.cc/">Kitchen Syncc</a> keeps track of what's in your pantry and on your shopping list. When something is checked off of the shopping list, it gets moved to the pantry, and when something gets used up in the pantry, it gets moved to the shopping list (or just removed for one-off ingredients).</p> <p>I also wrote a recipe parser that takes a recipe URL and spits out the list of ingredients in the recipe. Integration with the shopping list Coming Soon™!</p> <p>Built with <strong>React</strong>, <strong>Next.JS</strong>, <strong>GraphQL</strong>, and <strong>AWS Amplify</strong>. Free to sign up if you want to check it out!</p> </content>
</entry>
<entry>
<title>Blatherbrush</title>
<link href="https://wingmatt.dev/projects/blatherbrush/"/>
<updated>2023-11-15T00:00:00Z</updated>
<id>https://wingmatt.dev/projects/blatherbrush/</id>
<content type="html"><p><em>Infrastructure, architecture, design and development for a social image generation experience!</em></p> <p>My friend hosted a casual art exhibition, and I wanted to participate but I have all the artistic ability of a half-eaten ham sandwich.</p> <p>So I built this thing, which lets folks toss words into a secret, partially filled-out prompt without knowing the context. Once all the blanks are filled in, the prompt gets sent over to OepnAI's DALL-E while participants marvel at the prompt they created together. A gloriously chaotic image is then delivered at last for all to appreciate, or at least contemplate.</p> <p><a href="https://blatherbrush.com/">Take a look here!</a></p> <p>Heads up that you need an account to host a game, and each account only has a few credits for free before you have to pay for more. This is required to cover the cost of generating images each round with DALL-E. Blatherbrush is under active development, so play at your own risk!</p> <p>Built with <strong>Next.JS</strong>, <strong>React</strong>, <strong>TypeScript</strong>, <strong>Postgresql</strong>, <strong>OpenAI</strong>, and <strong>Stripe</strong>.</p> </content>
</entry>
<entry>
<title>Posts</title>
<link href="https://wingmatt.dev/posts/"/>
<updated>2024-09-11T02:00:59Z</updated>
<id>https://wingmatt.dev/posts/</id>
<content type="html"><ul class="tdbc-column-container"><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/" class="tdbc-card__title">Parsing Schema Data with node-html-parser</a> <p>How to get useful data from pasting a URL</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/posts/most-effecient-self-taught-learning/" class="tdbc-card__title">The Most Efficient Self-Taught Learning is Routine</a> <p>Make the most of your time by making time</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/posts/managing-side-projects/" class="tdbc-card__title">Managing Side Projects to Account for Life</a> <p>How to make the most of projects that don't get finished</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/posts/background-blend-mode-safari/" class="tdbc-card__title">Background-blend-mode and Safari</a> <p>AKA why your thing with a background looks wrong on your iPhone</p> </div> </li></ul></content>
</entry>
<entry>
<title>Background-blend-mode and Safari</title>
<link href="https://wingmatt.dev/posts/background-blend-mode-safari/"/>
<updated>2024-09-11T02:00:59Z</updated>
<id>https://wingmatt.dev/posts/background-blend-mode-safari/</id>
<content type="html"><p>I've been dabbling with <a href="https://www.toptal.com/designers/subtlepatterns/">Subtle Patterns</a> for years to break up big swaths of negative space in my website designs.</p> <p>Everything was rosy and magical... <em>until now.</em></p> <p>I used the Funky Lines subtle pattern for <a href="https://blatherbrush.com/">Blatherbrush</a>, which has a color scheme based on your device's light mode preference.</p> <p>To handle dark mode, I set up a good ol' <code>background-blend-mode: multiply</code> so the white parts of the pattern would become dark gray in dark mode. It worked on my machine so I deployed it and continued on my merry way.</p> <p>It wasn't until I popped the site open in my iPhone that I realized everything was actually terrible. Check this out:</p> <p class="codepen" data-height="300" data-default-tab="css,result" data-slug-hash="wvOKYPL" data-user="wingmatt" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/wingmatt/pen/wvOKYPL"> Uh Oh: Background-Blend-Mode + Big Repeating BG Image in Safari/Webkit</a> by Matt Wing (<a href="https://codepen.io/wingmatt">@wingmatt</a>) on <a href="https://codepen.io/">CodePen</a>.</span> </p> <p>If you're on most browsers, that's blue... But on iOS Safari, it's white. It will be white if you open the embedded codepen in another tab, too. In these cases, the <code>background-blend-mode</code> rule doesn't affect the <code>background-image</code>.</p> <p>Reproducing this quite the adventure as I couldn't figure out what ruleset was causing this. After barking up several wrong trees, I eventually learned that Safari has a hard time with <code>background-blend-mode</code> when the background in question has a repeating background tile over 256px in either dimension. Thanks to Damon and the Cassidoo Discord for helping me figure out what was actually going on here!</p> <p>Take a look at this example with the same CSS, just with the background tile's size shrunk down a bit. This should work as expected <em><strong>everywhere</strong></em>:</p> <p class="codepen" data-height="300" data-default-tab="css,result" data-slug-hash="BaEzpKz" data-user="wingmatt" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a href="https://codepen.io/wingmatt/pen/BaEzpKz"> All Good: Background-Blend-Mode + 256px Repeating BG Image in Safari/Webkit</a> by Matt Wing (<a href="https://codepen.io/wingmatt">@wingmatt</a>) on <a href="https://codepen.io/">CodePen</a>.</span> </p> <script async="" src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script> <p><a href="https://bugs.webkit.org/show_bug.cgi?id=259081">Here's the Webkit bug tracker for this issue in case you want to see it &quot;fixed&quot; in &quot;real time&quot;!</a></p> <p>I'm hoping this post becomes obsolete soon and you, dear reader, are baffled at the similarity between the examples above despite my claims. But as of March 2024, you can't use images over 256px in size for your repeating <code>background-image</code> tile if you're gonna be doing a <code>background-blend-mode</code> with them!</p> </content>
</entry>
<entry>
<title>Managing Side Projects to Account for Life</title>
<link href="https://wingmatt.dev/posts/managing-side-projects/"/>
<updated>2024-09-11T02:00:59Z</updated>
<id>https://wingmatt.dev/posts/managing-side-projects/</id>
<content type="html"><p>Everyone loves a good side project! It's your chance to actually work with the shiny new technology you've read 12 tutorials about, to add some real code to your resume to show your proficiency with the shiniest new technologies, and re-learn just how difficult it can be to step outside your coding comfort zone.</p> <p>It's common for coders to have a big barrel full of abandoned side projects. Each one is a milestone in a way. I personally have some concepts I've started from scratch on 3 or 4 times.</p> <p>Much like the first few levels in World of Warcraft, the first few hours of a new project are full of excitement. Anything involving a package manager gives you those first moments where you watch things fly by in the console and feel like a master hacker as you sip on your beverage of choice. Then you plug in the example code and see it work like it did in the example! Just like that! Wow!</p> <p>Eventually the joy ride ends as you come to terms with the vast gulf that spans between where your project is now, and its completed state. Invoking the magic words &quot;mimimum viable product&quot; can only do so much to reduce the scope of your dream when you're hoping to do something amazing. The rational part of your brain knows that spending an hour a day on this will get you to the finish line eventually.</p> <p>That would be all we need for motivation if humans ran entirely on rational thought! Alas, most side projects do eventually fizzle out as we tire of using our spare time to bang our head against the wall in frustration. Even with a mild concussion, the dream that sparked the side project's creation never dies.</p> <p>When you're ready to dive back into things, it's easy to feel overwhelmed by re-acquainting yourself with your old code. It's like loading up a game you almost beat six months ago and trying to remember what you were supposed to be doing.</p> <p>How do we make the most of our fizzled-out projects to make the most of these bursts of renewed motivation? Here are a few suggestions from someone who has plenty of stalled projects to experiment with:</p> <h2 id="document-the-heck-out-of-them" tabindex="-1">Document the heck out of them<a class="tdbc-anchor" href="https://wingmatt.dev/posts/managing-side-projects/#document-the-heck-out-of-them" aria-hidden="true">#</a></h2> <p>Write up rough design documents. Map out what classes or functions you think you'll need to make things happen. You could even write tests for them that roughly describe how you expect them to work. These don't need to be detailed or fancy - they're just for you.</p> <p>This does a few things for you:</p> <ul> <li>Just like in &quot;real&quot; software projects, you'll be able to reconsider architecture and design decisions before you're halfway through implementing your first guess at how things should be. It's so much easier to draw a new diagram than it is to refactor your app from the ground up.</li> <li>If (when?) your motivation for working on the project dries up, you can take a break and easily jump back in when you're ready. If you kept track of your progress too, you can just jump back in from where you left off.</li> <li>If you want to start from scratch, your planning docs won't be tightly tied to specific code, so you can use a fancy new framework instead of the <em>old</em> framework you were excited about the last time you started things up.</li> </ul> <p>I just did this in the aftermath of my most recent project hiatus. I wrote out a rough outline of the type of data the most complex objects would need to hold onto, some pseudocode for the most complicated functions, and a few basic unit tests. I found that it's easier for me to come up with unit testing ideas on paper than in the IDE - maybe because I don't get hung up on getting the syntax perfect as I'm trying to come up with them. What will you find easier to plan on paper? Only one way to find out!</p> <h2 id="use-what-youve-learned-for-next-time" tabindex="-1">Use what you've learned for next time<a class="tdbc-anchor" href="https://wingmatt.dev/posts/managing-side-projects/#use-what-youve-learned-for-next-time" aria-hidden="true">#</a></h2> <p>So... you didn't write up any design documents. That's okay! You moved fast, broke things, and got out of there. We've all done the same. Chances are good that you got stuck on a hard problem. In web development, running into one of those usually means one of three things:</p> <ul> <li>The real problem is that you don't fully understand what's going on, which makes a small problem seem like a huge problem.</li> <li>You're going to learn that someone solved that problem for you in a library - thanks, open source software!</li> <li>You are a pioneer staking your claim on something that, truly, no one has done before. If you solve this, you can become the hero in the last scenario.</li> </ul> <p>The first two situations are pretty common, at least in my experience. I've come back to projects before and found that the biggest problems had melted away.</p> <p>The third scenario is why people say things like &quot;the things worth doing in life are hard.&quot; If you run into a truly unique problem and keep puttering away at it, you'll become a master of that problem's domain before you know it.</p> <p>No matter what stopped you in your tracks, your experience taught you something that no tutorial was going to show you. Maybe you learned the limits of the shiny new technology that looked so effortless in the demos. Maybe you found the edges of a library's helpfulness, or a bug you can report. No matter what happened, you learned something from it, so the next time you start a side project, you'll be that much more knowledgeable about how to make it work.</p> <p>Like I said before, these are the practices I'm working on for myself in order to make the most of my personal programming time. What do you do to improve your chances of taking a side project to the finish line?</p> </content>
</entry>
<entry>
<title>The Most Efficient Self-Taught Learning is Routine</title>
<link href="https://wingmatt.dev/posts/most-effecient-self-taught-learning/"/>
<updated>2024-09-11T02:00:59Z</updated>
<id>https://wingmatt.dev/posts/most-effecient-self-taught-learning/</id>
<content type="html"><p>I love to learn, but I hate finding time for it.</p> <p>We all only have 24 hours in a day. Between work, sleep, and everything else, you're probably juggling a lot already! The challenge for anyone doing self-taught learning is finding out how much time you can set aside <strong>consistently</strong> — fitting it into your regular routine, the cadence of your daily life.</p> <p>Taking two hours to dive into something every other Saturday is a great supplement to your regular cadence. But if that's all you can do, you're gonna spend a good chunk of each time block just trying to remember where you left off.</p> <p>If you can set aside even a half hour every weekday, or even just three days a week, you will feel the progress. And any big blocks of time you <em>can</em> snag will be infinitely more productive!</p> <p>Fair warning that, best case, you'll be taking this time from your solo free time. Yes, that still sucks. But it's still better than depriving yourself of sleep, or neglecting your relationships.</p> <p>It's common for people trying to learn new skills to be drained by the time they get time for it. I found a way around this in my schedule by doing it first thing in the morning, before I hop into any other work. This works for me because I'm already awake a couple hours before my job typically starts.</p> <p>Everyone's schedule is different. As long as you're making the time and protecting it wherever it lands on your schedule, you're moving forward along your path. It doesn't have to be a huge long session each time, and it doesn't even have to be every day. It just needs to be <strong>consistent</strong>!</p> </content>
</entry>
<entry>
<title>Parsing Schema Data with node-html-parser</title>
<link href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/"/>
<updated>2024-09-11T02:00:59Z</updated>
<id>https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/</id>
<content type="html"><p>Did you know that there's a whole JSON object schema for providing machine-readable information about the contents of your website? Google uses the data in these objects to fill out search results and build rich snippets.</p> <p>Here's a secret - it can power other stuff too. For example, I'm building a Node JS web app that includes the ability to plug a URL in and get a list of that recipe's ingredients.</p> <p>Want to start parsing data yourself? Read on!</p> <h2 id="challenges" tabindex="-1">Challenges<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#challenges" aria-hidden="true">#</a></h2> <ul> <li>Fetching the raw HTML</li> <li>Making the Raw HTML Parse-able</li> <li>Finding the right Schema object out of all of the ones on the page</li> <li>Grabbing the right data out of that schema object</li> </ul> <h2 id="fetching-the-raw-html" tabindex="-1">Fetching the raw HTML<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#fetching-the-raw-html" aria-hidden="true">#</a></h2> <p>First things first — we want to be able to fetch the HTML code of whatever link we end up pasting into our app.</p> <p>There are a lot of ways to do this in Node JS. For this tutorial, we'll be using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">native JavaScript <code>fetch</code> API</a>.</p> <p>With that in mind, here's how to make <code>fetch</code> happen:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token comment">// Use an async function so we can wait for the fetch to complete</span><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getHtmlStringFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// responseHtml is a huge string containing the entire web page HTML.</span><br /> <span class="token comment">// In the next section, we'll process it into something we can work with</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> <h2 id="making-the-raw-html-parse-able" tabindex="-1">Making the Raw HTML Parse-able<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#making-the-raw-html-parse-able" aria-hidden="true">#</a></h2> <p>When we first fetch a URL and grab the response body, it's one enormous text string. There's HTML in there, but we can't really work with it yet. We need to plug this string into an HTML parser that will let us use DOM selectors to pick out the useful bits.</p> <p>node-html-parser is my personal choice for this. It lets us use all the usual JavaScript DOM selector methods, and it's pretty fast too. Add it to your project with this terminal command:</p> <p><code>yarn add node-html-parser</code></p> <p>Then import the parse command from the package into the JS file where you'll be using it:</p> <p><code>import { parse } from &quot;node-html-parser&quot;;</code></p> <p>Now we can take the response body string, plug it into our parser, and get to the real fun:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getHtmlDocumentFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token comment">// parse the HTML string into a DOM-like object we can navigate</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<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></code></pre> <p>That's all we need to get the HTML into something we can sift through! The returned object has all the same methods as a typical document object, such as querySelector, getElementByID, and so on.</p> <p>So, how do we work it to find the structured data we're looking for?</p> <h2 id="finding-the-right-schema-objects" tabindex="-1">Finding the right Schema object(s)<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#finding-the-right-schema-objects" aria-hidden="true">#</a></h2> <p>The nice thing about working with structured data is that you can make some assumptions about the data you're processing, because it <em>has</em> to be structured in a way that web crawlers can understand to be useful.</p> <p>The structured data Schema objects we're looking for are going to be found within <code>ld+json</code> script tags. Now that we've DOMified the HTML, we can run queries on it like this:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getSchemaNodeListFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Create a NodeList of elements containing the page's structured data JSON. So close to useful!</span><br /> <span class="token keyword">const</span> structuredData <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">'script[type="application/ld+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 /><span class="token punctuation">}</span></code></pre> <p>That will give us a NodeList of all the matching elements. That's close to perfect, but it's not a true array and could give us errors if we try to treat it like one (which we will soon). So let's turn it into an array:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getSchemaArrayFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Create an ARRAY of elements containing the page's structured data JSON. Just one more step!</span><br /> <span class="token keyword">const</span> structuredData <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'script[type="application/ld+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 punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /></code></pre> <p>Now we have an array of structured data nodes. In a way, we're back to square one with data that is <em>so close</em> to being useful. To make it useful, we need to grab the innerHTML of each node, which will come out as a big string. Then we can parse that into ✨real JSON!✨</p> <pre class="language-jsx"><code class="language-jsx"><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getJsonFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> structuredData <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'script[type="application/ld+json"]'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Get an array containing the contents of each structured data element on the page. This is the ✨useful stuff✨</span><br /> <span class="token keyword">const</span> structuredDataJson <span class="token operator">=</span> structuredData<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">node</span><span class="token punctuation">)</span> <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>node<span class="token punctuation">.</span>innerHTML<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">flat</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// We also flatten the array with .flat() to handle how some sites structure their schema data. See epilogue for more info</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> <p>Whoa, look at us. We've got the real, actual JSON object now. If you log structuredDataJson to your console, you'll see an array of structured data objects! Huzzah 🎉</p> <p>But of course, we're not done yet! There is likely to be a ton of data you don't need in this array, in addition to whatever you're actually looking for.</p> <h2 id="grabbing-the-right-data-out-of-that-schema-object" tabindex="-1">Grabbing the right data out of that schema object<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#grabbing-the-right-data-out-of-that-schema-object" aria-hidden="true">#</a></h2> <p>You're looking for some sort of specific data out of these objects. In my case, I'm looking for the list of <a href="https://schema.org/recipeIngredient">ingredients</a> within the <a href="https://schema.org/Recipe">Recipe object</a>. So, now that we have actual JSON, we can view certain properties and use it to whittle our array down to a single, useful, piece of data:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getIngredientsFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> structuredData <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'script[type="application/ld+json"]'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> structuredDataJson <span class="token operator">=</span> structuredData<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">node</span><span class="token punctuation">)</span> <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>node<span class="token punctuation">.</span>innerHTML<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">flat</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Look for a Recipe schema and return its ingredients if it exists </span><br /> <span class="token keyword">const</span> recipeData <span class="token operator">=</span> structuredDataJson<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">schema</span><span class="token punctuation">)</span> <span class="token operator">=></span> schema<span class="token punctuation">[</span><span class="token string">"@type"</span><span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token string">"Recipe"</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>recipeData<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> recipeData<span class="token punctuation">.</span>recipeIngredient<br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">return</span> <span class="token keyword">null</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> <p>If one of the structured data objects is for a Recipe, we'll get the array of ingredients we're looking for. If not, the function will return <code>null</code> so we know it failed to find what we were looking for.</p> <p>That's it! We've parsed the HTML into JSON into the actual thing we need 🎉</p> <h2 id="conclusion" tabindex="-1">Conclusion<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#conclusion" aria-hidden="true">#</a></h2> <p>At this point, you have a function that takes a URL and returns an array of whatever information you're looking for. This general process can be used to do a whole lot of interesting stuff depending on what you're grabbing. <a href="https://codesandbox.io/s/nextjs-recipe-schema-data-parsing-example-tblx7">Here's an example I put together to grab the ingredients within a recipe page.</a></p> <p><a href="https://schema.org/docs/schemas.html">Here are some of the most common schemas out there</a> for inspiration. In my case, I'm parsing recipe ingredients so I can see if they're in my pantry, and add them to my shopping list if they're not.</p> <p>How about you? If you end up using this process to parse website data in your web app, let me know what you're doing!</p> <h2 id="epilogue-handling-edge-cases-with-the-flat-method" tabindex="-1">Epilogue: Handling Edge Cases with the flat() method<a class="tdbc-anchor" href="https://wingmatt.dev/posts/parsing-schema-data-node-html-parser/#epilogue-handling-edge-cases-with-the-flat-method" aria-hidden="true">#</a></h2> <p>As mentioned earlier, structured data has to be readable by web crawlers to be useful, so we can make some assumptions about what it will look like. Still, we're ultimately trusting people to build their websites according to a certain convention, so you still might run into some issues across different websites and pages.</p> <p>When I was testing my recipe parser, I ran into a few websites that structured their data in non-standard ways, which caused some trouble early on. The most common issue I found was that some sites would wrap their schema JSON within an array. This prevented my array.find() method from finding any of the data within the nested array.</p> <p>In my production code, I handle this by flattening the parsed JSON to remove any nested arrays before I start looking for specific data. Here's what that looks like with the example code we've been using:</p> <pre class="language-jsx"><code class="language-jsx"><span class="token keyword">import</span> <span class="token punctuation">{</span> parse <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"node-html-parser"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getHtmlFromUrl</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">return</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<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">text</span><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">responseHtml</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> document <span class="token operator">=</span> <span class="token function">parse</span><span class="token punctuation">(</span>responseHtml<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> structuredData <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span><span class="token function">querySelectorAll</span><span class="token punctuation">(</span><span class="token string">'script[type="application/ld+json"]'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token comment">// Adding .flat() to the line below handles the most common edge cases I've found so far! </span><br /> <span class="token keyword">const</span> structuredDataJson <span class="token operator">=</span> structuredData<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">node</span><span class="token punctuation">)</span> <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>node<span class="token punctuation">.</span>innerHTML<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">flat</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> recipeData <span class="token operator">=</span> structuredDataJson<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">schema</span><span class="token punctuation">)</span> <span class="token operator">=></span> schema<span class="token punctuation">[</span><span class="token string">"@type"</span><span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token string">"Recipe"</span><span class="token punctuation">)</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>recipeData<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> recipeData<span class="token punctuation">.</span>recipeIngredient<br /> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">return</span> <span class="token keyword">null</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> </content>
</entry>
<entry>
<title>Projects</title>
<link href="https://wingmatt.dev/projects/"/>
<updated>2024-09-11T02:00:59Z</updated>
<id>https://wingmatt.dev/projects/</id>
<content type="html"><ul class="tdbc-column-container"><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/blatherbrush/" class="tdbc-card__title">Blatherbrush</a> <p>Multiplayer AI art image generation game</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/pantry-manager/" class="tdbc-card__title">Pantry & Grocery List Manager</a> <p>Your kitchen's inventory screen. Add ingredients to your shopping list with a click.</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/streaming-platform/" class="tdbc-card__title">Subscription Streaming Platform</a> <p>Nonprofit streaming platform to deliver live entertainment amidst a pandemic.</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/global-variations-plugin-design/" class="tdbc-card__title">WooCommerce Global Variation Templates</a> <p>Interface and architecture for a time-saving plugin for photography print sellers.</p> </div> </li><li class="tdbc-card"> <div class="tdbc-card__content"> <a href="https://wingmatt.dev/projects/jackson-fire-info/" class="tdbc-card__title">Jackson Fire Info</a> <p>Jackson County's lo-fi firewatch. Created during Southern Oregon's September 2020 fires. </p> </div> </li></ul></content>
</entry>
</feed>