{"id":2871,"date":"2024-02-22T17:00:00","date_gmt":"2024-02-22T18:00:00","guid":{"rendered":"https:\/\/vfflogs.com\/?p=2871"},"modified":"2024-04-06T16:28:37","modified_gmt":"2024-04-06T16:28:37","slug":"vanilla-javascript-libraries-and-the-quest-for-stateful-dom-rendering","status":"publish","type":"post","link":"https:\/\/vfflogs.com\/index.php\/2024\/02\/22\/vanilla-javascript-libraries-and-the-quest-for-stateful-dom-rendering\/","title":{"rendered":"Vanilla JavaScript, Libraries, And The Quest For Stateful DOM Rendering"},"content":{"rendered":"

Vanilla JavaScript, Libraries, And The Quest For Stateful DOM Rendering<\/title><\/p>\n<article>\n<header>\n<h1>Vanilla JavaScript, Libraries, And The Quest For Stateful DOM Rendering<\/h1>\n<address>Frederik Dohr<\/address>\n<p> 2024-02-22T18:00:00+00:00<br \/>\n 2024-04-06T16:05:18+00:00<br \/>\n <\/header>\n<p>In his seminal piece \u201c<a href=\"https:\/\/infrequently.org\/2023\/02\/the-market-for-lemons\/\">The Market For Lemons<\/a>\u201d, renowned web crank Alex Russell lays out the myriad failings of our industry, focusing on the disastrous consequences for end users. This indignation is entirely appropriate according to the <a href=\"https:\/\/www.w3.org\/TR\/html-design-principles\/#priority-of-constituencies\">bylaws of our medium<\/a>.<\/p>\n<p>Frameworks factor highly in that equation, yet there can also be good reasons for front-end developers to choose a framework, <a href=\"https:\/\/johan.hal.se\/wrote\/2023\/02\/17\/what-to-expect-from-your-framework\/\">or library<\/a> for that matter: Dynamically updating web interfaces can be tricky in non-obvious ways. Let\u2019s investigate by starting from the beginning and going back to the first principles.<\/p>\n<h2 id=\"markup-categories\">Markup Categories<\/h2>\n<p>Everything on the web starts with markup, i.e. HTML. Markup structures can roughly be divided into three categories:<\/p>\n<ol>\n<li>Static parts that always remain the same.<\/li>\n<li>Variable parts that are defined once upon instantiation.<\/li>\n<li>Variable parts that are updated dynamically at runtime.<\/li>\n<\/ol>\n<p>For example, an article\u2019s header might look like this:<\/p>\n<pre><code class=\"language-html\"><header>\n <h1>\u00abHello World\u00bb<\/h1>\n <small>\u00ab123\u00bb backlinks<\/small>\n<\/header>\n<\/code><\/pre>\n<p>Variable parts are wrapped in \u00abguillemets\u00bb here: \u201cHello World\u201d is the respective title, which only changes between articles. The backlinks counter, however, might be continuously updated via client-side scripting; we\u2019re ready to go viral in the blogosphere. Everything else remains identical across all our articles.<\/p>\n<p>The article you\u2019re reading now subsequently focuses on the third category: Content that needs to be updated at runtime.<\/p>\n<h2 id=\"color-browser\">Color Browser<\/h2>\n<p>Imagine we\u2019re building a simple color browser: A little widget to explore a pre-defined set of <a href=\"https:\/\/www.w3.org\/TR\/css-color-3\/#svg-color\">named colors<\/a>, presented as a list that pairs a color swatch with the corresponding color value. Users should be able to search colors names and toggle between hexadecimal color codes and Red, Blue, and Green (RGB) triplets. We can create an <a href=\"https:\/\/web.archive.org\/web\/20130924061832\/http:\/\/alistair.cockburn.us\/Walking+skeleton\">inert skeleton<\/a> with just a little bit of HTML and CSS:<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"RwdmbGd\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Color Browser (inert) [forked]](https:\/\/codepen.io\/smashingmag\/pen\/RwdmbGd) by <a href=\"https:\/\/codepen.io\/f-n-d\">FND<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/RwdmbGd\">Color Browser (inert) [forked]<\/a> by <a href=\"https:\/\/codepen.io\/f-n-d\">FND<\/a>.<\/figcaption><\/figure>\n<div data-audience=\"non-subscriber\" data-remove=\"true\" class=\"feature-panel-container\">\n<aside class=\"feature-panel\">\n<div class=\"feature-panel-left-col\">\n<div class=\"feature-panel-description\">\n<p><strong>Web forms<\/strong> are at the center of every meaningful interaction. Meet Adam Silver’s <strong><a href=\"https:\/\/www.smashingmagazine.com\/printed-books\/form-design-patterns\/\">Form Design Patterns<\/a><\/strong>, a practical guide to <strong>designing and building forms<\/strong> for the web.<\/p>\n<p><a data-instant href=\"https:\/\/www.smashingmagazine.com\/printed-books\/form-design-patterns\/\" class=\"btn btn--green btn--large\">Jump to table of contents \u21ac<\/a><\/div>\n<\/div>\n<div class=\"feature-panel-right-col\"><a data-instant href=\"https:\/\/www.smashingmagazine.com\/printed-books\/form-design-patterns\/\" class=\"feature-panel-image-link\"><\/p>\n<div class=\"feature-panel-image\">\n<img loading=\"lazy\" class=\"feature-panel-image-img\" src=\"https:\/\/archive.smashing.media\/assets\/344dbf88-fdf9-42bb-adb4-46f01eedd629\/51e0f837-d85d-4b28-bfab-1c9a47f0ce33\/form-design-patterns-shop-image.png\" alt=\"Feature Panel\" width=\"481\" height=\"698\" \/><\/p>\n<\/div>\n<p><\/a>\n<\/div>\n<\/aside>\n<\/div>\n<h2 id=\"client-side-rendering\">Client-Side Rendering<\/h2>\n<p>We\u2019ve grudgingly decided to employ client-side rendering for the interactive version. For our purposes here, it doesn\u2019t matter whether this widget constitutes a complete application or merely a <a href=\"https:\/\/jasonformat.com\/islands-architecture\">self-contained island<\/a> embedded within an otherwise static or server-generated HTML document.<\/p>\n<p>Given our predilection for vanilla JavaScript (cf. first principles and all), we start with the browser\u2019s built-in DOM APIs:<\/p>\n<pre><code class=\"language-javascript\">function renderPalette(colors) {\n let items = [];\n for(let color of colors) {\n let item = document.createElement(\"li\");\n items.push(item);\n\n let value = color.hex;\n makeElement(\"input\", {\n parent: item,\n type: \"color\",\n value\n });\n makeElement(\"span\", {\n parent: item,\n text: color.name\n });\n makeElement(\"code\", {\n parent: item,\n text: value\n });\n }\n\n let list = document.createElement(\"ul\");\n list.append(...items);\n return list;\n}\n<\/code><\/pre>\n<blockquote><p><strong>Note:<\/strong><br \/>The above relies on a small utility function for more concise element creation:<\/p>\n<pre><code class=\"language-javascript\">function makeElement(tag, { parent, children, text, ...attribs }) {\n let el = document.createElement(tag);\n\n if(text) {\n el.textContent = text;\n }\n\n for(let [name, value] of Object.entries(attribs)) {\n el.setAttribute(name, value);\n }\n\n if(children) {\n el.append(...children);\n }\n\n parent?.appendChild(el);\n return el;\n}\n<\/code><\/pre>\n<p>You might also have noticed a stylistic inconsistency: Within the <code>items<\/code> loop, newly created elements attach themselves to their container. Later on, we flip responsibilities, as the <code>list<\/code> container ingests child elements instead.<\/p><\/blockquote>\n<p>Voil\u00e0: <code>renderPalette<\/code> generates our list of colors. Let\u2019s add a form for interactivity:<\/p>\n<pre><code class=\"language-javascript\">function renderControls() {\n return makeElement(\"form\", {\n method: \"dialog\",\n children: [\n createField(\"search\", \"Search\"),\n createField(\"checkbox\", \"RGB\")\n ]\n });\n}\n<\/code><\/pre>\n<p>The <code>createField<\/code> utility function encapsulates DOM structures required for input fields; it\u2019s a little reusable markup component:<\/p>\n<pre><code class=\"language-javascript\">function createField(type, caption) {\n let children = [\n makeElement(\"span\", { text: caption }),\n makeElement(\"input\", { type })\n ];\n return makeElement(\"label\", {\n children: type === \"checkbox\" ? children.reverse() : children\n });\n}\n<\/code><\/pre>\n<p>Now, we just need to combine those pieces. Let\u2019s wrap them in a custom element:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">import { COLORS } from \".\/colors.js\"; \/\/ an array of `{ name, hex, rgb }` objects\n\ncustomElements.define(\"color-browser\", class ColorBrowser extends HTMLElement {\n colors = [...COLORS]; \/\/ local copy\n\n connectedCallback() {\n this.append(\n renderControls(),\n renderPalette(this.colors)\n );\n }\n});\n<\/code><\/pre>\n<\/div>\n<p class=\"c-pre-sidenote--left\">Henceforth, a <code><color-browser><\/code> element anywhere in our HTML will generate the entire user interface right there. (I like to think of it as a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Macro_%28computer_science%29\">macro<\/a> expanding in place.) This implementation is somewhat declarative<sup>1<\/sup>, with DOM structures being created by composing a variety of straightforward markup generators, clearly delineated components, if you will.<\/p>\n<p class=\"c-sidenote c-sidenote--right\"><sup>1<\/sup> The most useful explanation of the differences between declarative and imperative programming I\u2019ve come across focuses on readers. Unfortunately, that particular source escapes me, so I\u2019m paraphrasing here: Declarative code portrays the <em>what<\/em> while imperative code describes the <em>how<\/em>. One consequence is that imperative code requires cognitive effort to sequentially step through the code\u2019s instructions and build up a mental model of the respective result.<\/p>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"interactivity\">Interactivity<\/h2>\n<p>At this point, we\u2019re merely recreating our inert skeleton; there\u2019s no actual interactivity yet. Event handlers to the rescue:<\/p>\n<pre><code class=\"language-javascript\">class ColorBrowser extends HTMLElement {\n colors = [...COLORS];\n query = null;\n rgb = false;\n\n connectedCallback() {\n this.append(renderControls(), renderPalette(this.colors));\n this.addEventListener(\"input\", this);\n this.addEventListener(\"change\", this);\n }\n\n handleEvent(ev) {\n let el = ev.target;\n switch(ev.type) {\n case \"change\":\n if(el.type === \"checkbox\") {\n this.rgb = el.checked;\n }\n break;\n case \"input\":\n if(el.type === \"search\") {\n this.query = el.value.toLowerCase();\n }\n break;\n }\n }\n}\n<\/code><\/pre>\n<blockquote><p><strong>Note:<\/strong><br \/><code>handleEvent<\/code> means we don\u2019t have to <a href=\"https:\/\/gomakethings.com\/the-handleevent-method-is-the-absolute-best-way-to-handle-events-in-web-components\/\">worry about function binding<\/a>. It also comes with <a href=\"https:\/\/web.archive.org\/web\/20240121164212\/https:\/\/scribe.rip\/webreflection\/dom-handleevent-a-cross-platform-standard-since-year-2000-5bf17287fd38#a0ff\">various advantages<\/a>. Other patterns are available.<\/p><\/blockquote>\n<p class=\"c-pre-sidenote--left\">Whenever a field changes, we update the corresponding instance variable (sometimes called one-way data binding). Alas, changing this internal state<sup>2<\/sup> is not reflected anywhere in the UI so far.<\/p>\n<p class=\"c-sidenote c-sidenote--right\"><sup>2<\/sup> In your browser\u2019s developer console, check <code>document.querySelector(\"color-browser\").query<\/code> after entering a search term.<\/p>\n<p>Note that this event handler is tightly coupled to <code>renderControls<\/code> internals because it expects a checkbox and search field, respectively. Thus, any corresponding changes to <code>renderControls<\/code> — perhaps switching to radio buttons for color representations — now need to take into account this other piece of code: <a href=\"https:\/\/en.wikipedia.org\/wiki\/Action_at_a_distance_%28computer_programming%29\">action at a distance<\/a>! Expanding this component\u2019s contract to include<br \/>\n<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTML\/Element\/input#name\">field names<\/a> could alleviate those concerns.<\/p>\n<p>We\u2019re now faced with a choice between:<\/p>\n<ol>\n<li>Reaching into our previously created DOM to modify it, or<\/li>\n<li>Recreating it while incorporating a new state.<\/li>\n<\/ol>\n<h2 id=\"rerendering\">Rerendering<\/h2>\n<p>Since we\u2019ve already defined our markup composition in one place, let\u2019s start with the second option. We\u2019ll simply rerun our markup generators, feeding them the current state.<\/p>\n<pre><code class=\"language-javascript\">class ColorBrowser extends HTMLElement {\n \/\/ [previous details omitted]\n\n connectedCallback() {\n this.#render();\n this.addEventListener(\"input\", this);\n this.addEventListener(\"change\", this);\n }\n\n handleEvent(ev) {\n \/\/ [previous details omitted]\n this.#render();\n }\n\n #render() {\n this.replaceChildren();\n this.append(renderControls(), renderPalette(this.colors));\n }\n}\n<\/code><\/pre>\n<p class=\"c-pre-sidenote--left\">We\u2019ve moved all rendering logic into a dedicated method<sup>3<\/sup>, which we invoke not just once on startup but whenever the state changes.<\/p>\n<p class=\"c-sidenote c-sidenote--right\"><sup>3<\/sup> You might want to <a href=\"https:\/\/lea.verou.me\/blog\/2023\/04\/private-fields-considered-harmful\/\">avoid private properties<\/a>, especially if others might conceivably build upon your implementation.<\/p>\n<p>Next, we can turn <code>colors<\/code> into a getter to only return entries matching the corresponding state, i.e. the user\u2019s search query:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">class ColorBrowser extends HTMLElement {\n query = null;\n rgb = false;\n\n \/\/ [previous details omitted]\n\n get colors() {\n let { query } = this;\n if(!query) {\n return [...COLORS];\n }\n\n return COLORS.filter(color => color.name.toLowerCase().includes(query));\n }\n}\n<\/code><\/pre>\n<\/div>\n<blockquote><p><strong>Note:<\/strong><br \/>I\u2019m partial to the <a href=\"https:\/\/rikschennink.nl\/thoughts\/the-bouncer-pattern\/\">bouncer pattern<\/a>.<br \/>Toggling color representations is left as an exercise for the reader. You might pass <code>this.rgb<\/code> into <code>renderPalette<\/code> and then populate <code><code><\/code> with either <code>color.hex<\/code> or <code>color.rgb<\/code>, perhaps employing this utility:<\/p>\n<pre><code class=\"language-javascript\">function formatRGB(value) {\n return value.split(\",\").\n map(num => num.toString().padStart(3, \" \")).\n join(\", \");\n}\n<\/code><\/pre>\n<\/blockquote>\n<p>This now produces interesting (annoying, really) behavior:<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"YzgbKab\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Color Browser (defective) [forked]](https:\/\/codepen.io\/smashingmag\/pen\/YzgbKab) by <a href=\"https:\/\/codepen.io\/f-n-d\">FND<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/YzgbKab\">Color Browser (defective) [forked]<\/a> by <a href=\"https:\/\/codepen.io\/f-n-d\">FND<\/a>.<\/figcaption><\/figure>\n<p>Entering a query seems impossible as the input field loses focus after a change takes place, leaving the input field empty. However, entering an uncommon character (e.g. \u201cv\u201d) makes it clear that <em>something<\/em> is happening: The list of colors does indeed change.<\/p>\n<p>The reason is that our current do-it-yourself (DIY) approach is quite crude: <code>#render<\/code> erases and recreates the DOM wholesale with each change. Discarding existing DOM nodes also resets the corresponding state, including form fields\u2019 value, focus, and scroll position. That\u2019s no good!<\/p>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"incremental-rendering\">Incremental Rendering<\/h2>\n<p>The previous section\u2019s <a href=\"https:\/\/rauchg.com\/2015\/pure-ui\">data-driven UI<\/a> seemed like a nice idea: Markup structures are defined once and re-rendered at will, based on a data model cleanly representing the current state. Yet our component\u2019s explicit state is clearly insufficient; we need to reconcile it with the browser\u2019s implicit state while re-rendering.<\/p>\n<p>Sure, we might attempt to make that <em>implicit<\/em> state <em>explicit<\/em> and incorporate it into our data model, like including a field\u2019s <code>value<\/code> or <code>checked<\/code> properties. But that still leaves many things unaccounted for, including focus management, scroll position, and <a href=\"https:\/\/daverupert.com\/2024\/02\/ui-states\/\">myriad details<\/a> we probably haven\u2019t even thought of (frequently, that means accessibility features). Before long, we\u2019re effectively recreating the browser!<\/p>\n<p class=\"c-pre-sidenote--left\">We might instead try to identify which parts of the UI need updating and leave the rest of the DOM untouched. Unfortunately, that\u2019s far from trivial, which is where libraries like React came into play more than a decade ago: On the surface, they provided a more declarative way to define DOM structures<sup>4<\/sup> (while also encouraging componentized composition, establishing a single source of truth for each individual UI pattern). Under the hood, such libraries introduced mechanisms<sup>5<\/sup> to provide granular, incremental DOM updates instead of recreating DOM trees from scratch — both to avoid state conflicts and to improve performance<sup>6<\/sup>.<\/p>\n<p class=\"c-sidenote c-sidenote--right\"><sup>4<\/sup> In this context, that essentially means writing something that looks like HTML, which, <a href=\"https:\/\/en.wikipedia.org\/wiki\/False_equivalence\">depending on your belief system<\/a>, is either essential or revolting. The state of HTML templating was somewhat dire back then and remains subpar in some environments.<br \/><sup>5<\/sup> Nolan Lawson\u2019s \u201c<a href=\"https:\/\/nolanlawson.com\/2023\/12\/02\/lets-learn-how-modern-javascript-frameworks-work-by-building-one\/\">Let\u2019s learn how modern JavaScript frameworks work by building one<\/a>\u201d provides plenty of valuable insights on that topic. For even more details, <a href=\"https:\/\/github.com\/lit\/lit\/blob\/9c02b3876dc927c6a82b4420411256ecbb47c08c\/dev-docs\/design\/how-lit-html-works.md\">lit-html\u2019s developer documentation<\/a> is worth studying.<br \/><sup>6<\/sup> We\u2019ve since learned that <em>some<\/em> of those mechanisms are actually <a href=\"https:\/\/infrequently.org\/2024\/01\/performance-inequality-gap-2024\/\">ruinously expensive<\/a>.<\/p>\n<p>The bottom line: <strong>If we want to encapsulate markup definitions and then derive our UI from a variable data model, we kinda have to rely on a third-party library for reconciliation.<\/strong><\/p>\n<h2 id=\"actus-imperatus\">Actus Imperatus<\/h2>\n<p>At the other end of the spectrum, we might opt for surgical modifications. If we know what to target, our application code can reach into the DOM and modify only those parts that need updating.<\/p>\n<p>Regrettably, though, that approach typically leads to calamitously tight coupling, with interrelated logic being spread all over the application while targeted routines inevitably violate components\u2019 encapsulation. Things become even more complicated when we consider increasingly complex UI permutations (think edge cases, error reporting, and so on). Those are the very issues that the aforementioned libraries had hoped to eradicate.<\/p>\n<p>In our color browser\u2019s case, that would mean finding and hiding color entries that do not match the query, not to mention replacing the list with a substitute message if no matching entries remain. We\u2019d also have to swap color representations in place. You can probably imagine how the resulting code would end up dissolving any separation of concerns, messing with elements that originally belonged exclusively to <code>renderPalette<\/code>.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">class ColorBrowser extends HTMLElement {\n \/\/ [previous details omitted]\n\n handleEvent(ev) {\n \/\/ [previous details omitted]\n\n for(let item of this.#list.children) {\n item.hidden = !item.textContent.toLowerCase().includes(this.query);\n }\n if(this.#list.children.filter(el => !el.hidden).length === 0) {\n \/\/ inject substitute message\n }\n }\n\n #render() {\n \/\/ [previous details omitted]\n\n this.#list = renderPalette(this.colors);\n }\n}\n<\/code><\/pre>\n<\/div>\n<p>As a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Has_Been\">once wise man<\/a> once said: That\u2019s too much knowledge!<\/p>\n<p>Things get even more perilous with form fields: Not only might we have to update a field\u2019s specific state, but we would also need to know where to inject error messages. While reaching into <code>renderPalette<\/code> was bad enough, here we would have to pierce several layers: <code>createField<\/code> is a generic utility used by <code>renderControls<\/code>, which in turn is invoked by our top-level <code>ColorBrowser<\/code>.<\/p>\n<p>If things get hairy even in this minimal example, imagine having a more complex application with even more layers and indirections. Keeping on top of all those interconnections becomes all but impossible. Such systems commonly devolve into a big ball of mud where nobody dares change anything for fear of inadvertently breaking stuff.<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>There appears to be a glaring omission in standardized browser APIs. Our preference for dependency-free vanilla JavaScript solutions is thwarted by the need to non-destructively update existing DOM structures. That\u2019s assuming we value a declarative approach with inviolable encapsulation, otherwise known as \u201cModern Software Engineering: The Good Parts.\u201d<\/p>\n<p>As it currently stands, my personal opinion is that a small library like lit-html or Preact is often warranted, particularly when employed with <a href=\"https:\/\/adactio.com\/journal\/20837\">replaceability<\/a> in mind: A standardized API might still happen! Either way, <a href=\"https:\/\/infrequently.org\/2023\/02\/the-market-for-lemons\/#fn-alex-approved-1\">adequate libraries<\/a> have a light footprint and don\u2019t typically present much of an encumbrance to end users, especially when combined with <a href=\"https:\/\/cloudfour.com\/thinks\/html-web-components-are-having-a-moment\/\">progressive enhancement<\/a>.<\/p>\n<p>I don\u2019t wanna leave you hanging, though, so I\u2019ve tricked our vanilla JavaScript implementation to <em>mostly<\/em> do what we expect it to:<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"vYPwBro\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Color Browser [forked]](https:\/\/codepen.io\/smashingmag\/pen\/vYPwBro) by <a href=\"https:\/\/codepen.io\/f-n-d\">FND<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/vYPwBro\">Color Browser [forked]<\/a> by <a href=\"https:\/\/codepen.io\/f-n-d\">FND<\/a>.<\/figcaption><\/figure>\n<div class=\"signature\">\n <img src=\"https:\/\/www.smashingmagazine.com\/images\/logo\/logo--red.png\" alt=\"Smashing Editorial\" width=\"35\" height=\"46\" loading=\"lazy\" \/><br \/>\n <span>(yk)<\/span>\n<\/div>\n<\/article>\n","protected":false},"excerpt":{"rendered":"<p>Vanilla JavaScript, Libraries, And The Quest For Stateful DOM Rendering Vanilla JavaScript, Libraries, And The Quest For Stateful ...<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[10],"tags":[],"_links":{"self":[{"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/posts\/2871"}],"collection":[{"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/comments?post=2871"}],"version-history":[{"count":1,"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/posts\/2871\/revisions"}],"predecessor-version":[{"id":2872,"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/posts\/2871\/revisions\/2872"}],"wp:attachment":[{"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/media?parent=2871"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/categories?post=2871"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vfflogs.com\/index.php\/wp-json\/wp\/v2\/tags?post=2871"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}