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
7 changes: 7 additions & 0 deletions packages/11ty/_includes/components/head-tags/pagefind.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export default function (eleventyConfig) {
{
property: 'type',
content: layout
},
{
property: 'image',
content: ''
},
{ property: 'image_alt',
content: ''
}
]

Expand Down
12 changes: 1 addition & 11 deletions packages/11ty/_includes/components/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,6 @@ export default function (eleventyConfig) {
const icon = eleventyConfig.getFilter('icon')
return (params) => {
return html`
<template id="js-search-results-template">
<li class="quire-search__inner__list-item">
<a class="js-search-results-item" href=""><h2 class="title"><span class="js-search-results-item-title"></span></h2>
</a>
<p><span class="js-search-results-item-type"></span> | <span class="js-search-results-item-length"></span> words</p>
</li>
</template>

<div
aria-expanded="false"
class="quire-search"
Expand All @@ -44,9 +36,7 @@ export default function (eleventyConfig) {
/>
<span>${icon({ type: 'search', description: 'Search' })}</span>
</div>
<ul class="quire-search__inner__list" id="js-search-results-list">
<!-- js-search-results-template -->
</ul>
<q-search-results-list id="js-search-results-list"></q-search-results-list>
</div>
</div>
</section>
Expand Down
29 changes: 29 additions & 0 deletions packages/11ty/_includes/web-components/search/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# q-search-results-list

## Overview

The `q-search-results-list` component displays and manages search results using Pagefind search integration.

## Usage

```html
<q-search-results-list query="search term"></q-search-results-list>
```

### Attributes

The component takes a `query` attribute with the search query string to search for in a Pagefind index.

### Styling

- `.search-list` - Results container
- `.search-result` - Individual result item
- `.search-subresults` - Sub-results list

- `.result-title` - Result header area
- `.result-link` - Result links
- `.result-item` - Content container
- `.result-item-image` - Image container
- `.result-item-content` - Text content area
- `.result-meta` - Metadata paragraphs
- `.result-excerpt` - Excerpt text
176 changes: 176 additions & 0 deletions packages/11ty/_includes/web-components/search/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { LitElement, html } from 'lit'
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'
import { searchResultsListStyles } from './styles.js'

/**
* @type {Object|null} Global reference to Pagefind search functionality
* Lazily loaded when search is first performed
*/
let PAGEFIND_GLOBAL

/**
* @class SearchResultsList
* @extends LitElement
* @description A reactive Lit element for displaying and interacting with pagefind search results.
* Handles search queries, result fetching, and rendering of structured search results with metadata.
*
* @property {string} query - The current search query string
* @property {Array<Object>} results - Array of search result objects from Pagefind (internal state, not reflected as attribute)
*/
class SearchResultsList extends LitElement {
static properties = {
query: { type: String },
results: { type: Array, attribute: false }
}

static styles = [searchResultsListStyles]

constructor () {
super()
this.results = []
this.query = ''
}

connectedCallback () {
super.connectedCallback()
}

willUpdate (changedProperties) {
if (changedProperties.has('query')) {
this.updateResults(this.query)
}
}

/**
* updateResults
* @description Performs search using Pagefind and updates the results array.
* @param {string} [query=this.query] - The search query string to execute
* @returns {Promise<void>} Promise that resolves when results are updated
*/
async updateResults (query = this.query) {
if (!PAGEFIND_GLOBAL) {
PAGEFIND_GLOBAL = await import('../../../_search/pagefind.js')
}
const search = await PAGEFIND_GLOBAL.debouncedSearch(query)
if (!search) return

const resultsData = search.results.map(async rawResult => rawResult.data())
this.results = await Promise.all(resultsData)
}

/**
* resultHeaderTemplate
* @description Renders the header section of a search result with title and link
* @param {Object} result - Search result object from Pagefind
* @returns {TemplateResult} Lit HTML template for the result header
*/
resultHeaderTemplate (result) {
return html`
<div class="result-title">
<a class="result-link" href="${result.url}" @click="${window.toggleSearch}">
${result.meta.title}
</a>
</div>
`
}

/**
* resultContentTemplate
* @description Renders the main content section of a search result including image, metadata, and excerpt
* @param {Object} result - Search result object from Pagefind
* @returns {TemplateResult} Lit HTML template for the result content
*/
resultContentTemplate (result) {
return html`
<div class="result-item">
${this.resultImageTemplate(result)}
<div class="result-item-content">
${this.resultMetaTemplate(result)}
<p class="result-excerpt">${unsafeHTML(result.excerpt)}</p>
</div>
</div>
`
}

/**
* resultImageTemplate
* @description Renders the image section for a search result if an image is available
* @param {Object} result - Search result object from Pagefind
* @param {Object} result.meta - Metadata object
* @returns {TemplateResult|string} Lit HTML template for the image or empty string if no image
*/
resultImageTemplate (result) {
if (!result.meta.image) return ''
return html`
<div class="result-item-image">
<img src="${result.meta.image}" alt="${result.meta.image_alt}">
</div>
`
}

/**
* subResultsTemplate
* @description Renders sub-results for a search result and filters out sub-results are repeated.
* @param {Object} [params={}]
* @param {Array<Object>} [params.sub_results] - Array of sub-result objects
* @param {Object} [params.meta]
* @returns {TemplateResult|string} Lit HTML template for sub-results or empty string if none
*/
subResultsTemplate ({ sub_results: subresults, meta } = {}) {
const filteredResults = (subresults).filter(subitem => subitem.title !== meta.title)
if (filteredResults.length === 0) return ''
return html`
<ol class="search-subresults">
${filteredResults.map(subitem => html`
<li class="subresults-item">
<a class="result-link" href="${subitem.url}" @click="${window.toggleSearch}">
${subitem.title}
</a>
<p class="result-excerpt">${unsafeHTML(subitem.excerpt)}</p>
</li>
`)}
</ol>
`
}

/**
* resultMetaTemplate
* @description Renders metadata information for a search result including page title, contributors, and credits
* @param {Object} result - Search result object from Pagefind
* @param {Object} result.meta - Metadata object
* @param {string} [result.meta.pageTitle] - Page title metadata
* @param {string} [result.meta.contributors] - Contributors metadata
* @param {string} [result.meta.credit] - Credit metadata
* @returns {TemplateResult} Lit HTML template for the result metadata
*/
resultMetaTemplate (result) {
return html`
${result.meta.pageTitle ? html`<p class="result-meta">${result.meta.pageTitle}</p>` : ''}
${result.meta.contributors ? html`<p class="result-meta">${result.meta.contributors}</p>` : ''}
${result.meta.credit ? html`<p class="result-meta">${result.meta.credit}</p>` : ''}
`
}

/**
* resultTemplate
* @description Renders a complete search result item by combining header, content, and sub-results
* @param {Object} result - Search result object from Pagefind
* @returns {TemplateResult} Lit HTML template for the complete result item
*/
resultTemplate (result) {
return html`
<li class="search-result">
${this.resultHeaderTemplate(result)}
${this.resultContentTemplate(result)}
${this.subResultsTemplate(result)}
</li>`
}

render () {
return html`<ol class="search-list">
${this.results.map(result => this.resultTemplate(result))}
</ol>`
}
}

customElements.define('q-search-results-list', SearchResultsList)
87 changes: 87 additions & 0 deletions packages/11ty/_includes/web-components/search/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { css } from 'lit'

export const searchResultsListStyles = css`
:host {

}

ol {
list-style: none;
padding: 0;
margin: 0;
}

li {
font-family: var(--quire-primary-font, 'Noto Sans', sans-serif);
margin: 1.5rem 0 0 0;
}

.result-link {
width: fit-content;
color: var(--accent-color, #CB3434);
border-bottom: 1px dotted var(--accent-color, #CB3434);
letter-spacing: 0px;
text-decoration: none;
&:hover {
border-bottom: 1px solid var(--accent-color-hover, #a02a2a);
}
& * {
color: var(--accent-color, #CB3434);
}
}

.result-title {
font-size: 1.25rem;
line-height: 1.4;
font-family: var(--quire-headings-font, 'IBM Plex Sans Condensed', sans-serif);
text-transform: none;
margin-bottom: .25em;
}

.result-meta {
font-style: italic;
}

.result-item {
display: flex;
align-items: flex-start;
}

.result-item-content {
flex: 1;
}

.result-item-content p {
margin: 0 0 0.5rem 0;
}

.result-excerpt {
margin: 0;
}

.result-item-image {
margin-right: 1rem;
margin-top: 0.25rem;
}

.result-item-image img {
object-fit: contain;
max-height: 6rem;
}

.subresults-item {
margin: 1rem 0 0 0;
}

.subresults-item .result-link {
display: block;
margin-bottom: 0.25rem;
}

mark {
background-color:#ff0;
border-radius:2px;
padding:0 2px;
color:#000
}
`
2 changes: 1 addition & 1 deletion packages/11ty/_plugins/search/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const SEARCH_INDEX_DIR = '_search'
*
*/
export default function (eleventyConfig, collections, {
indexFigures = false,
indexFigures = true,
excludeSelectors = [],
searchIndexDir = SEARCH_INDEX_DIR
} = {}) {
Expand Down
4 changes: 2 additions & 2 deletions packages/11ty/_plugins/search/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default class SearchIndex {
* @returns {Promise<void>}
*/
async addFigureRecord ({ figureData, canonicalURL, title } = {}) {
const { id, caption, alt, src, thumbnail, label, credit, mediaType } = figureData
const { id, caption, alt, src, label, credit, mediaType } = figureData
const markdownify = this.eleventyConfig.getFilter('markdownify')
const removeHTML = this.eleventyConfig.getFilter('removeHTML')

Expand All @@ -103,7 +103,7 @@ export default class SearchIndex {
meta: {
title: label,
pageTitle: title,
image: thumbnail || this.assetSrc(src) || '',
image: this.assetSrc(src) || '',
image_alt: alt || '',
credit,
type: mediaType
Expand Down
2 changes: 1 addition & 1 deletion packages/11ty/_plugins/shortcodes/figure.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default function (eleventyConfig) {
}

return oneLine`
<figure id="${slugify(id)}" class="${['q-figure', 'q-figure--' + mediaType, ...classes].join(' ')}">
<figure id="${slugify(id)}" class="${['q-figure', 'q-figure--' + mediaType, ...classes].join(' ')}" data-pagefind-ignore="all">
${await component({ ...figure, lazyLoading })}
</figure>
`
Expand Down
Loading