Conversation
- Add scripts/new-post.ts: generates a new blog post file with frontmatter pre-filled (title, date, optional draft flag) - Add scripts/new-post.test.ts: 17 unit tests for slugify, formatDate, buildFilename, and buildFrontmatter - Install tsx for running TypeScript scripts directly - Add npm run new-post command to package.json - Update README with usage documentation
There was a problem hiding this comment.
Pull request overview
Adds a new TypeScript CLI script to scaffold blog posts in src/content/blog/ with pre-filled frontmatter, along with unit tests and npm wiring/documentation.
Changes:
- Add
scripts/new-post.tsto generate dated, slugified filenames and frontmatter (optionallydraft: true) - Add
scripts/new-post.test.ts(Vitest) covering the exported helper functions - Add
tsxand an npmnew-postscript; document usage in the README
Show a summary per file
| File | Description |
|---|---|
scripts/new-post.ts |
New CLI scaffolding script (slug/date/frontmatter + file creation) |
scripts/new-post.test.ts |
New Vitest unit tests for exported helpers |
package.json |
Adds tsx and npm run new-post entry |
README.md |
Documents the new scaffolding command |
package-lock.json |
Lockfile updates for tsx and transitive deps |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
scripts/new-post.ts:59
buildFrontmatter(title, new Date(), draft)should use the same capturedDateinstance as the filename to ensure thedate:field matches the file prefix in edge cases (e.g., around midnight).
const content = buildFrontmatter(title, new Date(), draft);
fs.writeFileSync(filepath, content, 'utf-8');
- Files reviewed: 4/5 changed files
- Comments generated: 5
|
|
||
| const BLOG_DIR = path.join(import.meta.dirname, '..', 'src', 'content', 'blog'); |
There was a problem hiding this comment.
import.meta.dirname is not a standard Node ESM feature and appears unused elsewhere in the repo; running this script in Node/tsx will likely throw at startup. Derive the directory from import.meta.url instead (e.g., via fileURLToPath + path.dirname).
| const BLOG_DIR = path.join(import.meta.dirname, '..', 'src', 'content', 'blog'); | |
| import { fileURLToPath } from 'node:url'; | |
| const currentFilePath = fileURLToPath(import.meta.url); | |
| const currentDir = path.dirname(currentFilePath); | |
| const BLOG_DIR = path.join(currentDir, '..', 'src', 'content', 'blog'); |
| if (process.argv[1] && import.meta.url.endsWith(process.argv[1])) { | ||
| main(); | ||
| } |
There was a problem hiding this comment.
The executable guard import.meta.url.endsWith(process.argv[1]) is brittle (notably on Windows where process.argv[1] is typically a C:\... path while import.meta.url is a file:///C:/... URL). Compare normalized file paths instead (e.g., fileURLToPath(import.meta.url) vs path.resolve(process.argv[1])).
| export function buildFilename(date: Date, title: string): string { | ||
| return `${formatDate(date)}-${slugify(title)}.md`; | ||
| } |
There was a problem hiding this comment.
If slugify(title) returns an empty string (e.g., a title containing only characters outside [a-z0-9], such as emoji or non-Latin text), buildFilename will produce a name like YYYY-MM-DD-.md. Consider validating that the slug is non-empty and exiting with a clear error (or using a different slugging approach that supports Unicode).
| const { title, draft } = parseArgs(process.argv); | ||
| const filename = buildFilename(new Date(), title); | ||
| const filepath = path.join(BLOG_DIR, filename); | ||
|
|
There was a problem hiding this comment.
new Date() is used to compute the filename; for consistency, capture a single now value once in main() and reuse it for both filename and frontmatter generation (avoids mismatches if the script runs across midnight).
This issue also appears on line 58 of the same file.
| if (fs.existsSync(filepath)) { | ||
| console.error(`Error: File already exists: ${filepath}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const content = buildFrontmatter(title, new Date(), draft); | ||
| fs.writeFileSync(filepath, content, 'utf-8'); |
There was a problem hiding this comment.
The existsSync pre-check is not atomic with the subsequent write, so the file could still be overwritten if it appears between the check and the write. Prefer an atomic create (e.g., open/write with 'wx') and handle the EEXIST error.
| if (fs.existsSync(filepath)) { | |
| console.error(`Error: File already exists: ${filepath}`); | |
| process.exit(1); | |
| } | |
| const content = buildFrontmatter(title, new Date(), draft); | |
| fs.writeFileSync(filepath, content, 'utf-8'); | |
| const content = buildFrontmatter(title, new Date(), draft); | |
| try { | |
| fs.writeFileSync(filepath, content, { encoding: 'utf-8', flag: 'wx' }); | |
| } catch (error: unknown) { | |
| if ( | |
| typeof error === 'object' && | |
| error !== null && | |
| 'code' in error && | |
| error.code === 'EEXIST' | |
| ) { | |
| console.error(`Error: File already exists: ${filepath}`); | |
| process.exit(1); | |
| } | |
| throw error; | |
| } |
Summary
Usage
Creates a file like
src/content/blog/2026-04-07-my-post-title.mdwith:The script errors if no title is given or if the file already exists.
Changes
scripts/new-post.tsscripts/new-post.test.tspackage.jsontsxdev dependency; addnew-postscriptREADME.mdnpm run new-postcommand