ハードウェアからサーバ・アプリまでワンストップで開発

ヘッドレスCMSを作ってみた

弊社ホームページをヘッドレスCMSにしてみました

  Napier

ogochan

前にも書きましたが、弊社のホームページのCMSは自作です。

初代は「RubyでRailsとか使わないでCGIでやってみよう」ということで始めました。Rails使うとメンテが面倒ですからねぇ。これは私の作。

その後、いろいろあってCGIをやめてSinatraを使ったものとなりました(Railsではありません)。これはmayumiの作です。

Railsはコンテンツを執筆するための部分だけに使っているので、表には見えていません。同じDBを使っているというだけです。

今回、Rubyを離れてヘッドレスにしてみました。バックエンドは先日書いた、

Railsで作ったDBをSequelizeで使う

です。フロントエンドはNapierのウェブサーバをちょっと拡張したものです。

背景

実は特に理由はありません。

CMSのエントリをAPIで取れるようになったことと、Napierのウェブサーバのマクロ機能をちゃんと使ってサイトを作ってみたかったというだけで、必然性は特にありません。

今使っている技術スタックの上の方がメンテしやすいとかあったりもしますが、Rubyがレガシーなんてことはないですから、技術的な必然性はありません。

強いて言えば、Napierの事例を増やしたかったことと、サーバの上でのプロセスの種類を減らしたいくらいでしょうか。どうせNapier(client)は動かしているわけなので、その中でウェブサーバを動かすか、別プロセスのSinatraにつなぐかという違いがあって、Sinatraのプロセスが止められるわけです。

実装

Napierのウェブサーバを使って実装されています。

Napierのウェブサーバは要するに単なるウェブサーバなので、そこにHTMLを置いておけばグローバルに公開することは可能です。

ただ、弊社のホームページはどこかの制作会社に頼んで作ってもらったものではなくて、弊社内で適当にコーディングして片手間に追加して... とやっています。ですから、単独のHTMLファイルを並べて置いておくだけだと、デザインや「調子」がバラバラになってしまいがちです。

マクロ

まず、定型的な部分、共通した部分をファイルに分けてしまって、参照時にそれを組み立てて出力するような機能が必要です。

こうしておけば、「ぐちゃぐちゃといろんなことが書かれているヘッダ部分」とかを単独のファイルにまとめておいて流用するというようなことが出来るので、いろんな手間が省けますね。

こういった類のことは結構あるもので、たとえば「パンくず」とか「フッタ」とか、同じようなものを表示する必要があるものは、マクロ化してあると都合が良いです。

<!DOCTYPE html>
<html lang="ja">
    <head>
    {{> /parts/header.ejs {
        title: "事業案内 | WASP株式会社",
        og_url: '/bussiness.html'
    } }}
    {{> /parts/favicon.html}}
    </head>
    <body id="body">
        {{> /parts/header2.html}}
		<main>
            {{> }}
			{{> /parts/bread_crumbs.ejs [
                {url: "/business.html", text: "事業案内"}
            ] }}
            {{> /parts/contact_us.html }}
		</main>
		<footer>
			{{> /parts/footer.html }}
		</footer>
    </body>
</html>

ここで{{> なんとか}}となっているのがそれです。

元々は普段使っているテンプレートエンジンのsprightlyを使っていたのですが、もうちょっと何とかならんかなーと思ってコードを見たら簡単なものだったので、自前の実装にして拡張してしまいました。

機能としては、

  • {{> なんとか}}という機能で他のファイルを読み込む(元々ある)
  • 読み込む時に引数が渡せて、引数としては任意のJavascriptの式が書ける(拡張した)
  • 「他のファイル」は、HTML, Markdown, EJSが使えて、読み込む時に適当な処理をしてくれる(拡張した)
  • {{式}}と書くと式の値が挿入される。これは元のsprightlyと同じ(元々ある)
  • {{> }}と書くと、「元のファイル名」のファイルを読み込む(拡張した)

があります。まぁ「元々ある」とか「拡張した」とか書いてありますがこれはあくまでも仕様のことであって、コードベースはフルスクラッチです。まぁsprightlyは元々その程度の規模のものですから、誰でもすぐ作れますよ。基本的にreplaceAllが1つあるだけみたいなものなので。

これで直接HTMLだけで書く時の面倒臭さが随分と解消されていて、CMSがなくてもCMSのテンプレート機能があるのと同じくらいには自由にいろいろ出来るようになりました。

URL書き換え

Apache等にもある、URLの書き換えルールが設定できるようにしました。

これはウェブサーバ機能の元にしたserver-handlerに元々あった機能を使えるようにしただけで、サーバのトップディレクトリにmap.rulesというファイルを置いて、

rewriterule /index.html         /templates/top.html
rewriterule /corporate.html     /templates/plain.html
rewriterule /business.html      /templates/business.html
rewriterule /projects.html      /templates/projects.html
rewriterule /news.html          /templates/news.html
rewriterule /news/*             /templates/news_entry.html
rewriterule /recruit.html       /templates/plain.html
rewriterule /blog/index.html    /templates/blog.html
rewriterule /blog/*             /templates/blog_entry.html
rewriterule /request.html      /templates/plain.html
rewriterule /privacy.html      /templates/plain.html
rewriterule /                   /templates/top.html

のように書いておくと、左のパスでアクセスされた時に実際に読み込まれるファイルは右のパスとなるというものです。なお、server-handlerのメインモジュールであるweb-server.jsはこういった修正を大量に入れているので、オリジナルとは随分と異なるコードになっています。それ以外のモジュールはそのまま使っているので、依存にはserver-handlerが入ってますけれど。

この例のように、実際に読み込まれるものはテンプレートとなるファイルですが、その中に前述の{{> }}があると書き換え前にアクセスされるべきファイルが読み込まれます。たとえば、/corporate.htmlにアクセスが来た場合、実際に読み込まれるファイルは$DOCUMENT_ROOT/template/plain.htmlですが、その中に{{> }}があった場合は、そこに$DOCUMENT_ROOT/corporate.htmlが読み込まれます。

クライアントサイドのJavascriptのlocationは「今実際にブラウザが表示しているURL」を元にしていますから、送出されたHTMLの内容がどこからのものであるか関係なく、書き換え前のURLをアクセスしたつもりになっています。なので、たとえば/news/123がアクセスされた場合はこの例にある/news/*が発動して/template/news_entry.htmlが送出され、その中のJavascriptから見た場合のlocation.pathname/news/123となります。なので、「123」をエントリidとして使うようなことが出来るわけですね。

動的コンテンツの処理

動的コンテンツを処理するために、Svelteを使ってコードを書いています。この辺は普通にaxiosを使ってAPI叩いて.... というありがちの処理です。

まぁここは特に説明の必要はないと思います。動的コンテンツの取得のためのエントリidは、前節で書いたような処理をしています。

動的コンテンツはテンプレートの内側に書かれるのですが、ヘッダの中にあるタイトルとかコンテンツの内容に連動させたい場合があって、その場合には

    for ( let target of document.querySelectorAll('.title') ) {
        target.innerHTML = title;
    }

こんな感じの姑息なことをしています。

その他

動的コンテンツは元々のCMSに依存したURL(主に画像とか)があったので、そこを無理やりAPIコールに書き換えるコードを入れたりしていますが、この辺はまぁどこでもあることでしょう。

基本的には元のサイトのパーツを使って作ったので、見た目そのものは同じになっています。

まとめ

ローカルでは正しく動いていますので、準備ができたところで入れ換えしてしまうつもりです。

テストをしていて気がついたのですが、ヘッドレスCMSは通信量が増えがちですね。

たとえば、ブログのページを表示する場合、

  • ベースとなるHTMLとアセットの取得
  • 本体コンテンツの取得
  • 関連エントリの取得
  • 最近のエントリ(右カラム)の取得
  • カテゴリー一覧の取得
  • (あれば)諸々画像の取得

のためにGETされるわけです。ヘッドレスでなければ、

  • HTMLとアセットの取得
  • (あれば)諸々の画像の取得

だけで終わります。どう考えても通信量は増えがちです。

もちろん、いろんな工夫で通信量を減らすことは可能です。たとえば、コンテンツを表示して前後に遷移するような時には、本体コンテンツだけ取得すれば良いわけですし。

とは言え、弊社のブログのアクセスパターンを見ていると、「どこかにリンクされたコンテンツをピンポイントでアクセス」というのが多いので、そういった工夫はまるで意味を持ってくれません。

そう考えると、ヘッドレスCMSというのは、「本体」ではヘッドレスにしないで他でコンテンツを流用したいという時に適用するのが正しい使い方だなぁと感じます。前ほど世間でヘッドレスCMSが喧伝されなくなったのは、こういった特性に依るものなのだろうと思います。

「ヘッドレス」の部分はさておき、Napierのウェブサーバ機能はこんな感じで拡張されたので、普通の企業サイト程度であれば使いものになるレベルになったのではないかなと思います。最新版(2.5.3)以降であれば使えるようになっています。

PS.

先程(2023/07/26 13:50)、フロントを移行してリリースさせました。

なんか元のCMSよりも速いレスポンスのように思えます。CMS移行と共にサーバも移行させてOrangePi5の上で動いているので、そのせいもあるかも知れません。

とりあえず弊社くらいだと、こういった構成で十分だということですね。

最近のエントリー

Hieronymusのインボイス番号対応について

会計システム「Hieronymus」の現状

OrangePi5にZabbixをインストールする

レビュー等の依頼について

オープンソースのノートアプリ「SiYuan」 - CasaOS AppStoreレビュー

お気に入りの色さがし1

創立記念日

現在の営業品目(2)

現在の営業品目(1)

SPDX License Listをデータ化した

Orange Pi5でC3TR-Adapterを試す

CasaOS上で会計システム「Hieronymus」を動かす

会計システム「Hieronymus」v1.0.0 リリースしました

CasaOSでファイル同期アプリSyncthingをセットアップする

第11回 Freshmeat

オープンソースノーコード「Activepieces」でワークフローを作る

RaspberryPiにパーソナルクラウドOS「CasaOS」を導入する

sequelize-cliでdb:migrateすると「SyntaxError: Unexpected token ':'」が出る

LED行燈の試作(2)

CMSの社内向けサービスのリニューアル