🖼️ Unique Image Naming - Path-Based Names with Conflict Resolution

🎯 Image generation now derives filenames from the full file path instead of just the post title, preventing accidental overwrites across blog series.
🔍 The Problem
🐛 Previously, image names were derived solely from the blog post title via titleToKebabCase. 📝 A post titled “Weekly Recap” in chickie-loo and another “Weekly Recap” in auto-blog-zero would both produce weekly-recap.jpg. 💥 The second image would silently overwrite the first.
| 📂 Blog Series | 📄 Post Title | 🖼️ Old Image Name | ⚠️ Conflict? |
|---|---|---|---|
| 🐔 chickie-loo | Weekly Recap | weekly-recap.jpg | ✅ Yes |
| 🤖 auto-blog-zero | Weekly Recap | weekly-recap.jpg | ✅ Yes |
🏗️ The Solution: Path-Based Naming
🗂️ notePathToImageBaseName
🔑 The new function derives the image base name from the file’s parent directory and file stem (filename without extension):
export const notePathToImageBaseName = (notePath: string): string => {
const dir = path.basename(path.dirname(notePath));
const stem = path.basename(notePath, path.extname(notePath));
return [dir, stem]
.join("-")
.toLowerCase()
.replace(/[^a-z0-9-]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}; 🎨 This produces unique, descriptive names:
| 📂 File Path | 🖼️ New Image Name |
|---|---|
🐔 chickie-loo/2026-03-22-weekly-recap.md | chickie-loo-2026-03-22-weekly-recap.jpg |
🤖 auto-blog-zero/2026-03-22-weekly-recap.md | auto-blog-zero-2026-03-22-weekly-recap.jpg |
📓 reflections/2026-01-01.md | reflections-2026-01-01.jpg |
🛡️ resolveUniqueImageName
🔒 A dynamic conflict resolution layer ensures no overwrites even in edge cases:
export const resolveUniqueImageName = (
baseName: string,
extension: string,
attachmentsDir: string,
): string => {
const candidate = `${baseName}${extension}`;
if (!fs.existsSync(path.join(attachmentsDir, candidate))) return candidate;
for (let n = 2; ; n++) {
const name = `${baseName}-${n}${extension}`;
if (!fs.existsSync(path.join(attachmentsDir, name))) return name;
}
}; 🔄 If chickie-loo-2026-03-22-weekly-recap.jpg somehow already exists, the function produces chickie-loo-2026-03-22-weekly-recap-2.jpg, then -3, and so on.
🔧 Changes to processNote
🔀 The core change in processNote replaces title-based naming with path-based naming:
- const kebabTitle = titleToKebabCase(title);
- if (!kebabTitle) return { skipped: true };
+ const baseName = notePathToImageBaseName(notePath);
+ if (!baseName) return { skipped: true };
...
- const imageName = `${kebabTitle}${extension}`;
+ const imageName = resolveUniqueImageName(baseName, extension, attachmentsDir); 🧩 The titleToKebabCase function is preserved for backward compatibility but no longer used in the image naming pipeline.
🧪 Test Coverage
📊 14 new tests added across three describe blocks:
| 🧪 Test Suite | 📈 Tests | 🎯 Coverage |
|---|---|---|
🗂️ notePathToImageBaseName | 8 | 🔤 Kebab-casing, parent dir extraction, special chars, deep nesting, cross-directory uniqueness |
🛡️ resolveUniqueImageName | 5 | ✅ No conflict, -2 suffix, -3 suffix, extension independence, missing directory |
🔄 processNote (updated) | 1 | 🐔🤖 Cross-directory naming conflict avoidance |
✅ All 896 tests pass across 204 suites with 0 failures.
📐 Design Principles
🧩 Functional and pure: notePathToImageBaseName is a pure function - no side effects, fully deterministic from its input. 🛡️ resolveUniqueImageName is the only function that touches the filesystem, and it only reads (never writes). 🔬 The recursive nextSuffix helper avoids mutable state entirely.
🔧 Unix philosophy: each function does one thing well - name derivation, conflict resolution, and orchestration are separate concerns.
🚀 No migration needed: existing images keep their old names. 🆕 Only newly generated images use the path-based naming scheme going forward.