tkeron 5.2.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,785 @@
1
+ ---
2
+ name: tkeron-components
3
+ description: "Deep dive into tkeron components and build-time scripts: .com.html, .com.ts, .com.md, .pre.ts, .post.ts — the com/document globals, attribute reading, resolution priority, nesting, the wrapper-disappears rule, and full-cycle examples. Use this skill when creating, editing, or debugging any tkeron component or pre/post-rendering script."
4
+ ---
5
+
6
+ # Tkeron — Components and Pre/Post-rendering
7
+
8
+ ## The 4 Component Types
9
+
10
+ ### 1. `.com.html` — Static HTML Component
11
+
12
+ Plain markup. Inlined in place. No logic, no attributes, no variables.
13
+
14
+ ```html
15
+ <!-- site-header.com.html -->
16
+ <header class="site-header">
17
+ <nav>
18
+ <a href="/">Home</a>
19
+ <a href="/about">About</a>
20
+ </nav>
21
+ </header>
22
+ ```
23
+
24
+ ```html
25
+ <!-- In any .html -->
26
+ <site-header></site-header>
27
+ ```
28
+
29
+ **Output**: `<site-header>` is replaced with the content of the `.com.html`.
30
+
31
+ **Can have multiple root elements:**
32
+
33
+ ```html
34
+ <!-- two-columns.com.html -->
35
+ <div class="col">Column 1</div>
36
+ <div class="col">Column 2</div>
37
+ ```
38
+
39
+ **Limitations**:
40
+
41
+ - ❌ Cannot read attributes (`{title}` does not work)
42
+ - ❌ No conditional logic
43
+ - ❌ No slots or Shadow DOM
44
+ - ❌ Cannot include itself (circular)
45
+
46
+ **When to use**: headers, footers, navs, static cards, SVG icons, fixed layouts.
47
+
48
+ ---
49
+
50
+ ### 2. `.com.ts` — Dynamic TypeScript Component
51
+
52
+ Runs in **Bun** during the build. Has the `com` variable (the custom element in the DOM). Only `com.innerHTML` is preserved in the output.
53
+
54
+ ```typescript
55
+ // user-badge.com.ts
56
+ const name = com.getAttribute("name") || "Guest";
57
+ const role = com.getAttribute("role") || "User";
58
+
59
+ com.innerHTML = `
60
+ <div class="badge">
61
+ <span>${name}</span>
62
+ <span>${role}</span>
63
+ </div>
64
+ `;
65
+ ```
66
+
67
+ ```html
68
+ <user-badge name="Alice" role="Admin"></user-badge>
69
+ <user-badge name="Bob"></user-badge>
70
+ ```
71
+
72
+ **The `com` variable** (HTMLElement):
73
+
74
+ ```typescript
75
+ com.getAttribute("attr") // Read an attribute
76
+ com.innerHTML = "..." // ONLY preserved effect → defines the output
77
+ com.tagName // Element name
78
+ com.querySelector(...) // Search inside (when innerHTML is already set)
79
+ ```
80
+
81
+ **Can import modules:**
82
+
83
+ ```typescript
84
+ // product-card.com.ts
85
+ import { formatPrice } from "./utils/format.js";
86
+
87
+ const price = parseFloat(com.getAttribute("price") || "0");
88
+ com.innerHTML = `<p>${formatPrice(price)}</p>`;
89
+ ```
90
+
91
+ **Can fetch:**
92
+
93
+ ```typescript
94
+ // weather-card.com.ts
95
+ const city = com.getAttribute("city") || "Madrid";
96
+ const res = await fetch(`https://api.weather.com?city=${city}`);
97
+ const data = await res.json();
98
+ com.innerHTML = `<div>${data.temp}°C in ${city}</div>`;
99
+ ```
100
+
101
+ **Can use npm packages** (installed with `bun add`):
102
+
103
+ ```typescript
104
+ // code-block.com.ts
105
+ import { codeToHtml } from "shiki";
106
+ const code = com.getAttribute("code") || "";
107
+ const html = await codeToHtml(code, {
108
+ lang: "typescript",
109
+ theme: "github-dark",
110
+ });
111
+ com.innerHTML = `<div class="code-block">${html}</div>`;
112
+ ```
113
+
114
+ **Full TypeScript**: interfaces, types, async/await, for/if, everything works.
115
+
116
+ **Limitations**:
117
+
118
+ - ❌ Event listeners are lost (only static HTML in output)
119
+ - ❌ No access to global `document` (only `com`)
120
+ - ❌ No `window`, `localStorage`, `navigator`
121
+ - ❌ No state between instances
122
+ - ❌ Local variables are discarded after the file runs
123
+
124
+ ---
125
+
126
+ ### 3. `.com.md` — Markdown Component
127
+
128
+ Markdown (full GFM) → HTML. Inlined just like `.com.html`.
129
+
130
+ ```markdown
131
+ <!-- hero-text.com.md -->
132
+
133
+ # Welcome
134
+
135
+ This is an **amazing site**.
136
+
137
+ - Fast
138
+ - Simple
139
+ - Powerful
140
+ ```
141
+
142
+ ```html
143
+ <hero-text></hero-text>
144
+ ```
145
+
146
+ **Supports full GFM**: headings, bold, italic, strikethrough, lists, tables, blockquotes, code blocks, links, images.
147
+
148
+ **Can contain custom elements** inside the Markdown (processed on the next iteration):
149
+
150
+ ```markdown
151
+ <!-- page-content.com.md -->
152
+
153
+ # Content
154
+
155
+ Introductory text.
156
+
157
+ <feature-list></feature-list>
158
+
159
+ Final text.
160
+ ```
161
+
162
+ **Limitations**:
163
+
164
+ - ❌ Cannot read attributes (static, like `.com.html`)
165
+ - ❌ No logic
166
+
167
+ **When to use**: text sections, FAQs, about, docs, rich content.
168
+
169
+ ---
170
+
171
+ ### 4. `.com.ts` + `.com.html` — Template + Logic
172
+
173
+ When BOTH `name.com.html` and `name.com.ts` exist for the same component name, the `.com.html` is loaded as `com.innerHTML` **before** the `.com.ts` runs. This separates structure from logic.
174
+
175
+ **Template** (`user-card.com.html`):
176
+
177
+ ```html
178
+ <div class="card">
179
+ <h3 class="name"></h3>
180
+ <p class="role"></p>
181
+ </div>
182
+ ```
183
+
184
+ **Logic** (`user-card.com.ts`):
185
+
186
+ ```typescript
187
+ const name = com.getAttribute("data-name") || "Unknown";
188
+ const role = com.getAttribute("data-role") || "N/A";
189
+
190
+ const nameEl = com.querySelector(".name");
191
+ const roleEl = com.querySelector(".role");
192
+ if (nameEl) nameEl.textContent = name;
193
+ if (roleEl) roleEl.textContent = `Role: ${role}`;
194
+ ```
195
+
196
+ **Usage:**
197
+
198
+ ```html
199
+ <user-card data-name="Alice" data-role="Developer"></user-card>
200
+ ```
201
+
202
+ The `.com.ts` can read, mutate, or completely replace the template.
203
+
204
+ ---
205
+
206
+ ## Build-time Scripts
207
+
208
+ In addition to components, tkeron has two "page-level" build-time scripts that operate on the full DOM of a paired `.html`:
209
+
210
+ ### `.pre.ts` — Pre-rendering (runs BEFORE components)
211
+
212
+ Runs in **Bun** at the **very start** of the build. Has `document` (the full DOM of the paired `.html`). Manipulates the whole document **before** any component is resolved.
213
+
214
+ **Automatic pairing**: `index.pre.ts` → `index.html`. `about.pre.ts` → `about.html`.
215
+
216
+ > **If the `.html` does not exist**, tkeron creates a base one automatically.
217
+
218
+ ```typescript
219
+ // index.pre.ts
220
+ import { fetchPosts } from "./api-service.js";
221
+
222
+ const title = document.querySelector("title");
223
+ if (title) title.textContent = "My Site";
224
+
225
+ const head = document.querySelector("head");
226
+ if (head) {
227
+ const meta = document.createElement("meta");
228
+ meta.setAttribute("name", "description");
229
+ meta.setAttribute("content", "Site description");
230
+ head.appendChild(meta);
231
+ }
232
+
233
+ const posts = await fetchPosts();
234
+ const container = document.getElementById("posts");
235
+ if (container) {
236
+ container.innerHTML = posts
237
+ .map((p) => `<article><h2>${p.title}</h2></article>`)
238
+ .join("");
239
+ }
240
+ ```
241
+
242
+ **Inject components dynamically** (they will be processed by the component loop afterwards):
243
+
244
+ ```typescript
245
+ // dashboard.pre.ts
246
+ const isDev = process.env.NODE_ENV === "development";
247
+ const body = document.querySelector("body");
248
+ if (body && isDev) {
249
+ const panel = document.createElement("debug-panel");
250
+ body.appendChild(panel);
251
+ }
252
+ ```
253
+
254
+ **Use cases**: SEO meta tags, dynamic `<title>`, fetch data at build time, conditionally insert custom elements that the component loop will then resolve.
255
+
256
+ ### `.post.ts` — Post-processing (runs AFTER components)
257
+
258
+ Runs in **Bun** at the **very end** of the build, **after** all components (`.com.html`, `.com.ts`, `.com.md`) have been inlined. Has `document` (the full DOM of the paired `.html`, with components already resolved). Last chance to mutate the page before it is written to `web/`.
259
+
260
+ **Automatic pairing**: `index.post.ts` → `index.html`. `about.post.ts` → `about.html`.
261
+
262
+ ```typescript
263
+ // index.post.ts
264
+ // At this point all <site-header>, <user-card>, etc. have been replaced
265
+ // with their final HTML. The DOM is ready for post-processing.
266
+
267
+ const links = document.querySelectorAll("a[href^='http']");
268
+ for (const link of links) {
269
+ link.setAttribute("rel", "noopener noreferrer");
270
+ link.setAttribute("target", "_blank");
271
+ }
272
+
273
+ const images = document.querySelectorAll("img:not([loading])");
274
+ for (const img of images) {
275
+ img.setAttribute("loading", "lazy");
276
+ }
277
+
278
+ const headings = document.querySelectorAll("h2, h3");
279
+ for (const h of headings) {
280
+ if (!h.id && h.textContent) {
281
+ h.setAttribute("id", h.textContent.toLowerCase().replace(/\s+/g, "-"));
282
+ }
283
+ }
284
+ ```
285
+
286
+ **Use cases**:
287
+
288
+ - Add `rel="noopener"` / `target="_blank"` to external links
289
+ - Add `loading="lazy"` to images that don't have it
290
+ - Auto-generate `id`s on headings for anchor links
291
+ - Build a Table of Contents from the resolved DOM
292
+ - Sanitize / rewrite attributes
293
+ - Final HTML transformations that depend on the **fully assembled** page
294
+
295
+ **Variables and limits**: identical to `.pre.ts` (`document`, full Bun runtime, no `window`/`localStorage`).
296
+
297
+ ### `.pre.ts` vs `.post.ts` — When to use which
298
+
299
+ | Concern | `.pre.ts` | `.post.ts` |
300
+ | --------------------------------------------- | ------------------------------- | --------------------- |
301
+ | Inject custom elements that must be processed | ✅ (loop runs after) | ❌ (loop already ran) |
302
+ | Set `<title>` / `<meta>` | ✅ | ✅ |
303
+ | Fetch data and seed page content | ✅ | ✅ |
304
+ | Walk the final, fully-resolved DOM | ❌ | ✅ |
305
+ | Rewrite links/images in components | ❌ (components not yet inlined) | ✅ |
306
+ | Generate TOC from real headings | ❌ | ✅ |
307
+
308
+ **Rule of thumb**: if you need to insert custom elements → `.pre.ts`. If you need to read the final HTML of components → `.post.ts`.
309
+
310
+ ---
311
+
312
+ ## Naming — Mandatory Rules
313
+
314
+ Every component **must have a hyphen** in its name (HTML custom-elements standard):
315
+
316
+ | File | Element | Valid? |
317
+ | ----------------------- | ------------------ | -------------------- |
318
+ | `user-card.com.html` | `<user-card>` | ✅ |
319
+ | `nav-menu.com.ts` | `<nav-menu>` | ✅ |
320
+ | `blog-post-card.com.ts` | `<blog-post-card>` | ✅ |
321
+ | `card.com.html` | `<card>` | ❌ no hyphen |
322
+ | `header.com.html` | `<header>` | ❌ standard HTML tag |
323
+ | `UserCard.com.html` | `<user-card>` | ✅ case-insensitive |
324
+
325
+ **Format**: kebab-case, lowercase with hyphens.
326
+
327
+ ---
328
+
329
+ ## Component Resolution
330
+
331
+ Tkeron looks up components in this order:
332
+
333
+ 1. **Same directory** as the file that uses it (high priority — local override)
334
+ 2. **Any directory** under `websrc/` (glob search)
335
+
336
+ ```
337
+ websrc/
338
+ ├── index.html # <blog-card> → components/blog-card.com.html
339
+ ├── components/
340
+ │ └── blog-card.com.html # Global component
341
+ └── blog/
342
+ ├── post.html # <blog-card> → blog/blog-card.com.html (local wins)
343
+ └── blog-card.com.html # Local override for blog/
344
+ ```
345
+
346
+ ### Priority between types
347
+
348
+ When multiple file types exist for the same name:
349
+
350
+ 1. **`.com.ts`** — highest priority
351
+ 2. **`.com.html`** — second
352
+ 3. **`.com.md`** — lowest
353
+
354
+ (Exception: if `.com.html` and `.com.ts` share a name, the HTML is loaded as `com.innerHTML` and the `.com.ts` runs on top of it — see "Template + Logic" above.)
355
+
356
+ ---
357
+
358
+ ## Iteration and Nesting
359
+
360
+ Tkeron processes components in an **iterative loop (max 10 cycles)** until no more changes are detected. This enables:
361
+
362
+ ### Components that use other components
363
+
364
+ ```html
365
+ <!-- user-card.com.html -->
366
+ <div class="user-card">
367
+ <user-avatar></user-avatar>
368
+ <div class="info">John Doe</div>
369
+ </div>
370
+ ```
371
+
372
+ ```html
373
+ <!-- user-avatar.com.html -->
374
+ <div class="avatar">
375
+ <img src="./avatars/default.jpg" alt="User" />
376
+ </div>
377
+ ```
378
+
379
+ Build: `<user-card>` is replaced → contains `<user-avatar>` → replaced on the next iteration.
380
+
381
+ ### `.com.ts` that emits custom elements
382
+
383
+ ```typescript
384
+ // user-profile.com.ts
385
+ const username = com.getAttribute("username") || "anon";
386
+ com.innerHTML = `
387
+ <div class="profile">
388
+ <user-avatar username="${username}"></user-avatar>
389
+ <span>${username}</span>
390
+ </div>
391
+ `;
392
+ // <user-avatar> will be processed on the next iteration
393
+ ```
394
+
395
+ ### `.pre.ts` that injects components
396
+
397
+ ```typescript
398
+ // index.pre.ts
399
+ const body = document.querySelector("body");
400
+ if (body) {
401
+ const nav = document.createElement("site-nav");
402
+ body.insertBefore(nav, body.firstChild);
403
+ }
404
+ // <site-nav> will be processed by the component loop
405
+ ```
406
+
407
+ ### Limits
408
+
409
+ - **Max 10 iterations** of the component loop
410
+ - **Max 50 levels** of nesting depth
411
+ - **No circular dependencies** (A uses B uses A → error)
412
+
413
+ ---
414
+
415
+ ## Interactivity: Build Time + Runtime
416
+
417
+ Components emit static HTML. For interactivity, use a browser `.ts` file:
418
+
419
+ **Static component:**
420
+
421
+ ```html
422
+ <!-- counter-btn.com.html -->
423
+ <button id="counter-btn">Clicks: <span id="count">0</span></button>
424
+ ```
425
+
426
+ **Browser interactivity:**
427
+
428
+ ```typescript
429
+ // index.ts (runtime, browser)
430
+ const btn = document.getElementById("counter-btn")!;
431
+ const count = document.getElementById("count")!;
432
+ let n = 0;
433
+ btn.addEventListener("click", () => {
434
+ n++;
435
+ count.textContent = String(n);
436
+ });
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Build in Action — Real Input and Output
442
+
443
+ ### Example 1: `.com.html` — direct replacement
444
+
445
+ **Source `websrc/`:**
446
+
447
+ ```
448
+ websrc/
449
+ ├── index.html
450
+ └── site-header.com.html
451
+ ```
452
+
453
+ ```html
454
+ <!-- websrc/site-header.com.html -->
455
+ <header class="site-header">
456
+ <nav>
457
+ <a href="/">Home</a>
458
+ <a href="/about">About</a>
459
+ </nav>
460
+ </header>
461
+ ```
462
+
463
+ ```html
464
+ <!-- websrc/index.html (BEFORE build) -->
465
+ <!DOCTYPE html>
466
+ <html lang="en">
467
+ <head>
468
+ <title>My Site</title>
469
+ </head>
470
+ <body>
471
+ <site-header></site-header>
472
+ <main><p>Content</p></main>
473
+ <script type="module" src="index.ts"></script>
474
+ </body>
475
+ </html>
476
+ ```
477
+
478
+ **Output `web/` after `tk build`:**
479
+
480
+ ```html
481
+ <!-- web/index.html (AFTER build) -->
482
+ <!DOCTYPE html>
483
+ <html lang="en">
484
+ <head>
485
+ <title>My Site</title>
486
+ </head>
487
+ <body>
488
+ <header class="site-header">
489
+ <nav>
490
+ <a href="/">Home</a>
491
+ <a href="/about">About</a>
492
+ </nav>
493
+ </header>
494
+ <main><p>Content</p></main>
495
+ <script type="module" src="index.js"></script>
496
+ </body>
497
+ </html>
498
+ ```
499
+
500
+ Note: `<site-header>` was replaced with the component HTML. `index.ts` was compiled and the reference rewritten to `index.js`. `site-header.com.html` does not exist in `web/`.
501
+
502
+ ### Example 2: `.com.ts` — dynamic component with attributes
503
+
504
+ **Source `websrc/`:**
505
+
506
+ ```
507
+ websrc/
508
+ ├── index.html
509
+ └── user-badge.com.ts
510
+ ```
511
+
512
+ ```typescript
513
+ // websrc/user-badge.com.ts
514
+ const name = com.getAttribute("name") || "Guest";
515
+ const role = com.getAttribute("role") || "User";
516
+
517
+ com.innerHTML = `
518
+ <div class="badge">
519
+ <span class="badge-name">${name}</span>
520
+ <span class="badge-role">${role}</span>
521
+ </div>
522
+ `;
523
+ ```
524
+
525
+ ```html
526
+ <!-- websrc/index.html (BEFORE build) -->
527
+ <body>
528
+ <user-badge name="Alice" role="Admin"></user-badge>
529
+ <user-badge name="Bob"></user-badge>
530
+ <user-badge></user-badge>
531
+ </body>
532
+ ```
533
+
534
+ **Output `web/index.html`:**
535
+
536
+ ```html
537
+ <body>
538
+ <div class="badge">
539
+ <span class="badge-name">Alice</span>
540
+ <span class="badge-role">Admin</span>
541
+ </div>
542
+ <div class="badge">
543
+ <span class="badge-name">Bob</span>
544
+ <span class="badge-role">User</span>
545
+ </div>
546
+ <div class="badge">
547
+ <span class="badge-name">Guest</span>
548
+ <span class="badge-role">User</span>
549
+ </div>
550
+ </body>
551
+ ```
552
+
553
+ Each `<user-badge>` instance is replaced with the `com.innerHTML` generated for that specific instance (with its own attributes).
554
+
555
+ ### Example 3: `.pre.ts` — document transformation
556
+
557
+ **Source `websrc/`:**
558
+
559
+ ```
560
+ websrc/
561
+ ├── index.html
562
+ └── index.pre.ts
563
+ ```
564
+
565
+ ```html
566
+ <!-- websrc/index.html (BEFORE build) -->
567
+ <!DOCTYPE html>
568
+ <html lang="en">
569
+ <head>
570
+ <title>Document</title>
571
+ </head>
572
+ <body>
573
+ <h1>Welcome</h1>
574
+ <p id="build-time"></p>
575
+ <ul id="posts"></ul>
576
+ </body>
577
+ </html>
578
+ ```
579
+
580
+ ```typescript
581
+ // websrc/index.pre.ts
582
+ const title = document.querySelector("title");
583
+ if (title) title.textContent = "My Blog";
584
+
585
+ const head = document.querySelector("head");
586
+ if (head) {
587
+ const meta = document.createElement("meta");
588
+ meta.setAttribute("name", "description");
589
+ meta.setAttribute("content", "My personal site");
590
+ head.appendChild(meta);
591
+ }
592
+
593
+ const buildTime = document.getElementById("build-time");
594
+ if (buildTime) buildTime.textContent = "Build: 2026-03-03";
595
+
596
+ const posts = document.getElementById("posts");
597
+ if (posts) {
598
+ posts.innerHTML = `
599
+ <li><a href="/post-1">Post 1</a></li>
600
+ <li><a href="/post-2">Post 2</a></li>
601
+ `;
602
+ }
603
+ ```
604
+
605
+ **Output `web/index.html`:**
606
+
607
+ ```html
608
+ <!DOCTYPE html>
609
+ <html lang="en">
610
+ <head>
611
+ <title>My Blog</title>
612
+ <meta name="description" content="My personal site" />
613
+ </head>
614
+ <body>
615
+ <h1>Welcome</h1>
616
+ <p id="build-time">Build: 2026-03-03</p>
617
+ <ul id="posts">
618
+ <li><a href="/post-1">Post 1</a></li>
619
+ <li><a href="/post-2">Post 2</a></li>
620
+ </ul>
621
+ </body>
622
+ </html>
623
+ ```
624
+
625
+ The `.pre.ts` file does not appear in `web/`. The HTML was mutated in memory during the build.
626
+
627
+ ### Example 4: `.post.ts` — final DOM walk
628
+
629
+ **Source `websrc/`:**
630
+
631
+ ```
632
+ websrc/
633
+ ├── index.html
634
+ ├── external-link.com.html
635
+ └── index.post.ts
636
+ ```
637
+
638
+ ```html
639
+ <!-- websrc/external-link.com.html -->
640
+ <a href="https://example.com">Example</a>
641
+ ```
642
+
643
+ ```html
644
+ <!-- websrc/index.html (BEFORE build) -->
645
+ <body>
646
+ <h2>Resources</h2>
647
+ <external-link></external-link>
648
+ <img src="./photo.jpg" alt="photo" />
649
+ </body>
650
+ ```
651
+
652
+ ```typescript
653
+ // websrc/index.post.ts
654
+ // Runs AFTER <external-link> has already been replaced
655
+ // by its <a href="https://example.com">...</a>
656
+
657
+ const links = document.querySelectorAll("a[href^='http']");
658
+ for (const link of links) {
659
+ link.setAttribute("rel", "noopener noreferrer");
660
+ link.setAttribute("target", "_blank");
661
+ }
662
+
663
+ const images = document.querySelectorAll("img:not([loading])");
664
+ for (const img of images) {
665
+ img.setAttribute("loading", "lazy");
666
+ }
667
+
668
+ const headings = document.querySelectorAll("h2");
669
+ for (const h of headings) {
670
+ if (!h.id && h.textContent) {
671
+ h.setAttribute("id", h.textContent.toLowerCase().replace(/\s+/g, "-"));
672
+ }
673
+ }
674
+ ```
675
+
676
+ **Output `web/index.html`:**
677
+
678
+ ```html
679
+ <body>
680
+ <h2 id="resources">Resources</h2>
681
+ <a href="https://example.com" rel="noopener noreferrer" target="_blank">
682
+ Example
683
+ </a>
684
+ <img src="./photo.jpg" alt="photo" loading="lazy" />
685
+ </body>
686
+ ```
687
+
688
+ The `<external-link>` was inlined first, then `.post.ts` walked the resolved DOM and added `rel`, `target`, `loading` and `id` attributes.
689
+
690
+ ### Example 5: Full cycle (`.pre.ts` + components + `.post.ts`)
691
+
692
+ **Source:**
693
+
694
+ ```
695
+ websrc/
696
+ ├── index.html
697
+ ├── index.pre.ts
698
+ ├── index.post.ts
699
+ ├── site-footer.com.html
700
+ └── alert-box.com.ts
701
+ ```
702
+
703
+ ```html
704
+ <!-- websrc/site-footer.com.html -->
705
+ <footer><p>&copy; 2026 My Site</p></footer>
706
+ ```
707
+
708
+ ```typescript
709
+ // websrc/alert-box.com.ts
710
+ const type = com.getAttribute("type") || "info";
711
+ const msg = com.getAttribute("msg") || "";
712
+ com.innerHTML = `<div class="alert alert-${type}">${msg}</div>`;
713
+ ```
714
+
715
+ ```html
716
+ <!-- websrc/index.html -->
717
+ <!DOCTYPE html>
718
+ <html>
719
+ <head>
720
+ <title>App</title>
721
+ </head>
722
+ <body>
723
+ <alert-box type="success" msg="Welcome"></alert-box>
724
+ <main><p>Content</p></main>
725
+ <site-footer></site-footer>
726
+ </body>
727
+ </html>
728
+ ```
729
+
730
+ ```typescript
731
+ // websrc/index.pre.ts
732
+ const title = document.querySelector("title");
733
+ if (title) title.textContent = "App — Home";
734
+ ```
735
+
736
+ ```typescript
737
+ // websrc/index.post.ts
738
+ const main = document.querySelector("main");
739
+ if (main) main.setAttribute("data-section", "home");
740
+ ```
741
+
742
+ **Output `web/index.html`:**
743
+
744
+ ```html
745
+ <!DOCTYPE html>
746
+ <html>
747
+ <head>
748
+ <title>App — Home</title>
749
+ </head>
750
+ <body>
751
+ <div class="alert alert-success">Welcome</div>
752
+ <main data-section="home"><p>Content</p></main>
753
+ <footer><p>&copy; 2026 My Site</p></footer>
754
+ </body>
755
+ </html>
756
+ ```
757
+
758
+ **`web/` contains only:**
759
+
760
+ ```
761
+ web/
762
+ └── index.html
763
+ ```
764
+
765
+ **Files that disappear from the output**: `index.pre.ts`, `index.post.ts`, `alert-box.com.ts`, `site-footer.com.html`.
766
+
767
+ ---
768
+
769
+ ## When to Use Each Type
770
+
771
+ | I need to... | Use... |
772
+ | ----------------------------------------------------- | ----------------------- |
773
+ | Reusable markup with no logic | `.com.html` |
774
+ | Dynamic HTML driven by attributes | `.com.ts` |
775
+ | Formatted text in Markdown | `.com.md` |
776
+ | Mutate the document BEFORE components run | `.pre.ts` |
777
+ | Mutate the document AFTER components are resolved | `.post.ts` |
778
+ | SEO meta tags, page title | `.pre.ts` or `.post.ts` |
779
+ | Fetch API at build time and seed content | `.pre.ts` or `.com.ts` |
780
+ | Inject custom elements conditionally | `.pre.ts` |
781
+ | Add `rel="noopener"` / `loading="lazy"` to final HTML | `.post.ts` |
782
+ | Auto-generate heading ids / table of contents | `.post.ts` |
783
+ | Separate template from logic | `.com.html` + `.com.ts` |
784
+ | Interactivity, events, user input | `.ts` (browser) |
785
+ | State, localStorage, animations | `.ts` (browser) |