Skip to content

Commit

Permalink
New search experience via Swiftype (#5223)
Browse files Browse the repository at this point in the history
* Prototyping new Swiftype search integration

* Remove script for building search index

* Remove search

* Script cleanup

* Style adjustments

* Relocate search bar to top of left nav

* Back out inadvertent package.json additions

* Reparent the Swiftype autocomplete container

Co-authored-by: Christian Nunciato <[email protected]>
  • Loading branch information
davidwrede and cnunciato authored Feb 20, 2021
1 parent 5df058e commit f0a24ce
Show file tree
Hide file tree
Showing 20 changed files with 111 additions and 371 deletions.
2 changes: 0 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ Which, on a page inside the `./content/reference` directory, will generate:

- **no_on_this_page** Specify this variable to prevent displaying an "On This Page" TOC on the right nav for the page.
- **block_external_search_index** Specify this variable to prevent crawlers from indexing the page.
- **exclude_from_pulumi_search_index** Specify this variable to prevent the page from appear in internal search results.

## Style guide

### Language and terminology styles
Expand Down
6 changes: 0 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,6 @@ build_components:
@echo -e "\033[0;32mBUILD COMPONENTS:\033[0m"
yarn --cwd components run build

.PHONY: build_search_index
build_search_index:
@echo -e "\033[0;32mBUILD SEARCH INDEX:\033[0m"
node ./scripts/build-search-index.js < ./public/docs/search-data/index.json > ./public/docs/search-index.json
rm -rf ./public/docs/search-data

.PHONY: generate
generate:
@echo -e "\033[0;32mGENERATE:\033[0m"
Expand Down
3 changes: 2 additions & 1 deletion assets/config/postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,15 @@ module.exports = {
],

// Whitelist custom parent selectors and their children.
whitelistPatterns: [/^fa-/, /^hs-/, /^highlight$/, /^pagination$/, /^code-/, /^copy-/, /^carousel/, /^bg-/, /BambooHR-/],
whitelistPatterns: [/^fa-/, /^hs-/, /^highlight$/, /^pagination$/, /^code-/, /^copy-/, /^carousel/, /^bg-/, /^st-/],
whitelistPatternsChildren: [
/^hs-/,
/^highlight$/,
/^pagination$/,
/^code-/,
/^copy-/,
/^carousel/,
/^st-/,

// Whitelist our web components along with any of their descendent selectors.
/^pulumi-chooser/,
Expand Down
198 changes: 32 additions & 166 deletions assets/js/search.js
Original file line number Diff line number Diff line change
@@ -1,172 +1,38 @@
"use strict";

(function () {
var searchBox = document.getElementById("search-query");
var spinner = document.getElementById("search-spinner");
var searchResultsContainer = document.getElementById("search-results");

// Use a worker to download and setup the index in the background.
var worker = new Worker("/js/search-worker.js");
worker.onmessage = function (message) {
var payload = message.data.payload;
displaySearchResults(payload.results);
};

// Extract the query from the browser's URL.
var query = getQueryVariable("q");
if (query) {
// Set the search-box's value to the query.
searchBox.value = query;
// Update the page title to include the query.
document.title = query + " - Pulumi";
// Kick-off the search by sending a message to the worker.
worker.postMessage({ type: "search", payload: query });
} else {
// If no query, display empty results.
displaySearchResults([]);
}

// Display the results of the search.
function displaySearchResults(results) {
// Hide the spinner.
spinner.style.display = "none";

if (results.length) {
// Group the results by category.
var categoryMap = {};
for (var i = 0; i < results.length; i++) {
var result = results[i];
var categoryName = getCategoryName(result.url);
var category = categoryMap[categoryName] = categoryMap[categoryName] || [];
prepareResult(result, categoryName);
category.push(result);
}

// Build up the HTML to display.
var appendString = "";
for (var i = 0; i < categories.length; i++) {
var categoryName = categories[i].name;
var category = categoryMap[categoryName];
if (category && category.length > 0) {
appendString += buildCategoryString(categoryName, category);
// Swiftype appends the autocomplete results container to the body element, which prevents us
// from being able to position it in a way that scrolls alongside the other content in the L2 nav.
// So we listen for DOM changes, and when Swiftype appends the element, we reposition it below the
// input field.

// Only bother doing this if we're on a page with a search box.
if (document.querySelector("#search-container")) {
const observer = new MutationObserver((mutations, observer) => {
var [ mutation ] = mutations;

// Only bother when nodes are added.
if (mutation && mutation.addedNodes && mutation.addedNodes.length > 0) {
const [ newNode ] = mutation.addedNodes;

if (newNode && (typeof newNode.getAttribute === "function") && newNode.getAttribute("id") === "st-injected-content") {

// Find our results container and reparent the Swiftype container with it.
var resultsContainer = document.querySelector("#search-results");
if (resultsContainer) {
resultsContainer.appendChild(newNode);
}

// Stop listening.
observer.disconnect();
}
searchResultsContainer.innerHTML = appendString;
} else {
searchResultsContainer.innerHTML = "<p>No results found.</p>";
}
}
});

var defaultCategory = "Other";
var categories = [
// Start listening for DOM mutation events.
observer.observe(
document.querySelector("body"),
{
name: "APIs",
predicate: function (url) {
return url.startsWith("/docs/reference/pkg/");
}
attributes: false,
childList: true, // New childNodes are all we care about.
subtree: false,
},
{
name: "CLI",
predicate: function (url) {
return url.startsWith("/docs/reference/cli/");
}
},
{
name: "Tutorials",
predicate: function (url) {
return url.startsWith("/docs/get-started/") ||
url.startsWith("/docs/tutorials/") ||
url.startsWith("/docs/guides/");
}
},
{
name: defaultCategory,
predicate: function (url) {
return true;
}
},
];

function getCategoryName(url) {
for (var i = 0; i < categories.length; i++) {
var category = categories[i];
if (category.predicate(url)) {
return category.name;
}
}
return defaultCategory;
}

function prepareResult(result, categoryName) {
switch (categoryName) {
case "APIs":
if (result.title.startsWith("Module ")) {
result.display = result.title.substring("Module ".length);
result.type = "module";
return;
} else if (result.title.startsWith("Package ")) {
result.display = result.title.substring("Package ".length);
result.type = "package";
return;
}
break;

case "CLI":
if (result.title.length === 0 && result.url.startsWith("/docs/reference/cli/")) {
var regex = /\/docs\/reference\/cli\/([a-z_]+)/gm;
var match = regex.exec(result.url)
if (match !== null) {
result.display = match[1].replace(/_/g, " ");
return;
}
}
break;
}

result.display = result.title || result.url;
}

function buildCategoryString(categoryName, category) {
var appendString = "<div class='search-results-category'><h2>" + categoryName + " (" + category.length + ")</h2>";

// Display the top 5 results first.
appendString += "<ul>";
var topResults = category.splice(0, 5);
for (var i = 0; i < topResults.length; i++) {
var item = topResults[i];
var prefix = getPrefix(item, categoryName);
appendString += "<li><a href='" + item.url + "' title='" + item.display + "'>" + prefix + item.display + "</a>";
}
appendString += "</ul>";

// Now display any remaining results, sorted alphabetically.
if (category.length > 0) {
category.sort(function (l, r) {
return l.display.toUpperCase() > r.display.toUpperCase() ? 1 : -1;
});
appendString += "<ul>";
for (var i = 0; i < category.length; i++) {
var item = category[i];
var prefix = getPrefix(item, categoryName);
appendString += "<li><a href='" + item.url + "'>" + prefix + item.display + "</a>";
}
appendString += "</ul>";
}

appendString += "</div>";
return appendString;
}

function getPrefix(result, categoryName) {
if (result.type) {
switch (result.type) {
case "module":
return "<span class='symbol module' title='Module'></span>";
case "package":
return "<span class='symbol package' title='Package'></span>"
}
}

return "";
}
})();
);
}
69 changes: 43 additions & 26 deletions assets/sass/_search.scss
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
.search-results-category {
@apply mb-4 justify-between flex-1 p-4 bg-gray-200 text-sm overflow-auto rounded;
// Override the default styles of the Swiftype input field.
.st-default-search-input {
display: block !important;
width: 100% !important;
font-size: .875rem !important;
padding: .75rem !important;
padding-left: 2.5rem !important;
border-radius: .25rem !important;
border-color: #cbd5e0 !important; /* border-gray-400 */
font-family: "Open Sans" !important;
background: none !important;

&:not(:last-of-type) {
@apply mr-4
}
@include transition;

h2 {
@apply mt-0 mb-4 text-gray-700 text-xl;
&:focus {
border-color: #52a6da !important; /* border-blue-500 */
}

ul {
@apply p-0 list-none;
@screen md {
margin-right: 1em !important;
}
}

li {
.symbol {
&:before {
@apply text-white text-xs rounded mr-2 px-1;
}
// Override the default styles of the Swiftype autocomplete results container.
.st-default-autocomplete {
z-index: 40 !important; /* z-40 */

&.module {
&:before {
content: "M";
@apply bg-green-700;
}
}
// Swiftype's JS imperatively positions the autocomplete container in relation to the top-left
// corner of the document, which is okay if your search box scrolls with the page, but since ours
// scrolls and sticks independently of the main content pane, this doesn't work. Overriding the
// top and left settings allows the container to be positioned absolutely in place.
top: auto !important;
left: auto !important;

&.package {
&:before {
content: "P";
@apply bg-purple-700;
}
}
.st-query-present {
.st-ui-result {
.st-ui-type-heading {
font-size: 0.75rem !important; /* text-xs */
color: #4387c7 !important; /* text-blue-600 */
margin-bottom: 4px !important;
}
.st-ui-type-detail {
color: #4a5568 !important; /* text-gray-600 */
}
}
}
}

// Override the default styles of the results overlay.
.st-ui-container {
@screen lg {
top: 80px !important;
}
}
2 changes: 1 addition & 1 deletion config/marketing-dev/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ignoreFiles = [ "content/docs/.*" ]
ignoreFiles = [ "content/docs/reference/pkg/*" ]
refLinksErrorLevel = "WARNING"
1 change: 0 additions & 1 deletion content/docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
title: Documentation
linktitle: Docs
meta_desc: Learn how to create, deploy, and manage infrastructure on any cloud using Pulumi's open source infrastructure as code SDK.
exclude_from_pulumi_search_index: true
no_on_this_page: true
menu:
header:
Expand Down
7 changes: 0 additions & 7 deletions content/docs/search-data.md

This file was deleted.

6 changes: 0 additions & 6 deletions content/docs/search.md

This file was deleted.

11 changes: 6 additions & 5 deletions layouts/docs/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ <h1>{{ .Title }}</h1>

<div class="md:w-3/12 md:pl-8">
<div class="sticky-sidebar">
<div class="mt-10 pt-8 border-t-2 border-gray-400 md:border-none md:block md:mb-4 md:pt-0 md:mt-0">
{{ partial "docs/search.html" . }}
</div>

<div class="ml-2 hidden md:block">
{{ partial "docs/right-nav.html" . }}
</div>
Expand All @@ -32,7 +28,12 @@ <h1>{{ .Title }}</h1>

<div class="md:w-3/12 pr-8 mb-2 pt-8 md:order-first md:border-none md:pt-0 md:mt-0">
<div class="sticky-sidebar">
{{ partial "docs/toc.html" . }}
<div class="md:mt-1 mb-6">
{{ partial "docs/search.html" . }}
</div>
<div>
{{ partial "docs/toc.html" . }}
</div>
</div>
</div>
</div>
Expand Down
12 changes: 0 additions & 12 deletions layouts/docs/search-data.json

This file was deleted.

Loading

0 comments on commit f0a24ce

Please sign in to comment.