Three VuePress slimsearch Search Box Fixes
Three VuePress slimsearch Search Box Fixes
This round I did three things to the blog search box: switched from the default search: true to the slimsearch plugin, reined in the number of search results, and fixed an accidental navigation on Enter with a Pinyin IME. Below is each issue, its root cause, the fix, and the pitfalls I hit.
The stack is VuePress 2 rc.30 + vuepress-theme-hope rc.107 + @vuepress/[email protected] + slimsearch 2.3.0, bilingual (zh-CN at /, en-US at /en/).
1. customFields formatter crash (split is not a function)
Symptom
After enabling slimsearch with customFields (categories/tags), searching threw this in the console:
n[f.value].split is not a functionNo results came back at all.
Root cause
@vuepress/plugin-slimsearch merges the customFields formatter with the default locale on the node side via getFullLocaleConfig / deepAssign. When you write it as an object:
formatter: {
"/": "Category: $content",
"/en/": "Category: $content",
}deepAssign treats this string as an enumerable object and iterates it, so each character index becomes a key and the value becomes a single character. "Category: $content" ends up mangled into {0:"C", 1:"a", ...}. The client then calls .split("$content") on it and throws split is not a function.
This is not a config mistake; it's a bug in how the plugin + @vuepress/helper locale merge handles string values.
Fix
In theme.ts, replace search: true with an explicit slimsearch config and write the formatter as an explicit string object (to bypass the mangled merge path):
slimsearch: {
indexContent: true, // changed to false later, see section 3
customFields: [
{
getter: (page) => page.frontmatter.category,
formatter: {
"/": "Category: $content",
"/en/": "Category: $content",
},
},
{
getter: (page) => page.frontmatter.tag,
formatter: {
"/": "Tag: $content",
"/en/": "Tag: $content",
},
},
],
},The formatter is a Record<string,string> (locale -> string); $content is replaced with the getter's return value. The client SearchResult component picks the string for the current routeLocale, then split("$content") to stitch the prefix and suffix.
Verification
Searching certificate / DevOps on the English site works, the customField shows "Tag: ..." / "Category: ...", no console error.
2. Too many search results
Symptom
A single query returned a flood: the -> 31 pages / 438 lines, certificate -> 3 pages / 35 lines.
Root cause
indexContent: true indexes the entire body. The slimsearch SearchResult component lists all matching paragraphs for each hit page, and the render loop n.map(...) in the source has no cap. So every paragraph containing the word gets rendered.
theme-hope passes through SlimSearchPluginOptions directly and does not expose combineWith / per-page hit cap / prefix / fuzzy, the native slimsearch options (SlimSearchPluginOptions only has indexContent/sortStrategy/indexOptions(tokenize/processTerm)/customFields/filter, etc.). There's no middle ground of "searchable body but few results".
Fix
slimsearch: {
indexContent: false, // only index title, headings, excerpt, categories/tags
...
}Measured comparison (English /en/)
| Query | indexContent:true | indexContent:false |
|---|---|---|
the | 31 pages / 438 lines | 17 pages / 30 lines |
server | 18 pages / 68 lines | 9 pages / 13 lines |
Docker | 4 pages / 42 lines | 3 pages / 7 lines |
Nginx | 2 pages / 3 lines | 0 pages (body-only words won't match) |
Hit lines drop about 80%.
Trade-off
Words that only appear in the body and not in titles/excerpts (like Nginx) won't match. This is a precision-vs-recall trade-off you tune to your content. The default is false, and it's the conventional choice for VuePress blogs.
To get "searchable body + converged results" later, you'd either fork the plugin to add a cap or switch search solutions; the current plugin config can't do it.
3. Accidental navigation on Enter with a Pinyin IME
Symptom
Typing aws with a Pinyin IME, without switching to the English keyboard, then pressing Enter to submit the raw letters aws, instead jumped straight to the first search result.
Root cause
The three e.key === "Enter" handlers in @vuepress/plugin-slimsearch client (one in SearchResult-*.js, two in client/config.js) don't check the IME composition state:
// On Enter, jump to the currently selected result
if (e.key === "Enter") {
const result = K.value.contents[U.value];
...
}During IME composition, pressing Enter means "commit the candidates as raw text", but the browser also fires a keydown whose isComposing is true (older browsers use keyCode === 229). The plugin doesn't guard against it, so it treats that "commit-text Enter" as a "confirm-navigation Enter".
Fix
Add a setup() hook in client.ts defineClientConfig to intercept composition keystrokes in the window capture phase:
export default defineClientConfig({
enhance({ app }) {
app.component("VisitorCounter", VisitorCounter);
},
setup() {
// During IME (e.g. Pinyin) composition, prevent Enter / arrow keys from
// triggering search navigation or form submission, so that typing "aws" and
// pressing Enter commits the raw letters instead of jumping to the first result.
window.addEventListener(
"keydown",
(event) => {
if (event.isComposing || event.keyCode === 229) {
event.stopPropagation();
event.stopImmediatePropagation();
}
},
true, // capture phase, runs before the plugin's bubbling listeners
);
},
rootComponents: [VisitorCounterInjector],
});Key points
- You must use the capture phase (third argument
true). The plugin's Enter listener is in the bubbling phase; only the capture phase runs before it, sostopImmediatePropagationcan kill the event at the window layer before it ever reaches the plugin. - Use both
stopPropagation+stopImmediatePropagation: the former stops further bubbling/capturing, the latter stops other listeners on the same target. - The condition
event.isComposing || event.keyCode === 229: the former is the modern standard, the latter is a fallback for old Safari/iOS.
Verification
- A normal (non-composing) Enter still navigates:
certificate-> jumps to the article, so the guard doesn't break normal search navigation. - The composing branch (
isComposing: true) is confirmed by code logic: the guard hitsstopPropagation, so the plugin's navigation handler never receives the event. TheisComposingguard is the standard fix for this class of IME problems.
Summary
The three issues sit at three layers: a string-handling bug in the plugin's locale merge, the plugin not exposing a hit cap so results explode, and the client not checking IME composition state. The first two are worked around with config; the third needs an IME guard added in the client capture phase. After the changes, search works on both the Chinese and English sites, and the Pinyin IME no longer triggers accidental navigation.
- Config:
src/.vuepress/theme.ts(theslimsearchblock) - Client hook:
src/.vuepress/client.ts(setup()IME guard) - Plugin source (read-only reference):
node_modules/@vuepress/plugin-slimsearch/dist/client/config.js: SearchBox / SearchModal, two Enter handlersSearchResult-*.js: result rendering + one Enter navigationnode/index.d.ts:SlimSearchPluginOptionsfield definitions
If you later want a searchable body with converged results, consider forking the plugin to add a cap like n.slice(0, maxPerRow) in the SearchResult render loop, or wiring in combineWith: "AND". With indexContent: false, body-only words won't match; watch reader feedback and weigh switching back if needed.
