Skip to content

Commit 726267d

Browse files
vishrclaude
andcommitted
feat(website): rebrand live Docusaurus site + interim redesign
Replace the LabStack cube favicon/logo with Echo's own mark, add a real 1200x630 social card (the configured docusaurus-social-card.jpg never existed), and the interim Docusaurus redesign (Ask Echo / DocActions, custom theme, phosphor icons). This is the currently-live site; the Astro rebuild in site/ supersedes it at cutover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c39465f commit 726267d

12 files changed

Lines changed: 858 additions & 273 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ vendor
55
.hugo_build.lock
66

77
.superpowers/
8+
.serena/
9+
.playwright-mcp/

website/docusaurus.config.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const darkTheme = themes.dracula;
99
const config = {
1010
title: 'Echo',
1111
tagline: 'High performance, extensible, minimalist Go web framework',
12-
favicon: 'img/favicon.ico',
12+
favicon: 'img/favicon.svg',
1313

1414
// Set the production url of your site here
1515
url: 'https://echo.labstack.com/',
@@ -32,9 +32,16 @@ const config = {
3232
// to replace "en" with "zh-Hans".
3333
i18n: {
3434
defaultLocale: 'en',
35-
locales: ['en'],
35+
locales: ['en', 'zh-Hans', 'ja', 'es', 'fr'],
3636
},
3737

38+
// Phosphor icon set (used by the Ask Echo palette and doc action toolbar).
39+
stylesheets: [
40+
'https://unpkg.com/@phosphor-icons/web@2.1.1/src/regular/style.css',
41+
'https://unpkg.com/@phosphor-icons/web@2.1.1/src/fill/style.css',
42+
'https://unpkg.com/@phosphor-icons/web@2.1.1/src/duotone/style.css',
43+
],
44+
3845
presets: [
3946
[
4047
'classic',
@@ -103,8 +110,12 @@ const config = {
103110
themeConfig:
104111
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
105112
({
106-
// Replace with your project's social card
107-
image: 'img/docusaurus-social-card.jpg',
113+
// Echo social card (1200×630) — the old docusaurus-social-card.jpg never existed.
114+
image: 'img/echo-social-card.png',
115+
colorMode: {
116+
defaultMode: 'dark',
117+
respectPrefersColorScheme: false,
118+
},
108119
navbar: {
109120
logo: {
110121
alt: 'Echo',
@@ -129,6 +140,10 @@ const config = {
129140
label: 'GitHub',
130141
position: 'right',
131142
},
143+
{
144+
type: 'localeDropdown',
145+
position: 'right',
146+
},
132147
],
133148
},
134149
footer: {

website/src/components/AskEcho.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import React, {useState, useEffect, useRef, useCallback} from 'react';
2+
3+
// Quick-start prompts shown before the user types.
4+
const SUGGESTIONS = [
5+
{icon: 'ph-lock-key', q: 'How do I add JWT authentication?'},
6+
{icon: 'ph-globe', q: 'How do I enable CORS?'},
7+
{icon: 'ph-link', q: 'How do I bind a JSON request body?'},
8+
{icon: 'ph-folder-open', q: 'How do I serve static files?'},
9+
];
10+
11+
// Demo answer used by the prototype. To make this real, replace `streamAnswer`
12+
// with a call to your RAG provider, e.g. kapa.ai:
13+
// const r = await fetch('https://api.kapa.ai/query/v1/projects/<id>/chat/',
14+
// {method:'POST', headers:{'X-API-KEY': KEY,'Content-Type':'application/json'},
15+
// body: JSON.stringify({query})});
16+
// then stream r.body. The UI below is provider-agnostic.
17+
const DEMO_ANSWER =
18+
`To add JWT authentication, use Echo's built-in <strong>JWT middleware</strong>. Register it on the routes (or group) you want to protect:
19+
20+
<pre><code>import echojwt "github.com/labstack/echo-jwt/v4"
21+
22+
e.Use(echojwt.WithConfig(echojwt.Config{
23+
SigningKey: []byte("your-secret"),
24+
}))</code></pre>
25+
26+
Requests must then send <code>Authorization: Bearer &lt;token&gt;</code>. Inside a handler, read claims via <code>c.Get("user")</code>. For login, issue a token with <strong>github.com/golang-jwt/jwt/v5</strong>.`;
27+
28+
const SOURCES = [
29+
'Guide › Middleware › JWT',
30+
'Cookbook › JWT Authentication',
31+
'API › echo-jwt',
32+
];
33+
34+
export default function AskEcho() {
35+
const [open, setOpen] = useState(false);
36+
const [query, setQuery] = useState('');
37+
const [answer, setAnswer] = useState('');
38+
const [thinking, setThinking] = useState(false);
39+
const [done, setDone] = useState(false);
40+
const inputRef = useRef(null);
41+
const timer = useRef(null);
42+
43+
const reset = () => {
44+
setQuery(''); setAnswer(''); setThinking(false); setDone(false);
45+
if (timer.current) clearInterval(timer.current);
46+
};
47+
const close = useCallback(() => { setOpen(false); reset(); }, []);
48+
const openPalette = useCallback(() => { reset(); setOpen(true); }, []);
49+
50+
useEffect(() => {
51+
const onKey = (e) => {
52+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
53+
e.preventDefault();
54+
setOpen((o) => { if (o) reset(); return !o; });
55+
}
56+
if (e.key === 'Escape') close();
57+
};
58+
const onOpen = () => openPalette();
59+
window.addEventListener('keydown', onKey);
60+
window.addEventListener('ask-echo-open', onOpen);
61+
return () => {
62+
window.removeEventListener('keydown', onKey);
63+
window.removeEventListener('ask-echo-open', onOpen);
64+
if (timer.current) clearInterval(timer.current);
65+
};
66+
}, [close, openPalette]);
67+
68+
useEffect(() => {
69+
if (open && inputRef.current) setTimeout(() => inputRef.current.focus(), 40);
70+
}, [open]);
71+
72+
// Typewriter stream of the (demo) answer.
73+
const ask = (q) => {
74+
setQuery(q); setThinking(true); setAnswer(''); setDone(false);
75+
if (timer.current) clearInterval(timer.current);
76+
setTimeout(() => {
77+
setThinking(false);
78+
let i = 0;
79+
timer.current = setInterval(() => {
80+
i += 5;
81+
setAnswer(DEMO_ANSWER.slice(0, i));
82+
if (i >= DEMO_ANSWER.length) {
83+
clearInterval(timer.current);
84+
setAnswer(DEMO_ANSWER);
85+
setDone(true);
86+
}
87+
}, 12);
88+
}, 450);
89+
};
90+
91+
if (!open) {
92+
return (
93+
<button className="ask-fab" onClick={openPalette} aria-label="Ask Echo">
94+
<i className="ph ph-sparkle" /> Ask Echo <kbd>⌘K</kbd>
95+
</button>
96+
);
97+
}
98+
99+
return (
100+
<div className="ask-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) close(); }}>
101+
<div className="ask-palette" role="dialog" aria-label="Ask Echo">
102+
<div className="ask-top">
103+
<i className="ph ph-sparkle ask-spark" />
104+
<input
105+
ref={inputRef}
106+
className="ask-input"
107+
placeholder="Ask Echo a question…"
108+
value={query}
109+
onChange={(e) => setQuery(e.target.value)}
110+
onKeyDown={(e) => { if (e.key === 'Enter' && query.trim()) ask(query.trim()); }}
111+
autoComplete="off"
112+
/>
113+
<span className="ask-esc">ESC</span>
114+
</div>
115+
116+
<div className="ask-body">
117+
{!answer && !thinking && (
118+
<div className="ask-suggest">
119+
{SUGGESTIONS.map((s) => (
120+
<button key={s.q} className="ask-s" onClick={() => ask(s.q)}>
121+
<i className={`ph ${s.icon}`} /> {s.q}
122+
<span className="ask-k"></span>
123+
</button>
124+
))}
125+
</div>
126+
)}
127+
128+
{thinking && (
129+
<div className="ask-badge"><span className="ask-dot" /> Ask Echo is thinking…</div>
130+
)}
131+
132+
{answer && (
133+
<>
134+
<div className="ask-badge"><span className="ask-dot" /> Ask Echo</div>
135+
<div
136+
className="ask-answer"
137+
dangerouslySetInnerHTML={{ __html: answer + (done ? '' : '<span class="ask-cursor"></span>') }}
138+
/>
139+
{done && (
140+
<div className="ask-sources">
141+
<h5>Sources</h5>
142+
{SOURCES.map((s, i) => (
143+
<div className="ask-src" key={s}><span className="ask-n">{i + 1}</span> {s}</div>
144+
))}
145+
</div>
146+
)}
147+
</>
148+
)}
149+
</div>
150+
151+
<div className="ask-foot">
152+
<span><b></b> ask</span><span><b>esc</b> close</span>
153+
<span style={{marginLeft: 'auto'}}>Powered by <b>Ask Echo</b> · answers in your language</span>
154+
</div>
155+
</div>
156+
</div>
157+
);
158+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React, {useState} from 'react';
2+
import {useLocation} from '@docusaurus/router';
3+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
4+
5+
// Toolbar rendered at the top of every doc page: Ask Echo + "open in AI" deep links.
6+
export default function DocActions() {
7+
const {siteConfig} = useDocusaurusContext();
8+
const {pathname} = useLocation();
9+
const [copied, setCopied] = useState(false);
10+
11+
const url = (siteConfig.url || '') + (pathname || '');
12+
const prompt =
13+
`I'm reading the Echo (Go web framework) docs page: ${url} — help me understand it and write example code.`;
14+
const gpt = 'https://chatgpt.com/?q=' + encodeURIComponent(prompt);
15+
const claude = 'https://claude.ai/new?q=' + encodeURIComponent(prompt);
16+
17+
const ask = () => {
18+
if (typeof window !== 'undefined') window.dispatchEvent(new Event('ask-echo-open'));
19+
};
20+
const copy = () => {
21+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
22+
navigator.clipboard.writeText(url);
23+
setCopied(true);
24+
setTimeout(() => setCopied(false), 1400);
25+
}
26+
};
27+
28+
return (
29+
<div className="doc-actions">
30+
<button className="doc-act doc-act--primary" onClick={ask}>
31+
<i className="ph ph-sparkle" /> Ask Echo
32+
</button>
33+
<button className="doc-act" onClick={copy}>
34+
<i className={`ph ${copied ? 'ph-check' : 'ph-copy'}`} /> {copied ? 'Copied' : 'Copy'}
35+
</button>
36+
<a className="doc-act" href={gpt} target="_blank" rel="noopener noreferrer">
37+
<i className="ph ph-chat-circle-dots" /> ChatGPT
38+
</a>
39+
<a className="doc-act" href={claude} target="_blank" rel="noopener noreferrer">
40+
<i className="ph ph-asterisk" /> Claude
41+
</a>
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)