この記事は Jung von Matt のデベロッパー、Thorsten Harders、Sebil Satici による The AMP Blog の記事 "In search of the amp.dev search " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
amp.dev では、ケース スタディ、各コンポーネントの動作の詳細、ベスト プラクティスを説明したガイド、詳細なチュートリアル、実行可能なたくさんのコードサンプルなど、AMP を使った開発を簡単にするたくさんのリソースが提供されています。実際、あまりにたくさんのリソースがあるので、デベロッパーがすべての既存コンテンツをすばやく見つけて利用できるような方法が必要になりました。私たちは、それを実現するために、サイトですべてのコンテンツを直接検索できるようにしたいと考えました。本記事では、amp.dev の検索を実装した手順について、順を追って説明します。
TL;DR : 新しい amp.dev の検索は、<amp-lightbox> 、<amp-list> 、<amp-autocomplete> 、<amp-mustache> 、そしてもちろん、これらすべてをまとめる <amp-bind> を使って構築されています。また、Google カスタム検索 を活用したカスタム API と、最後の検索クエリと検索結果をキャッシュするService Worker 機能 を利用しています。
AMP コンポーネントを使った構築
amp.dev の検索機能の構築には、以下の 3 つの目標がありました。
すべてのページで専用のレイヤーから検索できること
AMP デベロッパー エクスペリエンスの中核をなす AMP コンポーネントの検索に最適化されていること
有効な AMP 機能を使って実装すること
検索レイヤーの表示と非表示(amp-lightbox を利用)
ウェブを見渡せば、さまざまな種類の検索機能を見つけることができます。シンプルな入力項目になっているものもあれば、素敵なアニメーションで展開されたり折りたたまれたりするものもあります。結果のページングを実現するためにページ読み込みを行ったり、結果を非同期的に表示したりするものもあります。また、検索キーワードの候補を自動で表示してくれるものもあれば、そうでないものもあります。amp.dev では、こういったアプローチから最善の方法を組み合わせることで、できる限りユーザーの邪魔にならず、それでいてすばやくアクセスできるソリューションを生み出したいと考えました。そこで、全画面レイヤーの中に検索機能全体をカプセル化して、以下を実現することにしました。
ユーザーにとって興味深い記事(例: 公開された新しいガイドやコンポーネント)の候補は、ユーザーがアクセスしているページにかかわらず表示する
ユーザーがページ内の他の要素によって惑わされることがないようにする
別の結果ページを読み込むのではなく、検索結果をインラインですばやく表示する
これらのニーズを満たすために、<amp-lightbox> を引き続き利用することにしました。このコンポーネントは、検索レイヤーの表示と非表示を簡単にしてくれるだけでなく、AMP フロントエンドとの統合に役立つアクションやイベントも提供しています。
シームレスな操作の追加
ユーザー エクスペリエンスの観点から言えば、ユーザーが検索クエリの入力と結果の参照とをシームレスに行き来できることがとても重要です。ユーザーが検索レイヤーを開くと、自動的に入力項目にフォーカスが移動し、ユーザーは即座に入力を始めることができます。検索レイヤーを閉じると、キーボードを使っている人が以前と同じ位置で作業を続けられるように、フォーカスが検索トグルに戻ります。
この機能は、<amp-lightbox> のオープンおよびクローズ イベントと、グローバル フォーカス アクションを組み合わせて実装しました。
<amp-lightbox layout ="nodisplay"
on ="lightboxOpen:searchInput.focus;
lightboxClose:searchTriggerOpen.focus"
scrollable >
検索結果の一覧表示(amp-list と amp-mustache を利用)
ページへの検索結果の表示には、<amp-list> コンポーネントを使っています。このコンポーネントには、既にページングと無限スクロールが組み込まれています。これらはまさに、検索結果を一覧表示する際に必要な機能です。 実際の検索はサーバーサイドで実装されており、API エンドポイント /search/do 経由でアクセスできます。その結果、次のような JSON オブジェクトが返されます。
{
"result" : {
"pageCount" : 10 ,
"pages" : [
{
"title" : "Some title" ,
"description" : "Description" ,
"url" : "http://amp.dev/some/link"
},
...
]
},
"nextUrl" : "/search/do?q=amp&page=2"
}
<amp-list> の完全な検索 URL を更新するために、[src] 属性を入力クエリにバインドすることで、amp-bind を使っています。また、無限スクロールを実現するために load-more 属性を設定し、後続の検索が実行される際にクリーンな再読み込み操作を実現できるように、reset-on-refresh 属性を設定します。<amp-mustache> テンプレートでは、結果オブジェクトのデータを使って動的に一覧をレンダリングします。コードは次のようになります。
<amp-list id ="searchList"
src ="/search/initial-items"
[src ]="query ? '/search/initial-items : '/search/do?q=' +
encodeURIComponent(query)"
binding ="no"
items ="."
height ="80vh"
layout ="fixed-height"
load-more ="auto"
load-more-bookmark ="nextUrl"
reset-on-refresh
single-item >
<template type ="amp-mustache" >
<div class ="search-result-list" >
{{#result.pages}}
<a href ="{{url}}" >
<h4 > {{title}}</h4 >
<p > {{description}}</p >
</a >
{{/result.pages}}
</div >
</template >
</amp-list >
重要な候補の表示(amp-autocomplete を利用)
アナリティクス データによると、amp.dev にアクセスするデベロッパーの目的で最も多いのは、AMP コンポーネントのドキュメントやサンプルを参照することです。そのため、これらのコンテンツをできる限り探しやすくしたいと考えました。<amp-autocomplete> は、AMP で自動候補表示をすぐに実現できるソリューションです。ここでの目的は、利用できるすべての AMP コンポーネントの候補を自動表示することです。静的データソースは /search/autosuggest エンドポイントで提供され、autocomplete コンポーネントは filter="substring" 指定を利用してクライアントサイドで候補を生成します。
<amp-autocomplete filter ="substring"
min-characters ="1"
on ="select:AMP.setState({ query: event.value })"
submit-on-enter ="false"
src ="/search/autosuggest"
>
<input placeholder ="What are you looking for?" >
</amp-autocomplete >
検索の実行(amp-form を利用)
ユーザーは、自動表示されたいずれかのオプションを選択するか、フォームを送信することによって検索を呼び出します。実際の検索リクエストを実行するために、URL パラメータとしてステート query を利用します。
<amp-list [src ]="'/search/do?q=' + encodeURIComponent(query) + '&locale=en'" >
フォームが送信されたときと autocomplete コンポーネントが select イベントを送信したときの両方 の場合で、amp-autocomplete の on アクションに基づいて query が更新されます(それによって amp-list がレンダリングされます)。
<form action-xhr ="/search/echo"
on ="submit:
AMP.setState({ query: queryInput }),
searchResult.focus,
searchList.changeToLayoutContainer"
method ="POST" target ="_top" >
<amp-autocomplete filter ="substring"
min-characters ="1"
on ="select:AMP.setState({ query: event.value })"
submit-on-enter ="false"
src ="/search/autosuggest"
>
<input id ="searchInput"
placeholder ="What are you looking for?"
on ="input-throttled:AMP.setState({ queryInput: event.value })"
>
<button disabled [disabled ]="!queryInput" > Search</button >
</amp-autocomplete >
</form >
フォームが送信されたときは、最初のエントリに対して直接キーボード ナビゲーションを開始できるように、検索結果コンテナにフォーカスを移します。さらに、コンテンツに応じてリストの高さを変えて縮小できるように、<amp-list> の changeToLayoutContainer を呼び出します。この例では、フォームの submit イベントの結果は必要ないので、単純にエコー アクションに向けています。補足: 近日中にフォームを使わずに `amp-autocomplete` を利用できるようになる見込みなので、将来的にこれは不要になります。
Service Worker による以前の検索結果のキャッシュ
検索の最初のバージョンをリリースした後、かなり早い段階で関連機能リクエストを受け取りました。それは、ユーザーが別のページに移動したときでも、最後の検索クエリの結果を表示し続けるというものでした。
これを実現するために、AMP のワンライン Service Worker である amp-sw を利用しました。amp-sw は、キャッシュやオフライン ページといった基本的な PWA 機能を提供します。これを拡張することで、最後の検索クエリとそれに対応する検索結果を保存するようにしました。
検索を始めるとき、以前の検索クエリとその結果を表示します。そうでない場合は、候補記事の一覧を表示します。ページを読み込むときは、サーバー エンドポイント /search/latest-query を使って amp-state オブジェクトを初期化し、検索入力項目と検索結果を設定します。
<amp-state id="query" src="/search/latest-query" ></amp-state>
<amp-list src="/search/initial-items"
[src]="query ? '/search/initial-items : '/search/do?q=' + encodeURIComponent(query)"
...
</amp-list >
ここでのポイントは、このサーバー エンドポイントが実在しないことです。魔法の処理を行っているのは Service Worker です。Service Worker はこのルートをインターセプトし、ネットワークから元々のレスポンスを読み込む代わりに、ユーザーが最後に検索をリクエストした際にキャッシュしておいた検索クエリと検索結果から新しいレスポンスを作成し、それをページに送り返します。
最後の検索クエリの保存は、リクエストする URL から正規表現を使って query パラメータを取得し、新しく作成したレスポンス オブジェクトをキャッシュに格納することで実現します。
検索リクエストに一致するエントリがキャッシュ内に存在するかどうかは、ルート ハンドラがチェックします。存在する場合は、キャッシュから即座に結果が返されます。そうでない場合、リクエストはサーバーに到達し、結果は以降の呼び出しに備えてキャッシュされます。
async function searchDoRequestHandler (url, request ) {
const searchQuery = decodeURIComponent (url.search.match(/q=([^&]+)/ )[1 ]);
const cache = await caches.open(SEARCH_CACHE_NAME);
cache.put(SEARCH_LATEST_QUERY_PATH, new Response(`"${searchQuery} "` ));
let response = await cache.match(request);
if (response) return response;
response = await fetch(request);
if (response.status == 200 ) {
cache.delete(request, {
ignoreSearch : true ,
});
cache.put(request, response.clone());
}
return response;
}
こうすることで、ユーザーが別のページで検索レイヤーを開いたときでも、自動的に以前の検索結果を受信でき、中断したところから作業を継続することができます。
ハンドラ関数は、次のようにして登録しています。
self .addEventListener('fetch' , (event) => {
const requestUrl = new URL(event.request.url);
if (requestUrl.pathname === '/search/do' ) {
event.respondWith(searchDoRequestHandler(requestUrl, event.request));
}
});
<amp-state> や <amp-list> といった AMP コンポーネントは、リモート エンドポイントからデータを読み込みます。Service Worker API の助けを借りてリクエストをインターセプトし、それを動的に変更するというのは、こういったコンポーネントが利用するデータをパーソナライズしたい場合に適した方法です。ユーザーの最後の検索クエリをキャッシュすることで検索のユーザー エクスペリエンスを高めるのも、その一例です。
まとめ
amp.dev の検索機能を実装することで、ユーザーが直感的かつ効率的にサイトのコンテンツへの正確なナビゲーションを行えるようにするという目標を達成できました。さらにユーザー エクスペリエンスを向上させるため、Service Worker の機能を使って以前の検索結果をキャッシュできるようにもしました。
すばらしいのは、JavaScript を 1 行も使わずにこの検索機能を組み込むことができた点です(Service Worker 部分は除きます)。AMP の既存コンポーネントのみで、候補の自動表示や無限スクロールなどの便利な機能を組み込むことができました。これらを使わずにこういった機能を実装するのは、かなり難しい作業になるはずです。
投稿者: Jung von Matt のデベロッパー、Thorsten Harders、Sebil Satici
Reviewed by Chiko Shimizu - Developer Relations Team