<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Guillaume Gilles</title>
<link>https://ggilles.dev/blog.html</link>
<atom:link href="https://ggilles.dev/blog.xml" rel="self" type="application/rss+xml"/>
<description></description>
<generator>quarto-1.4.537</generator>
<lastBuildDate>Sat, 07 Mar 2026 23:00:00 GMT</lastBuildDate>
<item>
  <title>Adding an AI Chat Assistant to a Static Website</title>
  <dc:creator>Guillaume Gilles</dc:creator>
  <dc:creator>Claude Sonnet 4.6</dc:creator>
  <link>https://ggilles.dev/posts/resume-chat/</link>
  <description><![CDATA[ 




<section id="why-bother" class="level2">
<h2 class="anchored" data-anchor-id="why-bother">Why bother?</h2>
<p>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.</p>
<p>An AI assistant changes that dynamic. Instead of making the visitor do the work, you let them ask in plain language: <em>“Does he have any experience with time-series forecasting?”</em> or <em>“What level does he teach at?”</em> and get a direct, personalised answer in seconds.</p>
<p>The goal of this post is to build exactly that, in a way that is:</p>
<ul>
<li><strong>Free</strong> — 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.</li>
<li><strong>Maintenance-free</strong> — no server to patch, no database, no uptime to worry about.</li>
<li><strong>Portable</strong> — the approach works with any static site generator (Quarto, Hugo, Jekyll, plain HTML…).</li>
</ul>
</section>
<section id="the-big-picture" class="level2">
<h2 class="anchored" data-anchor-id="the-big-picture">The big picture</h2>
<p>Three moving parts, one simple flow:</p>
<pre><code>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</code></pre>
<p><strong>The widget</strong> is an inline HTML+JS block in <code>index.qmd</code>. It captures the visitor’s message and sends it to the Worker.</p>
<p><strong>The Worker</strong> is a tiny JavaScript function running on Cloudflare’s global edge network. It prepends a <em>system prompt</em> containing your full background, then forwards the conversation to Cloudflare Workers AI using the <code>env.AI</code> binding — no external API key required.</p>
<p><strong>The key insight</strong> 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.</p>
<hr>
</section>
<section id="part-1-the-cloudflare-worker" class="level2">
<h2 class="anchored" data-anchor-id="part-1-the-cloudflare-worker">Part 1 — The Cloudflare Worker</h2>
<p><a href="https://developers.cloudflare.com/workers/wrangler/">Wrangler</a> is the official Cloudflare CLI. Install it once, authenticate, and you’re ready to deploy:</p>
<div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">npm</span> install <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-g</span> wrangler</span>
<span id="cb2-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">wrangler</span> login   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># opens a browser window for OAuth</span></span></code></pre></div>
<p>The project needs two files inside a <code>worker/</code> directory at your site root.</p>
<pre><code>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</code></pre>
<p><code>wrangler.toml</code> is the configuration:</p>
<div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode toml code-with-copy"><code class="sourceCode toml"><span id="cb4-1"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">name</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"resume-chat"</span></span>
<span id="cb4-2"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">main</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"index.js"</span></span>
<span id="cb4-3"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">compatibility_date</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"2024-09-01"</span></span>
<span id="cb4-4"></span>
<span id="cb4-5"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">[ai]</span></span>
<span id="cb4-6"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">binding</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"AI"</span></span>
<span id="cb4-7"></span>
<span id="cb4-8"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">[vars]</span></span>
<span id="cb4-9"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">AI_MODEL</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"@cf/meta/llama-3.1-8b-instruct"</span></span></code></pre></div>
<p><code>name</code> sets the subdomain (<code>resume-chat.&lt;account&gt;.workers.dev</code>); <code>main</code> points to the Worker entry-point. The <code>[ai]</code> binding gives the Worker access to <code>env.AI</code> — Cloudflare’s built-in AI runtime, no external API key needed. <code>AI_MODEL</code> in <code>[vars]</code> makes the model configurable: change it in <code>wrangler.toml</code> and redeploy without touching any code.</p>
<p>The real work happens in <code>index.js</code>. Scroll through the walkthrough below.</p>
<div class="cr-section cr-column-screen sidebar-left">
<div class="narrative-col">
<div class="trigger new-trigger" data-focus-on="cr-prompt">
<div class="narrative">
<p>Here is a walkthrough of <code>worker/index.js</code>. Every section has a job — let’s go through them one by one.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-prompt">
<div class="narrative">
<p><strong>The system prompt</strong> is the most important part of the setup. It is prepended to <em>every</em> conversation before the visitor’s message. The instruction <em>“based strictly on the information below”</em> keeps the model from hallucinating anything that isn’t in your résumé. Keep this section updated whenever your situation changes.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-cors">
<div class="narrative">
<p><strong>CORS headers</strong> tell the browser that your Worker accepts cross-origin requests. Your widget lives on <code>ggilles.dev</code>; the Worker lives on <code>workers.dev</code> — without these headers, the browser would silently block every fetch. Setting <code>https://ggilles.dev</code> restricts access to the Worker only from your exact domain.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-preflight">
<div class="narrative">
<p><strong>The OPTIONS preflight</strong> is an automatic browser check sent before every cross-origin POST: <em>“Are you OK with this?”</em> The Worker must respond to it correctly, or the real request never goes through. Parsing the body is straightforward but worth wrapping in a try/catch — you want clear error responses rather than uncaught exceptions if something malformed hits the endpoint.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-messages">
<div class="narrative">
<p><strong>Building the messages array</strong> is where context lives. The system prompt anchors every conversation. <code>history.slice(-6)</code> keeps the last three exchanges (six messages: three user, three assistant) so the model can handle follow-up questions without ballooning the token count.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-ai">
<div class="narrative">
<p><strong>Calling Cloudflare Workers AI</strong> uses <code>env.AI.run()</code> with the model read from <code>env.AI_MODEL</code> (falling back to <code>@cf/meta/llama-3.1-8b-instruct</code> if the variable is unset). Keeping the model name in <code>wrangler.toml</code> means you can swap it without touching the Worker code — just edit <code>[vars]</code> and redeploy. <code>max_tokens: 400</code> caps reply length. The call is wrapped in a try/catch so failures return a clean, user-friendly error. On success, the Worker sends the browser a clean <code>{ reply: "…" }</code> JSON object with CORS headers attached so the browser actually accepts it.</p>
</div>
</div>
</div>
<div class="sticky-col">
<div class="sticky-col-stack">
<div id="cr-prompt" class="sticky">
<div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb5-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> SYSTEM_PROMPT <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">`You are an AI assistant representing Guillaume Gilles.</span></span>
<span id="cb5-2"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">Answer questions based strictly on the information below.</span></span>
<span id="cb5-3"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">Be concise, friendly, and professional.</span></span>
<span id="cb5-4"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">If asked about something not covered, say you don't have that information.</span></span>
<span id="cb5-5"></span>
<span id="cb5-6"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">--- PROFILE / EXPERIENCE / EDUCATION / SKILLS ---</span></span>
<span id="cb5-7"><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">…`</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div>
</div>
<div id="cr-cors" class="sticky">
<div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb6-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> CORS_HEADERS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb6-2">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Access-Control-Allow-Origin"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://ggilles.dev"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Access-Control-Allow-Methods"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"POST, OPTIONS"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-4">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Access-Control-Allow-Headers"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Content-Type"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb6-5">}<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div>
</div>
<div id="cr-preflight" class="sticky">
<div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb7-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">export</span> <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">default</span> {</span>
<span id="cb7-2">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">async</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fetch</span>(request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> env) {</span>
<span id="cb7-3"></span>
<span id="cb7-4">    <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">if</span> (request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">method</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">===</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"OPTIONS"</span>) {</span>
<span id="cb7-5">      <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">return</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">new</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">Response</span>(<span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">null</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> { <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">headers</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> CORS_HEADERS })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb7-6">    }</span>
<span id="cb7-7"></span>
<span id="cb7-8">    <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">let</span> body<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb7-9">    <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">try</span> {</span>
<span id="cb7-10">      body <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">await</span> request<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">json</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb7-11">    } <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">catch</span> {</span>
<span id="cb7-12">      <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">return</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">new</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">Response</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Invalid JSON"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> { <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">status</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">400</span> })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb7-13">    }</span>
<span id="cb7-14"></span>
<span id="cb7-15">    <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> { message<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> history <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [] } <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> body<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb7-16">  }<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb7-17">}<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div>
</div>
<div id="cr-messages" class="sticky">
<div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb8-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> messages <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [</span>
<span id="cb8-2">  { <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">role</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"system"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">content</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> SYSTEM_PROMPT }<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-3">  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span>history<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">slice</span>(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-4">  { <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">role</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"user"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">content</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> message }<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb8-5">]<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div>
</div>
<div id="cr-ai" class="sticky">
<div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb9-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">let</span> reply<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-2"><span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">try</span> {</span>
<span id="cb9-3">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> aiRes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">await</span> env<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">AI</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">run</span>(</span>
<span id="cb9-4">    env<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">AI_MODEL</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">??</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"@cf/meta/llama-3.1-8b-instruct"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb9-5">    { messages<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">max_tokens</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">400</span> }</span>
<span id="cb9-6">  )<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-7">  reply <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> aiRes<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">response</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-8">} <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">catch</span> (err) {</span>
<span id="cb9-9">  <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">return</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">new</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">Response</span>(</span>
<span id="cb9-10">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">JSON</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">stringify</span>({ <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">error</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"AI service unavailable. Please try again later."</span> })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb9-11">    { <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">status</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">502</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">headers</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> { <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Content-Type"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"application/json"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span>CORS_HEADERS } }</span>
<span id="cb9-12">  )<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb9-13">}</span>
<span id="cb9-14"></span>
<span id="cb9-15"><span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">return</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">new</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">Response</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">JSON</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">stringify</span>({ reply })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> {</span>
<span id="cb9-16">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">headers</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> { <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Content-Type"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"application/json"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">...</span>CORS_HEADERS }<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb9-17">})<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div>
</div>
</div>
</div>
</div>
<section id="deploying" class="level3">
<h3 class="anchored" data-anchor-id="deploying">Deploying</h3>
<p>No secrets to manage — the AI binding is configured in <code>wrangler.toml</code> and Cloudflare handles everything else. Just deploy:</p>
<div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> worker/</span>
<span id="cb10-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">wrangler</span> deploy</span>
<span id="cb10-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># → Deployed: https://resume-chat.&lt;account&gt;.workers.dev</span></span></code></pre></div>
<p>Test it immediately from the terminal:</p>
<div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">curl</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-X</span> POST https://resume-chat.<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span>account<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>.workers.dev <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb11-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-H</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Content-Type: application/json"</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb11-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'{"message": "What is his current job?"}'</span></span>
<span id="cb11-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># → {"reply":"Guillaume is currently a Regional Economist at the Banque de France…"}</span></span></code></pre></div>
<hr>
</section>
</section>
<section id="part-2-the-chat-widget" class="level2">
<h2 class="anchored" data-anchor-id="part-2-the-chat-widget">Part 2 — The chat widget</h2>
<p>The widget is a raw HTML block embedded directly in <code>index.qmd</code> using Quarto’s <code>{=html}</code> fence. It has two parts: markup and JavaScript.</p>
<div class="cr-section cr-column-screen sidebar-left">
<div class="narrative-col">
<div class="trigger new-trigger" data-focus-on="cr-html">
<div class="narrative">
<p>The complete widget — HTML structure up top, self-contained JavaScript below. Let’s walk through it.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-html">
<div class="narrative">
<p><strong>The HTML structure</strong> is minimal on purpose. <code>#chat-box</code> is a flex column: <code>#chat-messages</code> scrolls vertically as the conversation grows; <code>#chat-input-row</code> stays pinned at the bottom. No toggles, no overlays — the box lives inline in the page, right below the landing page text.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-state">
<div class="narrative">
<p><strong><code>WORKER_URL</code></strong> is the only value you need to change when you adapt this for your own site. <code>history</code> accumulates the conversation turn by turn; <code>busy</code> is a simple mutex that prevents double-sends while a fetch is in flight.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-append">
<div class="narrative">
<p><strong><code>appendMsg</code></strong> is a small factory: it creates a <code>div</code>, stamps it with the right CSS class (<code>user</code> or <code>bot</code>), appends it to the message area, and scrolls to the bottom. It returns the element so the caller can mutate it later — e.g.&nbsp;replace “Thinking…” with the actual reply in place.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-send">
<div class="narrative">
<p><strong><code>sendMessage</code></strong> opens with a guard clause — if the input is empty or a fetch is already running, it exits immediately. It then clears the input, renders the user bubble, and shows a “Thinking…” placeholder that gets replaced in-place once the response arrives. Both sides of the exchange are pushed into <code>history</code> for the next round. Error handling covers the case where the Worker is unreachable: after either path the busy flag is reset, the Send button re-enabled, and focus returned to the input.</p>
</div>
</div>
</div>
<div class="sticky-col">
<div class="sticky-col-stack">
<div id="cr-html" class="sticky">
<div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode html code-with-copy"><code class="sourceCode html"><span id="cb12-1"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">div</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> id</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-box"</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb12-2">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">div</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> id</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-messages"</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb12-3">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">div</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> class</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-msg bot"</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb12-4">      Hi! I'm an AI assistant. Ask me anything</span>
<span id="cb12-5">      about Guillaume's background. 👋</span>
<span id="cb12-6">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;/</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">div</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb12-7">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;/</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">div</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb12-8">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">div</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> id</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-input-row"</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb12-9">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">input</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> id</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-input"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> type</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"text"</span></span>
<span id="cb12-10"><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">           placeholder</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"e.g. What's his Python experience?"</span></span>
<span id="cb12-11"><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">           autocomplete</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"off"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> </span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">/&gt;</span></span>
<span id="cb12-12">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">button</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;"> id</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-send"</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span>Send<span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;/</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">button</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb12-13">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;/</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">div</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span>
<span id="cb12-14"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&lt;/</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">div</span><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">&gt;</span></span></code></pre></div>
</div>
<div id="cr-state" class="sticky">
<div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb13-1">(<span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">function</span> () {</span>
<span id="cb13-2">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> WORKER_URL <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://resume-chat.&lt;account&gt;.workers.dev"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-3"></span>
<span id="cb13-4">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> msgs  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">document</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">getElementById</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-messages"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-5">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> input <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">document</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">getElementById</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-input"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-6">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> send  <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">document</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">getElementById</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-send"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-7">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">let</span> history <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-8">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">let</span> busy    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">false</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-9"></span>
<span id="cb13-10">  send<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">addEventListener</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"click"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> sendMessage)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-11">  input<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">addEventListener</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"keydown"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> (e) <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">=&gt;</span> {</span>
<span id="cb13-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">if</span> (e<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">key</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">===</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Enter"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;&amp;</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span>e<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">shiftKey</span>) {</span>
<span id="cb13-13">      e<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">preventDefault</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sendMessage</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-14">    }</span>
<span id="cb13-15">  })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb13-16">})()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span></code></pre></div>
</div>
<div id="cr-append" class="sticky">
<div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb14-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">function</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">appendMsg</span>(text<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> role) {</span>
<span id="cb14-2">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> div <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">document</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">createElement</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"div"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb14-3">  div<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">className</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">`chat-msg </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">${</span>role<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">`</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb14-4">  div<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">textContent</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> text<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb14-5">  msgs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">appendChild</span>(div)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb14-6">  msgs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">scrollTop</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> msgs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">scrollHeight</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb14-7">  <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">return</span> div<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb14-8">}</span></code></pre></div>
</div>
<div id="cr-send" class="sticky">
<div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb15-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">async</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">function</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sendMessage</span>() {</span>
<span id="cb15-2">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> text <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> input<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">value</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">trim</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-3">  <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">if</span> (<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!</span>text <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">||</span> busy) <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">return</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-4">  busy <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">true</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> send<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">disabled</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">true</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> input<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">value</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-5"></span>
<span id="cb15-6">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">appendMsg</span>(text<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"user"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-7">  <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> thinking <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">appendMsg</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Thinking…"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bot typing"</span>)<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-8"></span>
<span id="cb15-9">  <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">try</span> {</span>
<span id="cb15-10">    <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> res <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">await</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">fetch</span>(WORKER_URL<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> {</span>
<span id="cb15-11">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">method</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"POST"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb15-12">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">headers</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> { <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Content-Type"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"application/json"</span> }<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb15-13">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">body</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">JSON</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">stringify</span>({ <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">message</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> text<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> history })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb15-14">    })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-15">    <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> data <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">await</span> res<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">json</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-16">    <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">const</span> reply <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">reply</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">||</span> data<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">error</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">||</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Something went wrong."</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-17">    thinking<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">className</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-msg bot"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-18">    thinking<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">textContent</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> reply<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-19">    history<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">push</span>({ <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">role</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"user"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span>      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">content</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> text  })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-20">    history<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">push</span>({ <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">role</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"assistant"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span> <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">content</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> reply })<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-21">  } <span class="cf" style="color: #003B4F;
background-color: null;
font-style: inherit;">catch</span> {</span>
<span id="cb15-22">    thinking<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">className</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"chat-msg bot"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-23">    thinking<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">textContent</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"⚠️ Could not reach the assistant."</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-24">  }</span>
<span id="cb15-25"></span>
<span id="cb15-26">  busy <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">false</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> send<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">disabled</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">false</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-27">  input<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">focus</span>()<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span> msgs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">scrollTop</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> msgs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">scrollHeight</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">;</span></span>
<span id="cb15-28">}</span></code></pre></div>
</div>
</div>
</div>
</div>
<hr>
</section>
<section id="part-3-integrating-with-quarto" class="level2">
<h2 class="anchored" data-anchor-id="part-3-integrating-with-quarto">Part 3 — Integrating with Quarto</h2>
<p>Quarto’s <code>{=html}</code> raw block passes content through to the rendered page verbatim. Drop the widget anywhere in a <code>.qmd</code> file:</p>
<pre class="qmd"><code>```{=html}
&lt;div id="chat-box"&gt;
  …
&lt;/div&gt;
&lt;script&gt;
(function () {
  const WORKER_URL = "https://resume-chat.&lt;account&gt;.workers.dev";
  …
})();
&lt;/script&gt;
```</code></pre>
<p>Because the block is scoped to <code>index.qmd</code>, 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 <code>include-after-body</code> option in <code>_quarto.yml</code> can inject an HTML file into every page instead.</p>
<hr>
</section>
<section id="putting-it-all-together" class="level2">
<h2 class="anchored" data-anchor-id="putting-it-all-together">Putting it all together</h2>
<div class="sourceCode" id="cb17" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb17-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 1. Install Wrangler</span></span>
<span id="cb17-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">npm</span> install <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-g</span> wrangler</span>
<span id="cb17-3"></span>
<span id="cb17-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 2. Authenticate with Cloudflare (creates a free account if needed)</span></span>
<span id="cb17-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">wrangler</span> login</span>
<span id="cb17-6"></span>
<span id="cb17-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 3. Deploy the Worker (the [ai] binding in wrangler.toml is all you need)</span></span>
<span id="cb17-8"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> worker/</span>
<span id="cb17-9"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">wrangler</span> deploy</span>
<span id="cb17-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Note the URL printed: https://resume-chat.&lt;subdomain&gt;.workers.dev</span></span>
<span id="cb17-11"></span>
<span id="cb17-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 4. Update the widget with your Worker URL</span></span>
<span id="cb17-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Open index.qmd and change:</span></span>
<span id="cb17-14"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#   const WORKER_URL = "https://resume-chat.YOUR-SUBDOMAIN.workers.dev";</span></span>
<span id="cb17-15"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># to your actual URL.</span></span>
<span id="cb17-16"></span>
<span id="cb17-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 5. Rebuild the landing page</span></span>
<span id="cb17-18"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ..</span>
<span id="cb17-19"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">quarto</span> render index.qmd</span>
<span id="cb17-20"></span>
<span id="cb17-21"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 6. Publish</span></span>
<span id="cb17-22"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> add <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-A</span></span>
<span id="cb17-23"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> commit <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Add AI résumé chat assistant"</span></span>
<span id="cb17-24"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push</span></code></pre></div>
<hr>
</section>
<section id="what-to-customise" class="level2">
<h2 class="anchored" data-anchor-id="what-to-customise">What to customise</h2>
<p><strong>Update the résumé content</strong> — open <code>worker/index.js</code> and edit the <code>SYSTEM_PROMPT</code> string. Then run <code>wrangler deploy</code> again to push the update. The website itself does not need to be rebuilt.</p>
<p><strong>Show the widget on every page</strong> — instead of embedding it in <code>index.qmd</code>, use Quarto’s <code>include-after-body</code> option in <code>_quarto.yml</code>:</p>
<div class="sourceCode" id="cb18" style="background: #f1f3f5;"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb18-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">format</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">:</span></span>
<span id="cb18-2"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">  </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">html</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">:</span></span>
<span id="cb18-3"><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">    </span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">include-after-body</span><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">:</span><span class="at" style="color: #657422;
background-color: null;
font-style: inherit;"> _partials/chat-widget.html</span></span></code></pre></div>
<p>Move the HTML+JS block to <code>_partials/chat-widget.html</code> and <code>quarto render</code> will inject it at the bottom of every page.</p>
<p><strong>Update the CORS origin</strong> — the Worker is already restricted to a single origin. When you adapt this for your own site, change <code>ggilles.dev</code> to your domain in <code>worker/index.js</code>:</p>
<div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode js code-with-copy"><code class="sourceCode javascript"><span id="cb19-1"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Access-Control-Allow-Origin"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://your-domain.com"</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">,</span></span></code></pre></div>
<p><strong>Swap the model</strong> — edit <code>AI_MODEL</code> in <code>wrangler.toml</code> and run <code>wrangler deploy</code>. No code change needed:</p>
<div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode toml code-with-copy"><code class="sourceCode toml"><span id="cb20-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-style: inherit;">[vars]</span></span>
<span id="cb20-2"><span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">AI_MODEL</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"@cf/mistral/mistral-7b-instruct-v0.1"</span></span></code></pre></div>
<p>Browse the full catalogue at <a href="https://developers.cloudflare.com/workers-ai/models/">developers.cloudflare.com/workers-ai/models</a>.</p>
<hr>
</section>
<section id="closing-thoughts" class="level2">
<h2 class="anchored" data-anchor-id="closing-thoughts">Closing thoughts</h2>
<p>The whole implementation is roughly 150-200 lines of code spread across three files (<code>worker/index.js</code>, <code>assets/dark.scss</code>, and the raw HTML block in <code>index.qmd</code>). 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).</p>
<p>If you replicate this on your own site, feel free to open a <a href="https://github.com/guillaumegilles/guillaumegilles.github.io">GitHub issue</a> or reach out on <a href="https://www.linkedin.com/in/guillaumegilles">LinkedIn</a> — I’d be happy to help you debug it.</p>
<script>
document.addEventListener('DOMContentLoaded', () => {
  const toc = document.querySelector('#TOC, .sidebar.toc-left, nav[role="doc-toc"]');
  const crSections = document.querySelectorAll('.cr-section');
  if (!toc || !crSections.length) return;
  const observer = new IntersectionObserver((entries) => {
    const anyVisible = entries.some(e => e.isIntersecting);
    toc.style.visibility = anyVisible ? 'hidden' : 'visible';
    toc.style.opacity    = anyVisible ? '0'       : '1';
    toc.style.transition = 'opacity 0.2s, visibility 0.2s';
  }, { threshold: 0 });
  crSections.forEach(s => observer.observe(s));
});
</script>


</section>

 ]]></description>
  <category>AI</category>
  <category>Cloudflare</category>
  <category>Quarto</category>
  <category>Web</category>
  <guid>https://ggilles.dev/posts/resume-chat/</guid>
  <pubDate>Sat, 07 Mar 2026 23:00:00 GMT</pubDate>
  <media:content url="https://ggilles.dev/posts/resume-chat/image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
<item>
  <title>GitHub Codespaces + Quarto</title>
  <dc:creator>Guillaume Gilles</dc:creator>
  <dc:creator>Claude Sonnet 4.6</dc:creator>
  <link>https://ggilles.dev/posts/quarto-codespaces/</link>
  <description><![CDATA[ 




<section id="the-problem-with-local-setup" class="level2">
<h2 class="anchored" data-anchor-id="the-problem-with-local-setup">The problem with local setup</h2>
<p>Every time you start a new project, you go through the same ritual: install the right version of Python, find the right extension for your editor, configure the environment, and debug why it doesn’t work on <em>your</em> machine. With Quarto, this often means installing the CLI, setting up Jupyter, adding VS Code extensions, and getting the preview server running — before writing a single line.</p>
<p><a href="https://github.com/features/codespaces">GitHub Codespaces</a> solves this by moving the entire development environment into the cloud. You define the environment once as code, and anyone (including future you) can launch a fully configured workspace from a browser in seconds.</p>
<p>Each codespace runs in a <a href="https://www.docker.com/resources/what-container/">Docker container</a> on a GitHub-managed virtual machine. Every personal GitHub account includes <strong>120 core hours of free compute time</strong> and <strong>15 GB of storage per month</strong> — more than enough for writing and publishing. (With the 4-core machine this template requests, that works out to 30 hours of usage per month.)</p>
</section>
<section id="the-template" class="level2">
<h2 class="anchored" data-anchor-id="the-template">The template</h2>
<p>I built a ready-to-use template repository that bundles everything you need to write and publish Quarto documents without installing anything locally. Click the button below to launch it instantly:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="https://codespaces.new/guillaumegilles/codespaces-quarto"><img src="https://github.com/codespaces/badge.svg" class="img-fluid figure-img" alt="Open in GitHub Codespaces"></a></p>
<figcaption>Open in GitHub Codespaces</figcaption>
</figure>
</div>
<p>When the codespace starts, you get:</p>
<ul>
<li>The <strong>Quarto CLI</strong> (latest), ready to render <code>.qmd</code> files</li>
<li><strong>Python</strong> with <code>jupyter</code>, <code>matplotlib</code>, and <code>plotly</code> pre-installed</li>
<li><strong>VS Code for the web</strong> with the Quarto, Python, and Jupyter extensions</li>
<li>A <strong>live preview</strong> of <code>hello.qmd</code> that opens automatically in the side panel</li>
</ul>
<p>All of this is driven by a single configuration file: <code>.devcontainer/devcontainer.json</code>.</p>
</section>
<section id="walking-through-devcontainer.json" class="level2">
<h2 class="anchored" data-anchor-id="walking-through-devcontainer.json">Walking through <code>devcontainer.json</code></h2>
<p>The <code>.devcontainer/devcontainer.json</code> file is the heart of any Codespaces setup. It tells GitHub exactly what container image to use, what tools to install, and how to configure the editor. Scroll through the story below to see how each piece of this file works.</p>
<div class="cr-section cr-column-screen sidebar-left">
<div class="narrative-col">
<div class="trigger new-trigger" data-focus-on="cr-config">
<div class="narrative">
<p>This is the complete <code>.devcontainer/devcontainer.json</code> for this template. Every key plays a specific role — let’s go through them one by one.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="2,4">
<div class="narrative">
<p><strong><code>name</code></strong> is just a human-readable label shown in the Codespaces dashboard. <strong><code>image</code></strong> sets the base container. The Microsoft Ubuntu image is a lean starting point with <code>git</code>, <code>curl</code>, and common shell tools — without the bloat of the universal image.</p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="6-13">
<div class="narrative">
<p><strong><code>features</code></strong> are the magic of dev containers. Each entry is a self-contained install script that layers on top of the base image. Here we pull in two: the official Python feature (which also sets up <code>pip</code>) and the Rocker project’s Quarto CLI feature (which installs the latest <code>quarto</code> binary). </p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="15-17">
<div class="narrative">
<p><strong><code>hostRequirements</code></strong> asks GitHub to provision a machine with at least 4 CPU cores. Quarto rendering — especially with Jupyter kernels — benefits from extra CPU headroom, so this prevents the codespace from running on the smallest tier. </p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="19">
<div class="narrative">
<p><strong><code>waitFor</code></strong> tells the lifecycle to wait until <code>onCreateCommand</code> has finished before running subsequent commands. This ensures the container is fully ready before installing Python packages or starting the preview server. </p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="20">
<div class="narrative">
<p><strong><code>updateContentCommand</code></strong> runs every time the codespace content is updated (including on creation). Here it installs all Python packages listed in <code>requirements.txt</code> — so adding a dependency to that file is all you need to do to make it available in your codespace. </p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="21-23">
<div class="narrative">
<p><strong><code>postAttachCommand</code></strong> runs once VS Code connects to the container. The <code>server</code> key starts a <code>quarto preview</code> process in the background, watching <code>hello.qmd</code> and serving a live preview on port 8000. </p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="24-29">
<div class="narrative">
<p><strong><code>portsAttributes</code></strong> tells Codespaces what to do when port 8000 is open. <code>"onAutoForward": "openPreview"</code> makes the preview appear directly in the VS Code side panel — no copying URLs, no browser tabs. </p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="31-42">
<div class="narrative">
<p><strong><code>customizations</code></strong> configures the editor itself. The <code>codespaces.openFiles</code> list causes <code>hello.qmd</code> to open automatically when the workspace loads. The <code>vscode.extensions</code> list pre-installs three extensions: <strong>Quarto</strong> for syntax highlighting and rendering commands, <strong>Python</strong> for IntelliSense, and <strong>Jupyter</strong> for running notebook cells inline. </p>
</div>
</div>
<div class="trigger new-trigger" data-focus-on="cr-config" data-highlight="44">
<div class="narrative">
<p><strong><code>forwardPorts</code></strong> ensures port 8000 is always forwarded to your browser, even if <code>portsAttributes</code> automatic forwarding hasn’t triggered yet. This is the safety net that guarantees the preview is always reachable. </p>
</div>
</div>
</div>
<div class="sticky-col">
<div class="sticky-col-stack">
<div id="cr-config" class="sticky">
<div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode json code-with-copy"><code class="sourceCode json"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-2">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"name"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Quarto Codespaces"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-3"></span>
<span id="cb1-4">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"image"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"mcr.microsoft.com/devcontainers/base:ubuntu"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-5"></span>
<span id="cb1-6">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"features"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-7">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"ghcr.io/devcontainers/features/python:1"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-8">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"version"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"latest"</span></span>
<span id="cb1-9">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb1-10">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"ghcr.io/rocker-org/devcontainer-features/quarto-cli:1"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-11">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"version"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"latest"</span></span>
<span id="cb1-12">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb1-13">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb1-14"></span>
<span id="cb1-15">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"hostRequirements"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-16">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"cpus"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span></span>
<span id="cb1-17">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb1-18"></span>
<span id="cb1-19">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"waitFor"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"onCreateCommand"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-20">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"updateContentCommand"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"python3 -m pip install -r requirements.txt"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-21">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"postAttachCommand"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-22">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"server"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"quarto preview hello.qmd --no-browser --port 8000"</span></span>
<span id="cb1-23">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb1-24">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"portsAttributes"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-25">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"8000"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-26">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"label"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Quarto Preview"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-27">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"onAutoForward"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"openPreview"</span></span>
<span id="cb1-28">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb1-29">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb1-30"></span>
<span id="cb1-31">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"customizations"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-32">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"codespaces"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-33">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"openFiles"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">[</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"hello.qmd"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb1-34">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb1-35">    <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"vscode"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">{</span></span>
<span id="cb1-36">      <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"extensions"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">[</span></span>
<span id="cb1-37">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"quarto.quarto"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-38">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ms-python.python"</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">,</span></span>
<span id="cb1-39">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ms-toolsai.jupyter"</span></span>
<span id="cb1-40">      <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb1-41">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">}</span></span>
<span id="cb1-42">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">},</span></span>
<span id="cb1-43"></span>
<span id="cb1-44">  <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">"forwardPorts"</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">:</span> <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">[</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8000</span><span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">]</span></span>
<span id="cb1-45"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">}</span></span></code></pre></div>
</div>
</div>
</div>
</div>
</section>
<section id="using-the-template" class="level2">
<h2 class="anchored" data-anchor-id="using-the-template">Using the template</h2>
<p>The quickest way to get started is to click the badge above. Alternatively, you can use it to seed your own repository:</p>
<ol type="1">
<li>Go to <a href="https://github.com/guillaumegilles/codespaces-quarto">github.com/guillaumegilles/codespaces-quarto</a></li>
<li>Click <strong>“Use this template”</strong> → <strong>“Create a new repository”</strong></li>
<li>Open your new repo and click <strong>“Code”</strong> → <strong>“Codespaces”</strong> → <strong>“Create codespace on main”</strong></li>
</ol>
<p>Within about 90 seconds the container will be built, Python packages installed, and a live Quarto preview will open in your browser — ready to write.</p>
</section>
<section id="going-further" class="level2">
<h2 class="anchored" data-anchor-id="going-further">Going further</h2>
<p>Once you’re comfortable with the template, you can extend it by editing <code>.devcontainer/devcontainer.json</code>:</p>
<ul>
<li><strong>Add R support</strong> by including the <code>ghcr.io/rocker-org/devcontainer-features/r-rig:1</code> feature</li>
<li><strong>Add more Python packages</strong> by editing <code>requirements.txt</code></li>
<li><strong>Pre-install more VS Code extensions</strong> by adding their IDs to the <code>extensions</code> list</li>
<li><strong>Change the base image</strong> to the universal image if you need a wider set of pre-installed runtimes</li>
</ul>
<p>The configuration-as-code approach means any change you commit is instantly available to anyone who opens the repository in a codespace.</p>
</section>
<section id="what-to-expect" class="level2">
<h2 class="anchored" data-anchor-id="what-to-expect">What to expect</h2>
<p>Here is what the codespace looks like once it has finished loading: VS Code on the left with <code>hello.qmd</code> open, and the live Quarto preview rendering a polar plot on the right — no local setup required.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://ggilles.dev/posts/quarto-codespaces/preview.png" class="img-fluid figure-img"></p>
<figcaption>VS Code for the web with <code>hello.qmd</code> open and its Quarto preview side by side.</figcaption>
</figure>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
  const toc = document.querySelector('#TOC, .sidebar.toc-left, nav[role="doc-toc"]');
  const crSection = document.querySelector('.cr-section');
  if (!toc || !crSection) return;
  const observer = new IntersectionObserver(([entry]) => {
    toc.style.visibility = entry.isIntersecting ? 'hidden' : 'visible';
    toc.style.opacity    = entry.isIntersecting ? '0'       : '1';
    toc.style.transition = 'opacity 0.2s, visibility 0.2s';
  }, { threshold: 0 });
  observer.observe(crSection);
});
</script>


</section>

 ]]></description>
  <category>Quarto</category>
  <category>GitHub</category>
  <category>Dev Tools</category>
  <guid>https://ggilles.dev/posts/quarto-codespaces/</guid>
  <pubDate>Mon, 23 Feb 2026 23:00:00 GMT</pubDate>
  <media:content url="https://ggilles.dev/posts/quarto-codespaces/image.png" medium="image" type="image/png" height="76" width="144"/>
</item>
</channel>
</rss>
