Skip to content

Conversation

atharvadeosthale
Copy link
Member

@atharvadeosthale atharvadeosthale commented Sep 17, 2025

Blog creation

Blog creation flow allows the user to quickly generate folders and markdoc files and paste the cover into the correct location just by dragging and dropping the file into the CLI.

image image image image image

Author creation

Flow is pretty similar for author creation

image

Summary by CodeRabbit

  • New Features

    • Interactive CLI to create blog posts with guided prompts (title, slug, description, date, read time, author, category, featured flag, cover image) and automated image handling.
    • Interactive CLI to create author profiles with guided prompts (name, slug, role, bio, avatar, social links) and validation/slug generation.
    • Enhanced terminal experience with colorized output, single/multi-select lists, and graceful cancellation.
  • Chores

    • Added npm scripts to run the new post and author creation tools.

Copy link
Contributor

coderabbitai bot commented Sep 17, 2025

Walkthrough

Adds two npm scripts to package.json: create-blog and create-author. Adds three new CLI scripts under scripts/blog/: create-blog.js (interactive blog post creator), create-author.js (interactive author profile creator), and utils.js (shared CLI utilities for prompts, selections, image handling, colors, and readline management). Scripts handle slug generation/validation, frontmatter generation, filesystem operations to create directories/files, optional image copying, and discovery of authors/categories from repository paths. utils.js exports multiple helper functions and COLORS; no existing public API signatures were modified.

Suggested reviewers

  • adityaoberai

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Scripts to add blog and authors" accurately and concisely summarizes the main change in this PR—adding CLI scripts and utilities to create blog posts and author pages (scripts/blog/create-blog.js, create-author.js, scripts/blog/utils.js and package.json entries). It is clear, relevant to the files changed, and not misleading or overly broad. A reviewer scanning history would understand the primary intent from this title.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-blog-scripts

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5edb54c and 668151e.

📒 Files selected for processing (2)
  • scripts/blog/create-author.js (1 hunks)
  • scripts/blog/utils.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/blog/utils.js
  • scripts/blog/create-author.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (11)
scripts/blog/utils.js (5)

213-214: Fix multi-select value extraction (false/0 currently lost).

Use an explicit undefined check; || mis-handles falsy option values.

-                const selected = Array.from(selectedItems).map(i => options[i].value || options[i]);
+                const selected = Array.from(selectedItems).map((i) =>
+                    options[i].value !== undefined ? options[i].value : options[i]
+                );

101-107: Add non‑TTY fallback to avoid crashes when raw mode is unavailable.

process.stdin.setRawMode(true) throws if stdin isn’t a TTY (CI, pipes). Provide a simple numeric fallback.

         render();
 
-        process.stdin.setRawMode(true);
-        process.stdin.resume();
-        process.stdin.setEncoding('utf8');
+        if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
+            // Fallback: simple numeric selection
+            console.log('\n' + options.map((o, i) => `  ${i + 1}) ${o.label || o}`).join('\n'));
+            questionWithDefault('Enter choice number', '1').then((ans) => {
+                const idx = Math.min(options.length - 1, Math.max(0, (parseInt(ans, 10) || 1) - 1));
+                resolve(options[idx].value !== undefined ? options[idx].value : options[idx]);
+            });
+            return;
+        }
+        process.stdin.setRawMode(true);
+        process.stdin.resume();
+        process.stdin.setEncoding('utf8');

108-113: Close readline on Ctrl+C to restore terminal cleanly.

Ensure rl is closed before exit.

             if (key === '\u0003') { // Ctrl+C
                 process.stdin.setRawMode(false);
                 process.stdin.pause();
                 console.log('\n');
+                try { rl.close(); } catch {}
                 process.exit();
             }

194-201: Mirror the non‑TTY fallback in multi-select.

Same raw‑mode concern applies here.

         render();
 
-        process.stdin.setRawMode(true);
-        process.stdin.resume();
-        process.stdin.setEncoding('utf8');
+        if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function') {
+            console.log('\n' + options.map((o, i) => `  ${i + 1}) ${o.label || o}`).join('\n'));
+            questionWithDefault('Enter comma-separated numbers', '').then((ans) => {
+                const indices = (ans || '')
+                    .split(',')
+                    .map((s) => parseInt(s.trim(), 10) - 1)
+                    .filter((i) => Number.isInteger(i) && i >= 0 && i < options.length);
+                const selected = (indices.length ? indices : [0]).map((i) =>
+                    options[i].value !== undefined ? options[i].value : options[i]
+                );
+                resolve(selected);
+            });
+            return;
+        }
+        process.stdin.setRawMode(true);
+        process.stdin.resume();
+        process.stdin.setEncoding('utf8');

201-206: Also close readline on Ctrl+C here.

             if (key === '\u0003') { // Ctrl+C
                 process.stdin.setRawMode(false);
                 process.stdin.pause();
                 console.log('\n');
+                try { rl.close(); } catch {}
                 process.exit();
             }
scripts/blog/create-author.js (2)

33-40: Slugify: trim leading/trailing hyphens.

Prevents slugs like -foo- if the name starts/ends with symbols.

 function slugify(text) {
     return text
         .toLowerCase()
         .replace(/[^\w\s-]/g, '')
         .replace(/\s+/g, '-')
         .replace(/--+/g, '-')
-        .trim();
+        .replace(/^-+|-+$/g, '')
+        .trim();
 }

277-278: Command hint: pnpm script invocation.

“pnpm create-blog” is fine with pnpm (alias to run). Consider “pnpm run create-blog” for clarity to npm users reading logs.

package.json (1)

25-27: Unrelated: duplicate dependency key detected.

posthog-js appears twice (Lines 32 and 91) with different versions; JSON keeps only the latter. Please dedupe to a single version.

scripts/blog/create-blog.js (3)

33-43: Robustly read author names/slugs from frontmatter (handle quoted values).

Dequote matched scalars to avoid showing quotes in the picker.

-                    if (match) {
-                        const frontmatter = match[1];
-                        const nameMatch = frontmatter.match(/name:\s*(.+)/);
-                        const slugMatch = frontmatter.match(/slug:\s*(.+)/);
-
-                        if (nameMatch && slugMatch) {
-                            authors.push({
-                                label: nameMatch[1].trim(),
-                                value: slugMatch[1].trim()
-                            });
-                        }
-                    }
+                    if (match) {
+                        const frontmatter = match[1];
+                        const nameMatch = frontmatter.match(/^\s*name:\s*(.+)$/m);
+                        const slugMatch = frontmatter.match(/^\s*slug:\s*(.+)$/m);
+                        if (nameMatch && slugMatch) {
+                            const dequote = (s) => s.trim().replace(/^['"]|['"]$/g, '');
+                            const name = dequote(nameMatch[1]);
+                            const slug = dequote(slugMatch[1]);
+                            authors.push({ label: name, value: slug });
+                        }
+                    }

133-137: Validate date format and coerce timeToRead to a positive integer.

Prevents malformed frontmatter types.

-    const today = new Date().toISOString().split('T')[0];
-    blogInfo.date = await questionWithDefault('Date (YYYY-MM-DD)', today);
-
-    blogInfo.timeToRead = await questionWithDefault('Time to read (minutes)', '5');
+    const today = new Date().toISOString().split('T')[0];
+    blogInfo.date = await questionWithDefault('Date (YYYY-MM-DD)', today);
+    const dateRe = /^\d{4}-\d{2}-\d{2}$/;
+    while (!dateRe.test(blogInfo.date)) {
+        console.log(`${COLORS.red}Invalid date. Use YYYY-MM-DD.${COLORS.reset}`);
+        blogInfo.date = await question('Date (YYYY-MM-DD): ');
+    }
+
+    let ttrInput = await questionWithDefault('Time to read (minutes)', '5');
+    let ttr = Number.parseInt(String(ttrInput), 10);
+    while (!Number.isInteger(ttr) || ttr <= 0) {
+        console.log(`${COLORS.red}Enter a positive integer.${COLORS.reset}`);
+        ttrInput = await question('Time to read (minutes): ');
+        ttr = Number.parseInt(String(ttrInput), 10);
+    }
+    blogInfo.timeToRead = ttr;

166-183: Optional: omit cover field if no image was provided.

Reduces broken image risk until the asset is added.

-    const imageResult = await imagePathInput('Add Cover Image', fullCoverPath);
-    blogInfo.cover = coverPath;
+    const imageResult = await imagePathInput('Add Cover Image', fullCoverPath);
+    blogInfo.cover = coverPath;
     blogInfo.coverSourcePath = imageResult.sourcePath;
     blogInfo.coverTargetPath = imageResult.targetPath;
+    if (!blogInfo.coverSourcePath) {
+        delete blogInfo.cover; // handled in generateMarkdocContent if desired
+    }

If you prefer this, I can adjust generateMarkdocContent to conditionally include cover.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57a29ea and 5edb54c.

📒 Files selected for processing (4)
  • package.json (1 hunks)
  • scripts/blog/create-author.js (1 hunks)
  • scripts/blog/create-blog.js (1 hunks)
  • scripts/blog/utils.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
scripts/blog/create-blog.js (1)
scripts/blog/utils.js (12)
  • path (281-281)
  • fs (266-266)
  • fs (280-280)
  • COLORS (9-21)
  • COLORS (9-21)
  • question (37-43)
  • questionWithDefault (45-54)
  • selectFromList (56-139)
  • imagePathInput (244-275)
  • copyImage (277-298)
  • printHeader (23-35)
  • closeReadline (300-302)
scripts/blog/create-author.js (1)
scripts/blog/utils.js (11)
  • path (281-281)
  • COLORS (9-21)
  • COLORS (9-21)
  • fs (266-266)
  • fs (280-280)
  • question (37-43)
  • questionWithDefault (45-54)
  • imagePathInput (244-275)
  • copyImage (277-298)
  • selectFromList (56-139)
  • closeReadline (300-302)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: tests
  • GitHub Check: build
🔇 Additional comments (1)
package.json (1)

26-27: LGTM: scripts wired for the new CLIs.

Comment on lines +161 to +184
let content = `---
layout: author
slug: ${authorInfo.slug}
name: ${authorInfo.name}
role: ${authorInfo.role}
bio: ${authorInfo.bio}
avatar: ${authorInfo.avatar}`;

if (authorInfo.twitter) {
content += `\ntwitter: ${authorInfo.twitter}`;
}

if (authorInfo.github) {
content += `\ngithub: ${authorInfo.github}`;
}

if (authorInfo.linkedin) {
content += `\nlinkedin: ${authorInfo.linkedin}`;
}

content += '\n---\n';

return content;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Quote/escape YAML frontmatter to avoid parse breaks.

Unquoted user input (name/role/bio/links) will break on : or special chars. Use JSON strings (valid YAML) for safety.

 function generateAuthorMarkdoc(authorInfo) {
-    let content = `---
-layout: author
-slug: ${authorInfo.slug}
-name: ${authorInfo.name}
-role: ${authorInfo.role}
-bio: ${authorInfo.bio}
-avatar: ${authorInfo.avatar}`;
+    let content = `---
+layout: author
+slug: ${JSON.stringify(authorInfo.slug)}
+name: ${JSON.stringify(authorInfo.name)}
+role: ${JSON.stringify(authorInfo.role)}
+bio: ${JSON.stringify(authorInfo.bio)}
+avatar: ${JSON.stringify(authorInfo.avatar)}`;
 
     if (authorInfo.twitter) {
-        content += `\ntwitter: ${authorInfo.twitter}`;
+        content += `\ntwitter: ${JSON.stringify(authorInfo.twitter)}`;
     }
 
     if (authorInfo.github) {
-        content += `\ngithub: ${authorInfo.github}`;
+        content += `\ngithub: ${JSON.stringify(authorInfo.github)}`;
     }
 
     if (authorInfo.linkedin) {
-        content += `\nlinkedin: ${authorInfo.linkedin}`;
+        content += `\nlinkedin: ${JSON.stringify(authorInfo.linkedin)}`;
     }
 
     content += '\n---\n';
 
     return content;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let content = `---
layout: author
slug: ${authorInfo.slug}
name: ${authorInfo.name}
role: ${authorInfo.role}
bio: ${authorInfo.bio}
avatar: ${authorInfo.avatar}`;
if (authorInfo.twitter) {
content += `\ntwitter: ${authorInfo.twitter}`;
}
if (authorInfo.github) {
content += `\ngithub: ${authorInfo.github}`;
}
if (authorInfo.linkedin) {
content += `\nlinkedin: ${authorInfo.linkedin}`;
}
content += '\n---\n';
return content;
}
function generateAuthorMarkdoc(authorInfo) {
let content = `---
layout: author
slug: ${JSON.stringify(authorInfo.slug)}
name: ${JSON.stringify(authorInfo.name)}
role: ${JSON.stringify(authorInfo.role)}
bio: ${JSON.stringify(authorInfo.bio)}
avatar: ${JSON.stringify(authorInfo.avatar)}`;
if (authorInfo.twitter) {
content += `\ntwitter: ${JSON.stringify(authorInfo.twitter)}`;
}
if (authorInfo.github) {
content += `\ngithub: ${JSON.stringify(authorInfo.github)}`;
}
if (authorInfo.linkedin) {
content += `\nlinkedin: ${JSON.stringify(authorInfo.linkedin)}`;
}
content += '\n---\n';
return content;
}
🤖 Prompt for AI Agents
In scripts/blog/create-author.js around lines 161 to 184, the YAML frontmatter
is built by directly interpolating user-provided fields (name, role, bio,
avatar, twitter, github, linkedin), which can break parsing if values contain
":" or special characters; replace direct interpolation with a safe-quoting
strategy by wrapping each inserted user value with JSON.stringify(...) (or an
equivalent YAML-escaping helper) so values are emitted as valid quoted strings
(e.g., name: "..." role: "..." bio: "..." etc.), apply the same to optional
fields when present, and ensure the final content still ends with '\n---\n'.

Comment on lines +187 to +218
function generateMarkdocContent(blogInfo) {
const frontmatter = `---
layout: post
title: ${blogInfo.title}
description: ${blogInfo.description}
date: ${blogInfo.date}
cover: ${blogInfo.cover}
timeToRead: ${blogInfo.timeToRead}
author: ${blogInfo.author}
category: ${blogInfo.category}
featured: ${blogInfo.featured}
---

# ${blogInfo.title}

Start writing your blog post here...

## Introduction

Your introduction paragraph goes here.

## Main Content

Add your main content sections here.

## Conclusion

Wrap up your blog post with a conclusion.
`;

return frontmatter;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Quote/escape string fields in post frontmatter.

Avoid YAML parse issues from : and special chars; keep numbers/bools unquoted.

 function generateMarkdocContent(blogInfo) {
     const frontmatter = `---
 layout: post
-title: ${blogInfo.title}
-description: ${blogInfo.description}
-date: ${blogInfo.date}
-cover: ${blogInfo.cover}
-timeToRead: ${blogInfo.timeToRead}
-author: ${blogInfo.author}
-category: ${blogInfo.category}
-featured: ${blogInfo.featured}
+title: ${JSON.stringify(blogInfo.title)}
+description: ${JSON.stringify(blogInfo.description)}
+date: ${JSON.stringify(blogInfo.date)}
+cover: ${JSON.stringify(blogInfo.cover)}
+timeToRead: ${blogInfo.timeToRead}
+author: ${JSON.stringify(blogInfo.author)}
+category: ${JSON.stringify(blogInfo.category)}
+featured: ${blogInfo.featured}
 ---
 
 # ${blogInfo.title}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function generateMarkdocContent(blogInfo) {
const frontmatter = `---
layout: post
title: ${blogInfo.title}
description: ${blogInfo.description}
date: ${blogInfo.date}
cover: ${blogInfo.cover}
timeToRead: ${blogInfo.timeToRead}
author: ${blogInfo.author}
category: ${blogInfo.category}
featured: ${blogInfo.featured}
---
# ${blogInfo.title}
Start writing your blog post here...
## Introduction
Your introduction paragraph goes here.
## Main Content
Add your main content sections here.
## Conclusion
Wrap up your blog post with a conclusion.
`;
return frontmatter;
}
function generateMarkdocContent(blogInfo) {
const frontmatter = `---
layout: post
title: ${JSON.stringify(blogInfo.title)}
description: ${JSON.stringify(blogInfo.description)}
date: ${JSON.stringify(blogInfo.date)}
cover: ${JSON.stringify(blogInfo.cover)}
timeToRead: ${blogInfo.timeToRead}
author: ${JSON.stringify(blogInfo.author)}
category: ${JSON.stringify(blogInfo.category)}
featured: ${blogInfo.featured}
---
# ${blogInfo.title}
Start writing your blog post here...
## Introduction
Your introduction paragraph goes here.
## Main Content
Add your main content sections here.
## Conclusion
Wrap up your blog post with a conclusion.
`;
return frontmatter;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant