Drupal × Remixで遊んでたら、なんかちゃんとしたサイトできちゃった件
はじめに
お久しぶりです。
気づけば、まともにサイトを作るのは数年ぶり。
「もうそろそろ作ってもいい頃かな」と思いつつ、
億劫さに勝てずにNetflixばかり見ていたシハンセイカーエンジニアです。
で、最近ふと「Remix」ってやつを触ってみたら、
……これ、めちゃくちゃ良いじゃないか。
感触があまりにも良かったので、
せっかくだから暇つぶし全力モードでブログサイトを作ってみることにしました。
構成:とりあえず動くやつ
まずはdocker-compose.ymlを眺めながら、
「これくらいなら指が覚えてるだろ」と思って立ち上げた構成がこちら。
- nginx
- Drupal(on php-fpm)
- Remix(Node)
- MariaDB
……まぁ、よくある感じです。
最初はこれをそのままデプロイして、「お、俺やればできるじゃん」的な満足感を得ようとしたのですが、
現実はそんなに甘くありません。
メンテナンスコスト+サーバー代=趣味の域を軽く超える。
Nodeが動くレンタルサーバーを日本で探してみたけど、ない。
(あっても妙に怪しい。いや、怖い。)
そんな中、Next.jsのSSGが脳裏をよぎる。
「やっぱNextか…」と思いかけたそのとき、RemixにSPAモードなる存在が。
これだ!と思い、方針変更。
DrupalのAPIを叩いてJSONを保存し、
RemixをSPAモードで静的HTML化して、
それをさくらレンタルサーバーにrsyncするという、
地味に職人芸っぽい構成に着地しました。
Drupal:控えめに言って堅実
Drupal側は至ってシンプル。
構成の要は GraphQL + graphql_core_schema。
graphql_core_schemaの何が良いかって、
Entity Queryが使えること。
もうこれだけで、「とりあえず一覧出したい」というニーズの8割をカバーします。
使っているのはノード・タクソノミー・イメージスタイル。
要するに、Drupalの「無駄に高機能だけど必要十分」なところをつまみ食いしてる感じです。

認証まわり:OAuthなんて知らん
使っているモジュールはこれだけ。
- key_auth
- metatag
SSGなのにOAuth?
……いやいや、そんな高尚なことしません。
「アノニマスはちょっと不安」くらいの理由で、
key_authで軽くロックをかける程度。
このへんの“適度な手抜き感”が、
年季の入ったエンジニアの知恵です(?)
GraphQL Codegen:型書かずに済む幸福
GraphQL Codegen、これは素晴らしい。
スキーマとクエリから型とコードを自動生成。
DrupalのGraphQLエンドポイントにさえアクセスできれば、
「型定義で時間を溶かす」というあの苦痛から解放されます。
設定はこんな感じ👇
import { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: [
{
'http://nginx:8080/graphql': {
headers: { 'api-key': process.env.DRUPAL_AUTH_KEY },
},
},
],
documents: ['./graphql/documents/*.graphql'],
generates: {
'./graphql/generated.ts': {
plugins: [
"typescript",
"typescript-operations",
"typescript-graphql-request",
]
}
}
}
export default config
これを走らせるだけで、
「TypeScriptでGraphQL叩くの怖くない世界」にワープできます。
50代でも快適。
Remix:静的なのに動的っぽく
ルート構成は以下のような感じ。
_indexarticle.$path
Drupal側ではPathautoを使って /article/ 配下にURLを整備。
SSRモードではローダーを定義して、
Remixから直接Drupalを叩くことも可能です。
export const loader = async ({ params }) => {
const data = await Drupal.sdk().GetArticleByPath({path:'/article/' + params.path})
if (!data.route) {
throw new Response("Not Found", { status: 404 })
}
return { route: data.route }
}
が、今回はSPAモードなので、
静的JSONを読みに行く方針に。
export const clientLoader = async ({ params }) => {
const index = await (await fetch("/data/index.json")).json()
const item = index.entityQuery.items.find((i) => i.url.path == '/article/' + params.path)
if (!item) throw new Response(null, { status: 404 })
const data = await (await fetch("/data/"+ item.uuid +".json")).json()
return { data }
}
はい、静的です。だけどなんか動いてるっぽい。
この“なんちゃって動的感”がRemixのいいところ。
(しかもレンタルサーバー代が安く済む!)
データ生成スクリプト:地味だけど要
この仕組みを支えているのが以下のスクリプト。
地味ですが、これが全ての要です。
import { writeFileSync } from "fs"
import { Drupal } from "lib/Drupal"
async function makeData() {
const index = await Drupal.sdk().GetAllArticles()
writeFileSync('./public/data/index.json', JSON.stringify(index, null, 2))
for (const item of index.entityQuery.items) {
const data = await Drupal.sdk().GetArticleByPath({ path: item.url.path })
Drupal.transform(data)
writeFileSync(`./public/data/${item.uuid}.json`, JSON.stringify(data, null, 2))
}
}
makeData()
このスクリプトを叩くだけで、
DrupalのAPIレスポンスが静的JSONとして吐き出されます。
もうバックエンド要らない。俺の暇つぶし完了。
Drupalクラス:オトナの分別ある設計
最後に中核となるDrupalクラス。
コードの一部だけ抜粋します。
class _Drupal {
host() {
return process.env.DRUPAL_URL || 'http://nginx:8080'
}
client() {
return new GraphQLClient(this.host() + '/graphql', {
headers: () => ({ "api-key": process.env.DRUPAL_AUTH_KEY }),
})
}
sdk() {
return getSdk(this.client())
}
metatag(data) {
return data.map((item) => {
if (!item) return null
if (item.tag === 'link') {
item.attributes = { ...item.attributes, tagName: "link" }
}
return item.attributes
})
}
}
export const Drupal = new _Drupal()
大事なのは「無駄に凝らない」こと。
若い頃は“フルスタック”とか言って全方位に張ってたけど、
今は“最短ルートで目的達成”が信条です。
まとめ:暇つぶしは本気でやると危険
というわけで、Drupal × Remix の初回構築でした。
一見ふざけてるけど、ちゃんと動く。
(というか、思った以上にちゃんとしてしまった。)
このあと気が向いたら、
- 目次機能の追加
- 開発環境の改善
- Google連携
あたりをネタに、引き続き暇つぶししていこうと思います。
要するに、技術は遊びながら覚えるのが一番。
大人の全力の暇つぶし、侮るなかれ。