編集記 / CMS改善ログ
この記事は、新びんえい通信の軽量CMSを実際に使いながら磨いた制作記録です。タブ幅、表示切替、記事選択、画像の扱い、保存履歴、そして公開ページの見え方まで、記事を書く流れそのものをテストケースにして改善を重ねました。
今回の狙いは、CMSを「作った」で止めず、記事制作の現場で本当に使えるかどうかを確かめることでした。本文を書き、画像を配置し、プレビューで確認し、公開ページを見て、違和感をコードへ戻す。この一巡のサイクルが回るようになると、編集画面はかなり頼れる道具に育ちます。
実際の画面で見た状態
以下は、今回の検証で保存したスクリーンショットです。記事の出来上がり、CMS上での選択状態、幅調整後の編集画面、トップ一覧への反映を、同じ記事の中に記録しています。
触って分かった違和感
最初に気になったのは、編集画面のプルダウンでした。カテゴリ選択と記事選択の役割が近く見え、今どの記事を触っているのかが一瞬ぼやけてしまいます。そこで、記事名の選択欄を独立させ、カテゴリ欄とは別の意味を持つUIとして整理しました。
もうひとつは横幅の問題です。本文HTMLを集中して直したいときもあれば、プレビューを広く見たいときもあります。固定幅の3ペインは見た目こそ整っていますが、作業の重心に合わせるのが難しいと感じました。そこで、一覧・メタ情報・HTML・プレビューをそれぞれオンオフでき、境界をドラッグして幅を変えられる構造に改修しています。
コードレベルで見た改善点
今回いちばん効果が大きかったのは、画像URLの扱いを公開テンプレートとCMSプレビューの両方で統一したことです。外部画像は従来どおりに使いつつ、今回のようなthumbs/...の同一サイト画像も自然に扱えるようにしました。
function article_asset_url($value, $absolute = false) {
if (preg_match('#^https?://#i', $value)) return $value;
if (strpos($value, '//') === 0) return $absolute ? 'https:' . $value : $value;
if ($value[0] === '/') return $absolute ? article_origin_url() . $value : $value;
if (preg_match('/^[A-Za-z0-9.-]+\.[A-Za-z]{2,}(\/|$)/', $value)) {
return $absolute ? 'https://' . $value : '//' . $value;
}
return $absolute ? article_origin_url() . $base . '/' . ltrim($value, '/') : $value;
}
記事本文に表示する画像は相対パスのまま使い、OGPのように絶対URLが必要な場所でだけ絶対URL化しています。これにより、ローカル検証ではhttp://localhost/xn--c5r/thumbs/...として、本番環境ではサイトURLとして正しく扱えるようになりました。
function absoluteImage(src) {
const value = String(src || "").trim();
if (/^https?:\/\//i.test(value)) return value;
if (value.startsWith("//")) return `https:${value}`;
if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) return value;
if (/^[A-Za-z0-9.-]+\.[A-Za-z]{2,}(\/|$)/.test(value)) return `https://${value}`;
return new URL(value, new URL("../", window.location.href)).href;
}
CMSは/_cms/の下で動くため、単にthumbs/foo.pngと書くと/_cms/thumbs/foo.pngを見に行ってしまいます。そこでnew URL("../", window.location.href)を基準URLとして使い、サイト直下の画像として正しくプレビューできるようにしました。
document.querySelectorAll('.card-img img').forEach((img) => {
const markMissing = () => img.parentElement.classList.add('is-missing-image');
if (img.complete && img.naturalWidth === 0) markMissing();
img.addEventListener('error', markMissing, { once: true });
});
XAMPPの検証環境では、外部画像や外部CSSがネットワーク制限によって読み込めないことがあります。その場合でもカードの高さや余白が壊れないよう、画像が欠落した際にはカード内に「新びんえい通信」のプレースホルダーを表示する処理を入れました。
バージョン管理の仕組み
CMSの保存処理は、単純な上書きではありません。保存前のファイルを_cms_data/versions/に退避してから、新しい内容を反映する仕組みです。記事ファイル、メタCSV、関連キャッシュがすべて履歴に残るため、編集で崩してしまっても戻すための材料が常に確保されています。
$stamp = cms_version_stamp();
$manifest = ['action' => 'article_save', 'stamp' => $stamp];
cms_backup_existing($oldPath, $stamp, $manifest, 'article');
cms_backup_existing(cms_meta_path(), $stamp, $manifest, 'metadata');
cms_move_cache_files([$oldPath . '-cache.html', $path . '-cache.html', 'index-cache.html'], $stamp, $manifest);
$manifest['sha256_after'] = hash_file('sha256', $path);
file_put_contents($versionDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT));
YYYYMMDD-HHMMSS-xxxxxx形式のスタンプが付与されます。blog-articles.csv、記事キャッシュ、トップキャッシュがバックアップの対象です。sha256_afterが記録され、どの内容が反映されたかを追跡できます。before_restoreとして残してから過去のバックアップを戻します。今回の検証結果
AIアシストの位置づけ
AIボタンは、本文を丸投げするためではなく、編集者の迷いを短くする補助輪として位置づけるのがよいと考えています。概要案、見出し案、SEO確認、HTML化、文体の整え直し――人間が最終判断を下し、AIが候補を出す形にすれば、軽量CMSであっても十分に実用的です。
ただし、APIキーやモデル設定はCMSの設定画面に閉じ込め、記事本文や公開ページには一切表示されないようにしています。便利さを足すほどに、秘密情報を表に漏らさない設計の重要性が増してきます。
次に育てたいところ
- 画像アップロードをCMS内で完結させ、ファイル名、alt、OGP設定まで一気に扱えるようにしたいと考えています。
- AI提案は上書きではなく差分表示にして、採用前に比較できる形を目指します。
- 保存後に作成されたバージョンスタンプを、画面上でもっと目立たせたいところです。
- 記事一覧の検索を、カテゴリ、優先度、更新日でも絞れるようにする予定です。
今回の結論。軽量CMSであっても、スクリーンショットで出来上がりを確認し、コードレベルで改善理由を説明でき、保存履歴でいつでも戻れる状態まで持っていけば、記事制作の安心感はかなり高まります。この記事自体が、そのままCMSの実地テストになりました。