シニアWebエンジニア M.H.
2026.7.22
Movable Type(以下MT)で作られたサイトは静的HTMLで構成されています。そのため、動的に出力されるWordPressのように柔軟に記事を検索・フィルタリングすることができません。(後述しますが、検索機能自体はあります)
とはいえMTは、HTMLだけでなく、JS・CSS・XML等のテキスト形式であればほぼ何でも出力できるCMSです。
そこで今回は、この「何でも出力できる」というMTの特性を活かし、記事の内容をJSON形式で出力し、そのデータをJavaScriptで読み取って記事検索を実現する方法を紹介します。
なお、この記事はソフトウェア版の話になりますので、Movable Type.netだとなにかしら違うかもしれません。
前準備として検証用のテーマ「sample01」を作り、その中にテンプレート類を配置していきます。既存テーマにテンプレートを追加する方法でも対応できるかと思います。(テーマに含めない場合はJavaScriptだけは手動でサイト内のどこかに配置するか、テンプレートにして出力する必要あり)
こんな感じで登録されている記事の情報をJSONとして出力するテンプレートを作成します。

出力対象は下記とします。
タイトル
パーマリンク
更新日
カテゴリ
抜粋(本文からタグ除いた100文字抜き出し)
search_index_json:
label: '検索インデックスJSON'
outfile: data/search-index.json
rebuild_me: '1'
[
<mt:Entries lastn="999999" sort_by="authored_on" sort_order="descend">
<mt:SetVarBlock name="search_excerpt"><$mt:EntryBody remove_html="1"$><mt:If tag="EntryMore"> <$mt:EntryMore remove_html="1"$></mt:If></mt:SetVarBlock>
<mt:Unless name="__first__">,</mt:Unless>
{
"title": "<$mt:EntryTitle remove_html="1" escape="js"$>",
"permalink": "<$mt:EntryPermalink escape="js"$>",
"updated_at": "<$mt:EntryModifiedDate format="%Y-%m-%d"$>",
"category": "<mt:EntryCategories type="primary"><$mt:CategoryLabel remove_html="1" escape="js"$></mt:EntryCategories>",
"excerpt": "<$mt:Var name="search_excerpt" trim_to="100" escape="js"$>"
}
</mt:Entries>
]
HTMLファイルを出力するテンプレートとそんなに差はないですが、JSON出力にあたって、下記モディファイアを使用しました。
remove_html="1" でHTMLのタグを削除
escape="JS" でJavaScript用にエスケープをする
[
{
"title": "お試し記事お知らせ",
"permalink": "<http://localhost/2026/04/post-4.html>",
"updated_at": "2026-07-22",
"category": "お知らせ",
"excerpt": "ここは本文です。\nリッチテキストエディタを使っています。\nこれはリンクです。\n画像も入れて見ましょう。\n"
}
,
{
"title": "お試し記事トピックス",
"permalink": "<http://localhost/2026/01/post-3.html>",
"updated_at": "2026-07-22",
"category": "トピックス",
"excerpt": "これはリッチではない簡素なテキストエディタ(改行を変換)を使っています。\n「改行を変換」ではなくHTMLソースと書いた方がわかりやすいかもしれません。\nエディタのツールも押してもHTMLソースが出てき"
}
~~~省略~~~
スペースや改行が気になるようでしたら、<mt:for> と regex_replace モディファイアを利用して除去しておきましょう。
検索ページではカテゴリを選択できるようにしたいので、カテゴリ一覧出力用のテンプレートも作成しておきます。
search_categories_json:
label: 'カテゴリ一覧JSON'
outfile: data/categories.json
rebuild_me: '1'
[
<mt:Categories>
<mt:Unless name="__first__">,</mt:Unless>
"<$mt:CategoryLabel remove_html="1" escape="js"$>"
</mt:Categories>
]
イベント設定やDOMの変更などが面倒なので、プレーンなJavaScriptではなく、軽量フロントフレームワークの Alpine.js を使用して検索(フィルタリング)機能を実装します。
search_page:
label: '静的検索ページ'
outfile: search/index.html
rebuild_me: '1'
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>記事検索 | <$mt:BlogName encode_html="1"$></title>
<link rel="stylesheet" href="<$mt:Link template="stylesheet" encode_html="1"$>">
<script defer src="<$mt:SupportDirectoryURL encode_html="1"$>theme_static/sample01/js/search-page.js"></script>
<script defer src="<https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js>"></script>
</head>
<body>
<div
class="page"
x-data="staticSearch()"
x-init="init()"
data-search-index-url="<$mt:Link template="search_index_json" encode_html="1"$>"
data-categories-url="<$mt:Link template="search_categories_json" encode_html="1"$>"
>
<main class="content">
<section class="entry-list search-results">
<h2>記事検索</h2>
<p class="search-summary">キーワードは半角または全角スペース区切りで OR 検索します。カテゴリを選択した場合は AND 条件で絞り込みます。</p>
<section class="search-form">
<form @submit.prevent="runSearch()">
<div class="search-form-row search-form-row-stacked">
<input
type="search"
x-model.trim="keyword"
placeholder="キーワードを入力"
>
<select x-model="selectedCategory">
<option value="">すべてのカテゴリ</option>
<template x-for="category in categories" :key="category">
<option :value="category" x-text="category"></option>
</template>
</select>
<button type="submit" :disabled="loading">検索</button>
</div>
</form>
</section>
<template x-if="loading">
<p class="empty">検索データを読み込んでいます。</p>
</template>
<template x-if="!loading && errorMessage">
<p class="empty" x-text="errorMessage"></p>
</template>
<template x-if="!loading && !errorMessage && searched">
<div>
<p class="search-summary" x-text="summaryText"></p>
<template x-if="results.length === 0">
<p class="empty">該当する記事がありませんでした。</p>
</template>
<template x-for="entry in results" :key="entry.permalink">
<article class="entry-item">
<div class="entry-meta">
<time :datetime="entry.updated_at" x-text="entry.updated_at"></time>
<p class="entry-category"><span x-text="entry.category || '未分類'"></span></p>
</div>
<h3><a :href="entry.permalink" x-text="entry.title"></a></h3>
<p x-text="entry.excerpt"></p>
</article>
</template>
</div>
</template>
</section>
</main>
</div>
</body>
</html>
テンプレートではなく静的に作成して、テーマフォルダの static/js/search-page.js に置きます。
function staticSearch() {
return {
keyword: '',
selectedCategory: '',
entries: [],
categories: [],
results: [],
searched: false,
loading: true,
errorMessage: '',
searchedKeyword: '',
searchedCategory: '',
get summaryText() {
const parts = [];
const count = this.results.length;
if (this.searchedKeyword) {
parts.push('キーワード: ' + this.searchedKeyword);
}
if (this.searchedCategory) {
parts.push('カテゴリ: ' + this.searchedCategory);
}
if (parts.length === 0) {
return count + '件の記事を表示しています。';
}
return parts.join(' / ') + ' の検索結果 ' + count + '件';
},
async init() {
const root = this.$root;
const searchIndexUrl = root.dataset.searchIndexUrl;
const categoriesUrl = root.dataset.categoriesUrl;
try {
const responses = await Promise.all([
fetch(searchIndexUrl),
fetch(categoriesUrl)
]);
if (!responses[0].ok || !responses[1].ok) {
throw new Error('検索データの取得に失敗しました。');
}
this.entries = await responses[0].json();
this.categories = await responses[1].json();
} catch (error) {
this.errorMessage = error.message || '検索データの取得に失敗しました。';
} finally {
this.loading = false;
}
},
runSearch() {
const normalizedKeyword = this.keyword.replace(/ /g, ' ').trim();
const tokens = normalizedKeyword ? normalizedKeyword.split(/\s+/).filter(Boolean) : [];
const selectedCategory = this.selectedCategory;
this.results = this.entries.filter(function(entry) {
const matchesCategory = !selectedCategory || entry.category === selectedCategory;
const haystack = [entry.title, entry.category, entry.excerpt].join(' ').toLowerCase();
const matchesKeyword = tokens.length === 0 || tokens.some(function(token) {
return haystack.indexOf(token.toLowerCase()) !== -1;
});
return matchesCategory && matchesKeyword;
});
this.searchedKeyword = normalizedKeyword;
this.searchedCategory = selectedCategory;
this.searched = true;
}
};
}
window.staticSearch = staticSearch;
表示結果はこのような感じになりました。

今回は記事をJSON出力しましたが、コンテンツタイプ(コンテンツデータ)のJSON出力も可能です。
検索対象とする記事・コンテンツデータの件数がそんなに多くなければ、今回紹介した方法で問題無く検索・フィルタリング機能が提供できるかと思います。
mt-search.cgi (記事・ページ検索)
mt-cdsearch.cgi(コンテンツデータ検索)
mt-data-api.cgi(Data API:検索も含めAPI経由で色々なことができる)
これらの機能で事足りる場合もありますが、管理画面以外でCGIを動かすことになるので、静的HTMLだけで構成できるMTの利点が損なわれてしまうという懸念点があります。
そして環境にもよるかもしれませんが、動作がちょっともっさりしてたりもします……。
Data APIはMTで構築されたサイトで使用するというより、Headless CMSとして他のアプリなどから利用する、という用途向けかなとも思います。
MT既存の検索機能は複雑な検索条件の指定が限られていたりするので、それを拡張するプラグインも存在します。
ただし、こちらもCGIで動かすことになるので、前述の懸念点は残ったままとなります。
「静的サイト 検索」などで検索すればいっぱい出てきますので、ここでは割愛します。
MTは静的HTMLでサイト全体を公開できるため、サーバー側の負荷や管理の手間を抑えつつ、安定した運用が可能です。
その一方で、テキスト形式であれば何でも出力できるという柔軟性を持っており、この特性を活かして記事データをJSON形式で出力すれば、サーバーサイドのプログラムに頼ることなく、フロントエンドだけで使い勝手の良い検索・フィルタリング機能を実現できます。
静的サイトの手軽さと、動的サイトのような柔軟な検索性を両立できる点は、MTならではの強みと言えるでしょう。ぜひ自分のサイトに合わせてアレンジしてみてください。
関連する記事
この人が書いた記事