How To Add Search To Eleventy
If you have an Eleventy-based site, there are a few options you can take to add search to your page. One low-fi solution that I use on this website at the moment is based on a big JSON file and some JavaScript that searches through that JSON. This solution won't work so well for big-ish websites. This blog has about 2k posts and the full JSON-file is 1.3 MB at the moment. It works on my connection, but that's not nothing. So keep that in mind.
Here's how it works.
Create The Big JSON File
The big JSON file is just a JSON feed which you can generate using the Eleventy RSS plugin.
(Re-)Configure the RSS plugin
First thing we'll need to do is to configure the Eleventy RSS plugin. Here's what you'll need to add to your eleventy.config.js
:
// ...
import pluginRss from "@11ty/eleventy-plugin-rss";
// ...
export default async function(eleventyConfig) {
//...
eleventyConfig.addPlugin(pluginRss);
//...
};
If you are using a recent version of the eleventy-base-blog, you might see this kind of config:
eleventyConfig.addPlugin(feedPlugin, {
type: "atom", // or "rss", "json"
outputPath: "/feed/feed.xml",
stylesheet: "pretty-atom-feed.xsl",
templateData: {
eleventyNavigation: {
key: "Feed",
order: 4
}
},
collection: {
name: "posts",
limit: 10,
},
metadata: {
language: "en",
title: "Blog Title",
subtitle: "This is a longer description about your blog.",
base: "https://example.com/",
author: {
name: "Your Name"
}
}
});
You want to replace that with the much shorter version I have provided above. This longer config uses a so-called "virtual template" to provide the normal feed to your blog. However since we want more than one feed (the standard feed and the new "big json file" archive feed) we need to go the "manual template" route.
(Re-)Add The Basic Feed
Because most people will want to have a basic (non-json) feed for their site as well and because it exists in the base blog, before we changed everything, let's start by adding a basic feed to the site, the manual way. In your content folder create a feed.njk
with the following contents (change the metadata, of course):
---json
{
"permalink": "feed.xml",
"layout": "layouts/empty.njk",
"eleventyExcludeFromCollections": true,
"metadata": {
"title": "My Blog about Boats",
"description": "I am writing about my experiences as a naval navel-gazer.",
"language": "en",
"base": "https://example.com/",
"author": {
"name": "Boaty McBoatFace"
}
}
}
---
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ metadata.language or page.lang }}">
<title>{{ metadata.title }}</title>
<subtitle>{{ metadata.description }}</subtitle>
<link href="{{ permalink | htmlBaseUrl(metadata.base) }}" rel="self" />
<link href="{{ metadata.base | addPathPrefixToFullUrl }}" />
<updated>{{ collections.posts | getNewestCollectionItemDate | dateToRfc3339 }}</updated>
<id>{{ metadata.base | addPathPrefixToFullUrl }}</id>
<author>
<name>{{ metadata.author.name }}</name>
</author>
{%- for post in collections.posts | reverse %}
{%- set absolutePostUrl %}{{ post.url | htmlBaseUrl(metadata.base) }}{% endset %}
<entry>
<title>{{ post.data.title }}</title>
<link href="{{ absolutePostUrl }}" />
<updated>{{ post.date | dateToRfc3339 }}</updated>
<id>{{ absolutePostUrl }}</id>
<content type="html">{{ post.content | renderTransforms(post.data.page, metadata.base) }}</content>
</entry>
{%- endfor %}
</feed>
This is the "Atom" sample feed template from the Eleventy Docs with one important change: We added "layout": "layouts/empty.njk",
to the frontmatter. This was done, because my content folder includes a content.11tydata.js
data file which has the following contents:
export default {
layout: "layouts/home.njk",
};
But for our feed, that's not what we want. So we overwrite the layout in the feed template with an empty one. That layouts/empty.njk
looks like this:
{{ content | safe }}
Phew! Let's move on to what we were actually going to accomplish.
Create An Additional JSON-Feed
Create a file called archive.njk
. Mine lives in content/feeds
. This is again basically just the sample json feed from the docs, except that I changed the permalink.
---json
{
"permalink": "feeds/archive.json",
"eleventyExcludeFromCollections": true,
"metadata": {
"title": "My Blog about Boats",
"description": "I am writing about my experiences as a naval navel-gazer.",
"language": "en",
"base": "https://example.com/",
"author": {
"name": "Boaty McBoatFace"
}
}
}
---
{
"version": "https://jsonfeed.org/version/1.1",
"title": "{{ metadata.title }}",
"language": "{{ metadata.language or page.lang }}",
"home_page_url": "{{ metadata.base | addPathPrefixToFullUrl }}",
"feed_url": "{{ permalink | htmlBaseUrl(metadata.base) }}",
"description": "{{ metadata.description }}",
"authors": [
{
"name": "{{ metadata.author.name }}"
}
],
"items": [
{%- for post in collections.posts | reverse %}
{%- set absolutePostUrl %}{{ post.url | htmlBaseUrl(metadata.base) }}{% endset %}
{
"id": "{{ absolutePostUrl }}",
"url": "{{ absolutePostUrl }}",
"title": "{{ post.data.title }}",
"content_html": {% if post.content %}{{ post.content | renderTransforms(post.data.page, metadata.base) | dump | safe }}{% else %}""{% endif %},
"date_published": "{{ post.date | dateToRfc3339 }}"
}
{% if not loop.last %},{% endif %}
{%- endfor %}
]
}
Additionally, I have a data file in that folder feeds.11tydata.js
which has these contents:
export default {
layout: "layouts/empty.njk",
};
In other words, it specifies the empty.njk
layout for all feeds that live in this folder.
Finally, we have finished the preparatory work for the search.
Create The Search Page
This one's easier. Here's the template for the search.md
file:
---
title: "Search"
eleventyNavigation:
key: "Search"
order: 100
date: 2021-09-14T16:26:36+0200
permalink: "/search/"
---
<script language="javascript">
let archive_results = {};
function runSearch(q) {
const results_node = document.getElementById("list_results");
results_node.innerHTML = "";
if (q.length > 0) {
for (let i = 0; i < archive_results.items.length; i++) {
const item = archive_results.items[i];
const title_lower = item.title.toLowerCase();
const text_lower = item.content_html.toLowerCase();
if (title_lower.includes(q) || text_lower.includes(q)) {
let s ; const p_node = document.createElement("p");
const link_node = document.createElement("a");
const d = Date.parse(item.date_published);
const date_s = new Date(d).toISOString().substr(0, 10);
const date_node = document.createTextNode(date_s);
link_node.appendChild(date_node);
link_node.href = item.url;
let title_node = null;
if (item.title.length > 0) {
title_node = document.createElement("span");
title_node.innerHTML = ": <b>" + item.title + "</b>";
s = item.title + ": " + item.content_html;
}
s = item.content_html;
if (s.length > 200) {
s = s.substr(0, 200) + "...";
}
const text_node = document.createElement("span");
text_node.innerHTML = ": " + s;
p_node.appendChild(link_node);
if (title_node != null) {
p_node.appendChild(title_node);
}
p_node.appendChild(text_node);
results_node.appendChild(p_node);
}
}
}
}
function submitSearch(q) {
runSearch(q);
const url = new URL(window.location.href);
url.searchParams.set("q", q);
history.pushState({}, "", url);
}
document.addEventListener("DOMContentLoaded", function() {
fetch("/feeds/archive.json").then(response => response.json()).then(data => {
archive_results = data;
const url = window.location.href;
const params = new URLSearchParams(new URL(url).search);
const q = params.get("q");
if (q && (q.length > 0)) {
document.getElementById("input_search").value = q;
runSearch(q);
}
});
});
</script>
<style>
.field {
width: 270px;
height: 34px;
font-size: 13px;
font-weight: 400;
padding-left: 12px;
border: 2px solid #eee;
margin-top: 20px;
margin-bottom: 20px;
border-radius: 17px;
-webkit-appearance: none;
}
</style>
<form onSubmit="return false;">
<input class="field" type="text" name="q" id="input_search" placeholder="Search" onChange="submitSearch(this.value.toLowerCase());" />
</form>
<div id="list_results"></div>
This template is a slightly adapted version of this template, which was meant for a hugo-based blog, but it works beautifully for our purposes here, too.
A quick code walk-through:
When the search page is fully loaded (DOMContentLoaded) we download (fetch) the archive.json and save its contents into a variable. If the user then tries to search using the search field, we go through the entries of archive.json and check if either the title or the body of a post includes our search term, if it does we create a little bit of html using Javascript which in turn gets then displayed on our search page. We also put the search term in the url and manipulate the browser history, so that the user can go back to their previous searches.
-
← Previous
DailyDogo 1174 🐶 -
Next →
DailyDogo 1175 🐶