Skip to content

Commit d05a546

Browse files
authored
Merge pull request #48 from ethmarks/thessa-v2
Rewrite Thessa post(s)
2 parents f96a47e + d2a2766 commit d05a546

File tree

5 files changed

+305
-84
lines changed

5 files changed

+305
-84
lines changed
51.9 KB
Loading

content/posts/partialtocomponent.md

Lines changed: 108 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ Inserting styles into a webpage is easy: I just need to include a link to the CS
1919

2020
```scss
2121
/* components.scss */
22-
@import 'components/header';
23-
@import 'components/footer';
22+
@import "components/header";
23+
@import "components/footer";
2424
```
2525

2626
Normally I'd need to publish `components.scss` in order to use it, but I created a Hugo template a while ago that automatically transpiles and publishes all SCSS files. Here's what it looks like, if you're curious.
@@ -38,7 +38,10 @@ Normally I'd need to publish `components.scss` in order to use it, but I created
3838
So all I need to do to import the component styles into another project is include this line of HTML:
3939

4040
```html
41-
<link rel="stylesheet" href="https://ethmarks.github.io/css/components.min.css">
41+
<link
42+
rel="stylesheet"
43+
href="https://ethmarks.github.io/css/components.min.css"
44+
/>
4245
```
4346

4447
These component styles combined with ClasslessSpearmint provide all the styles necessary to imitate my site's aesthetic.
@@ -52,13 +55,15 @@ Here's what the HTML for my footer looks like as of the time of writing. The hea
5255
```html
5356
<!-- footer.html -->
5457
<footer>
55-
<span id="repo">
56-
<a href="https://github.com/ethmarks/ethmarks.github.io" target="_blank">Website Source</a>
57-
</span>
58-
<span id="copyright"><a href="/about/">Ethan Marks</a>, &copy;2025</span>
59-
<span id="email">
60-
<a href="mailto:ethmarks.dev@gmail.com" target="_blank">Contact</a>
61-
</span>
58+
<span id="repo">
59+
<a href="https://github.com/ethmarks/ethmarks.github.io" target="_blank"
60+
>Website Source</a
61+
>
62+
</span>
63+
<span id="copyright"><a href="/about/">Ethan Marks</a>, &copy;2025</span>
64+
<span id="email">
65+
<a href="mailto:ethmarks.dev@gmail.com" target="_blank">Contact</a>
66+
</span>
6267
</footer>
6368
```
6469

@@ -84,7 +89,7 @@ The only question is how to get the data from the HTML partials into a JavaScrip
8489

8590
I was curious to see how the [Zed Agent](https://zed.dev/agentic) would approach this problem without any supervision. I had no intention of using the code that it came up with, but I thought that this would be a good opportunity to experiment with vibe coding. I set Zed to use Claude Sonnet 4 and gave it the following prompt.
8691

87-
> **User** \
92+
> **User** \
8893
> I want a script, accessible at `/js/ethmarks-components.js`, that defines two Web Components whose content is the same as @header.html and @footer.html. It is absolutely cardinally important that the code be DRY, so you can't just copy the HTMl. Instead, you'll have to do something clever with Hugo that dynamically creates a JS file and publishes it.
8994
>
9095
> The end result should be as such:
@@ -97,44 +102,45 @@ I was curious to see how the [Zed Agent](https://zed.dev/agentic) would approach
97102
98103
It spent a few minutes reading my existing files, writing new ones, testing if the site built without errors (it often did not), and continued iterating until it had something that worked.
99104

100-
Its solution was *really* dumb.
105+
Its solution was _really_ dumb.
101106

102107
It ignored my instruction to create the JS file dynamically and opted instead to make a static JS file that fetches the HTML content from a pseudo-dynamic API that it also created.
103108

104109
Here's the JS script it created to fetch that API data.
105110

106111
```js
107112
// components.js
108-
const apiURL = '/api/index.json'
113+
const apiURL = "/api/index.json";
109114

110115
class SiteHeader extends HTMLElement {
111-
async connectedCallback() {
112-
try {
113-
const response = await fetch(apiURL);
114-
const data = await response.json();
115-
this.innerHTML = data.header;
116-
} catch (error) {
117-
console.error('Failed to load header:', error);
118-
this.innerHTML = '<header><a href="https://ethmarks.github.io">Ethan Marks</a></header>';
119-
}
116+
async connectedCallback() {
117+
try {
118+
const response = await fetch(apiURL);
119+
const data = await response.json();
120+
this.innerHTML = data.header;
121+
} catch (error) {
122+
console.error("Failed to load header:", error);
123+
this.innerHTML =
124+
'<header><a href="https://ethmarks.github.io">Ethan Marks</a></header>';
120125
}
126+
}
121127
}
122128

123129
class SiteFooter extends HTMLElement {
124-
async connectedCallback() {
125-
try {
126-
const response = await fetch(apiURL);
127-
const data = await response.json();
128-
this.innerHTML = data.footer;
129-
} catch (error) {
130-
console.error('Failed to load footer:', error);
131-
this.innerHTML = '<footer><span>Ethan Marks, &copy;2025</span></footer>';
132-
}
130+
async connectedCallback() {
131+
try {
132+
const response = await fetch(apiURL);
133+
const data = await response.json();
134+
this.innerHTML = data.footer;
135+
} catch (error) {
136+
console.error("Failed to load footer:", error);
137+
this.innerHTML = "<footer><span>Ethan Marks, &copy;2025</span></footer>";
133138
}
139+
}
134140
}
135141

136-
customElements.define('site-header', SiteHeader);
137-
customElements.define('site-footer', SiteFooter);
142+
customElements.define("site-header", SiteHeader);
143+
customElements.define("site-footer", SiteFooter);
138144
```
139145

140146
Here's its Hugo template to create a JSON file containing the header and footer HTML.
@@ -176,11 +182,11 @@ And here are the changes it made to my Hugo config to create the "API" output fo
176182

177183
This solution is bad for two reasons.
178184

179-
First, it's hideously overcomplicated; as you'll see later, I managed to accomplish the same thing in a single file, with *far* less code, and without messing with the Hugo config.
185+
First, it's hideously overcomplicated; as you'll see later, I managed to accomplish the same thing in a single file, with _far_ less code, and without messing with the Hugo config.
180186

181187
Second, the JS script fetches the data from an API. This introduces an extra network request, slows down the page load, and possibly causes CORS issues.
182188

183-
To the Zed Agent's credit, it *did* adhere to my requirements. Its solution does define the Web Components, doesn't interfere with my existing partials, and references the partials in a DRY manner. And the only thing that it cost me (other than eating into my prompt quota) was a few minutes of my time (which I spent reading [Ringworld](https://www.goodreads.com/book/show/61179.Ringworld)).
189+
To the Zed Agent's credit, it _did_ adhere to my requirements. Its solution does define the Web Components, doesn't interfere with my existing partials, and references the partials in a DRY manner. And the only thing that it cost me (other than eating into my prompt quota) was a few minutes of my time (which I spent reading [Ringworld](https://www.goodreads.com/book/show/61179.Ringworld)).
184190

185191
I've come to similar conclusions from my other experiments with vibe coding. If your goal is 'code that works', vibe coding is probably the easiest and most labor-efficient way to achieve that goal. But if you care even slightly about the quality of the code, AI agents tend to take shortcuts and make dumb architectural decisions.
186192

@@ -230,18 +236,20 @@ As of the time of writing, my template produces the following JS:
230236

231237
class EthmarksHeader extends HTMLElement {
232238
connectedCallback() {
233-
this.innerHTML = "\u003cheader\u003e\n \u003ca href=\"/\" id=\"title\" tabindex=\"0\" aria-label=\"Home\"\u003eEthan Marks\u003c/a\u003e\n \u003cnav\u003e\n \u003ca class=\"staggered\" href=\"/\"\u003eHome\u003c/a\u003e\n \u003ca class=\"staggered\" href=\"/about/\"\u003eAbout\u003c/a\u003e\n \u003ca class=\"staggered\" href=\"/posts/\"\u003ePosts\u003c/a\u003e\n \u003ca class=\"staggered active\" href=\"/tags/projects/\"\u003eProjects\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n";
239+
this.innerHTML =
240+
'\u003cheader\u003e\n \u003ca href="/" id="title" tabindex="0" aria-label="Home"\u003eEthan Marks\u003c/a\u003e\n \u003cnav\u003e\n \u003ca class="staggered" href="/"\u003eHome\u003c/a\u003e\n \u003ca class="staggered" href="/about/"\u003eAbout\u003c/a\u003e\n \u003ca class="staggered" href="/posts/"\u003ePosts\u003c/a\u003e\n \u003ca class="staggered active" href="/tags/projects/"\u003eProjects\u003c/a\u003e\n \u003c/nav\u003e\n\u003c/header\u003e\n';
234241
}
235242
}
236243

237244
class EthmarksFooter extends HTMLElement {
238245
connectedCallback() {
239-
this.innerHTML = "\u003cfooter\u003e\n \u003cspan id=\"repo\"\u003e\n \u003ca href=\"https://github.com/ethmarks/ethmarks.github.io\" target=\"_blank\"\u003eWebsite Source\u003c/a\u003e\n \u003c/span\u003e\n \u003cspan id=\"copyright\"\u003e\u003ca href=\"/about/\"\u003eEthan Marks\u003c/a\u003e, \u0026copy;2025\u003c/span\u003e\n \u003cspan id=\"email\"\u003e\n \u003ca href=\"mailto:ethmarks.dev@gmail.com\" target=\"_blank\"\u003eContact\u003c/a\u003e\n \u003c/span\u003e\n\u003c/footer\u003e\n";
246+
this.innerHTML =
247+
'\u003cfooter\u003e\n \u003cspan id="repo"\u003e\n \u003ca href="https://github.com/ethmarks/ethmarks.github.io" target="_blank"\u003eWebsite Source\u003c/a\u003e\n \u003c/span\u003e\n \u003cspan id="copyright"\u003e\u003ca href="/about/"\u003eEthan Marks\u003c/a\u003e, \u0026copy;2025\u003c/span\u003e\n \u003cspan id="email"\u003e\n \u003ca href="mailto:ethmarks.dev@gmail.com" target="_blank"\u003eContact\u003c/a\u003e\n \u003c/span\u003e\n\u003c/footer\u003e\n';
240248
}
241249
}
242250

243-
customElements.define('ethmarks-header', EthmarksHeader);
244-
customElements.define('ethmarks-footer', EthmarksFooter);
251+
customElements.define("ethmarks-header", EthmarksHeader);
252+
customElements.define("ethmarks-footer", EthmarksFooter);
245253
```
246254

247255
The final JS isn't exactly pretty, but the source template (the code whose prettiness is actually important) is simple, concise, and follows best practices.
@@ -294,31 +302,73 @@ Here's a demo page that does exactly that.
294302
```html
295303
<!DOCTYPE html>
296304
<html lang="en">
297-
<head>
298-
<meta charset="UTF-8">
299-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
305+
<head>
306+
<meta charset="UTF-8" />
307+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
300308
<title>Components Test</title>
301309

302310
<!-- global.css includes site styles & component styles -->
303-
<link rel="stylesheet" href="https://ethmarks.github.io/css/global.min.css">
304-
305-
<script src="https://ethmarks.github.io/js/ethmarks-components.js" defer></script>
306-
</head>
307-
<body>
311+
<link
312+
rel="stylesheet"
313+
href="https://ethmarks.github.io/css/global.min.css"
314+
/>
315+
316+
<script
317+
src="https://ethmarks.github.io/js/ethmarks-components.js"
318+
defer
319+
></script>
320+
</head>
321+
<body>
308322
<ethmarks-header></ethmarks-header>
309323
<main>
310-
<article>
311-
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
312-
<p>Olim erat homo quidam qui in silva habitabat. Hic vir singulis diebus ad fontem ambulabat ut aquam hauriret. Sed quadam die, cum ad fontem pervenit, vidit ibi puellam pulcherrimam capillis aureis. Puella ei dixit se esse nympham fontis et si vellet secum manere, magnam felicitatem ei daturam </p>
313-
<p>Vir autem, qui uxorem domi habebat et liberos tres, respondit se non posse relinquere familiam suam. Tunc nympha irascens malum carmen cantavit et statim vir in cervum conversus est. Cornua magna ei creverunt et pedes eius ungulae factae sunt.</p>
314-
<p>Diu per silvam errabat, suam formam humanam desiderans. Aliquando prope domum suam ibat et uxorem liberosque videbat, sed illi eum non cognoscebant. Uxor eius saepe dicebat viris vicinis maritum suum mysterioso modo evanisse.</p>
315-
<p>Post multos menses, cervus ad fontem rediit et nympham rogavit ut maledicto liberaret eum. Nympha, quae iam paenitentiam egerat crudelitatis suae, respondit se posse eum liberare si aliquis pro eo mortem oppeteret. Cervus tristis factus est, nam neminem noverat qui hoc faceret.</p>
316-
<p>Sed ecce! Canis fidelis qui eum per totam silvam secutus erat, subito in aquam saluit. Aquae magicae canem statim necaverunt, sed simul cervum in hominem retransformaverunt. Vir domum cucurrit et familiam suam amplexatus est, numquam obliturus sacrificium canis fidelis.</p>
317-
<p>Postea semper ad fontem revertebatur ut flores ibi poneret in memoriam canis sui, et nympha, quae nunc benigna facta erat, fonti virtutem dedit ut aegros sanaret qui cum fide ad eum venirent.</p>
318-
</article>
324+
<article>
325+
<p>
326+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
327+
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
328+
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
329+
aliquip ex ea commodo consequat.
330+
</p>
331+
<p>
332+
Olim erat homo quidam qui in silva habitabat. Hic vir singulis diebus
333+
ad fontem ambulabat ut aquam hauriret. Sed quadam die, cum ad fontem
334+
pervenit, vidit ibi puellam pulcherrimam capillis aureis. Puella ei
335+
dixit se esse nympham fontis et si vellet secum manere, magnam
336+
felicitatem ei daturam
337+
</p>
338+
<p>
339+
Vir autem, qui uxorem domi habebat et liberos tres, respondit se non
340+
posse relinquere familiam suam. Tunc nympha irascens malum carmen
341+
cantavit et statim vir in cervum conversus est. Cornua magna ei
342+
creverunt et pedes eius ungulae factae sunt.
343+
</p>
344+
<p>
345+
Diu per silvam errabat, suam formam humanam desiderans. Aliquando
346+
prope domum suam ibat et uxorem liberosque videbat, sed illi eum non
347+
cognoscebant. Uxor eius saepe dicebat viris vicinis maritum suum
348+
mysterioso modo evanisse.
349+
</p>
350+
<p>
351+
Post multos menses, cervus ad fontem rediit et nympham rogavit ut
352+
maledicto liberaret eum. Nympha, quae iam paenitentiam egerat
353+
crudelitatis suae, respondit se posse eum liberare si aliquis pro eo
354+
mortem oppeteret. Cervus tristis factus est, nam neminem noverat qui
355+
hoc faceret.
356+
</p>
357+
<p>
358+
Sed ecce! Canis fidelis qui eum per totam silvam secutus erat, subito
359+
in aquam saluit. Aquae magicae canem statim necaverunt, sed simul
360+
cervum in hominem retransformaverunt. Vir domum cucurrit et familiam
361+
suam amplexatus est, numquam obliturus sacrificium canis fidelis.
362+
</p>
363+
<p>
364+
Postea semper ad fontem revertebatur ut flores ibi poneret in memoriam
365+
canis sui, et nympha, quae nunc benigna facta erat, fonti virtutem
366+
dedit ut aegros sanaret qui cum fide ad eum venirent.
367+
</p>
368+
</article>
319369
</main>
320370
<ethmarks-footer></ethmarks-footer>
321-
</body>
371+
</body>
322372
</html>
323373
```
324374

@@ -328,7 +378,7 @@ And here's what it looks when rendered.
328378

329379
It looks almost exactly like a page on my site. All that from just a few lines of HTML!
330380

331-
I'm already using this approach for [Thessa](https://ethmarks.github.io/thessa/). The [source code for Thessa](https://github.com/ethmarks/thessa) lives in a separate repo from my personal website, so I couldn't just use my normal Hugo partials. Instead, I used `ethmarks-components.js` to import the header and footer using Web Components. The components on Thessa look exactly the same as the components on my personal website. This is because they *are* exactly the same, right down to the HTML.
381+
I'm already using this approach for [Thessa](/posts/thessa/). The [source code for Thessa](https://github.com/ethmarks/thessa) lives in a separate repo from my personal website, so I couldn't just use my normal Hugo partials. Instead, I used `ethmarks-components.js` to import the header and footer using Web Components. The components on Thessa look exactly the same as the components on my personal website. This is because they _are_ exactly the same, right down to the HTML.
332382

333383
## Conclusion
334384

0 commit comments

Comments
 (0)