jcudit HF Staff commited on
Commit
fe47933
·
verified ·
1 Parent(s): 96b1329

Deploy from HuggingChat

Browse files
Files changed (2) hide show
  1. README.md +9 -5
  2. index.html +527 -18
README.md CHANGED
@@ -1,10 +1,14 @@
1
  ---
2
- title: Blog
3
- emoji: 🐨
4
- colorFrom: yellow
5
- colorTo: red
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: "blog"
3
+ emoji: 🌈
4
+ colorFrom: green
5
+ colorTo: purple
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - huggingchat
10
  ---
11
 
12
+ # blog
13
+
14
+ Built with [HuggingChat](https://huggingface.co/chat).
index.html CHANGED
@@ -1,19 +1,528 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>root level</title>
7
+ <style>
8
+ :root{
9
+ --bg:#f8f7f5; --surface:#ffffff; --text:#1b1c21; --muted:#71767f;
10
+ --border:#e7e4de; --accent:#4f46e5; --accent-soft:rgba(79,70,229,.09);
11
+ --code-bg:#f2f0ec; --shadow:0 10px 28px rgba(20,20,40,.07);
12
+ }
13
+ html[data-theme="dark"]{
14
+ --bg:#0e1014; --surface:#151923; --text:#e8eaf0; --muted:#98a0ae;
15
+ --border:#242a36; --accent:#8f8cf8; --accent-soft:rgba(143,140,248,.13);
16
+ --code-bg:#0b0d12; --shadow:0 12px 32px rgba(0,0,0,.45);
17
+ }
18
+ *{box-sizing:border-box}
19
+ body{margin:0;background:var(--bg);color:var(--text);
20
+ font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
21
+ transition:background .25s,color .25s;}
22
+ .wrap{max-width:820px;margin:0 auto;padding:0 20px}
23
+ code,.meta,.stats,.chip,.tag,.dir,figcaption,.back,.footer-inner,.eyebrow{
24
+ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
25
+
26
+ #progressBar{position:fixed;top:0;left:0;height:3px;width:0;background:var(--accent);
27
+ z-index:60;display:none;transition:width .1s linear}
28
+
29
+ .site-header{position:sticky;top:0;z-index:40;border-bottom:1px solid var(--border);
30
+ background:color-mix(in srgb,var(--bg) 82%,transparent);backdrop-filter:blur(10px)}
31
+ .header-inner{display:flex;align-items:center;justify-content:space-between;height:60px}
32
+ .brand{display:flex;gap:10px;align-items:center;text-decoration:none;color:var(--text);
33
+ font-weight:700;letter-spacing:-.02em;font-size:17px}
34
+ .brand-mark{width:13px;height:13px;border-radius:4px;background:var(--accent)}
35
+ .site-header nav{display:flex;align-items:center}
36
+ .site-header nav a{color:var(--muted);text-decoration:none;margin-left:20px;font-size:14px}
37
+ .site-header nav a:hover{color:var(--text)}
38
+ #themeBtn{background:none;border:1px solid var(--border);border-radius:8px;color:var(--text);
39
+ width:34px;height:34px;cursor:pointer;margin-left:20px;font-size:14px}
40
+ #themeBtn:hover{border-color:var(--accent)}
41
+
42
+ .hero{padding:60px 0 30px}
43
+ .eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.14em;color:var(--accent)}
44
+ .hero h1{font-size:clamp(2.2rem,5vw,3.1rem);letter-spacing:-.03em;line-height:1.05;margin:.3em 0 .25em}
45
+ .hero p{color:var(--muted);max-width:56ch;margin:0 0 12px}
46
+ .stats{font-size:12.5px;color:var(--muted)}
47
+
48
+ .controls{display:flex;flex-wrap:wrap;gap:10px;margin:24px 0 20px}
49
+ .search{flex:1;min-width:220px}
50
+ .search input{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:10px;
51
+ background:var(--surface);color:var(--text);font-size:14px;outline:none}
52
+ .search input:focus{border-color:var(--accent)}
53
+ .chips{display:flex;flex-wrap:wrap;gap:8px;width:100%}
54
+ .chip{font-size:12px;padding:5px 11px;border:1px solid var(--border);border-radius:999px;
55
+ background:var(--surface);color:var(--muted);cursor:pointer;text-decoration:none}
56
+ .chip:hover{border-color:var(--accent);color:var(--text)}
57
+ .chip.active{background:var(--accent);border-color:var(--accent);color:#fff}
58
+
59
+ .card{display:block;text-decoration:none;color:var(--text);background:var(--surface);
60
+ border:1px solid var(--border);border-radius:16px;padding:22px 24px;margin-bottom:14px;
61
+ transition:transform .18s,box-shadow .18s,border-color .18s}
62
+ .card:hover{transform:translateY(-3px);box-shadow:var(--shadow);border-color:var(--accent)}
63
+ .meta{font-size:12px;color:var(--muted);display:flex;gap:8px;align-items:center;flex-wrap:wrap}
64
+ .card h2{margin:8px 0 6px;font-size:1.28rem;letter-spacing:-.02em}
65
+ .card p{margin:0;color:var(--muted);font-size:15px}
66
+ .tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:12px}
67
+ .tag{font-size:11px;color:var(--accent);background:var(--accent-soft);padding:3px 9px;border-radius:6px}
68
+ .badge-ext{font-size:11px;color:var(--muted);border:1px dashed var(--border);
69
+ padding:2px 8px;border-radius:6px}
70
+ .empty{border:1px dashed var(--border);border-radius:14px;padding:44px 20px;
71
+ text-align:center;color:var(--muted)}
72
+
73
+ .post-head{padding:52px 0 0}
74
+ .back{font-size:13px;color:var(--muted);text-decoration:none}
75
+ .back:hover{color:var(--accent)}
76
+ .post-head h1{font-size:clamp(1.9rem,4.5vw,2.6rem);letter-spacing:-.03em;
77
+ line-height:1.12;margin:14px 0 10px}
78
+ .post-meta{display:flex;gap:10px;flex-wrap:wrap;font-size:12.5px;color:var(--muted)}
79
+ .actions{margin-top:18px;display:flex;gap:10px;flex-wrap:wrap}
80
+ .btn{display:inline-flex;align-items:center;gap:6px;font-size:13.5px;padding:8px 14px;
81
+ border-radius:9px;border:1px solid var(--border);background:var(--surface);
82
+ color:var(--text);cursor:pointer;text-decoration:none}
83
+ .btn:hover{border-color:var(--accent)}
84
+ .btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
85
+
86
+ .post-content{margin:32px 0 8px;font-size:17px;line-height:1.78}
87
+ .post-content h2{margin:1.9em 0 .55em;letter-spacing:-.02em}
88
+ .post-content h3{margin:1.7em 0 .5em}
89
+ .post-content p{margin:0 0 1.1em}
90
+ .post-content a{color:var(--accent)}
91
+ .post-content code{font-size:13.5px;background:var(--accent-soft);color:var(--accent);
92
+ padding:2px 6px;border-radius:6px}
93
+ .post-content pre{background:var(--code-bg);border:1px solid var(--border);
94
+ border-radius:12px;padding:16px 18px;overflow-x:auto;margin:0 0 1.2em}
95
+ .post-content pre code{background:none;color:var(--text);padding:0;font-size:13.5px;line-height:1.6}
96
+ .post-content blockquote{margin:0 0 1.2em;padding:4px 0 4px 16px;
97
+ border-left:3px solid var(--accent);color:var(--muted)}
98
+ .post-content ul,.post-content ol{margin:0 0 1.2em;padding-left:1.4em}
99
+ .post-content figure{margin:26px 0;text-align:center}
100
+ .post-content svg{max-width:100%;height:auto}
101
+ figcaption{font-size:12.5px;color:var(--muted);margin-top:8px}
102
+ hr{border:none;border-top:1px solid var(--border);margin:34px 0}
103
+
104
+ .external-panel{border:1px solid var(--border);background:var(--surface);border-radius:14px;
105
+ padding:20px;margin:28px 0 16px;display:flex;justify-content:space-between;
106
+ gap:16px;align-items:center;flex-wrap:wrap}
107
+ .frame-wrap iframe{width:100%;height:72vh;border:1px solid var(--border);
108
+ border-radius:14px;background:#fff}
109
+
110
+ .post-nav{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:40px 0 56px}
111
+ .post-nav a{border:1px solid var(--border);border-radius:12px;padding:14px 16px;
112
+ text-decoration:none;color:var(--text);background:var(--surface);font-size:14px}
113
+ .post-nav a:hover{border-color:var(--accent)}
114
+ .post-nav .dir{font-size:11px;color:var(--muted);display:block;margin-bottom:4px}
115
+ .post-nav .next{text-align:right}
116
+ @media(max-width:560px){.post-nav{grid-template-columns:1fr}}
117
+
118
+ footer{border-top:1px solid var(--border)}
119
+ .footer-inner{padding:26px 20px 44px;font-size:12.5px;color:var(--muted);
120
+ display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
121
+ footer a{color:var(--muted)}
122
+ footer a:hover{color:var(--accent)}
123
+ </style>
124
+ </head>
125
+ <body>
126
+ <div id="progressBar"></div>
127
+
128
+ <header class="site-header">
129
+ <div class="wrap header-inner">
130
+ <a class="brand" href="#/"><span class="brand-mark"></span><span id="brandTitle">root level</span></a>
131
+ <nav>
132
+ <a href="#/">Articles</a>
133
+ <a id="profileLink" href="#" target="_blank" rel="noopener">HF Profile</a>
134
+ <button id="themeBtn" title="Toggle theme">☾</button>
135
+ </nav>
136
+ </div>
137
+ </header>
138
+
139
+ <main id="app" class="wrap"></main>
140
+
141
+ <footer>
142
+ <div class="wrap footer-inner">
143
+ <span>© 2026 · <span id="footAuthor">Your Name</span></span>
144
+ <span>one index.html on a Hugging Face Space</span>
145
+ </div>
146
+ </footer>
147
+
148
+ <script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
149
+ <script>
150
+ /* ════════════════════════════════════════════════════════════
151
+ CONFIG — make it yours
152
+ ════════════════════════════════════════════════════════════ */
153
+ const CONFIG = {
154
+ title: "root level",
155
+ tagline: "Technical concepts, explained visually — diagrams, code, and intuition over walls of text.",
156
+ author: "Your Name",
157
+ profileUrl: "https://huggingface.co/YOUR-USERNAME",
158
+ accent: "" // e.g. "#e8590c" to override the accent color
159
+ };
160
+
161
+ /* ════════════════════════════════════════════════════════════
162
+ ADD A NEW ARTICLE
163
+ 1. Copy one of the objects below into the ARTICLES array.
164
+ 2. Give it a unique `slug` — it becomes the URL: #/post/slug
165
+ 3. Pick ONE style:
166
+ • Inline: write `content` as an HTML string (or markdown
167
+ with format: "md" — use ~~~ for code fences, because
168
+ backticks would end the template string).
169
+ • Linked Space: set `url` to a standalone Space instead.
170
+ For the in-page embed use the direct subdomain URL
171
+ (https://USER-SPACE.hf.space); the /spaces/ page URL is
172
+ best for the "Open" button.
173
+ 4. Commit this file — the card appears on the home page.
174
+ ════════════════════════════════════════════════════════════ */
175
+ const ARTICLES = [
176
+
177
+ {
178
+ slug: "welcome",
179
+ title: "How this blog works",
180
+ date: "2026-09-30",
181
+ tags: ["meta", "guide"],
182
+ summary: "Start here: the structure of this Space, and how to publish a new article in under two minutes.",
183
+ content: `
184
+ <h2>What this is</h2>
185
+ <p>This whole blog is a single <code>index.html</code> running on a Hugging Face
186
+ <em>static</em> Space. The home page lists every article as a card; each article
187
+ lives underneath it at a hash route like <code>#/post/welcome</code> — shareable,
188
+ bookmarkable, no build step, no backend.</p>
189
+
190
+ <h2>Two kinds of articles</h2>
191
+ <ul>
192
+ <li><strong>Inline</strong> — the full post lives in this file, as an HTML or
193
+ markdown string. Best for visual explainers with SVG diagrams and code.</li>
194
+ <li><strong>Linked Space</strong> — the card points to a standalone Space you
195
+ generated elsewhere. Readers can open it in a new tab or read it embedded
196
+ inside this blog.</li>
197
+ </ul>
198
+
199
+ <h2>Publishing a new article</h2>
200
+ <ol>
201
+ <li>Open <code>index.html</code> in the Space's Files tab.</li>
202
+ <li>Copy any object in the <code>ARTICLES</code> array and edit it:</li>
203
+ </ol>
204
+ <pre><code>{
205
+ slug: "my-post", // URL becomes #/post/my-post
206
+ title: "My Post",
207
+ date: "2026-10-01",
208
+ tags: ["demo"],
209
+ summary: "Shown on the card.",
210
+ content: "&lt;p&gt;Hello!&lt;/p&gt;" // or url: "https://u-s.hf.space"
211
+ }</code></pre>
212
+ <p>Commit, wait a few seconds for the Space to rebuild, and the card is live.</p>
213
+
214
+ <h2>Make it yours</h2>
215
+ <p>The <code>CONFIG</code> block at the top of the script controls the blog title,
216
+ tagline, author name, profile link, and accent color.</p>
217
+ `
218
+ },
219
+
220
+ {
221
+ slug: "self-attention-visual",
222
+ title: "Self-attention, visualized",
223
+ date: "2026-09-21",
224
+ tags: ["transformers", "visual"],
225
+ summary: "Queries, keys, values — what attention actually computes, in one diagram and six lines of code.",
226
+ content: `
227
+ <p>Self-attention answers one question: <em>for each token, which other tokens
228
+ should it listen to, and how much?</em> Everything else is plumbing.</p>
229
+
230
+ <figure>
231
+ <svg viewBox="0 0 660 240" role="img" aria-label="Self-attention data flow">
232
+ <defs>
233
+ <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7"
234
+ markerHeight="7" orient="auto-start-reverse">
235
+ <path d="M0,0 L10,5 L0,10 z" style="fill:var(--muted)"></path>
236
+ </marker>
237
+ </defs>
238
+ <g style="font:13px ui-monospace,monospace">
239
+ <rect x="20" y="28" width="86" height="42" rx="9" style="fill:var(--accent-soft);stroke:var(--accent)"/>
240
+ <text x="63" y="55" text-anchor="middle" style="fill:var(--text)">Q</text>
241
+ <rect x="20" y="98" width="86" height="42" rx="9" style="fill:var(--accent-soft);stroke:var(--accent)"/>
242
+ <text x="63" y="125" text-anchor="middle" style="fill:var(--text)">K</text>
243
+ <rect x="20" y="168" width="86" height="42" rx="9" style="fill:var(--accent-soft);stroke:var(--accent)"/>
244
+ <text x="63" y="195" text-anchor="middle" style="fill:var(--text)">V</text>
245
+ <rect x="216" y="86" width="170" height="66" rx="11" style="fill:var(--surface);stroke:var(--border)"/>
246
+ <text x="301" y="113" text-anchor="middle" style="fill:var(--text)">attention scores</text>
247
+ <text x="301" y="133" text-anchor="middle" style="fill:var(--muted);font-size:11px">softmax(QKᵀ / √d)</text>
248
+ <circle cx="482" cy="119" r="30" style="fill:var(--accent-soft);stroke:var(--accent)"/>
249
+ <text x="482" y="125" text-anchor="middle" style="fill:var(--text)">Σ</text>
250
+ <rect x="560" y="98" width="80" height="42" rx="9" style="fill:var(--surface);stroke:var(--accent)"/>
251
+ <text x="600" y="125" text-anchor="middle" style="fill:var(--text)">output</text>
252
+ <line x1="106" y1="49" x2="216" y2="100" style="stroke:var(--muted);stroke-width:1.5" marker-end="url(#arr)"/>
253
+ <line x1="106" y1="119" x2="216" y2="119" style="stroke:var(--muted);stroke-width:1.5" marker-end="url(#arr)"/>
254
+ <line x1="106" y1="189" x2="452" y2="142" style="stroke:var(--muted);stroke-width:1.5" marker-end="url(#arr)"/>
255
+ <line x1="386" y1="119" x2="450" y2="119" style="stroke:var(--muted);stroke-width:1.5" marker-end="url(#arr)"/>
256
+ <line x1="512" y1="119" x2="558" y2="119" style="stroke:var(--muted);stroke-width:1.5" marker-end="url(#arr)"/>
257
+ <text x="282" y="184" text-anchor="middle" style="fill:var(--muted);font-size:11px">weights × V</text>
258
+ </g>
259
+ </svg>
260
+ <figcaption>Queries meet keys to produce weights; values are mixed by those weights.</figcaption>
261
+ </figure>
262
+
263
+ <h2>In code</h2>
264
+ <pre><code>import math
265
+ Q = X @ W_q # (batch, seq, d_k)
266
+ K = X @ W_k
267
+ V = X @ W_v
268
+
269
+ scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k)
270
+ weights = scores.softmax(dim=-1) # each row sums to 1
271
+ out = weights @ V # weighted mix of values</code></pre>
272
+
273
+ <p>Three intuitions worth keeping:</p>
274
+ <ul>
275
+ <li><strong>Q·K is a similarity score.</strong> A query asks “who is relevant to me?”</li>
276
+ <li><strong>√d keeps softmax sane.</strong> Without it, large dims saturate the gradients.</li>
277
+ <li><strong>The output is a remix.</strong> Every token walks away as a blend of all tokens.</li>
278
+ </ul>
279
+ <blockquote>Inline SVG like the figure above scales crisply and follows the
280
+ light/dark theme — perfect for visual explainers.</blockquote>
281
+ `
282
+ },
283
+
284
+ {
285
+ slug: "markdown-notes",
286
+ title: "Markdown entries work too",
287
+ date: "2026-09-08",
288
+ tags: ["notes"],
289
+ summary: "For quick posts, set the format flag and write plain markdown instead of HTML.",
290
+ format: "md",
291
+ content: `
292
+ ### Markdown works here
293
+
294
+ Set <code>format: "md"</code> on an entry and the content is run through a markdown parser at render time.
295
+
296
+ - drop quick notes without writing HTML
297
+ - **bold**, lists, quotes and links all work
298
+
299
+ ~~~python
300
+ # tilde fences keep the template string safe
301
+ print("no backticks needed")
302
+ ~~~
303
+
304
+ > Tip: for pixel-perfect figures, paste generated HTML straight into <code>content</code> instead.
305
+ `
306
+ },
307
+
308
+ {
309
+ slug: "linked-space-example",
310
+ title: "Example: a linked Space article",
311
+ date: "2026-09-01",
312
+ tags: ["demo"],
313
+ summary: "This card has no inline content — it links to a standalone Space and embeds it here. Replace the url with one of yours.",
314
+ url: "https://YOUR-USERNAME-YOUR-ARTICLE.hf.space"
315
+ }
316
+
317
+ ];
318
+
319
+ /* ════════════════════════════════════════════════════════════
320
+ Engine below — no need to edit
321
+ ════════════════════════════════════════════════════════════ */
322
+ const state = { query: "", tag: null };
323
+ const app = document.getElementById('app');
324
+ const bar = document.getElementById('progressBar');
325
+
326
+ function esc(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
327
+ function fmtDate(d){ return new Date(d + 'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}); }
328
+ function readTime(html){ const w = String(html).replace(/<[^>]*>/g,' ').trim().split(/\s+/).length; return Math.max(1, Math.round(w/200)); }
329
+ function sortedArticles(){ return [...ARTICLES].sort((a,b)=> new Date(b.date) - new Date(a.date)); }
330
+ function tagCounts(){ const m = new Map(); ARTICLES.forEach(a => (a.tags||[]).forEach(t => m.set(t,(m.get(t)||0)+1))); return [...m.entries()].sort((a,b)=>b[1]-a[1]); }
331
+ function mdToHtml(src){
332
+ if (window.marked) return marked.parse(src);
333
+ return '<pre><code>' + esc(src) + '</code></pre>';
334
+ }
335
+
336
+ function cardHTML(a){
337
+ const external = a.url && !a.content;
338
+ return `
339
+ <a class="card" href="#/post/${a.slug}">
340
+ <div class="meta">
341
+ <span>${fmtDate(a.date)}</span><span>·</span>
342
+ <span>${external ? 'hosted Space' : readTime(a.content) + ' min read'}</span>
343
+ ${external ? '<span class="badge-ext">↗ Space</span>' : ''}
344
+ </div>
345
+ <h2>${a.title}</h2>
346
+ <p>${a.summary}</p>
347
+ <div class="tags">${(a.tags||[]).map(t=>`<span class="tag">#${t}</span>`).join('')}</div>
348
+ </a>`;
349
+ }
350
+
351
+ function renderChips(){
352
+ const chips = document.getElementById('chips');
353
+ const all = [['__all__', ARTICLES.length], ...tagCounts()];
354
+ chips.innerHTML = all.map(([t,n]) => {
355
+ const active = (t === '__all__' && !state.tag) || state.tag === t;
356
+ return `<button class="chip ${active?'active':''}" data-chip="${t==='__all__'?'':t}">${t==='__all__'?'All':t} · ${n}</button>`;
357
+ }).join('');
358
+ chips.querySelectorAll('[data-chip]').forEach(b => b.addEventListener('click', () => {
359
+ state.tag = b.dataset.chip || null;
360
+ renderChips(); renderList();
361
+ }));
362
+ }
363
+
364
+ function renderList(){
365
+ const q = state.query.trim().toLowerCase();
366
+ const items = sortedArticles().filter(a => {
367
+ const tagOK = !state.tag || (a.tags||[]).includes(state.tag);
368
+ const qOK = !q || (a.title + ' ' + a.summary + ' ' + (a.tags||[]).join(' ')).toLowerCase().includes(q);
369
+ return tagOK && qOK;
370
+ });
371
+ document.getElementById('list').innerHTML = items.length
372
+ ? items.map(cardHTML).join('')
373
+ : '<div class="empty">Nothing matches — try clearing the search or tag filter.</div>';
374
+ }
375
+
376
+ function renderIndex(){
377
+ bar.style.display = 'none';
378
+ const arts = sortedArticles();
379
+ const latest = arts.length ? fmtDate(arts[0].date) : '—';
380
+ app.innerHTML = `
381
+ <section class="hero">
382
+ <div class="eyebrow">${esc(CONFIG.author)} · field notes</div>
383
+ <h1>${esc(CONFIG.title)}</h1>
384
+ <p>${esc(CONFIG.tagline)}</p>
385
+ <div class="stats">${arts.length} articles · ${tagCounts().length} tags · updated ${latest}</div>
386
+ </section>
387
+ <div class="controls">
388
+ <div class="search"><input id="q" type="search" placeholder="Search articles…" value="${esc(state.query)}"></div>
389
+ <div class="chips" id="chips"></div>
390
+ </div>
391
+ <div id="list"></div>`;
392
+ document.getElementById('q').addEventListener('input', e => { state.query = e.target.value; renderList(); });
393
+ renderChips(); renderList();
394
+ }
395
+
396
+ function externalHTML(a){
397
+ return `
398
+ <div class="external-panel">
399
+ <div>
400
+ <strong>This article lives in its own Space.</strong>
401
+ <div style="color:var(--muted);font-size:14px;margin-top:4px">Open it directly, or read it embedded below.</div>
402
+ </div>
403
+ <a class="btn primary" href="${a.url}" target="_blank" rel="noopener">Open Space ↗</a>
404
+ </div>
405
+ <div class="frame-wrap"><iframe src="${a.url}" title="${esc(a.title)}" loading="lazy"></iframe></div>`;
406
+ }
407
+
408
+ function renderPost(slug){
409
+ const list = sortedArticles();
410
+ const i = list.findIndex(a => a.slug === slug);
411
+ bar.style.display = 'block'; bar.style.width = '0';
412
+ if (i < 0){
413
+ app.innerHTML = `
414
+ <div class="post-head">
415
+ <a class="back" href="#/">← All articles</a>
416
+ <h1>404 — not found</h1>
417
+ <p style="color:var(--muted)">No article with the slug “${esc(slug)}”.</p>
418
+ </div>`;
419
+ return;
420
+ }
421
+ const a = list[i];
422
+ const newer = list[i-1], older = list[i+1];
423
+ let body;
424
+ if (a.content) body = a.format === 'md' ? mdToHtml(a.content) : a.content;
425
+ else if (a.url) body = externalHTML(a);
426
+ else body = '<p style="color:var(--muted)">This article is empty.</p>';
427
+
428
+ app.innerHTML = `
429
+ <article>
430
+ <header class="post-head">
431
+ <a class="back" href="#/">← All articles</a>
432
+ <h1>${a.title}</h1>
433
+ <div class="post-meta">
434
+ <span>${fmtDate(a.date)}</span><span>·</span>
435
+ <span>${esc(CONFIG.author)}</span><span>·</span>
436
+ <span>${a.content ? readTime(a.content) + ' min read' : 'hosted Space'}</span>
437
+ </div>
438
+ <div class="tags">${(a.tags||[]).map(t=>`<a class="chip" href="#/" data-tag="${t}">#${t}</a>`).join('')}</div>
439
+ <div class="actions">
440
+ <button class="btn" id="copyBtn">Copy link</button>
441
+ ${a.url ? `<a class="btn primary" href="${a.url}" target="_blank" rel="noopener">Open in Space ↗</a>` : ''}
442
+ </div>
443
+ </header>
444
+ <div class="post-content">${body}</div>
445
+ <hr>
446
+ <nav class="post-nav">
447
+ ${newer ? `<a href="#/post/${newer.slug}"><span class="dir">← Newer</span>${newer.title}</a>` : '<span></span>'}
448
+ ${older ? `<a class="next" href="#/post/${older.slug}"><span class="dir">Older →</span>${older.title}</a>` : '<span></span>'}
449
+ </nav>
450
+ </article>`;
451
+
452
+ const cb = document.getElementById('copyBtn');
453
+ cb.addEventListener('click', () => {
454
+ navigator.clipboard.writeText(location.href).catch(()=>{});
455
+ cb.textContent = 'Copied ✓';
456
+ setTimeout(()=> cb.textContent = 'Copy link', 1500);
457
+ });
458
+ }
459
+
460
+ function router(){
461
+ const m = (location.hash || '#/').match(/^#\/post\/([\w-]+)/);
462
+ if (m) renderPost(m[1]); else renderIndex();
463
+ window.scrollTo(0,0);
464
+ }
465
+
466
+ /* tag chips inside a post → pre-filter the index */
467
+ app.addEventListener('click', e => {
468
+ const el = e.target.closest('[data-tag]');
469
+ if (el){ state.tag = el.dataset.tag; state.query = ''; }
470
+ });
471
+
472
+ /* reading progress bar */
473
+ window.addEventListener('scroll', () => {
474
+ if (bar.style.display === 'none') return;
475
+ const h = document.documentElement;
476
+ const max = h.scrollHeight - h.clientHeight;
477
+ bar.style.width = (max > 0 ? (h.scrollTop / max) * 100 : 0) + '%';
478
+ });
479
+
480
+ /* theme (in-memory only) */
481
+ function setTheme(t){
482
+ document.documentElement.dataset.theme = t;
483
+ document.getElementById('themeBtn').textContent = t === 'dark' ? '☀' : '☾';
484
+ }
485
+ document.getElementById('themeBtn').addEventListener('click', () => {
486
+ setTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark');
487
+ });
488
+
489
+ /* init */
490
+ (function init(){
491
+ document.getElementById('brandTitle').textContent = CONFIG.title;
492
+ document.getElementById('footAuthor').textContent = CONFIG.author;
493
+ document.title = CONFIG.title;
494
+ if (CONFIG.profileUrl) document.getElementById('profileLink').href = CONFIG.profileUrl;
495
+ else document.getElementById('profileLink').remove();
496
+ if (CONFIG.accent) document.documentElement.style.setProperty('--accent', CONFIG.accent);
497
+ setTheme(matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
498
+ window.addEventListener('hashchange', router);
499
+ router();
500
+ })();
501
+ </script>
502
+ </body>
503
  </html>
504
+ <script data-huggingchat-badge="script">
505
+ (function () {
506
+ try {
507
+ if (document.querySelector("[data-huggingchat-badge]:not(script)")) return;
508
+ var host = document.createElement("div");
509
+ host.setAttribute("data-huggingchat-badge", "");
510
+ var styles = [["all","initial"],["position","fixed"],["right","12px"],["bottom","12px"],["z-index","2147483647"],["display","block"],["visibility","visible"],["opacity","1"],["pointer-events","auto"],["width","auto"],["height","auto"],["margin","0"],["padding","0"],["max-width","none"],["max-height","none"],["transform","none"],["filter","none"],["clip-path","none"],["color-scheme","light"]];
511
+ for (var i = 0; i < styles.length; i++) {
512
+ host.style.setProperty(styles[i][0], styles[i][1], "important");
513
+ }
514
+ host.attachShadow({ mode: "closed" }).innerHTML = "<style>\n:host { all: initial; }\na {\n display: flex;\n align-items: center;\n gap: 5px;\n box-sizing: border-box;\n height: 26px;\n padding: 0 9px 0 7px;\n border-radius: 999px;\n border: 1px solid rgba(0, 0, 0, 0.07);\n background: rgba(255, 255, 255, 0.92);\n -webkit-backdrop-filter: saturate(180%) blur(8px);\n backdrop-filter: saturate(180%) blur(8px);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08), 0 6px 16px rgba(0, 0, 0, 0.06);\n color: #111827;\n font: 500 11px/1 ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n text-decoration: none;\n white-space: nowrap;\n opacity: 0.72;\n transition: opacity 0.15s ease, transform 0.15s ease;\n}\na:hover, a:focus-visible { opacity: 1; transform: translateY(-1px); }\nsvg { width: 14px; height: 14px; flex: none; }\n@media print { a { display: none; } }\n<\/style><a href=\"https://huggingface.co/chat\" target=\"_blank\" rel=\"noopener noreferrer nofollow\"><svg viewBox=\"0 0 32 32\" fill=\"none\" aria-hidden=\"true\"><path d=\"M16.0006 25.9992C13.8266 25.999 11.7118 25.2901 9.97686 23.9799C8.2419 22.6698 6.98127 20.8298 6.38599 18.7388C5.79071 16.6478 5.89323 14.4198 6.678 12.3923C7.46278 10.3648 8.88705 8.64837 10.735 7.50308C12.5829 6.35779 14.7538 5.84606 16.9187 6.04544C19.0837 6.24481 21.1246 7.14442 22.7323 8.60795C24.34 10.0715 25.4268 12.0192 25.8281 14.1559C26.2293 16.2926 25.9232 18.5019 24.9561 20.449C24.7703 20.8042 24.7223 21.2155 24.8211 21.604L25.4211 23.8316C25.4803 24.0518 25.4805 24.2837 25.4216 24.5039C25.3627 24.7242 25.2468 24.925 25.0856 25.0862C24.9244 25.2474 24.7235 25.3633 24.5033 25.4222C24.283 25.4811 24.0512 25.4809 23.831 25.4217L21.6034 24.8217C21.2172 24.7248 20.809 24.7729 20.4558 24.9567C19.0683 25.6467 17.5457 26.0068 16.0006 26.0068V25.9992Z\" fill=\"currentColor\"/><path d=\"M9.62598 16.0013C9.62598 15.3799 10.1294 14.8765 10.7508 14.8765C11.3721 14.8765 11.8756 15.3799 11.8756 16.0013C11.8756 17.0953 12.3102 18.1448 13.0838 18.9184C13.8574 19.692 14.9069 20.1266 16.001 20.1267C17.095 20.1267 18.1445 19.692 18.9181 18.9184C19.6918 18.1448 20.1264 17.0953 20.1264 16.0013C20.1264 15.3799 20.6299 14.8765 21.2512 14.8765C21.8725 14.8765 22.3759 15.3799 22.3759 16.0013C22.3759 17.6921 21.7046 19.3137 20.509 20.5093C19.3134 21.7049 17.6918 22.3762 16.001 22.3762C14.3102 22.3762 12.6885 21.7049 11.4929 20.5093C10.2974 19.3137 9.62598 17.6921 9.62598 16.0013Z\" fill=\"#fff\"/><\/svg><span>Made with HuggingChat<\/span><\/a>";
515
+ function attach() {
516
+ var parent = document.body || document.documentElement;
517
+ if (parent && host.parentNode !== parent) parent.appendChild(host);
518
+ }
519
+ attach();
520
+ // Artifacts that rewrite document.body wholesale would drop the badge.
521
+ if (document.body && typeof MutationObserver === "function") {
522
+ new MutationObserver(attach).observe(document.body, { childList: true });
523
+ } else {
524
+ document.addEventListener("DOMContentLoaded", attach);
525
+ }
526
+ } catch (e) {}
527
+ })();
528
+ </script>