Skip to content

Commit

Permalink
feat: add site wide search (resolves #393, #316) (#394)
Browse files Browse the repository at this point in the history
  • Loading branch information
jobara authored Jun 11, 2024
1 parent 164839d commit 9ed821d
Show file tree
Hide file tree
Showing 31 changed files with 632 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ insert_final_newline = true
indent_style = space
indent_size = 4

[*.yml]
[*.{yml,scss}]
indent_style = space
indent_size = 2
8 changes: 8 additions & 0 deletions eleventy.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const fluidPlugin = require("eleventy-plugin-fluid");
const rssPlugin = require("@11ty/eleventy-plugin-rss");
const navigationPlugin = require("@11ty/eleventy-navigation");
const eleventyImage = require("@11ty/eleventy-img");
const {exec} = require("child_process");

const exampleBlockShortcode = require("./src/_shortcodes/example-block.js");
const learningBlockShortcode = require("./src/_shortcodes/learning-block.js");
Expand Down Expand Up @@ -108,6 +109,13 @@ module.exports = function (eleventyConfig) {
].reverse();
});

eleventyConfig.on("afterBuild", async () => {
// TODO: Once 11ty v3 is stable and the project updated to use it, it will be possible to use Pagefind's
// NodeJS API instead of calling `npx` with `exec`. This is because 11ty currently doesn't support ES6 modules.
// https://pagefind.app/docs/node-api/
await exec("npx pagefind");
});

return {
dir: {
input: "src"
Expand Down
7 changes: 7 additions & 0 deletions pagefind.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"site": "_site",
"excludeSelectors": [
"footer",
".toc-menu"
]
}
4 changes: 3 additions & 1 deletion src/_includes/layouts/base.njk
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{% block headerScripts %}
{% endblock %}
</head>
<body{% if bodyClass %} class="{{ bodyClass }}"{% endif %}>
<body{% if bodyClass %} class="{{ bodyClass }}"{% endif %} {% if searchable %} data-pagefind-body{% endif %} {% if category %} data-pagefind-meta="category:{{ category }}"{% endif %}>
{% uioTemplate %}
{% include "partials/global/header.njk" %}
<main>
Expand All @@ -27,6 +27,8 @@
{% set itemType = "Article" %}
{% elif page.url === "/about/" %}
{% set itemType = "AboutPage" %}
{% elif page.url === "/search/" %}
{% set itemType = "Search" %}
{% endif %}
<article itemscope itemtype="https://schema.org/{{ itemType }}">
{% block pageHeader %}
Expand Down
6 changes: 3 additions & 3 deletions src/_includes/layouts/case-study.njk
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

{% set bodyClass = "single-case-study" %}

{% block headerScripts %}
<script type="text/javascript" src="/assets{{ assets['/scripts/toc.js'] }}" defer></script>
{% endblock %}
{% block headerScripts %}
<script type="text/javascript" src="/assets/scripts/toc.js" defer></script>
{% endblock %}

{% block pageHeader %}
<header class="[ header ] [ {% if bannerBg %}bg-{{ bannerBg }} {% endif %} {% if bannerText %}text-{{ bannerText }} {% endif %}]">
Expand Down
6 changes: 3 additions & 3 deletions src/_includes/layouts/design-process.njk
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

{% set bodyClass = "page design-process" %}

{% block headerScripts %}
<script type="text/javascript" src="/assets{{ assets['/scripts/toc.js'] }}" defer></script>
{% endblock %}
{% block headerScripts %}
<script type="text/javascript" src="/assets/scripts/toc.js" defer></script>
{% endblock %}

{% block pageHeader %}
<header class="[ header ] [ {% if bannerBg %}bg-{{ bannerBg }} {% endif %} {% if bannerText %}text-{{ bannerText }} {% endif %}]">
Expand Down
8 changes: 4 additions & 4 deletions src/_includes/layouts/page.njk
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

{% set bodyClass = "page" %}

{% block headerScripts %}
<script type="text/javascript" src="/assets{{ assets['/scripts/toc.js'] }}" defer></script>
{% endblock %}
{% block headerScripts %}
<script type="text/javascript" src="/assets/scripts/toc.js" defer></script>
{% endblock %}

{% block content %}
<div class="[ content{% if toc %} content-with-sidebar{% endif %} ]">
{% if toc %}
<div class="sidebar">
<nav id="toc" class="toc" aria-labelledby="table-of-contents">
<nav id="toc" class="toc" aria-labelledby="table-of-contents" data-pagefind-ignore>
<button type="button" aria-expanded="false">Table of contents <svg class="inline" width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0.912109 0.990478L5.91211 5.99048L10.9121 0.990477" stroke="currentColor" stroke-width="2"/></svg></button>
<div class="toc-menu">
<h2 id="table-of-contents" class="h3">Table of contents</h2>
Expand Down
6 changes: 3 additions & 3 deletions src/_includes/layouts/resource.njk
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

{% set bodyClass = "single-resource" %}

{% block headerScripts %}
<script type="text/javascript" src="/assets{{ assets['/scripts/toc.js'] }}" defer></script>
{% endblock %}
{% block headerScripts %}
<script type="text/javascript" src="/assets/scripts/toc.js" defer></script>
{% endblock %}

{% block pageHeader %}
<header class="[ header ] [ {% if bannerBg %}bg-{{ bannerBg }} {% endif %} {% if bannerText %}text-{{ bannerText }} {% endif %}]">
Expand Down
49 changes: 49 additions & 0 deletions src/_includes/layouts/search.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{% extends "layouts/base.njk" %}

{% set bodyClass = "page search" %}

{% block headerScripts %}
<script src="/assets/scripts/search.js"></script>
{% endblock %}

{% block pageHeader %}
<header class="[ header ] [ {% if bannerBg %}bg-{{ bannerBg }} {% endif %} {% if bannerText %}text-{{ bannerText }} {% endif %}]" data-background="dark">
<div class="wrapper">
<h1 itemprop="name" class="wider">
{{ title | safe}}
</h1>
<form action="">
<label for="search" class="visually-hidden">Search the Community-led Co-design Kit</label>
<div class="search-container">
{% include "svg/search.svg" %}
<input id="search" type="search" name="q" autofocus>
</div>
<button type="submit" class="button"><span>Search</span></button>
</form>
</div>
</header>

{% endblock %}

{% block content %}
<div id="results-container" class="[ content ] cloak">
<div class="inner-content">
<div id="search-results"></div>
<script type="module">
// Pagefind scripts are added at build time
const pagefind = await import("/pagefind/pagefind.js");
await pagefind.options({
highlightParam: "highlight"
});
pagefind.init();
let results = await search(pagefind, {
svgs: {
previous: `{% include "svg/previous.svg" %}`,
next: `{% include "svg/next.svg" %}`,
}
});
</script>
</div>
</div>
{% endblock %}
12 changes: 9 additions & 3 deletions src/_includes/partials/components/navigation.njk
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
{% if entry.rel %}
{% set relAttribute = ' rel="' + entry.rel + '"' %}
{% endif %}

{% if currentPage %}
{% set currentAttribute = ' aria-current="page"' %}
{% endif %}
Expand All @@ -37,7 +37,13 @@
{% if entry.url == "#" %}
<p {{ parentAttribute | safe }}>{{ entry.title }}</p>
{% else %}
<a{{ relAttribute | safe }}{{ parentAttribute | safe }}{{ currentAttribute | safe }} href="{{ entry.url | url }}">{{ entry.title }}</a>
<a{{ relAttribute | safe }}{{ parentAttribute | safe }}{{ currentAttribute | safe }} href="{{ entry.url | url }}">
{% if entry.icon %}
{% set navIcon = entry.icon | slugify + ".svg" %}
{% include "svg/" + navIcon %}
{% endif %}
{{ entry.title }}
</a>
{% endif %}
{%- if entry.children.length > 0 %}
<ul role="list">
Expand All @@ -47,7 +53,7 @@
{% if child.rel %}
{% set relAttribute = ' rel="' + child.rel + '"' %}
{% endif %}

{% if currentPage %}
{% set currentAttribute = ' aria-current="page"' %}
{% endif %}
Expand Down
5 changes: 5 additions & 0 deletions src/_includes/partials/global/scripts.njk
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<script type="text/javascript" src="/assets/scripts/app.js" defer></script>
{% uioScripts %}
<script type="text/javascript" src="/assets/scripts/uio.js" defer></script>
<script type="text/javascript" src="/pagefind/pagefind-highlight.js"></script>
<script type="module">
await import('/pagefind/pagefind-highlight.js');
new PagefindHighlight({ highlightParam: "highlight" });
</script>
4 changes: 4 additions & 0 deletions src/_includes/svg/next.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/_includes/svg/previous.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/_includes/svg/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions src/assets/scripts/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"use strict";

const clamp = function (value, min, max) {
return Math.max(Math.min(value, max), min);
};

const constructHeading = function (level = 2) {
return `<h${level}>Search results</h${level}>`;
};

const constructSummary = function (term, count) {
return `<strong>${count} results for “${term}”</strong>`;
};

const constructResults = function (results) {
let searchResults = "";

results.forEach((result) => {
searchResults += `
<li>
<div class="search-results__title">
<a href="${result.url}">${result.meta.title}</a>
<em>${result.meta.category ?? ""}</em>
</div>
<p class="search-results__excerpt">${result.excerpt}</p>
</li>
`;
});

return `<ol class="search-results" role="list">${searchResults}</ol>`;
};

const constructHREF = function (baseURL, params = {}, withOrigin = true) {
const url = new URL(baseURL);
for (const param in params) {
url.searchParams.set(param, params[param]);
}
return withOrigin ? url.href : url.href.substring(url.origin.length);
};

const constructPageLinks = function (baseURL, pages, page = 1, svgs = {}) {
if (pages <= 1) {
return "";
}

const current = clamp(page, 1, pages);
const prev = `<li><a href="${constructHREF(baseURL, {page: current - 1})}" rel="prev"df>${svgs.previous ? `<span class="visually-hidden">Previous</span>${svgs.previous}` : "Previous"}</a></li>`;
const next = `<li><a href="${constructHREF(baseURL, {page: current + 1})}" rel="next">${svgs.next ? `<span class="visually-hidden">Next</span>${svgs.next}` : "Next"}</a></li>`;

let pageLinks = "";

for (let i = 1; i <= pages; i++) {
pageLinks += `<li><a href="${constructHREF(baseURL, {page: i})}" ${i === current ? "aria-current=\"page\"" : ""}>${i}</a></li>`;
}

return `
<nav class="pagination" aria-label="Search pagination">
<ul role="list">
${current > 1 ? prev : ""}
${pageLinks}
${current < pages ? next : ""}
</ul>
</nav>
`;
};

const getPagedResults = async function (search, page = 1, itemsPerPage = 5) {
const pages = Math.ceil(search.results.length / itemsPerPage);
const start = clamp(page - 1, 0, pages) * itemsPerPage;
const end = Math.max(start, Math.min(start + 5, search.results.length));
return await Promise.all(search.results.slice(start, end).map(r => r.data()));
};

const render = async function (container, search, term, page = 1, options) {
let opts = {
"itemsPerPage": 5,
...options
};

const containerElm = document.querySelector(container);
const pages = Math.ceil(search.results.length / opts.itemsPerPage);
const pagedResults = await getPagedResults(search, page, opts.itemsPerPage);

containerElm.innerHTML = `
${constructHeading()}
${constructSummary(term, search.results.length)}
${constructResults(pagedResults)}
${constructPageLinks(window.location, pages, page, opts.svgs)}
`;

return pagedResults;
};

/*
This is intended for performing a single search on a page.
However, Pagefind can support more functionality like sorting, filtering,
debounced search and etc.
*/
const search = async function (pagefind, options) {
const opts = {
"itemsPerPage": 5,
"inputSelector": "#search",
"resultsContainer": "#results-container",
"resultsSelector": "#search-results",
"cloak": "cloak",
"queryParam": "q",
"pageParam": "page",
...options
};

const params = new URLSearchParams(window.location.search);
let term = params.get(opts.queryParam);

const searchInput = document.querySelector(opts.inputSelector);
searchInput.value = term;

let page = params.get(opts.pageParam);
page = isNaN(page) ? 1 : Number(page ?? 1);

term = typeof term === "string" ? term.trim() : "";

if (!term) {
return [];
}

const search = await pagefind.search(term);
let results = await render(opts.resultsSelector, search, term, page, opts);

if (opts.cloak) {
document.querySelector(opts.resultsContainer).classList.remove(opts.cloak);
}

return results;
};

window.search = search;
10 changes: 10 additions & 0 deletions src/assets/styles/abstracts/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,14 @@ $scale: (
--nav-submenu-hover-color: var(--white);
--nav-submenu-focus-background: var(--blue-100);
--nav-submenu-focus-color: var(--blue-600);

// Pagination
--pagination-color: var(--blue-500);
--pagination-current-background: var(--blue-100);
--pagination-hover-background: var(--blue-100);
--pagination-hover-color: var(--blue-600);
--pagination-focus-background: var(--blue-600);
--pagination-focus-color: var(--white);
--pagination-active-background: var(--blue-500);
--pagination-active-color: var(--white);
}
4 changes: 3 additions & 1 deletion src/assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"components/hr",
"components/image-text",
"components/navigation",
"components/pagination",
"components/select",
"components/table-of-contents";

Expand All @@ -48,7 +49,8 @@
"pages/design-process",
"pages/home",
"pages/resource",
"pages/resources";
"pages/resources",
"pages/search";

// 7. Themes
@import
Expand Down
Loading

0 comments on commit 9ed821d

Please sign in to comment.