Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
152 changes: 101 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,94 @@
<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 */
// eslint-disable-next-line require-jsdoc
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 221 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
// NOSONAR - top-level await is unsupported in Jest's CJS transform
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