Skip to content

Commit 3fc02fb

Browse files
gordon 404
Signed-off-by: Craig Osterhout <craig.osterhout@docker.com>
1 parent ac5a254 commit 3fc02fb

1 file changed

Lines changed: 178 additions & 39 deletions

File tree

layouts/404.html

Lines changed: 178 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,185 @@
22
{{ end }}
33

44
{{ 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.
1119
</p>
1220

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;
36155
}
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>
47186
{{ end }}

0 commit comments

Comments
 (0)