Skip to content

Commit c753ba0

Browse files
Add DocSearch meta tags and search interceptor for improved search relevance (#952)
Co-authored-by: qodo-code-review[bot] <151058649+qodo-code-review[bot]@users.noreply.github.com>
1 parent 42d348e commit c753ba0

File tree

6 files changed

+433
-257
lines changed

6 files changed

+433
-257
lines changed

docs/.vuepress/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {linkCheckPlugin} from "./markdown/linkCheck";
1414
import {replaceLinkPlugin} from "./markdown/replaceLink";
1515
import {importCodePlugin} from "./markdown/xode/importCodePlugin";
1616
import {llmsPlugin} from '@vuepress/plugin-llms'
17+
import fs from 'fs';
18+
19+
const searchInterceptor = fs.readFileSync(path.join(__dirname, 'util/searchInterceptor.js'), 'utf8');
1720

1821

1922
dotenv.config({path: path.join(__dirname, '..', '..', '.algolia', '.env')});
@@ -105,6 +108,8 @@ export default defineUserConfig({
105108
'data-user-analytics-cookie-enabled': false,
106109
}],
107110

111+
["script", {}, searchInterceptor],
112+
108113
// CSS override to hide the modal mask and wrapper entirely
109114
['style', {}, `
110115
.redirect-modal-mask,

docs/.vuepress/configs/plugins/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@ import {hostname} from "./shared";
77
import {redirect} from "./redirect";
88
import type {SitemapPluginOptions} from "@vuepress/plugin-sitemap";
99

10+
const docsearch = {
11+
apiKey: process.env.ALGOLIA_SEARCH_API_KEY,
12+
indexName: process.env.ALGOLIA_INDEX_NAME,
13+
appId: process.env.ALGOLIA_APPLICATION_ID,
14+
maxResultsPerGroup: 10,
15+
}
16+
1017
export default {
1118
components: {
1219
components: ["Badge", "VPBanner", "VPCard", "VidStack"]
1320
},
14-
docsearch: {
15-
apiKey: process.env.ALGOLIA_SEARCH_API_KEY,
16-
indexName: process.env.ALGOLIA_INDEX_NAME,
17-
appId: process.env.ALGOLIA_APPLICATION_ID,
18-
maxResultsPerGroup:10
19-
},
21+
docsearch,
2022
seo: seoPlugin,
2123
sitemap: {
2224
hostname: hostname,

docs/.vuepress/configs/plugins/seo.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { SeoPluginOptions } from "@vuepress/plugin-seo";
22
import { match } from "ts-pattern";
33
import type { App, HeadConfig, Page } from "vuepress";
44
import { hostname } from "./shared";
5+
import { instance as versioning, type Version as VersionGroup } from "../../lib/versioning";
56

67
interface DocumentationPath {
78
version: string | null;
@@ -59,6 +60,100 @@ const normalize = (str: string): string =>
5960
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
6061
.join(' ');
6162

63+
/**
64+
* Finds the version group for a given section path.
65+
* @param section The section path (e.g., "server", "clients/dotnet", "server/kubernetes-operator").
66+
* @returns The version group or null if not found.
67+
*/
68+
const findVersionGroup = (section: string): VersionGroup | null => {
69+
// Special case for kubernetes-operator
70+
if (section === "server/kubernetes-operator") {
71+
return versioning.all.find((v) => v.id === "kubernetes-operator") || null;
72+
}
73+
74+
// Try to find by basePath match
75+
const byBasePath = versioning.all.find((v) => v.basePath === section);
76+
if (byBasePath) {
77+
return byBasePath;
78+
}
79+
80+
// Try to match client sections to their version group IDs
81+
const clientMatch = section.match(/^clients\/(\w+)/);
82+
if (clientMatch) {
83+
const clientName = clientMatch[1];
84+
const clientId = `${clientName}-client`;
85+
return versioning.all.find((v) => v.id === clientId) || null;
86+
}
87+
88+
return null;
89+
};
90+
91+
/**
92+
* Gets the latest version string for a given section.
93+
* @param section The section path.
94+
* @returns The latest version string, or null if not found.
95+
*/
96+
const getLatestVersionForSection = (section: string): string | null => {
97+
const versionGroup = findVersionGroup(section);
98+
if (!versionGroup?.versions?.length) {
99+
return null;
100+
}
101+
102+
// Find the first non-preview, non-excluded version (which is the latest)
103+
const excludedVersions = EXCLUDED_VERSIONS[section] || [];
104+
const latestVersion = versionGroup.versions.find(
105+
v => v.version && !v.preview && !excludedVersions.includes(v.version)
106+
);
107+
108+
return latestVersion?.version || null;
109+
};
110+
111+
/**
112+
* Gets the docsearch:version content for a page.
113+
* @param section The section path.
114+
* @param currentVersion The current version string from the path.
115+
* @returns The version content string (e.g., "v1.2,latest" or "v1.1" or "v1.0,legacy").
116+
*/
117+
const getDocSearchVersionContent = (section: string, currentVersion: string | null): string | null => {
118+
if (!currentVersion) return null;
119+
120+
const parts: string[] = [currentVersion];
121+
const latestVersion = getLatestVersionForSection(section);
122+
123+
if (latestVersion && currentVersion === latestVersion) parts.push("latest");
124+
125+
const isLegacy = ["legacy", "tcp"].some(seg => section.split("/").includes(seg));
126+
if (isLegacy) parts.push("legacy");
127+
128+
return parts.join(",");
129+
};
130+
131+
/**
132+
* Maps a section path to a docsearch product name.
133+
* @param section The section path (e.g., "clients/dotnet", "server").
134+
* @returns The product name for docsearch (e.g., "dotnet_sdk", "js_sdk", "server") or null if not in the list.
135+
*/
136+
const getDocSearchProduct = (section: string): string | null => {
137+
return match(section)
138+
.with("clients/dotnet", () => "dotnet_sdk")
139+
.with("clients/golang", () => "golang_sdk")
140+
.with("clients/java", () => "java_sdk")
141+
.with("clients/node", () => "js_sdk")
142+
.with("clients/python", () => "python_sdk")
143+
.with("clients/rust", () => "rust_sdk")
144+
.with("clients/dotnet/legacy", () => "dotnet_sdk")
145+
.with("clients/golang/legacy", () => "golang_sdk")
146+
.with("clients/java/legacy", () => "java_sdk")
147+
.with("clients/node/legacy", () => "js_sdk")
148+
.with("clients/python/legacy", () => "python_sdk")
149+
.with("clients/rust/legacy", () => "rust_sdk")
150+
.with("clients/tcp/dotnet", () => "dotnet_sdk_tcp")
151+
.with("cloud", () => "cloud")
152+
.with("server/kubernetes-operator", () => "kubernetes_operator")
153+
.with("server", () => "server")
154+
.otherwise(() => null);
155+
};
156+
62157
export const seoPlugin: SeoPluginOptions = {
63158
hostname,
64159

@@ -96,6 +191,9 @@ export const seoPlugin: SeoPluginOptions = {
96191
* Sets the following tags:
97192
* e.g. <meta name="es:category" content=".NET Client" />
98193
* <meta name="es:version" content="v1.0" />
194+
* <meta name="docsearch:version" content="v1.0,v1.1,v1.2" />
195+
* <meta name="docsearch:language" content="en" />
196+
* <meta name="docsearch:product" content="dotnet_sdk" />
99197
*
100198
* If it's a legacy or tcp client, it will be labelled as "Legacy"
101199
*/
@@ -131,5 +229,21 @@ export const seoPlugin: SeoPluginOptions = {
131229
.otherwise(() => normalize(section));
132230

133231
head.push(["meta", { name: "es:category", content: category }]);
232+
233+
// Add DocSearch meta tags
234+
// Add language tag (defaulting to "en")
235+
head.push(["meta", { name: "docsearch:language", content: "en" }]);
236+
237+
// Add product tag (only if section is in the list)
238+
const product = getDocSearchProduct(section);
239+
if (product) {
240+
head.push(["meta", { name: "docsearch:product", content: product }]);
241+
}
242+
243+
// Add version tag with current version
244+
const docSearchVersion = getDocSearchVersionContent(section, version);
245+
if (docSearchVersion) {
246+
head.push(["meta", { name: "docsearch:version", content: docSearchVersion }]);
247+
}
134248
}
135249
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
function getFilters() {
2+
// Read from meta tags to ensure we match what's in the HTML
3+
// This includes "latest" when applicable, which is calculated server-side
4+
const productMeta = document.querySelector('meta[name="docsearch:product"]');
5+
const versionMeta = document.querySelector('meta[name="docsearch:version"]');
6+
7+
const product = productMeta ? productMeta.getAttribute('content') : null;
8+
const version = versionMeta ? versionMeta.getAttribute('content') : null;
9+
10+
return {
11+
product: product,
12+
version: version
13+
};
14+
}
15+
16+
function buildOptionalFilters(product, version) {
17+
const optionalFilters = [];
18+
if (product) {
19+
optionalFilters.push("product:" + product);
20+
}
21+
if (version) {
22+
const versionParts = version.split(',');
23+
versionParts.forEach((v) => {
24+
optionalFilters.push("version:" + v.trim());
25+
});
26+
}
27+
return optionalFilters;
28+
}
29+
30+
function applyFiltersToBody(body, optionalFilters) {
31+
if (optionalFilters.length > 0) {
32+
body.requests = body.requests || [];
33+
body.requests.forEach((req) => {
34+
// Set optionalFilters directly on the request
35+
req.optionalFilters = optionalFilters;
36+
});
37+
}
38+
}
39+
40+
const searchInterceptor = function() {
41+
// Intercept XMLHttpRequest (XHR)
42+
const proxied = window.XMLHttpRequest.prototype.open;
43+
window.XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
44+
const urlString = typeof url === 'string' ? url : url.toString();
45+
if (urlString.includes('algolia.net')) {
46+
const send = this.send;
47+
this.send = function(data) {
48+
if (typeof data === 'string' && data.trim()) {
49+
try {
50+
const body = JSON.parse(data);
51+
const { product, version } = getFilters();
52+
const optionalFilters = buildOptionalFilters(product, version);
53+
54+
if (optionalFilters.length > 0) {
55+
applyFiltersToBody(body, optionalFilters);
56+
const modifiedData = JSON.stringify(body);
57+
return send.apply(this, [modifiedData]);
58+
}
59+
} catch(e) {
60+
console.error("Error modifying Algolia request");
61+
}
62+
}
63+
return send.apply(this, [data]);
64+
};
65+
}
66+
return proxied.call(this, method, url, async ?? true, username, password);
67+
}
68+
}
69+
70+
searchInterceptor();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"@segment/analytics-next": "^1.79.0",
3737
"@vuepress/bundler-vite": "^2.0.0-rc.23",
3838
"@vuepress/helper": "^2.0.0-rc.110",
39-
"@vuepress/plugin-docsearch": "^2.0.0-rc.110",
39+
"@vuepress/plugin-docsearch": "2.0.0-rc.121",
4040
"@vuepress/plugin-notice": "^2.0.0-rc.110",
4141
"@vuepress/plugin-seo": "^2.0.0-rc.110",
4242
"@vuepress/plugin-sitemap": "^2.0.0-rc.110",

0 commit comments

Comments
 (0)