From 77855547c006f955ce527ec36ff18c27746bd3c5 Mon Sep 17 00:00:00 2001 From: NoahMaizels Date: Fri, 3 Apr 2026 06:02:06 +0700 Subject: [PATCH 1/2] fix: replace timing delay with retryFeedRead helper in script-02 Replace the hardcoded 1-second delay before feed reads with the retryFeedRead helper, which retries until the feed is indexed and prompts the user if it takes longer than 10 seconds. This matches the pattern described in the Dynamic Content guide. Co-Authored-By: Claude Sonnet 4.6 --- dynamic-content/script-02.js | 65 +++++++++-- multi-author-blog/.env | 2 + multi-author-blog/.gitignore | 5 + multi-author-blog/add-post.js | 90 +++++++++++++++ multi-author-blog/init.js | 181 ++++++++++++++++++++++++++++++ multi-author-blog/package.json | 16 +++ multi-author-blog/read.js | 54 +++++++++ multi-author-blog/update-index.js | 84 ++++++++++++++ 8 files changed, 487 insertions(+), 10 deletions(-) create mode 100644 multi-author-blog/.env create mode 100644 multi-author-blog/.gitignore create mode 100644 multi-author-blog/add-post.js create mode 100644 multi-author-blog/init.js create mode 100644 multi-author-blog/package.json create mode 100644 multi-author-blog/read.js create mode 100644 multi-author-blog/update-index.js diff --git a/dynamic-content/script-02.js b/dynamic-content/script-02.js index d57fcbd..f3a27e5 100644 --- a/dynamic-content/script-02.js +++ b/dynamic-content/script-02.js @@ -39,12 +39,9 @@ const writer = bee.makeFeedWriter(topic, pk); await writer.upload(batchId, upload.reference); console.log("Feed updated at index 0"); -// Brief pause to allow the node to index the feed chunk -await new Promise((r) => setTimeout(r, 1000)); - -// Read the latest reference from the feed +// Read the latest reference from the feed (retries until indexed) const reader = bee.makeFeedReader(topic, owner); -const result = await reader.downloadReference(); +const result = await retryFeedRead(() => reader.downloadReference()); console.log("Latest reference:", result.reference.toHex()); console.log("Current index:", result.feedIndex.toBigInt()); @@ -58,10 +55,58 @@ console.log("\nNew content hash:", upload2.reference.toHex()); await writer.upload(batchId, upload2.reference); console.log("Feed updated at index 1"); -// Brief pause to allow the node to index the feed chunk -await new Promise((r) => setTimeout(r, 1000)); - -// Reading the feed now returns the updated reference -const result2 = await reader.downloadReference(); +// Reading the feed now returns the updated reference. +// Pass minFeedIndex so the retry waits for the new entry, not just any entry. +const result2 = await retryFeedRead( + () => reader.downloadReference(), + result.feedIndex.toBigInt() + 1n +); console.log("Latest reference:", result2.reference.toHex()); console.log("Current index:", result2.feedIndex.toBigInt()); // 1n + +// --- Retry helper --- + +/** + * Retries a feed read function until it returns an entry at or above + * minFeedIndex. Logs elapsed time on each attempt and prompts the user + * to continue or exit every 10 seconds when running interactively. + * + * @param {() => Promise<{feedIndex: {toBigInt: () => bigint}, reference: any}>} fn + * @param {bigint} minFeedIndex Minimum feed index to accept (default 0n) + * @returns {Promise} Resolved value of fn once the index requirement is met + */ +async function retryFeedRead(fn, minFeedIndex = 0n) { + const RETRY_INTERVAL_MS = 1_000; + const PROMPT_INTERVAL_MS = 10_000; + const start = Date.now(); + let lastPrompt = start; + + while (true) { + try { + const value = await fn(); + if (value.feedIndex.toBigInt() >= minFeedIndex) { + if (Date.now() > start + RETRY_INTERVAL_MS) { + process.stdout.write("\n"); // clear the retrying line + } + return value; + } + // Got a stale entry — treat as a miss and keep retrying + } catch {} + + const elapsed = Math.round((Date.now() - start) / 1000); + process.stdout.write( + `\rFeed not yet indexed, retrying... (${elapsed}s elapsed) ` + ); + + const now = Date.now(); + if (process.stdin.isTTY && now - lastPrompt >= PROMPT_INTERVAL_MS) { + lastPrompt = now; + process.stdout.write( + `\nStill waiting after ${elapsed}s. Press Enter to keep retrying, or Ctrl+C to exit: ` + ); + await new Promise((resolve) => process.stdin.once("data", resolve)); + } + + await new Promise((r) => setTimeout(r, RETRY_INTERVAL_MS)); + } +} diff --git a/multi-author-blog/.env b/multi-author-blog/.env new file mode 100644 index 0000000..b85661d --- /dev/null +++ b/multi-author-blog/.env @@ -0,0 +1,2 @@ +BEE_URL=http://localhost:1633 +BATCH_ID=YOUR_BATCH_ID diff --git a/multi-author-blog/.gitignore b/multi-author-blog/.gitignore new file mode 100644 index 0000000..4a0bccc --- /dev/null +++ b/multi-author-blog/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +config.json +authors.json +alice-posts.json +bob-posts.json diff --git a/multi-author-blog/add-post.js b/multi-author-blog/add-post.js new file mode 100644 index 0000000..ff9997e --- /dev/null +++ b/multi-author-blog/add-post.js @@ -0,0 +1,90 @@ +/** + * add-post.js — Publish a new post as an author. + * + * Each author independently controls their own feed. This script: + * 1. Loads the author's key and topic from config.json + * 2. Appends the new post to the author's local post list + * 3. Regenerates the author's HTML page + * 4. Uploads it and updates the author's feed + * + * Usage: + * node add-post.js "Post title" "Post body" + */ + +import { Bee, Topic, PrivateKey } from "@ethersphere/bee-js"; +import { readFileSync, writeFileSync } from "fs"; +import { config } from "dotenv"; +config(); + +const [,, authorArg, title, ...bodyWords] = process.argv; +const body = bodyWords.join(" "); + +if (!authorArg || !title || !body) { + console.error('Usage: node add-post.js "Post title" "Post body"'); + process.exit(1); +} + +const bee = new Bee(process.env.BEE_URL); +const batchId = process.env.BATCH_ID; +const cfg = JSON.parse(readFileSync("config.json", "utf-8")); + +const author = cfg[authorArg]; +if (!author) { + console.error(`Unknown author: ${authorArg}`); + process.exit(1); +} + +const pk = new PrivateKey(author.privateKey); +const topic = Topic.fromString(cfg.topics[authorArg]); + +// Load or initialize the author's post list +const postsFile = `${authorArg}-posts.json`; +let posts = []; +try { + posts = JSON.parse(readFileSync(postsFile, "utf-8")); +} catch { + // First post — file doesn't exist yet +} + +const newPost = { title, body, date: new Date().toISOString() }; +posts.push(newPost); +writeFileSync(postsFile, JSON.stringify(posts, null, 2)); + +// Regenerate the author's page HTML +const html = generateAuthorHTML( + authorArg.charAt(0).toUpperCase() + authorArg.slice(1), + posts +); + +// Upload and update the author's feed +const upload = await bee.uploadFile(batchId, html, "index.html", { + contentType: "text/html", +}); +const writer = bee.makeFeedWriter(topic, pk); +await writer.upload(batchId, upload.reference); + +console.log(`Post published by ${authorArg}! (${posts.length} total)`); +console.log("View: " + `${process.env.BEE_URL}/bzz/${cfg.manifests[authorArg]}/`); + +function generateAuthorHTML(name, posts) { + const items = posts + .map( + (p) => ` +
+

${p.title}

+ ${p.date} +

${p.body}

+
` + ) + .join("\n"); + + return ` + +${name}'s Blog + +

${name}'s Blog

+

${posts.length} post${posts.length !== 1 ? "s" : ""}

+ ${items} + +`; +} diff --git a/multi-author-blog/init.js b/multi-author-blog/init.js new file mode 100644 index 0000000..9dff96b --- /dev/null +++ b/multi-author-blog/init.js @@ -0,0 +1,181 @@ +/** + * init.js — Initialize the multi-author blog. + * + * Run once to: + * 1. Generate keys for admin, Alice, and Bob + * 2. Create feeds and feed manifests for each author + * 3. Build and upload the authors.json index feed + * 4. Generate and upload the homepage feed + * 5. Save all config to config.json + * + * Usage: + * node init.js + */ + +import { Bee, Topic, PrivateKey } from "@ethersphere/bee-js"; +import crypto from "crypto"; +import { writeFileSync } from "fs"; +import { config } from "dotenv"; +config(); + +const bee = new Bee(process.env.BEE_URL); +const batchId = process.env.BATCH_ID; + +function makeKey() { + const hex = "0x" + crypto.randomBytes(32).toString("hex"); + return new PrivateKey(hex); +} + +// Generate keys for admin, Alice, and Bob +const adminKey = makeKey(); +const aliceKey = makeKey(); +const bobKey = makeKey(); + +const adminOwner = adminKey.publicKey().address(); +const aliceOwner = aliceKey.publicKey().address(); +const bobOwner = bobKey.publicKey().address(); + +// Topics — each feed has a unique topic +const aliceTopic = Topic.fromString("alice-posts"); +const bobTopic = Topic.fromString("bob-posts"); +const indexTopic = Topic.fromString("blog-index"); +const homeTopic = Topic.fromString("blog-home"); + +// --- Step 1: Upload initial author pages --- +const aliceHTML = generateAuthorHTML("Alice", []); +const bobHTML = generateAuthorHTML("Bob", []); + +const aliceUpload = await bee.uploadFile(batchId, aliceHTML, "index.html", { + contentType: "text/html", +}); +const bobUpload = await bee.uploadFile(batchId, bobHTML, "index.html", { + contentType: "text/html", +}); + +// --- Step 2: Create author feeds --- +const aliceWriter = bee.makeFeedWriter(aliceTopic, aliceKey); +const bobWriter = bee.makeFeedWriter(bobTopic, bobKey); + +await aliceWriter.upload(batchId, aliceUpload.reference); +await bobWriter.upload(batchId, bobUpload.reference); + +// --- Step 3: Create author feed manifests (stable references) --- +const aliceManifest = await bee.createFeedManifest(batchId, aliceTopic, aliceOwner); +const bobManifest = await bee.createFeedManifest(batchId, bobTopic, bobOwner); + +console.log("Alice feed manifest:", aliceManifest.toHex()); +console.log("Bob feed manifest: ", bobManifest.toHex()); + +// --- Step 4: Build and upload the authors.json index --- +const authors = [ + { + name: "Alice", + topic: "alice-posts", + owner: aliceOwner.toHex(), + feedManifest: aliceManifest.toHex(), + }, + { + name: "Bob", + topic: "bob-posts", + owner: bobOwner.toHex(), + feedManifest: bobManifest.toHex(), + }, +]; +const authorsJson = JSON.stringify(authors, null, 2); +writeFileSync("authors.json", authorsJson); + +const indexUpload = await bee.uploadFile(batchId, authorsJson, "authors.json", { + contentType: "application/json", +}); + +// --- Step 5: Create the index feed --- +const indexWriter = bee.makeFeedWriter(indexTopic, adminKey); +await indexWriter.upload(batchId, indexUpload.reference); +const indexManifest = await bee.createFeedManifest(batchId, indexTopic, adminOwner); + +console.log("Index feed manifest:", indexManifest.toHex()); + +// --- Step 6: Generate and upload the homepage --- +const homeHTML = generateHomepageHTML(authors, []); +const homeUpload = await bee.uploadFile(batchId, homeHTML, "index.html", { + contentType: "text/html", +}); + +const homeWriter = bee.makeFeedWriter(homeTopic, adminKey); +await homeWriter.upload(batchId, homeUpload.reference); +const homeManifest = await bee.createFeedManifest(batchId, homeTopic, adminOwner); + +// --- Step 7: Save config --- +const cfg = { + admin: { privateKey: adminKey.toHex(), owner: adminOwner.toHex() }, + alice: { privateKey: aliceKey.toHex(), owner: aliceOwner.toHex() }, + bob: { privateKey: bobKey.toHex(), owner: bobOwner.toHex() }, + topics: { + alice: "alice-posts", + bob: "bob-posts", + index: "blog-index", + home: "blog-home", + }, + manifests: { + alice: aliceManifest.toHex(), + bob: bobManifest.toHex(), + index: indexManifest.toHex(), + home: homeManifest.toHex(), + }, +}; +writeFileSync("config.json", JSON.stringify(cfg, null, 2)); + +console.log("\nBlog initialized!"); +console.log("Homepage: " + `${process.env.BEE_URL}/bzz/${homeManifest.toHex()}/`); +console.log("Alice's feed: " + `${process.env.BEE_URL}/bzz/${aliceManifest.toHex()}/`); +console.log("Bob's feed: " + `${process.env.BEE_URL}/bzz/${bobManifest.toHex()}/`); + +function generateAuthorHTML(name, posts) { + const items = posts + .map( + (p) => ` +
+

${p.title}

+ ${p.date} +

${p.body}

+
` + ) + .join("\n"); + + return ` + +${name}'s Blog + +

${name}'s Blog

+

${posts.length} post${posts.length !== 1 ? "s" : ""}

+ ${items || "

No posts yet.

"} + +`; +} + +function generateHomepageHTML(authors, latestPosts) { + const cards = authors + .map( + (a) => { + const latest = latestPosts.find((p) => p.author === a.name); + const preview = latest + ? `

${latest.title} — ${latest.date}

${latest.body.slice(0, 120)}…

` + : `

No posts yet.

`; + return `
+

${a.name}

+ ${preview} +
`; + } + ) + .join("\n"); + + return ` + +Multi-Author Blog + +

Multi-Author Blog

+

${authors.length} author${authors.length !== 1 ? "s" : ""}

+ ${cards} + +`; +} diff --git a/multi-author-blog/package.json b/multi-author-blog/package.json new file mode 100644 index 0000000..57e3e9a --- /dev/null +++ b/multi-author-blog/package.json @@ -0,0 +1,16 @@ +{ + "name": "multi-author-blog", + "version": "1.0.0", + "description": "A decentralized multi-author blog on Swarm — example project for the Multi-Author Blog guide.", + "type": "module", + "scripts": { + "init": "node init.js", + "add-post": "node add-post.js", + "update-index": "node update-index.js", + "read": "node read.js" + }, + "dependencies": { + "@ethersphere/bee-js": "^9.1.1", + "dotenv": "^16.4.7" + } +} diff --git a/multi-author-blog/read.js b/multi-author-blog/read.js new file mode 100644 index 0000000..6682477 --- /dev/null +++ b/multi-author-blog/read.js @@ -0,0 +1,54 @@ +/** + * read.js — Read the multi-author blog without private keys. + * + * Demonstrates that any third party can discover all authors and + * read their feeds using only the index feed manifest hash. + * No private keys are required. + * + * Usage: + * node read.js + */ + +import { Bee, Topic, EthAddress } from "@ethersphere/bee-js"; +import { readFileSync } from "fs"; +import { config } from "dotenv"; +config(); + +const bee = new Bee(process.env.BEE_URL); +const cfg = JSON.parse(readFileSync("config.json", "utf-8")); + +// Read the index feed to get the current authors manifest +const indexTopic = Topic.fromString(cfg.topics.index); +const indexOwner = new EthAddress(cfg.admin.owner); +const indexReader = bee.makeFeedReader(indexTopic, indexOwner); +const indexResult = await indexReader.downloadReference(); +console.log("Index feed at index:", indexResult.feedIndex.toBigInt()); + +// Download the authors.json manifest +const authorsData = await bee.downloadFile(indexResult.reference); +const authors = JSON.parse(authorsData.data.toUtf8()); + +console.log(`\n${authors.length} authors in blog:\n`); + +// For each author, read their feed +for (const author of authors) { + const topic = Topic.fromString(author.topic); + const owner = new EthAddress(author.owner); + const reader = bee.makeFeedReader(topic, owner); + try { + const result = await reader.download(); + console.log(`${author.name}`); + console.log(` Feed index: ${result.feedIndex.toBigInt()}`); + console.log(` URL: ${process.env.BEE_URL}/bzz/${author.feedManifest}/`); + } catch { + console.log(`${author.name}: feed not yet populated`); + } +} + +// Read the homepage feed +const homeTopic = Topic.fromString(cfg.topics.home); +const homeOwner = new EthAddress(cfg.admin.owner); +const homeReader = bee.makeFeedReader(homeTopic, homeOwner); +const homeResult = await homeReader.download(); +console.log(`\nHomepage feed at index: ${homeResult.feedIndex.toBigInt()}`); +console.log(`Homepage URL: ${process.env.BEE_URL}/bzz/${cfg.manifests.home}/`); diff --git a/multi-author-blog/update-index.js b/multi-author-blog/update-index.js new file mode 100644 index 0000000..dae0256 --- /dev/null +++ b/multi-author-blog/update-index.js @@ -0,0 +1,84 @@ +/** + * update-index.js — Admin aggregates author feeds and updates the homepage. + * + * Reads each author's feed to confirm it is live, loads the latest post + * from the local post sidecars, regenerates the homepage HTML with + * previews, and updates the homepage feed. + * + * Usage: + * node update-index.js + */ + +import { Bee, Topic, EthAddress, PrivateKey } from "@ethersphere/bee-js"; +import { readFileSync, writeFileSync } from "fs"; +import { config } from "dotenv"; +config(); + +const bee = new Bee(process.env.BEE_URL); +const batchId = process.env.BATCH_ID; +const cfg = JSON.parse(readFileSync("config.json", "utf-8")); +const authors = JSON.parse(readFileSync("authors.json", "utf-8")); + +// Read each author's latest feed entry to confirm their feed is live +const latestPosts = []; +for (const author of authors) { + const topic = Topic.fromString(author.topic); + const owner = new EthAddress(author.owner); + const reader = bee.makeFeedReader(topic, owner); + + try { + const result = await reader.download(); + console.log(`${author.name}: feed index ${result.feedIndex.toBigInt()}`); + + // Load the local post sidecar to get post data for the preview + const postsFile = `${author.name.toLowerCase()}-posts.json`; + const posts = JSON.parse(readFileSync(postsFile, "utf-8")); + const latest = posts.at(-1); + if (latest) { + latestPosts.push({ author: author.name, ...latest }); + } + } catch { + console.log(`${author.name}: no feed entries yet`); + } +} + +// Regenerate homepage with latest post previews from all authors +const homeHTML = generateHomepageHTML(authors, latestPosts); +const homeUpload = await bee.uploadFile(batchId, homeHTML, "index.html", { + contentType: "text/html", +}); + +const adminKey = new PrivateKey(cfg.admin.privateKey); +const homeTopic = Topic.fromString(cfg.topics.home); +const homeWriter = bee.makeFeedWriter(homeTopic, adminKey); +await homeWriter.upload(batchId, homeUpload.reference); + +console.log("\nHomepage updated!"); +console.log("View: " + `${process.env.BEE_URL}/bzz/${cfg.manifests.home}/`); + +function generateHomepageHTML(authors, latestPosts) { + const cards = authors + .map( + (a) => { + const latest = latestPosts.find((p) => p.author === a.name); + const preview = latest + ? `

${latest.title} — ${latest.date}

${latest.body.slice(0, 120)}…

` + : `

No posts yet.

`; + return `
+

${a.name}

+ ${preview} +
`; + } + ) + .join("\n"); + + return ` + +Multi-Author Blog + +

Multi-Author Blog

+

${authors.length} author${authors.length !== 1 ? "s" : ""}

+ ${cards} + +`; +} From 84b2078766f81a4056948176cc0ac608a743edbf Mon Sep 17 00:00:00 2001 From: NoahMaizels Date: Fri, 3 Apr 2026 07:01:43 +0700 Subject: [PATCH 2/2] feat: add add-author.js script to multi-author-blog example Adds a complete runnable add-author.js script that handles the full flow of adding a new author: key generation, initial page upload, feed and manifest creation, authors.json update, and config.json update. Also updates package.json scripts and .gitignore pattern. Co-Authored-By: Claude Sonnet 4.6 --- multi-author-blog/.gitignore | 1 + multi-author-blog/add-author.js | 127 ++++++++++++++++++++++++++++++++ multi-author-blog/package.json | 1 + 3 files changed, 129 insertions(+) create mode 100644 multi-author-blog/add-author.js diff --git a/multi-author-blog/.gitignore b/multi-author-blog/.gitignore index 4a0bccc..6454e76 100644 --- a/multi-author-blog/.gitignore +++ b/multi-author-blog/.gitignore @@ -3,3 +3,4 @@ config.json authors.json alice-posts.json bob-posts.json +*-posts.json diff --git a/multi-author-blog/add-author.js b/multi-author-blog/add-author.js new file mode 100644 index 0000000..4cb9fc1 --- /dev/null +++ b/multi-author-blog/add-author.js @@ -0,0 +1,127 @@ +/** + * add-author.js — Add a new author to the multi-author blog. + * + * This script: + * 1. Generates a new key for the author + * 2. Uploads an empty initial blog page for them + * 3. Creates their feed and feed manifest + * 4. Appends their entry to authors.json and re-uploads to the index feed + * 5. Updates config.json so the author can use add-post.js + * + * Run update-index.js afterwards to refresh the homepage with the new author. + * + * Usage: + * node add-author.js + * + * Example: + * node add-author.js charlie + */ + +import { Bee, Topic, PrivateKey } from "@ethersphere/bee-js"; +import crypto from "crypto"; +import { readFileSync, writeFileSync } from "fs"; +import { config } from "dotenv"; +config(); + +const [,, nameArg] = process.argv; +if (!nameArg) { + console.error("Usage: node add-author.js "); + process.exit(1); +} + +const authorName = nameArg.toLowerCase(); +const authorLabel = authorName.charAt(0).toUpperCase() + authorName.slice(1); + +const bee = new Bee(process.env.BEE_URL); +const batchId = process.env.BATCH_ID; +const cfg = JSON.parse(readFileSync("config.json", "utf-8")); + +if (cfg[authorName]) { + console.error(`Author "${authorName}" already exists in config.json.`); + process.exit(1); +} + +// --- Step 1: Generate a key for the new author --- +const hex = "0x" + crypto.randomBytes(32).toString("hex"); +const authorKey = new PrivateKey(hex); +const authorOwner = authorKey.publicKey().address(); +const authorTopic = Topic.fromString(`${authorName}-posts`); + +console.log(`Adding author: ${authorLabel}`); +console.log(`Address: ${authorOwner.toHex()}`); + +// --- Step 2: Upload initial empty page --- +const html = generateAuthorHTML(authorLabel, []); +const upload = await bee.uploadFile(batchId, html, "index.html", { + contentType: "text/html", +}); +console.log("Uploaded initial page:", upload.reference.toHex()); + +// --- Step 3: Create feed and manifest --- +const writer = bee.makeFeedWriter(authorTopic, authorKey); +await writer.upload(batchId, upload.reference); + +const manifest = await bee.createFeedManifest(batchId, authorTopic, authorOwner); +console.log("Feed manifest:", manifest.toHex()); + +// --- Step 4: Append to authors.json and re-upload to index feed --- +const authors = JSON.parse(readFileSync("authors.json", "utf-8")); + +if (authors.find((a) => a.name.toLowerCase() === authorName)) { + console.error(`Author "${authorName}" already exists in authors.json.`); + process.exit(1); +} + +authors.push({ + name: authorLabel, + topic: `${authorName}-posts`, + owner: authorOwner.toHex(), + feedManifest: manifest.toHex(), +}); +const authorsJson = JSON.stringify(authors, null, 2); +writeFileSync("authors.json", authorsJson); + +const indexUpload = await bee.uploadFile(batchId, authorsJson, "authors.json", { + contentType: "application/json", +}); +const adminKey = new PrivateKey(cfg.admin.privateKey); +const indexTopic = Topic.fromString(cfg.topics.index); +const indexWriter = bee.makeFeedWriter(indexTopic, adminKey); +await indexWriter.upload(batchId, indexUpload.reference); +console.log("Index feed updated."); + +// --- Step 5: Update config.json --- +cfg[authorName] = { + privateKey: authorKey.toHex(), + owner: authorOwner.toHex(), +}; +cfg.topics[authorName] = `${authorName}-posts`; +cfg.manifests[authorName] = manifest.toHex(); +writeFileSync("config.json", JSON.stringify(cfg, null, 2)); + +console.log(`\nAuthor "${authorLabel}" added! (${authors.length} authors total)`); +console.log(`Their feed: ${process.env.BEE_URL}/bzz/${manifest.toHex()}/`); +console.log(`Run update-index.js to refresh the homepage.`); + +function generateAuthorHTML(name, posts) { + const items = posts + .map( + (p) => ` +
+

${p.title}

+ ${p.date} +

${p.body}

+
` + ) + .join("\n"); + + return ` + +${name}'s Blog + +

${name}'s Blog

+

${posts.length} post${posts.length !== 1 ? "s" : ""}

+ ${items || "

No posts yet.

"} + +`; +} diff --git a/multi-author-blog/package.json b/multi-author-blog/package.json index 57e3e9a..3e24b22 100644 --- a/multi-author-blog/package.json +++ b/multi-author-blog/package.json @@ -6,6 +6,7 @@ "scripts": { "init": "node init.js", "add-post": "node add-post.js", + "add-author": "node add-author.js", "update-index": "node update-index.js", "read": "node read.js" },