@brightsideoy/kama 0.1.0-alpha.12 → 0.1.0-alpha.121

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.
Files changed (86) hide show
  1. package/README.md +195 -0
  2. package/bin/cli.js +602 -0
  3. package/content/definitions/block-layouts/three-column.yaml +20 -0
  4. package/content/definitions/block-layouts/two-column.yaml +29 -0
  5. package/content/definitions/blocks/button.yaml +24 -0
  6. package/content/definitions/blocks/header.yaml +13 -0
  7. package/content/definitions/blocks/header_image.yaml +21 -0
  8. package/content/definitions/blocks/hero.yaml +28 -0
  9. package/content/definitions/blocks/html.yaml +12 -0
  10. package/content/definitions/blocks/image_card.yaml +56 -0
  11. package/content/definitions/blocks/single_image.yaml +27 -0
  12. package/content/definitions/blocks/text.yaml +15 -0
  13. package/content/definitions/channels/ecommerce.yaml +28 -0
  14. package/content/definitions/channels/website.yaml +17 -0
  15. package/content/definitions/field-sets/access.yaml +27 -0
  16. package/content/definitions/field-sets/channel-base.yaml +50 -0
  17. package/content/definitions/field-sets/channel-seo.yaml +21 -0
  18. package/content/definitions/field-sets/comments.yaml +11 -0
  19. package/content/definitions/field-sets/content-block.yaml +41 -0
  20. package/content/definitions/field-sets/page-base.yaml +44 -0
  21. package/content/definitions/field-sets/page-seo.yaml +34 -0
  22. package/content/definitions/field-sets/page-social.yaml +78 -0
  23. package/content/definitions/field-sets/schema-type.yaml +43 -0
  24. package/content/definitions/field-sets/tree-base.yaml +31 -0
  25. package/content/definitions/field-sets/visibility.yaml +8 -0
  26. package/content/definitions/node-layouts/content.yml +12 -0
  27. package/content/definitions/node-layouts/landing-page.yml +25 -0
  28. package/content/definitions/node-layouts/single-column.yml +44 -0
  29. package/content/definitions/node-layouts/two-column.yml +49 -0
  30. package/content/definitions/nodes/address.yaml +40 -0
  31. package/content/definitions/nodes/article.yaml +48 -0
  32. package/content/definitions/nodes/event.yaml +55 -0
  33. package/content/definitions/nodes/external-link.yaml +29 -0
  34. package/content/definitions/nodes/page-skeleton.yaml +48 -0
  35. package/content/definitions/nodes/page.yaml +57 -0
  36. package/content/definitions/nodes/person.yaml +41 -0
  37. package/content/definitions/nodes/product.yaml +89 -0
  38. package/content/definitions/trees/navigation.yaml +24 -0
  39. package/definitions/editor/button.ts +11 -0
  40. package/definitions/editor/default.ts +21 -0
  41. package/definitions/editor/header.ts +36 -0
  42. package/definitions/editor/minimal.ts +15 -0
  43. package/definitions/validators/iban.ts +12 -0
  44. package/definitions/validators/magic-word.ts +7 -0
  45. package/dist/assets/index-C6GoXL-R.js +162 -0
  46. package/dist/assets/index-Drmp3h6P.css +1 -0
  47. package/dist/index.html +13 -0
  48. package/dist/index.js +12209 -2045
  49. package/documentation/SCHEMAS.md +182 -0
  50. package/package.json +14 -11
  51. package/schema.sql +204 -0
  52. package/templates/astro-starter/astro.config.mjs +40 -0
  53. package/templates/astro-starter/middleware/api-purge.ts +130 -0
  54. package/templates/astro-starter/middleware/index.ts +10 -0
  55. package/templates/astro-starter/middleware/kv-cache.ts +76 -0
  56. package/templates/astro-starter/middleware/response-headers.ts +43 -0
  57. package/templates/astro-starter/package.json.template +20 -0
  58. package/templates/astro-starter/public/css/standard-theme.css +157 -0
  59. package/templates/astro-starter/public/robots.txt +23 -0
  60. package/templates/astro-starter/src/components/blocks/BlockRenderer.astro +133 -0
  61. package/templates/astro-starter/src/components/blocks/ColumnLayout.astro +66 -0
  62. package/templates/astro-starter/src/components/blocks/HeaderImage.astro +131 -0
  63. package/templates/astro-starter/src/components/blocks/Hero.astro +218 -0
  64. package/templates/astro-starter/src/components/blocks/HtmlBlock.astro +24 -0
  65. package/templates/astro-starter/src/components/blocks/ImageBlock.astro +160 -0
  66. package/templates/astro-starter/src/components/blocks/TextBlock.astro +13 -0
  67. package/templates/astro-starter/src/components/global/Breadcrumb.astro +62 -0
  68. package/templates/astro-starter/src/components/global/Footer.astro +48 -0
  69. package/templates/astro-starter/src/components/global/Header.astro +92 -0
  70. package/templates/astro-starter/src/components/global/NavigationMenu.astro +450 -0
  71. package/templates/astro-starter/src/components/seo/SeoHead.astro +150 -0
  72. package/templates/astro-starter/src/components/ui/KamaImage.astro +149 -0
  73. package/templates/astro-starter/src/layouts/SkeletonLayout.astro +45 -0
  74. package/templates/astro-starter/src/layouts/StandardLayout.astro +52 -0
  75. package/templates/astro-starter/src/lib/cms.ts +149 -0
  76. package/templates/astro-starter/src/lib/media/kama-image-service.ts +41 -0
  77. package/templates/astro-starter/src/lib/media/media-proxy.ts +127 -0
  78. package/templates/astro-starter/src/lib/media/media-url.ts +34 -0
  79. package/templates/astro-starter/src/pages/404.astro +46 -0
  80. package/templates/astro-starter/src/pages/[...slug].astro +201 -0
  81. package/templates/astro-starter/src/pages/llms-full.txt.ts +161 -0
  82. package/templates/astro-starter/src/pages/llms.txt.ts +107 -0
  83. package/templates/astro-starter/src/pages/media/[...path].ts +7 -0
  84. package/templates/astro-starter/src/utils/tiptap.ts +113 -0
  85. package/templates/astro-starter/tsconfig.json +0 -0
  86. package/test-local.sh +76 -0
@@ -0,0 +1,450 @@
1
+ ---
2
+ // src/components/global/NavigationMenu.astro
3
+ import type { TreeNode } from "@/lib/cms";
4
+
5
+ interface Props {
6
+ tree: TreeNode[];
7
+ }
8
+
9
+ const { tree = [] } = Astro.props;
10
+
11
+ // 1. Get current path from Astro context
12
+ const currentPath = Astro.url.pathname.replace(/\/$/, "") || "/";
13
+
14
+ const homeNode = tree.find(
15
+ (item: TreeNode) => item.slug === "home" || item.path === "/",
16
+ );
17
+
18
+ const topLevelNavItems =
19
+ homeNode?.children && homeNode.children.length > 0
20
+ ? homeNode.children
21
+ : tree.filter((item: TreeNode) => item.slug !== "home");
22
+
23
+ // Normalize path comparison helper
24
+ const normalizePath = (path?: string) => {
25
+ if (!path) return "";
26
+ return path.replace(/\/$/, "") || "/";
27
+ };
28
+
29
+ // Active trail check (parent active when on a child page)
30
+ const isPathActive = (itemPath?: string) => {
31
+ const normItem = normalizePath(itemPath);
32
+ if (normItem === "/") return currentPath === "/";
33
+ return currentPath === normItem || currentPath.startsWith(`${normItem}/`);
34
+ };
35
+
36
+ // Exact path check
37
+ const isExactActive = (itemPath?: string) => {
38
+ return currentPath === normalizePath(itemPath);
39
+ };
40
+ ---
41
+
42
+ <!-- 2-LINE HAMBURGER TOGGLE BUTTON -->
43
+ <button
44
+ type="button"
45
+ class="mobile-toggle"
46
+ id="menu-toggle"
47
+ aria-label="Toggle Navigation Menu"
48
+ aria-expanded="false"
49
+ >
50
+ <span class="line line-1"></span>
51
+ <span class="line line-2"></span>
52
+ </button>
53
+
54
+ <!-- NAVIGATION BLOCK & FULLSCREEN CONTAINER -->
55
+ <nav class="nav-block" id="nav-overlay">
56
+ <ul class="nav-list">
57
+ {
58
+ topLevelNavItems.map((item: TreeNode) => {
59
+ const hasChildren = item.children && item.children.length > 0;
60
+ const isParentActive = isPathActive(item.path);
61
+
62
+ if (hasChildren) {
63
+ return (
64
+ <li class="nav-item">
65
+ <details class="nav-details">
66
+ <summary
67
+ class:list={[
68
+ "nav-button",
69
+ "dropdown-summary",
70
+ { active: isParentActive },
71
+ ]}
72
+ >
73
+ <span>{item.title}</span>
74
+ <span class="indicator">▼</span>
75
+ </summary>
76
+ <div class="submenu-container">
77
+ <ul class="dropdown-panel">
78
+ <li>
79
+ <a
80
+ href={item.path}
81
+ class:list={[
82
+ "dropdown-item",
83
+ "overview-item",
84
+ { active: isExactActive(item.path) },
85
+ ]}
86
+ >
87
+ {item.title} Overview
88
+ </a>
89
+ </li>
90
+ {item.children?.map((child: TreeNode) => (
91
+ <li>
92
+ <a
93
+ href={child.path}
94
+ class:list={[
95
+ "dropdown-item",
96
+ { active: isExactActive(child.path) },
97
+ ]}
98
+ >
99
+ {child.title}
100
+ </a>
101
+ </li>
102
+ ))}
103
+ </ul>
104
+ </div>
105
+ </details>
106
+ </li>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <li class="nav-item">
112
+ <a
113
+ href={item.path}
114
+ class:list={["nav-button", { active: isExactActive(item.path) }]}
115
+ >
116
+ {item.title}
117
+ </a>
118
+ </li>
119
+ );
120
+ })
121
+ }
122
+ </ul>
123
+ </nav>
124
+ <script>
125
+ const toggleBtn = document.getElementById("menu-toggle");
126
+ const navOverlay = document.getElementById("nav-overlay");
127
+
128
+ const closeMenu = () => {
129
+ if (toggleBtn && navOverlay) {
130
+ toggleBtn.setAttribute("aria-expanded", "false");
131
+ toggleBtn.classList.remove("is-open");
132
+ navOverlay.classList.remove("is-open");
133
+ document.body.classList.remove("menu-lock-scroll");
134
+ }
135
+ };
136
+
137
+ if (toggleBtn && navOverlay) {
138
+ // Toggle Menu
139
+ toggleBtn.addEventListener("click", () => {
140
+ const isOpen = toggleBtn.getAttribute("aria-expanded") === "true";
141
+ toggleBtn.setAttribute("aria-expanded", (!isOpen).toString());
142
+ toggleBtn.classList.toggle("is-open");
143
+ navOverlay.classList.toggle("is-open");
144
+ document.body.classList.toggle("menu-lock-scroll");
145
+ });
146
+
147
+ // 🎯 UPDATED LINK CLICK LOGIC
148
+ const navLinks = navOverlay.querySelectorAll("a");
149
+ navLinks.forEach(link => {
150
+ link.addEventListener("click", e => {
151
+ // Check where the link is going
152
+ const targetUrl = new URL(link.href);
153
+ const currentUrl = new window.URL(window.location.href);
154
+
155
+ // If it's a link to the exact page we are already on (like a #hash anchor),
156
+ // close the menu so the user can see the page.
157
+ if (targetUrl.pathname === currentUrl.pathname) {
158
+ closeMenu();
159
+ }
160
+ // If it's a new page, DO NOTHING. The menu stays visually open,
161
+ // acting as a loading screen until the browser paints the new page!
162
+ });
163
+ });
164
+ }
165
+
166
+ // Close menu on back/forward browser navigation (bfcache)
167
+ window.addEventListener("pageshow", () => {
168
+ closeMenu();
169
+ });
170
+
171
+ // Close desktop dropdowns when clicking outside
172
+ document.addEventListener("click", event => {
173
+ const allDetails = document.querySelectorAll("details.nav-details");
174
+ allDetails.forEach(details => {
175
+ if (
176
+ details.hasAttribute("open") &&
177
+ !details.contains(event.target as Node)
178
+ ) {
179
+ details.removeAttribute("open");
180
+ }
181
+ });
182
+ });
183
+ </script>
184
+
185
+ <style>
186
+ /* -------------------------------------------------------------
187
+ MOBILE HAMBURGER BUTTON (2-LINE TRANSFORMING TO X)
188
+ ------------------------------------------------------------- */
189
+ .mobile-toggle {
190
+ display: flex;
191
+ flex-direction: column;
192
+ justify-content: center;
193
+ align-items: center;
194
+ gap: 6px;
195
+ width: 44px;
196
+ height: 44px;
197
+ background: transparent;
198
+ border: none;
199
+ cursor: pointer;
200
+ padding: 0;
201
+ z-index: 1001;
202
+ }
203
+
204
+ .mobile-toggle .line {
205
+ width: 22px;
206
+ height: 2px;
207
+ background-color: #0f172a;
208
+ transition:
209
+ transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
210
+ opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
211
+ transform-origin: center;
212
+ }
213
+
214
+ .mobile-toggle.is-open .line-1 {
215
+ transform: translateY(4px) rotate(45deg);
216
+ }
217
+
218
+ .mobile-toggle.is-open .line-2 {
219
+ transform: translateY(-4px) rotate(-45deg);
220
+ }
221
+
222
+ /* -------------------------------------------------------------
223
+ MOBILE FULLSCREEN OVERLAY (DEFAULT MOBILE VIEW < 768px)
224
+ ------------------------------------------------------------- */
225
+ .nav-block {
226
+ position: fixed;
227
+ top: 0;
228
+ left: 0;
229
+ width: 100vw;
230
+ height: 100vh;
231
+ background-color: #ffffff;
232
+ padding: 80px 24px 40px 24px;
233
+ box-sizing: border-box;
234
+ display: flex;
235
+ flex-direction: column;
236
+ overflow-y: auto;
237
+ opacity: 0;
238
+ visibility: hidden;
239
+ pointer-events: none;
240
+ transition:
241
+ opacity 0.3s ease,
242
+ visibility 0.3s ease;
243
+ z-index: 1000;
244
+ }
245
+
246
+ .nav-block.is-open {
247
+ opacity: 1;
248
+ visibility: visible;
249
+ pointer-events: auto;
250
+ }
251
+
252
+ .nav-list {
253
+ display: flex;
254
+ flex-direction: column;
255
+ gap: 8px;
256
+ list-style: none;
257
+ margin: 0;
258
+ padding: 0;
259
+ width: 100%;
260
+ }
261
+
262
+ .nav-item {
263
+ position: relative;
264
+ width: 100%;
265
+ }
266
+
267
+ .nav-button {
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: space-between;
271
+ width: 100%;
272
+ height: 52px;
273
+ padding: 0 16px;
274
+ font-size: 18px;
275
+ font-weight: 500;
276
+ color: #334155;
277
+ text-decoration: none;
278
+ background: transparent;
279
+ border: none;
280
+ border-radius: 4px;
281
+ cursor: pointer;
282
+ box-sizing: border-box;
283
+ }
284
+
285
+ .nav-button:hover {
286
+ color: #0f172a;
287
+ background: #f8fafc;
288
+ }
289
+
290
+ .nav-button.active {
291
+ color: #0c4a6e;
292
+ font-weight: 600;
293
+ }
294
+
295
+ .dropdown-summary {
296
+ list-style: none;
297
+ user-select: none;
298
+ }
299
+
300
+ .dropdown-summary::-webkit-details-marker {
301
+ display: none;
302
+ }
303
+
304
+ .indicator {
305
+ font-size: 11px;
306
+ color: #64748b;
307
+ transition: transform 0.2s ease;
308
+ }
309
+
310
+ details[open] .indicator {
311
+ transform: rotate(180deg);
312
+ }
313
+
314
+ /* OPEN DROPDOWN SINGLE BLOCK WITH CONNECTING LINE & INDENTATION */
315
+ details.nav-details {
316
+ border-radius: 6px;
317
+ transition: background-color 0.2s ease;
318
+ }
319
+
320
+ details[open].nav-details {
321
+ background: #f8fafc;
322
+ padding-bottom: 8px;
323
+ }
324
+
325
+ /* Left vertical accent line connecting parent item and sub-levels */
326
+ .submenu-container {
327
+ position: relative;
328
+ margin-left: 24px;
329
+ padding-left: 16px;
330
+ border-left: 2px solid #cbd5e1;
331
+ }
332
+
333
+ .dropdown-panel {
334
+ list-style: none;
335
+ margin: 4px 0 0 0;
336
+ padding: 0;
337
+ display: flex;
338
+ flex-direction: column;
339
+ gap: 4px;
340
+ }
341
+
342
+ .dropdown-item {
343
+ display: block;
344
+ padding: 10px 12px;
345
+ font-size: 16px;
346
+ font-weight: 400;
347
+ color: #475569;
348
+ text-decoration: none;
349
+ border-radius: 4px;
350
+ transition:
351
+ background 0.15s ease,
352
+ color 0.15s ease;
353
+ }
354
+
355
+ .overview-item {
356
+ font-weight: 500;
357
+ color: #334155;
358
+ }
359
+
360
+ .dropdown-item:hover {
361
+ background: #e2e8f0;
362
+ color: #0f172a;
363
+ }
364
+
365
+ .dropdown-item.active {
366
+ font-weight: 600;
367
+ color: #0c4a6e;
368
+ background: #e0f2fe;
369
+ }
370
+
371
+ /* Prevent body scrolling when menu open */
372
+ :global(body.menu-lock-scroll) {
373
+ overflow: hidden !important;
374
+ }
375
+
376
+ /* -------------------------------------------------------------
377
+ DESKTOP MEDIA QUERY (>= 768px)
378
+ ------------------------------------------------------------- */
379
+ @media (min-width: 768px) {
380
+ .mobile-toggle {
381
+ display: none;
382
+ }
383
+
384
+ .nav-block {
385
+ position: static;
386
+ width: 100%;
387
+ height: auto;
388
+ background: transparent;
389
+ padding: 0;
390
+ opacity: 1;
391
+ visibility: visible;
392
+ pointer-events: auto;
393
+ overflow-y: visible;
394
+ display: block;
395
+ }
396
+
397
+ .nav-list {
398
+ flex-direction: row;
399
+ gap: 4px;
400
+ justify-content: flex-end;
401
+ }
402
+
403
+ .nav-item {
404
+ width: auto;
405
+ }
406
+
407
+ .nav-button {
408
+ display: inline-flex;
409
+ justify-content: flex-start;
410
+ gap: 6px;
411
+ height: 40px;
412
+ padding: 0 16px;
413
+ font-size: 14px;
414
+ border-radius: 4px;
415
+ margin-top: 4px;
416
+ }
417
+
418
+ details[open].nav-details {
419
+ background: transparent;
420
+ padding-bottom: 0;
421
+ }
422
+
423
+ .submenu-container {
424
+ margin-left: 0;
425
+ padding-left: 0;
426
+ border-left: none;
427
+ }
428
+
429
+ .dropdown-panel {
430
+ position: absolute;
431
+ top: 100%;
432
+ left: 0;
433
+ min-width: 220px;
434
+ background: #ffffff;
435
+ border: 1px solid #e2e8f0;
436
+ margin: 0;
437
+ gap: 0;
438
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
439
+ border-radius: 4px;
440
+ padding: 4px;
441
+ }
442
+
443
+ .dropdown-item {
444
+ font-size: 14px;
445
+ padding: 8px 16px;
446
+ border-radius: 4px;
447
+ border-bottom: none;
448
+ }
449
+ }
450
+ </style>
@@ -0,0 +1,150 @@
1
+ ---
2
+ // src/components/seo/SeoHead.astro
3
+ import type { ContentBlock } from "@/lib/cms";
4
+ import { buildAgnosticImageUrl } from "@/lib/media/media-url"; // Update path if you moved this!
5
+
6
+ interface Props {
7
+ node: Record<string, any>;
8
+ content: Record<string, ContentBlock[]>;
9
+ normalizedSlug: string;
10
+ isHomePage: boolean;
11
+ headerBlocks?: ContentBlock[];
12
+ }
13
+
14
+ const {
15
+ node = {},
16
+ content = {},
17
+ normalizedSlug,
18
+ isHomePage,
19
+ headerBlocks = []
20
+ } = Astro.props;
21
+
22
+ // 🎯 Detect LCP image candidate to preload immediately
23
+ let lcpImageUrl = "";
24
+ const firstHeaderBlock = headerBlocks && headerBlocks[0];
25
+ const firstContentBlock = content?.content && content.content[0];
26
+
27
+ const candidateBlock = (firstHeaderBlock?.data?.image ? firstHeaderBlock : null)
28
+ || (firstContentBlock?.data?.image ? firstContentBlock : null);
29
+
30
+ if (candidateBlock?.data?.image) {
31
+ const candidateImg = candidateBlock.data.image;
32
+ const rawUrl = candidateImg.variants?.standard?.url || candidateImg.url || candidateImg.path;
33
+ if (rawUrl) {
34
+ lcpImageUrl = buildAgnosticImageUrl(rawUrl, 1352, 75);
35
+ }
36
+ }
37
+
38
+ const siteOrigin = import.meta.env.PUBLIC_SITE_URL || Astro.site?.origin || "https://example.com";
39
+ const brandName = import.meta.env.PUBLIC_SITE_NAME || "KAMA Starter";
40
+ const agencyName = import.meta.env.PUBLIC_AGENCY_NAME || "My Company Name";
41
+ const brandLogo = `${siteOrigin}/logo.png`;
42
+
43
+ const prefix = node?.titlePrefix?.trim() ? `${node.titlePrefix.trim()} ` : "";
44
+ const suffix = node?.titleSuffix?.trim() ? ` ${node.titleSuffix.trim()}` : "";
45
+ const baseTitle = node?.title?.trim() || brandName;
46
+ const fullTitle = `${prefix}${baseTitle}${suffix}`;
47
+
48
+ const canonicalURL = new URL(Astro.url.pathname, siteOrigin).href;
49
+
50
+ const breadcrumbListItems = [
51
+ {
52
+ "@type": "ListItem",
53
+ "position": 1,
54
+ "name": "Home",
55
+ "item": siteOrigin,
56
+ },
57
+ ];
58
+
59
+ if (!isHomePage && normalizedSlug) {
60
+ const segments = normalizedSlug.split("/").filter(Boolean);
61
+ let currentPath = "";
62
+
63
+ segments.forEach((segment, index) => {
64
+ currentPath += `/${segment}`;
65
+ const isLast = index === segments.length - 1;
66
+
67
+ const fallbackTitle = segment
68
+ .replace(/-/g, " ")
69
+ .replace(/\b\w/g, (char) => char.toUpperCase());
70
+
71
+ const segmentTitle = isLast && node?.title ? node.title : fallbackTitle;
72
+
73
+ breadcrumbListItems.push({
74
+ "@type": "ListItem",
75
+ "position": index + 2,
76
+ "name": segmentTitle,
77
+ "item": `${siteOrigin}${currentPath}`,
78
+ });
79
+ });
80
+ }
81
+
82
+ const structuredDataGraph = {
83
+ "@context": "https://schema.org",
84
+ "@graph": [
85
+ {
86
+ "@type": "Organization",
87
+ "@id": `${siteOrigin}/#organization`,
88
+ "name": agencyName,
89
+ "url": siteOrigin,
90
+ "logo": {
91
+ "@type": "ImageObject",
92
+ "url": brandLogo,
93
+ "caption": agencyName
94
+ }
95
+ },
96
+ {
97
+ "@type": "SoftwareApplication",
98
+ "@id": `${siteOrigin}/#application`,
99
+ "name": brandName,
100
+ "applicationCategory": "WebApplication",
101
+ "operatingSystem": "Cloudflare Workers",
102
+ "publisher": {
103
+ "@id": `${siteOrigin}/#organization`
104
+ }
105
+ },
106
+ {
107
+ "@type": "WebSite",
108
+ "@id": `${siteOrigin}/#website`,
109
+ "url": siteOrigin,
110
+ "name": brandName,
111
+ "publisher": {
112
+ "@id": `${siteOrigin}/#organization`
113
+ }
114
+ },
115
+ {
116
+ "@type": "BreadcrumbList",
117
+ "@id": `${siteOrigin}${normalizedSlug ? `/${normalizedSlug}` : ''}/#breadcrumb`,
118
+ "itemListElement": breadcrumbListItems
119
+ }
120
+ ]
121
+ };
122
+ ---
123
+
124
+ <meta charset="utf-8" />
125
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
126
+
127
+ <title>{fullTitle}</title>
128
+
129
+ <meta
130
+ name="description"
131
+ content={node?.summary || `${brandName} - Corporate Web Application`}
132
+ />
133
+
134
+ <link rel="canonical" href={canonicalURL} />
135
+ <link rel="author" type="text/plain" href="/llms.txt" title="LLM Directory" />
136
+
137
+ {lcpImageUrl && (
138
+ <link
139
+ rel="preload"
140
+ as="image"
141
+ href={lcpImageUrl}
142
+ fetchpriority="high"
143
+ />
144
+ )}
145
+
146
+ <script
147
+ type="application/ld+json"
148
+ is:inline
149
+ set:html={JSON.stringify(structuredDataGraph)}
150
+ />