Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .eslintignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@ node_modules
*.d.ts

packages/cli/src/lib/live-server/*
packages/cli/test/**/pagefind/**/*.js


# Docs
docs/_site/

# --- packages/core ---

Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,9 @@ packages/core/template/*/_site
# Generated site (MarkBind)
packages/cli/test/functional/*/_site

# Generated pagefind directories for sites and in expected
packages/cli/test/functional/**/pagefind

# Ignore .page-vue-render.js files in functional test and subdirectories on update
packages/cli/test/functional/**/*.page-vue-render.js

Expand DownExpand Up@@ -107,6 +110,8 @@ packages/core/index.js
packages/core/src/lib/progress/*.js
# --- packages/core end ---

# Nx for Lerna
.nx/cache
.nx/workspace-data
.nx/workspace-data

# Pagefind fragments
*.pf_fragment
29 changes: 29 additions & 0 deletions docs/userGuide/makingTheSiteSearchable.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,35 @@ You can add a search bar component to your website to allow users to search the
<include src="syntax/keywords.md" />
<include src="syntax/indexing.md" />



## Using Pagefind (Beta)

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.

<box type="info">
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).
</box>

<box type="warning">
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.
</box>

To add the Pagefind search bar to your page, simply insert the following `div` where you want it to appear:

```html
<div id="pagefind-search-input"></div>
```

MarkBind will automatically inject the necessary scripts and styles to render the search UI.

The following UI will be rendered, which is provided by Pagefind:

<div id="pagefind-search-input"></div>


<br>

## Using External Search Services

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.

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that Pagefind provides "full-text search capabilities without external services" (line 44), but then on line 71, it says "Unlike the built-in search, Algolia provides full-text search." This creates confusion about whether the built-in Pagefind feature provides full-text search or not. The messaging should be clarified for consistency.

Suggested change
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.
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Algolia is a hosted service that also provides full-text search and advanced search features. See the panel below for more info.

Copilot uses AI. Check for mistakes.
Expand Down
146 changes: 146 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/cli/test/functional/test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,12 +43,14 @@ expectedErrors.forEach((error, index) => {
logger.info(`${index + 1}: ${error}`);
});

const GENERATED_DIRECTORIES_TO_IGNORE = ['pagefind'];

testSites.forEach((siteName) => {
console.log(`Running ${siteName} tests`);
try {
execSync(`node ../../index.js build ${siteName}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
compare(siteName, 'expected', '_site', siteIgnoredFiles);
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, siteName);
process.exit(1);
Expand All@@ -63,7 +65,8 @@ testConvertSites.forEach((sitePath) => {
execSync(`node ../../index.js init ${nonMarkBindSitePath} -c`, execOptions);
execSync(`node ../../index.js build ${nonMarkBindSitePath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
cleanupConvert(path.resolve(__dirname, sitePath));
Expand All@@ -83,7 +86,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
execSync(`node ../../index.js init ${siteCreationTempPath} --template ${flag}`, execOptions);
execSync(`node ../../index.js build ${siteCreationTempPath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
fs.removeSync(path.resolve(__dirname, siteCreationTempPath));
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/test/functional/testUtil/compare.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,13 +43,19 @@ function filterPageVueRenderFiles(filePaths) {
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
* @param {string} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
*/
function compare(root, expectedSiteRelativePath = 'expected', siteRelativePath = '_site', ignoredPaths = []) {
function compare(root,
expectedSiteRelativePath = 'expected',
siteRelativePath = '_site',
ignoredPaths = [],
ignoredDirectories = []) {
const expectedDirectory = path.join(root, expectedSiteRelativePath);
const actualDirectory = path.join(root, siteRelativePath);

let expectedPaths = walkSync(expectedDirectory, { directories: false });
let actualPaths = walkSync(actualDirectory, { directories: false });
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
let actualPaths = walkSync(actualDirectory, walkSyncOptions);

// Vue render JS files (*.page-vue-render.js) are not committed to version control,
// so we exclude them from the comparison to avoid false positive diffs.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/bugs/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -359,5 +360,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -1025,5 +1026,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add PageFind beta (Full Text Search across Site) by gerteck · Pull Request #2771 · MarkBind/markbind · GitHub
Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .eslintignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@ node_modules
*.d.ts

packages/cli/src/lib/live-server/*
packages/cli/test/**/pagefind/**/*.js


# Docs
docs/_site/

# --- packages/core ---

Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,9 @@ packages/core/template/*/_site
# Generated site (MarkBind)
packages/cli/test/functional/*/_site

# Generated pagefind directories for sites and in expected
packages/cli/test/functional/**/pagefind

# Ignore .page-vue-render.js files in functional test and subdirectories on update
packages/cli/test/functional/**/*.page-vue-render.js

Expand DownExpand Up@@ -107,6 +110,8 @@ packages/core/index.js
packages/core/src/lib/progress/*.js
# --- packages/core end ---

# Nx for Lerna
.nx/cache
.nx/workspace-data
.nx/workspace-data

# Pagefind fragments
*.pf_fragment
29 changes: 29 additions & 0 deletions docs/userGuide/makingTheSiteSearchable.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,35 @@ You can add a search bar component to your website to allow users to search the
<include src="syntax/keywords.md" />
<include src="syntax/indexing.md" />



## Using Pagefind (Beta)

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.

<box type="info">
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).
</box>

<box type="warning">
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.
</box>

To add the Pagefind search bar to your page, simply insert the following `div` where you want it to appear:

```html
<div id="pagefind-search-input"></div>
```

MarkBind will automatically inject the necessary scripts and styles to render the search UI.

The following UI will be rendered, which is provided by Pagefind:

<div id="pagefind-search-input"></div>


<br>

## Using External Search Services

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.

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that Pagefind provides "full-text search capabilities without external services" (line 44), but then on line 71, it says "Unlike the built-in search, Algolia provides full-text search." This creates confusion about whether the built-in Pagefind feature provides full-text search or not. The messaging should be clarified for consistency.

Suggested change
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.
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Algolia is a hosted service that also provides full-text search and advanced search features. See the panel below for more info.

Copilot uses AI. Check for mistakes.
Expand Down
146 changes: 146 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/cli/test/functional/test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,12 +43,14 @@ expectedErrors.forEach((error, index) => {
logger.info(`${index + 1}: ${error}`);
});

const GENERATED_DIRECTORIES_TO_IGNORE = ['pagefind'];

testSites.forEach((siteName) => {
console.log(`Running ${siteName} tests`);
try {
execSync(`node ../../index.js build ${siteName}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
compare(siteName, 'expected', '_site', siteIgnoredFiles);
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, siteName);
process.exit(1);
Expand All@@ -63,7 +65,8 @@ testConvertSites.forEach((sitePath) => {
execSync(`node ../../index.js init ${nonMarkBindSitePath} -c`, execOptions);
execSync(`node ../../index.js build ${nonMarkBindSitePath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
cleanupConvert(path.resolve(__dirname, sitePath));
Expand All@@ -83,7 +86,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
execSync(`node ../../index.js init ${siteCreationTempPath} --template ${flag}`, execOptions);
execSync(`node ../../index.js build ${siteCreationTempPath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
fs.removeSync(path.resolve(__dirname, siteCreationTempPath));
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/test/functional/testUtil/compare.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,13 +43,19 @@ function filterPageVueRenderFiles(filePaths) {
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
* @param {string} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
*/
function compare(root, expectedSiteRelativePath = 'expected', siteRelativePath = '_site', ignoredPaths = []) {
function compare(root,
expectedSiteRelativePath = 'expected',
siteRelativePath = '_site',
ignoredPaths = [],
ignoredDirectories = []) {
const expectedDirectory = path.join(root, expectedSiteRelativePath);
const actualDirectory = path.join(root, siteRelativePath);

let expectedPaths = walkSync(expectedDirectory, { directories: false });
let actualPaths = walkSync(actualDirectory, { directories: false });
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
let actualPaths = walkSync(actualDirectory, walkSyncOptions);

// Vue render JS files (*.page-vue-render.js) are not committed to version control,
// so we exclude them from the comparison to avoid false positive diffs.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/bugs/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -359,5 +360,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -1025,5 +1026,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add PageFind beta (Full Text Search across Site) by gerteck · Pull Request #2771 · MarkBind/markbind · GitHub
Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .eslintignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@ node_modules
*.d.ts

packages/cli/src/lib/live-server/*
packages/cli/test/**/pagefind/**/*.js


# Docs
docs/_site/

# --- packages/core ---

Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,9 @@ packages/core/template/*/_site
# Generated site (MarkBind)
packages/cli/test/functional/*/_site

# Generated pagefind directories for sites and in expected
packages/cli/test/functional/**/pagefind

# Ignore .page-vue-render.js files in functional test and subdirectories on update
packages/cli/test/functional/**/*.page-vue-render.js

Expand DownExpand Up@@ -107,6 +110,8 @@ packages/core/index.js
packages/core/src/lib/progress/*.js
# --- packages/core end ---

# Nx for Lerna
.nx/cache
.nx/workspace-data
.nx/workspace-data

# Pagefind fragments
*.pf_fragment
29 changes: 29 additions & 0 deletions docs/userGuide/makingTheSiteSearchable.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,35 @@ You can add a search bar component to your website to allow users to search the
<include src="syntax/keywords.md" />
<include src="syntax/indexing.md" />



## Using Pagefind (Beta)

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.

<box type="info">
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).
</box>

<box type="warning">
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.
</box>

To add the Pagefind search bar to your page, simply insert the following `div` where you want it to appear:

```html
<div id="pagefind-search-input"></div>
```

MarkBind will automatically inject the necessary scripts and styles to render the search UI.

The following UI will be rendered, which is provided by Pagefind:

<div id="pagefind-search-input"></div>


<br>

## Using External Search Services

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.

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that Pagefind provides "full-text search capabilities without external services" (line 44), but then on line 71, it says "Unlike the built-in search, Algolia provides full-text search." This creates confusion about whether the built-in Pagefind feature provides full-text search or not. The messaging should be clarified for consistency.

Suggested change
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.
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Algolia is a hosted service that also provides full-text search and advanced search features. See the panel below for more info.

Copilot uses AI. Check for mistakes.
Expand Down
146 changes: 146 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/cli/test/functional/test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,12 +43,14 @@ expectedErrors.forEach((error, index) => {
logger.info(`${index + 1}: ${error}`);
});

const GENERATED_DIRECTORIES_TO_IGNORE = ['pagefind'];

testSites.forEach((siteName) => {
console.log(`Running ${siteName} tests`);
try {
execSync(`node ../../index.js build ${siteName}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
compare(siteName, 'expected', '_site', siteIgnoredFiles);
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, siteName);
process.exit(1);
Expand All@@ -63,7 +65,8 @@ testConvertSites.forEach((sitePath) => {
execSync(`node ../../index.js init ${nonMarkBindSitePath} -c`, execOptions);
execSync(`node ../../index.js build ${nonMarkBindSitePath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
cleanupConvert(path.resolve(__dirname, sitePath));
Expand All@@ -83,7 +86,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
execSync(`node ../../index.js init ${siteCreationTempPath} --template ${flag}`, execOptions);
execSync(`node ../../index.js build ${siteCreationTempPath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
fs.removeSync(path.resolve(__dirname, siteCreationTempPath));
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/test/functional/testUtil/compare.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,13 +43,19 @@ function filterPageVueRenderFiles(filePaths) {
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
* @param {string} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
*/
function compare(root, expectedSiteRelativePath = 'expected', siteRelativePath = '_site', ignoredPaths = []) {
function compare(root,
expectedSiteRelativePath = 'expected',
siteRelativePath = '_site',
ignoredPaths = [],
ignoredDirectories = []) {
const expectedDirectory = path.join(root, expectedSiteRelativePath);
const actualDirectory = path.join(root, siteRelativePath);

let expectedPaths = walkSync(expectedDirectory, { directories: false });
let actualPaths = walkSync(actualDirectory, { directories: false });
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
let actualPaths = walkSync(actualDirectory, walkSyncOptions);

// Vue render JS files (*.page-vue-render.js) are not committed to version control,
// so we exclude them from the comparison to avoid false positive diffs.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/bugs/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -359,5 +360,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -1025,5 +1026,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add PageFind beta (Full Text Search across Site) by gerteck · Pull Request #2771 · MarkBind/markbind · GitHub
Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .eslintignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@ node_modules
*.d.ts

packages/cli/src/lib/live-server/*
packages/cli/test/**/pagefind/**/*.js


# Docs
docs/_site/

# --- packages/core ---

Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,9 @@ packages/core/template/*/_site
# Generated site (MarkBind)
packages/cli/test/functional/*/_site

# Generated pagefind directories for sites and in expected
packages/cli/test/functional/**/pagefind

# Ignore .page-vue-render.js files in functional test and subdirectories on update
packages/cli/test/functional/**/*.page-vue-render.js

Expand DownExpand Up@@ -107,6 +110,8 @@ packages/core/index.js
packages/core/src/lib/progress/*.js
# --- packages/core end ---

# Nx for Lerna
.nx/cache
.nx/workspace-data
.nx/workspace-data

# Pagefind fragments
*.pf_fragment
29 changes: 29 additions & 0 deletions docs/userGuide/makingTheSiteSearchable.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,35 @@ You can add a search bar component to your website to allow users to search the
<include src="syntax/keywords.md" />
<include src="syntax/indexing.md" />



## Using Pagefind (Beta)

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.

<box type="info">
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).
</box>

<box type="warning">
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.
</box>

To add the Pagefind search bar to your page, simply insert the following `div` where you want it to appear:

```html
<div id="pagefind-search-input"></div>
```

MarkBind will automatically inject the necessary scripts and styles to render the search UI.

The following UI will be rendered, which is provided by Pagefind:

<div id="pagefind-search-input"></div>


<br>

## Using External Search Services

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.

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that Pagefind provides "full-text search capabilities without external services" (line 44), but then on line 71, it says "Unlike the built-in search, Algolia provides full-text search." This creates confusion about whether the built-in Pagefind feature provides full-text search or not. The messaging should be clarified for consistency.

Suggested change
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.
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Algolia is a hosted service that also provides full-text search and advanced search features. See the panel below for more info.

Copilot uses AI. Check for mistakes.
Expand Down
146 changes: 146 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/cli/test/functional/test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,12 +43,14 @@ expectedErrors.forEach((error, index) => {
logger.info(`${index + 1}: ${error}`);
});

const GENERATED_DIRECTORIES_TO_IGNORE = ['pagefind'];

testSites.forEach((siteName) => {
console.log(`Running ${siteName} tests`);
try {
execSync(`node ../../index.js build ${siteName}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
compare(siteName, 'expected', '_site', siteIgnoredFiles);
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, siteName);
process.exit(1);
Expand All@@ -63,7 +65,8 @@ testConvertSites.forEach((sitePath) => {
execSync(`node ../../index.js init ${nonMarkBindSitePath} -c`, execOptions);
execSync(`node ../../index.js build ${nonMarkBindSitePath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
cleanupConvert(path.resolve(__dirname, sitePath));
Expand All@@ -83,7 +86,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
execSync(`node ../../index.js init ${siteCreationTempPath} --template ${flag}`, execOptions);
execSync(`node ../../index.js build ${siteCreationTempPath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
fs.removeSync(path.resolve(__dirname, siteCreationTempPath));
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/test/functional/testUtil/compare.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,13 +43,19 @@ function filterPageVueRenderFiles(filePaths) {
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
* @param {string} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
*/
function compare(root, expectedSiteRelativePath = 'expected', siteRelativePath = '_site', ignoredPaths = []) {
function compare(root,
expectedSiteRelativePath = 'expected',
siteRelativePath = '_site',
ignoredPaths = [],
ignoredDirectories = []) {
const expectedDirectory = path.join(root, expectedSiteRelativePath);
const actualDirectory = path.join(root, siteRelativePath);

let expectedPaths = walkSync(expectedDirectory, { directories: false });
let actualPaths = walkSync(actualDirectory, { directories: false });
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
let actualPaths = walkSync(actualDirectory, walkSyncOptions);

// Vue render JS files (*.page-vue-render.js) are not committed to version control,
// so we exclude them from the comparison to avoid false positive diffs.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/bugs/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -359,5 +360,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -1025,5 +1026,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add PageFind beta (Full Text Search across Site) by gerteck · Pull Request #2771 · MarkBind/markbind · GitHub
Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .eslintignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@ node_modules
*.d.ts

packages/cli/src/lib/live-server/*
packages/cli/test/**/pagefind/**/*.js


# Docs
docs/_site/

# --- packages/core ---

Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,9 @@ packages/core/template/*/_site
# Generated site (MarkBind)
packages/cli/test/functional/*/_site

# Generated pagefind directories for sites and in expected
packages/cli/test/functional/**/pagefind

# Ignore .page-vue-render.js files in functional test and subdirectories on update
packages/cli/test/functional/**/*.page-vue-render.js

Expand DownExpand Up@@ -107,6 +110,8 @@ packages/core/index.js
packages/core/src/lib/progress/*.js
# --- packages/core end ---

# Nx for Lerna
.nx/cache
.nx/workspace-data
.nx/workspace-data

# Pagefind fragments
*.pf_fragment
29 changes: 29 additions & 0 deletions docs/userGuide/makingTheSiteSearchable.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,35 @@ You can add a search bar component to your website to allow users to search the
<include src="syntax/keywords.md" />
<include src="syntax/indexing.md" />



## Using Pagefind (Beta)

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.

<box type="info">
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).
</box>

<box type="warning">
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.
</box>

To add the Pagefind search bar to your page, simply insert the following `div` where you want it to appear:

```html
<div id="pagefind-search-input"></div>
```

MarkBind will automatically inject the necessary scripts and styles to render the search UI.

The following UI will be rendered, which is provided by Pagefind:

<div id="pagefind-search-input"></div>


<br>

## Using External Search Services

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.

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that Pagefind provides "full-text search capabilities without external services" (line 44), but then on line 71, it says "Unlike the built-in search, Algolia provides full-text search." This creates confusion about whether the built-in Pagefind feature provides full-text search or not. The messaging should be clarified for consistency.

Suggested change
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.
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Algolia is a hosted service that also provides full-text search and advanced search features. See the panel below for more info.

Copilot uses AI. Check for mistakes.
Expand Down
146 changes: 146 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/cli/test/functional/test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,12 +43,14 @@ expectedErrors.forEach((error, index) => {
logger.info(`${index + 1}: ${error}`);
});

const GENERATED_DIRECTORIES_TO_IGNORE = ['pagefind'];

testSites.forEach((siteName) => {
console.log(`Running ${siteName} tests`);
try {
execSync(`node ../../index.js build ${siteName}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
compare(siteName, 'expected', '_site', siteIgnoredFiles);
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, siteName);
process.exit(1);
Expand All@@ -63,7 +65,8 @@ testConvertSites.forEach((sitePath) => {
execSync(`node ../../index.js init ${nonMarkBindSitePath} -c`, execOptions);
execSync(`node ../../index.js build ${nonMarkBindSitePath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
cleanupConvert(path.resolve(__dirname, sitePath));
Expand All@@ -83,7 +86,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
execSync(`node ../../index.js init ${siteCreationTempPath} --template ${flag}`, execOptions);
execSync(`node ../../index.js build ${siteCreationTempPath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
fs.removeSync(path.resolve(__dirname, siteCreationTempPath));
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/test/functional/testUtil/compare.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,13 +43,19 @@ function filterPageVueRenderFiles(filePaths) {
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
* @param {string} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
*/
function compare(root, expectedSiteRelativePath = 'expected', siteRelativePath = '_site', ignoredPaths = []) {
function compare(root,
expectedSiteRelativePath = 'expected',
siteRelativePath = '_site',
ignoredPaths = [],
ignoredDirectories = []) {
const expectedDirectory = path.join(root, expectedSiteRelativePath);
const actualDirectory = path.join(root, siteRelativePath);

let expectedPaths = walkSync(expectedDirectory, { directories: false });
let actualPaths = walkSync(actualDirectory, { directories: false });
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
let actualPaths = walkSync(actualDirectory, walkSyncOptions);

// Vue render JS files (*.page-vue-render.js) are not committed to version control,
// so we exclude them from the comparison to avoid false positive diffs.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/bugs/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -359,5 +360,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -1025,5 +1026,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add PageFind beta (Full Text Search across Site) by gerteck · Pull Request #2771 · MarkBind/markbind · GitHub
Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .eslintignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@ node_modules
*.d.ts

packages/cli/src/lib/live-server/*
packages/cli/test/**/pagefind/**/*.js


# Docs
docs/_site/

# --- packages/core ---

Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,9 @@ packages/core/template/*/_site
# Generated site (MarkBind)
packages/cli/test/functional/*/_site

# Generated pagefind directories for sites and in expected
packages/cli/test/functional/**/pagefind

# Ignore .page-vue-render.js files in functional test and subdirectories on update
packages/cli/test/functional/**/*.page-vue-render.js

Expand DownExpand Up@@ -107,6 +110,8 @@ packages/core/index.js
packages/core/src/lib/progress/*.js
# --- packages/core end ---

# Nx for Lerna
.nx/cache
.nx/workspace-data
.nx/workspace-data

# Pagefind fragments
*.pf_fragment
29 changes: 29 additions & 0 deletions docs/userGuide/makingTheSiteSearchable.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,35 @@ You can add a search bar component to your website to allow users to search the
<include src="syntax/keywords.md" />
<include src="syntax/indexing.md" />



## Using Pagefind (Beta)

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.

<box type="info">
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).
</box>

<box type="warning">
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.
</box>

To add the Pagefind search bar to your page, simply insert the following `div` where you want it to appear:

```html
<div id="pagefind-search-input"></div>
```

MarkBind will automatically inject the necessary scripts and styles to render the search UI.

The following UI will be rendered, which is provided by Pagefind:

<div id="pagefind-search-input"></div>


<br>

## Using External Search Services

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.

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that Pagefind provides "full-text search capabilities without external services" (line 44), but then on line 71, it says "Unlike the built-in search, Algolia provides full-text search." This creates confusion about whether the built-in Pagefind feature provides full-text search or not. The messaging should be clarified for consistency.

Suggested change
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.
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Algolia is a hosted service that also provides full-text search and advanced search features. See the panel below for more info.

Copilot uses AI. Check for mistakes.
Expand Down
146 changes: 146 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/cli/test/functional/test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,12 +43,14 @@ expectedErrors.forEach((error, index) => {
logger.info(`${index + 1}: ${error}`);
});

const GENERATED_DIRECTORIES_TO_IGNORE = ['pagefind'];

testSites.forEach((siteName) => {
console.log(`Running ${siteName} tests`);
try {
execSync(`node ../../index.js build ${siteName}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
compare(siteName, 'expected', '_site', siteIgnoredFiles);
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, siteName);
process.exit(1);
Expand All@@ -63,7 +65,8 @@ testConvertSites.forEach((sitePath) => {
execSync(`node ../../index.js init ${nonMarkBindSitePath} -c`, execOptions);
execSync(`node ../../index.js build ${nonMarkBindSitePath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
cleanupConvert(path.resolve(__dirname, sitePath));
Expand All@@ -83,7 +86,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
execSync(`node ../../index.js init ${siteCreationTempPath} --template ${flag}`, execOptions);
execSync(`node ../../index.js build ${siteCreationTempPath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
fs.removeSync(path.resolve(__dirname, siteCreationTempPath));
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/test/functional/testUtil/compare.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,13 +43,19 @@ function filterPageVueRenderFiles(filePaths) {
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
* @param {string} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
*/
function compare(root, expectedSiteRelativePath = 'expected', siteRelativePath = '_site', ignoredPaths = []) {
function compare(root,
expectedSiteRelativePath = 'expected',
siteRelativePath = '_site',
ignoredPaths = [],
ignoredDirectories = []) {
const expectedDirectory = path.join(root, expectedSiteRelativePath);
const actualDirectory = path.join(root, siteRelativePath);

let expectedPaths = walkSync(expectedDirectory, { directories: false });
let actualPaths = walkSync(actualDirectory, { directories: false });
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
let actualPaths = walkSync(actualDirectory, walkSyncOptions);

// Vue render JS files (*.page-vue-render.js) are not committed to version control,
// so we exclude them from the comparison to avoid false positive diffs.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/bugs/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -359,5 +360,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -1025,5 +1026,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add PageFind beta (Full Text Search across Site) by gerteck · Pull Request #2771 · MarkBind/markbind · GitHub
Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .eslintignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@ node_modules
*.d.ts

packages/cli/src/lib/live-server/*
packages/cli/test/**/pagefind/**/*.js


# Docs
docs/_site/

# --- packages/core ---

Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,9 @@ packages/core/template/*/_site
# Generated site (MarkBind)
packages/cli/test/functional/*/_site

# Generated pagefind directories for sites and in expected
packages/cli/test/functional/**/pagefind

# Ignore .page-vue-render.js files in functional test and subdirectories on update
packages/cli/test/functional/**/*.page-vue-render.js

Expand DownExpand Up@@ -107,6 +110,8 @@ packages/core/index.js
packages/core/src/lib/progress/*.js
# --- packages/core end ---

# Nx for Lerna
.nx/cache
.nx/workspace-data
.nx/workspace-data

# Pagefind fragments
*.pf_fragment
29 changes: 29 additions & 0 deletions docs/userGuide/makingTheSiteSearchable.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,35 @@ You can add a search bar component to your website to allow users to search the
<include src="syntax/keywords.md" />
<include src="syntax/indexing.md" />



## Using Pagefind (Beta)

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.

<box type="info">
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).
</box>

<box type="warning">
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.
</box>

To add the Pagefind search bar to your page, simply insert the following `div` where you want it to appear:

```html
<div id="pagefind-search-input"></div>
```

MarkBind will automatically inject the necessary scripts and styles to render the search UI.

The following UI will be rendered, which is provided by Pagefind:

<div id="pagefind-search-input"></div>


<br>

## Using External Search Services

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.

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that Pagefind provides "full-text search capabilities without external services" (line 44), but then on line 71, it says "Unlike the built-in search, Algolia provides full-text search." This creates confusion about whether the built-in Pagefind feature provides full-text search or not. The messaging should be clarified for consistency.

Suggested change
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.
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Algolia is a hosted service that also provides full-text search and advanced search features. See the panel below for more info.

Copilot uses AI. Check for mistakes.
Expand Down
146 changes: 146 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/cli/test/functional/test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,12 +43,14 @@ expectedErrors.forEach((error, index) => {
logger.info(`${index + 1}: ${error}`);
});

const GENERATED_DIRECTORIES_TO_IGNORE = ['pagefind'];

testSites.forEach((siteName) => {
console.log(`Running ${siteName} tests`);
try {
execSync(`node ../../index.js build ${siteName}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
compare(siteName, 'expected', '_site', siteIgnoredFiles);
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, siteName);
process.exit(1);
Expand All@@ -63,7 +65,8 @@ testConvertSites.forEach((sitePath) => {
execSync(`node ../../index.js init ${nonMarkBindSitePath} -c`, execOptions);
execSync(`node ../../index.js build ${nonMarkBindSitePath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
cleanupConvert(path.resolve(__dirname, sitePath));
Expand All@@ -83,7 +86,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
execSync(`node ../../index.js init ${siteCreationTempPath} --template ${flag}`, execOptions);
execSync(`node ../../index.js build ${siteCreationTempPath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
fs.removeSync(path.resolve(__dirname, siteCreationTempPath));
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/test/functional/testUtil/compare.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,13 +43,19 @@ function filterPageVueRenderFiles(filePaths) {
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
* @param {string} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
*/
function compare(root, expectedSiteRelativePath = 'expected', siteRelativePath = '_site', ignoredPaths = []) {
function compare(root,
expectedSiteRelativePath = 'expected',
siteRelativePath = '_site',
ignoredPaths = [],
ignoredDirectories = []) {
const expectedDirectory = path.join(root, expectedSiteRelativePath);
const actualDirectory = path.join(root, siteRelativePath);

let expectedPaths = walkSync(expectedDirectory, { directories: false });
let actualPaths = walkSync(actualDirectory, { directories: false });
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
let actualPaths = walkSync(actualDirectory, walkSyncOptions);

// Vue render JS files (*.page-vue-render.js) are not committed to version control,
// so we exclude them from the comparison to avoid false positive diffs.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/bugs/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -359,5 +360,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -1025,5 +1026,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Add PageFind beta (Full Text Search across Site) by gerteck · Pull Request #2771 · MarkBind/markbind · GitHub
Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .eslintignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,11 @@ node_modules
*.d.ts

packages/cli/src/lib/live-server/*
packages/cli/test/**/pagefind/**/*.js


# Docs
docs/_site/

# --- packages/core ---

Expand Down
9 changes: 7 additions & 2 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,9 @@ packages/core/template/*/_site
# Generated site (MarkBind)
packages/cli/test/functional/*/_site

# Generated pagefind directories for sites and in expected
packages/cli/test/functional/**/pagefind

# Ignore .page-vue-render.js files in functional test and subdirectories on update
packages/cli/test/functional/**/*.page-vue-render.js

Expand DownExpand Up@@ -107,6 +110,8 @@ packages/core/index.js
packages/core/src/lib/progress/*.js
# --- packages/core end ---

# Nx for Lerna
.nx/cache
.nx/workspace-data
.nx/workspace-data

# Pagefind fragments
*.pf_fragment
29 changes: 29 additions & 0 deletions docs/userGuide/makingTheSiteSearchable.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,35 @@ You can add a search bar component to your website to allow users to search the
<include src="syntax/keywords.md" />
<include src="syntax/indexing.md" />



## Using Pagefind (Beta)

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.

<box type="info">
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).
</box>

<box type="warning">
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.
</box>

To add the Pagefind search bar to your page, simply insert the following `div` where you want it to appear:

```html
<div id="pagefind-search-input"></div>
```

MarkBind will automatically inject the necessary scripts and styles to render the search UI.

The following UI will be rendered, which is provided by Pagefind:

<div id="pagefind-search-input"></div>


<br>

## Using External Search Services

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.

CopilotAIJan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that Pagefind provides "full-text search capabilities without external services" (line 44), but then on line 71, it says "Unlike the built-in search, Algolia provides full-text search." This creates confusion about whether the built-in Pagefind feature provides full-text search or not. The messaging should be clarified for consistency.

Suggested change
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.
MarkBind sites can use Algolia Doc Search services easily via the Algolia plugin. Algolia is a hosted service that also provides full-text search and advanced search features. See the panel below for more info.

Copilot uses AI. Check for mistakes.
Expand Down
146 changes: 146 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/cli/test/functional/test.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,12 +43,14 @@ expectedErrors.forEach((error, index) => {
logger.info(`${index + 1}: ${error}`);
});

const GENERATED_DIRECTORIES_TO_IGNORE = ['pagefind'];

testSites.forEach((siteName) => {
console.log(`Running ${siteName} tests`);
try {
execSync(`node ../../index.js build ${siteName}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTestSites[siteName];
compare(siteName, 'expected', '_site', siteIgnoredFiles);
compare(siteName, 'expected', '_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, siteName);
process.exit(1);
Expand All@@ -63,7 +65,8 @@ testConvertSites.forEach((sitePath) => {
execSync(`node ../../index.js init ${nonMarkBindSitePath} -c`, execOptions);
execSync(`node ../../index.js build ${nonMarkBindSitePath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForConvertSites[siteName];
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'non_markbind_site/_site', siteIgnoredFiles,
GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
cleanupConvert(path.resolve(__dirname, sitePath));
Expand All@@ -83,7 +86,7 @@ testTemplateSites.forEach((templateAndSitePath) => {
execSync(`node ../../index.js init ${siteCreationTempPath} --template ${flag}`, execOptions);
execSync(`node ../../index.js build ${siteCreationTempPath}`, execOptions);
const siteIgnoredFiles = plantumlGeneratedFilesForTemplateSites[siteName];
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles);
compare(sitePath, 'expected', 'tmp/_site', siteIgnoredFiles, GENERATED_DIRECTORIES_TO_IGNORE);
} catch (err) {
printFailedMessage(err, sitePath);
fs.removeSync(path.resolve(__dirname, siteCreationTempPath));
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/test/functional/testUtil/compare.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,13 +43,19 @@ function filterPageVueRenderFiles(filePaths) {
* @param {string} expectedSiteRelativePath - Relative path to expected site output (default: "expected")
* @param {string} siteRelativePath - Relative path to actual generated site output (default: "_site")
* @param {string} ignoredPaths - Specify any paths to ignore for comparison, but still check for existence.
* @param {string[]} ignoredDirectories - Specify any directories to ignore for comparison (e.g. 'pagefind')
*/
function compare(root, expectedSiteRelativePath = 'expected', siteRelativePath = '_site', ignoredPaths = []) {
function compare(root,
expectedSiteRelativePath = 'expected',
siteRelativePath = '_site',
ignoredPaths = [],
ignoredDirectories = []) {
const expectedDirectory = path.join(root, expectedSiteRelativePath);
const actualDirectory = path.join(root, siteRelativePath);

let expectedPaths = walkSync(expectedDirectory, { directories: false });
let actualPaths = walkSync(actualDirectory, { directories: false });
const walkSyncOptions = { directories: false, ignore: ignoredDirectories };
let expectedPaths = walkSync(expectedDirectory, walkSyncOptions);
let actualPaths = walkSync(actualDirectory, walkSyncOptions);

// Vue render JS files (*.page-vue-render.js) are not committed to version control,
// so we exclude them from the comparison to avoid false positive diffs.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/bugs/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -359,5 +360,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
17 changes: 17 additions & 0 deletions packages/cli/test/functional/test_site/expected/index.html
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
<link rel="stylesheet" href="/test_site/markbind/glyphicons/css/bootstrap-glyphicons.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/codeblock-dark.min.css">
<link rel="stylesheet" href="/test_site/markbind/css/markbind.min.css">
<link rel="stylesheet" href="/test_site/pagefind/pagefind-ui.css">
<link rel="stylesheet" href="/test_site/plugins/testMarkbindPlugin/testMarkbindPluginStylesheet.css">
<link rel="stylesheet" href="/test_site/plugins/web3Form/web-3-form.css">
<link rel="stylesheet" href="/test_site/plugins/markbind-plugin-anchors/markbind-plugin-anchors.css">
Expand DownExpand Up@@ -1025,5 +1026,21 @@ <h1 id="heading-in-footer-should-not-be-indexed">Heading in footer should not be
});

</script>
<script src="/test_site/pagefind/pagefind-ui.js"></script>

<script>
window.addEventListener('DOMContentLoaded', (event) => {
const searchContainers = document.querySelectorAll('#pagefind-search-input');
if (searchContainers.length) {
searchContainers.forEach((container) => {
new window.PagefindUI({
element: container,
showSubResults: true,
showImages: false,
});
});
}
});
</script>

</html>
Loading