Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"test:integration": "playwright test",
"test:unit": "vitest",
"optimize": "node ./scripts/optimize-assets.js",
"optimize:all": "node ./scripts/optimize-all.js"
"optimize:all": "node ./scripts/optimize-all.js",
"create-blog": "node ./scripts/blog/create-blog.js",
"create-author": "node ./scripts/blog/create-author.js"
},
"packageManager": "[email protected]",
"dependencies": {
Expand Down
296 changes: 296 additions & 0 deletions scripts/blog/create-author.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import {
COLORS,
printHeader,
question,
questionWithDefault,
selectFromList,
imagePathInput,
copyImage,
closeReadline
} from './utils.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ROOT_DIR = path.join(__dirname, '..', '..');

function printAuthorHeader() {
console.clear();
console.log(`${COLORS.pink}${COLORS.bright}`);
console.log(' ___ _ __ ');
console.log(' / _ | ___ ___ _ ___ ____(_) /____ ');
console.log(' / __ |/ _ \\/ _ \\| |/|/ / __/ / / __/ -_)');
console.log(' /_/ |_/ .__/ .__/|__,__/_/ /_/_/\\__/\\__/ ');
console.log(' /_/ /_/ ');
console.log('');
console.log(' AUTHOR CREATOR');
console.log(`${COLORS.reset}\n`);
console.log(`${COLORS.dim}Add a new author to the Appwrite blog${COLORS.reset}\n`);
}

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

function validateSlug(slug) {
const authorPath = path.join(ROOT_DIR, 'src', 'routes', 'blog', 'author', slug);

if (fs.existsSync(authorPath)) {
return {
valid: false,
message: `An author with slug "${slug}" already exists!`
};
}

if (!/^[a-z0-9-]+$/.test(slug)) {
return {
valid: false,
message: 'Slug should only contain lowercase letters, numbers, and hyphens'
};
}

return { valid: true };
}

function validateUrl(url) {
if (!url) return true; // Optional field
try {
new URL(url);
return true;
} catch {
return false;
}
}

async function collectAuthorInfo() {
const authorInfo = {};

console.log(`\n${COLORS.bright}👤 Author Information${COLORS.reset}\n`);

// Name
let name = '';
while (!name) {
name = await question('Full Name: ');
if (!name) {
console.log(`${COLORS.red}Name is required!${COLORS.reset}`);
}
}
authorInfo.name = name;

// Auto-generate slug from name
const suggestedSlug = slugify(authorInfo.name);
let slug = await questionWithDefault('Slug', suggestedSlug);

let validation = validateSlug(slug);
while (!validation.valid) {
console.log(`${COLORS.red}${validation.message}${COLORS.reset}`);
slug = await question('Please enter a different slug: ');
validation = validateSlug(slug);
}
authorInfo.slug = slug;

// Role
console.log(`\n${COLORS.bright}💼 Professional Information${COLORS.reset}\n`);

let role = '';
while (!role) {
role = await question('Role (e.g., Developer Advocate, Software Engineer): ');
if (!role) {
console.log(`${COLORS.red}Role is required!${COLORS.reset}`);
}
}
authorInfo.role = role;

// Bio
console.log(`\n${COLORS.bright}📝 Bio${COLORS.reset}`);
console.log(`${COLORS.dim}Write a short bio (1-2 sentences)${COLORS.reset}\n`);
const bio = await question('Bio: ');

authorInfo.bio = bio || `${authorInfo.role} at Appwrite`;

// Avatar
console.log(`\n${COLORS.bright}🖼️ Avatar${COLORS.reset}`);
const avatarFileName = `${authorInfo.slug}.png`;
const avatarPath = `/images/avatars/${avatarFileName}`;
const fullAvatarPath = path.join(ROOT_DIR, 'static', 'images', 'avatars', avatarFileName);

const imageResult = await imagePathInput('Add Avatar Image', fullAvatarPath);
authorInfo.avatar = avatarPath;
authorInfo.avatarSourcePath = imageResult.sourcePath;
authorInfo.avatarTargetPath = imageResult.targetPath;

// Social Links
console.log(`\n${COLORS.bright}🔗 Social Links${COLORS.reset}`);
console.log(`${COLORS.dim}Leave blank to skip any social link${COLORS.reset}\n`);

// Twitter/X
let twitter = await question('Twitter/X URL (e.g., https://x.com/username): ');
while (twitter && twitter.trim() && !validateUrl(twitter)) {
console.log(`${COLORS.red}Invalid URL format${COLORS.reset}`);
twitter = await question('Twitter/X URL (leave blank to skip): ');
}
authorInfo.twitter = twitter ? twitter.trim() : '';

// GitHub
let github = await question('GitHub URL (e.g., https://github.com/username): ');
while (github && github.trim() && !validateUrl(github)) {
console.log(`${COLORS.red}Invalid URL format${COLORS.reset}`);
github = await question('GitHub URL (leave blank to skip): ');
}
authorInfo.github = github ? github.trim() : '';

// LinkedIn
let linkedin = await question('LinkedIn URL (e.g., https://www.linkedin.com/in/username): ');
while (linkedin && linkedin.trim() && !validateUrl(linkedin)) {
console.log(`${COLORS.red}Invalid URL format${COLORS.reset}`);
linkedin = await question('LinkedIn URL (leave blank to skip): ');
}
authorInfo.linkedin = linkedin ? linkedin.trim() : '';

return authorInfo;
}

function generateAuthorMarkdoc(authorInfo) {
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;
}
Comment on lines +147 to +170
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'.


async function createAuthorFiles(authorInfo) {
const authorPath = path.join(ROOT_DIR, 'src', 'routes', 'blog', 'author', authorInfo.slug);

console.log(`\n${COLORS.bright}📁 Creating author files...${COLORS.reset}\n`);

try {
// Create author directory
fs.mkdirSync(authorPath, { recursive: true });
console.log(
`${COLORS.pink}✓${COLORS.reset} Created directory: ${COLORS.dim}${authorPath}${COLORS.reset}`
);

// Create the markdoc file
const markdocPath = path.join(authorPath, '+page.markdoc');
const content = generateAuthorMarkdoc(authorInfo);
fs.writeFileSync(markdocPath, content);
console.log(
`${COLORS.pink}✓${COLORS.reset} Created file: ${COLORS.dim}${markdocPath}${COLORS.reset}`
);

// Copy avatar image if provided
let avatarCopied = false;
if (authorInfo.avatarSourcePath && authorInfo.avatarTargetPath) {
avatarCopied = await copyImage(authorInfo.avatarSourcePath, authorInfo.avatarTargetPath);
}

return { success: true, authorPath, markdocPath, avatarCopied };
} catch (error) {
return { success: false, error: error.message };
}
}

async function main() {
printAuthorHeader();

try {
const authorInfo = await collectAuthorInfo();

// Review information
console.log(`\n${COLORS.bright}📋 Review Author Information${COLORS.reset}\n`);
console.log(`${COLORS.pink}Name:${COLORS.reset} ${authorInfo.name}`);
console.log(`${COLORS.pink}Slug:${COLORS.reset} ${authorInfo.slug}`);
console.log(`${COLORS.pink}Role:${COLORS.reset} ${authorInfo.role}`);
console.log(`${COLORS.pink}Bio:${COLORS.reset} ${authorInfo.bio}`);
console.log(`${COLORS.pink}Avatar:${COLORS.reset} ${authorInfo.avatar}`);

if (authorInfo.twitter) {
console.log(`${COLORS.pink}Twitter:${COLORS.reset} ${authorInfo.twitter}`);
}
if (authorInfo.github) {
console.log(`${COLORS.pink}GitHub:${COLORS.reset} ${authorInfo.github}`);
}
if (authorInfo.linkedin) {
console.log(`${COLORS.pink}LinkedIn:${COLORS.reset} ${authorInfo.linkedin}`);
}

const confirm = await selectFromList('\nCreate this author profile?', [
{ label: 'Yes, create it!', value: true },
{ label: 'No, cancel', value: false }
]);

if (!confirm) {
console.log(`\n${COLORS.yellow}Author creation cancelled.${COLORS.reset}`);
closeReadline();
return;
}

const result = await createAuthorFiles(authorInfo);

if (result.success) {
console.log(
`\n${COLORS.pink}${COLORS.bright}✨ Success! Author profile created successfully!${COLORS.reset}\n`
);
console.log(`${COLORS.bright}Created files:${COLORS.reset}`);
console.log(` • Author profile: ${COLORS.pink}${result.markdocPath}${COLORS.reset}`);

console.log(`\n${COLORS.bright}Next steps:${COLORS.reset}`);
if (!result.avatarCopied) {
const avatarPath = path.join(ROOT_DIR, 'static', 'images', 'avatars', `${authorInfo.slug}.png`);
console.log(
`1. Add avatar image to: ${COLORS.pink}${avatarPath}${COLORS.reset}`
);
console.log(
`2. The author "${authorInfo.name}" is now available for blog posts`
);
} else {
console.log(
`The author "${authorInfo.name}" is now available for blog posts`
);
}
console.log(
`\n${COLORS.dim}Run 'pnpm create-blog' to create a blog post with this author!${COLORS.reset}`
);
} else {
console.log(`\n${COLORS.red}Error creating author files: ${result.error}${COLORS.reset}`);
}

} catch (error) {
console.log(`\n${COLORS.red}Error: ${error.message}${COLORS.reset}`);
}

closeReadline();
}

process.on('SIGINT', () => {
console.log(`\n${COLORS.yellow}Process interrupted. Exiting...${COLORS.reset}`);
closeReadline();
process.exit(0);
});

main();
Loading
Loading