<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by John Franey on Medium]]></title>
        <description><![CDATA[Stories by John Franey on Medium]]></description>
        <link>https://medium.com/@johnfraney?source=rss-350ce7cb1744------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*4yZohUyj2IsB6n7Lb68xSA.png</url>
            <title>Stories by John Franey on Medium</title>
            <link>https://medium.com/@johnfraney?source=rss-350ce7cb1744------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Mon, 22 Jun 2026 16:22:55 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@johnfraney/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[How to unit test a Vue composable with TypeScript]]></title>
            <link>https://johnfraney.medium.com/how-to-unit-test-a-vue-composable-with-typescript-3883b2e0fee4?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/3883b2e0fee4</guid>
            <category><![CDATA[unit-testing]]></category>
            <category><![CDATA[vuejs]]></category>
            <category><![CDATA[typescript]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Wed, 03 Dec 2025 12:32:10 GMT</pubDate>
            <atom:updated>2025-12-03T12:32:10.155Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Blurred image of the testComposable function mentioned in the post" src="https://cdn-images-1.medium.com/max/1024/0*xInJJwD1NvrBVfgC.png" /></figure><h3>Introduction</h3><p>Have you ever tried to test a Vue composable function (“use&quot; function) and hit this error?</p><pre>SyntaxError: Must be called at the top of a `setup` function</pre><p>I sure have. I added <a href="https://vitest.dev/guide/in-source.html">in-source tests</a> for composable function in a project I’m working on that does currency formatting, but once I updated that composable to use <a href="https://vue-i18n.intlify.dev/guide/essentials/number.html">Vue I18n number formatting</a>, I hit that syntax error.</p><p>Why would adding I18n to the function change how it needs to be tested? The Vue documentation explains that certain composable functions can only be used in a setup function:</p><blockquote><em>A composable depends on a host component instance when it uses the following APIs:</em></blockquote><blockquote><em>- Lifecycle hooks<br>- Provide / Inject</em></blockquote><blockquote><a href="https://vuejs.org/guide/scaling-up/testing.html#testing-composables"><em>https://vuejs.org/guide/scaling-up/testing.html#testing-composables</em></a></blockquote><p>Lo’ and behold, Vue I18n uses provide/inject by default: <a href="https://vue-i18n.intlify.dev/guide/advanced/composition.html#implicit-with-injected-properties-and-functions">https://vue-i18n.intlify.dev/guide/advanced/composition.html#implicit-with-injected-properties-and-functions</a>. That means to test a similar composable, we need our test to include a setup function.</p><p>So what is the best way to unit test a Vue composable function? Let’s find out.</p><p><strong>Info</strong>: Looking for a package that does this? Check out <a href="https://github.com/wobsoriano/vue-composable-testing/">github.com/wobsoriano/vue-composable-testing</a>.</p><h3>First steps</h3><p>Let’s not reinvent the wheel.</p><p>Vue provides <a href="https://vuejs.org/guide/scaling-up/testing.html#testing-composables">an example of how to test a composable</a>, but the example uses vanilla JavaScript and out-of-the box it isn’t type-safe. Let’s check it out:</p><pre>import { createApp } from &#39;vue&#39;<br><br>export function withSetup(composable) {<br>  let result<br>  const app = createApp({<br>    setup() {<br>      result = composable()<br>      // suppress missing template warning<br>      return () =&gt; {}<br>    }<br>  })<br>  app.mount(document.createElement(&#39;div&#39;))<br>  // return the result and the app instance<br>  // for testing provide/unmount<br>  return [result, app]<br>}</pre><p>I’m a big <a href="https://www.typescriptlang.org/">TypeScript</a> user, so let’s add types to that example (see the T):</p><pre>import { createApp } from &#39;vue&#39;<br><br>export function withSetup&lt;T&gt;(composable: () =&gt; T) {<br>  let result: T<br>  const app = createApp({<br>    setup() {<br>      result = composable()<br>      // suppress missing template warning<br>      return () =&gt; {}<br>    }<br>  })<br>  app.mount(document.createElement(&#39;div&#39;))<br>  // return the result and the app instance<br>  // for testing provide/unmount<br>  return [result, app]<br>}</pre><p>This is looking pretty good! TypeScript knows that the return type of withSetup is the same as the return type of the composable function.</p><p>But there’s a wrinkle. Using this code in a test causes the following type error:</p><pre>Variable &#39;result&#39; is used before being assigned. ts(2454)</pre><p>A limitation of using this method to test a composable is that it isn’t fully type-safe because result can be undefined. Because result is only set in the setup() function of createApp() but is returned right away, TypeScript rightly lets us know of a race condition where we&#39;re using result while it still might not be set.</p><h3>“Solution” 1: definite assignment</h3><p>Because of how Vue works, result likely <em>will</em> be set after app.mount(), though, so we can let TypeScript know this with a <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions">definite assignment assertion</a>, which is specified with a ! after a variable name.</p><pre>import { createApp } from &#39;vue&#39;<br><br>export function withSetup(composable) {<br>  let result<br>  const app = createApp({<br>    setup() {<br>      result = composable()<br>      // suppress missing template warning<br>      return () =&gt; {}<br>    }<br>  })<br>  app.mount(document.createElement(&#39;div&#39;))<br>  // return the result and the app instance<br>  // for testing provide/unmount<br>  return [result!, app] // &lt;-- JF: TypeScript hack so result is defined<br>}</pre><p>Using definite assignment assertions like this can paper over runtime issues in your code, like race conditions. I try to avoid them as a matter of course.</p><p>A more type-safe way to do this would be to throw an error if result is undefined:</p><pre>if (result === undefined) throw new Error(&#39;result is undefined&#39;)<br>return [result, app] // &lt;-- no more &#39;!&#39;</pre><h3>Solution 2: Promise</h3><p>This is the type-safe solution I came up with to test a Vue composable.</p><p>This method side-steps the race condition of whether result is defined by using a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</a>, which guarantees that result is defined when it is used in a test.</p><pre>// @/utils/testing.ts<br>import { createApp } from &#39;vue&#39;<br><br>/**<br> * @param getComposableResult Function that returns the value of a composable for testing<br> */<br>export async function testComposable&lt;T&gt;(getComposableResult: () =&gt; T): Promise&lt;T&gt; {<br>  return new Promise((resolve) =&gt; {<br>    const app = createApp({<br>      setup() {<br>        resolve(getComposableResult())<br>        // suppress missing template warning<br>        return () =&gt; {}<br>      },<br>    })<br>    // Install global plugins here with app.use()<br>    app.mount(document.createElement(&#39;div&#39;))<br>  })<br>}</pre><p>If your tests rely on Vue plugins, like for i18n, you can install them in your testComposable function so they&#39;re available to use in your tests.</p><p>Mounting a Vue application in a test environment requires a DOM implementation, like <a href="https://github.com/capricorn86/happy-dom">happy-dom</a>, added to <a href="https://vitest.dev/config/#environment">the </a><a href="https://vitest.dev/config/#environment">environment setting of your test Vitest config</a>.</p><p>Here’s how we can use our testComposable it in a test for a composable named useFormatCurrency:</p><pre>import { describe, expect, it } from &#39;vitest&#39;<br><br>import { useFormatCurrency } from &#39;@/composables/currency&#39;<br>import { testComposable } from &#39;@/utils/testing&#39;<br><br>describe(&#39;useFormatCurrency&#39;, () =&gt; {<br>  it(&#39;prepends the correct currency symbol&#39;, async () =&gt; {<br>    const result = await testComposable(() =&gt; {<br>      const formatCurrency = useFormatCurrency()<br>      return formatCurrency(0.20000001)<br>    })<br>    expect(result).toEqual(&#39;$0.20&#39;)<br>  })<br>})</pre><p><strong>I</strong>nfo: The tests are written with Vitest, an excellent test runner for TypeScript and JavaScript projects.</p><h3>Wrap-up</h3><p>That’s it! With this quick Promise-based utility, it’s easy to write type-safe unit tests for Vue composables.</p><p>This method is great for unit testing composables outside of components. For a component-centric solution where you’re looking to test how a composable works with component props, check out this example from the Vue Test Utils docs: <a href="https://test-utils.vuejs.org/guide/advanced/reusability-composition.html#Testing-composables">https://test-utils.vuejs.org/guide/advanced/reusability-composition.html#Testing-composables</a>.</p><p>Happy testing!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3883b2e0fee4" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to triple* Google search traffic with one wording change]]></title>
            <link>https://johnfraney.medium.com/how-to-triple-google-search-traffic-with-one-wording-change-28c1b9bd25bf?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/28c1b9bd25bf</guid>
            <category><![CDATA[keyword-research]]></category>
            <category><![CDATA[seo]]></category>
            <category><![CDATA[technical-seo]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Mon, 13 Jan 2025 14:46:09 GMT</pubDate>
            <atom:updated>2025-01-13T14:46:09.695Z</atom:updated>
            <content:encoded><![CDATA[<p>*Your results will definitely vary.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/692/1*hW-gwHf1jRtHoDRlyJCmCA.png" /></figure><p><em>Most of this post was written on November 13th, and as of January 13th, 2025, Pyrfecter’s search performance has gotten even better: 191 clicks in 28 days.</em></p><h3>Introduction</h3><p>In August 2024, <a href="https://pyrfecter.com/">Pyrfecter.com</a> reached 20 clicks from Google Search in 28 days for the first time. Three months later, the site is just a click away from reaching 120 clicks in 28 days.</p><p>Granted, this isn’t colossal organic search traffic, but it is a pretty big jump considering that almost all of the new traffic can be attributed to one wording change on one page on October 9th.</p><h3>The steps</h3><h3>Step 1: have good content</h3><p>This might not need to be said, but if your site doesn’t provide something people are searching for, no amount of SEO can save you.</p><h3>Step 2: research queries</h3><p>To optimize for specific queries, you need to know what they are.</p><p><em>“Thanks, Captain Obvious.</em>”</p><p>Fair, fair. But how can you find out what keywords to optimize for?</p><ul><li>look at the search terms that are already surfacing your content using tools like <a href="https://search.google.com/search-console">Google Search Console</a> and <a href="https://www.bing.com/webmasters/about">Bing Webmaster Tools</a><br><em>NB: if your site is very new, the data here will be limited</em></li><li>do research using a tool like <a href="https://www.semrush.com/">Semrush</a> to see which keywords competing sites are ranking for</li><li>compare keyword popularity using <a href="https://trends.google.com/trends/">Google Trends</a></li></ul><p>Here are the queries where a Pyfecter page appeared in the Google results for the 3 months ending November 11, 2024 (from Google Search Console):</p><pre>| Top queries             | Impressions |<br>| ----------------------- | ----------- |<br>| python linter           | 553         |<br>| python linter online    | 532         |<br>| linting python          | 418         |<br>| python lint online      | 371         |<br>| python linting          | 363         |<br>| walrus operator python  | 279         |<br>| python walrus operator  | 254         |<br>| walrus operator         | 203         |<br>| what is a python linter | 195         |<br>| linting in python       | 194         |</pre><p><em>Table generated with </em><a href="https://tabletomarkdown.com/"><em>Table to Markdown</em></a></p><p>And here are the Google Trends over the past 5 years for “python linter” and “lint python”, showing that “python linter” consistently trends higher:</p><figure><img alt="Google Trends comparing “python linter” and “lint python”" src="https://cdn-images-1.medium.com/max/791/0*ox07w4IDzqBZAIaI.png" /></figure><h3>Step 3: incorporate those queries in your page</h3><p>Now that you know what language people use to find content like yours, it’s time to update your page to use that language. And — I can’t stress this enough — it needs to feel natural.</p><p>If your page’s content reads as though it’s cramming in as many keywords as it can, your page may get punished for keyword stuffing:</p><blockquote><em>Keyword stuffing refers to the practice of filling a web page with keywords or numbers in an attempt to manipulate rankings in Google Search results.<br>- </em>Google Search Central, <a href="https://developers.google.com/search/docs/essentials/spam-policies#keyword-stuffing">Spam policies for Google web search</a></blockquote><p>With keywords, a little goes a long way.</p><p>Below is the Markdown source code for <a href="https://pyrfecter.com/lint/">https://pyrfecter.com/lint/</a>, which is built using my static site generator <a href="https://github.com/blurry-dev/blurry/">Blurry</a>.</p><p>You’ll notice three changes in the <a href="https://en.wikipedia.org/wiki/Diff">diff</a> (red lines were removed; green lines were added):</p><ol><li>name TOML front matter, which is used as the page&#39;s &lt;title&gt;, updated with the new wording</li><li>a newer datePublished, which is included in the site&#39;s <a href="https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview">sitemap</a> to tell search engines that the page has changed</li><li>the page’s top-level heading updated with the new wording (# in Markdown is an &lt;h1&gt; in HTML)</li></ol><pre>--- a/content/lint.md<br>+++ b/content/lint.md<br>@@ -1,12 +1,12 @@<br> +++<br> &quot;@type&quot; = &quot;WebPage&quot;<br>-name = &quot;Lint Python code securely in your browser with Pyrfecter&quot;<br>+name = &quot;Pyrfecter: the secure online Python linter&quot;<br> abstract = &quot;With Pyrfecter, you can lint Python code quickly and securely right in your browser. Plus, it&#39;s 100% free.&quot;<br>-datePublished = 2024-05-11<br>+datePublished = 2024-10-09<br> _show_code = true<br> +++<br> <br>-# Lint Python online with Pyrfecter<br>+# Pyrfecter: the secure online Python linter</pre><p>Blurry uses Schema.org types, like <a href="https://schema.org/WebPage">WebPage</a>, to describe page content. This describes the content to search engines and can be used to show rich search results like you see for products (<a href="https://schema.org/Product">Product</a>) and restaurants (<a href="https://schema.org/Restaurant">Restaurant</a>).</p><p>That three-line diff is the whole git commit that kicked off a steady increase in organic search traffic.</p><h3>Step 4: tell search engines</h3><p>Before you can see real-world results from your SEO changes, you’ll need to tell search engines that your content has changed and ask them to index the updated pages.</p><p>For Google, open Google Cearch Console and navigate to <a href="https://support.google.com/webmasters/answer/9012289">Google’s URL Inspection tool</a> to request indexing for the URL.</p><p>The process is similar for Bing: input your updated URL in the <a href="https://www.bing.com/webmasters/help/url-submission-62f2860b">Bing Webmaster Tools URL Submission tool</a>.</p><p>Bing also supports triggering automatic indexing of updated URLs using <a href="https://www.bing.com/webmasters/help/indexnow-0z209wby">IndexNow</a>, but that’s a topic for another day.</p><h3>Step 5: watch the numbers go up</h3><p>It can take a couple of days before changes in search performance start to happen, so this step requires a bit of patience.</p><p>But, if your SEO work was a success, you’ll soon be able to watch your organic search traffic go up and to the right:</p><figure><img alt="Google search results performance graph" src="https://cdn-images-1.medium.com/max/692/0*RQSBJUqlzfZ2Y-Ef.png" /></figure><h3>Wrap-up</h3><p>For search marketers and plenty of developers, the information in this post may be old hat.</p><p>As someone who has been learning about and experimenting with SEO for 15 years, though, I’m still surprised at what an impact even one small change can have on search performance.</p><p>Granted, when a site doesn’t get much organic search traffic, it’s much easier to see large percentage improvements. But I think this point stands on its own: using the same language as your site’s prospective visitors is fundamental to SEO and can get you measurable results.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=28c1b9bd25bf" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to write user stories that actually get done]]></title>
            <link>https://medium.com/codex/how-to-write-user-stories-that-actually-get-done-cb035466e540?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/cb035466e540</guid>
            <category><![CDATA[project-management]]></category>
            <category><![CDATA[user-stories]]></category>
            <category><![CDATA[agile]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Thu, 25 Jan 2024 03:12:48 GMT</pubDate>
            <atom:updated>2024-02-01T10:06:21.300Z</atom:updated>
            <content:encoded><![CDATA[<h3>How To Write User Stories That Actually Get Done</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/498/1*S1_g2Tb2ERHZ3whNGMss0A.gif" /></figure><h3>Introduction</h3><p>At each stop in my career, I seem to develop a reputation for being a Fussy Franey with user stories. I’ve seen some eye rolls when I’ve asked for one story to be broken into three, and I’ve faced incredulity when pointing out that one “user story” is actually an entire epic — or even so broad in scope that it could be an entirely separate business.</p><p>I like to get things done, and I’ve found that small, well-written user stories are the single biggest enabler of dev velocity.</p><h3>User stories should be small</h3><p>Very small. How small?</p><p>They should encompass a <em>single</em> user interaction, single visual element, or single application reaction.</p><p>Reducing scope for a feature or launch should not require rewriting user stories</p><h3>What counts as a user interaction?</h3><ul><li>Viewing something, like a list of contacts</li><li>Navigating to a page</li><li>Entering a value in a form field</li><li>Submitting a form</li></ul><p><em>Note: submitting a form doesn’t include form submission accoutrements, like showing a loading icon (separate story), showing an error message (separate story), or showing a success message (separate story)</em></p><h3>What counts as a visual element?</h3><p>Viewing is an action, and seeing a visual/UI element counts as a user story. Visual elements can be tiny, too, like a red notification dot you might see on a new menu link.</p><p>It might seem like overkill to have an entire user story for a notification dot, but the menu item <em>can</em> exist without the notification dot. Because the notification dot depends on the menu item, it can be implemented separately from the menu item and prioritized separately, too.</p><h3>What counts as an application reaction?</h3><p>An application reaction is a change in the application that happens as a result of a user action or as a result of other application logic. The reaction could take place within an application (a toast notification) or across applications (sending an SMS).</p><p>Some quick examples:</p><ul><li>Validating an input</li><li>Showing a toast</li><li>Showing a loading element</li><li>Showing a logout warning</li><li>Sending a confirmation email</li></ul><h3>Q: When is a story not a story?</h3><p>A: When it’s an epic!</p><p>For example, let’s take the following story:</p><blockquote><em>As a user, I can delete a to-do item</em></blockquote><p>Sounds like a small, regular story, right? Not so fast.</p><p>A story like that could have a number of requirements before it could be considered done (optional requirements marked with asterisks):</p><ol><li>Showing a confirmation modal when clicking the delete button*</li><li>Writing the API delete endpoint</li><li>Performing the API delete request on click</li><li>Disabling the button while the request is processing*</li><li>Removing the to-do item on a success response</li><li>Showing a success toast*</li></ol><p>There’s a lot happening there. There’s enough happening here to make deleting a to-do item an <strong>epic</strong> rather than a single story.</p><p>And what’s an epic? An epic is “a large, high-level piece of work” (<a href="https://resources.scrumalliance.org/Article/epic-agile">ScrumAlliance</a>) that consists of multiple user stories and other pieces of work, like non-user-facing dev tasks.</p><p>Atlassian has a helpful explanation of epics as a strategy to break down work into smaller pieces:</p><blockquote><em>Epics are a helpful way to organize your work and to create a hierarchy. The idea is to break work down into shippable pieces so that large projects can actually get done and you can continue to ship value to your customers on a regular basis. Epics help teams break their work down, while continuing to work towards a bigger goal. (Atlassian, </em><a href="https://www.atlassian.com/agile/project-management/epics"><em>“Agile epics: definition, examples, and templates”</em></a><em>)</em></blockquote><p>In other words, an epic captures an entire feature flow, like the end-to-end (and frontend-to-backend) process of creating a to-do item, and it consists of one or more user stories and/or dev tasks.</p><p><em>Note: “Managing” something should never be a single user story. If your user story starts, “As a user, I can manage…”, it’s likely a project, and each of the </em><a href="https://en.wikipedia.org/wiki/Create%2C_read%2C_update_and_delete"><em>Create, Read, Update, and Delete</em></a><em> actions may well be its own epic.</em></p><h3>Can stories have multiple acceptance criteria?</h3><p>They can!</p><p>But they still need to be small, and the acceptance criteria need to be so intimately related that separating them would be impossible to implement or impossible to verify. So what is a story that can have multiple acceptance criteria? The second requirement in the epic above is a good example:</p><blockquote><em>As a user, I can confirm whether I want to delete a to-do item</em></blockquote><p>This story might have acceptance criteria like the following:</p><ol><li>GIVEN I am a user viewing a to-do item<br>WHEN I click the delete button<br>THEN I am shown a confirmation modal</li><li>GIVEN I am a user confirming whether to delete a to-do item<br>WHEN I click the close button on the confirmation modal<br>THEN the modal is closed AND the to-do item is not deleted</li><li>GIVEN I am a user viewing the delete to-do confirmation modal<br>WHEN I confirm my intent to delete the to-do item<br>THEN the item is deleted AND the modal is closed</li></ol><p>Showing a confirmation modal (application reaction) when clicking the delete button (user action) encompasses two UI elements and one action+reaction, but still makes sense to have in the same user story because there’s no way to validate that the delete button behaves as expected without having a confirmation modal to view.</p><p>The story could be broken into subtasks to parallelize development by creating a subtask for the design of the confirmation modal. Ideally, the confirmation modal would be reusable and wouldn’t have to be implemented separately for each action. For more on this, see the “User stories should be new” section below.</p><p><em>Note: Multiple user stories can be coded in a single branch and merged in a single pull request.</em></p><p><em>For example, showing a success message on a successful form submission and showing an error message on an unsuccessful one could make sense to code together. They are different enough that they should be in separate user stories, though, because one could be done without doing the other.</em></p><h3>User stories should be explicit</h3><p>A story’s definition of “done” should be unambiguous.</p><p>Acceptance criteria should be specific and comprehensive, and it should be described in writing rather than being hidden in designs.</p><p>If a story involves changing text, for example, include the old and new text in the acceptance criteria. When acceptance criteria read something like “Update wording to match designs”, changes are easy to miss, which can throw off estimates and lead to QA rejections.</p><p><em>Note: </em><a href="https://cucumber.io/docs/gherkin/reference/#steps"><em>Gherkin syntax</em></a><em> (GIVEN/WHEN/THEN) is helpful to describe a story’s scope. If a story is difficult to describe in the GIVEN/WHEN/THEN format, it’s also going to be difficult to code, review, and QA.</em></p><h3>User stories should be new</h3><p>Existing capabilities can be acceptance criteria, while new capabilities should be user stories.</p><p>If you have a form component that shows an error message on a 500 response, for example, there doesn’t need to be a new user story to use this behaviour in a new form. For another example, if you have a phone numer field and you want to see a phone number input on a mobile device, because this is a built-in capability of an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/tel">&lt;input type=&quot;tel&quot;&gt;</a>, it doesn&#39;t need to be its own user story.</p><p><em>Note: Subtle changes to existing behaviour should be captured in a separate ticket. It’s a good idea to have one or more developers audit acceptance criteria to identify new features that may be masquerading as acceptance criteria.</em></p><h3>Lightning summary ⚡</h3><p>While it may seem tedious to have stories with such a narrow scope, large user stories often mask complexity in a project, posing challenges to estimating, sequencing, QAing, and descoping work.</p><p>User stories that actually get done are small, explicit, and new.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cb035466e540" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/how-to-write-user-stories-that-actually-get-done-cb035466e540">How to write user stories that actually get done</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AWS documentation frustrations]]></title>
            <link>https://medium.com/codex/aws-documentation-frustrations-15db9de0b0e6?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/15db9de0b0e6</guid>
            <category><![CDATA[technical-writing]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[documention]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Tue, 05 Sep 2023 12:02:26 GMT</pubDate>
            <atom:updated>2023-09-13T14:45:17.567Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*pkZ16kJvMfSVoww7gHzPDQ.jpeg" /><figcaption>AWS logo (sad edition)</figcaption></figure><h3>Introduction</h3><p>AWS has a ton of documentation, and sometimes it misses the mark. Recently, I came across two different examples of AWS documentation either perplexing or frustrating me.</p><p><strong>Note</strong>:By &quot;frustrating&quot;, I mostly mean it in the &quot;hinder or prevent (the efforts, plans, or desires) of&quot; meaning of the word (<a href="https://www.wordwebonline.com/search.pl?w=frustrating">WordWeb</a>). Mostly.</p><p>Now, I don&#39;t want to come across as a complainy Franey, but since I&#39;ve been working on <a href="https://blurry-docs.netlify.app/">documentation</a> for <a href="https://github.com/blurry-dev/blurry">Blurry</a>, a static site generator I open-sourced (and that builds this site), I&#39;ve been thinking about what makes good technical documentation good. I don&#39;t think I&#39;ve figured it out <em>quite</em> yet, but I have found two traits of AWS docs that can hurt documentation, and maybe doing the opposite can improve it.</p><p>Read on for a break-down of two AWS documentation breakdowns and what I learned therefrom.</p><h3>Trait 1: perplexing</h3><p>Video: <a href="https://johnfraney.ca/blog/videos/aws-vs-code-extension-installation.mp4">Installing the AWS VS Code extension from the Lambda console</a></p><p>How many steps does it take to get a direct link to the VS Code extension?</p><p>From the Lambda function console page, it took 4 clicks, reading, and scrolling to get to the actual VS Code extension. Even from the <a href="https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html">AWS Toolkit for Visual Studio Code</a> site, it was still two clicks to get to the page with the direct VS Code link (and that link was below-the-fold).</p><p>That&#39;s quite a long walk for what ended up being a link that opened the extension in VS Code itself. Four clicks for something that could be one click? That&#39;s pretty perplexing.</p><p>There&#39;s some risk in having that much documentation about a VS Code extension, too.<br> Having a documentation site separate from the README could mean:</p><ol><li>You have to maintain documentation in multiple places, which can be difficult to keep in sync</li><li>If documentation in one of the two places isn&#39;t unique enough to have to keep in sync, that means the docs may be general enough to be not <em>that</em> helpful</li></ol><p><strong>Info</strong>: I do need to give credit where it&#39;s due: an earlier version of the VS Code extension documentation didn&#39;t have a link that opened the extension in VS Code; after all that clicking, it asked that we open VS Code and search for the extension.</p><h3>Trait 2: frustrating</h3><p>It&#39;s frustrating when documentation is incomplete — especially in code examples.</p><p>Take this Python code sample for <a href="https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_lambda_python_alpha/PythonFunction.html">AWS CDK&#39;s </a><a href="https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_lambda_python_alpha/PythonFunction.html">PythonFunction</a>, for instance:</p><pre>entry = &quot;/path/to/function&quot;<br>image = DockerImage.from_build(entry)<br><br>python.PythonFunction(self, &quot;function&quot;,<br>    entry=entry,<br>    runtime=Runtime.PYTHON_3_8,<br>    bundling=python.BundlingOptions(<br>        build_args={&quot;PIP_INDEX_URL&quot;: &quot;https://your.index.url/simple/&quot;, &quot;PIP_EXTRA_INDEX_URL&quot;: &quot;https://your.extra-index.url/simple/&quot;}<br>    )<br>)</pre><p>Linting the code using Pylint via (<a href="https://pyrfecter.com/">Pyrfecter</a>), we can see a number of linting issues:</p><pre>2:9: undefined name &#39;DockerImage&#39;<br>4:1: undefined name &#39;python&#39;<br>4:23: undefined name &#39;self&#39;<br>6:13: undefined name &#39;Runtime&#39;<br>7:14: undefined name &#39;python&#39;</pre><p>There are a couple missing imports that were easy to sort out (Runtime, DockerImage), but python? I couldn&#39;t find that in the <a href="https://docs.aws.amazon.com/cdk/v2/guide/home.html">CDK Developer Guide</a> or the <a href="https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.aws_lambda_python_alpha.html">CDK API documentation</a> or the <a href="https://github.com/aws-samples/aws-cdk-examples">AWS CDK Examples</a>.</p><p>After getting help from a search engine, I found that the missing python import is:</p><pre>from aws_cdk import aws_lambda_python_alpha as python</pre><p>And the PyPI package to install is:</p><pre>aws-cdk-aws-lambda-python-alpha</pre><p>And self?<br> The python.PythonFunction call shoud be inside a CDK Stack.</p><p>If we check <a href="https://docs.aws.amazon.com/cdk/v2/guide/stacks.html">the documentation for that</a>, we get an example of an App and a Construct, but the Stack classes are stubbed:</p><pre>from aws_cdk import App, Stack<br>from constructs import Construct<br><br># imagine these stacks declare a bunch of related resources<br>class ControlPlane(Stack): pass<br>class DataPlane(Stack): pass<br>class Monitoring(Stack): pass<br><br>class MyService(Construct):<br><br>  def __init__(self, scope: Construct, id: str, *, prod=False):<br><br>    super().__init__(scope, id)<br><br>    # we might use the prod argument to change how the service is configured<br>    ControlPlane(self, &quot;cp&quot;)<br>    DataPlane(self, &quot;data&quot;)<br>    Monitoring(self, &quot;mon&quot;)<br><br>app = App();<br>MyService(app, &quot;beta&quot;)<br>MyService(app, &quot;prod&quot;, prod=True)<br><br>app.synth()</pre><p>There are no linting errors, at least!</p><p>For a code example of a Stack, I went to the <a href="https://docs.aws.amazon.com/cdk/v2/guide/apps.html">documentation for </a><a href="https://docs.aws.amazon.com/cdk/v2/guide/apps.html">App</a> and found this:</p><pre>class MyFirstStack(Stack):<br><br>    def __init__(self, scope: Construct, id: str, **kwargs):<br>        super().__init__(scope, id, **kwargs)<br><br>        s3.Bucket(self, &quot;MyFirstBucket&quot;)</pre><p>And the linting output?</p><pre>1:20: undefined name &#39;Stack&#39;<br>3:31: undefined name &#39;Construct&#39;<br>6:9: undefined name &#39;s3&#39;</pre><p>We can get the Stack and Construct imports from the code sample in the <a href="https://docs.aws.amazon.com/cdk/v2/guide/stacks.html">Stacks page</a>.</p><p>So. After checking the the aws_cdk.aws_lambda_python_alpha API docs, multiple pages of the AWS CDK Developers Guide, the GitHub examples repo, and some searching to find <a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html">the docs for &quot;@aws-cdk/aws-lambda-python-alpha module&quot;</a> (which is separate from the CDK API docs), we have enough documentation to use these new (and very helpful!) Python CDK constructs.</p><p>But, boy, did I have to work for it. I can&#39;t remember how much time it took for me to get a complete working example, and I shudder to think of how many dev-hours have been spent in a similar pursuit.</p><p>A complete, working code example would save oodles of time, and I&#39;m happy to oblige:</p><pre>from aws_cdk import Duration, RemovalPolicy, Stack<br>from aws_cdk import aws_dynamodb as dynamodb<br>from aws_cdk import aws_lambda as lambda_<br>from aws_cdk import aws_lambda_event_sources as event_sources<br>from aws_cdk import aws_lambda_python_alpha as lambda_python<br>from aws_cdk.aws_lambda_python_alpha import PythonFunction<br>from constructs import Construct<br><br><br>class WorkingStack(Stack):<br>    def __init__(self, scope: Construct, construct_id: str, **kwargs) -&gt; None:<br>        super().__init__(scope, construct_id, **kwargs)<br><br>        # Resources<br>        python_dependencies_layer = lambda_python.PythonLayerVersion(<br>            self,<br>            &quot;PoetryLayer&quot;,<br>            entry=&quot;./src/layer&quot;,<br>            compatible_runtimes=[lambda_.Runtime.PYTHON_3_10],<br>        )<br><br>        dynamodb_table: dynamodb.Table = dynamodb.Table(<br>            self,<br>            &quot;WorkingTable&quot;,<br>            partition_key=dynamodb.Attribute(<br>                name=&quot;PK&quot;, type=dynamodb.AttributeType.STRING<br>            ),<br>            stream=dynamodb.StreamViewType.NEW_IMAGE,<br>            sort_key=dynamodb.Attribute(name=&quot;SK&quot;, type=dynamodb.AttributeType.STRING),<br>            removal_policy=RemovalPolicy.RETAIN,<br>            billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,<br>        )<br><br>        # Functions<br>        react_to_new_entries_function: PythonFunction = PythonFunction(<br>            self,<br>            &quot;ReactToNewEntries&quot;,<br>            entry=&quot;./src/functions&quot;,<br>            index=&quot;react_to_new_entries.py&quot;,<br>            runtime=lambda_.Runtime.PYTHON_3_10,<br>            layers=[python_dependencies_layer],<br>            environment={<br>                &quot;TABLE_NAME&quot;: dynamodb_table.table_name,<br>            },<br>            timeout=Duration.seconds(29),<br>        )<br><br>        # Event handling<br>        react_to_new_entries_function.add_event_source(<br>            event_sources.DynamoEventSource(<br>                dynamodb_table,<br>                starting_position=lambda_.StartingPosition.TRIM_HORIZON,<br>                batch_size=5,<br>                bisect_batch_on_error=True,<br>                retry_attempts=1,<br>            )<br>        )<br><br>        dynamodb_table.grant_read_write_data(react_to_new_entries_function)</pre><p><strong>Info</strong>: I&#39;m working on another post showing a complete AWS CDK project with typed Python.Stay tuned!</p><h3>Summary</h3><h3>Documentation should yield more answers than questions</h3><p>If your documentation requires multiple other (possibly unlinked) documentation sources to be helpful, there&#39;s a problem. Code samples, especially, should be complete, to make it easy to get started using your code.</p><p><strong>Info</strong>: Maintaining complete and accurate code samples can be hard to maintain and verify, which is one of the reasons I once made <a href="https://github.com/johnfraney/flake8-markdown">flake8-markdown</a>, a little package to run flake8 on Python code blocks embedded in Markdown files.</p><p>(I should probably be using it on this site, now that I think of it.)</p><h3>Documentation should anticipate the information a reader is most likely looking for and make that easy to find</h3><p>If someone clicks a link about a VS Code extension, make the call-to-action to download that extension dead-simple to find. Additional information can be helpful, too, especially for someone learning a new skill or concept, but make it easy for someone who has a good idea what they&#39;re looking for.</p><h3>Last rites writes words</h3><p>Writing documentation is hard. And it&#39;s easy to pick on AWS, not only because they have <em>so much</em> documentation, but also because they have the resources to maintain great docs.</p><p>But reading documentation is much easier than writing it, and complaining about documentation is much easier than fixing it. So keep an eye out for things that make documentation great and grody, send up PRs, and write some code...y.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=15db9de0b0e6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/aws-documentation-frustrations-15db9de0b0e6">AWS documentation frustrations</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Use Vue in a static site with Web Component custom elements]]></title>
            <link>https://medium.com/codex/use-vue-in-a-static-site-with-web-component-custom-elements-91b09b9cb3e0?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/91b09b9cb3e0</guid>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[markdown]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[vuejs]]></category>
            <category><![CDATA[static-site]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Mon, 27 Mar 2023 01:06:02 GMT</pubDate>
            <atom:updated>2023-03-27T13:13:41.925Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YKCBOYFxRH1Hev4TSvjcug.png" /><figcaption>Vue logo inside opening &amp; closing HTML tags</figcaption></figure><h3>Learn how to use Vue to create custom elements that are easy to drop into your static site’s Markdown files</h3><h3>Intro</h3><p>Recently I rebuilt <a href="https://tabletomarkdown.com/">Table to Markdown</a>, converting it from a <a href="https://nuxt.com/">Nuxt</a> project to a static site built with <a href="https://gitlab.com/johnfraney/blurry">Blurry</a>, a pre-release static site generator (“SSG”) I’ve been working on. I wanted the simplicity and performance of a static site, but I still needed interactivity.</p><p><em>Although I use Blurry to generate static sites, this guide will work for any static site generator, like </em><a href="https://gohugo.io/"><em>Hugo</em></a><em> or </em><a href="https://jekyllrb.com/"><em>Jekyll</em></a><em>.</em></p><p>“What simplicity and performance?” you ask? Well, one challenge I had with the Nuxt.js hybrid version of Table to Markdown is that it was hard to accurately count page views since sometimes they would be tracked in server-side code and sometimes they’d be tracked in client-side code — and I didn’t want to count them twice. I had the same challenges with ad impressions, and accidentally counting those more than once could have gotten me in trouble with any ad networks I was using.</p><p>With a static site, those page views and ad impressions didn’t require any thought because they would fire when the HTML was loaded, and that was that.</p><p>Using Vue for the entire site led to quite a bit of JavaScript for things that I really didn’t need JavaScript for, like site navigation. Using a static site meant I could rely on regular hyperlink navigation without requiring a single line of JS, which meant faster page loads.</p><p>But, there was a catch: I still needed the interactivity I got from Vue, so I couldn’t replace <em>all</em> of the Vue code with a static site.</p><h3>Web Components with Vue</h3><p>To make it easy to integrate Vue with my static site, rather than using Vue to manage the whole page, I opted to use Vue in a single custom HTML element via Web Components.</p><p>For a sneak peak of what we’re building toward, here’s a snippet of a Markdown file from Table to Markdown:</p><pre># Convert spreadsheet cells to Markdown<br><br>&lt;div class=&quot;custom-element-container&quot;&gt;<br>  &lt;table-converter&gt;&lt;/table-converter&gt;<br>&lt;/div&gt;<br><br>Table to Markdown makes it easy to convert cells from Microsoft Excel, [...]&lt;div class=&quot;custom-element-container&quot;&gt;<br>  &lt;table-converter&gt;&lt;/table-converter&gt;<br>&lt;/div&gt;</pre><pre>Table to Markdown makes it easy to convert cells from Microsoft Excel, [...]</pre><p>Web Components allow you to create custom self-contained HTML elements that can leverage JavaScript to do whatever you want them to. A big benefit is that once you include the JS where the Web Components are defined, you can use them just like you’d use a regular HTML element, like a &lt;p&gt; or &lt;a&gt;.</p><p>Sounds good, eh? Almost <em>too</em> good? Well, there are some wrinkles with this approach, but they’re nothing we can’t iron out.</p><h3>Wrinkles</h3><h4>Layout shift once web component loads</h4><p>If you follow this blog, you’ll have noticed that I have a thing for PageSpeed scores.</p><p>One thing to keep in mind when using Web Components is that their height isn’t taken into account when the page renders.</p><p>By matching the height of your Web Components and the height of a web component wrapper, you can avoid content layout shifts, which are disruptive to your visitors:</p><pre>/* Main stylesheet */<br>.custom-element-container {<br>  min-height: 300px;<br>}</pre><p><em>You can use responsive heights in your custom element container so your content corresponds to your visitors’ screen sizes.</em></p><h4>CSS scoping</h4><p>To ensure that your styling is consistent in your static site and your web components, you can use the same CSS file in both places. Simply import your CSS file in your Vue component:</p><pre>&lt;style&gt;<br>@import(&#39;main.css&#39;)<br>&lt;/style&gt;</pre><p><em>Keep this CSS file as small as you can since its content may be loaded twice. Include only the necessary styles, and, for a page speed boost, consider </em><a href="https://web.dev/critical-rendering-path-page-speed-rules-and-recommendations/#inline-render-blocking-css"><em>inlining this CSS file</em></a><em> in your static site’s base template file.</em></p><h4>Child component CSS</h4><p>If your single-file Vue component uses other, non-custom element Vue components with &lt;style&gt; tags, you may be surprised to see those styles disappear when you use your custom element.</p><p>For some workarounds:</p><ul><li>@import() relevant CSS in your child components</li><li>Rather than scoping your CSS (&lt;style scoped&gt;) in your top-level component, define CSS rules that will also apply to child components</li><li>Style classes, elements, or use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties">CSS custom properties (variables)</a> in inline style=&quot;&quot;/:style=&quot;{}&quot; attributes on elements in your child components</li></ul><h3>Implementation</h3><p>Now that we’ve identified some wrinkles and ways to smooth them out, it’s time to see how to convert a Vue file into a custom element.</p><h4>Vite config</h4><p>First, update the Vue plugin for <a href="https://vitejs.dev/">Vite</a> to generate web components, and configure Vite to place your built components where you want them:</p><pre>import { fileURLToPath, URL } from &#39;node:url&#39;<br>import { resolve } from &#39;path&#39;<br><br>import { defineConfig } from &#39;vite&#39;<br>import vue from &#39;@vitejs/plugin-vue&#39;<br>// https://vitejs.dev/config/<br>export default defineConfig({<br>  // Get Vue to output custom elements<br>  plugins: [vue({ customElement: true })],<br>  resolve: {<br>    alias: {<br>      &#39;@&#39;: fileURLToPath(new URL(&#39;./src&#39;, import.meta.url))<br>    }<br>  },<br>  build: {<br>    // Specify a directory in your SSG&#39;s build directory<br>    outDir: &#39;dist/elements&#39;,<br>    rollupOptions: {<br>      // Improvement: loop through files in the elements directory to build this object<br>      input: {<br>        &#39;your-component&#39;: resolve(__dirname, &#39;src/elements/your-component.ts&#39;),<br>      },<br>      // Use predictable file names<br>      output: {<br>        entryFileNames(chunkInfo) {<br>          return `${chunkInfo.name}.js`<br>        },<br>      },<br>    }<br>  }<br>})</pre><p><em>For more info on using Vue to build web components, check out </em><a href="https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue#using-vue-sfcs-as-custom-elements"><em>the Vue docs</em></a><em>.</em></p><h4>Custom element file</h4><p>In a separate file, define your Vue component as a custom element, which will make your component available to use in the HTML DOM:</p><pre>// src/elements/your-component.ts<br>import { defineCustomElement } from &#39;vue&#39;<br><br>import YourComponent from &#39;@/components/YourComponent.vue&#39;<br><br>customElements.define(&#39;your-component&#39;, defineCustomElement(YourComponent))</pre><p>Creating a separate file for each web component can help keep your JavaScript bundles small by including only the required code for that component.</p><p><em>You can also define many custom elements in a single file. See </em><a href="https://vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue"><em>the Vue docs</em></a><em> for more info and recommendations for building a library of custom elements.</em></p><h4>Include the JS and custom element in your SSG files</h4><p>Now all that’s left is do is to include your custom element .js file in the relevant template file of your SSG:</p><pre>&lt;script src=&quot;/elements/your-component.js&quot; type=&quot;module&quot;&gt;&lt;/script&gt;<br><br>&lt;your-component&gt;&lt;/your-component&gt;</pre><h3>Wrap-up</h3><p>That’s a wrap.</p><p>This article is wrapped up like a Vue single-file component in a Web Component custom element after following the above steps.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=91b09b9cb3e0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/use-vue-in-a-static-site-with-web-component-custom-elements-91b09b9cb3e0">Use Vue in a static site with Web Component custom elements</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[SEO-driven feature development: a success story]]></title>
            <link>https://johnfraney.medium.com/seo-driven-feature-development-a-success-story-8e6c4668e8c9?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/8e6c4668e8c9</guid>
            <category><![CDATA[project-management]]></category>
            <category><![CDATA[side-hustle]]></category>
            <category><![CDATA[markdown]]></category>
            <category><![CDATA[seo]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Mon, 02 Jan 2023 17:43:06 GMT</pubDate>
            <atom:updated>2023-01-02T17:43:06.344Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*peuNTqZgAwDxI4bX.png" /><figcaption>Table to Markdown’s “Markdown Table Generator” page</figcaption></figure><h3>Introduction</h3><p>SEO is tricky business — -or used to be, at least. In the history SEO, there were plenty of tricks people would use to try to improve their sites’ Google rankings, like <a href="https://developers.google.com/search/docs/essentials/spam-policies?hl=en#keyword-stuffing">keyword stuffing</a>, the practice of cramming specific words or phrases into site content so it masquerades as a better resource for those search terms than it actually is.</p><p>See Google Search Central’s <a href="https://developers.google.com/search/docs/essentials/spam-policies?hl=en">Spam policies for Google web search</a> for a rundown of different SEO “tricks” that sites are punished for nowadays.</p><p>With time, search engines got better at sniffing out sites employing these SEO hacks, which led to more and different SEO tricks to try to game search engines’ ranking algorithms. In 2006, Douglas Merrill, who served as Google’s Chief Information Officer and VP of Engineering, summarized:</p><blockquote>Spam is an arms race.</blockquote><blockquote>— Douglas Merrill of Google in BBC’s <a href="http://news.bbc.co.uk/2/hi/technology/5140066.stm">Google to stay focused on search</a></blockquote><p>This post isn’t about a shiny new SEO trick. If you’re looking to improve your site’s search performance, there are reams of online resources on how to improve a site’s SEO, and this article doesn’t purport to be another.</p><p>Many guides assume that your site’s content already exists and that you need to tweak either your content or your site’s metadata to improve your site’s search performance. In other words, changing content to build search performance.</p><p>This post takes the opposite approach: using search results to decide what to build.</p><h3>The problem</h3><p>Since first launching tabletomarkdown.com, I described it as a table/spreadsheet to Markdown <em>converter</em>: taking one type of table and converting it into another. With time and once the site got a little bit of traffic, I was able to see in Google Search Console that many more people searched for “markdown table generator” than any search terms I expected, like:</p><ul><li>“excel to markdown”</li><li>“spreadsheet to markdown”</li><li>“web table to markdown”</li></ul><p>I did pretty well for those search terms, eventually ranking as the 1st or 2nd Google search result. But I still ranked pretty low for “markdown table generator”, and the search volume for that term is 2x greater than those other three combined.</p><p>After adding the ability to <a href="https://tabletomarkdown.com/format-markdown-table/">format an existing Markdown table</a>, I jumped up in the search results for Markdown table formatting-relating search terms. But still, the biggest opportunity by far was “markdown table generator”.</p><h3>The experiment</h3><p>For quite a while, I thought that searchers were using “converter” and “generator” interchangeably, but they <em>are</em> pretty different. For a Markdown table converter to work, you need a table to exist in order to make it into something new. To generate a table, however, you’re starting from scratch.</p><p>But to generate a table generator, thankfully <em>I</em> didn’t have to start from scratch.</p><h3>Generating a table from scratch</h3><p>The big trick that makes Table to Markdown possible is that when you copy tables from a website or from a spreadsheet application, the table is stored in your clipboard both as both plain text and HTML.</p><p>Because Table to Markdown’s code could already convert an HTML table to Markdown, all I needed was an easy way for visitors to build an HTML table. The interactive elements of Table to Markdown are built using <a href="https://vuejs.org/">Vue.js</a>, and after some poking around, I found developer-friendly, Vue-compatible WYSIWYG (“What You See Is What You Get”) HTML editor with <a href="https://tiptap.dev/api/nodes/table">a solid table plugin</a>: <a href="https://tiptap.dev/">Tiptap</a>.</p><p>What are the non-interactive aspects of Table to Markdown built with? Good question! I’ll write a post about that soon.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/959/0*nXoD1qdPk1ZqERkp.png" /><figcaption>Screenshot of Tiptap’s table editor demo</figcaption></figure><p>With a table generator in hand, all I had to do was write some content and publish the new page.</p><h3>The results</h3><p>The experiment was a success! I <a href="https://ko-fi.com/i/IF1F1F4UOU">launched the new feature</a> on September 19th, 2022, and it didn’t take long before I started getting more clicks for “markdown table generator”:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kJeUh7d6KDYyiaxL.png" /><figcaption>Table to Markdown Google Search Console traffic August — November 2022</figcaption></figure><p>Before September 19th it was rare to get more than 15 clicks/day for that search term, but shortly afterwards, I was getting 15 clicks/day multiple weekdays a week. (Traffic is reliably slower on weekends when people tend to take time off from tampering with tables.)</p><p>I was already appearing in plenty of searches for “markdown table generator”, but after providing that capability to my visitors, I started to creep up in the rankings — -and my click-through rate followed suit:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/912/0*EpBlqxX8ZqBDDmml.png" /><figcaption>Table to Markdown Google Search Console impressions &amp; click-through rate</figcaption></figure><p>We can see that impressions (purple line) didn’t change much, but look at how the Click-Through Rate (CTR; green line) jumps up. It went from under 4% to 6%: an increase of over 50%!</p><p>In the month following the launch of the new Markdown table generator (October, 2022), “markdown table generator” became Table to Markdown’s fastest growing search query:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/384/0*iLhSNP9sRhThxEKs.png" /><figcaption>Top growing Google search queries for tabletomarkdown.com, October 2022</figcaption></figure><p>Stats for that query continued to look good through November, too, with October growing by 32% and November growing a further 30%.</p><p>Put those numbers on a graph and they’re a growth marketer’s dream: up and to the right!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/498/0*AkA1jbq9an9x6eT8.gif" /><figcaption>“In conclusion: the lines all go up, so I’m happy.”</figcaption></figure><h3>Wrap-up</h3><p>This post isn’t going to win any awards for cutting-edge SEO techniques or feature development strategies. I hope it serves as a reminder to check out search terms that are related to your site but that may not be directly related.</p><p>If there’s a juicy search term that you’re missing out on, with a bit of work, you may not miss out on it for much longer.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8e6c4668e8c9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to use DynamoDB with Python type hints]]></title>
            <link>https://johnfraney.medium.com/how-to-use-dynamodb-with-python-type-hints-d9ce1504adc1?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/d9ce1504adc1</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[python-types]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[dynamodb]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Thu, 30 Jun 2022 02:55:01 GMT</pubDate>
            <atom:updated>2022-06-30T02:55:01.406Z</atom:updated>
            <content:encoded><![CDATA[<p>Power up your Python DynamoDB project by adding types using user-defined type guards and TypedDicts</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/703/1*8asDBeJehtvgqHntZowzFQ.png" /></figure><h3>Introduction</h3><p><em>To see all the code used in this post, check out </em><a href="https://github.com/johnfraney/blog-examples/tree/master/dynamodb-python-type-hints"><em>my blog examples repo</em></a><em>. It even has unit tests!</em></p><p>Python has had <a href="https://peps.python.org/pep-0484/">type hints</a> since <a href="https://www.python.org/downloads/release/python-350/">version 3.5</a>, released way back in September 2015.</p><p>Since then, some pretty cool projects have spun up, like <a href="https://github.com/mypyc/mypyc">mypyc</a>, which “uses standard Python type hints to generate fast code”, and Microsoft’s <a href="https://github.com/Microsoft/pyright">Pyright</a>, a static type checker that plays nicely with their <a href="https://code.visualstudio.com/">Visual Studio Code</a> editor.</p><p>Back in 2019, Airbnb mentioned that around 38% of their bugs would have been prevented if they’d been using types in their codebase all along (<a href="https://news.ycombinator.com/item?id=19131272">HackerNews discussion</a>). Although Airbnb was talking about JavaScript bugs that could have been prevented by using TypeScript, the general theme of types preventing errors by catching type issues before they’re exposed in runtime holds true for Python, too.</p><h3>Type Support</h3><p>When reading AWS’s <a href="https://docs.aws.amazon.com/code-samples/latest/catalog/code-catalog-python-example_code-dynamodb.html">Python Code Samples for Amazon DynamoDB</a> or the <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html">boto3 documentation</a>, it isn’t clear how to use DynamoDB with Python types because neither source uses type hints. There are third-party projects that look to fill this gap, like <a href="https://youtype.github.io/boto3_stubs_docs/">boto3-stubs</a>, part of the <a href="https://github.com/youtype/mypy_boto3_builder">mypy_boto3_builder</a> project.</p><p>boto3-stubs gives us quite a bit of type information and can help speed up development by providing auto-completion in your code editor, but it doesn&#39;t know <em>exactly</em> what we&#39;re putting into or getting out of a DynamoDB table.</p><p>Let’s take a look at the default type for a DynamoDB item, taken here from the return value of table.get_item():</p><pre>&quot;Item&quot;: Dict[<br>    str,<br>    Union[<br>        bytes,<br>        bytearray,<br>        str,<br>        int,<br>        Decimal,<br>        bool,<br>        Set[int],<br>        Set[Decimal],<br>        Set[str],<br>        Set[bytes],<br>        Set[bytearray],<br>        Sequence[Any],<br>        Mapping[str, Any],<br>        None,<br>    ],<br>],</pre><p>So a DynamoDB item is a dict with strings for keys and values that could be <em>almost</em> anything. Maybe it&#39;s an int, or maybe it&#39;s a Set of &#39;em, or maybe even some kind of map of str to int, or, if you can believe it, a sequence of Any! Why not?!</p><p>So how can we type individual DynamoDB items? Read on, dear reader. Read on.</p><h3>Typing a DynamoDB Item</h3><p>In the spirit of <a href="https://nordicapis.com/using-a-schema-first-design-as-your-single-source-of-truth/">schema first design</a>, to start, we’ll create a type for our DynamoDB item. As we can see above, a DynamoDB item is returned as a dict, and to properly type that dict, we’ll use — contain your surprise — a <a href="https://www.python.org/dev/peps/pep-0589/">TypedDict</a>.</p><p>A TypedDict &quot;support[s] the use case where a dictionary object has a specific set of string keys, each with a value of a specific type&quot;, so we can guarantee both a dict&#39;s key names and its value types. This allows us to benefit from typing without having to translate the dict into, say, a <a href="https://docs.python.org/3/library/dataclasses.html">Data Class</a>, then back to a dict.</p><p>As an example, here’s a type for a news article, something you might find in an RSS feed:</p><pre># news/types.py<br>from typing import TypedDict<br></pre><pre>class NewsItem(TypedDict):<br>    PK: str<br>    SK: str<br>    title: str<br>    description: str | None<br>    published: str<br>    link: str</pre><p>This gives us a type we can use in application code, but we still need a way to ensure that the type we’re putting into and pulling out of a DynamoDB table actually matches that type.</p><p>To ensure the DynamoDB item matches our more specific NewsItem type, we&#39;ll define some type guards (<a href="https://peps.python.org/pep-0647/">PEP-647</a>) to do type narrowing. This will ensure that the values contained in a DynamoDB item are the types we expect.</p><p>This example is a little sparse because a NewsItem has only two different types (str and str | None), but this technique will work for items with more varied types, too.</p><pre># news/type_guards.py<br>from typing import Any<br></pre><pre>class TypeGuardException(Exception):<br>    pass<br></pre><pre>def guard_optional_string(value: Any) -&gt; str | None:<br>    if value is None:<br>        return value<br>    if isinstance(value, str):<br>        return value<br>    raise TypeGuardException(<br>        f&quot;Received unexpected type: &quot;<br>        f&quot;expected str | None but received value of type {type(value)}: {value}&quot;<br>    )<br></pre><pre>def guard_string(value: Any) -&gt; str:<br>    if isinstance(value, str):<br>        return value</pre><pre>    raise TypeGuardException(<br>        f&quot;Received unexpected type: &quot;<br>        f&quot;expected str but received value of type {type(value)}: {value}&quot;<br>    )</pre><p>These functions are pretty simple: they take in a value that can be any type, and they either raise an exception or spit out that same value. The key difference is in the return type, like -&gt; str: now our type checker knows exactly what type value is.</p><p>Let’s see how these type guards work in practice:</p><pre>&gt;&gt;&gt; guard_optional_string(&#39;En garde!&#39;) == &#39;En garde!&#39;<br>True<br>&gt;&gt;&gt; guard_optional_string(None) is None<br>True<br>&gt;&gt;&gt; guard_optional_string(100) is 100<br>Traceback (most recent call last):<br>  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;<br>  File &quot;/home/john/Code/blog-examples/dynamodb-python-type-hints/news/type_guards.py&quot;, line 15, in guard_optional_string<br>    raise TypeGuardException(<br>news.type_guards.TypeGuardException: Received unexpected type: expected str | None but received value of type &lt;class &#39;int&#39;&gt;: 100</pre><p>Now that we can guarantee the types of individual fields, we can use these type guards to narrow the entire NewsItem dict:</p><pre># news/type_guards.py<br>from news.types import NewsItem<br></pre><pre>def guard_news_item(item: dict) -&gt; NewsItem:<br>    return NewsItem(<br>        PK=guard_string(item.get(&quot;PK&quot;)),<br>        SK=guard_string(item.get(&quot;SK&quot;)),<br>        title=guard_string(item.get(&quot;title&quot;)),<br>        description=guard_optional_string(item.get(&quot;description&quot;)),<br>        published=guard_string(item.get(&quot;published&quot;)),<br>        link=guard_string(item.get(&quot;link&quot;)),<br>    )</pre><p>The guard_string() and guard_optional_string() functions will throw if there&#39;s an issue initializing a NewsItem, so we don&#39;t need to throw a separate exception:</p><pre>&gt;&gt;&gt; from news.type_guards import guard_news_item<br>&gt;&gt;&gt; guard_news_item({&quot;PK&quot;: &quot;News&quot;})</pre><p>Because we’re trying to create a NewsItem without all the necessary fields, guard_news_item() raises an exception:</p><pre>Traceback (most recent call last):<br>  File &quot;&lt;stdin&gt;&quot;, line 1, in &lt;module&gt;<br>  File &quot;/home/john/Code/blog-examples/dynamodb-python-type-hints/news/type_guards.py&quot;, line 34, in guard_news_item<br>    SK=guard_string(item.get(&quot;SK&quot;)),<br>  File &quot;/home/john/Code/blog-examples/dynamodb-python-type-hints/news/type_guards.py&quot;, line 25, in guard_string<br>    raise TypeGuardException(<br>news.type_guards.TypeGuardException: Received unexpected type: expected str but received value of type &lt;class &#39;NoneType&#39;&gt;: None</pre><p>We can see in the traceback that the error is a TypeGuardException raised because SK is None. That&#39;s a required field and needs to be a string, and we&#39;d keep getting type errors for invalid or missing values until the item we created matches NewsItem. Our NewsItem type is safe and sound!</p><h3>Wrap-up</h3><p>Sure, Python is a <a href="https://realpython.com/lessons/duck-typing/">duck-typed</a> language rather than a strictly typed language, but with a bit of upfront work, type guards and TypedDicts can add type safety to your Python AWS project by guaranteeing that your DynamoDB inputs and outputs are the types you expect.</p><p>In other words, when it comes to typing, guards give a duck teeth:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/720/1*NmgoIba9ISDjXr5aOckTcQ.gif" /></figure><h3>Bonus: abstracting DynamoDB logic into a controller</h3><p>As a bonus, here’s an example of how I’m using this code in a project. The database logic is nicely contained in a controller class, and the class takes a DynamoDB Table instance in its constructor, so it&#39;s easy to test the controller logic using unit tests (by <a href="http://127.0.0.1:8000/blog/unit-test-dynamodb-python-pytest-dynalite">passing in a </a><a href="http://127.0.0.1:8000/blog/unit-test-dynamodb-python-pytest-dynalite">dynalite connection</a>, say).</p><pre># news/controllers.py<br>from typing import Iterator, TypedDict</pre><pre>from boto3.dynamodb.conditions import Key<br>from mypy_boto3_dynamodb.service_resource import Table</pre><pre>from news.type_guards import guard_news_item<br>from news.types import NewsItem<br></pre><pre>class PutNewsItemsResponse(TypedDict):<br>    saved_item_count: int<br></pre><pre>class NewsController:<br>    def __init__(self, dynamo_table: Table):<br>        self.dynamo_table = dynamo_table</pre><pre>    def get_newest_news_item(self) -&gt; NewsItem | None:<br>        newest_news_items = self.dynamo_table.query(<br>            KeyConditionExpression=Key(&quot;PK&quot;).eq(&quot;News&quot;),<br>            ScanIndexForward=False,<br>            Limit=1,<br>        )[&quot;Items&quot;]<br>        if not newest_news_items or not newest_news_items[0]:<br>            return None<br>        newest_item = guard_news_item(newest_news_items[0])<br>        return newest_item</pre><pre>    def put_items(self, items: Iterator[NewsItem]) -&gt; PutNewsItemsResponse:<br>        saved_item_count = 0<br>        with self.dynamo_table.batch_writer() as batch:<br>            for item in items:<br>                batch.put_item(Item=item)<br>                saved_item_count += 1<br>        return {&quot;saved_item_count&quot;: saved_item_count}</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d9ce1504adc1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Lessons learned from a small side project]]></title>
            <link>https://johnfraney.medium.com/lessons-learned-from-a-small-side-project-58f34aaea21c?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/58f34aaea21c</guid>
            <category><![CDATA[css]]></category>
            <category><![CDATA[html]]></category>
            <category><![CDATA[side-hustle]]></category>
            <category><![CDATA[seo]]></category>
            <category><![CDATA[markdown]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Thu, 10 Mar 2022 12:25:05 GMT</pubDate>
            <atom:updated>2022-03-10T12:25:05.617Z</atom:updated>
            <content:encoded><![CDATA[<p><em>This post was mostly written in April, 2021, and although some of the data is out-of-date, the trends hold true. What a year!</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_0jMvWDGq70ABtEy.png" /><figcaption>Homepage of tabletomarkdown.com</figcaption></figure><h3>Technical challenges</h3><h3>Trojan horse CSS</h3><p>(I almost wrote “Trojan horCSS” but thought better of it.)</p><p>When an HTML element says it’s contenteditable=&quot;true&quot;, it really means it. That element is so editable that it can change an entire page:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kKVwouPQkb4bxIl7.gif" /><figcaption>Table to Markdown CSS changing when pasting into a content editable div</figcaption></figure><p>Hey! You stole my font!</p><p>What’s going on there? On pasting a table from <a href="https://www.libreoffice.org/discover/calc/">LibreOffice Calc</a>, a secret stylesheet sneaks onto the scene and supersedes some site-wide styling.</p><pre>// The rest of this Vue.js 2.x class-based component is omitted for brevity<br>@Component<br>export default class Editor extends Vue {<br>  public observer?: MutationObserver = undefined</pre><pre>  mounted() {<br>   // Prevent adding script and style tags on paste<br>   this.observer = new MutationObserver(filterForbiddenElements)<br>   this.observer.observe(this.$refs.pasteBox, { childList: true })<br>  }</pre><pre>  destroy() {<br>    if (!this.observer) {<br>      return<br>    }<br>    this.observer.disconnect()<br>  }<br>}</pre><p>To stop this style stick-up, I added a <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">MutationObserver</a> to watch for HTML being added to the paste box and remove any offending elements, like &lt;style&gt; and &lt;script&gt;.</p><h3>Partial tables</h3><p>Take a look at this table:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/278/0*bNfEbKL0Srnprr47.png" /><figcaption>A fully selected table? Maybe.</figcaption></figure><p>Sure, the table content is selected — but is the <em>table</em> selected? It’s hard to say. When I first coded Table to Markdown, selecting from just to the left of “Measurement” and just to the right of “25%” would result in HTML like this being added to the clipboard:</p><pre>&lt;th&gt;Measurement&lt;/th&gt;  <br>&lt;th&gt;Value&lt;/th&gt;  <br>&lt;th&gt;Reduction&lt;/th&gt;  <br>&lt;tr&gt;  <br>  &lt;td&gt;Max width&lt;/td&gt;  <br>  &lt;td&gt;428px&lt;/td&gt;  <br>  &lt;td&gt;32%&lt;/td&gt;  <br>&lt;/tr&gt;  <br>...</pre><p>If you selected from above the table to below the table? You’d get a full &lt;table&gt; element, complete with a &lt;thead&gt; and &lt;tbody&gt;.</p><p>Browsers seem to have gotten better at recognizing partially-selected tables. In my testing for this post, both Firefox and Chromium add a complete &lt;table&gt; element to the clipboard. I <em>could</em> remove the code that builds a full table from a partial one, perhaps, but it&#39;s not hurting anyone.</p><h3>Traffic &amp; SEO</h3><p>For me, most of the fun of publishing side projects is having numbers to look at. It’s even more fun if the numbers go up, so let’s take a look at some numbers that go up and a couple ways to steer them in that direction.</p><h3>Web traffic</h3><p>These numbers show visitors/users rather than page views. Why not page views? Well, I was counting each page view twice for quite a while (oops), so number of users is a more accurate metric.</p><p>First, let’s take a look at web traffic since launch. I say “launch”, but that’s not quite true. The first real mention of Table to Markdown was in <a href="https://johnfraney.ca/blog/improve-page-speed-google-fonts/">this blog post</a> from December 9, 2019. That post got a decent amount of traffic, and 41 users decided to check out a “pre-launch version” of Table to Markdown, resulting in the first little spike near the y-axis:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/397/0*LWRr86nxzmEUHxU1.png" /><figcaption>Visitors November 28, 2019 — March 31, 2021</figcaption></figure><p>The next big spike is in March 2021, and although I got excited when I first saw that traffic, I’m <em>pretty</em> sure that was bot traffic. Let’s pretend that last spike doesn’t exist.</p><p>Take another look at that chart, and look at the numbers going up! It’s a slow and steady climb, but in a bit over a year, the site went from almost no traffic in a day to what looks to be peaks of around 150 users.</p><p>Speaking of peaks: what’s up with all those ups and downs? It looks like a chart of yo-yo distance from the ground. It turns out that there is a good reason for those peaks and valleys: weekends.</p><p>If we zoom into the four weeks ending April 14, 2021, we can see that Saturday (March 20th &amp; 27th, April 3rd &amp; 10th) is consistently the slowest day of the week, with Sunday right behind:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/397/0*lyDMYOwYMyE4f67n.png" /><figcaption>Website traffic March 18 — April 14, 2021</figcaption></figure><p>What can we learn from this? In all likelihood, people are using Table to Markdown at work. Wednesday is often slower than Monday, Tuesday, and Thursday, too. Hump day? More like slump day.</p><p>Now that we’ve seen some numbers that go up, let’s check out some more: organic search traffic.</p><h4>Google traffic</h4><p>Remember that graph “Users November 28, 2019 — March 31, 2021”? If we ignore that bot spike in March, the user traffic correlates well with the number of clicks per day from Google searches:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/757/0*IuBokKmENSNmsCwF.png" /><figcaption>Google Search Console clicks November 28, 2019 — March 31, 2021</figcaption></figure><p>It correlates almost <em>too</em> well, doesn’t it? A person might think that virtually all my traffic comes from Google. Well, a person thinking that would be <strong>wrong</strong>! Well, <em>wrong</em>. Okay: just a little wrong.</p><p>Here are the top 8 sources user traffic as of April 14th, 2021:</p><ol><li>Google (77.4%)</li><li>Direct (9.5%)</li><li>Bing (5.5%)</li><li>DuckDuckGo (3.4%)</li><li>Bing (China) (0.93%)</li><li><a href="https://johnfraney.ca">My blog</a> (0.57%)</li><li>Ecosia (0.49%)</li><li>GitHub (0.32%)</li></ol><p>More than three out of every four users finds Table to Markdown via Google, and almost one in ten come to the site directly (presumably by copying a link from a search engine or social media instead of clicking it).</p><p>At first glance, non-Google, non-direct traffic is coming from all over. Apart from my blog and GitHub, though, there’s <em>sort of</em> one source that accounts for 10% of my traffic: Bing.</p><h3>Bing? Oh, right. Bing</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/0*s4tHVXmb7HdTLqzs.png" /></figure><p>If website users were money and Bing were a banana stand:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/498/0*3n7QkLPoBBiN_8Jd.gif" /></figure><p>10% of users may not seem like a ton, but Google wasn’t always my top source of traffic. Something I didn’t expect when the site was young is that I got more traffic from <a href="https://bing.com">Bing</a> than I did from Google. (Good thing I submitted a sitemap to <a href="https://bing.com/webmasters/about">Bing Webmaster Tools</a>!)</p><p>In the first three months after launch, I had 21 users from Google, and 34 users from Bing:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/480/0*1m_6ZiJSFJ2gR_g4.png" /><figcaption>Web traffic by referral source, December 1, 2019 to March 31, 2020</figcaption></figure><p>What about that other organic search traffic? Well, DuckDuckGo also <a href="https://help.duckduckgo.com/duckduckgo-help-pages/results/sources/">uses Bing for most of its regular search results</a>, as does <a href="https://ecosia.zendesk.com/hc/en-us/articles/206153381-Where-do-Ecosia-s-search-results-come-from-">Ecosia</a>. Knowing that, Table to Markdown had 3x the traffic from Bing-powered search engines than from Google in the early days, and around 10% overall.</p><p>Don’t forget about Bing!</p><h3>To “convert” or not convert</h3><p>My web copy focused on “converting” from a number of sources to Markdown, but as we can see from the following impression count, most users search how to <em>generate</em> Markdown tables:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/677/0*WpTAnAwFcL8k-4RX.png" /><figcaption>Selected Google Search Console performance, March 19–25, 2021</figcaption></figure><p>Because of this, my click-through rate (CTR) for queries containing “convert” is pretty solid! I was a bit <a href="https://dictionary.cambridge.org/dictionary/english/be-penny-wise-and-pound-foolish">penny-wise, pound-foolish</a> in this case because I optimized for less-common search terms instead of the most common search term for my content.</p><h3>Take-aways</h3><ol><li>Clipboard HTML can be a mangled mess, and pasting it into a content editable div can bork your site</li><li>Submit a sitemap to <a href="https://bing.com/webmasters/about">Bing Webmaster Tools</a></li><li>Use language that <em>searchers</em> use to describe your site and content</li></ol><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=58f34aaea21c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unit test DynamoDB in Python with pytest and dynalite]]></title>
            <link>https://awstip.com/unit-test-dynamodb-in-python-with-pytest-and-dynalite-44f594e86887?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/44f594e86887</guid>
            <category><![CDATA[tdd]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[dynamodb]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Mon, 07 Feb 2022 15:50:18 GMT</pubDate>
            <atom:updated>2022-07-22T20:22:23.352Z</atom:updated>
            <content:encoded><![CDATA[<h4>Introduction</h4><p><em>TL;DR? Test-driven development of DynamoDB queries is difficult, but it’s possible with dynalite.</em></p><p><em>A working example of this code can be found in </em><a href="https://github.com/johnfraney/blog-examples/tree/master/unit-test-dynamodb-python-pytest-dynalite"><em>my blog-examples repo</em></a><em>.</em></p><p>Recently I was working on a little project recently with Python and <a href="https://github.com/aws/chalice">Chalice</a>, a serverless framework Python. I had a logic error in one of my <a href="https://aws.amazon.com/dynamodb/">DynamoDB</a> queries that I didn’t catch until I deployed the project. Because I was new to using DynamoDB with Python, it took a few more deployments over an hour before I fixed the query.</p><p>Do you know what would’ve been nice? Having access to DynamoDB locally so I could have written that query using <a href="http://steipe.biochemistry.utoronto.ca/bio/FND-CSC-Test_driven_development.html">Test-Driven Development</a> (TDD).</p><p>Why couldn’t I? Well, code used in serverless development, like with <a href="https://aws.amazon.com/lambda/">AWS Lambda</a>, isn’t designed to run on your local machine:</p><blockquote><em>Serverless is a cloud-native development model that allows developers to build and run applications without having to manage servers. (</em><a href="https://www.redhat.com/en/topics/cloud-native-apps/what-is-serverless"><em>Red Hat: “What is serverless?”</em></a><em>)</em></blockquote><p>The “cloud-native” descriptor is important here. With serverless development, and Function-as-a-Service (FaaS) development specifically in this case, it’s possible to publish a function without, say, having to <a href="https://nginx.org/en/docs/beginners_guide.html">set up a webserver</a>, <a href="https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/">configure a reverse proxy</a>, and <a href="https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/">add SSL</a>. The servers are still <em>there</em>, of course, but the server management is abstracted away by the cloud hosting provider.</p><p>Serverless development being cloud-native is a double-edged sword, however, because although you can build applications without administering servers by leveraging cloud development frameworks, your applications are now designed to run in a cloud environment.</p><p>What does this mean? For one, it can be difficult to run your applications on your local machine. What does <em>that</em> mean? Testing complicated queries in a cloud-native database like DynamoDB can be error-prone and cumbersome.</p><p>There are services to help with local serverless develoment, like <a href="https://github.com/localstack/localstack">LocalStack</a>, and frameworks that make cloud development feel more like local development, like <a href="https://serverless-stack.com/">Serverless Stack</a>, but LocalStack was overkill for me and I didn’t want to refactor my code to work with Serverless Stack. I just needed a way to test functions containing DynamoDB queries since I could test the rest of my application using mocks.</p><p>That’s where dynalite comes in.</p><h3>Setup</h3><p>To unit test DynamoDB locally, we’re going to use <a href="https://github.com/mhart/dynalite">dynalite</a>, an in-memory implementation of DynamoDB, along with <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html">boto3</a> for Python bindings to DynamoDB, and <a href="https://docs.pytest.org/en/stable/">pytest</a> to run the unit tests.</p><p>First, <a href="https://github.com/mhart/dynalite#installation">install dynalite</a> globally:</p><pre>npm install -g dynalite</pre><p>or locally:</p><pre>npm install -D dynalite</pre><p>Then, install pytest and boto3, the AWS Python SDK:</p><pre>poetry add boto3 pytest</pre><p>I like to use Poetry for <a href="https://johnfraney.ca/blog/create-publish-python-package-poetry">package</a> and <a href="https://johnfraney.ca/blog/pipenv-poetry-benchmarks-ergonomics-2">dependency</a> management, but you could use <a href="https://pipenv.pypa.io/en/latest/">Pipenv</a> or pip.</p><h3>Pytest config</h3><p>Now that we have our dependencies installed, let’s tie things together.</p><p>To use dynalite in our unit tests, we’ll need to ensure that dynalite is running before our unit tests start. We could start dynalite in a separate terminal window before running pytest and stop that dynalite process after the tests finish, but that sounds like a lot of work. Instead, we can use a <a href="https://docs.pytest.org/en/stable/fixture.html#fixture">pytest fixture</a> in a file called conftest.py:</p><pre># conftest.py<br>import subprocess</pre><pre>import boto3<br>from pytest import fixture</pre><pre>TABLE_NAME = &quot;PersonTestTable&quot;<br>DYNALITE_PORT = &quot;4567&quot;<br></pre><pre>@fixture(scope=&quot;session&quot;)<br>def dynamodb_table(request):<br>    proc = subprocess.Popen(<br>        [&quot;dynalite&quot;, &quot;--port&quot;, DYNALITE_PORT, &quot;--createTableMs&quot;, &quot;0&quot;]<br>    )<br>    dynamodb = boto3.resource(<br>        &quot;dynamodb&quot;,<br>        endpoint_url=f&quot;http://localhost:{DYNALITE_PORT}&quot;,<br>    )</pre><pre>    request.addfinalizer(lambda: proc.kill())</pre><pre>    try:<br>        table = dynamodb.create_table(<br>            TableName=TABLE_NAME,<br>            KeySchema=[<br>                {&quot;AttributeName&quot;: &quot;PK&quot;, &quot;KeyType&quot;: &quot;HASH&quot;},<br>                {&quot;AttributeName&quot;: &quot;SK&quot;, &quot;KeyType&quot;: &quot;RANGE&quot;},<br>            ],<br>            AttributeDefinitions=[<br>                {&quot;AttributeName&quot;: &quot;PK&quot;, &quot;AttributeType&quot;: &quot;S&quot;},<br>                {&quot;AttributeName&quot;: &quot;SK&quot;, &quot;AttributeType&quot;: &quot;S&quot;},<br>            ],<br>            BillingMode=&quot;PAY_PER_REQUEST&quot;,<br>        )<br>        yield table<br>    except Exception:<br>        yield dynamodb.Table(TABLE_NAME)</pre><p>This feature does a few things.</p><p>First, it’s <a href="https://docs.pytest.org/en/stable/fixture.html#fixture-scopes">scoped</a> to the pytest session so it will be available to every test that runs in that pytest invocation:</p><pre>@fixture(scope=&quot;session&quot;)<br>def dynamodb_table(request):</pre><p>You may want to change the scope to function if you want to start with a clean database for each test.</p><p>Second, because this fixture runs dynalite in a subprocess, we need to stop that subprocess once the tests have run. To do that, the fixture includes a &quot;finalizer&quot;, a callable that contains cleanup/teardown code (<a href="https://docs.pytest.org/en/stable/fixture.html#adding-finalizers-directly">docs</a>):</p><pre># proc is the subprocess running dynalite<br>request.addfinalizer(lambda: proc.kill())</pre><p>Third, the fixture creates a DynamoDB table:</p><pre>table = dynamodb.create_table(<br>    TableName=TABLE_NAME,<br>    KeySchema=[<br>        {&quot;AttributeName&quot;: &quot;PK&quot;, &quot;KeyType&quot;: &quot;HASH&quot;},<br>        {&quot;AttributeName&quot;: &quot;SK&quot;, &quot;KeyType&quot;: &quot;RANGE&quot;},<br>    ],<br>    AttributeDefinitions=[<br>        {&quot;AttributeName&quot;: &quot;PK&quot;, &quot;AttributeType&quot;: &quot;S&quot;},<br>        {&quot;AttributeName&quot;: &quot;SK&quot;, &quot;AttributeType&quot;: &quot;S&quot;},<br>    ],<br>    BillingMode=&quot;PAY_PER_REQUEST&quot;,<br>)</pre><p>Finally, with what is admitedly a dirty hack, the fixture creates the table only once by wrapping the dynamodb.create_table() call in a try/except block that returns the table if there&#39;s an error creating it:</p><pre>def dynamodb_table(request):<br>    # ...<br>    try:<br>        table = dynamodb.create_table(<br>            # arguments omitted for brevity<br>        )<br>        yield table<br>    except Exception:<br>        yield dynamodb.Table(TABLE_NAME)</pre><h3>Example</h3><p>Example test using fixture:</p><pre>def test_find_people_with_middle_initials(dynamodb_table):<br>    dynamodb_table.put_item(<br>        Item=dict(<br>            PK=&quot;Person&quot;,<br>            SK=&quot;123456&quot;,<br>            first_name=&quot;John&quot;,<br>            middle_initial=&quot;R&quot;,<br>            last_name=&quot;Franey&quot;,<br>        )<br>    )<br>    dynamodb_table.put_item(<br>        Item=dict(<br>            PK=&quot;Person&quot;,<br>            SK=&quot;234567&quot;,<br>            first_name=&quot;Bilbo&quot;,<br>            last_name=&quot;Baggins&quot;,<br>        )<br>    )<br>    people_with_middle_initials = find_people_with_middle_initials()<br>    assert len(people_with_middle_initials) == 1<br>    assert people_with_middle_initials[0].get(&quot;middle_initial&quot;) == &quot;R&quot;</pre><h3>Wrap-up</h3><p>That’s that! With a guest appearance from the Node.js world, dynalite, it&#39;s easy to unit test DynamoDB queries in Python on your local machine.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=44f594e86887" width="1" height="1" alt=""><hr><p><a href="https://awstip.com/unit-test-dynamodb-in-python-with-pytest-and-dynalite-44f594e86887">Unit test DynamoDB in Python with pytest and dynalite</a> was originally published in <a href="https://awstip.com">AWS Tip</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Use TypeScript to Synchronize Django REST Framework and Vue.js]]></title>
            <link>https://medium.com/codex/use-typescript-to-synchronize-django-rest-framework-and-vue-js-a2af07aabf89?source=rss-350ce7cb1744------2</link>
            <guid isPermaLink="false">https://medium.com/p/a2af07aabf89</guid>
            <category><![CDATA[django]]></category>
            <category><![CDATA[metadata]]></category>
            <category><![CDATA[vuejs]]></category>
            <category><![CDATA[typescript]]></category>
            <category><![CDATA[django-rest-framework]]></category>
            <dc:creator><![CDATA[John Franey]]></dc:creator>
            <pubDate>Wed, 06 Jan 2021 14:35:49 GMT</pubDate>
            <atom:updated>2021-01-07T15:10:04.571Z</atom:updated>
            <content:encoded><![CDATA[<h3>Use TypeScript to Synchronize Django REST Framework and Vue.js: Part 2</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pAWYIPrHvD9U-0t5" /><figcaption>Photo by <a href="https://unsplash.com/@steve3p_0?utm_source=medium&amp;utm_medium=referral">Steve Halama</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><h3>Part 2: Consuming Metadata</h3><p>See <a href="https://medium.com/@johnfraney/use-typescript-to-synchronize-django-rest-framework-and-vue-js-d103cf416e23"><em>Part I: Generating API Metadata</em></a> to get caught up. This post uses the User.json metadata generated using the steps described in that post.</p><h4>(Un)structured data</h4><p>The idea of using API metadata in the client code isn’t revolutionary. <a href="https://github.com/Azure/AutoRest">AutoRest</a> uses <a href="https://swagger.io/resources/open-api/">OpenAPI</a> schemas to generate code for a number of languages, and Django REST Framework’s docs suggest using metadata to jumpstart client libraries:</p><blockquote>API schemas are a useful tool that allow for a range of use cases, including generating reference documentation, or driving dynamic client libraries that can interact with your API.<br>- <a href="https://www.django-rest-framework.org/api-guide/schemas/#generating-an-openapi-schema">https://www.django-rest-framework.org/api-guide/schemas/</a></blockquote><p>Whereas the first part of this post series covered generating metadata to use in a JavaScript/TypeScript client, this post makes good on that promise and uses that user metadata in a <a href="https://vuejs.org/">Vue</a> frontend.</p><p>How? Well, there are a number of ways to use API metadata to power a frontend, like:</p><ul><li>Generating constants</li><li>Creating type-safe forms</li><li>Automatic form generation</li><li>Data class generation</li></ul><p>This blog post will cover type-safe and (mostly) automatically-generated forms.</p><h4>Type-safe forms from JSON metadata</h4><p>Here’s a simple Vue component containing a form to create a new user:</p><pre>&lt;template&gt;<br>  &lt;div id=&quot;app&quot;&gt;<br>    &lt;h1&gt;User Form&lt;/h1&gt;<br>    &lt;form <a href="http://twitter.com/submit">@submit</a>.prevent=&quot;onSubmit&quot;&gt;<br>      &lt;label&gt;First name&lt;/label&gt;<br>      &lt;input<br>        v-model=&quot;form.firstName&quot;<br>      &gt;</pre><pre>      &lt;label&gt;Last name&lt;/label&gt;<br>      &lt;input<br>        v-model=&quot;form.lastName&quot;<br>      &gt;</pre><pre>      &lt;label&gt;Email address&lt;/label&gt;<br>      &lt;input<br>        v-model=&quot;form.firstName&quot;<br>      &gt;</pre><pre>      &lt;label&gt;Username&lt;/label&gt;<br>      &lt;input<br>        v-model=&quot;form.username&quot;<br>      &gt;</pre><pre>      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;<br>    &lt;/form&gt;<br>  &lt;/div&gt;<br>&lt;/template&gt;<br>&lt;script lang=&quot;ts&quot;&gt;<br>import Vue from &#39;vue&#39;;</pre><pre>export default Vue.extend({<br>  name: &#39;App&#39;,</pre><pre>  data() {<br>    return {<br>      form: {},<br>    }<br>  },<br><br>  methods: {<br>    onSubmit() {<br>      // Pretend a user is created<br>    },<br>  },<br>});<br>&lt;/script&gt;</pre><p>Notice anything wrong with that form?</p><p>Go ahead and take a good look.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Ftenor.com%2Fembed%2F12615961&amp;display_name=Tenor&amp;url=https%3A%2F%2Ftenor.com%2Fview%2Fwhere-is-it-look-close-honey-ishrunk-thekids-looking-gif-12615961&amp;image=https%3A%2F%2Fmedia.tenor.com%2Fimages%2F36ad9b6004b236025ff49273438a14b4%2Ftenor.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=tenor" width="600" height="400" frameborder="0" scrolling="no"><a href="https://medium.com/media/b5711c38ff801fb2866c8d1624bff075/href">https://medium.com/media/b5711c38ff801fb2866c8d1624bff075/href</a></iframe><p>At first, it looks like a standard form, certainly. The more you look at it, however, the more you may realize how plain it is. It is unremarkable in whole and in part. There’s so little to it that it could well pass for unobjectionable. After all, there isn’t anything <em>wrong</em> with the form, really — but it isn’t much right with it. Although Vue considers the above form to be correctly typed, we could add a number of useful type-related improvements:</p><ul><li>Required fields</li><li>Typed inputs (like input=&quot;email&quot;)</li><li>A typed form object</li></ul><p>Enter User metadata.</p><p>There are a number of ways to use the metadata specified in <a href="https://gist.githubusercontent.com/johnfraney/d8fe9868948fdf7bdc599ff0a9c5cbcf/raw/5a32bd7c68e6eda0883241a1947b3d7d6e48c575/user_options.json">User.json</a> (described in detail in <a href="https://medium.com/nepfin-engineering/use-typescript-to-synchronize-django-rest-framework-and-vue-js-d103cf416e23">Part I of this series</a>) to improve this form.</p><p>Check out this typed form, and then we’ll look at how each metadata usage is an improvement over the untyped form above:</p><pre>&lt;template&gt;<br>  &lt;div id=&quot;app&quot;&gt;<br>    &lt;h1&gt;User Form&lt;/h1&gt;<br>    &lt;form <a href="http://twitter.com/submit">@submit</a>.prevent=&quot;onSubmit&quot;&gt;<br>      &lt;div&gt;<br>        &lt;label v-text=&quot;userMetadata.first_name.label&quot; /&gt;<br>        &lt;input<br>          v-model=&quot;form[userMetadata.first_name.field_name]&quot;<br>          :maxlength=&quot;userMetadata.first_name.max_length&quot;<br>          :name=&quot;userMetadata.first_name.field_name&quot;<br>          :required=&quot;userMetadata.first_name.required&quot;<br>        &gt;<br>      &lt;/div&gt;<br>      &lt;div&gt;<br>        &lt;label v-text=&quot;userMetadata.last_name.label&quot; /&gt;<br>        &lt;input<br>          v-model=&quot;form[userMetadata.last_name.field_name]&quot;<br>          :maxlength=&quot;userMetadata.last_name.max_length&quot;<br>          :name=&quot;userMetadata.last_name.field_name&quot;<br>          :required=&quot;userMetadata.last_name.required&quot;<br>        &gt;<br>      &lt;/div&gt;<br>      &lt;div&gt;<br>        &lt;label v-text=&quot;userMetadata.email.label&quot; /&gt;<br>        &lt;input<br>          v-model=&quot;form[userMetadata.email.field_name]&quot;<br>          :maxlength=&quot;userMetadata.email.max_length&quot;<br>          :name=&quot;userMetadata.email.field_name&quot;<br>          :required=&quot;userMetadata.email.required&quot;<br>          :type=&quot;userMetadata.email.format&quot;<br>        &gt;<br>      &lt;/div&gt;<br>      &lt;div&gt;<br>        &lt;label v-text=&quot;userMetadata.username.label&quot; /&gt;<br>        &lt;input<br>          v-model=&quot;form[userMetadata.username.field_name]&quot;<br>          :maxlength=&quot;userMetadata.username.max_length&quot;<br>          :name=&quot;userMetadata.username.field_name&quot;<br>          :required=&quot;userMetadata.username.required&quot;<br>        &gt;<br>      &lt;/div&gt;<br>      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;<br>    &lt;/form&gt;<br>    &lt;pre v-text=&quot;userMetadata&quot; /&gt;<br>  &lt;/div&gt;<br>&lt;/template&gt;</pre><pre>&lt;script lang=&quot;ts&quot;&gt;<br>import Vue from &#39;vue&#39;</pre><pre>import User from  &#39;./metadata/User.json&#39;</pre><pre>export default Vue.extend({<br>  name: &#39;App&#39;,</pre><pre>  data() {<br>    return {<br>      form: {},<br>      userMetadata: User,<br>    }<br>  },</pre><pre>  methods: {<br>    onSubmit() {<br>      // Pretend a user is created<br>    },<br>  },<br>})<br>&lt;/script&gt;</pre><p>Most changes here are in the template. In the component itself, the only change is userMetadata, which is a typed object created from User.json. (To use JSON files in this way, ensure that your <a href="https://www.staging-typescript.org/tsconfig#resolveJsonModule">tsconfig.json has </a><a href="https://www.staging-typescript.org/tsconfig#resolveJsonModule">resolveJSONModule: true</a>.)</p><p>The template includes these changes, all of which help the form reflect the backend user model:</p><ul><li>&lt;label v-text&gt;: Instead of specifying form labels manually, they are pulled from the user metadata.</li><li>&lt;input v-model&gt;: Instead of writing a property accessor in the template, like form.firtsName, we can guarantee that the key for the user form is the same as the database field name, e.g. form[userMetadata.first_name.field_name]. Oh, and did you notice the fristName typo in that property accessor? Using the metadata for field names is more typo-proof.</li><li>&lt;input type&gt;: API metadata can specify which type of input should be used for each field, like email or number. This helps prevent validation errors.</li><li>Input validation attributes (required, maxlength): Speaking of preventing validation errors, database-level constraints for use fields are reflected in the JSON metadata, which we can use directly in &lt;input&gt; elements. User.json even includes pattern, so you can reuse regular expressions defined on the server in your client forms. This is a nice win because <a href="https://johnfraney.ca/posts/2019/03/24/human-readable-python-regular-expressions/">writing and maintaining regular expressions isn’t user-friendly</a>.</li><li>&lt;input name&gt;: This is a small change, but it makes it easy to select specific inputs in unit tests.</li></ul><p>Now that we’ve got a typed form, why not take things one step further? because our metadata tells us so much about the API endpoint’s expectations, could we use that data to generate a form automatically?</p><p>Dear reader, we can.</p><h4>Automatically-generated forms from JSON metadata</h4><p>This is where having robust metadata really shines. With some tweaking, we can leverage our metadata as the basis for a fully-typed component with a small, maintainable template.</p><p>Take a look:</p><pre>&lt;template&gt;<br>  &lt;div id=&quot;app&quot;&gt;<br>    &lt;h1&gt;User Form&lt;/h1&gt;<br>    &lt;form <a href="http://twitter.com/submit">@submit</a>.prevent=&quot;onSubmit&quot;&gt;<br>      &lt;div v-for=&quot;(inputData, inputName) in fields&quot; :key=&quot;inputName&quot;&gt;<br>        &lt;label v-text=&quot;inputData.label&quot; /&gt;<br>        &lt;input<br>          v-model=&quot;form[inputName]&quot;<br>          v-bind=&quot;inputData.inputAttributes&quot;<br>        &gt;<br>      &lt;/div&gt;<br>      &lt;button type=&quot;submit&quot;&gt;Submit&lt;/button&gt;<br>    &lt;/form&gt;<br>  &lt;/div&gt;<br>&lt;/template&gt;</pre><pre>&lt;script lang=&quot;ts&quot;&gt;<br>import Vue from &#39;vue&#39;;</pre><pre>import User from  &#39;./metadata/User.json&#39;</pre><pre>type UserForm = {<br>  [UserField in keyof typeof User]?: typeof User[UserField][&quot;initial&quot;]<br>}</pre><pre>interface InputAttributes {<br>  [key: string]: boolean | number | string<br>}</pre><pre>interface InputData {<br>  inputAttributes: InputAttributes<br>  label: string<br>}</pre><pre>function convertFieldMetadataToInputData(fieldMetadata: typeof User[keyof typeof User]): InputData {<br>  const label = fieldMetadata.label<br>  const inputAttributes: InputAttributes = {<br>    name: fieldMetadata.field_name,<br>    required: fieldMetadata.required,<br>    type: fieldMetadata.type,<br>  }<br>  // Add non-universal properties<br>  if (&#39;max_length&#39; in fieldMetadata) {<br>    inputAttributes.maxlength = fieldMetadata.max_length<br>  }<br>  if (&#39;pattern&#39; in fieldMetadata) {<br>    inputAttributes.maxlength = fieldMetadata.pattern<br>  }<br>  return {<br>    inputAttributes,<br>    label,<br>  }<br>}</pre><pre>export default Vue.extend({<br>  name: &#39;App&#39;,</pre><pre>  data() {<br>    return {<br>      fields: {<br>        [User.first_name.field_name]: convertFieldMetadataToInputData(User.first_name),<br>        [User.last_name.field_name]: convertFieldMetadataToInputData(User.last_name),<br>        [User.email.field_name]: convertFieldMetadataToInputData(User.email),<br>        [User.username.field_name]: convertFieldMetadataToInputData(User.username),<br>      },<br>      form: {},<br>    }<br>  },</pre><pre>methods: {<br>    onSubmit() {<br>      // Pretend a user is created<br>    },<br>  },<br>});<br>&lt;/script&gt;</pre><p>Although this component is a few more lines than the one preceding it, much of this logic can be removed from this component and shared across every form in your application.</p><p>Quite a bit of new is happening in the component, so let’s take a look at the important changes.</p><p>First, the form body:</p><pre>&lt;div v-for=&quot;(inputData, inputName) in fields&quot; :key=&quot;inputName&quot;&gt;<br>  &lt;label v-text=&quot;inputData.label&quot; /&gt;<br>  &lt;input<br>    v-model=&quot;form[inputName]&quot;<br>    v-bind=&quot;inputData.inputAttributes&quot;<br>  &gt;<br>&lt;/div&gt;</pre><p>Is that it? You bet. Instead of specifying every field, with a bit of work in the component, we can just loop through fields.</p><p>(I should note that this implementation works only an all-&lt;input&gt; form. If your form contains &lt;select&gt; elements, checkboxes, or radio buttons, you may need to create a separate component that will output the correct form field element.)</p><p>We’ve also added some extra type interfaces:</p><ul><li>UserForm uses some TypeScript magic (<a href="https://www.staging-typescript.org/docs/handbook/advanced-types.html#index-types-and-index-signatures">index types and index signatures</a>, specifically) to generate a type that represents a user form, using the initial value from our user metadata.</li><li>For InputAttributes, the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#Attributes">HTML attributes</a> we’ve specified in our metadata take one of three types: boolean for attributes such as <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required">required</a>; the number for attributes such as <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#htmlattrdefmaxlength">maxlength</a>; and string for attributes such as <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#attr-type">type</a>.</li><li>InputData is an object representing the information we need to generate our form. We use the label property for the&lt;label&gt; element and pass inputAttributes to &lt;input&gt; elements using Vue’s <a href="https://vuejs.org/v2/api/#vm-attrs">v-bind</a>, like v-bind=&quot;inputAttributes&quot;. This way we don’t have to specify each prop individually.</li></ul><p>To get usable inputAttributes, we need to map the name of the metadata field to the correct HTML attribute name. We also need to do null checking for attributes that aren’t present on every metadata node, and therefore input element. This work is handled by convertFieldMetadataToInputData.</p><p>But why do we need convertFieldMetadataToInputData in the first place?</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Ftenor.com%2Fembed%2F19287039&amp;display_name=Tenor&amp;url=https%3A%2F%2Ftenor.com%2Fview%2Fbh187-mr-bean-awkward-what-the-uh-oh-gif-19287039&amp;image=https%3A%2F%2Fmedia.tenor.com%2Fimages%2Fcc2732c994c8055d36883e9859263d0d%2Ftenor.gif&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=tenor" width="600" height="400" frameborder="0" scrolling="no"><a href="https://medium.com/media/87cf7463bce1265401ac0ba23b9a83e5/href">https://medium.com/media/87cf7463bce1265401ac0ba23b9a83e5/href</a></iframe><p>Well, it’s because I didn’t plan ahead. If the API metadata used the strings that these HTML attributes expect, we’d be able to use them directly instead of transforming them. Shame on me for not thinking of that earlier!</p><h3>Wrap-up</h3><p>There we have it! With a little massaging, we’re able to generate a fully-typed form using metadata from a REST-like JSON API. This post also served as an example of how difficult it can be to keep the frontend and backend of a web application in sync — even when attempting to show a method for doing just that.</p><p>Still, if done with careful planning, leveraging API metadata can simplify SPA applications and reduce boilerplate and difficult-to-sync code in JavaScript web applications. And by “careful planning” I <em>do</em> mean planning that was more careful than was mine.</p><p><strong>Happy coding!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a2af07aabf89" width="1" height="1" alt=""><hr><p><a href="https://medium.com/codex/use-typescript-to-synchronize-django-rest-framework-and-vue-js-a2af07aabf89">Use TypeScript to Synchronize Django REST Framework and Vue.js</a> was originally published in <a href="https://medium.com/codex">CodeX</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>