Skip to content

Commit 19b34fe

Browse files
Add PageFind beta V2 (Full Text Search across Site) (#2857)
* Copy gerteck's pagefind implementation * Add initial agolia styling & remove redundant code * Implement temporary url fix * Add arrow key navigation support * Add auto highlight first result * Add hover focus support * Increase results limit to 100 * Support pagefind.json configuration file * Change logic of how url is fixed * Remove pagefind.json support & Add site.json support * Add glob configuration support for pagefind in site.json This added support for configuring Pagefind search indexing via site.json is aimed towards allowing users to declare glob patterns to customize what pages in which directory should be indexed by pagefind. * Add support for multiple glob patterns * Update userguide and remove incomplete configuration options Initially, I felt that including the root_selector option and force_language option would be useful for pagefind configuration options. But perhaps for now it isn't the most pressing issue so I'm going to exlcude it from the current implementation of pagefind. * Update styling to be more like cards * Remove pagefind configuration from site.json in docs * Add error handling for indexSiteWithPagefind() This helps resolve an issue with generate() test case within Site.functional.test.ts. Namely, because Jest's cannot intercept eval('import("pagefind")') runtime dynamic import used by pagefind. Hence, for now at least, the error handling would catch this error and allow the site generation to continue normally. * Resolve lint errors * Update tests * Add tests for SiteGenerationManager * Create Search.spec.js tests * Fix minor lint issue * Exclude pagefind generated files from file count & comparison * Ignore pagefind directory * Update directory to ignore * Code quality improvements for Search.vue Extracted processResult into its own helper function. Added rel="noopener noreferrer" attribute for improved security and privacy when being redirected to pagefind's github repo. * Add validation for glob patterns * Allow absolute path as valid * Fix search icon svg issue * Fix lint issue * Resolve issues raised in Search.vue * Update pagefindCss/Js to injected only if index succeeds Now indexSiteWithPagefind returns a boolean which is then determines if there is a need to inject the pagefindcss & pagefindJs. * Update documentation * Add support for windows glob pattern Since isValidGlobPattern is only used by normalizeGlobPattern, changed it to private access. Now normalization of pattern happens solely in normalizeGlobPattern and validation is purely a validation method. Better separation of concerns. Also resolved a bug where the 404s required pages to be created before knowing if indexing succeeds by defaulting index success to true and only if it fails does the flag get set to false. * Fix typo in search.css * Update docs * Resolve merge conflicts * Use static import for pagefind * Remove uncessary methods
1 parent 64c1dca commit 19b34fe

File tree

132 files changed

+2154
-11
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

132 files changed

+2154
-11
lines changed

.eslintignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ node_modules
66

77
packages/cli/src/lib/live-server/*
88

9+
packages/cli/test/**/pagefind/**/*.js
10+
911
# --- packages/core ---
1012

1113
# Ignore JS files that are compiled from TS

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/* eslint quotes: ["error", "double"] */
33

44
module.exports = {
5+
"ignorePatterns": ["docs/_site/**", "**/dist/**", "**/node_modules/**"],
56
"env": {
67
"node": true,
78
"es6": true,

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ packages/core/template/*/_site
4646
# Generated site (MarkBind)
4747
packages/cli/test/functional/*/_site
4848

49+
# Generated pagefind directories for sites and in expected
50+
packages/cli/test/functional/**/pagefind
51+
4952
# Ignore .page-vue-render.js files in functional test and subdirectories on update
5053
packages/cli/test/functional/**/*.page-vue-render.js
5154

@@ -117,6 +120,9 @@ packages/core/src/lib/markdown-it/patches/**/*.js
117120
.nx/cache
118121
.nx/workspace-data
119122

123+
# Pagefind fragments
124+
*.pf_fragment
125+
120126
# AI Tools directories (symlinks)
121127
.opencode
122128
.cline

.stylelintrc.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,16 @@ module.exports = {
66
// MarkBind generates some blank CSS files when initialising a site,
77
// which violates the no-empty-source rule
88
"no-empty-source": null
9-
}
9+
},
10+
"overrides": [
11+
{
12+
// pagefind uses BEM-style class names (e.g., .pagefind-ui__result) as default.
13+
// Since we currently style pagefind's default UI classes, we need to ignore the kebab-case rule here.
14+
// This override should be removed once we no longer rely on pagefind's default CSS classes.
15+
"files": ["**/pagefindSearchBar/**"],
16+
"rules": {
17+
"selector-class-pattern": null
18+
}
19+
}
20+
]
1021
};

docs/userGuide/makingTheSiteSearchable.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,112 @@ You can add a search bar component to your website to allow users to search the
3737
<include src="syntax/keywords.md" />
3838
<include src="syntax/indexing.md" />
3939

40+
41+
42+
43+
44+
## Using Pagefind (Beta)
45+
46+
47+
MarkBind now supports [Pagefind](https://pagefind.app/), a static low-bandwidth search library, as a built-in feature. This provides full-text search capabilities without external services.
48+
49+
<box type="info">
50+
This is a <strong>beta</strong> feature and will be refined in future updates. To use it, you must have <code>enableSearch: true</code> in your <code>site.json</code> (this is the default).
51+
</box>
52+
53+
<box type="warning">
54+
The Pagefind index is currently only generated during a full site build (e.g., <code>markbind build</code>). It will <strong>not</strong> repeatedly update during live reload (<code>markbind serve</code>) when you modify pages. You must restart the server (re-run <code>markbind serve</code>) or rebuild to refresh the search index.
55+
</box>
56+
57+
To add the Pagefind search bar to your page, simply insert the following element where you want it to appear:
58+
59+
```md
60+
<search />
61+
```
62+
63+
The following UI will be rendered, which is provided by Pagefind:
64+
65+
<search />
66+
67+
<br>
68+
69+
### Ignoring Individual Elements from Pagefind Search
70+
71+
You can exclude specific elements from the search index by adding the `data-pagefind-ignore` attribute to them:
72+
73+
```html
74+
<div>
75+
<h1>This content will be in your search index.</h1>
76+
<div data-pagefind-ignore>
77+
This content and all its children will be excluded from search.
78+
</div>
79+
</div>
80+
```
81+
82+
For more details, see the [Pagefind documentation on removing individual elements](https://pagefind.app/docs/indexing/#removing-individual-elements-from-the-index).
83+
84+
### Using Pagefind Configuration
85+
86+
You can customize Pagefind's indexing behavior by adding a `pagefind` configuration in your `site.json`. This allows you to control which content is indexed and how search works.
87+
88+
#### Excluding Content from Search Index
89+
90+
You can use the `exclude_selectors` option to exclude specific elements from the search index. This is useful if you are migrating from Algolia and want to reuse your existing CSS class selectors.
91+
92+
In your `site.json`:
93+
94+
```json
95+
{
96+
"pagefind": {
97+
"exclude_selectors": [".algolia-no-index", "[class*='algolia-no-index']"]
98+
}
99+
}
100+
```
101+
102+
This tells Pagefind to exclude any element with the `algolia-no-index` class (or containing it in a space-separated list) from the search index, similar to using `data-pagefind-ignore`.
103+
104+
#### Limiting Which Pages Are Searchable
105+
106+
You can use the `glob` option to limit which pages are indexed by Pagefind. This is useful when you want search results to only show pages from specific sections of your site.
107+
108+
In your `site.json`:
109+
110+
```json
111+
{
112+
"pagefind": {
113+
"glob": [
114+
"devGuide",
115+
"userGuide/*"
116+
]
117+
}
118+
}
119+
```
120+
121+
MarkBind supports glob patterns and will automatically append `.html` to your patterns if not specified. For example:
122+
- `"devGuide"` becomes `"devGuide/**/*.html"`
123+
- `"devGuide/*"` becomes `"devGuide/*.html"`
124+
- `"**/devGuide/**"` becomes `"**/devGuide/**/*.html"`
125+
- `"*.html"` remains `"*.html"` (no change needed)
126+
127+
Only pages matching these glob patterns will appear in search results. This can be particularly useful for:
128+
- Multi-site setups where you want to search only specific sections
129+
- Including only certain directories from search results
130+
131+
For more details on glob patterns, see the [Pagefind documentation](https://pagefind.app/docs/config-options/#glob).
132+
133+
<panel header="Potential Future Enhancements">
134+
135+
Additional Pagefind configuration options may be supported in future releases:
136+
137+
- **`root_selector`**: Allows specifying a custom root element for indexing (default: `html`). Useful for sites with specific content containers.
138+
- **`force_language`**: Forces a specific language for indexing (e.g., `"en"`, `"pt"`). Improves search accuracy for multilingual sites.
139+
140+
</panel>
141+
142+
143+
144+
<br>
145+
40146
## Using External Search Services
41147

42148
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Unlike the built-in search, Algolia provides full-text search. See the panel below for more info.

package-lock.json

Lines changed: 96 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/test/functional/test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,14 @@ expectedErrors.forEach((error, index) => {
5050
logger.info(`${index + 1}: ${error}`);
5151
});
5252

53+
const GENERATED_DIRECTORIES_TO_IGNORE = ['**/pagefind/**'];
54+
5355
testSites.forEach((siteName) => {
5456
console.log(`Running ${siteName} tests`);
5557
try {
5658
execSync(`node ${CLI_PATH} build ${siteName}`, execOptions);
5759
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
58-
compare(siteName, 'expected', '_site', siteIgnoredFiles);
60+
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
5961
} catch (err) {
6062
if (_.isError(err)) {
6163
printFailedMessage(err, siteName);
@@ -74,7 +76,8 @@ testConvertSites.forEach((sitePath) => {
7476
execSync(`node ${CLI_PATH} init ${nonMarkBindSitePath} -c`, execOptions);
7577
execSync(`node ${CLI_PATH} build ${nonMarkBindSitePath}`, execOptions);
7678
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
77-
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
79+
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
80+
GENERATED_DIRECTORIES_TO_IGNORE);
7881
} catch (err) {
7982
if (_.isError(err)) {
8083
printFailedMessage(err, sitePath);
@@ -98,7 +101,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
98101
execSync(`node ${CLI_PATH} init ${siteCreationTempPath} --template ${flag}`, execOptions);
99102
execSync(`node ${CLI_PATH} build ${siteCreationTempPath}`, execOptions);
100103
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
101-
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
104+
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
102105
} catch (err) {
103106
if (_.isError(err)) {
104107
printFailedMessage(err, sitePath);
@@ -141,7 +144,7 @@ function testEmptyDirectoryBuild() {
141144
} catch (err) {
142145
// Verify that test_empty directory remains empty using compare()
143146
try {
144-
compare(siteRootName, 'expected', 'empty_dir', [], true);
147+
compare(siteRootName, 'expected', 'empty_dir', [], [], true);
145148
} catch (compareErr) {
146149
if (_.isError(compareErr)) {
147150
printFailedMessage(compareErr, siteRootName);

packages/cli/test/functional/testUtil/compare.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ const TEST_BLACKLIST = ignore().add([
1313
'*.log',
1414
'*.woff',
1515
'*.woff2',
16+
'*.pf_fragment',
17+
'*.pf_index',
18+
'*.pf_meta',
19+
'*.wasm.pagefind',
20+
'wasm.unknown.pagefind',
1621
]);
1722

1823
const CRLF_REGEX = /\r\n/g;
@@ -54,10 +59,12 @@ function getDirectoryStructure(dirPath: string) {
5459
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
5560
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
5661
* @param {string[]} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
62+
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
5763
* @param {boolean} compareDirectories - Whether to compare directory structures (default: false)
5864
*/
5965
function compare(root: string, expectedSiteRelativePath = 'expected', siteRelativePath = '_site',
60-
ignoredPaths: string[] = [], compareDirectories = false) {
66+
ignoredPaths: string[] = [], ignoredDirectories: string[] = [],
67+
compareDirectories = false) {
6168
const expectedDirectory = path.join(root, expectedSiteRelativePath);
6269
const actualDirectory = path.join(root, siteRelativePath);
6370

@@ -73,8 +80,9 @@ function compare(root: string, expectedSiteRelativePath = 'expected', siteRelati
7380
}
7481
}
7582

76-
let expectedPaths = walkSync(expectedDirectory, { directories: false });
77-
let actualPaths = walkSync(actualDirectory, { directories: false });
83+
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
84+
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
85+
let actualPaths = walkSync(actualDirectory, walkSyncOptions);
7886

7987
// Vue render JS files (*.page-vue-render.js) are not committed to version control,
8088
// so we exclude them from the comparison to avoid false positive diffs.

packages/cli/test/functional/test_site/expected/bugs/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
1717
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
1818
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
19+
<link rel="stylesheet" href="/test_site/markbind/pagefind/pagefind-ui.css">
1920
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
2021
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
2122
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
@@ -359,5 +360,6 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
359360
});
360361

361362
</script>
363+
<script src="/test_site/markbind/pagefind/pagefind-ui.js"></script>
362364

363365
</html>

packages/cli/test/functional/test_site/expected/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
1717
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
1818
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
19+
<link rel="stylesheet" href="/test_site/markbind/pagefind/pagefind-ui.css">
1920
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
2021
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
2122
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
@@ -1025,5 +1026,6 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
10251026
});
10261027

10271028
</script>
1029+
<script src="/test_site/markbind/pagefind/pagefind-ui.js"></script>
10281030

10291031
</html>

0 commit comments

Comments
 (0)