[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"doc-ssr":3},{"html":4,"headings":5},"\u003Cp>HelpBites's public reads and drop-in scripts work with a server-rendered site. \u003Ccode>ssr.mjs\u003C\u002Fcode> is a small helper\n(MIT, no dependencies, any runtime with \u003Ccode>fetch\u003C\u002Fcode>) that your server uses to:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Render on the server when HelpBites answers fast, and hand over to the browser when it doesn't.\u003C\u002Fstrong> Every read has a\ntime budget (300 ms by default). Inside it the page ships with the content. Past it the page ships with a skeleton,\nthe request keeps going so the next visit is served from memory, and the browser loads the data itself.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Show the last answer first and refresh it behind it.\u003C\u002Fstrong> Answers are kept in memory (or your Redis). Once older than\n\u003Ccode>ttl\u003C\u002Fcode> they are still served at once while a fresh one is fetched.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Server-render the plug and play scripts.\u003C\u002Fstrong> \u003Ccode>changelog.js\u003C\u002Fcode>, \u003Ccode>feedback.js\u003C\u002Fcode> and \u003Ccode>widget.js\u003C\u002Fcode> land in a container that\nalready holds a snapshot of the content (changelog, roadmap) or a skeleton; the script keeps it on screen until its\nframe is ready, then swaps it, so nothing jumps and a crawler sees the text.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Self-host and cache the scripts.\u003C\u002Fstrong> Serve \u003Ccode>widget.js\u003C\u002Fcode> and the others from your own domain, kept on your server, so\na page never waits on HelpBites for code.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Only public reads go through it (no secret, no identity token), so what is cached is the same for everyone.\u003C\u002Fp>\n\u003Cp>Get it from your HelpBites, and copy it into your project:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-bash\">\u003Ccode>\u003Cspan class=\"shj-syn-func\">curl\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\"> -o\u003C\u002Fspan> server\u002Flib\u002Fhelpbites-ssr.mjs https:\u002F\u002Fsupport.example.com\u002Fssr.mjs\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-js\">\u003Ccode>\u003Cspan class=\"shj-syn-kwd\">import\u003C\u002Fspan> { createHelpBites } \u003Cspan class=\"shj-syn-kwd\">from\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'.\u002Fhelpbites-ssr.mjs'\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-syn-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> hb \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">createHelpBites\u003C\u002Fspan>({ host\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'https:\u002F\u002Fsupport.example.com'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> pk\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'pk_…'\u003C\u002Fspan> })\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch2 id=\"reading-the-api-while-you-render\">Reading the API while you render\u003Ca class=\"heading-anchor\" href=\"#reading-the-api-while-you-render\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-js\">\u003Ccode>\u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> { data\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> source } \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">await\u003C\u002Fspan> hb\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">roadmap\u003C\u002Fspan>()      \u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F GET \u002Fv1\u002Ffeedback\u002Froadmap?pk=…\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode>source\u003C\u002Fcode> says where \u003Ccode>data\u003C\u002Fcode> came from:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003Ccode>source\u003C\u002Fcode>\u003C\u002Fth>\n\u003Cth>Meaning\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>cache\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Fresh in memory. No request was made.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>stale\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Older than \u003Ccode>ttl\u003C\u002Fcode>: answered from memory, refreshed in the background.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>live\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>HelpBites answered inside the budget.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>late\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>It did not. \u003Ccode>data\u003C\u002Fcode> is \u003Ccode>null\u003C\u002Fcode>: draw a skeleton and fetch in the browser. The request goes on, so the next visit is \u003Ccode>cache\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>error\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The request failed (\u003Ccode>status\u003C\u002Fcode> has the HTTP status, \u003Ccode>0\u003C\u002Fcode> for a network error) and nothing is cached.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Ready-made reads: \u003Ccode>hb.changelog(query?)\u003C\u002Fcode> (\u003Ccode>GET \u002Fv1\u002Fchangelog\u003C\u002Fcode>, so \u003Ccode>{ flat: 1, limit: 5 }\u003C\u002Fcode> works), \u003Ccode>hb.roadmap()\u003C\u002Fcode>,\n\u003Ccode>hb.legal(slug)\u003C\u002Fcode> (\u003Ccode>data\u003C\u002Fcode> is the document: \u003Ccode>title\u003C\u002Fcode>, \u003Ccode>html\u003C\u002Fcode>, \u003Ccode>markdown\u003C\u002Fcode>…), \u003Ccode>hb.loader()\u003C\u002Fcode> (\u003Ccode>GET \u002Fv1\u002Fhub\u002F{pk}\u002Floader\u003C\u002Fcode>) and\n\u003Ccode>hb.get('\u002Fv1\u002F…', { query, budget })\u003C\u002Fcode> for any other public GET. Each takes a \u003Ccode>budget\u003C\u002Fcode> of its own in its last argument.\u003C\u002Fp>\n\u003Cp>A page that loads the data in the browser when the server was late (Nuxt; the same shape works anywhere):\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-ts\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F server\u002Fapi\u002Froadmap.get.ts\n\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">defineEventHandler\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-kwd\">async\u003C\u002Fspan> (event) \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> {\n  \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> within \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">Number\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-func\">getQuery\u003C\u002Fspan>(event)\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>within) \u003Cspan class=\"shj-syn-oper\">||\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">15_000\u003C\u002Fspan>\n  \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> { data } \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">await\u003C\u002Fspan> hb\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">roadmap\u003C\u002Fspan>({ budget\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> within })\n  \u003Cspan class=\"shj-syn-kwd\">return\u003C\u002Fspan> data   \u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F null: the page is told to ask again\n\u003C\u002Fspan>})\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>&lt;script setup lang=\u003Cspan class=\"shj-syn-str\">\"ts\"\u003C\u002Fspan>&gt;\n\u002F\u002F On the server ask with a short budget; in the browser, wait as long as it takes.\nconst { data, refresh } = await useFetch('\u002Fapi\u002Froadmap', { query: import.meta.server ? { within: 300 } : {} })\nonMounted(() =&gt; { if (!data.value) refresh() })\n&lt;\u002Fscript&gt;\n\n&lt;template&gt;\n  &lt;RoadmapBoard v-if=\u003Cspan class=\"shj-syn-str\">\"data\"\u003C\u002Fspan> :roadmap=\u003Cspan class=\"shj-syn-str\">\"data\"\u003C\u002Fspan> \u002F&gt;\n  &lt;USkeleton v-else class=\u003Cspan class=\"shj-syn-str\">\"h-64 w-full\"\u003C\u002Fspan> \u002F&gt;\n&lt;\u002Ftemplate&gt;\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>If the browser can reach HelpBites directly it can skip your server on that second request: the API sends\n\u003Ccode>Access-Control-Allow-Origin: *\u003C\u002Fcode>. Do not let a page that is cached as a whole (an \u003Ccode>swr\u003C\u002Fcode> route rule, a CDN) keep the\nskeleton version: either do not cache it, or let the cached copy be the one that has data.\u003C\u002Fp>\n\u003Ch2 id=\"the-plug-and-play-scripts-server-rendered\">The plug and play scripts, server-rendered\u003Ca class=\"heading-anchor\" href=\"#the-plug-and-play-scripts-server-rendered\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-js\">\u003Ccode>\u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> { html\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> source } \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">await\u003C\u002Fspan> hb\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">embed\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-str\">'changelog'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> { scriptSrc\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'\u002Fhb\u002Fchangelog.js'\u003C\u002Fspan> })\n\u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F &lt;style&gt;…&lt;\u002Fstyle&gt;&lt;div id=\"hb-changelog\"&gt;…snapshot or skeleton…&lt;\u002Fdiv&gt;&lt;script src=\"\u002Fhb\u002Fchangelog.js\" data-project=\"pk_…\" data-host=\"https:\u002F\u002Fsupport.example.com\" data-target=\"#hb-changelog\" defer&gt;&lt;\u002Fscript&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Kind\u003C\u002Fth>\n\u003Cth>Script\u003C\u002Fth>\n\u003Cth>Server-rendered\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>changelog\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>changelog.js\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A snapshot: releases and entries, newest first.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>roadmap\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>feedback.js\u003C\u002Fcode> (\u003Ccode>data-view=&quot;roadmap&quot; data-only=&quot;roadmap&quot;\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>A snapshot: one list per status that has items.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>feedback\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>feedback.js\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>A skeleton (the board is interactive; the script draws it).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>chat\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>widget.js\u003C\u002Fcode> (\u003Ccode>data-mode=&quot;inline&quot;\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>A skeleton.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Put \u003Ccode>html\u003C\u002Fcode> in your page as it is. A framework that adds scripts itself (Vue, React) takes the parts instead: \u003Ccode>inner\u003C\u002Fcode> is\nthe container's content, \u003Ccode>attrs\u003C\u002Fcode> the script's \u003Ccode>data-*\u003C\u002Fcode> attributes, and \u003Ccode>ssr\u003C\u002Fcode> is \u003Ccode>'ready'\u003C\u002Fcode> for a snapshot or \u003Ccode>'pending'\u003C\u002Fcode>\nfor a skeleton.\u003C\u002Fp>\n\u003Cp>With no \u003Ccode>embed()\u003C\u002Fcode> at all, the scripts still show a skeleton in their target while they load: \u003Ccode>changelog.js\u003C\u002Fcode> and\n\u003Ccode>feedback.js\u003C\u002Fcode> (inline) and \u003Ccode>widget.js\u003C\u002Fcode> (\u003Ccode>data-mode=&quot;inline&quot;\u003C\u002Fcode>). Whatever is in the target when the script runs and carries\n\u003Ccode>data-hb-ssr\u003C\u002Fcode> is kept in place of the skeleton. The script swaps it when its frame says it is ready, or after 8 seconds,\nso an error message still shows. The snapshot is plain HTML in your page's own fonts and colours; the frame that\nreplaces it wears the project's look, so the two differ in style on purpose.\u003C\u002Fp>\n\u003Cp>Style the snapshot with the \u003Ccode>.hb-ssr\u003C\u002Fcode> classes (\u003Ccode>article\u003C\u002Fcode>, \u003Ccode>.hb-release\u003C\u002Fcode>, \u003Ccode>.hb-stage\u003C\u002Fcode>, \u003Ccode>.hb-tag\u003C\u002Fcode>, \u003Ccode>.hb-body\u003C\u002Fcode>…) or the\nvariables \u003Ccode>--hb-ssr-line\u003C\u002Fcode>, \u003Ccode>--hb-ssr-muted\u003C\u002Fcode> and \u003Ccode>--hb-ssr-radius\u003C\u002Fcode>. The rules use \u003Ccode>:where()\u003C\u002Fcode>, so yours win. The HTML inside\na changelog entry is sanitised by HelpBites and inserted as it is.\u003C\u002Fp>\n\u003Ch2 id=\"self-hosting-and-caching-the-scripts\">Self-hosting and caching the scripts\u003Ca class=\"heading-anchor\" href=\"#self-hosting-and-caching-the-scripts\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>\u003Ccode>hb.script('widget.js')\u003C\u002Fcode> fetches a file once, keeps it (\u003Ccode>ttl\u003C\u002Fcode>, then stale-while-revalidate) and returns \u003Ccode>{ body }\u003C\u002Fcode>.\nServe it from your own route:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-ts\">\u003Ccode>\u003Cspan class=\"shj-syn-cmnt\">\u002F\u002F server\u002Froutes\u002Fhb\u002F[file].get.ts (Nuxt)\n\u003C\u002Fspan>\u003Cspan class=\"shj-syn-kwd\">export\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">default\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">defineEventHandler\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-kwd\">async\u003C\u002Fspan> (event) \u003Cspan class=\"shj-syn-kwd\">=&gt;\u003C\u002Fspan> {\n  \u003Cspan class=\"shj-syn-kwd\">const\u003C\u002Fspan> { body } \u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-syn-kwd\">await\u003C\u002Fspan> hb\u003Cspan class=\"shj-syn-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-syn-func\">script\u003C\u002Fspan>(\u003Cspan class=\"shj-syn-func\">getRouterParam\u003C\u002Fspan>(event\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'file'\u003C\u002Fspan>) \u003Cspan class=\"shj-syn-oper\">??\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">''\u003C\u002Fspan>)\n  \u003Cspan class=\"shj-syn-kwd\">if\u003C\u002Fspan> (\u003Cspan class=\"shj-syn-oper\">!\u003C\u002Fspan>body) \u003Cspan class=\"shj-syn-kwd\">throw\u003C\u002Fspan> \u003Cspan class=\"shj-syn-func\">createError\u003C\u002Fspan>({ statusCode\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-num\">502\u003C\u002Fspan> })\n  \u003Cspan class=\"shj-syn-func\">setHeaders\u003C\u002Fspan>(event\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> { 'Content-Type'\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'text\u002Fjavascript; charset=utf-8'\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">,\u003C\u002Fspan> 'Cache-Control'\u003Cspan class=\"shj-syn-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-syn-str\">'public, max-age=300, stale-while-revalidate=86400'\u003C\u002Fspan> })\n  \u003Cspan class=\"shj-syn-kwd\">return\u003C\u002Fspan> body\n})\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>The scripts are MIT, so a file in your repo or on your CDN is fine too. A copy tells the script where HelpBites is with\n\u003Ccode>data-host\u003C\u002Fcode>: \u003Ccode>widget.js\u003C\u002Fcode>, \u003Ccode>feedback.js\u003C\u002Fcode>, \u003Ccode>changelog.js\u003C\u002Fcode>, \u003Ccode>onboarding.js\u003C\u002Fcode>, \u003Ccode>blog.js\u003C\u002Fcode>, \u003Ccode>forms.js\u003C\u002Fcode> and \u003Ccode>pricing.js\u003C\u002Fcode> all read it\n(\u003Ccode>embed()\u003C\u002Fcode> sets it for you). With the one-tag loader, \u003Ccode>data-scripts\u003C\u002Fcode> is the folder the module scripts come from:\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-html\">\u003Ccode>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">src\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u002Fhb\u002Fhelpbites.js\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-project\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-host\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002Fsupport.example.com\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">data-scripts\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">=\u003C\u002Fspan>\u003Cspan class=\"shj-syn-str\">\"\u002Fhb\"\u003C\u002Fspan> \u003Cspan class=\"shj-syn-class\">async\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&lt;\u002F\u003C\u002Fspan>\u003Cspan class=\"shj-syn-var\">script\u003C\u002Fspan>\u003Cspan class=\"shj-syn-oper\">&gt;\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode>helpbites.js\u003C\u002Fcode> also remembers the project's answer to &quot;which embeds do you have&quot; in the visitor's browser (a week): the\nnext page starts from it at once and the fresh answer is kept for the page after. \u003Ccode>data-cache=&quot;false&quot;\u003C\u002Fcode> turns that off.\u003C\u002Fp>\n\u003Cp>HelpBites sends its scripts with \u003Ccode>Cache-Control: public, max-age=300, stale-while-revalidate=86400\u003C\u002Fcode>, so the browser\ncache already serves a script it has without waiting, and refreshes it behind the page.\u003C\u002Fp>\n\u003Ch2 id=\"options\">Options\u003Ca class=\"heading-anchor\" href=\"#options\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Option\u003C\u002Fth>\n\u003Cth>Default\u003C\u002Fth>\n\u003Cth>\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>host\u003C\u002Fcode>, \u003Ccode>pk\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>required\u003C\u002Ftd>\n\u003Ctd>Your HelpBites address and the project's public key.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>budget\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>300\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Milliseconds a read may take before it is \u003Ccode>late\u003C\u002Fcode>. \u003Ccode>0\u003C\u002Fcode> never waits.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>ttl\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>60000\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Milliseconds an answer is fresh.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>stale\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>one day\u003C\u002Ftd>\n\u003Ctd>Milliseconds after which an old answer is not served any more.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>timeout\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>8000\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Milliseconds before a request to HelpBites is given up, whoever is still waiting for it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>backoff\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>10000\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Milliseconds a failed address is not asked again (an answer cached earlier is still served).\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>store\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>in memory, 200 answers\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ get(key), set(key, { at, data }) }\u003C\u002Fcode>, sync or async: a Redis or KV shared between servers.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>fetch\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>globalThis.fetch\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Your own, for tests.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>A broken store is a miss and a failed request is \u003Ccode>error\u003C\u002Fcode>: HelpBites being down never throws in your render.\u003C\u002Fp>\n\u003Ch2 id=\"in-a-swift-app\">In a Swift app\u003Ca class=\"heading-anchor\" href=\"#in-a-swift-app\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>The same idea in SwiftUI: show what was kept, draw a skeleton if there is nothing yet, then replace it with the fresh\nanswer. Plain \u003Ccode>URLSession\u003C\u002Fcode>, no package.\u003C\u002Fp>\n\u003Cdiv class=\"code-block\">\u003Cbutton type=\"button\" class=\"copy-btn\" data-copy aria-label=\"Copy to clipboard\">Copy\u003C\u002Fbutton>\u003Cpre class=\"shj-lang-plain\">\u003Ccode>import SwiftUI\n\nstruct HelpBitesChangelog: View {\n    var host = \u003Cspan class=\"shj-syn-str\">\"https:\u002F\u002Fsupport.example.com\"\u003C\u002Fspan>\n    var pk = \u003Cspan class=\"shj-syn-str\">\"pk_…\"\u003C\u002Fspan>\n    var accent: Color = .accentColor\n    var cornerRadius: CGFloat = 12\n\n    struct Page: Codable { var entries: [Entry] }\n    struct Entry: Codable, Identifiable { var id: String; var title: String; var tag: String; var summary: String }\n\n    @State private var entries: [Entry] = []\n    @State private var loading = true\n\n    private var kept: URL {\n        FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent(\u003Cspan class=\"shj-syn-str\">\"helpbites-changelog-\\(pk).json\"\u003C\u002Fspan>)\n    }\n\n    var body: some View {\n        LazyVStack(alignment: .leading, spacing: 12) {\n            if entries.isEmpty &#38;&#38; loading {\n                ForEach(0..&lt;3, id: \\.self) { _ in\n                    RoundedRectangle(cornerRadius: cornerRadius).fill(.quaternary).frame(height: 84)\n                }\n            }\n            ForEach(entries) { entry in\n                VStack(alignment: .leading, spacing: 4) {\n                    Text(entry.title).font(.headline)\n                    Text(entry.tag.capitalized).font(.caption).foregroundStyle(accent)\n                    if !entry.summary.isEmpty { Text(entry.summary).font(.subheadline).foregroundStyle(.secondary) }\n                }\n                .frame(maxWidth: .infinity, alignment: .leading)\n                .padding()\n                .background(.quaternary.opacity(0.4), in: RoundedRectangle(cornerRadius: cornerRadius))\n            }\n        }\n        .task { await load() }\n    }\n\n    func load() async {\n        defer { loading = false }\n        \u002F\u002F The last answer first.\n        if let data = try? Data(contentsOf: kept), let page = try? JSONDecoder().decode(Page.self, from: data) {\n            entries = page.entries\n        }\n        \u002F\u002F Then HelpBites, kept for next time.\n        guard let url = URL(string: \u003Cspan class=\"shj-syn-str\">\"\\(host)\u002Fv1\u002Fchangelog?pk=\\(pk)&#38;flat=1&#38;limit=20\"\u003C\u002Fspan>),\n              let (data, _) = try? await URLSession.shared.data(from: url),\n              let page = try? JSONDecoder().decode(Page.self, from: data) else { return }\n        entries = page.entries\n        try? data.write(to: kept)\n    }\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n\u003Ch2 id=\"your-own-ui\">Your own UI\u003Ca class=\"heading-anchor\" href=\"#your-own-ui\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cp>Everything here sits on the public API: \u003Ca href=\"\u002Fdocs\u002Ffeedback\">\u003Ccode>GET \u002Fv1\u002Fchangelog\u003C\u002Fcode>\u003C\u002Fa>, \u003Ccode>GET \u002Fv1\u002Ffeedback\u002Froadmap\u003C\u002Fcode>,\n\u003Ccode>GET \u002Fv1\u002Flegal\u002F{pk}\u002F{slug}\u003C\u002Fcode> (see \u003Ca href=\"\u002Fdocs\u002Flegal\">LEGAL.md\u003C\u002Fa>) and \u003Ccode>GET \u002Fv1\u002Fhub\u002F{pk}\u002Floader\u003C\u002Fcode> (\u003Ca href=\"\u002Fdocs\u002Fhub\">HUB.md\u003C\u002Fa>). Read them with\n\u003Ccode>hb.get()\u003C\u002Fcode> and draw them with your own components, as \u003Ccode>ssr.mjs\u003C\u002Fcode> does for the snapshots. The helper never caches anything\nsent with a token, because it never sends one.\u003C\u002Fp>\n\u003Ch2 id=\"rules\">Rules\u003Ca class=\"heading-anchor\" href=\"#rules\" aria-label=\"Link to this section\">#\u003C\u002Fa>\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>Only \u003Ccode>GET\u003C\u002Fcode> requests to public endpoints are made, with the project public key; there is no way to pass a secret or an\nidentity token. Signed-in, per-person data is the scripts' job, in the browser.\u003C\u002Fli>\n\u003Cli>Concurrent reads of the same address share one request.\u003C\u002Fli>\n\u003Cli>A \u003Ccode>404\u003C\u002Fcode> (a project without a public changelog, a draft document) is \u003Ccode>error\u003C\u002Fcode> with \u003Ccode>status: 404\u003C\u002Fcode> and is not cached.\u003C\u002Fli>\n\u003Cli>\u003Ccode>script()\u003C\u002Fcode> accepts only plain file names (\u003Ccode>widget.js\u003C\u002Fcode>, \u003Ccode>helpbites.js\u003C\u002Fcode>, \u003Ccode>cms.mjs\u003C\u002Fcode>…), never a path.\u003C\u002Fli>\n\u003Cli>Scripts and \u003Ccode>ssr.mjs\u003C\u002Fcode> must come from the same HelpBites version family: a copy that is much older than the server may\nnot send the \u003Ccode>ready\u003C\u002Fcode> message, in which case the skeleton gives way after 8 seconds.\u003C\u002Fli>\n\u003C\u002Ful>\n",[6,10,13,16,19,22,25],{"id":7,"text":8,"level":9},"reading-the-api-while-you-render","Reading the API while you render",2,{"id":11,"text":12,"level":9},"the-plug-and-play-scripts-server-rendered","The plug and play scripts, server-rendered",{"id":14,"text":15,"level":9},"self-hosting-and-caching-the-scripts","Self-hosting and caching the scripts",{"id":17,"text":18,"level":9},"options","Options",{"id":20,"text":21,"level":9},"in-a-swift-app","In a Swift app",{"id":23,"text":24,"level":9},"your-own-ui","Your own UI",{"id":26,"text":27,"level":9},"rules","Rules",1792766843063]