Upstream Pagefind/pagefind state of the touched files before a8baf2727f

This commit is contained in:
upstream committed 2026-10-03 01:33:54 +02:00
1 parent a114271432
commit ed42ec389b
4 files changed
+590

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
---
title: "<pagefind-results>"
nav_title: "<pagefind-results>"
nav_section: Components
weight: 20
---
Displays search results with automatic lazy loading. Results load their full data only when scrolled into view.
<div class="demo-box demo-box-attached" style="flex-direction: column; gap: 1rem; height: 400px;">
<pagefind-input instance="results-demo"></pagefind-input>
<div class="results-scroll" style="flex: 1; overflow-y: auto; width: 100%; min-height: 0;">
<pagefind-results instance="results-demo"></pagefind-results>
</div>
<pagefind-keyboard-hints instance="results-demo"></pagefind-keyboard-hints>
</div>
```html
<pagefind-results></pagefind-results>
```
## Attributes
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `show-images` | boolean | `false` | Show/hide result images |
| `hide-sub-results` | boolean | `false` | Hide anchor-based sub-results |
| `max-sub-results` | number | `3` | Maximum number of sub-results per result |
| `max-results` | number | unlimited | Limit number of results shown |
| `link-target` | string | — | Set `target` attribute on result links (e.g., `_blank` for new tab) |
| `instance` | string | `"default"` | Connect to a specific Pagefind instance |
## Keyboard Navigation
When results are focused, use these keys to navigate:
| Key | Action |
|-----|--------|
| `↓` / `↑` | Navigate between results |
| `Enter` | Follow the selected link |
| `Backspace` | Focus input and delete last character |
| `/` | Focus the search input |
| Any letter | Focus input and type the character |
## Custom Templates
Customize result rendering using `<script type="text/pagefind-template">` elements inside the component. Templates use a simple syntax with variables, conditionals, and loops.
### Basic Template
```html
<pagefind-results>
<script type="text/pagefind-template">
<li class="my-result">
<h3>{{ meta.title }}</h3>
<a href="{{ url | safeUrl }}">{{ url }}</a>
<p>{{+ excerpt +}}</p>
</li>
</script>
</pagefind-results>
```
Your template must have a single root element (e.g. a single `<li>`). Keyboard navigation moves between `<a>` elements in the results list, so ensure your main result link is an `<a>` tag.
The full templating syntax can be seen in the [adequate-little-templates](https://github.com/bglw/adequate-little-templates?tab=readme-ov-file#syntax) package.
### Available Data
Templates receive this data structure:
| Field | Description |
|-------|-------------|
| `meta.title` | Page title |
| `meta.url` | Page URL |
| `meta.image` | Page image (if set) |
| `meta.image_alt` | Image alt text |
| `meta.*` | Any custom metadata |
| `excerpt` | Search excerpt with `<mark>` highlighting (use `{{+ excerpt +}}`) |
| `url` | Fallback URL |
| `sub_results` | Array of matching sections within the page |
| `options.*` | The attributes supplied to the `<pagefind-results>` element |
Each sub-result has: `title`, `url`, `excerpt`.
### Template Filters
The template engine provides [built-in filters](https://github.com/bglw/adequate-little-templates?tab=readme-ov-file#syntax). The Component UI registers the following additional filters:
#### `resolveUrl(pageUrl)`
Resolves a relative URL against a page URL. Absolute URLs are returned unchanged.
```
"images/hero.png" | resolveUrl("/blog/post/") → /blog/post/images/hero.png
```
### Placeholder Template
While results are loading, a skeleton placeholder is shown. You can customize it with a second template using `data-template="placeholder"`, as shown in the full example below. The placeholder template receives no data — it is static HTML.
### Full Template Example
Here's the built-in template. Copy and customize it:
```html
<pagefind-results>
<script type="text/pagefind-template">
<li class="pf-result">
<div class="pf-result-card">
{{#if and(options.show_images, meta.image)}}
<img class="pf-result-image" src="{{ meta.image | resolveUrl(meta.url | default(url)) }}" alt="{{ meta.image_alt | default(meta.title) }}">
{{/if}}
<div class="pf-result-content">
<p class="pf-result-title">
<a class="pf-result-link" href="{{ meta.url | default(url) | safeUrl }}"{{#if options.link_target}} target="{{ options.link_target }}"{{/if}}{{#if eq(options.link_target, "_blank")}} rel="noopener"{{/if}}>{{ meta.title }}</a>
</p>
{{#if excerpt}}
<p class="pf-result-excerpt">{{+ excerpt +}}</p>
{{/if}}
</div>
</div>
{{#if sub_results}}
<ul class="pf-heading-chips">
{{#each sub_results as sub}}
<li class="pf-heading-chip">
<a class="pf-heading-link" href="{{ sub.url | safeUrl }}"{{#if options.link_target}} target="{{ options.link_target }}"{{/if}}{{#if eq(options.link_target, "_blank")}} rel="noopener"{{/if}}>{{ sub.title }}</a>
<p class="pf-heading-excerpt">{{+ sub.excerpt +}}</p>
</li>
{{/each}}
</ul>
{{/if}}
</li>
</script>
<script type="text/pagefind-template" data-template="placeholder">
<li class="pf-result" aria-hidden="true">
<div class="pf-result-card">
<div class="pf-skeleton pf-skeleton-image"></div>
<div class="pf-result-content">
<p class="pf-result-title pf-skeleton pf-skeleton-title"></p>
<p class="pf-result-excerpt pf-skeleton pf-skeleton-excerpt"></p>
</div>
</div>
</li>
</script>
</pagefind-results>
```
## Results Layout
By default, results display in a single column. You can change the layout of the results container using CSS variables, for example a grid layout:
```css
:root {
--pf-results-display: grid;
--pf-results-columns: repeat(auto-fill, minmax(250px, 1fr));
--pf-results-gap: 16px;
}
```
See [CSS Variables](/docs/css-variables/) for all available layout variables.
+43
View File
@@ -0,0 +1,43 @@
export { getInstanceManager, configureInstance } from "./instance-manager";
import { registerFunction } from "adequate-little-templates";
registerFunction("resolveUrl", (url, pageUrl) => {
const s = String(url ?? "");
if (!s || /^[a-z][a-z0-9+.-]*:/i.test(s) || /^\/\//.test(s) || s.startsWith("/")) return s;
try {
return new URL(s, new URL(String(pageUrl ?? "/"), "https://p")).pathname;
} catch {
return s;
}
});
import "./pagefind-config";
import "./pagefind-input";
import "./pagefind-summary";
import "./pagefind-results";
import "./pagefind-filter-pane";
import "./pagefind-filter-dropdown";
import "./pagefind-modal";
import "./pagefind-modal-trigger";
import "./pagefind-modal-header";
import "./pagefind-modal-body";
import "./pagefind-modal-footer";
import "./pagefind-keyboard-hints";
import "./pagefind-searchbox";
export { PagefindConfig } from "./pagefind-config";
export { PagefindInput } from "./pagefind-input";
export { PagefindSummary } from "./pagefind-summary";
export { PagefindResults } from "./pagefind-results";
export { PagefindFilterPane } from "./pagefind-filter-pane";
export { PagefindFilterDropdown } from "./pagefind-filter-dropdown";
export { PagefindModal } from "./pagefind-modal";
export { PagefindModalTrigger } from "./pagefind-modal-trigger";
export { PagefindModalHeader } from "./pagefind-modal-header";
export { PagefindModalBody } from "./pagefind-modal-body";
export { PagefindModalFooter } from "./pagefind-modal-footer";
export { PagefindKeyboardHints } from "./pagefind-keyboard-hints";
export { PagefindSearchbox } from "./pagefind-searchbox";
export { PagefindElement } from "./base-element";
+176
View File
@@ -0,0 +1,176 @@
<script>
export let show_images = true;
export let process_result = null;
export let result = { data: async () => {} };
const skipMeta = ["title", "image", "image_alt", "url"];
let data;
let meta = [];
const resolveImageUrl = (src, pageUrl) => {
if (!src || /^[a-z][a-z0-9+.-]*:/i.test(src) || /^\/\//.test(src) || src.startsWith("/")) return src;
try {
return new URL(src, new URL(pageUrl || "/", "https://p")).pathname;
} catch {
return src;
}
};
const load = async (r) => {
data = await r.data();
data = process_result?.(data) ?? data;
if (data.meta?.image) {
data = { ...data, meta: { ...data.meta, image: resolveImageUrl(data.meta.image, data.meta.url || data.url) } };
}
meta = Object.entries(data.meta).filter(
([key]) => !skipMeta.includes(key),
);
};
$: load(result);
const placeholder = (max = 30) => {
return ". ".repeat(Math.floor(10 + Math.random() * max));
};
</script>
<li class="pagefind-ui__result">
{#if data}
{#if show_images}
<div class="pagefind-ui__result-thumb">
{#if data.meta.image}
<img
class="pagefind-ui__result-image"
src={data.meta?.image}
alt={data.meta?.image_alt || data.meta?.title}
/>
{/if}
</div>
{/if}
<div class="pagefind-ui__result-inner">
<p class="pagefind-ui__result-title">
<a
class="pagefind-ui__result-link"
href={data.meta?.url || data.url}>{data.meta?.title}</a
>
</p>
<p class="pagefind-ui__result-excerpt">{@html data.excerpt}</p>
{#if meta.length}
<ul class="pagefind-ui__result-tags">
{#each meta as [metaTitle, metaValue]}
<li
class="pagefind-ui__result-tag"
data-pagefind-ui-meta={metaTitle}
>
{metaTitle.replace(/^(\w)/, (c) =>
c.toLocaleUpperCase(),
)}: {metaValue}
</li>
{/each}
</ul>
{/if}
</div>
{:else}
{#if show_images}
<div class="pagefind-ui__result-thumb pagefind-ui__loading" />
{/if}
<div class="pagefind-ui__result-inner">
<p class="pagefind-ui__result-title pagefind-ui__loading">
{placeholder(30)}
</p>
<p class="pagefind-ui__result-excerpt pagefind-ui__loading">
{placeholder(40)}
</p>
</div>
{/if}
</li>
<style>
.pagefind-ui__result {
list-style-type: none;
display: flex;
align-items: flex-start;
gap: min(calc(40px * var(--pagefind-ui-scale)), 3%);
padding: calc(30px * var(--pagefind-ui-scale)) 0
calc(40px * var(--pagefind-ui-scale));
border-top: solid var(--pagefind-ui-border-width)
var(--pagefind-ui-border);
}
.pagefind-ui__result:last-of-type {
border-bottom: solid var(--pagefind-ui-border-width)
var(--pagefind-ui-border);
}
.pagefind-ui__result-thumb {
width: min(
30%,
calc((30% - (100px * var(--pagefind-ui-scale))) * 100000)
);
max-width: calc(120px * var(--pagefind-ui-scale));
margin-top: calc(10px * var(--pagefind-ui-scale));
aspect-ratio: var(--pagefind-ui-image-box-ratio);
position: relative;
}
.pagefind-ui__result-image {
display: block;
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 0;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
border-radius: var(--pagefind-ui-image-border-radius);
}
.pagefind-ui__result-inner {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: calc(10px * var(--pagefind-ui-scale));
}
.pagefind-ui__result-title {
display: inline-block;
font-weight: 700;
font-size: calc(21px * var(--pagefind-ui-scale));
margin-top: 0;
margin-bottom: 0;
}
.pagefind-ui__result-title .pagefind-ui__result-link {
color: var(--pagefind-ui-text);
text-decoration: none;
}
.pagefind-ui__result-title .pagefind-ui__result-link:hover {
text-decoration: underline;
}
.pagefind-ui__result-excerpt {
display: inline-block;
font-weight: 400;
font-size: calc(16px * var(--pagefind-ui-scale));
margin-top: calc(4px * var(--pagefind-ui-scale));
margin-bottom: 0;
min-width: calc(250px * var(--pagefind-ui-scale));
}
.pagefind-ui__loading {
color: var(--pagefind-ui-text);
background-color: var(--pagefind-ui-text);
border-radius: var(--pagefind-ui-border-radius);
opacity: 0.1;
pointer-events: none;
}
.pagefind-ui__result-tags {
list-style-type: none;
padding: 0;
display: flex;
gap: calc(20px * var(--pagefind-ui-scale));
flex-wrap: wrap;
margin-top: calc(20px * var(--pagefind-ui-scale));
}
.pagefind-ui__result-tag {
padding: calc(4px * var(--pagefind-ui-scale))
calc(8px * var(--pagefind-ui-scale));
font-size: calc(14px * var(--pagefind-ui-scale));
border-radius: var(--pagefind-ui-border-radius);
background-color: var(--pagefind-ui-tag);
}
</style>
@@ -0,0 +1,211 @@
<script>
export let show_images = true;
export let process_result = null;
export let result = { data: async () => {} };
const skipMeta = ["title", "image", "image_alt", "url"];
let data;
let meta = [];
let non_root_sub_results = [];
let has_root_sub_result = false;
const thin_sub_results = (results, limit) => {
if (results.length <= limit) {
return results;
}
const top_results = [...results]
.sort((a, b) => b.locations.length - a.locations.length)
.slice(0, 3)
.map((r) => r.url);
return results.filter((r) => top_results.includes(r.url));
};
const load = async (r) => {
data = await r.data();
data = process_result?.(data) ?? data;
meta = Object.entries(data.meta).filter(([key]) => !skipMeta.includes(key));
if (Array.isArray(data.sub_results)) {
has_root_sub_result =
data.sub_results?.[0]?.url === (data.meta?.url || data.url);
if (has_root_sub_result) {
non_root_sub_results = thin_sub_results(data.sub_results.slice(1), 3);
} else {
non_root_sub_results = thin_sub_results([...data.sub_results], 3);
}
}
};
$: load(result);
const placeholder = (max = 30) => {
return ". ".repeat(Math.floor(10 + Math.random() * max));
};
</script>
<li class="pagefind-ui__result">
{#if data}
{#if show_images}
<div class="pagefind-ui__result-thumb">
{#if data.meta.image}
<img
class="pagefind-ui__result-image"
src={data.meta?.image}
alt={data.meta?.image_alt || data.meta?.title}
/>
{/if}
</div>
{/if}
<div class="pagefind-ui__result-inner">
<p class="pagefind-ui__result-title">
<a class="pagefind-ui__result-link" href={data.meta?.url || data.url}
>{data.meta?.title}</a
>
</p>
{#if has_root_sub_result}
<p class="pagefind-ui__result-excerpt">{@html data.excerpt}</p>
{/if}
{#each non_root_sub_results as subres}
<div class="pagefind-ui__result-nested">
<p class="pagefind-ui__result-title">
<a class="pagefind-ui__result-link" href={subres.url}
>{subres.title}</a
>
</p>
<p class="pagefind-ui__result-excerpt">{@html subres.excerpt}</p>
</div>
{/each}
{#if meta.length}
<ul class="pagefind-ui__result-tags">
{#each meta as [metaTitle, metaValue]}
<li
class="pagefind-ui__result-tag"
data-pagefind-ui-meta={metaTitle}
>
{metaTitle.replace(/^(\w)/, (c) => c.toLocaleUpperCase())}: {metaValue}
</li>
{/each}
</ul>
{/if}
</div>
{:else}
{#if show_images}
<div class="pagefind-ui__result-thumb pagefind-ui__loading" />
{/if}
<div class="pagefind-ui__result-inner">
<p class="pagefind-ui__result-title pagefind-ui__loading">
{placeholder(30)}
</p>
<p class="pagefind-ui__result-excerpt pagefind-ui__loading">
{placeholder(40)}
</p>
</div>
{/if}
</li>
<style>
.pagefind-ui__result {
list-style-type: none;
display: flex;
align-items: flex-start;
gap: min(calc(40px * var(--pagefind-ui-scale)), 3%);
padding: calc(30px * var(--pagefind-ui-scale)) 0
calc(40px * var(--pagefind-ui-scale));
border-top: solid var(--pagefind-ui-border-width) var(--pagefind-ui-border);
}
.pagefind-ui__result:last-of-type {
border-bottom: solid var(--pagefind-ui-border-width)
var(--pagefind-ui-border);
}
.pagefind-ui__result-nested {
display: flex;
flex-direction: column;
padding-left: calc(20px * var(--pagefind-ui-scale));
}
.pagefind-ui__result-nested:first-of-type {
padding-top: calc(10px * var(--pagefind-ui-scale));
}
.pagefind-ui__result-nested .pagefind-ui__result-link {
font-size: 0.9em;
position: relative;
}
.pagefind-ui__result-nested .pagefind-ui__result-link::before {
content: "⤷ ";
position: absolute;
top: 0;
right: calc(100% + 0.1em);
}
.pagefind-ui__result-thumb {
width: min(30%, calc((30% - (100px * var(--pagefind-ui-scale))) * 100000));
max-width: calc(120px * var(--pagefind-ui-scale));
margin-top: calc(10px * var(--pagefind-ui-scale));
aspect-ratio: var(--pagefind-ui-image-box-ratio);
position: relative;
}
.pagefind-ui__result-image {
display: block;
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 0;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
border-radius: var(--pagefind-ui-image-border-radius);
}
.pagefind-ui__result-inner {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
margin-top: calc(10px * var(--pagefind-ui-scale));
}
.pagefind-ui__result-title {
display: inline-block;
font-weight: 700;
font-size: calc(21px * var(--pagefind-ui-scale));
margin-top: 0;
margin-bottom: 0;
}
.pagefind-ui__result-title .pagefind-ui__result-link {
color: var(--pagefind-ui-text);
text-decoration: none;
}
.pagefind-ui__result-title .pagefind-ui__result-link:hover {
text-decoration: underline;
}
.pagefind-ui__result-excerpt {
display: inline-block;
font-weight: 400;
font-size: calc(16px * var(--pagefind-ui-scale));
margin-top: calc(4px * var(--pagefind-ui-scale));
margin-bottom: 0;
min-width: calc(250px * var(--pagefind-ui-scale));
}
.pagefind-ui__loading {
color: var(--pagefind-ui-text);
background-color: var(--pagefind-ui-text);
border-radius: var(--pagefind-ui-border-radius);
opacity: 0.1;
pointer-events: none;
}
.pagefind-ui__result-tags {
list-style-type: none;
padding: 0;
display: flex;
gap: calc(20px * var(--pagefind-ui-scale));
flex-wrap: wrap;
margin-top: calc(20px * var(--pagefind-ui-scale));
}
.pagefind-ui__result-tag {
padding: calc(4px * var(--pagefind-ui-scale))
calc(8px * var(--pagefind-ui-scale));
font-size: calc(14px * var(--pagefind-ui-scale));
border-radius: var(--pagefind-ui-border-radius);
background-color: var(--pagefind-ui-tag);
}
</style>