From b0f08fe8e2b467640ca1b6da77456f33a584c471 Mon Sep 17 00:00:00 2001 From: Kevin Pruett Date: Tue, 1 Sep 2020 11:59:08 -0400 Subject: [PATCH] Integrate @hashicorp/react-search into layout --- website/components/search-bar/index.jsx | 28 ++++++ website/components/search-bar/style.css | 4 + website/layouts/api.jsx | 19 +++- website/layouts/docs.jsx | 19 +++- website/layouts/intro.jsx | 19 +++- website/package-lock.json | 65 ++++++++++++ website/package.json | 9 +- website/pages/style.css | 42 ++++---- website/scripts/index_search_content.js | 126 +----------------------- 9 files changed, 165 insertions(+), 166 deletions(-) create mode 100644 website/components/search-bar/index.jsx create mode 100644 website/components/search-bar/style.css diff --git a/website/components/search-bar/index.jsx b/website/components/search-bar/index.jsx new file mode 100644 index 000000000..5d998ca88 --- /dev/null +++ b/website/components/search-bar/index.jsx @@ -0,0 +1,28 @@ +import Search from '@hashicorp/react-search' + +export default function SearchBar() { + return ( + ( + <> + + + + + + + + )} + resolveHitLink={(hit) => ({ + href: { + pathname: `/${transformIdtoUrl(hit.objectID)}`, + }, + })} + placeholder="Search Consul documentation" + /> + ) +} + +function transformIdtoUrl(id) { + return id.replace(/\/index$/, '') +} diff --git a/website/components/search-bar/style.css b/website/components/search-bar/style.css new file mode 100644 index 000000000..3004fb498 --- /dev/null +++ b/website/components/search-bar/style.css @@ -0,0 +1,4 @@ +.g-search { + width: calc(100% - 2rem); + max-width: 600px; +} diff --git a/website/layouts/api.jsx b/website/layouts/api.jsx index 9858f78b0..706032295 100644 --- a/website/layouts/api.jsx +++ b/website/layouts/api.jsx @@ -1,18 +1,21 @@ -import DocsPage from '@hashicorp/react-docs-page' -import order from '../data/api-navigation.js' -import { frontMatter as data } from '../pages/api-docs/**/*.mdx' import Head from 'next/head' import Link from 'next/link' import { createMdxProvider } from '@hashicorp/nextjs-scripts/lib/providers/docs' +import DocsPage from '@hashicorp/react-docs-page' +import { SearchProvider } from '@hashicorp/react-search' +import SearchBar from '../components/search-bar' +import { frontMatter as data } from '../pages/api-docs/**/*.mdx' +import order from '../data/api-navigation.js' const MDXProvider = createMdxProvider({ product: 'consul' }) function ApiDocsLayoutWrapper(pageMeta) { function ApiDocsLayout(props) { + const { children, ...propsWithoutChildren } = props return ( + > + + + {children} + + ) } diff --git a/website/layouts/docs.jsx b/website/layouts/docs.jsx index 97c4e8eb6..08f0c5da8 100644 --- a/website/layouts/docs.jsx +++ b/website/layouts/docs.jsx @@ -1,18 +1,21 @@ -import DocsPage from '@hashicorp/react-docs-page' -import order from '../data/docs-navigation.js' -import { frontMatter as data } from '../pages/docs/**/*.mdx' import Head from 'next/head' import Link from 'next/link' import { createMdxProvider } from '@hashicorp/nextjs-scripts/lib/providers/docs' +import DocsPage from '@hashicorp/react-docs-page' +import { SearchProvider } from '@hashicorp/react-search' +import SearchBar from '../components/search-bar' +import { frontMatter as data } from '../pages/docs/**/*.mdx' +import order from '../data/docs-navigation.js' const MDXProvider = createMdxProvider({ product: 'consul' }) function DocsLayoutWrapper(pageMeta) { function DocsLayout(props) { + const { children, ...propsWithoutChildren } = props return ( + > + + + {children} + + ) } diff --git a/website/layouts/intro.jsx b/website/layouts/intro.jsx index 2813310ce..9394a2ea4 100644 --- a/website/layouts/intro.jsx +++ b/website/layouts/intro.jsx @@ -1,18 +1,21 @@ -import DocsPage from '@hashicorp/react-docs-page' -import order from '../data/intro-navigation.js' -import { frontMatter as data } from '../pages/intro/**/*.mdx' import Head from 'next/head' import Link from 'next/link' import { createMdxProvider } from '@hashicorp/nextjs-scripts/lib/providers/docs' +import DocsPage from '@hashicorp/react-docs-page' +import { SearchProvider } from '@hashicorp/react-search' +import SearchBar from '../components/search-bar' +import { frontMatter as data } from '../pages/intro/**/*.mdx' +import order from '../data/intro-navigation.js' const MDXProvider = createMdxProvider({ product: 'consul' }) function IntroLayoutWrapper(pageMeta) { function IntroLayout(props) { + const { children, ...propsWithoutChildren } = props return ( + > + + + {children} + + ) } diff --git a/website/package-lock.json b/website/package-lock.json index afd97fb24..926667eaa 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -3768,6 +3768,23 @@ "@hashicorp/react-image": "^2.0.3" } }, + "@hashicorp/react-search": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@hashicorp/react-search/-/react-search-2.1.0.tgz", + "integrity": "sha512-vaTht+2G9ipsVyusK3b3TtUpuy9ccsxj3NMSWXJyGsoT39K1Oovb8aLiIlbUU5Ll72KEi5yq5OS3WAJDdSqW+g==", + "requires": { + "@hashicorp/react-inline-svg": "^1.0.2", + "@hashicorp/remark-plugins": "^3.0.0", + "algoliasearch": "^4.4.0", + "dotenv": "^8.2.0", + "glob": "^7.1.6", + "gray-matter": "^4.0.2", + "react-instantsearch-dom": "^6.7.0", + "remark": "^12.0.1", + "search-insights": "^1.6.0", + "unist-util-visit": "^2.0.3" + } + }, "@hashicorp/react-section-header": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@hashicorp/react-section-header/-/react-section-header-2.0.2.tgz", @@ -4584,6 +4601,21 @@ "@algolia/transporter": "4.4.0" } }, + "algoliasearch-helper": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.2.2.tgz", + "integrity": "sha512-/3XvE33R+gQKaiPdy3nmHYqhF8hqIu8xnlOicVxb1fD6uMFmxW8rGLzzrRfsPfxgAfm+c1NslLb3TzQVIB8aVA==", + "requires": { + "events": "^1.1.1" + }, + "dependencies": { + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + } + } + }, "ally.js": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/ally.js/-/ally.js-1.4.1.tgz", @@ -14310,6 +14342,34 @@ } } }, + "react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, + "react-instantsearch-core": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/react-instantsearch-core/-/react-instantsearch-core-6.7.0.tgz", + "integrity": "sha512-wIvSIwkWfqPbaQZcbKsfBK3Gpm1e7ahSwU8Bmx1N5RfUqA/NghqS0Ppv3sz4vCXjoEAdPV06R+Fpn9lT+cE9/Q==", + "requires": { + "@babel/runtime": "^7.1.2", + "algoliasearch-helper": "^3.1.0", + "prop-types": "^15.5.10", + "react-fast-compare": "^3.0.0" + } + }, + "react-instantsearch-dom": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/react-instantsearch-dom/-/react-instantsearch-dom-6.7.0.tgz", + "integrity": "sha512-J1C9xkHHLLa6rkKLKFDa7szA0TDo6yPFGmDzh2+JLaq4o694RIqivfUpROHus0Ki3BAQu9QmzLtodf6K1NOBWQ==", + "requires": { + "@babel/runtime": "^7.1.2", + "algoliasearch-helper": "^3.1.0", + "classnames": "^2.2.5", + "prop-types": "^15.5.10", + "react-instantsearch-core": "^6.7.0" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -15320,6 +15380,11 @@ "ajv-keywords": "^3.1.0" } }, + "search-insights": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-1.6.2.tgz", + "integrity": "sha512-mpy+57HZVMZH5HsMHYMCLvkf+tUvhy+ycP2tDy1j7wmj+mQsNZ3LC61IcMYomok02NozaMR3GiGyfH6uc+ibdA==" + }, "section-matter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", diff --git a/website/package.json b/website/package.json index 1079ffef5..40fe6af15 100644 --- a/website/package.json +++ b/website/package.json @@ -22,6 +22,7 @@ "@hashicorp/react-mega-nav": "4.0.1-2", "@hashicorp/react-product-downloader": "4.1.1", "@hashicorp/react-product-features-list": "1.0.3", + "@hashicorp/react-search": "^2.1.0", "@hashicorp/react-section-header": "2.0.2", "@hashicorp/react-subnav": "3.2.6", "@hashicorp/react-text-and-content": "4.1.4", @@ -31,21 +32,15 @@ "@hashicorp/react-text-split-with-logo-grid": "1.3.0", "@hashicorp/react-use-cases": "1.0.6", "@hashicorp/react-vertical-text-block-list": "2.0.3", - "algoliasearch": "4.4.0", "babel-plugin-import-glob-array": "0.2.0", - "dotenv": "8.2.0", - "gray-matter": "4.0.2", "next": "9.4.4", "nuka-carousel": "4.7.0", "react": "16.13.1", "react-device-detect": "1.13.1", - "react-dom": "16.13.1", - "remark": "12.0.1", - "unist-util-visit": "2.0.3" + "react-dom": "16.13.1" }, "devDependencies": { "dart-linkcheck": "2.0.15", - "glob": "7.1.6", "husky": "4.2.5", "prettier": "2.0.5" }, diff --git a/website/pages/style.css b/website/pages/style.css index 8d323a889..ed2f68996 100644 --- a/website/pages/style.css +++ b/website/pages/style.css @@ -10,31 +10,32 @@ --highlight-color: var(--consul); } -@import '~@hashicorp/react-button/dist/style.css'; -@import '~@hashicorp/react-section-header/dist/style.css'; -@import '~@hashicorp/react-logo-grid/dist/style.css'; -@import '~@hashicorp/react-product-features-list/dist/style.css'; -@import '~@hashicorp/react-product-downloader/dist/style.css'; -@import '~@hashicorp/react-vertical-text-block-list/dist/style.css'; -@import '~@hashicorp/react-docs-sidenav/dist/style.css'; -@import '~@hashicorp/react-content/dist/style.css'; -@import '~@hashicorp/react-subnav/dist/style.css'; -@import '~@hashicorp/react-text-and-content/dist/style.css'; -@import '~@hashicorp/react-consent-manager/dist/style.css'; -@import '~@hashicorp/react-toggle/dist/style.css'; +@import '~@hashicorp/react-alert-banner/dist/style.css'; @import '~@hashicorp/react-alert/dist/style.css'; -@import '~@hashicorp/react-text-split/dist/style.css'; -@import '~@hashicorp/react-text-split-with-code/dist/style.css'; -@import '~@hashicorp/react-enterprise-alert/dist/style.css'; -@import '~@hashicorp/react-mega-nav/style.css'; -@import '~@hashicorp/react-docs-page/style.css'; +@import '~@hashicorp/react-button/dist/style.css'; @import '~@hashicorp/react-call-to-action/dist/style.css'; @import '~@hashicorp/react-case-study-slider/dist/style.css'; -@import '~@hashicorp/react-tabs/dist/style.css'; @import '~@hashicorp/react-code-block/dist/style.css'; -@import '~@hashicorp/react-alert-banner/dist/style.css'; -@import '~@hashicorp/react-use-cases/dist/style.css'; +@import '~@hashicorp/react-consent-manager/dist/style.css'; +@import '~@hashicorp/react-content/dist/style.css'; +@import '~@hashicorp/react-docs-page/style.css'; +@import '~@hashicorp/react-docs-sidenav/dist/style.css'; +@import '~@hashicorp/react-enterprise-alert/dist/style.css'; @import '~@hashicorp/react-featured-slider/dist/style.css'; +@import '~@hashicorp/react-logo-grid/dist/style.css'; +@import '~@hashicorp/react-mega-nav/style.css'; +@import '~@hashicorp/react-product-downloader/dist/style.css'; +@import '~@hashicorp/react-product-features-list/dist/style.css'; +@import '~@hashicorp/react-search/dist/style.css'; +@import '~@hashicorp/react-section-header/dist/style.css'; +@import '~@hashicorp/react-subnav/dist/style.css'; +@import '~@hashicorp/react-tabs/dist/style.css'; +@import '~@hashicorp/react-text-and-content/dist/style.css'; +@import '~@hashicorp/react-text-split-with-code/dist/style.css'; +@import '~@hashicorp/react-text-split/dist/style.css'; +@import '~@hashicorp/react-toggle/dist/style.css'; +@import '~@hashicorp/react-use-cases/dist/style.css'; +@import '~@hashicorp/react-vertical-text-block-list/dist/style.css'; /* Local Components */ @import '../components/basic-hero/style.css'; @@ -43,6 +44,7 @@ @import '../components/learn-callout/style.css'; @import '../components/case-study-carousel/style.css'; @import '../components/cloud-offerings-list/style.css'; +@import '../components/search-bar/style.css'; /* Layouts */ @import '../layouts/use-cases/style.css'; diff --git a/website/scripts/index_search_content.js b/website/scripts/index_search_content.js index 7eeb7524f..f853b4ddf 100644 --- a/website/scripts/index_search_content.js +++ b/website/scripts/index_search_content.js @@ -1,125 +1,3 @@ -require('dotenv').config() +const { indexDocsContent } = require('@hashicorp/react-search/tools') -const algoliasearch = require('algoliasearch') -const glob = require('glob') -const matter = require('gray-matter') -const path = require('path') -const remark = require('remark') -const visit = require('unist-util-visit') - -// In addition to the content of the page, -// define additional front matter attributes that will be search-indexable -const SEARCH_DIMENSIONS = ['page_title', 'description'] - -main() - -async function main() { - const pagesFolder = path.join(__dirname, '../pages') - - // Grab all search-indexable content and format for Algolia - const searchObjects = await Promise.all( - glob.sync(path.join(pagesFolder, '**/*.mdx')).map(async (fullPath) => { - const { content, data } = matter.read(fullPath) - - const searchableDimensions = SEARCH_DIMENSIONS.reduce( - (acc, dimension) => { - return { ...acc, [dimension]: data[dimension] } - }, - {} - ) - - const headings = await collectHeadings(content) - - // Get path relative to `pages` - const __resourcePath = fullPath.replace(`${pagesFolder}/`, '') - - // Use clean URL for Algolia id - const objectID = __resourcePath.replace('.mdx', '') - - return { - ...searchableDimensions, - headings, - objectID, - } - }) - ) - - try { - await indexSearchContent(searchObjects) - } catch (e) { - console.error(e) - process.exit(1) - } -} - -async function indexSearchContent(objects) { - const { - NEXT_PUBLIC_ALGOLIA_APP_ID: appId, - NEXT_PUBLIC_ALGOLIA_INDEX: index, - ALGOLIA_API_KEY: apiKey, - } = process.env - - if (!apiKey || !appId || !index) { - throw new Error( - `[*** Algolia Search Indexing Error ***] Received: ALGOLIA_API_KEY=${apiKey} ALGOLIA_APP_ID=${appId} ALGOLIA_INDEX=${index} \n Please ensure all Algolia Search-related environment vars are set in CI settings.` - ) - } - - console.log(`updating ${objects.length} indices...`) - - try { - const searchClient = algoliasearch(appId, apiKey) - const searchIndex = searchClient.initIndex(index) - - const { objectIDs } = await searchIndex.partialUpdateObjects(objects, { - createIfNotExists: true, - }) - - let staleIds = [] - - await searchIndex.browseObjects({ - query: '', - batch: (batch) => { - staleIds = staleIds.concat( - batch - .filter(({ objectID }) => !objectIDs.includes(objectID)) - .map(({ objectID }) => objectID) - ) - }, - }) - - if (staleIds.length > 0) { - console.log(`deleting ${staleIds.length} stale indices:`) - console.log(staleIds) - - await searchIndex.deleteObjects(staleIds) - } - - console.log('done') - process.exit(0) - } catch (error) { - throw new Error(error) - } -} - -async function collectHeadings(mdxContent) { - const headings = [] - - const headingMapper = () => (tree) => { - visit(tree, 'heading', (node) => { - const title = node.children.reduce((m, n) => { - if (n.value) m += n.value - return m - }, '') - // Only include level 1 or level 2 headings - if (node.depth < 3) { - headings.push(title) - } - }) - } - - return remark() - .use(headingMapper) - .process(mdxContent) - .then(() => headings) -} +indexDocsContent()