Langflow ships an embed widget. For Astra Docs Chat
I wanted a full page that matches the rest of jamieede.com: same header, same typography, no iframe chrome. This post covers the Hugo layout and the vanilla JavaScript that streams markdown answers from /api/astra-chat.
Series context: Building Astra Docs Chat
Related: Proxy · Langflow chat flow · DeepSeek swap
The API contract this UI expects is documented in Proxying Langflow from Cloudflare Pages Functions .
Live page: Astra Docs Chat
Why not the embed widget? ¶
Langflow’s web component works for internal tools. For a public portfolio site I wanted:
- Same site header, footer, and theme tokens as
/analyzer - No iframe sizing quirks or third-party widget CSS
- Full control over markdown rendering and code block wrapping
- A single pattern I can copy for the next tool
The trade-off is maintenance: you own the JS. For this site, that was already true on the analyzer page.
File layout ¶
content/astra-chat.md → front matter, url: /astra-chat
layouts/astra-chat/single.html → full-page shell (header/footer from theme)
static/css/astra-chat.css → chat panel, bubbles, code wrap
static/js/astra-chat.js → fetch, SSE parse, markdown render
config.toml → nav link: "Astra Chat"
content/astra-chat.md sets layout: astra-chat and url: /astra-chat. The HTML template loads marked from jsDelivr and the chat script at the bottom of the page.
The layout mirrors Document Analyzer : one custom Hugo layout, one JS module, no build step beyond Hugo itself.
Page structure ¶
The shell is intentionally boring:
- Message thread (
#chat-messages,aria-live="polite") - Starter prompt buttons (
#starters) - Form with textarea and Send
Starter prompts match common doc questions:
- “How do I create a collection in Astra DB?”
- “What are PCU groups?”
- “Explain hybrid search in Astra DB Serverless.”
A welcome assistant bubble is injected on load so the page is never an empty box.
CSS adds an astra-chat-shell--active class once the user sends a message or clicks a starter. That tightens vertical space and de-emphasises the starter row after the first turn.
An “How it works” section below the panel links back to the parent technical write-up and mentions 271 pages, the Cloudflare proxy, and DeepSeek + Astra DB.
Session continuity ¶
Langflow accepts a session_id on each run so follow-up questions stay in context. The UI generates a UUID once and stores it in sessionStorage:
const SESSION_KEY = 'astra-chat-session-id';
function getSessionId() {
let id = sessionStorage.getItem(SESSION_KEY);
if (!id) {
id = crypto.randomUUID();
sessionStorage.setItem(SESSION_KEY, id);
}
return id;
}
Refresh the page: same session. Close the tab: new session. There is no server-side history UI in v1; this is lightweight continuity, not a chat archive.
Each POST includes { message, session_id }.
Sending a message ¶
Flow:
- Append user bubble (markdown-rendered for consistency)
- Disable Send while in flight
- Append assistant bubble with blinking cursor (
▋) fetch('/api/astra-chat', { method: 'POST', ... })- Read
response.bodyas a stream - On each SSE chunk, append text and re-render markdown + cursor
- On
[DONE]or stream end, remove cursor; re-enable Send
Mid-stream answer on /astra-chat: starter prompts, markdown, and fenced code visible before the stream finishes.
Non-OK responses parse { error } from JSON when possible and show an error-styled bubble: “Message required”, “Request timed out”, or a generic network message.
Enter submits; Shift+Enter inserts a newline in the textarea.
Parsing Server-Sent Events ¶
The proxy emits lines like:
data: {"chunk":"partial text"}
data: [DONE]
The client buffers incomplete lines, splits on \n, and handles only lines starting with data: :
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') break;
const { chunk } = JSON.parse(data);
fullText += chunk;
assistantBubble.innerHTML = renderMarkdown(fullText) +
'<span class="astra-chat-cursor">▋</span>';
Same pattern as the analyzer’s streaming reader: copy-paste friendly if you add more chat tools to the site.
Incremental markdown ¶
Re-parsing the full assistant text on every chunk is simple and good enough for doc-style answers. marked runs with { breaks: true } so single newlines render as line breaks.
Trade-off: mid-stream markdown can briefly look wrong until closing fences arrive (e.g. half a code block). In practice, code blocks usually stabilise within a second or two of streaming.
CSS wraps long lines inside code blocks so API samples do not blow out the panel width: overflow-wrap and white-space: pre-wrap on .astra-chat-bubble pre code. Without that, horizontal scroll on narrow viewports breaks the chat panel layout.
Starter prompts ¶
Starter buttons use data-prompt attributes and delegate clicks from #starters:
<button type="button" class="astra-chat-starter" data-prompt="What are PCU groups?">
PCU groups
</button>
Clicking a starter calls the same sendMessage() path as manual input: no special API.
What is not in the front end ¶
- No Langflow URL
- No API keys
- No WebSocket: HTTP POST + SSE only
- No client-side retrieval or embedding
- No source citations panel in v1
All of that stays in Langflow + the Pages Function.
Local development ¶
hugo server: page atlocalhost:1313/astra-chat- Run Pages Functions locally with secrets (
wrangler pages dev public) so/api/astra-chatresolves
Without step 2, the UI loads but chat POSTs 404 or fail.
Set Cloudflare secrets the same way as production:
wrangler pages secret put LANGFLOW_URL --project-name jamieedecom
wrangler pages secret put LANGFLOW_API_KEY --project-name jamieedecom
Compared to Langflow’s embed ¶
| Custom Hugo page | Langflow embed | |
|---|---|---|
| Visual match to site | Yes | iframe / widget styling |
| Secrets in browser | No (with proxy) | Depends on hosting |
| Markdown control | Full (marked, your CSS) |
Widget defaults |
| Maintenance | You own JS | Upstream widget updates |
For a portfolio site with an existing analyzer pattern, custom won. The same SSE parsing approach appears on Document Analyzer ; for Worker-side streaming details see Rebuilding the document analyzer on Cloudflare’s full stack .
Next in the series ¶
- Re-ingesting a RAG doc corpus when upstream docs change : keeping the corpus fresh for this UI
- Docs-only guardrails when retrieval finds nothing : keeping answers honest when context is empty
- Batch-ingesting markdown through Langflow : trilogy part 2 (edge → ingest → browser)
Series index: Building Astra Docs Chat
Open Astra Docs Chat
and watch the Network tab while a message streams: you should see only same-origin /api/astra-chat, never Langflow.