I'm even using metascraper to scrape unsplash for an image, it's alt text, and author (unsplash does not make any of these things easy, yes I realize they have an API, but their terms say you have to hotlink to them if you use it and I don't want to).

I'm using cloudinary auto-uploads so I don't have to bother uploading the photo manually either :)

Install new-post

// Menu: New Post
// Description: Create a new blog post
// Author: Kent C. Dodds
// Shortcut: command option control p
// Twitter: @kentcdodds
const dateFns = await npm('date-fns')
const prettier = await npm('prettier')
const YAML = await npm('yaml')
const slugify = await npm('@sindresorhus/slugify')
const {format: formatDate} = await npm('date-fns')
const makeMetascraper = await npm('metascraper')
const {$filter, toRule} = await npm('@metascraper/helpers')
const unsplashTitleToAlt = toRule(str => str.replace(/ photo – .*$/, ''))
const unsplashOGTitleToAuthor = toRule(str =>
str.replace(/Photo by (.*?) on Unsplash/, '$1'),
const unsplashImageToPhotoId = toRule(str =>
new URL(str).pathname.replace('/', ''),
const metascraper = makeMetascraper([
unsplashPhotoId: [
unsplashImageToPhotoId($ =>
author: [
unsplashOGTitleToAuthor($ =>
{alt: [unsplashTitleToAlt($ => $('title').text())]},
async function getMetadata(url) {
const html = await fetch(url).then(res => res.text())
return metascraper({html, url})
const blogDir = await env(
`What's the path to the blog content directory on this machine?`,
const title = await arg({
placeholder: `What's the title of this post?`,
hint: 'Title',
ignoreBlur: true,
const description = await arg({
placeholder: `What's the description of this post?`,
hint: 'Description',
input: 'TODO: add a description',
ignoreBlur: true,
const categories = (
await arg({
placeholder: `What are the categories of this post?`,
hint: 'Categories (comma separated)',
ignoreBlur: true,
.map(c => c.trim())
const keywords = (
await arg({
placeholder: `What are the keywords of this post?`,
hint: 'Keywords (comma separated)',
ignoreBlur: true,
.map(c => c.trim())
const unsplashPhotoInput = await arg({
placeholder: `What's the unsplash photo?`,
hint: 'Unsplash Photo',
ignoreBlur: true,
const unsplashPhotoUrl = unsplashPhotoInput.startsWith('http')
? unsplashPhotoInput
: `https://unsplash.com/photos/${unsplashPhotoInput}`
const metadata = await getMetadata(unsplashPhotoUrl)
const frontmatter = YAML.stringify({
date: dateFns.format(new Date(), 'yyyy-MM-dd'),
meta: {keywords},
bannerCloudinaryId: `unsplash/${metadata.unsplashPhotoId}`,
bannerAlt: metadata.alt,
bannerCredit: `Photo by [${metadata.author}](${unsplashPhotoUrl})`,
const md = `---
Be excellent to each other.
// prettify the markdown
const prettyMd = await prettier.format(md, {
parser: 'markdown',
arrowParens: 'avoid',
bracketSpacing: false,
embeddedLanguageFormatting: 'auto',
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxBracketSameLine: false,
jsxSingleQuote: false,
printWidth: 80,
proseWrap: 'always',
quoteProps: 'as-needed',
requirePragma: false,
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
useTabs: false,
vueIndentScriptAndStyle: false,
const filename = slugify(title, {decamelize: false})
const newFile = path.join(blogDir, `${filename}.mdx`)
await writeFile(newFile, prettyMd)
await edit(newFile)