diff --git a/static/src/assets/api.js b/static/src/assets/api.js
new file mode 100644
index 0000000..bec2740
--- /dev/null
+++ b/static/src/assets/api.js
@@ -0,0 +1,73 @@
+
+const doFetch = async (req) => {
+ let res, jsonRes;
+ try {
+ res = await fetch(req);
+ jsonRes = await res.json();
+
+ } catch (e) {
+
+ if (e instanceof SyntaxError)
+ e = new Error(`status ${res.status}, empty (or invalid) response body`);
+
+ console.error(`api call ${req.method} ${req.url}: unexpected error:`, e);
+ throw e;
+ }
+
+ if (jsonRes.error) {
+ console.error(
+ `api call ${req.method} ${req.url}: application error:`,
+ res.status,
+ jsonRes.error,
+ );
+
+ throw jsonRes.error;
+ }
+
+ return jsonRes;
+}
+
+// may throw
+const solvePow = async () => {
+
+ const res = await call('GET', '/api/pow/challenge');
+
+ const worker = new Worker('/assets/solvePow.js');
+
+ const p = new Promise((resolve, reject) => {
+ worker.postMessage({seedHex: res.seed, target: res.target});
+ worker.onmessage = resolve;
+ });
+
+ const powSol = (await p).data;
+ worker.terminate();
+
+ return {seed: res.seed, solution: powSol};
+}
+
+const call = async (method, route, opts = {}) => {
+ const { body = {}, requiresPow = false } = opts;
+
+ const reqOpts = { method };
+
+ if (requiresPow) {
+ const {seed, solution} = await solvePow();
+ body.powSeed = seed;
+ body.powSolution = solution;
+ }
+
+ if (Object.keys(body).length > 0) {
+
+ const form = new FormData();
+ for (const key in body) form.append(key, body[key]);
+
+ reqOpts.body = form;
+ }
+
+ const req = new Request(route, reqOpts);
+ return doFetch(req);
+}
+
+export {
+ call,
+}
diff --git a/static/src/follow.md b/static/src/follow.md
index 4e949dd..b02e7f0 100644
--- a/static/src/follow.md
+++ b/static/src/follow.md
@@ -4,6 +4,8 @@ title: "Follow the Blog"
nofollow: true
---
+
+
Here's your options for receiving updates about new blog posts:
## Option 1: Email
@@ -40,82 +42,45 @@ is published then input your email below and smash that subscribe button!