Adding an AI Chat Assistant to a Static Website
Most personal websites are one-way streets: you publish, visitors read. What if they could ask instead? In this post I walk through adding a small AI assistant — powered by Cloudflare Workers AI — that lets anyone query my experience and résumé in plain English, with zero backend server to maintain and, under typical free-tier usage, zero API costs.
Why bother?
A PDF résumé is a frozen snapshot. Visitors have to skim it, guess what is relevant to them, and hope they find the right section. A recruiter looking for Python experience and a professor curious about your teaching history need very different things — but they both have to read the same document.
An AI assistant changes that dynamic. Instead of making the visitor do the work, you let them ask in plain language: “Does he have any experience with time-series forecasting?” or “What level does he teach at?” and get a direct, personalised answer in seconds.
The goal of this post is to build exactly that, in a way that is:
- Free — Cloudflare Workers has a generous free tier (100,000 requests/day); Cloudflare Workers AI also runs on the free tier, so this stack costs nothing to operate.
- Maintenance-free — no server to patch, no database, no uptime to worry about.
- Portable — the approach works with any static site generator (Quarto, Hugo, Jekyll, plain HTML…).
The big picture
Three moving parts, one simple flow:
Visitor's browser
│ 1. types a question
▼
Chat widget (HTML/JS)
│ 2. POST { message, history } to Worker URL
▼
Cloudflare Worker ──► Cloudflare Workers AI
│ │
│ 3. system prompt │ 4. returns answer
│ + message │
◄─────────────────────┘
│ 5. forwards { reply }
▼
Chat widget displays reply
The widget is an inline HTML+JS block in index.qmd. It captures the visitor’s message and sends it to the Worker.
The Worker is a tiny JavaScript function running on Cloudflare’s global edge network. It prepends a system prompt containing your full background, then forwards the conversation to Cloudflare Workers AI using the env.AI binding — no external API key required.
The key insight is that the browser never talks to the AI model directly — everything goes through your Worker, where you can add rate limits or filters later if you need them.
Part 1 — The Cloudflare Worker
Wrangler is the official Cloudflare CLI. Install it once, authenticate, and you’re ready to deploy:
npm install -g wrangler
wrangler login # opens a browser window for OAuthThe project needs two files inside a worker/ directory at your site root.
website/
├── assets/
│ └── dark.scss # site theme — includes chat widget styles
├── posts/
│ └── resume-chat/
│ └── index.qmd # this post
├── worker/
│ ├── index.js # Cloudflare Worker — calls Workers AI, returns reply
│ └── wrangler.toml # Worker config — AI binding, model var, deployment
├── _quarto.yml # site-wide Quarto config
└── index.qmd # landing page — embeds the chat widget
wrangler.toml is the configuration:
name = "resume-chat"
main = "index.js"
compatibility_date = "2024-09-01"
[ai]
binding = "AI"
[vars]
AI_MODEL = "@cf/meta/llama-3.1-8b-instruct"name sets the subdomain (resume-chat.<account>.workers.dev); main points to the Worker entry-point. The [ai] binding gives the Worker access to env.AI — Cloudflare’s built-in AI runtime, no external API key needed. AI_MODEL in [vars] makes the model configurable: change it in wrangler.toml and redeploy without touching any code.
The real work happens in index.js. Scroll through the walkthrough below.
Deploying
No secrets to manage — the AI binding is configured in wrangler.toml and Cloudflare handles everything else. Just deploy:
cd worker/
wrangler deploy
# → Deployed: https://resume-chat.<account>.workers.devTest it immediately from the terminal:
curl -X POST https://resume-chat.<account>.workers.dev \
-H "Content-Type: application/json" \
-d '{"message": "What is his current job?"}'
# → {"reply":"Guillaume is currently a Regional Economist at the Banque de France…"}Part 2 — The chat widget
The widget is a raw HTML block embedded directly in index.qmd using Quarto’s {=html} fence. It has two parts: markup and JavaScript.
Part 3 — Integrating with Quarto
Quarto’s {=html} raw block passes content through to the rendered page verbatim. Drop the widget anywhere in a .qmd file:
```{=html}
<div id="chat-box">
…
</div>
<script>
(function () {
const WORKER_URL = "https://resume-chat.<account>.workers.dev";
…
})();
</script>
```
Because the block is scoped to index.qmd, the widget only appears on the landing page — not on blog posts or teaching pages, where it would be out of place. If you later want it site-wide, Quarto’s include-after-body option in _quarto.yml can inject an HTML file into every page instead.
Putting it all together
# 1. Install Wrangler
npm install -g wrangler
# 2. Authenticate with Cloudflare (creates a free account if needed)
wrangler login
# 3. Deploy the Worker (the [ai] binding in wrangler.toml is all you need)
cd worker/
wrangler deploy
# Note the URL printed: https://resume-chat.<subdomain>.workers.dev
# 4. Update the widget with your Worker URL
# Open index.qmd and change:
# const WORKER_URL = "https://resume-chat.YOUR-SUBDOMAIN.workers.dev";
# to your actual URL.
# 5. Rebuild the landing page
cd ..
quarto render index.qmd
# 6. Publish
git add -A
git commit -m "Add AI résumé chat assistant"
git pushWhat to customise
Update the résumé content — open worker/index.js and edit the SYSTEM_PROMPT string. Then run wrangler deploy again to push the update. The website itself does not need to be rebuilt.
Show the widget on every page — instead of embedding it in index.qmd, use Quarto’s include-after-body option in _quarto.yml:
format:
html:
include-after-body: _partials/chat-widget.htmlMove the HTML+JS block to _partials/chat-widget.html and quarto render will inject it at the bottom of every page.
Update the CORS origin — the Worker is already restricted to a single origin. When you adapt this for your own site, change ggilles.dev to your domain in worker/index.js:
"Access-Control-Allow-Origin": "https://your-domain.com",Swap the model — edit AI_MODEL in wrangler.toml and run wrangler deploy. No code change needed:
[vars]
AI_MODEL = "@cf/mistral/mistral-7b-instruct-v0.1"Browse the full catalogue at developers.cloudflare.com/workers-ai/models.
Closing thoughts
The whole implementation is roughly 150-200 lines of code spread across three files (worker/index.js, assets/dark.scss, and the raw HTML block in index.qmd). Despite its small footprint, it covers several important concepts that show up in many real-world projects: serverless functions, CORS, prompt engineering, and progressive enhancement (the site works perfectly for visitors who never open the chat).
If you replicate this on your own site, feel free to open a GitHub issue or reach out on LinkedIn — I’d be happy to help you debug it.