Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
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
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const config = {
coverageReporters: ['text', 'lcov', 'json-summary'],
coverageDirectory: 'coverage',
collectCoverageFrom: ['scripts/**/*.ts'],
coveragePathIgnorePatterns: ['scripts/compose.ts', 'scripts/tools/categorylist.ts', 'scripts/tools/tags-color.ts'],
coveragePathIgnorePatterns: ['scripts/tools/categorylist.ts', 'scripts/tools/tags-color.ts'],
testMatch: ['**/tests/**/*.test.*', '!**/netlify/**/*.test.*'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
};
Expand Down
150 changes: 99 additions & 51 deletions scripts/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
* Script based on https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/scripts/compose.js
*/

import { fileURLToPath } from 'node:url';

import dayjs from 'dayjs';
import dedent from 'dedent';
import fs from 'fs';
import inquirer from 'inquirer';
import dayjs from 'dayjs';

import { logger } from './helpers/logger';

Expand All @@ -20,25 +22,43 @@
canonical: string;
};

/**
* Converts a blog post title into a URL-safe kebab-case slug.
*
* Lowercases the title, strips all non-alphanumeric characters (except spaces),
* replaces spaces with hyphens, and collapses consecutive hyphens into one.
*
* @param title - The raw post title string.
* @returns The generated slug string.
*/
export function getSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-zA-Z0-9 ]/g, '')
.replace(/ /g, '-')
.replace(/-+/g, '-');
}

/**
* Generates a complete Markdown front matter block for a blog post.
*
* Constructs a YAML front matter section using the blog post details provided by the user,
* including title, current date, type, canonical URL, and comma-separated tags. The front matter
* also embeds fixed cover image and author metadata along with a Markdown template containing guidelines
* for composing the blog content.
* also embeds fixed cover image and author metadata along with a Markdown template containing
* guidelines for composing the blog content.
*
* @param answers - User inputs for the blog post, including title, excerpt, comma-separated tags, type, and canonical URL.
* @param answers - User inputs for the blog post, including title, excerpt, tags, type, and canonical URL.
* @returns The generated Markdown front matter and blog post content template.
*/
function genFrontMatter(answers: ComposePromptType): string {
export function genFrontMatter(answers: ComposePromptType): string {
const tagArray = answers.tags.split(',');

tagArray.forEach((tag: string, index: number) => {
tagArray[index] = tag.trim();
});
const tags = `'${tagArray.join("','")}'`;

/* eslint-disable max-len */
let frontMatter = dedent`---
title: ${answers.title ? answers.title : 'Untitled'}
date: ${dayjs().format('YYYY-MM-DDTh:mm:ssZ')}
Expand Down Expand Up @@ -112,64 +132,92 @@
<center><iframe src="https://anchor.fm/asyncapi/embed/episodes/April-2021-at-AsyncAPI-Initiative-e111lo9" height="102px" width="400px" frameborder="0" scrolling="no"></iframe></center>

`;
/* eslint-enable max-len */

frontMatter += '\n---';

return frontMatter;
}

inquirer
.prompt([
{
name: 'title',
message: 'Enter post title:',
type: 'input'
},
{
name: 'excerpt',
message: 'Enter post excerpt:',
type: 'input'
},
{
name: 'tags',
message: 'Any Tags? Separate them with , or leave empty if no tags.',
type: 'input'
},
{
name: 'type',
message: 'Enter the post type:',
type: 'list',
choices: ['Communication', 'Community', 'Engineering', 'Marketing', 'Strategy', 'Video']
},
{
name: 'canonical',
message: 'Enter the canonical URL if any:',
type: 'input'
}
])
.then((answers: ComposePromptType) => {
// Remove special characters and replace space with -
const fileName = answers.title
.toLowerCase()
.replace(/[^a-zA-Z0-9 ]/g, '')
.replace(/ /g, '-')
.replace(/-+/g, '-');
const frontMatter = genFrontMatter(answers);
const filePath = `pages/blog/${fileName || 'untitled'}.md`;
/**
* Writes the blog post front matter to a file at the computed path.
*
* Resolves the file path from the slug generated from `answers.title` and writes
* the generated front matter content. Logs success on completion and throws on failure.
*
* @param answers - User inputs used to compute the slug and generate front matter content.
* @returns A Promise that resolves to the file path that was written.
*/
export function writePost(answers: ComposePromptType): Promise<string> {
const slug = getSlug(answers.title);
const filePath = `pages/blog/${slug || 'untitled'}.md`;
const frontMatter = genFrontMatter(answers);

return new Promise((resolve, reject) => {
fs.writeFile(filePath, frontMatter, { flag: 'wx' }, (err) => {
if (err) {
throw err;
logger.error(err);
reject(err);
} else {
logger.info(`Blog post generated successfully at ${filePath}`);
resolve(filePath);
}
});
})
.catch((error) => {
});
}

/**
* Runs the interactive CLI prompt and writes the new blog post file.
*
* @returns A Promise that resolves when the blog post is written.
* istanbul ignore next
*/
async function main(): Promise<void> {
await inquirer
.prompt([
{
name: 'title',
message: 'Enter post title:',
type: 'input'
},
{
name: 'excerpt',
message: 'Enter post excerpt:',
type: 'input'
},
{
name: 'tags',
message: 'Any Tags? Separate them with , or leave empty if no tags.',
type: 'input'
},
{
name: 'type',
message: 'Enter the post type:',
type: 'list',
choices: ['Communication', 'Community', 'Engineering', 'Marketing', 'Strategy', 'Video']
},
{
name: 'canonical',
message: 'Enter the canonical URL if any:',
type: 'input'
}
])
.then((answers: ComposePromptType) => {
return writePost(answers);
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.catch((error) => {
logger.error(error);
if (error.isTtyError) {
logger.error("Prompt couldn't be rendered in the current environment");
} else {
logger.error('Something went wrong, sorry!');
}
});
}

/* istanbul ignore next */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch((error) => {

Check warning on line 220 in scripts/compose.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer top-level await over using a promise chain.

See more on https://sonarcloud.io/project/issues?id=asyncapi_website&issues=AZ9byYZ8k0zcjiSGpHvz&open=AZ9byYZ8k0zcjiSGpHvz&pullRequest=5626
logger.error(error);
if (error.isTtyError) {
logger.error("Prompt couldn't be rendered in the current environment");
} else {
logger.error('Something went wrong, sorry!');
}
});
}
Loading
Loading