|
2 | 2 | {{ end }} |
3 | 3 |
|
4 | 4 | {{ define "main" }} |
5 | | - <article class="prose max-w-none dark:prose-invert"> |
6 | | - <h1>404</h1> |
7 | | - <p> |
8 | | - There might be a mistake in the URL or you might've clicked a link to |
9 | | - content that no longer exists. If you think it's the latter, please file |
10 | | - an issue in our issue tracker on GitHub. |
| 5 | + <div class="flex min-h-[60vh] flex-col items-center justify-center py-16 text-center"> |
| 6 | + |
| 7 | + <div class="mb-6 text-red-500 dark:text-red-400" style="width: 2.5rem; height: 2.5rem;"> |
| 8 | + <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width: 100%; height: 100%;"> |
| 9 | + <path d="M12 8.00008V12.0001M12 16.0001H12.01M3 7.94153V16.0586C3 16.4013 3 16.5726 3.05048 16.7254C3.09515 16.8606 3.16816 16.9847 3.26463 17.0893C3.37369 17.2077 3.52345 17.2909 3.82297 17.4573L11.223 21.5684C11.5066 21.726 11.6484 21.8047 11.7985 21.8356C11.9315 21.863 12.0685 21.863 12.2015 21.8356C12.3516 21.8047 12.4934 21.726 12.777 21.5684L20.177 17.4573C20.4766 17.2909 20.6263 17.2077 20.7354 17.0893C20.8318 16.9847 20.9049 16.8606 20.9495 16.7254C21 16.5726 21 16.4013 21 16.0586V7.94153C21 7.59889 21 7.42756 20.9495 7.27477C20.9049 7.13959 20.8318 7.01551 20.7354 6.91082C20.6263 6.79248 20.4766 6.70928 20.177 6.54288L12.777 2.43177C12.4934 2.27421 12.3516 2.19543 12.2015 2.16454C12.0685 2.13721 11.9315 2.13721 11.7985 2.16454C11.6484 2.19543 11.5066 2.27421 11.223 2.43177L3.82297 6.54288C3.52345 6.70928 3.37369 6.79248 3.26463 6.91082C3.16816 7.01551 3.09515 7.13959 3.05048 7.27477C3 7.42756 3 7.59889 3 7.94153Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| 10 | + </svg> |
| 11 | + </div> |
| 12 | + |
| 13 | + <h1 class="mb-3 text-3xl font-bold text-gray-900 dark:text-white">Page not found</h1> |
| 14 | + |
| 15 | + <p class="mb-6 max-w-sm text-gray-500 dark:text-gray-400"> |
| 16 | + There might be a mistake in the URL or you might've clicked a broken link. |
| 17 | + Use the navigation, or try the following suggestions from Gordon. |
| 18 | + If you followed a link from a Docker product, consider filing an issue. |
11 | 19 | </p> |
12 | 20 |
|
13 | | - <a |
14 | | - id="newissue" |
15 | | - class="link" |
16 | | - href="{{ site.Params.repo }}/issues/new" |
17 | | - > |
18 | | - <strong>Create a new issue</strong></a |
19 | | - > |
20 | | - |
21 | | - <a href="{{ site.BaseURL }}"> |
22 | | - <figure> |
23 | | - <img src="/assets/images/404-docs.png" alt="404 page not found" /> |
24 | | - <figcaption class="link">Go to the homepage</figcaption> |
25 | | - </figure> |
26 | | - </a> |
27 | | - |
28 | | - <script> |
29 | | - let el = document.querySelector("#newissue"); |
30 | | - if (el) { |
31 | | - let url = new URL("{{ site.Params.repo }}/issues/new"); |
32 | | - url.searchParams.set("title", "404 at " + window.location.pathname); |
33 | | - let body = "I found a broken link : " + window.location.href; |
34 | | - if (document.referrer !== "") { |
35 | | - body += "\nI arrived on this page through: " + document.referrer |
| 21 | + <div class="mb-8 flex flex-wrap justify-center gap-3" x-data> |
| 22 | + <button |
| 23 | + @click="$store.gordon.open(window.location.pathname + ' was not found. Suggest alternative pages.', true)" |
| 24 | + class="cursor-pointer rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-600 transition hover:border-blue-500 hover:text-blue-600 dark:border-gray-700 dark:text-gray-300 dark:hover:border-blue-500 dark:hover:text-blue-400" |
| 25 | + > |
| 26 | + Ask Gordon for help |
| 27 | + </button> |
| 28 | + <a |
| 29 | + id="newissue" |
| 30 | + href="{{ site.Params.repo }}/issues/new" |
| 31 | + class="rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-600 transition hover:border-blue-500 hover:text-blue-600 dark:border-gray-700 dark:text-gray-300 dark:hover:border-blue-500 dark:hover:text-blue-400" |
| 32 | + > |
| 33 | + Create an issue |
| 34 | + </a> |
| 35 | + </div> |
| 36 | + |
| 37 | + <div class="w-full max-w-sm" x-data="gordon404()"> |
| 38 | + <p class="mb-3 text-base font-semibold text-gray-700 dark:text-gray-300"> |
| 39 | + Suggested pages |
| 40 | + </p> |
| 41 | + |
| 42 | + <ul x-show="isLoading || links.length > 0" class="space-y-2"> |
| 43 | + <template x-for="i in 5" :key="i"> |
| 44 | + <li class="flex h-6 items-center justify-center"> |
| 45 | + <template x-if="links[i - 1]"> |
| 46 | + <a |
| 47 | + :href="links[i - 1].url" |
| 48 | + x-text="links[i - 1].text" |
| 49 | + class="text-sm text-blue-600 hover:underline dark:text-blue-400" |
| 50 | + ></a> |
| 51 | + </template> |
| 52 | + <template x-if="!links[i - 1] && isLoading"> |
| 53 | + <div class="h-3.5 w-2/3 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div> |
| 54 | + </template> |
| 55 | + </li> |
| 56 | + </template> |
| 57 | + </ul> |
| 58 | + |
| 59 | + <p x-show="!isLoading && links.length === 0" class="text-sm text-gray-500 dark:text-gray-400"> |
| 60 | + No suggestions found. Try asking Gordon for help. |
| 61 | + </p> |
| 62 | + </div> |
| 63 | + |
| 64 | + </div> |
| 65 | + |
| 66 | + <script> |
| 67 | + function gordon404() { |
| 68 | + return { |
| 69 | + links: [], |
| 70 | + isLoading: true, |
| 71 | + |
| 72 | + async init() { |
| 73 | + const path = window.location.pathname; |
| 74 | + try { |
| 75 | + // Kick off both requests in parallel — metadata fetch is free while Gordon thinks |
| 76 | + const [gordonResponse, metadataResponse] = await Promise.all([ |
| 77 | + fetch(window.GORDON_BASE_URL + '/public/ask', { |
| 78 | + method: 'POST', |
| 79 | + headers: { 'Content-Type': 'application/json' }, |
| 80 | + body: JSON.stringify({ |
| 81 | + messages: [{ |
| 82 | + role: 'user', |
| 83 | + content: `The page "${path}" was not found. Suggest alternative pages.`, |
| 84 | + copilot_references: [{ |
| 85 | + data: { |
| 86 | + origin: 'docs-website', |
| 87 | + email: 'docs@docker.com', |
| 88 | + uuid: `docs-404-${Date.now()}`, |
| 89 | + action: 'AskGordon', |
| 90 | + page_url: window.location.href, |
| 91 | + page_title: '404 Not Found' |
| 92 | + } |
| 93 | + }] |
| 94 | + }] |
| 95 | + }) |
| 96 | + }), |
| 97 | + fetch('/metadata.json') |
| 98 | + ]); |
| 99 | + |
| 100 | + // Build pathname → title lookup from metadata (pathname only so local/prod domains don't matter) |
| 101 | + const titleMap = {}; |
| 102 | + if (metadataResponse.ok) { |
| 103 | + const metadata = await metadataResponse.json(); |
| 104 | + for (const item of metadata) { |
| 105 | + if (item.url && item.title) { |
| 106 | + try { |
| 107 | + const pathname = new URL(item.url).pathname.replace(/\/$/, ''); |
| 108 | + titleMap[pathname] = item.title; |
| 109 | + } catch {} |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + if (!gordonResponse.ok) return; |
| 115 | + |
| 116 | + // Drain the full stream into a buffer |
| 117 | + const reader = gordonResponse.body.getReader(); |
| 118 | + const decoder = new TextDecoder(); |
| 119 | + let buffer = ''; |
| 120 | + |
| 121 | + while (true) { |
| 122 | + const { done, value } = await reader.read(); |
| 123 | + if (done) break; |
| 124 | + for (const line of decoder.decode(value, { stream: true }).split('\n')) { |
| 125 | + if (!line.startsWith('data: ')) continue; |
| 126 | + const data = line.slice(6); |
| 127 | + if (data === '[DONE]') continue; |
| 128 | + try { |
| 129 | + const parsed = JSON.parse(data); |
| 130 | + if (parsed.choices?.[0]?.delta?.content) { |
| 131 | + buffer += parsed.choices[0].delta.content; |
| 132 | + } |
| 133 | + } catch {} |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + // Parse only the Sources section — structured, reliable URLs |
| 138 | + const sourcesSection = buffer.split(/\n+\*{0,2}Sources:?\*{0,2}\s*\n/i)[1] ?? ''; |
| 139 | + const seen = new Set(); |
| 140 | + for (const match of sourcesSection.matchAll(/https?:\/\/[^\s<>()\[\]"']+/g)) { |
| 141 | + const rawUrl = match[0].replace(/[.,;]+$/, ''); |
| 142 | + // Rewrite domain to current origin so links work locally too |
| 143 | + const url = rawUrl.replace(/^https?:\/\/[^/]+/, window.location.origin); |
| 144 | + if (!seen.has(url)) { |
| 145 | + seen.add(url); |
| 146 | + const pathname = new URL(rawUrl).pathname.replace(/\/$/, ''); |
| 147 | + const title = titleMap[pathname] ?? this.titleFromUrl(url); |
| 148 | + this.links.push({ url, text: title }); |
| 149 | + if (this.links.length >= 5) break; |
| 150 | + } |
| 151 | + } |
| 152 | + } catch {} |
| 153 | + finally { |
| 154 | + this.isLoading = false; |
36 | 155 | } |
37 | | - url.searchParams.set("body", body); |
38 | | - url.searchParams.set("template", "broken_link.yml") |
39 | | - url.searchParams.set("title", "[404]: " + window.location.pathname); |
40 | | - url.searchParams.set("target", window.location.href); |
41 | | - url.searchParams.set("location", document.referrer); |
42 | | - url.searchParams.set("labels", "status/triage"); |
43 | | - el.setAttribute("href", url.toString()); |
44 | | - } |
45 | | - </script> |
46 | | - </article> |
| 156 | + }, |
| 157 | + |
| 158 | + titleFromUrl(url) { |
| 159 | + try { |
| 160 | + return new URL(url).pathname |
| 161 | + .split('/').filter(Boolean) |
| 162 | + .map(s => s.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())) |
| 163 | + .join(' / '); |
| 164 | + } catch { |
| 165 | + return url; |
| 166 | + } |
| 167 | + }, |
| 168 | + |
| 169 | + }; |
| 170 | + } |
| 171 | + |
| 172 | + const issueLink = document.querySelector('#newissue'); |
| 173 | + if (issueLink) { |
| 174 | + const url = new URL('{{ site.Params.repo }}/issues/new'); |
| 175 | + url.searchParams.set('title', '[404]: ' + window.location.pathname); |
| 176 | + let body = 'I found a broken link: ' + window.location.href; |
| 177 | + if (document.referrer) body += '\nI arrived via: ' + document.referrer; |
| 178 | + url.searchParams.set('body', body); |
| 179 | + url.searchParams.set('template', 'broken_link.yml'); |
| 180 | + url.searchParams.set('target', window.location.href); |
| 181 | + url.searchParams.set('location', document.referrer); |
| 182 | + url.searchParams.set('labels', 'status/triage'); |
| 183 | + issueLink.href = url.toString(); |
| 184 | + } |
| 185 | + </script> |
47 | 186 | {{ end }} |
0 commit comments