diff --git a/hugo.yaml b/hugo.yaml index e0f4acf255cf..2aaad8599d75 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -166,6 +166,10 @@ params: # Docs repository URL repo: https://github.com/docker/docs + # MCP endpoint that serves semantic search over the docs. Used by the 404 page. + # The stage backend has no /mcp route, so every environment uses production. + docsSearchURL: https://ai-backend-service.docker.com/mcp + # === Example versions === # The following parameters are used in various ways in content and templates. # Mostly as examples in code snippets, but also in other exotic ways. diff --git a/layouts/404.html b/layouts/404.html index 412c16f7dc1c..d9ade290c7b5 100644 --- a/layouts/404.html +++ b/layouts/404.html @@ -2,46 +2,161 @@ {{ end }} {{ define "main" }} -
-

404

-

- There might be a mistake in the URL or you might've clicked a link to - content that no longer exists. If you think it's the latter, please file - an issue in our issue tracker on GitHub. +

+ +
+ + + +
+ +

Page not found

+ +

+ There might be a mistake in the URL, or you might have followed a broken + link. Use the navigation, or try one of the suggested pages below. If you + followed a link from a Docker product, consider filing an issue.

- - Create a new issue - - -
- 404 page not found - -
-
- - -
+ + try { + // The search tool only returns results for queries that mention + // Docker, so prefix the words taken from the broken URL. + const message = 'Docker documentation: ' + words.join(' '); + const response = await fetch('{{ site.Params.docsSearchURL }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json, text/event-stream', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'search_docker_docs', arguments: { message } }, + }), + }); + if (!response.ok) return; + + const result = await response.json(); + const documents = result?.result?.structuredContent?.documents ?? []; + + const seen = new Set(); + for (const doc of documents) { + const source = doc?.metadata?.source; + if (!source) continue; + + // Rewrite the origin so links also work in previews and locally. + const url = new URL(source); + url.protocol = window.location.protocol; + url.host = window.location.host; + if (url.pathname === path || seen.has(url.pathname)) continue; + + seen.add(url.pathname); + this.links.push({ + url: url.toString(), + text: this.titleFromContent(doc.content) || this.titleFromPath(url.pathname), + }); + if (this.links.length >= this.maxLinks) break; + } + } catch { + } finally { + this.isLoading = false; + } + }, + + // Chunks start with the page title, optionally followed by " > section". + titleFromContent(content) { + if (typeof content !== 'string') return ''; + const firstLine = content.split('\n')[0]; + return firstLine.replace(/^#+\s*/, '').split(' > ')[0].trim(); + }, + + titleFromPath(pathname) { + return pathname + .split('/') + .filter(Boolean) + .map((s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())) + .join(' / '); + }, + }; + } + + const issueLink = document.querySelector('#newissue'); + if (issueLink) { + const url = new URL('{{ site.Params.repo }}/issues/new'); + let body = 'I found a broken link: ' + window.location.href; + if (document.referrer) body += '\nI arrived via: ' + document.referrer; + url.searchParams.set('body', body); + url.searchParams.set('template', 'broken_link.yml'); + url.searchParams.set('title', '[404]: ' + window.location.pathname); + url.searchParams.set('target', window.location.href); + url.searchParams.set('location', document.referrer); + url.searchParams.set('labels', 'status/triage'); + issueLink.href = url.toString(); + } + {{ end }} diff --git a/static/assets/images/404-docs.png b/static/assets/images/404-docs.png deleted file mode 100644 index 8343579b9d56..000000000000 Binary files a/static/assets/images/404-docs.png and /dev/null differ