Skip to content

Commit b3df436

Browse files
authored
feat(docs): dynamic version badge + command-palette search empty state (#410)
feat(docs): dynamic version badge + command-palette search empty state
2 parents 49ae2db + 5b5fc34 commit b3df436

5 files changed

Lines changed: 111 additions & 3 deletions

File tree

site/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default defineConfig({
2929
components: {
3030
Footer: './src/components/Footer.astro',
3131
PageTitle: './src/components/PageTitle.astro',
32+
Search: './src/components/Search.astro',
3233
},
3334
head: [
3435
// Google Analytics (carried over from the Docusaurus site, anonymized IP).

site/src/components/HomeHero.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// Living Terminal hero: code window + a terminal pane that types `curl`
33
// and streams Echo's JSON response. Animation is client-side, with a
44
// static final-state fallback for reduced-motion / no-JS.
5-
import { starsLabel } from '../data/github.ts';
5+
import { starsLabel, echoVersion } from '../data/github.ts';
66
---
77

88
<section class="hh">
99
<div class="hh-left">
10-
<span class="hh-eyebrow"><i class="ph ph-sparkle"></i> Echo v5.2 — now released</span>
10+
<span class="hh-eyebrow"><i class="ph ph-sparkle"></i> Echo {echoVersion} — now released</span>
1111
<h1>Build fast Go APIs.<br /><span class="g">Without the bloat.</span></h1>
1212
<p class="hh-sub">
1313
A high-performance, minimalist Go web framework — a zero-allocation

site/src/components/Search.astro

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
import Default from '@astrojs/starlight/components/Search.astro';
3+
---
4+
<Default />
5+
6+
<script>
7+
// Command-palette empty state: inject quick links + a shortcut footer into the
8+
// Pagefind modal once it mounts. terminal.css shows it only while the query is
9+
// empty (the results drawer carries .pagefind-ui__hidden then).
10+
const PANEL = `
11+
<div class="echo-search-empty">
12+
<div class="ess-group">
13+
<div class="ess-label">Start here</div>
14+
<a class="ess-link" href="/guide/quickstart/"><i aria-hidden="true" class="ph ph-rocket-launch"></i> Quickstart <span class="ess-sec">Guide</span></a>
15+
<a class="ess-link" href="/guide/routing/"><i aria-hidden="true" class="ph ph-signpost"></i> Routing <span class="ess-sec">Guide</span></a>
16+
<a class="ess-link" href="/guide/binding/"><i aria-hidden="true" class="ph ph-brackets-curly"></i> Binding <span class="ess-sec">Guide</span></a>
17+
</div>
18+
<div class="ess-group">
19+
<div class="ess-label">Popular</div>
20+
<a class="ess-link" href="/middleware/jwt/"><i aria-hidden="true" class="ph ph-lock-key"></i> JWT <span class="ess-sec">Middleware</span></a>
21+
<a class="ess-link" href="/middleware/cors/"><i aria-hidden="true" class="ph ph-globe"></i> CORS <span class="ess-sec">Middleware</span></a>
22+
<a class="ess-link" href="/cookbook/hello-world/"><i aria-hidden="true" class="ph ph-cube"></i> Hello World <span class="ess-sec">Cookbook</span></a>
23+
</div>
24+
<div class="ess-foot">
25+
<span><kbd>↵</kbd>open</span><span><kbd>↑</kbd><kbd>↓</kbd>navigate</span><span><kbd>esc</kbd>close</span>
26+
</div>
27+
</div>`;
28+
29+
function inject() {
30+
const ui = document.querySelector('.pagefind-ui');
31+
if (!ui || ui.querySelector('.echo-search-empty')) return !!ui;
32+
const form = ui.querySelector('.pagefind-ui__form');
33+
if (!form) return false;
34+
form.insertAdjacentHTML('afterend', PANEL);
35+
return true;
36+
}
37+
38+
// Pagefind mounts lazily when the dialog first opens — watch for it, then stop.
39+
if (!inject()) {
40+
const obs = new MutationObserver(() => { if (inject()) obs.disconnect(); });
41+
obs.observe(document.body, { childList: true, subtree: true });
42+
// Bound the observer: if Pagefind never mounts (disabled in dev, or the
43+
// Starlight markup changed on upgrade), stop watching so it can't run for
44+
// the page's lifetime. Losing the launchpad is harmless; search still works.
45+
setTimeout(() => {
46+
obs.disconnect();
47+
if (import.meta.env.DEV && !document.querySelector('.echo-search-empty')) {
48+
console.warn('[search] empty-state panel not injected — .pagefind-ui selectors may have changed');
49+
}
50+
}, 10000);
51+
}
52+
</script>

site/src/data/github.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async function fetchStars(): Promise<number> {
1515
const token = process.env.GITHUB_TOKEN;
1616
if (token) headers.Authorization = `Bearer ${token}`;
1717

18-
const res = await fetch(`https://api.github.com/repos/${REPO}`, { headers });
18+
const res = await fetch(`https://api.github.com/repos/${REPO}`, { headers, signal: AbortSignal.timeout(5000) });
1919
if (!res.ok) throw new Error(`GitHub API responded ${res.status}`);
2020
const data = await res.json();
2121
return typeof data.stargazers_count === 'number' ? data.stargazers_count : FALLBACK_STARS;
@@ -29,3 +29,30 @@ export const stars = await fetchStars();
2929

3030
/** e.g. 32412 -> "32.4k" */
3131
export const starsLabel = stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars);
32+
33+
const FALLBACK_VERSION = 'v5';
34+
35+
// Latest released echo/v5, via the Go module proxy — canonical and v5-only
36+
// (avoids GitHub "latest release" occasionally pointing at a v4 patch).
37+
async function fetchLatestVersion(): Promise<string> {
38+
try {
39+
const res = await fetch('https://proxy.golang.org/github.com/labstack/echo/v5/@latest', {
40+
signal: AbortSignal.timeout(5000),
41+
});
42+
if (!res.ok) throw new Error(`Go proxy responded ${res.status}`);
43+
const data = await res.json();
44+
const v = typeof data.Version === 'string' ? data.Version.trim() : '';
45+
// Require a real "vX..." tag — an empty/garbage value would silently render a blank badge.
46+
if (!/^v\d/.test(v)) {
47+
console.warn(`[github] unexpected version ${JSON.stringify(data.Version)}; using fallback ${FALLBACK_VERSION}`);
48+
return FALLBACK_VERSION;
49+
}
50+
return v;
51+
} catch (e) {
52+
console.warn(`[github] version fetch failed; using fallback ${FALLBACK_VERSION}:`, e);
53+
return FALLBACK_VERSION;
54+
}
55+
}
56+
57+
/** e.g. "v5.2.1" */
58+
export const echoVersion = await fetchLatestVersion();

site/src/styles/terminal.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,34 @@
8787
/* search box (chrome) */
8888
site-search button { border-radius: 8px !important; }
8989

90+
/* Empty search modal: a command-palette launchpad (quick links + shortcut
91+
footer) injected by Search.astro, shown only before a query exists. Pagefind
92+
marks the results drawer .pagefind-ui__hidden when empty. */
93+
.echo-search-empty { display: none; }
94+
.pagefind-ui:has(.pagefind-ui__drawer.pagefind-ui__hidden) .echo-search-empty { display: block; }
95+
.echo-search-empty { margin-top: 16px; }
96+
.ess-group + .ess-group { margin-top: 14px; }
97+
.ess-label {
98+
font-family: var(--sl-font-mono); font-size: 0.66rem; letter-spacing: 0.12em;
99+
text-transform: uppercase; color: var(--sl-color-gray-4); padding: 0 4px 6px;
100+
}
101+
.ess-link {
102+
display: flex; align-items: center; gap: 11px; padding: 9px 12px; border-radius: 9px;
103+
text-decoration: none; color: var(--sl-color-white); font-size: 0.9rem; line-height: 1;
104+
}
105+
.ess-link i { font-size: 1.05rem; color: var(--echo-cyan-text); width: 1.2em; text-align: center; }
106+
.ess-link .ess-sec { margin-left: auto; font-family: var(--sl-font-mono); font-size: 0.7rem; color: var(--sl-color-gray-4); }
107+
.ess-link:hover, .ess-link:focus-visible { background: var(--echo-soft); outline: none; }
108+
.ess-foot {
109+
display: flex; gap: 18px; justify-content: center; flex-wrap: wrap;
110+
margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--sl-color-hairline);
111+
font-family: var(--sl-font-mono); font-size: 0.72rem; color: var(--sl-color-gray-4);
112+
}
113+
.ess-foot kbd {
114+
font-family: var(--sl-font-mono); font-size: 0.72rem; background: var(--sl-color-bg-inline-code);
115+
border: 1px solid var(--sl-color-hairline); border-radius: 5px; padding: 1px 6px; margin-right: 5px;
116+
}
117+
90118
/* code blocks — let Expressive Code round the whole frame as one unit
91119
(title bar: rounded top, code body: rounded bottom). Don't force the inner
92120
<pre> corners or it detaches from the title bar into a second box. */

0 commit comments

Comments
 (0)