-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathcompose.ts
More file actions
218 lines (183 loc) · 6.8 KB
/
Copy pathcompose.ts
File metadata and controls
218 lines (183 loc) · 6.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/*
* Script based on https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/scripts/compose.js
*/
import dayjs from 'dayjs';
import dedent from 'dedent';
import fs from 'fs';
import inquirer from 'inquirer';
import { fileURLToPath } from 'url';
import { logger } from './helpers/logger';
/**
* Type definition for the answers from the compose prompt.
*/
type ComposePromptType = {
title: string;
excerpt: string;
tags: string;
type: string;
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.
*
* @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.
*/
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')}
type: ${answers.type}
canonical: ${answers.canonical ? answers.canonical : ''}
tags: [${answers.tags ? tags : ''}]
cover: /img/posts/may-2021-at-asyncapi/cover.webp
authors:
- name: Lukasz Gornicki
photo: /img/avatars/lpgornicki.webp
link: https://twitter.com/derberq
byline: AsyncAPI Maintainer and Community Guardian
excerpt: ${answers.excerpt ? answers.excerpt : ' '}
---
Write your blog post content here, just remember to mention "AsyncAPI" :smile:. If you need a refresher on Markdown,
you can take a look at [this guide](https://tailwind-nextjs-starter-blog.vercel.app/blog/github-markdown-guide).
## Test sub-section 1
**Authors**
Before submitting your blog post, don't forget to change the \`authors\` array field to include yourself :smile:
## Test sub-section 2
**Images**
Please make sure to give credit to source of the image, for example:
> Cover image by <a href="https://pixabay.com/users/silviarita-3142410/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2634391">silviarita</a> from <a href="https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2634391">Pixabay</a>
Add a cover image, you can change it on the metadata field above called \`cover\`. In case you need some inspiration we recommend https://unsplash.com/.
All images should be stored in the \`public/img/posts/\` folder, below is an example of using an image in your post with a caption (used as \`alt\` attribute):
<Figure
src="/img/posts/2020-summary/linkedin-folowers.webp"
caption="Figure 3: Followers growth in 2020"
/>
\`<Figure src="/img/posts/2020-summary/linkedin-folowers.webp" caption="Figure 3: Followers growth in 2020" />\`
Also, make sure to have the following:
* **Compress the image as much as possible**, we recommend https://squoosh.app/
* The output format needs to be \`.webp\`
* Include a clear \`alt\` description for people that cannot see images
**Twitter**
To embed a tweet on your post you can use a \`TwitterTweetEmbed\` React component, like so:
<TwitterTweetEmbed
tweetId='1384127726861258756'
options={{
cards: 'hidden',
width: 500,
align: 'center'
}}
/>
**YouTube**
To embed a YouTube video use the \`YouTube\` React component, like so:
<YouTube id="yILtksZriqA" />
**Podcast**
To embed a Podcast audio use, like so:
<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;
}
/**
* 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) {
logger.error(err);
reject(err);
} else {
logger.info(`Blog post generated successfully at ${filePath}`);
resolve(filePath);
}
});
});
}
/**
* Runs the interactive CLI prompt and writes the new blog post file.
*/
/* istanbul ignore next */
async function main() {
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);
})
.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();
}