From a6bb22d7c2a5ab483b97dc39b2e03c0aa9e596e6 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 4 Dec 2025 14:55:01 +0000 Subject: [PATCH 01/18] Bind Meilisearch searches to monthly pseudonymous user id with DNT --- docusaurus/src/theme/SearchBar/index.js | 88 ++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index 0929206a4b..c9a004c1d9 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -10,6 +10,54 @@ function SearchBarContent() { const dropdownRef = useRef(null); const searchInstanceRef = useRef(null); const [isLoaded, setIsLoaded] = useState(false); + const originalFetchRef = useRef(null); + + function isDoNotTrackEnabled() { + try { + const dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack || '').toString(); + return dnt === '1' || dnt.toLowerCase() === 'yes'; + } catch (_) { + return false; + } + } + + function getOrCreateMonthlyUserId() { + if (isDoNotTrackEnabled()) return null; + try { + const key = 'msUserId'; + const now = new Date(); + const monthKey = now.toISOString().slice(0, 7); // YYYY-MM (UTC) + const raw = window.localStorage.getItem(key); + if (raw) { + try { + const parsed = JSON.parse(raw); + if (parsed && parsed.id && parsed.month === monthKey) { + return parsed.id; + } + } catch {} + } + const uuid = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36); + const value = JSON.stringify({ id: uuid, month: monthKey }); + window.localStorage.setItem(key, value); + return uuid; + } catch (_) { + return null; + } + } + + function shouldAttachUserIdHeader(urlStr) { + try { + const meiliHost = siteConfig?.customFields?.meilisearch?.host; + if (!meiliHost) return false; + const u = new URL(urlStr, window.location.origin); + const meili = new URL(meiliHost); + if (u.origin !== meili.origin) return false; + // Only for search requests + return /\/indexes\/[^/]+\/search$/.test(u.pathname); + } catch { + return false; + } + } useEffect(() => { if (!searchButtonRef.current) { @@ -40,6 +88,38 @@ function SearchBarContent() { document.addEventListener('keydown', handleKeyDown, true); + // Prepare pseudonymous monthly user id (respects DNT) + const userId = getOrCreateMonthlyUserId(); + + // Scoped fetch interceptor to add X-MS-USER-ID for Meilisearch search requests + if (typeof window !== 'undefined' && window.fetch && !originalFetchRef.current) { + originalFetchRef.current = window.fetch.bind(window); + window.fetch = async (input, init) => { + try { + const url = typeof input === 'string' ? input : (input && input.url) ? input.url : ''; + if (!userId || !shouldAttachUserIdHeader(url)) { + return originalFetchRef.current(input, init); + } + + // Attach header depending on input type + if (typeof input === 'string' || input instanceof URL) { + const headers = new Headers(init && init.headers ? init.headers : undefined); + if (!headers.has('X-MS-USER-ID')) headers.set('X-MS-USER-ID', userId); + return originalFetchRef.current(input, { ...(init || {}), headers }); + } + + // input is Request + const req = input; + const headers = new Headers(req.headers); + if (!headers.has('X-MS-USER-ID')) headers.set('X-MS-USER-ID', userId); + const newReq = new Request(req, { headers }); + return originalFetchRef.current(newReq); + } catch (_) { + return originalFetchRef.current(input, init); + } + }; + } + if (searchInstanceRef.current) { searchInstanceRef.current.destroy?.(); searchInstanceRef.current = null; @@ -151,6 +231,12 @@ function SearchBarContent() { return () => { document.removeEventListener('keydown', handleKeyDown, true); + if (originalFetchRef.current) { + try { + window.fetch = originalFetchRef.current; + } catch {} + originalFetchRef.current = null; + } if (searchInstanceRef.current) { searchInstanceRef.current.destroy?.(); searchInstanceRef.current = null; @@ -171,4 +257,4 @@ export default function SearchBar() { {() => } ); -} \ No newline at end of file +} From 63a587969343807ff6c255d5f896da3bd42c707f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 4 Dec 2025 15:15:23 +0000 Subject: [PATCH 02/18] Document docs search analytics and monthly pseudonymous id with DNT --- docusaurus/docs/cms/usage-information.md | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docusaurus/docs/cms/usage-information.md b/docusaurus/docs/cms/usage-information.md index 62196614f7..6ad33a5c46 100644 --- a/docusaurus/docs/cms/usage-information.md +++ b/docusaurus/docs/cms/usage-information.md @@ -82,3 +82,28 @@ Data collection can later be re-enabled by deleting the flag or setting it to fa :::note If you have any questions or concerns regarding data collection, please contact us at the following email address [privacy@strapi.io](mailto:privacy@strapi.io). ::: + +## Search analytics for docs.strapi.io + +To improve our documentation, the public website at `docs.strapi.io` collects anonymous search usage metrics using Meilisearch Cloud. These metrics help us understand how the search performs and where we can make it better: + +- Total searches +- Total users (estimated) +- Most searched keywords +- Searches without results + +To make the “Total users” metric more accurate while preserving privacy, the site creates a pseudonymous identifier and stores it in the browser’s localStorage under the `msUserId` key. This identifier: + +- Is randomly generated and contains no personal data +- Rotates monthly on first visit of each new month (calendar month, UTC) +- Is sent only with documentation search requests as the `X-MS-USER-ID` header to Meilisearch Cloud +- Is not used for any other purpose and is not shared with third parties beyond Meilisearch processing the search + +Respecting privacy choices: + +- If the browser’s Do Not Track setting is enabled, the site does not create or send this identifier +- You can remove the identifier at any time by clearing the `msUserId` entry from your browser’s localStorage for `docs.strapi.io` + +Notes: + +- We currently do not send click-through or conversion events to Meilisearch. Only the metrics listed above are collected. From 5e66feb9ce0b1eb2f8f65054173b08f7d4f764fa Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 4 Dec 2025 15:25:43 +0000 Subject: [PATCH 03/18] Refine docs search analytics disclosure and add note callout --- docusaurus/docs/cms/usage-information.md | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/docusaurus/docs/cms/usage-information.md b/docusaurus/docs/cms/usage-information.md index 6ad33a5c46..48fb3ab7d3 100644 --- a/docusaurus/docs/cms/usage-information.md +++ b/docusaurus/docs/cms/usage-information.md @@ -92,18 +92,10 @@ To improve our documentation, the public website at `docs.strapi.io` collects an - Most searched keywords - Searches without results -To make the “Total users” metric more accurate while preserving privacy, the site creates a pseudonymous identifier and stores it in the browser’s localStorage under the `msUserId` key. This identifier: +To make the “Total users” metric more accurate while preserving privacy, the site creates a pseudonymous identifier in the browser’s localStorage under the `msUserId` key. It is randomly generated, contains no personal data, rotates on the first visit of each calendar month (UTC), and is sent only with documentation search requests as the `X-MS-USER-ID` header. It is not used for any other purpose. -- Is randomly generated and contains no personal data -- Rotates monthly on first visit of each new month (calendar month, UTC) -- Is sent only with documentation search requests as the `X-MS-USER-ID` header to Meilisearch Cloud -- Is not used for any other purpose and is not shared with third parties beyond Meilisearch processing the search - -Respecting privacy choices: - -- If the browser’s Do Not Track setting is enabled, the site does not create or send this identifier -- You can remove the identifier at any time by clearing the `msUserId` entry from your browser’s localStorage for `docs.strapi.io` - -Notes: +:::note +Privacy choices: If your browser’s Do Not Track setting is enabled, the site does not create or send this identifier. You can remove it at any time by clearing the `msUserId` entry from your browser’s localStorage for `docs.strapi.io`. +::: -- We currently do not send click-through or conversion events to Meilisearch. Only the metrics listed above are collected. +We do not send click-through or conversion events to Meilisearch. From 0c572baa9bfefd47b82a40e70ef0a5e92e2d9ce1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 4 Dec 2025 15:35:57 +0000 Subject: [PATCH 04/18] Revise privacy note wording for docs search analytics --- docusaurus/docs/cms/usage-information.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docusaurus/docs/cms/usage-information.md b/docusaurus/docs/cms/usage-information.md index 48fb3ab7d3..6f80e65333 100644 --- a/docusaurus/docs/cms/usage-information.md +++ b/docusaurus/docs/cms/usage-information.md @@ -94,8 +94,10 @@ To improve our documentation, the public website at `docs.strapi.io` collects an To make the “Total users” metric more accurate while preserving privacy, the site creates a pseudonymous identifier in the browser’s localStorage under the `msUserId` key. It is randomly generated, contains no personal data, rotates on the first visit of each calendar month (UTC), and is sent only with documentation search requests as the `X-MS-USER-ID` header. It is not used for any other purpose. -:::note -Privacy choices: If your browser’s Do Not Track setting is enabled, the site does not create or send this identifier. You can remove it at any time by clearing the `msUserId` entry from your browser’s localStorage for `docs.strapi.io`. +:::note Note: Privacy choices +Privacy choices: If your browser’s Do Not Track setting is enabled, the site does not create or send this identifier. + +If this identifier is created, you can remove it at any time by clearing the `msUserId` entry from your browser’s localStorage for docs.strapi.io. ::: We do not send click-through or conversion events to Meilisearch. From de289267aa4f83936c791e0719d8286253c55858 Mon Sep 17 00:00:00 2001 From: Pierre Wizla Date: Thu, 4 Dec 2025 16:39:21 +0100 Subject: [PATCH 05/18] Apply suggestion from @pwizla --- docusaurus/docs/cms/usage-information.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/cms/usage-information.md b/docusaurus/docs/cms/usage-information.md index 6f80e65333..7eaf2fe5d0 100644 --- a/docusaurus/docs/cms/usage-information.md +++ b/docusaurus/docs/cms/usage-information.md @@ -95,7 +95,7 @@ To improve our documentation, the public website at `docs.strapi.io` collects an To make the “Total users” metric more accurate while preserving privacy, the site creates a pseudonymous identifier in the browser’s localStorage under the `msUserId` key. It is randomly generated, contains no personal data, rotates on the first visit of each calendar month (UTC), and is sent only with documentation search requests as the `X-MS-USER-ID` header. It is not used for any other purpose. :::note Note: Privacy choices -Privacy choices: If your browser’s Do Not Track setting is enabled, the site does not create or send this identifier. +If your browser’s Do Not Track setting is enabled, the site does not create or send this identifier. If this identifier is created, you can remove it at any time by clearing the `msUserId` entry from your browser’s localStorage for docs.strapi.io. ::: From 6eaab99e7103af5cad780404580b166f7eab540f Mon Sep 17 00:00:00 2001 From: Pierre Wizla <4233866+pwizla@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:50:44 +0100 Subject: [PATCH 06/18] Rework usage information page --- docusaurus/docs/cms/usage-information.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docusaurus/docs/cms/usage-information.md b/docusaurus/docs/cms/usage-information.md index 7eaf2fe5d0..9cd1972932 100644 --- a/docusaurus/docs/cms/usage-information.md +++ b/docusaurus/docs/cms/usage-information.md @@ -27,7 +27,7 @@ However, these above actions alone are often insufficient to maintain an overall Without these metrics, we wouldn't be able to make the right choices as we continue to move forward with the roadmap and provide what you, the community and users, are asking for. -## Collected data +## Collected Strapi-related data The following data is collected: @@ -83,7 +83,7 @@ Data collection can later be re-enabled by deleting the flag or setting it to fa If you have any questions or concerns regarding data collection, please contact us at the following email address [privacy@strapi.io](mailto:privacy@strapi.io). ::: -## Search analytics for docs.strapi.io +## Collected search-related data for docs.strapi.io To improve our documentation, the public website at `docs.strapi.io` collects anonymous search usage metrics using Meilisearch Cloud. These metrics help us understand how the search performs and where we can make it better: @@ -94,10 +94,10 @@ To improve our documentation, the public website at `docs.strapi.io` collects an To make the “Total users” metric more accurate while preserving privacy, the site creates a pseudonymous identifier in the browser’s localStorage under the `msUserId` key. It is randomly generated, contains no personal data, rotates on the first visit of each calendar month (UTC), and is sent only with documentation search requests as the `X-MS-USER-ID` header. It is not used for any other purpose. -:::note Note: Privacy choices -If your browser’s Do Not Track setting is enabled, the site does not create or send this identifier. +We do not send click-through or conversion events to Meilisearch. -If this identifier is created, you can remove it at any time by clearing the `msUserId` entry from your browser’s localStorage for docs.strapi.io. -::: +### Opt-out -We do not send click-through or conversion events to Meilisearch. +If your browser’s Do Not Track setting is enabled, the site does not create or send this identifier. + +If this identifier is created, you can remove it at any time by clearing the `msUserId` entry from your browser’s localStorage for docs.strapi.io. \ No newline at end of file From aefc42bf28cae1019c4c12164239f5f1872ac270 Mon Sep 17 00:00:00 2001 From: Pierre Wizla <4233866+pwizla@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:51:08 +0100 Subject: [PATCH 07/18] Fix duplicate labels in sidebar --- docusaurus/sidebars.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js index 03b7a41e58..9ad0a66e67 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -363,6 +363,7 @@ const sidebars = { type: 'doc', id: 'cms/customization', // TODO: rename to Introduction label: 'Introduction', + key: 'cms-customization-introduction', }, 'cms/configurations/functions', { @@ -376,6 +377,7 @@ const sidebars = { type: 'doc', id: 'cms/backend-customization', label: 'Overview', + key: 'cms-backend-customization-overview', }, 'cms/backend-customization/requests-responses', 'cms/backend-customization/routes', @@ -415,6 +417,7 @@ const sidebars = { type: 'doc', id: 'cms/admin-panel-customization', label: 'Overview', + key: 'cms-admin-panel-customization-overview', }, 'cms/admin-panel-customization/logos', 'cms/admin-panel-customization/favicon', @@ -457,6 +460,7 @@ const sidebars = { type: 'doc', id: 'cms/typescript', label: 'Introduction', + key: 'cms-typescript-introduction', }, { type: 'doc', From 59a389350f7179748fbdcada6bfabe5f8bed4796 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 4 Dec 2025 16:54:39 +0000 Subject: [PATCH 08/18] Attach X-MS-USER-ID for XHR-based Meilisearch requests --- docusaurus/src/theme/SearchBar/index.js | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index c9a004c1d9..9f3bb9f5d7 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -120,6 +120,32 @@ function SearchBarContent() { }; } + // Also patch XMLHttpRequest for libraries that use XHR under the hood + const originalXHROpen = (typeof XMLHttpRequest !== 'undefined' && XMLHttpRequest.prototype.open) ? XMLHttpRequest.prototype.open : null; + const originalXHRSend = (typeof XMLHttpRequest !== 'undefined' && XMLHttpRequest.prototype.send) ? XMLHttpRequest.prototype.send : null; + let xhrPatched = false; + if (originalXHROpen && originalXHRSend) { + try { + XMLHttpRequest.prototype.open = function(method, url, async, user, password) { + try { this.__ms_url = url; } catch {} + return originalXHROpen.apply(this, arguments); + }; + XMLHttpRequest.prototype.send = function(body) { + try { + if (userId && this && typeof this.setRequestHeader === 'function') { + const url = this.__ms_url || ''; + if (shouldAttachUserIdHeader(url)) { + // Only set if not already set + try { this.setRequestHeader('X-MS-USER-ID', userId); } catch {} + } + } + } catch {} + return originalXHRSend.apply(this, arguments); + }; + xhrPatched = true; + } catch {} + } + if (searchInstanceRef.current) { searchInstanceRef.current.destroy?.(); searchInstanceRef.current = null; @@ -237,6 +263,12 @@ function SearchBarContent() { } catch {} originalFetchRef.current = null; } + if (xhrPatched && originalXHROpen && originalXHRSend) { + try { + XMLHttpRequest.prototype.open = originalXHROpen; + XMLHttpRequest.prototype.send = originalXHRSend; + } catch {} + } if (searchInstanceRef.current) { searchInstanceRef.current.destroy?.(); searchInstanceRef.current = null; From fbe478416476c4922f654a25103601bf8a0ca5d0 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 12:39:48 +0000 Subject: [PATCH 09/18] Add X-MS-USER-ID via docsearch requestConfig and headers --- docusaurus/src/theme/SearchBar/index.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index 9f3bb9f5d7..c2c077a765 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -157,7 +157,7 @@ function SearchBarContent() { import('meilisearch-docsearch'), import('meilisearch-docsearch/css') ]).then(([{ docsearch }]) => { - const search = docsearch({ + const baseOptions = { container: searchButtonRef.current, host: siteConfig.customFields.meilisearch.host, apiKey: siteConfig.customFields.meilisearch.apiKey, @@ -241,7 +241,19 @@ function SearchBarContent() { getMissingResultsUrl: ({ query }) => { return `https://github.com/strapi/documentation/issues/new?title=Missing+search+results+for+${query}`; }, - }); + }; + + // Prefer official header wiring if library supports it + if (userId) { + // Some versions accept requestConfig.headers, others accept headers; set both safely + baseOptions.requestConfig = { + ...(baseOptions.requestConfig || {}), + headers: { 'X-MS-USER-ID': userId } + }; + baseOptions.headers = { ...(baseOptions.headers || {}), 'X-MS-USER-ID': userId }; + } + + const search = docsearch(baseOptions); searchInstanceRef.current = search; setIsLoaded(true); From f738a47ca31363f4fa9a00c8b2d29b70336c39a5 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 12:40:45 +0000 Subject: [PATCH 10/18] Add X-MS-USER-ID via docsearch options and guard init timing --- docusaurus/src/theme/SearchBar/index.js | 72 +++++++++++++------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index c2c077a765..4ccb78fcc7 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -60,9 +60,6 @@ function SearchBarContent() { } useEffect(() => { - if (!searchButtonRef.current) { - return; - } const handleKeyDown = (e) => { const kapaContainer = document.getElementById('kapa-widget-container'); @@ -150,18 +147,22 @@ function SearchBarContent() { searchInstanceRef.current.destroy?.(); searchInstanceRef.current = null; } - - searchButtonRef.current.innerHTML = ''; - Promise.all([ - import('meilisearch-docsearch'), - import('meilisearch-docsearch/css') - ]).then(([{ docsearch }]) => { - const baseOptions = { - container: searchButtonRef.current, - host: siteConfig.customFields.meilisearch.host, - apiKey: siteConfig.customFields.meilisearch.apiKey, - indexUid: siteConfig.customFields.meilisearch.indexUid, + if (searchButtonRef.current) { + searchButtonRef.current.innerHTML = ''; + } + + // Initialize docsearch only when container is ready + if (searchButtonRef.current) { + Promise.all([ + import('meilisearch-docsearch'), + import('meilisearch-docsearch/css') + ]).then(([{ docsearch }]) => { + const baseOptions = { + container: searchButtonRef.current, + host: siteConfig.customFields.meilisearch.host, + apiKey: siteConfig.customFields.meilisearch.apiKey, + indexUid: siteConfig.customFields.meilisearch.indexUid, transformItems: (items) => { return items.map((item) => { @@ -241,31 +242,32 @@ function SearchBarContent() { getMissingResultsUrl: ({ query }) => { return `https://github.com/strapi/documentation/issues/new?title=Missing+search+results+for+${query}`; }, - }; - - // Prefer official header wiring if library supports it - if (userId) { - // Some versions accept requestConfig.headers, others accept headers; set both safely - baseOptions.requestConfig = { - ...(baseOptions.requestConfig || {}), - headers: { 'X-MS-USER-ID': userId } }; - baseOptions.headers = { ...(baseOptions.headers || {}), 'X-MS-USER-ID': userId }; - } - const search = docsearch(baseOptions); + // Prefer official header wiring if library supports it + if (userId) { + // Some versions accept requestConfig.headers, others accept headers; set both safely + baseOptions.requestConfig = { + ...(baseOptions.requestConfig || {}), + headers: { 'X-MS-USER-ID': userId } + }; + baseOptions.headers = { ...(baseOptions.headers || {}), 'X-MS-USER-ID': userId }; + } - searchInstanceRef.current = search; - setIsLoaded(true); + const search = docsearch(baseOptions); - if (colorMode === 'dark') { - dropdownRef.current?.classList.add('dark'); - } else { - dropdownRef.current?.classList.remove('dark'); - } - }).catch((error) => { - console.error('Failed to load MeiliSearch:', error); - }); + searchInstanceRef.current = search; + setIsLoaded(true); + + if (colorMode === 'dark') { + dropdownRef.current?.classList.add('dark'); + } else { + dropdownRef.current?.classList.remove('dark'); + } + }).catch((error) => { + console.error('Failed to load MeiliSearch:', error); + }); + } return () => { document.removeEventListener('keydown', handleKeyDown, true); From 6408d50c0071cbde574adb41072d0f84801990c3 Mon Sep 17 00:00:00 2001 From: Pierre Wizla <4233866+pwizla@users.noreply.github.com> Date: Fri, 5 Dec 2025 14:22:48 +0100 Subject: [PATCH 11/18] Remove `key` declarations in sidebar Only useful for later Docusaurus upgrades --- docusaurus/sidebars.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js index 9ad0a66e67..cdc5ace439 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -363,7 +363,7 @@ const sidebars = { type: 'doc', id: 'cms/customization', // TODO: rename to Introduction label: 'Introduction', - key: 'cms-customization-introduction', + // key: 'cms-customization-introduction', }, 'cms/configurations/functions', { @@ -377,7 +377,7 @@ const sidebars = { type: 'doc', id: 'cms/backend-customization', label: 'Overview', - key: 'cms-backend-customization-overview', + // key: 'cms-backend-customization-overview', }, 'cms/backend-customization/requests-responses', 'cms/backend-customization/routes', @@ -417,7 +417,7 @@ const sidebars = { type: 'doc', id: 'cms/admin-panel-customization', label: 'Overview', - key: 'cms-admin-panel-customization-overview', + // key: 'cms-admin-panel-customization-overview', }, 'cms/admin-panel-customization/logos', 'cms/admin-panel-customization/favicon', @@ -460,7 +460,7 @@ const sidebars = { type: 'doc', id: 'cms/typescript', label: 'Introduction', - key: 'cms-typescript-introduction', + // key: 'cms-typescript-introduction', }, { type: 'doc', From c18bbce3dc43aa96900915b89ac5ede68cd82ab2 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 13:32:03 +0000 Subject: [PATCH 12/18] Route docsearch to Vercel proxy and forward X-MS-USER-ID server-side --- docusaurus/api/meili-search.js | 49 +++++++++++++++++++++++++ docusaurus/docusaurus.config.js | 4 +- docusaurus/src/theme/SearchBar/index.js | 13 ++++++- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 docusaurus/api/meili-search.js diff --git a/docusaurus/api/meili-search.js b/docusaurus/api/meili-search.js new file mode 100644 index 0000000000..4a5bc4fd07 --- /dev/null +++ b/docusaurus/api/meili-search.js @@ -0,0 +1,49 @@ +export default async function handler(req, res) { + if (req.method !== 'POST') { + res.status(405).json({ error: 'Method Not Allowed' }); + return; + } + + try { + const { indexUid } = req.query || {}; + if (!indexUid) { + res.status(400).json({ error: 'Missing indexUid' }); + return; + } + + const host = process.env.MEILI_HOST || process.env.NEXT_PUBLIC_MEILI_HOST || ''; + const apiKey = process.env.MEILI_API_KEY || process.env.NEXT_PUBLIC_MEILI_API_KEY || ''; + if (!host || !apiKey) { + res.status(500).json({ error: 'Meilisearch host or API key not configured' }); + return; + } + + const userIdFromCookie = (() => { + try { + const cookie = req.headers['cookie'] || ''; + const match = cookie.match(/(?:^|;\s*)msUserId=([^;]+)/); + return match ? decodeURIComponent(match[1]) : undefined; + } catch { + return undefined; + } + })(); + + const url = new URL(`/indexes/${encodeURIComponent(indexUid)}/search`, host); + const meiliRes = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'X-Meilisearch-Client': req.headers['x-meilisearch-client'] || 'StrapiDocs Proxy', + ...(userIdFromCookie ? { 'X-MS-USER-ID': userIdFromCookie } : {}), + }, + body: JSON.stringify(req.body || {}), + }); + + const data = await meiliRes.json(); + res.status(meiliRes.status).json(data); + } catch (e) { + res.status(500).json({ error: e.message || 'Proxy error' }); + } +} + diff --git a/docusaurus/docusaurus.config.js b/docusaurus/docusaurus.config.js index 5037ef7b8a..f3a11a22e7 100644 --- a/docusaurus/docusaurus.config.js +++ b/docusaurus/docusaurus.config.js @@ -130,8 +130,8 @@ const config = { ], customFields: { meilisearch: { - host: 'https://ms-47f23e4f6fb9-30446.fra.meilisearch.io', - apiKey: '45326fd7e6278ec3fc83af7a5c20a2ab4261f8591bd186adf8bf8f962581622b', + host: process.env.NEXT_PUBLIC_MEILI_HOST || 'https://ms-47f23e4f6fb9-30446.fra.meilisearch.io', + apiKey: process.env.NEXT_PUBLIC_MEILI_API_KEY || '45326fd7e6278ec3fc83af7a5c20a2ab4261f8591bd186adf8bf8f962581622b', indexUid: 'strapi-docs', searchParams: { attributesToHighlight: null diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index 4ccb78fcc7..e3ebbdc048 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -39,6 +39,12 @@ function SearchBarContent() { const uuid = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36); const value = JSON.stringify({ id: uuid, month: monthKey }); window.localStorage.setItem(key, value); + try { + // Also set a same‑origin cookie so serverless proxy can read it + const ttlDays = 45; // longer than a month for safety; we rotate monthly client‑side + const maxAge = ttlDays * 24 * 60 * 60; + document.cookie = `msUserId=${uuid}; Max-Age=${maxAge}; Path=/; SameSite=Lax` + (window.location.protocol === 'https:' ? '; Secure' : ''); + } catch {} return uuid; } catch (_) { return null; @@ -158,9 +164,12 @@ function SearchBarContent() { import('meilisearch-docsearch'), import('meilisearch-docsearch/css') ]).then(([{ docsearch }]) => { + const meiliHost = siteConfig.customFields.meilisearch.host; + // Use proxy on non-localhost to avoid CORS limits and inject server-side header + const useProxy = typeof window !== 'undefined' && window.location && window.location.hostname !== 'localhost'; const baseOptions = { container: searchButtonRef.current, - host: siteConfig.customFields.meilisearch.host, + host: useProxy ? `${window.location.origin}/api` : meiliHost, apiKey: siteConfig.customFields.meilisearch.apiKey, indexUid: siteConfig.customFields.meilisearch.indexUid, @@ -256,6 +265,8 @@ function SearchBarContent() { const search = docsearch(baseOptions); + // If using proxy, docsearch will call `${origin}/api/indexes//search` + searchInstanceRef.current = search; setIsLoaded(true); From 0a0080b02974b23316218bdadc882bc22ff57d96 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 13:37:14 +0000 Subject: [PATCH 13/18] Simplify: remove proxy; keep monthly msUserId only --- docusaurus/api/meili-search.js | 49 ------------------------- docusaurus/src/theme/SearchBar/index.js | 10 +---- 2 files changed, 2 insertions(+), 57 deletions(-) delete mode 100644 docusaurus/api/meili-search.js diff --git a/docusaurus/api/meili-search.js b/docusaurus/api/meili-search.js deleted file mode 100644 index 4a5bc4fd07..0000000000 --- a/docusaurus/api/meili-search.js +++ /dev/null @@ -1,49 +0,0 @@ -export default async function handler(req, res) { - if (req.method !== 'POST') { - res.status(405).json({ error: 'Method Not Allowed' }); - return; - } - - try { - const { indexUid } = req.query || {}; - if (!indexUid) { - res.status(400).json({ error: 'Missing indexUid' }); - return; - } - - const host = process.env.MEILI_HOST || process.env.NEXT_PUBLIC_MEILI_HOST || ''; - const apiKey = process.env.MEILI_API_KEY || process.env.NEXT_PUBLIC_MEILI_API_KEY || ''; - if (!host || !apiKey) { - res.status(500).json({ error: 'Meilisearch host or API key not configured' }); - return; - } - - const userIdFromCookie = (() => { - try { - const cookie = req.headers['cookie'] || ''; - const match = cookie.match(/(?:^|;\s*)msUserId=([^;]+)/); - return match ? decodeURIComponent(match[1]) : undefined; - } catch { - return undefined; - } - })(); - - const url = new URL(`/indexes/${encodeURIComponent(indexUid)}/search`, host); - const meiliRes = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'X-Meilisearch-Client': req.headers['x-meilisearch-client'] || 'StrapiDocs Proxy', - ...(userIdFromCookie ? { 'X-MS-USER-ID': userIdFromCookie } : {}), - }, - body: JSON.stringify(req.body || {}), - }); - - const data = await meiliRes.json(); - res.status(meiliRes.status).json(data); - } catch (e) { - res.status(500).json({ error: e.message || 'Proxy error' }); - } -} - diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index e3ebbdc048..a0c6e0cffd 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -39,12 +39,6 @@ function SearchBarContent() { const uuid = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36); const value = JSON.stringify({ id: uuid, month: monthKey }); window.localStorage.setItem(key, value); - try { - // Also set a same‑origin cookie so serverless proxy can read it - const ttlDays = 45; // longer than a month for safety; we rotate monthly client‑side - const maxAge = ttlDays * 24 * 60 * 60; - document.cookie = `msUserId=${uuid}; Max-Age=${maxAge}; Path=/; SameSite=Lax` + (window.location.protocol === 'https:' ? '; Secure' : ''); - } catch {} return uuid; } catch (_) { return null; @@ -165,8 +159,8 @@ function SearchBarContent() { import('meilisearch-docsearch/css') ]).then(([{ docsearch }]) => { const meiliHost = siteConfig.customFields.meilisearch.host; - // Use proxy on non-localhost to avoid CORS limits and inject server-side header - const useProxy = typeof window !== 'undefined' && window.location && window.location.hostname !== 'localhost'; + // Always call Meili directly (no proxy) since env config isn’t accessible + const useProxy = false; const baseOptions = { container: searchButtonRef.current, host: useProxy ? `${window.location.origin}/api` : meiliHost, From 04a51cea072c73dbd82d20254a53650e84b9d923 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 13:37:59 +0000 Subject: [PATCH 14/18] Revert "Simplify: remove proxy; keep monthly msUserId only" This reverts commit 0a0080b02974b23316218bdadc882bc22ff57d96. --- docusaurus/api/meili-search.js | 49 +++++++++++++++++++++++++ docusaurus/src/theme/SearchBar/index.js | 10 ++++- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 docusaurus/api/meili-search.js diff --git a/docusaurus/api/meili-search.js b/docusaurus/api/meili-search.js new file mode 100644 index 0000000000..4a5bc4fd07 --- /dev/null +++ b/docusaurus/api/meili-search.js @@ -0,0 +1,49 @@ +export default async function handler(req, res) { + if (req.method !== 'POST') { + res.status(405).json({ error: 'Method Not Allowed' }); + return; + } + + try { + const { indexUid } = req.query || {}; + if (!indexUid) { + res.status(400).json({ error: 'Missing indexUid' }); + return; + } + + const host = process.env.MEILI_HOST || process.env.NEXT_PUBLIC_MEILI_HOST || ''; + const apiKey = process.env.MEILI_API_KEY || process.env.NEXT_PUBLIC_MEILI_API_KEY || ''; + if (!host || !apiKey) { + res.status(500).json({ error: 'Meilisearch host or API key not configured' }); + return; + } + + const userIdFromCookie = (() => { + try { + const cookie = req.headers['cookie'] || ''; + const match = cookie.match(/(?:^|;\s*)msUserId=([^;]+)/); + return match ? decodeURIComponent(match[1]) : undefined; + } catch { + return undefined; + } + })(); + + const url = new URL(`/indexes/${encodeURIComponent(indexUid)}/search`, host); + const meiliRes = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'X-Meilisearch-Client': req.headers['x-meilisearch-client'] || 'StrapiDocs Proxy', + ...(userIdFromCookie ? { 'X-MS-USER-ID': userIdFromCookie } : {}), + }, + body: JSON.stringify(req.body || {}), + }); + + const data = await meiliRes.json(); + res.status(meiliRes.status).json(data); + } catch (e) { + res.status(500).json({ error: e.message || 'Proxy error' }); + } +} + diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index a0c6e0cffd..e3ebbdc048 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -39,6 +39,12 @@ function SearchBarContent() { const uuid = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36); const value = JSON.stringify({ id: uuid, month: monthKey }); window.localStorage.setItem(key, value); + try { + // Also set a same‑origin cookie so serverless proxy can read it + const ttlDays = 45; // longer than a month for safety; we rotate monthly client‑side + const maxAge = ttlDays * 24 * 60 * 60; + document.cookie = `msUserId=${uuid}; Max-Age=${maxAge}; Path=/; SameSite=Lax` + (window.location.protocol === 'https:' ? '; Secure' : ''); + } catch {} return uuid; } catch (_) { return null; @@ -159,8 +165,8 @@ function SearchBarContent() { import('meilisearch-docsearch/css') ]).then(([{ docsearch }]) => { const meiliHost = siteConfig.customFields.meilisearch.host; - // Always call Meili directly (no proxy) since env config isn’t accessible - const useProxy = false; + // Use proxy on non-localhost to avoid CORS limits and inject server-side header + const useProxy = typeof window !== 'undefined' && window.location && window.location.hostname !== 'localhost'; const baseOptions = { container: searchButtonRef.current, host: useProxy ? `${window.location.origin}/api` : meiliHost, From e595f142b6c942c5df4c2da81f92569d3842ad17 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 13:49:10 +0000 Subject: [PATCH 15/18] Revert "Route docsearch to Vercel proxy and forward X-MS-USER-ID server-side" This reverts commit c18bbce3dc43aa96900915b89ac5ede68cd82ab2. --- docusaurus/api/meili-search.js | 49 ------------------------- docusaurus/docusaurus.config.js | 4 +- docusaurus/src/theme/SearchBar/index.js | 13 +------ 3 files changed, 3 insertions(+), 63 deletions(-) delete mode 100644 docusaurus/api/meili-search.js diff --git a/docusaurus/api/meili-search.js b/docusaurus/api/meili-search.js deleted file mode 100644 index 4a5bc4fd07..0000000000 --- a/docusaurus/api/meili-search.js +++ /dev/null @@ -1,49 +0,0 @@ -export default async function handler(req, res) { - if (req.method !== 'POST') { - res.status(405).json({ error: 'Method Not Allowed' }); - return; - } - - try { - const { indexUid } = req.query || {}; - if (!indexUid) { - res.status(400).json({ error: 'Missing indexUid' }); - return; - } - - const host = process.env.MEILI_HOST || process.env.NEXT_PUBLIC_MEILI_HOST || ''; - const apiKey = process.env.MEILI_API_KEY || process.env.NEXT_PUBLIC_MEILI_API_KEY || ''; - if (!host || !apiKey) { - res.status(500).json({ error: 'Meilisearch host or API key not configured' }); - return; - } - - const userIdFromCookie = (() => { - try { - const cookie = req.headers['cookie'] || ''; - const match = cookie.match(/(?:^|;\s*)msUserId=([^;]+)/); - return match ? decodeURIComponent(match[1]) : undefined; - } catch { - return undefined; - } - })(); - - const url = new URL(`/indexes/${encodeURIComponent(indexUid)}/search`, host); - const meiliRes = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'X-Meilisearch-Client': req.headers['x-meilisearch-client'] || 'StrapiDocs Proxy', - ...(userIdFromCookie ? { 'X-MS-USER-ID': userIdFromCookie } : {}), - }, - body: JSON.stringify(req.body || {}), - }); - - const data = await meiliRes.json(); - res.status(meiliRes.status).json(data); - } catch (e) { - res.status(500).json({ error: e.message || 'Proxy error' }); - } -} - diff --git a/docusaurus/docusaurus.config.js b/docusaurus/docusaurus.config.js index f3a11a22e7..5037ef7b8a 100644 --- a/docusaurus/docusaurus.config.js +++ b/docusaurus/docusaurus.config.js @@ -130,8 +130,8 @@ const config = { ], customFields: { meilisearch: { - host: process.env.NEXT_PUBLIC_MEILI_HOST || 'https://ms-47f23e4f6fb9-30446.fra.meilisearch.io', - apiKey: process.env.NEXT_PUBLIC_MEILI_API_KEY || '45326fd7e6278ec3fc83af7a5c20a2ab4261f8591bd186adf8bf8f962581622b', + host: 'https://ms-47f23e4f6fb9-30446.fra.meilisearch.io', + apiKey: '45326fd7e6278ec3fc83af7a5c20a2ab4261f8591bd186adf8bf8f962581622b', indexUid: 'strapi-docs', searchParams: { attributesToHighlight: null diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index e3ebbdc048..4ccb78fcc7 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -39,12 +39,6 @@ function SearchBarContent() { const uuid = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : Math.random().toString(36).slice(2) + Date.now().toString(36); const value = JSON.stringify({ id: uuid, month: monthKey }); window.localStorage.setItem(key, value); - try { - // Also set a same‑origin cookie so serverless proxy can read it - const ttlDays = 45; // longer than a month for safety; we rotate monthly client‑side - const maxAge = ttlDays * 24 * 60 * 60; - document.cookie = `msUserId=${uuid}; Max-Age=${maxAge}; Path=/; SameSite=Lax` + (window.location.protocol === 'https:' ? '; Secure' : ''); - } catch {} return uuid; } catch (_) { return null; @@ -164,12 +158,9 @@ function SearchBarContent() { import('meilisearch-docsearch'), import('meilisearch-docsearch/css') ]).then(([{ docsearch }]) => { - const meiliHost = siteConfig.customFields.meilisearch.host; - // Use proxy on non-localhost to avoid CORS limits and inject server-side header - const useProxy = typeof window !== 'undefined' && window.location && window.location.hostname !== 'localhost'; const baseOptions = { container: searchButtonRef.current, - host: useProxy ? `${window.location.origin}/api` : meiliHost, + host: siteConfig.customFields.meilisearch.host, apiKey: siteConfig.customFields.meilisearch.apiKey, indexUid: siteConfig.customFields.meilisearch.indexUid, @@ -265,8 +256,6 @@ function SearchBarContent() { const search = docsearch(baseOptions); - // If using proxy, docsearch will call `${origin}/api/indexes//search` - searchInstanceRef.current = search; setIsLoaded(true); From 3047ea23d428b271300d5948d615bbb61c328aef Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 13:56:43 +0000 Subject: [PATCH 16/18] Route search to same-origin API and pass X-MS-USER-ID header --- docusaurus/api/indexes/[indexUid]/search.js | 46 +++++++++++++++++++++ docusaurus/src/theme/SearchBar/index.js | 6 +-- 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 docusaurus/api/indexes/[indexUid]/search.js diff --git a/docusaurus/api/indexes/[indexUid]/search.js b/docusaurus/api/indexes/[indexUid]/search.js new file mode 100644 index 0000000000..08935d96f2 --- /dev/null +++ b/docusaurus/api/indexes/[indexUid]/search.js @@ -0,0 +1,46 @@ +export default async function handler(req, res) { + if (req.method !== 'POST') { + res.status(405).json({ error: 'Method Not Allowed' }); + return; + } + + try { + const { indexUid } = req.query || {}; + if (!indexUid) { + res.status(400).json({ error: 'Missing indexUid' }); + return; + } + + // Read Meilisearch project URL and key. Fallback to values used in Docusaurus config if envs are not set. + const host = process.env.MEILI_HOST || process.env.NEXT_PUBLIC_MEILI_HOST || 'https://ms-47f23e4f6fb9-30446.fra.meilisearch.io'; + const apiKey = process.env.MEILI_API_KEY || process.env.NEXT_PUBLIC_MEILI_API_KEY || '45326fd7e6278ec3fc83af7a5c20a2ab4261f8591bd186adf8bf8f962581622b'; + + // Forward X-MS-USER-ID if present from the browser request (same-origin; no CORS issue) + const userId = req.headers['x-ms-user-id']; + + const url = new URL(`/indexes/${encodeURIComponent(indexUid)}/search`, host); + const upstream = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'X-Meilisearch-Client': req.headers['x-meilisearch-client'] || 'StrapiDocs Proxy', + ...(userId ? { 'X-MS-USER-ID': String(userId) } : {}), + }, + body: JSON.stringify(req.body || {}), + }); + + const body = await upstream.text(); + // Pass through status and JSON body + res.status(upstream.status); + try { + res.setHeader('Content-Type', 'application/json'); + res.send(body); + } catch { + res.json({ error: 'Invalid upstream response' }); + } + } catch (e) { + res.status(500).json({ error: e.message || 'Proxy error' }); + } +} + diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index 4ccb78fcc7..c44afa1f9f 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -160,7 +160,8 @@ function SearchBarContent() { ]).then(([{ docsearch }]) => { const baseOptions = { container: searchButtonRef.current, - host: siteConfig.customFields.meilisearch.host, + // Route through same-origin API to add headers server-side without CORS + host: `${window.location.origin}/api`, apiKey: siteConfig.customFields.meilisearch.apiKey, indexUid: siteConfig.customFields.meilisearch.indexUid, @@ -244,9 +245,8 @@ function SearchBarContent() { }, }; - // Prefer official header wiring if library supports it + // Send X-MS-USER-ID to same-origin API; no CORS preflight restrictions if (userId) { - // Some versions accept requestConfig.headers, others accept headers; set both safely baseOptions.requestConfig = { ...(baseOptions.requestConfig || {}), headers: { 'X-MS-USER-ID': userId } From 9088dd3b75af5da54c9df6414efb4c028b5ccdd7 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 15:02:45 +0000 Subject: [PATCH 17/18] docs(search): route docsearch via same-origin API and attach X-MS-USER-ID; add root API proxy; forward Meilisearch host/key via headers\n\n- Add api/indexes/[indexUid]/search.js to proxy POST /api/indexes/:indexUid/search to Meilisearch and set X-MS-USER-ID server-side\n- Keep host/apiKey in docusaurus.config.js but pass via headers (X-Meili-Host, X-Meili-Api-Key) to avoid literals in server code\n- Update SearchBar to use same-origin /api host; respect DNT and monthly rotation for msUserId\n- Restore search UI while enabling user-bound analytics without CORS issues --- api/indexes/[indexUid]/search.js | 52 +++++++++++++++++++++++++ docusaurus/src/theme/SearchBar/index.js | 26 +++++++++---- 2 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 api/indexes/[indexUid]/search.js diff --git a/api/indexes/[indexUid]/search.js b/api/indexes/[indexUid]/search.js new file mode 100644 index 0000000000..fbdbd289cd --- /dev/null +++ b/api/indexes/[indexUid]/search.js @@ -0,0 +1,52 @@ +export default async function handler(req, res) { + if (req.method !== 'POST') { + res.status(405).json({ error: 'Method Not Allowed' }); + return; + } + + try { + const { indexUid } = req.query || {}; + if (!indexUid) { + res.status(400).json({ error: 'Missing indexUid' }); + return; + } + + // Read Meilisearch project URL and key from headers or env + const hostHeader = req.headers['x-meili-host']; + const keyHeader = req.headers['x-meili-api-key']; + const host = (typeof hostHeader === 'string' && hostHeader) || process.env.MEILI_HOST || process.env.NEXT_PUBLIC_MEILI_HOST; + const apiKey = (typeof keyHeader === 'string' && keyHeader) || process.env.MEILI_API_KEY || process.env.NEXT_PUBLIC_MEILI_API_KEY; + + if (!host || !apiKey) { + res.status(500).json({ error: 'Meilisearch host or API key not configured' }); + return; + } + + // Forward X-MS-USER-ID if present from the browser request (same-origin; no CORS issue) + const userId = req.headers['x-ms-user-id']; + + const url = new URL(`/indexes/${encodeURIComponent(indexUid)}/search`, host); + const upstream = await fetch(url.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'X-Meilisearch-Client': req.headers['x-meilisearch-client'] || 'StrapiDocs Proxy', + ...(userId ? { 'X-MS-USER-ID': String(userId) } : {}), + }, + body: JSON.stringify(req.body || {}), + }); + + const body = await upstream.text(); + res.status(upstream.status); + try { + res.setHeader('Content-Type', 'application/json'); + res.send(body); + } catch { + res.json({ error: 'Invalid upstream response' }); + } + } catch (e) { + res.status(500).json({ error: e.message || 'Proxy error' }); + } +} + diff --git a/docusaurus/src/theme/SearchBar/index.js b/docusaurus/src/theme/SearchBar/index.js index c44afa1f9f..4eaf4fcc2d 100644 --- a/docusaurus/src/theme/SearchBar/index.js +++ b/docusaurus/src/theme/SearchBar/index.js @@ -162,7 +162,8 @@ function SearchBarContent() { container: searchButtonRef.current, // Route through same-origin API to add headers server-side without CORS host: `${window.location.origin}/api`, - apiKey: siteConfig.customFields.meilisearch.apiKey, + // dummy key for docsearch client; real key sent via header to API + apiKey: 'public', indexUid: siteConfig.customFields.meilisearch.indexUid, transformItems: (items) => { @@ -246,13 +247,22 @@ function SearchBarContent() { }; // Send X-MS-USER-ID to same-origin API; no CORS preflight restrictions - if (userId) { - baseOptions.requestConfig = { - ...(baseOptions.requestConfig || {}), - headers: { 'X-MS-USER-ID': userId } - }; - baseOptions.headers = { ...(baseOptions.headers || {}), 'X-MS-USER-ID': userId }; - } + const meiliHost = siteConfig.customFields.meilisearch.host; + const meiliKey = siteConfig.customFields.meilisearch.apiKey; + baseOptions.requestConfig = { + ...(baseOptions.requestConfig || {}), + headers: { + ...(userId ? { 'X-MS-USER-ID': userId } : {}), + ...(meiliHost ? { 'X-Meili-Host': meiliHost } : {}), + ...(meiliKey ? { 'X-Meili-Api-Key': meiliKey } : {}), + }, + }; + baseOptions.headers = { + ...(baseOptions.headers || {}), + ...(userId ? { 'X-MS-USER-ID': userId } : {}), + ...(meiliHost ? { 'X-Meili-Host': meiliHost } : {}), + ...(meiliKey ? { 'X-Meili-Api-Key': meiliKey } : {}), + }; const search = docsearch(baseOptions); From 4a96f631f905d26337e2e9286af177c720d01259 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 5 Dec 2025 15:09:29 +0000 Subject: [PATCH 18/18] docs(search): keep only root API route and fix path to /api/indexes/strapi-docs/search\n\n- Remove duplicate function under docusaurus/api\n- Replace dynamic [indexUid] route with fixed strapi-docs path to match our single index\n- Keep header-forwarded host/key and X-MS-USER-ID support --- .../{[indexUid] => strapi-docs}/search.js | 7 +-- docusaurus/api/indexes/[indexUid]/search.js | 46 ------------------- 2 files changed, 4 insertions(+), 49 deletions(-) rename api/indexes/{[indexUid] => strapi-docs}/search.js (89%) delete mode 100644 docusaurus/api/indexes/[indexUid]/search.js diff --git a/api/indexes/[indexUid]/search.js b/api/indexes/strapi-docs/search.js similarity index 89% rename from api/indexes/[indexUid]/search.js rename to api/indexes/strapi-docs/search.js index fbdbd289cd..d534f19c6d 100644 --- a/api/indexes/[indexUid]/search.js +++ b/api/indexes/strapi-docs/search.js @@ -6,8 +6,9 @@ export default async function handler(req, res) { try { const { indexUid } = req.query || {}; - if (!indexUid) { - res.status(400).json({ error: 'Missing indexUid' }); + // Enforce index name from path folder + if (indexUid !== 'strapi-docs') { + res.status(400).json({ error: 'Invalid indexUid' }); return; } @@ -25,7 +26,7 @@ export default async function handler(req, res) { // Forward X-MS-USER-ID if present from the browser request (same-origin; no CORS issue) const userId = req.headers['x-ms-user-id']; - const url = new URL(`/indexes/${encodeURIComponent(indexUid)}/search`, host); + const url = new URL(`/indexes/strapi-docs/search`, host); const upstream = await fetch(url.toString(), { method: 'POST', headers: { diff --git a/docusaurus/api/indexes/[indexUid]/search.js b/docusaurus/api/indexes/[indexUid]/search.js deleted file mode 100644 index 08935d96f2..0000000000 --- a/docusaurus/api/indexes/[indexUid]/search.js +++ /dev/null @@ -1,46 +0,0 @@ -export default async function handler(req, res) { - if (req.method !== 'POST') { - res.status(405).json({ error: 'Method Not Allowed' }); - return; - } - - try { - const { indexUid } = req.query || {}; - if (!indexUid) { - res.status(400).json({ error: 'Missing indexUid' }); - return; - } - - // Read Meilisearch project URL and key. Fallback to values used in Docusaurus config if envs are not set. - const host = process.env.MEILI_HOST || process.env.NEXT_PUBLIC_MEILI_HOST || 'https://ms-47f23e4f6fb9-30446.fra.meilisearch.io'; - const apiKey = process.env.MEILI_API_KEY || process.env.NEXT_PUBLIC_MEILI_API_KEY || '45326fd7e6278ec3fc83af7a5c20a2ab4261f8591bd186adf8bf8f962581622b'; - - // Forward X-MS-USER-ID if present from the browser request (same-origin; no CORS issue) - const userId = req.headers['x-ms-user-id']; - - const url = new URL(`/indexes/${encodeURIComponent(indexUid)}/search`, host); - const upstream = await fetch(url.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'X-Meilisearch-Client': req.headers['x-meilisearch-client'] || 'StrapiDocs Proxy', - ...(userId ? { 'X-MS-USER-ID': String(userId) } : {}), - }, - body: JSON.stringify(req.body || {}), - }); - - const body = await upstream.text(); - // Pass through status and JSON body - res.status(upstream.status); - try { - res.setHeader('Content-Type', 'application/json'); - res.send(body); - } catch { - res.json({ error: 'Invalid upstream response' }); - } - } catch (e) { - res.status(500).json({ error: e.message || 'Proxy error' }); - } -} -