- Published on
Hugoで検索機能
- Authors
- Name
- Kikusan
Hugoサイトの検索機能をLunr.js, Reactによって作成しました。 手順を残します。
事前準備
まず、検索に必要なjsonファイルはhugoの機能で作成します。index.jsonを_default直下に配置し, config.tomlに以下を記入することでindex.jsonが作成されます。
- config.toml
# ........
[outputs]
home = ["JSON", "HTML"]
- index.json
{{ $dateFormat := default "Mon Jan 2, 2006" (index .Site.Params "date_format") }}
{{ $utcFormat := "2006-01-02T15:04:05Z07:00" }}
{{- $.Scratch.Add "index" slice -}}
{{- range .Site.RegularPages -}}
{{- $.Scratch.Add "index" (dict "title" .Title "tags" .Params.tags "description" .Description "categories" .Params.categories "contents" .Plain "href" .Permalink "utc_time" (.Date.Format $utcFormat) "formated_time" (.Date.Format $dateFormat)) -}}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}
リポジトリにjsxフォルダを作成し、ローカルでビルドします。参照
cd jsx
npm init -y
npm install babel-cli@6 babel-preset-react-app@3
npx babel --watch src --out-dir ../static/js --presets react-app/prod
これでjsx配下のファイルは、/static/jsにビルドされて配置されます。
Lunr.js
ココとココから以下を持ってきて、/static/js/に配置します。
- lunr.js.js
- lunr.js
- lunr.multi.js
- lunr.stemmer.support.js
- tinyseg.js
ページで読み込みます。(私の場合、hugoのhead-custom.htmlファイル)
<script src="/js/lunr.js"></script>
<script src="/js/lunr.stemmer.support.js"></script>
<script src="/js/tinyseg.js"></script>
<script src="/js/lunr.ja.js"></script>
<script src="/js/lunr.multi.js"></script>
Reactで検索の実装
<script src="https://unpkg.com/react@17/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js" crossorigin></script>
<script src="/js/search.js"></script>
こう読みこまれるsearch.jsを、jsxで作成します。(一部jquery使用)
let lunrIndex, pagesIndex;
function initLunr() {
$.getJSON("/index.json").done((index) => {
pagesIndex = index;
lunrIndex = lunr(function() {
let lunrConfig = this;
lunrConfig.use(lunr.multiLanguage('en', 'ja'));
lunrConfig.ref("href");
lunrConfig.field("title", { boost: 10 });
lunrConfig.field("contents");
pagesIndex.forEach((page) => {
lunrConfig.add(page);
});
});
})
.fail((jqxhr, textStatus, error) => {
let err = textStatus + ", " + error;
console.error("Error getting Hugo index file:", err);
});
}
function search(){
let query = document.getElementById('search-query').value;
if (query.length < 1) {
return;
}
renderResults(results(query));
}
function results(query) {
// Get the object that matches the search result from the page
return lunrIndex.search(`*${query}*`).map((result) => {
return pagesIndex.filter((page) => {
return page.href === result.ref;
})[0];
});
}
function renderResults(results) {
let eResult = document.querySelector("#blog-main");
const noMatch = <p>No matches found</p>;
if (!results.length) {
ReactDOM.render(noMatch, eResult);
return;
}
ReactDOM.render(<Articles results={results} />, eResult);
}
function Articles(props) {
const articles = props.results.map((r, i) =>
<article key={i} className="border-bottom">
<a className="article-hover d-block p-3 text-decoration-none" href={r.href}>
<header>
<h2 className="blog-post-title" dir="auto">{r.title}</h2>
<p className="blog-post-meta">
<time dateTime={r.utc_time}>{r.formated_time}</time>
<Categories categories={r.categories} />
<Tags tags={r.tags} />
</p>
</header>
<span className="text-muted">{r.description} </span>
Read more →
</a>
</article>
);
return (
<React.Fragment>
{articles}
</React.Fragment>
);
}
function Categories(props) {
const categories = props.categories.map((c, i) =>
<React.Fragment key={i}>
{i > 0 &&
" , "
}
<object>
<a className="text-info" href={`/categories/${c.toLowerCase()}/`} rel="category tag">
{c}
</a>
</object>
</React.Fragment>
);
return (
<React.Fragment>
in <span className="fas fa-folder" aria-hidden="true"></span>
{categories}
</React.Fragment>
)
}
function Tags(props) {
const tags = props.tags.map((t, i) =>
<React.Fragment key={i}>
{i > 0 &&
" , "
}
<object>
<a className="badge badge-tag" href={`/tags/${t.toLowerCase()}/`} rel="category tag">
{t}
</a>
</object>
</React.Fragment>
);
return (
<React.Fragment>
<span className="fas fa-tag" aria-hidden="true"></span>
{tags}
</React.Fragment>
)
}
initLunr();
initLunrで検索するjsonを読み込んでおき、searchで検索し、レンダリングします。
Articles以下のjsxはこのサイトのテーマに沿っているので、自由にカスタムしてください。