no7.space
🧑‍💻

記事用の Markdown ファイルをコマンドで生成する

前回に引き続き、ファイルベースで記事を管理しているのですが、そうなると当然記事を保存するファイルを生成するという手順が必要です。
まぁそれだけなら、適当に右クリックして新規作成でもいいし、ターミナルから touch してもいいんですが、記事として機能させるにはそれなりに条件があります。

1つはフロントマターです。
ここに投稿のメタ情報、つまり

  • 日付
  • タグ
  • 概要テキスト

といったものをいれてあげないといけません。
まぁ、これも最悪スニペットとか登録しておけばいいんですけど、日時周りは地味にめんどうですよね。

2つ目は、ファイル名です。
これは、ファイル名をそのままURLにも使ってる関係でファイル名=スラッグとなりますが、スラッグというのはユニークじゃないといけないので

  1. 記事タイトル(日本語タイトルはそのままだとうまくいくのかな)
  2. 記事タイトルをもとに英語表記にしたもの
  3. 現在の日時ベースに(202507260120000)
  4. ランダム文字列

あたりになるとおもいます。
1と2はそもそもはじめから記事タイトルが決まってるならいいけれども、そうでない場合や、原稿が書き上がってからあらためて考えたいって場合は書き換えないといけないので面倒です
日付ベースやランダム文字列は、手で毎回いれるのはあまり現実的じゃないかもしれません。特に後者。

ということで、コマンドを使って雛形を自動生成できるようにしてみます。

仕様を考える

では、実際にどんな機能があればよさそうなのか、考えていきます。

  • node.js で書く
  • npm script から呼び出せる
  • ファイル名は日時ベースかつユニークなもの

基本的にプレビューするときは Next の dev サーバーつかって見てるので、プロジェクトをエディタで開いてるか、ターミナルでその場所にいるはずです。
なので、そこから呼び出せるように、npm script で呼べる形がよさそうなのと、普段から JS / TS を書いててかつファイル操作とかもしたいので、Node.js にします。
npm scripts は node ./scripts/myscripts.js みたいにして node を動かせるし、Next 使って開発してるなら node は確実に入ってるはずなのでそういう意味でも都合が良いです。

将来的には GitHub Packages で別パッケージに切り分けて管理したいですね。

ファイル名は、記事タイトルかそのスラッグが一番管理しやすいのですが、ファイル名はファイル生成時には確定してるほうが後々都合が良いので一旦パス。
ランダム文字列は一応 UUID とか Firebase の AutoID のような感じにすれば重複はしないですが、ソートに難アリです。

実際サイト上でのソートはフロントマターに書いた日付をベースにするのですが、ファイルマネージャ上では基本的にはファイル名でのソートがメインになりますので、ここで日付順に並んでくれると嬉しいですよね。
なので、今回は ULID を使うことにしました。Node.js 用のライブラリもありますしね。

Note

ULID についてここでは詳しくは触れませんが、UUID 同様ランダムな文字列なのですが、戦闘の 48bit 分はタイムスタンプとなっているため、ファイル名でソートするだけで時系列順に並べられるという特徴があります。
ちなみに、UUID も v7 ならタイムスタンプを含んでるので同様にソート可能みたいです。

他にもまぁいろいろとやりたいこととかはあるんですが、とりあえずこれだけあればはじめられそうなので、まずはさくっと。
じゃないとまともに記事も書き始められないので……

とりあえずつくる

const fs = require('fs')
const path = require('path')
const { ulid } = require('ulid')

const title = process.argv[2] || 'Untitled'

const utcDate = new Date()
// ISO8601 で +09:00 な日時を生成 ---
const pad = (n) => String(n).padStart(2, '0')

const year = utcDate.getFullYear()
const month = pad(utcDate.getMonth() + 1)
const day = pad(utcDate.getDate())
const hours = pad(utcDate.getHours())
const minutes = pad(utcDate.getMinutes())
const seconds = pad(utcDate.getSeconds())

// JST (+0900) にしたい場合
const offsetMinutes = -utcDate.getTimezoneOffset()
const sign = offsetMinutes >= 0 ? '+' : '-'
const absOffset = Math.abs(offsetMinutes)
const offsetHours = pad(Math.floor(absOffset / 60))
const offsetMins = pad(absOffset % 60)

const offset = `${sign}${offsetHours}${offsetMins}`
const date = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offset}`
// ------

const id = ulid()
const fileName = `${id}.md`
const filePath = path.join(__dirname, '../content/blog', fileName)

const template = `---
title: ${title}
date: ${date}
ulid: ${id}
description:
tags:
  -
---

`

fs.writeFileSync(filePath, template)
console.log(`Created: content/blog/${fileName}`)

Date には ISO8601 にしてくれる toISOString() というのがあるのですが、これが UTC 表記しかできないっぽくて、
日本時間かつ末尾に +0900 をつけた表記というのがそのままだとできないみたいで、ChatGPT にきいたところなんか思ったよりも複雑になってしまいました。

仮コマンドといいつつも、もうちょっとちゃんとライブラリとか使いつつやったほうがよかったかなぁ

使い方

スクリプトをプロジェクト内に scripts/create-blog-post.js として保存して、 package.json の scripts に以下のように追記します

{
  "scripts": {
    "new:post": "node scripts/create-blog-post.js"
  }
}

で、

npm run new:post 記事タイトル

みたいにすれば、

---
title: 記事タイトル
date: 2025-07-26T08:27:40+0900
ulid: 01K11YKTNV27JJSE8HB9V20AHV
description:
tags:
  -
---

みたいなファイルができあがりますので、これでかなり執筆が楽になりそうです。
もちろんタイトルはここでつけなくても良くて、その場合は Untitled が自動で設定されます。

タイトルについては、なんでもいいから入ってないと、一覧表示とかのときにタイトルからしかリンクを貼ってない場合とかに、開発画面から飛べないとまずいのでこういうふうにしてみました。

今後の拡張予定

  • commander.js とか使ってきれいに整えたい
  • 今は新規作成用だけど、将来的には管理用コマンド群になるようにしていきたい
    • 記事削除とかもできるようにしたい
      • 記事にアップロードしたファイルとかもセットで消せるように
    • タグのつけ外し
    • 更新日時のアップデート
  • GitHub Package とかつかって、別プロジェクトに切り出したうえで npm からインストールして管理できるようにしたい
  • 記事更新時にフロントマターの更新日を更新するような仕組みもなんか欲しいよなぁ

ということで、サイト自体もまだまだ中途半端な状態なのでいろいろ調整が必要なので、まずはそっち優先ですが、追々調整できたらまた記事にしようと思います。