@stnd/layout 0.2.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.
package/Base.astro ADDED
@@ -0,0 +1,165 @@
1
+ ---
2
+ import Meta from "./Meta.astro";
3
+ import Menu from "../../components/Menu.astro";
4
+ import Logo from "../../components/Logo.astro";
5
+ import Comments from "../../components/Comments.astro";
6
+ import Toast from "../../components/Toast.astro";
7
+ // import { ClientRouter } from "astro:transitions";
8
+ import Log from "@stnd/log";
9
+ import { Image } from "astro:assets";
10
+
11
+ // Create logger for this layout
12
+ const log = Log({ scope: "Base Layout" });
13
+
14
+ // Access site configuration from global
15
+ import type { StandardFrameworkConfig } from "../types/standard";
16
+ const config =
17
+ (globalThis["__STANDARD_CONFIG__"] as StandardFrameworkConfig) || {};
18
+
19
+ // ========================================
20
+ // PROPS INTERFACE
21
+ // ========================================
22
+
23
+ interface Props {
24
+ frontmatter?: {
25
+ title?: string;
26
+ description?: string;
27
+ image?: string;
28
+ created?: string;
29
+ modified?: string;
30
+ tags?: string[];
31
+ securityHeaders?: boolean;
32
+ backlinks?: {
33
+ inbound?: string[];
34
+ outbound?: string[];
35
+ meta?: Record<string, any>;
36
+ };
37
+ };
38
+ site?: StandardFrameworkConfig;
39
+ title?: string;
40
+ description?: string;
41
+ image?: string;
42
+ created?: string;
43
+ modified?: string;
44
+ tags?: string[];
45
+ securityHeaders?: boolean;
46
+ class?: string;
47
+ }
48
+
49
+ // Use config from virtual module, with fallback to props for backward compatibility
50
+ const {
51
+ frontmatter,
52
+ site = config || {}, // Use config as default, allow override via props
53
+ title = frontmatter?.title || config?.title,
54
+ description = frontmatter?.description || config?.description,
55
+ image = frontmatter?.image,
56
+ created = frontmatter?.created,
57
+ modified = frontmatter?.modified,
58
+ tags = frontmatter?.tags || config?.tags || [],
59
+ securityHeaders = frontmatter?.securityHeaders,
60
+ class: className,
61
+ } = Astro.props as Props;
62
+
63
+ // ✨ Get backlinks from the page's frontmatter/data
64
+ const backlinks = frontmatter?.backlinks || {};
65
+ const { inbound = [], outbound = [], meta = {} } = backlinks;
66
+
67
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
68
+ const _unused = { inbound, outbound, meta };
69
+
70
+ const year = new Date().getFullYear();
71
+ ---
72
+
73
+ <!doctype html>
74
+ <html
75
+ class="rhythm"
76
+ lang={site.language || config?.language || "en"}
77
+ data-standard-version={site.version || config?.version}
78
+ data-theme="system"
79
+ >
80
+ <head>
81
+ <meta name="astro-view-transitions-enabled" content="true" />
82
+ <Meta
83
+ site={site}
84
+ title={title}
85
+ description={description}
86
+ image={image}
87
+ created={created}
88
+ modified={modified}
89
+ tags={tags}
90
+ securityHeaders={securityHeaders}
91
+ />
92
+ <!-- <ClientRouter /> -->
93
+ <script
94
+ is:inline
95
+ data-astro-rerun
96
+ src="/assets/standard/standard.bundle.full.js"
97
+ transition:persist></script>
98
+ <!-- Check if lab is enabled in config -->
99
+ {
100
+ config.lab ? (
101
+ <script
102
+ is:inline
103
+ src="/assets/standard/standard.lab.js"
104
+ transition:persist
105
+ />
106
+ ) : null
107
+ }
108
+ <script is:inline src="/assets/standard/standard.lab.js" transition:persist
109
+ ></script>
110
+ </head>
111
+
112
+ <body>
113
+ <!-- Persistent container for StandardLab UI that survives DOM swaps -->
114
+ <!--div id="standard-lab-root" transition:persist></div>
115
+
116
+ <a href="#main" class="skip-link">Skip to content</a-->
117
+
118
+ <header
119
+ class="flex font-interface"
120
+ style="justify-content: space-between; max-width: 100%;"
121
+ >
122
+ <p>
123
+ <a href="/" class="no-underline"><Logo /></a>
124
+ </p>
125
+ <Menu
126
+ items={site.nav?.header || config?.nav?.header || []}
127
+ class="horizontal"
128
+ />
129
+ </header>
130
+
131
+ <main id="main" class="rhythm prose mt-6">
132
+ <slot />
133
+
134
+ <Comments />
135
+ </main>
136
+
137
+ <hr />
138
+
139
+ <footer class="smaller font-interface">
140
+ <p>
141
+ <Image
142
+ src="https://img.shields.io/npm/v/@stnd/astro"
143
+ alt="Standard Version Badge"
144
+ width="94"
145
+ height="20"
146
+ />
147
+ </p>
148
+ <p>
149
+ © {year}
150
+ {site.author?.name || site.author || config?.author?.name} ·
151
+ <a href="/">{site.title || config?.title}</a> ·
152
+ <a
153
+ href="https://github.com/ZeFish/Standard/blob/main/LICENSE"
154
+ class="external-link">MIT License</a
155
+ >
156
+ <a href="https://standard.garden/" class="external-link"
157
+ >Standard::Garden</a
158
+ >
159
+ </p>
160
+ </footer>
161
+
162
+ <!-- Universal Toast Notification System -->
163
+ <Toast />
164
+ </body>
165
+ </html>
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @stnd/layout
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6d8888a: First release
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [6d8888a]
12
+ - @stnd/log@0.2.0
13
+ - @stnd/utils@0.2.0
@@ -0,0 +1,196 @@
1
+ ---
2
+ import "../../styles/standard.scss";
3
+
4
+ const { site = {}, encryptedData } = Astro.props;
5
+ ---
6
+
7
+ <!doctype html>
8
+ <html lang={site.language || "en"} class="rhythm" data-theme="system">
9
+ <head>
10
+ <meta charset="utf-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12
+ <title>Protected Content</title>
13
+ </head>
14
+ <body>
15
+ <main class="prose md">
16
+ <div class="encrypted-container">
17
+ <h2>Cette page est protégée par un mot de passe</h2>
18
+
19
+ <form id="decrypt-form">
20
+ <div class="form-group">
21
+ <input
22
+ type="password"
23
+ id="password"
24
+ placeholder="Mot de passe"
25
+ required
26
+ autocomplete="current-password"
27
+ />
28
+ </div>
29
+ <button type="submit" id="unlock-btn">
30
+ <span class="btn-text">Déverrouiller</span>
31
+ <span class="btn-loading" style="display: none;"
32
+ >Déverrouillage...</span
33
+ >
34
+ </button>
35
+ </form>
36
+
37
+ <div id="error" class="error">
38
+ Mot de passe incorrect. Veuillez réessayer.
39
+ </div>
40
+ <div id="loading" class="loading">
41
+ Déchiffrement du contenu...
42
+ </div>
43
+ </div>
44
+
45
+ <div id="encrypted-content" style="display: none;">
46
+ {encryptedData}
47
+ </div>
48
+
49
+ <script is:inline>
50
+ const encryptedData =
51
+ document.getElementById("encrypted-content").textContent;
52
+ const maxAttempts = 5;
53
+ let attempts = 0;
54
+
55
+ const form = document.getElementById("decrypt-form");
56
+ const passwordInput = document.getElementById("password");
57
+ const unlockBtn = document.getElementById("unlock-btn");
58
+ const errorDiv = document.getElementById("error");
59
+ const loadingDiv = document.getElementById("loading");
60
+ const btnText = unlockBtn.querySelector(".btn-text");
61
+ const btnLoading = unlockBtn.querySelector(".btn-loading");
62
+
63
+ form.addEventListener("submit", async function (e) {
64
+ e.preventDefault();
65
+ const password = passwordInput.value;
66
+ if (!password) return;
67
+ await attemptDecryption(password);
68
+ });
69
+
70
+ function setLoading(loading) {
71
+ unlockBtn.disabled = loading;
72
+ btnText.style.display = loading ? "none" : "inline";
73
+ btnLoading.style.display = loading ? "inline" : "none";
74
+ loadingDiv.style.display = loading ? "block" : "none";
75
+ }
76
+
77
+ function showError(message) {
78
+ errorDiv.textContent = message;
79
+ errorDiv.style.display = "block";
80
+ errorDiv.style.animation = "shake 0.5s ease-in-out";
81
+ setTimeout(() => {
82
+ errorDiv.style.animation = "";
83
+ }, 500);
84
+ }
85
+
86
+ function hideError() {
87
+ errorDiv.style.display = "none";
88
+ }
89
+
90
+ async function decrypt(encryptedData, password) {
91
+ try {
92
+ const encrypted = Uint8Array.from(
93
+ atob(encryptedData),
94
+ (c) => c.charCodeAt(0),
95
+ );
96
+ const encoder = new TextEncoder();
97
+ const passwordBytes = encoder.encode(password);
98
+ const hashBuffer = await crypto.subtle.digest(
99
+ "SHA-256",
100
+ passwordBytes,
101
+ );
102
+ const key = new Uint8Array(hashBuffer);
103
+
104
+ const decrypted = new Uint8Array(encrypted.length);
105
+ for (let i = 0; i < encrypted.length; i++) {
106
+ decrypted[i] = encrypted[i] ^ key[i % key.length];
107
+ }
108
+
109
+ const result = new TextDecoder().decode(decrypted);
110
+ return result;
111
+ } catch (error) {
112
+ throw new Error("Decryption failed");
113
+ }
114
+ }
115
+
116
+ function isValidHTML(str) {
117
+ const hasHTMLTags = /<[a-z][\s\S]*>/i.test(str);
118
+ const hasDoctype = /<!DOCTYPE/i.test(str);
119
+ const hasHtmlTag = /<html/i.test(str);
120
+ const nonPrintable = str.match(
121
+ /[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g,
122
+ );
123
+ const nonPrintableRatio = nonPrintable
124
+ ? nonPrintable.length / str.length
125
+ : 0;
126
+ return (
127
+ (hasHTMLTags || hasDoctype || hasHtmlTag) &&
128
+ nonPrintableRatio < 0.1
129
+ );
130
+ }
131
+
132
+ async function attemptDecryption(password) {
133
+ if (attempts >= maxAttempts) {
134
+ showError(
135
+ "Trop de tentatives. Veuillez rafraîchir la page.",
136
+ );
137
+ return;
138
+ }
139
+
140
+ setLoading(true);
141
+ hideError();
142
+
143
+ try {
144
+ const decrypted = await decrypt(
145
+ encryptedData,
146
+ password,
147
+ );
148
+
149
+ if (!isValidHTML(decrypted)) {
150
+ throw new Error("Invalid decryption result");
151
+ }
152
+
153
+ const container = document.querySelector(
154
+ ".encrypted-container",
155
+ );
156
+ container.outerHTML = decrypted;
157
+ } catch (error) {
158
+ attempts++;
159
+ const remaining = maxAttempts - attempts;
160
+
161
+ if (remaining > 0) {
162
+ showError(
163
+ `Mot de passe incorrect. Veuillez réessayer. (${remaining} tentatives restantes)`,
164
+ );
165
+ } else {
166
+ showError(
167
+ "Trop de tentatives. Veuillez rafraîchir la page.",
168
+ );
169
+ unlockBtn.disabled = true;
170
+ }
171
+
172
+ passwordInput.value = "";
173
+ passwordInput.focus();
174
+ } finally {
175
+ setLoading(false);
176
+ }
177
+ }
178
+
179
+ const hash = window.location.hash;
180
+ if (hash && hash.length > 1) {
181
+ const passwordFromHash = hash.substring(1);
182
+ if (passwordFromHash) {
183
+ history.replaceState(
184
+ null,
185
+ null,
186
+ window.location.pathname + window.location.search,
187
+ );
188
+ attemptDecryption(passwordFromHash);
189
+ }
190
+ } else {
191
+ passwordInput.focus();
192
+ }
193
+ </script>
194
+ </main>
195
+ </body>
196
+ </html>
package/Meta.astro ADDED
@@ -0,0 +1,402 @@
1
+ ---
2
+ /**
3
+ * Meta.astro - Universal Meta Layout
4
+ *
5
+ * A flexible meta tag component for any Astro project.
6
+ * Supports standard meta tags, Open Graph, Twitter Cards, and Schema.org.
7
+ */
8
+ import { getExcerpt } from "@stnd/utils";
9
+ // import { ClientRouter } from "astro:transitions";
10
+
11
+ // Access site configuration from global
12
+ const config = (globalThis['__STANDARD_CONFIG__'] as any) || {};
13
+
14
+ // Props interface for universal usage
15
+ interface Props {
16
+ // Core metadata
17
+ frontmatter?: any;
18
+ site?: any;
19
+ page?: any;
20
+ title?: string;
21
+ description?: string;
22
+ image?: string;
23
+ created?: string;
24
+ modified?: string;
25
+ tags?: string[];
26
+ securityHeaders?: any;
27
+
28
+ // Advanced options
29
+ dataSource?: any; // Optional data object with toViewModel() method for dynamic content
30
+ rssFeedUrl?: string | null; // Custom RSS feed URL
31
+ rssFeedTitle?: string | undefined; // Custom RSS feed title
32
+ enableClientRouter?: boolean; // Enable Astro view transitions
33
+ injectState?: boolean; // Inject state to window for client-side hydration
34
+ faviconPath?: string; // Custom favicon path (defaults to /favicons or /assets/favicon)
35
+ fontsUrl?: string; // Optional external fonts URL
36
+ language?: string; // Page language override
37
+ headExtensions?: string | string[]; // Plugin head extensions
38
+ }
39
+
40
+ const {
41
+ frontmatter,
42
+ site: propsSite,
43
+ page = {},
44
+ title: propsTitle,
45
+ description: propsDescription,
46
+ image: propsImage,
47
+ created: propsCreated,
48
+ modified: propsModified,
49
+ tags: propsTags,
50
+ securityHeaders: propsSecurityHeaders,
51
+ dataSource,
52
+ rssFeedUrl,
53
+ rssFeedTitle,
54
+ enableClientRouter = false,
55
+ injectState = false,
56
+ faviconPath,
57
+ fontsUrl,
58
+ language: propsLanguage,
59
+ headExtensions,
60
+ // Backward compatibility (deprecated)
61
+ note,
62
+ injectVisitorState,
63
+ } = Astro.props as Props & { note?: any; injectVisitorState?: boolean };
64
+
65
+ // Backward compatibility: map old prop names to new ones
66
+ const effectiveDataSource = dataSource || note;
67
+ const effectiveInjectState = injectState || injectVisitorState || false;
68
+
69
+ // Access context data if available (framework-agnostic)
70
+ const contextData = (Astro.locals as any)?.garden || (Astro.locals as any)?.context;
71
+ const dataView = effectiveDataSource && typeof effectiveDataSource.toViewModel === 'function'
72
+ ? await effectiveDataSource.toViewModel()
73
+ : null;
74
+
75
+ // Build site configuration (prefer props, fallback to config)
76
+ const site = propsSite || config?.site || config || {
77
+ title: "Site Title",
78
+ url: Astro.site || "",
79
+ };
80
+
81
+ // Get user preferences if context is available
82
+ const userPrefs = contextData?.visitor?.raw?.preferences || contextData?.preferences || {
83
+ theme: "default",
84
+ colorMode: "light",
85
+ uiLanguage: "en",
86
+ };
87
+
88
+ // Extract frontmatter from data source if available
89
+ const sourceFrontmatter = dataView?.frontmatter || effectiveDataSource?.raw?.frontmatter || {};
90
+ const effectiveFrontmatter = { ...frontmatter, ...sourceFrontmatter };
91
+
92
+ // Build metadata with dynamic data support
93
+ const ogData = dataView?.og || {};
94
+ const title = propsTitle || ogData.title || effectiveFrontmatter?.title || config?.title;
95
+ const description = propsDescription || ogData.description || effectiveFrontmatter?.description || config?.description;
96
+ const image = propsImage || ogData.image || effectiveFrontmatter?.image;
97
+ const created = propsCreated || effectiveFrontmatter?.created;
98
+ const modified = propsModified || effectiveFrontmatter?.modified;
99
+ const tags = propsTags || effectiveFrontmatter?.tags;
100
+ const securityHeaders = propsSecurityHeaders || effectiveFrontmatter?.securityHeaders || site?.securityHeaders;
101
+
102
+ const fullTitle = title && title !== site.title ? `${title} · ${site.title}` : site.title;
103
+
104
+ // Ensure we have a valid base URL
105
+ const baseUrl = site.url || config?.url || Astro.site || 'http://localhost:4321';
106
+ const pageUrl = new URL(Astro.url.pathname, baseUrl);
107
+ const canonicalUrl = new URL(
108
+ Astro.url.pathname.replace(/index\.html$/, ""),
109
+ baseUrl,
110
+ );
111
+
112
+ // Description logic
113
+ const pageExcerpt = page.rawInput ? String(page.rawInput) : "";
114
+ const metaDescription = (description ||
115
+ getExcerpt(pageExcerpt, { maxChars: 150 }) ||
116
+ site.description ||
117
+ "").toString();
118
+
119
+ // Image logic
120
+ const imageUrl = image
121
+ ? `${String(site.url)}/assets/img/${image}`
122
+ : ogData.image || (site.url
123
+ ? `${String(site.url).replace(/\/$/, "")}/assets/img/default.png`
124
+ : "/assets/img/default.png");
125
+
126
+ const imageAlt = title || site.title;
127
+
128
+ // Date logic
129
+ const datePublished = ogData.date
130
+ ? new Date(ogData.date).toISOString()
131
+ : created
132
+ ? new Date(created).toISOString()
133
+ : dataView?.date
134
+ ? new Date(dataView.date).toISOString()
135
+ : null;
136
+ const dateModified = modified || created
137
+ ? new Date(modified || created).toISOString()
138
+ : null;
139
+
140
+ const authorName = ogData.author || site.author?.name || site.author || "";
141
+ const twitterUsername = site.twitter_username || site.twitter || "";
142
+
143
+ // Determine language
144
+ const pageLanguage = propsLanguage || userPrefs.uiLanguage || site.language || "en";
145
+
146
+ // Determine favicon path structure
147
+ const usesAssetsFavicon = faviconPath === "/assets/favicon" || (contextData && !faviconPath);
148
+ const faviconBasePath = faviconPath || (usesAssetsFavicon ? "/assets/favicon" : `${site.url || ""}/favicons`);
149
+
150
+ // Process head extensions
151
+ const headExtraHtml = Array.isArray(headExtensions) ? headExtensions.join("\n") : (headExtensions || "");
152
+ ---
153
+
154
+ <meta charset="utf-8" />
155
+ {headExtraHtml && <Fragment set:html={headExtraHtml} />}
156
+ <!-- Standard Framework | https://standard.ffp.co -->
157
+
158
+ <meta
159
+ name="viewport"
160
+ content="width=device-width, initial-scale=1, viewport-fit=cover"
161
+ />
162
+
163
+ <!-- iOS PWA support for safe-area-inset -->
164
+ {contextData && (
165
+ <>
166
+ <meta name="mobile-web-app-capable" content="yes" />
167
+ <meta
168
+ name="apple-mobile-web-app-status-bar-style"
169
+ content="black-translucent"
170
+ />
171
+ </>
172
+ )}
173
+
174
+ {fontsUrl && (
175
+ <!-- External fonts -->
176
+ <link rel="stylesheet" href={fontsUrl} />
177
+ )}
178
+
179
+ <title>{fullTitle}</title>
180
+ <meta name="description" content={metaDescription} />
181
+ <meta name="author" content={authorName} />
182
+ <meta name="language" content={pageLanguage} />
183
+ <meta name="referrer" content="strict-origin-when-cross-origin" />
184
+
185
+ <!-- Canonical URL -->
186
+ <link rel="canonical" href={canonicalUrl} />
187
+
188
+ <!-- Feed & Sitemap -->
189
+ {
190
+ (site?.url || rssFeedUrl) ? (
191
+ <>
192
+ <link
193
+ rel="alternate"
194
+ type="application/atom+xml"
195
+ title={rssFeedTitle || site.title}
196
+ href={rssFeedUrl || `${site.url}/feed.xml`}
197
+ />
198
+ {site?.url && (
199
+ <link
200
+ rel="sitemap"
201
+ type="application/xml"
202
+ title="Sitemap"
203
+ href={`${site.url}/sitemap.xml`}
204
+ />
205
+ )}
206
+ </>
207
+ ) : null
208
+ }
209
+
210
+ {
211
+ securityHeaders && (
212
+ <>
213
+ {securityHeaders.csp && (
214
+ <meta
215
+ http-equiv="Content-Security-Policy"
216
+ content={securityHeaders.csp}
217
+ />
218
+ )}
219
+ {securityHeaders.referrerPolicy && (
220
+ <meta
221
+ http-equiv="Referrer-Policy"
222
+ content={securityHeaders.referrerPolicy}
223
+ />
224
+ )}
225
+ {securityHeaders.permissions && (
226
+ <meta
227
+ http-equiv="Permissions-Policy"
228
+ content={securityHeaders.permissions}
229
+ />
230
+ )}
231
+ {securityHeaders.xContentTypeOptions && (
232
+ <meta
233
+ http-equiv="X-Content-Type-Options"
234
+ content={securityHeaders.xContentTypeOptions}
235
+ />
236
+ )}
237
+ </>
238
+ )
239
+ }
240
+
241
+ <!-- Robots Meta -->
242
+ {
243
+ site?.visibility === "private" ? (
244
+ <meta name="robots" content="noindex, nofollow" />
245
+ ) : (
246
+ <meta
247
+ name="robots"
248
+ content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"
249
+ />
250
+ )
251
+ }
252
+
253
+ <!-- Open Graph -->
254
+ <meta property="og:title" content={title || site.title} />
255
+ <meta property="og:type" content={title ? "article" : "website"} />
256
+ <meta property="og:locale" content={pageLanguage} />
257
+ <meta property="og:site_name" content={site.title} />
258
+ <meta property="og:url" content={pageUrl} />
259
+ <meta property="og:description" content={metaDescription} />
260
+
261
+ <meta property="og:image" content={imageUrl} />
262
+ <meta property="og:image:alt" content={imageAlt} />
263
+ <meta property="og:image:width" content="1200" />
264
+ <meta property="og:image:height" content="630" />
265
+
266
+ {
267
+ datePublished && (
268
+ <>
269
+ <meta property="article:published_time" content={datePublished} />
270
+ <meta property="article:author" content={authorName} />
271
+ {tags &&
272
+ tags.map((tag: string) => (
273
+ <meta property="article:tag" content={tag} />
274
+ ))}
275
+ </>
276
+ )
277
+ }
278
+
279
+ <!-- Twitter Cards -->
280
+ <meta name="twitter:card" content={imageUrl ? "summary_large_image" : "summary"} />
281
+ <meta name="twitter:image" content={imageUrl} />
282
+ <meta name="twitter:image:alt" content={imageAlt} />
283
+ <meta name="twitter:site" content={`@${twitterUsername}`} />
284
+ <meta name="twitter:creator" content={`@${twitterUsername}`} />
285
+ <meta name="twitter:title" content={fullTitle} />
286
+ <meta name="twitter:description" content={metaDescription} />
287
+
288
+ <!-- Schema.org -->
289
+ <script
290
+ type="application/ld+json"
291
+ set:html={JSON.stringify({
292
+ "@context": "https://schema.org",
293
+ "@type": title ? "BlogPosting" : "WebSite",
294
+ ...(title
295
+ ? {
296
+ headline: title,
297
+ description: metaDescription,
298
+ author: {
299
+ "@type": "Person",
300
+ name: authorName,
301
+ url: `${site.url}/now/`,
302
+ },
303
+ publisher: {
304
+ "@type": "Person",
305
+ name: authorName,
306
+ url: site.url,
307
+ },
308
+ ...(datePublished ? { datePublished } : {}),
309
+ ...(dateModified ? { dateModified } : {}),
310
+ image: {
311
+ "@type": "ImageObject",
312
+ url: imageUrl,
313
+ width: 1200,
314
+ height: 630,
315
+ },
316
+ mainEntityOfPage: {
317
+ "@type": "WebPage",
318
+ "@id": pageUrl,
319
+ },
320
+ }
321
+ : {
322
+ name: site.title,
323
+ description: site.description,
324
+ url: site.url,
325
+ author: {
326
+ "@type": "Person",
327
+ name: authorName,
328
+ url: site.url,
329
+ },
330
+ }),
331
+ })}
332
+ />
333
+
334
+ <!-- Favicons -->
335
+ {usesAssetsFavicon ? (
336
+ <>
337
+ <link
338
+ rel="icon"
339
+ type="image/png"
340
+ href={`${faviconBasePath}/favicon-96x96.png`}
341
+ sizes="96x96"
342
+ />
343
+ <link rel="icon" type="image/svg+xml" href={`${faviconBasePath}/favicon.svg`} />
344
+ <link rel="shortcut icon" href={`${faviconBasePath}/favicon.ico`} />
345
+ <link
346
+ rel="apple-touch-icon"
347
+ sizes="180x180"
348
+ href={`${faviconBasePath}/apple-touch-icon.png`}
349
+ />
350
+ <meta name="apple-mobile-web-app-title" content={site.title} />
351
+ <link rel="manifest" href={`${faviconBasePath}/site.webmanifest`} />
352
+ </>
353
+ ) : (
354
+ <>
355
+ <link
356
+ rel="apple-touch-icon"
357
+ sizes="180x180"
358
+ href={`${faviconBasePath}/apple-touch-icon.png`}
359
+ />
360
+ <link
361
+ rel="icon"
362
+ type="image/png"
363
+ sizes="32x32"
364
+ href={`${faviconBasePath}/favicon-32x32.png`}
365
+ />
366
+ <link
367
+ rel="icon"
368
+ type="image/png"
369
+ sizes="16x16"
370
+ href={`${faviconBasePath}/favicon-16x16.png`}
371
+ />
372
+ <link rel="manifest" href="/site.webmanifest" />
373
+ <link
374
+ rel="mask-icon"
375
+ href={`${faviconBasePath}/safari-pinned-tab.svg`}
376
+ color="#000000"
377
+ />
378
+ <link rel="shortcut icon" href={`${faviconBasePath}/favicon.ico`} />
379
+ </>
380
+ )}
381
+
382
+ <!-- Mobile -->
383
+ <meta
384
+ name="theme-color"
385
+ content="#ffffff"
386
+ media="(prefers-color-scheme: light)"
387
+ />
388
+ <meta
389
+ name="theme-color"
390
+ content="#000000"
391
+ media="(prefers-color-scheme: dark)"
392
+ />
393
+
394
+ <!-- Windows Tiles -->
395
+ <meta name="msapplication-TileColor" content="#000000" />
396
+ <meta
397
+ name="msapplication-config"
398
+ content={`${site.url || ""}/favicons/browserconfig.xml`}
399
+ />
400
+
401
+ <!-- Client Router (Astro view transitions) -->
402
+ <!-- {enableClientRouter && <ClientRouter />} -->
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@stnd/layout",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "exports": {
6
+ "./*": "./*.astro"
7
+ },
8
+ "dependencies": {
9
+ "@stnd/utils": "0.2.0",
10
+ "@stnd/log": "0.2.0"
11
+ },
12
+ "peerDependencies": {
13
+ "astro": ">=5.0.0"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ }
18
+ }