Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
609 changes: 372 additions & 237 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@thegetty/quire",
"version": "1.0.0-rc.33",
"version": "1.0.0-rc.34",
"private": true,
"description": "a multi-format book publishing framework",
"author": "Getty Digital",
Expand Down
37 changes: 37 additions & 0 deletions packages/11ty/_includes/components/head-tags/pagefind.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable camelcase */
import { html } from 'common-tags'

/**
* Renders <head> <meta> data tags for PageFind search
*
* @param {Object} eleventyConfig
*
* @return {String} HTML meta elements
*/
export default function (eleventyConfig) {
const contributors = eleventyConfig.getFilter('contributors')
const removeHTML = eleventyConfig.getFilter('removeHTML')

return function ({ page, layout }) {
if (!page) {
return
}

const meta = [
{
property: 'type',
content: layout
}
]

if (page.contributor) {
const contributorContent = contributors({ context: page.contributor, format: 'string', type: 'primary' })
meta.push({
property: 'contributors',
content: removeHTML(contributorContent)
})
}

return html`${meta.map(({ property, content }) => `<meta property="pagefind:${property}" content="${content}" data-pagefind-meta="${property}[content]">`)}`
}
}
3 changes: 3 additions & 0 deletions packages/11ty/_includes/components/head.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function (eleventyConfig) {
const opengraph = eleventyConfig.getFilter('opengraph')
const removeHTML = eleventyConfig.getFilter('removeHTML')
const twitterCard = eleventyConfig.getFilter('twitterCard')
const pagefind = eleventyConfig.getFilter('pagefind')

const { application, publication } = eleventyConfig.globalData

Expand Down Expand Up @@ -70,6 +71,8 @@ export default function (eleventyConfig) {

${twitterCard({ abstract, cover, layout })}

${pagefind({ page, layout })}

<script type="application/ld+json">${jsonld({ canonicalURL, page })}</script>

<link rel="icon" href="/_assets/images/icons/favicon.ico" />
Expand Down
1 change: 1 addition & 0 deletions packages/11ty/_includes/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export { default as modal } from './modal/index.js'
export { default as navigation } from './navigation.js'
export { default as opengraph } from './head-tags/opengraph.js'
export { default as pageButtons } from './page-buttons.js'
export { default as pagefind } from './head-tags/pagefind.js'
export { default as pageHeader } from './page-header.js'
export { default as pageTitle } from './page-title.js'
export { default as search } from './search.js'
Expand Down
3 changes: 0 additions & 3 deletions packages/11ty/_includes/components/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { html } from '#lib/common-tags/index.js'
export default function (eleventyConfig) {
const icon = eleventyConfig.getFilter('icon')
return (params) => {
const { url } = params.publication
const searchIndex = new URL('search-index.json', url)
return html`
<template id="js-search-results-template">
<li class="quire-search__inner__list-item">
Expand All @@ -23,7 +21,6 @@ export default function (eleventyConfig) {
<div
aria-expanded="false"
class="quire-search"
data-search-index="${searchIndex}"
id="js-search"
>
<div class="quire-search__close-button">
Expand Down
4 changes: 3 additions & 1 deletion packages/11ty/_layouts/base.11ty.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export default async function (data) {
const id = this.slugify(url) || path.parse(inputPath).name
const pageId = `page-${id}`
const figures = pageData.page.figures
const shouldIndex = pageData.page.search !== false
const search = shouldIndex ? 'data-pagefind-body' : ''

return html`
<!doctype html>
Expand All @@ -33,7 +35,7 @@ export default async function (data) {
</div>
<div class="quire__primary">
${this.navigation(data)}
<main class="quire-page ${classes}" data-output-path="${outputPath}" data-page-id="${pageId}" >
<main class="quire-page ${classes}" data-output-path="${outputPath}" data-page-id="${pageId}" ${search}>
${content}
</main>
</div>
Expand Down
78 changes: 62 additions & 16 deletions packages/11ty/_plugins/search/index.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,70 @@
import write from './write.js'
import SearchIndex from './search.js'
import path from 'node:path'

const QUIRE_FIGURE_CLASS = '.q-figure'
const SEARCH_INDEX_DIR = '_search'

/**
* An Eleventy plugin for full-text search
* An Eleventy plugin for full-text search using Pagefind
*
* @param {Object} eleventyConfig eleventy configuration
* @param {Object} [options] search engine options
* @param {Object} collections eleventy collections
* @param {Object} options
* @param {boolean} options.indexFigures
* Configures if figures should be indexed separately
* @param {Array} options.excludeSelectors
* CSS selectors to exclude from search index
* @param {string} options.searchIndexDir
* The directory name for outputing the search index
*
*/
export default function (eleventyConfig, collections, options) {
/**
* Copy search module and write index to the output directory
* @see {@link https://www.11ty.dev/docs/copy/ Passthrough copy in 11ty}
* @todo set output destination to 'js/search'
*/
eleventyConfig.addPassthroughCopy('plugins/search/search.js')

/**
* Write index
*/
eleventyConfig.on('eleventy.after', async () => {
export default function (eleventyConfig, collections, {
indexFigures = false,
excludeSelectors = [],
searchIndexDir = SEARCH_INDEX_DIR
} = {}) {
eleventyConfig.on('eleventy.after', async ({ results }) => {
const { outputDir, publicDir } = eleventyConfig.globalData.directoryConfig

write(collections, publicDir || outputDir)
/**
* Add figures to the excluded selectors if indexing them separately.
*/
if (indexFigures) {
excludeSelectors = excludeSelectors ? [QUIRE_FIGURE_CLASS] : excludeSelectors.push(QUIRE_FIGURE_CLASS)
}

/**
* Create a new search index for each build.
*/
const index = new SearchIndex(eleventyConfig, {
excludeSelectors
})
await index.create()

/**
* Adds each results HTML content to the search index.
*/
await Promise.all(results.map(async ({ url, content }) => {
const page = collections.html.find(({ url: pageUrl }) => url === pageUrl)
if (!page || page.search === false) return
const { canonicalURL } = page.data
await index.addPageRecord({ url: canonicalURL, content })
}))

/**
* Add figures from each page to the search index.
*/
if (indexFigures) {
await Promise.all(collections.html.map(async ({ data }) => {
await index.addFiguresFromPage(data)
}))
}

/**
* Output the search index and compiled pagefind.js library to a _search directory.
*/
const outputPath = path.join(publicDir || outputDir, searchIndexDir)
await index.write(outputPath)
await index.close()
})
}
174 changes: 143 additions & 31 deletions packages/11ty/_plugins/search/search.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,151 @@
import lunr from 'lunr'

/**
* A full-text search module that implements methods to build and query an index of content.
*
* @class Search
*/
export default class Search {
constructor (data) {
this.contentList = data
this.index = this.buildIndex(data)
}

buildIndex (pages) {
return lunr(function () {
this.field('abstract')
this.field('content')
this.field('contributor')
this.field('subtitle')
this.field('title')
this.field('url')
this.ref('url')
pages.forEach((page) => {
this.add(page)
})
import * as pagefind from 'pagefind'
import path from 'node:path'

export default class SearchIndex {
constructor (eleventyConfig, {
excludeSelectors = []
} = {}
) {
this.eleventyConfig = eleventyConfig
this.config = eleventyConfig.globalData.config
this.publication = eleventyConfig.globalData.publication
this.excludeSelectors = excludeSelectors
}

#index = null

get index () {
return this.#index
}

set index (index) {
this.#index = index
}

/**
* Creates a new pagefind index.
*
* @param {Array} excludeSelectors
* @returns {Promise}
*/
async create (excludeSelectors) {
const { index } = await pagefind.createIndex({
excludeSelectors: excludeSelectors || this.excludeSelectors
})

this.#index = index

return index
}

/**
* Query the index of for the provided string or terms
* Outputs the pagefind index and library files.
*
* @param {String} query Terms for which to search
* @return {Array} An array of result objects
* @param {string} outputPath
* @returns {Promise<void>}
*/
search (query) {
const results = this.index.search(query)
return results.map((result) => this.contentList.find(({ url }) => url === result.ref))
async write (outputPath) {
await this.#index.writeFiles({
outputPath
})
}

/**
* Disconnect the PageFind backend.
*/
async close () {
this.#index = null
// We don't want to close the PageFind backend when running tests,
// as test runs are parallel and will share the same PageFind backend.
if (process.env.NODE_ENV === 'test') return
await pagefind.close()
}

/**
* Returns the served asset location for this image
* accounting for fully qualified asset URLs
*
* @param {string} srcPath
* @returns {string}
*/
assetSrc (srcPath) {
if (!srcPath) return null
const regexp = /^(https?:\/\/|\/iiif\/|\\iiif\\)/
const { imageDir } = this.config.figures
return regexp.test(srcPath) ? srcPath : path.posix.join(imageDir, srcPath)
}

/**
* Adds figures to the search index as non-HTML records,
* with their own metadata and links.
*
* @param {Object} figure
* @param {Object} figure.figureData
* @param {Object} figure.canonicalURL
* @param {Object} figure.title
* @returns {Promise<void>}
*/
async addFigureRecord ({ figureData, canonicalURL, title } = {}) {
const { id, caption, alt, src, thumbnail, label, credit, mediaType } = figureData
const markdownify = this.eleventyConfig.getFilter('markdownify')
const removeHTML = this.eleventyConfig.getFilter('removeHTML')

// Need to strip markdown and HTML tags for indexing
const htmlContent = markdownify(caption)
const content = removeHTML(htmlContent)

if (!label || !caption) {
return
}
await this.#index.addCustomRecord({
url: canonicalURL + '#' + id,
content,
meta: {
title: label,
pageTitle: title,
image: thumbnail || this.assetSrc(src) || '',
image_alt: alt || '',
credit,
type: mediaType
},
language: this.publication.language || 'en'
})
}

/**
* Adds pages to the search index as HTML files.
*
* @param {Object} page
* @param {string} page.url
* @param {string} page.content
* @returns {Promise<void>}
*/
async addPageRecord ({ url, content } = {}) {
await this.#index.addHTMLFile({
url,
content
})
}

/**
* Adds all figures in a page, if the page is searchable.
*
* @param {Object} pageData
* @param {Object} pageData.page
* @param {Object} pageData.search
* @returns {Promise<void>}
*/
async addFiguresFromPage (data) {
if (!this.#index) await this.create()
const { canonicalURL, page, search } = data
if (search === false || !page.figures) {
return
}

const pageTitle = this.eleventyConfig.getFilter('pageTitle')
const title = pageTitle(data)
await Promise.all(page.figures.map(async (figureData) => {
await this.addFigureRecord({ figureData, canonicalURL, title })
}))
}
}
Loading