@parche/core 0.3.0-alpha.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/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +55 -0
- package/src/components/DynamicRenderer.astro +96 -0
- package/src/components/LayoutRenderer.astro +65 -0
- package/src/components/SectionWrapper.astro +46 -0
- package/src/components/common/LocaleSwitcher.astro +168 -0
- package/src/components/common/OptimizedImage.astro +41 -0
- package/src/components/common/ThemePanel.astro +152 -0
- package/src/components/common/ThemeSelector.astro +67 -0
- package/src/components/common/ThemeToggle.astro +82 -0
- package/src/config/font-variables.ts +29 -0
- package/src/config/fonts.ts +18 -0
- package/src/content/index.ts +11 -0
- package/src/content/schemas.ts +253 -0
- package/src/integration/index.ts +156 -0
- package/src/integration/registry.ts +193 -0
- package/src/integration/types.ts +193 -0
- package/src/integration/virtual.d.ts +326 -0
- package/src/integration/vite-plugin-parche.ts +432 -0
- package/src/layouts/BaseLayout.astro +121 -0
- package/src/routes/404.astro +46 -0
- package/src/routes/[...slug].astro +216 -0
- package/src/routes/middleware.ts +18 -0
- package/src/styles/base.css +221 -0
- package/src/styles/semantic.css +95 -0
- package/src/styles/shadcn-compat.css +120 -0
- package/src/styles/tokens.css +237 -0
- package/src/types/config.ts +102 -0
- package/src/utils/i18n.ts +79 -0
- package/src/utils/layout.ts +46 -0
- package/src/utils/metadata.ts +277 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { MetadataEntry, PageEntry } from '../content/schemas.js';
|
|
2
|
+
import type { SiteConfig } from '../types/config.js';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Resolved metadata — the fully merged result passed to BaseLayout
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export interface ResolvedMetadata {
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
canonical?: string;
|
|
12
|
+
keywords?: string;
|
|
13
|
+
noindex: boolean;
|
|
14
|
+
nofollow: boolean;
|
|
15
|
+
robots?: { maxSnippet?: number; maxImagePreview?: 'none' | 'standard' | 'large'; maxVideoPreview?: number };
|
|
16
|
+
ogTitle: string;
|
|
17
|
+
ogDescription: string;
|
|
18
|
+
ogImage?: string;
|
|
19
|
+
ogType: string;
|
|
20
|
+
twitterCard: string;
|
|
21
|
+
article?: {
|
|
22
|
+
author?: string;
|
|
23
|
+
publishedDate?: string;
|
|
24
|
+
modifiedDate?: string;
|
|
25
|
+
section?: string;
|
|
26
|
+
tags?: string[];
|
|
27
|
+
};
|
|
28
|
+
jsonLd?: unknown;
|
|
29
|
+
siteName: string;
|
|
30
|
+
locale: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Merge page-level metadata with site-wide config defaults.
|
|
35
|
+
* Resolution order: metadata field → page field → site config → hardcoded fallback.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveMetadata(
|
|
38
|
+
pageData: PageEntry,
|
|
39
|
+
config: SiteConfig,
|
|
40
|
+
options: { locale?: string } = {},
|
|
41
|
+
): ResolvedMetadata {
|
|
42
|
+
const meta = pageData.metadata;
|
|
43
|
+
const seo = config.seo ?? {};
|
|
44
|
+
|
|
45
|
+
const title = meta?.title ?? pageData.title;
|
|
46
|
+
const description = meta?.description ?? pageData.description ?? '';
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
title,
|
|
50
|
+
description,
|
|
51
|
+
canonical: meta?.canonical,
|
|
52
|
+
keywords: meta?.keywords,
|
|
53
|
+
noindex: meta?.noindex ?? false,
|
|
54
|
+
nofollow: meta?.nofollow ?? false,
|
|
55
|
+
robots: meta?.robots ?? seo.defaultRobots,
|
|
56
|
+
ogTitle: meta?.ogTitle ?? title,
|
|
57
|
+
ogDescription: meta?.ogDescription ?? description,
|
|
58
|
+
ogImage: meta?.ogImage ?? config.metadata?.ogImage,
|
|
59
|
+
ogType: meta?.ogType ?? seo.defaultOgType ?? 'website',
|
|
60
|
+
twitterCard: meta?.twitterCard ?? seo.defaultTwitterCard ?? 'summary_large_image',
|
|
61
|
+
article: meta?.article,
|
|
62
|
+
jsonLd: meta?.jsonLd,
|
|
63
|
+
siteName: config.site.name,
|
|
64
|
+
locale: options.locale ?? config.site.defaultLanguage ?? 'en',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Robots meta content builder
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export function buildRobotsContent(resolved: ResolvedMetadata): string {
|
|
73
|
+
const parts: string[] = [
|
|
74
|
+
resolved.noindex ? 'noindex' : 'index',
|
|
75
|
+
resolved.nofollow ? 'nofollow' : 'follow',
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const robots = resolved.robots;
|
|
79
|
+
if (robots) {
|
|
80
|
+
if (robots.maxSnippet != null) parts.push(`max-snippet:${robots.maxSnippet}`);
|
|
81
|
+
if (robots.maxImagePreview) parts.push(`max-image-preview:${robots.maxImagePreview}`);
|
|
82
|
+
if (robots.maxVideoPreview != null) parts.push(`max-video-preview:${robots.maxVideoPreview}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parts.join(', ');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Breadcrumb helper
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
export interface BreadcrumbItem {
|
|
93
|
+
name: string;
|
|
94
|
+
url: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildBreadcrumbs(urlPath: string, siteUrl: string, pageTitle: string): BreadcrumbItem[] {
|
|
98
|
+
const items: BreadcrumbItem[] = [{ name: 'Home', url: siteUrl }];
|
|
99
|
+
const segments = urlPath.replace(/^\/|\/$/g, '').split('/').filter(Boolean);
|
|
100
|
+
|
|
101
|
+
let accumulated = siteUrl.replace(/\/$/, '');
|
|
102
|
+
for (let i = 0; i < segments.length; i++) {
|
|
103
|
+
accumulated += `/${segments[i]}`;
|
|
104
|
+
const name = i === segments.length - 1
|
|
105
|
+
? pageTitle
|
|
106
|
+
: segments[i].charAt(0).toUpperCase() + segments[i].slice(1).replace(/-/g, ' ');
|
|
107
|
+
items.push({ name, url: accumulated });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return items;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// JSON-LD generators — return plain objects for @graph composition
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
export interface JsonLdWebSite {
|
|
118
|
+
name: string;
|
|
119
|
+
url: string;
|
|
120
|
+
description?: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface JsonLdWebPage {
|
|
124
|
+
name: string;
|
|
125
|
+
url: string;
|
|
126
|
+
description?: string;
|
|
127
|
+
isPartOf?: { name: string; url: string };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function generateJsonLdWebSite(site: JsonLdWebSite): Record<string, unknown> {
|
|
131
|
+
return {
|
|
132
|
+
'@type': 'WebSite',
|
|
133
|
+
name: site.name,
|
|
134
|
+
url: site.url,
|
|
135
|
+
...(site.description && { description: site.description }),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function generateJsonLdWebPage(page: JsonLdWebPage): Record<string, unknown> {
|
|
140
|
+
return {
|
|
141
|
+
'@type': 'WebPage',
|
|
142
|
+
name: page.name,
|
|
143
|
+
url: page.url,
|
|
144
|
+
...(page.description && { description: page.description }),
|
|
145
|
+
...(page.isPartOf && {
|
|
146
|
+
isPartOf: {
|
|
147
|
+
'@type': 'WebSite',
|
|
148
|
+
name: page.isPartOf.name,
|
|
149
|
+
url: page.isPartOf.url,
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function generateJsonLdOrganization(config: SiteConfig): Record<string, unknown> | null {
|
|
156
|
+
const org = config.organization;
|
|
157
|
+
if (!org) return null;
|
|
158
|
+
|
|
159
|
+
const name = org.name ?? config.site.name;
|
|
160
|
+
if (!name) return null;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
'@type': org.type ?? 'Organization',
|
|
164
|
+
name,
|
|
165
|
+
...(org.legalName && { legalName: org.legalName }),
|
|
166
|
+
...(org.url ?? config.site.url ? { url: org.url ?? config.site.url } : {}),
|
|
167
|
+
...(org.logo ?? config.site.logo ? { logo: org.logo ?? config.site.logo } : {}),
|
|
168
|
+
...(org.description && { description: org.description }),
|
|
169
|
+
...(org.foundingDate && { foundingDate: org.foundingDate }),
|
|
170
|
+
...(org.socialProfiles?.length && { sameAs: org.socialProfiles }),
|
|
171
|
+
...(org.address && {
|
|
172
|
+
address: {
|
|
173
|
+
'@type': 'PostalAddress',
|
|
174
|
+
...(org.address.street && { streetAddress: org.address.street }),
|
|
175
|
+
...(org.address.city && { addressLocality: org.address.city }),
|
|
176
|
+
...(org.address.region && { addressRegion: org.address.region }),
|
|
177
|
+
...(org.address.postalCode && { postalCode: org.address.postalCode }),
|
|
178
|
+
...(org.address.country && { addressCountry: org.address.country }),
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
...(org.contactPoint && {
|
|
182
|
+
contactPoint: {
|
|
183
|
+
'@type': 'ContactPoint',
|
|
184
|
+
...(org.contactPoint.telephone && { telephone: org.contactPoint.telephone }),
|
|
185
|
+
...(org.contactPoint.contactType && { contactType: org.contactPoint.contactType }),
|
|
186
|
+
...(org.contactPoint.email && { email: org.contactPoint.email }),
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function generateJsonLdBreadcrumbList(items: BreadcrumbItem[]): Record<string, unknown> {
|
|
193
|
+
return {
|
|
194
|
+
'@type': 'BreadcrumbList',
|
|
195
|
+
itemListElement: items.map((item, i) => ({
|
|
196
|
+
'@type': 'ListItem',
|
|
197
|
+
position: i + 1,
|
|
198
|
+
name: item.name,
|
|
199
|
+
item: item.url,
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function generateJsonLdArticle(
|
|
205
|
+
resolved: ResolvedMetadata,
|
|
206
|
+
pageUrl: string,
|
|
207
|
+
): Record<string, unknown> | null {
|
|
208
|
+
if (resolved.ogType !== 'article' || !resolved.article) return null;
|
|
209
|
+
|
|
210
|
+
const article = resolved.article;
|
|
211
|
+
return {
|
|
212
|
+
'@type': 'Article',
|
|
213
|
+
headline: resolved.title,
|
|
214
|
+
url: pageUrl,
|
|
215
|
+
...(resolved.description && { description: resolved.description }),
|
|
216
|
+
...(resolved.ogImage && { image: resolved.ogImage }),
|
|
217
|
+
...(article.author && {
|
|
218
|
+
author: { '@type': 'Person', name: article.author },
|
|
219
|
+
}),
|
|
220
|
+
...(article.publishedDate && { datePublished: article.publishedDate }),
|
|
221
|
+
...(article.modifiedDate && { dateModified: article.modifiedDate }),
|
|
222
|
+
...(article.section && { articleSection: article.section }),
|
|
223
|
+
...(article.tags?.length && { keywords: article.tags }),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build the complete JSON-LD @graph for a page.
|
|
229
|
+
*/
|
|
230
|
+
export function buildJsonLdGraph(
|
|
231
|
+
resolved: ResolvedMetadata,
|
|
232
|
+
pageUrl: string,
|
|
233
|
+
siteUrl: string,
|
|
234
|
+
config: SiteConfig,
|
|
235
|
+
breadcrumbs?: BreadcrumbItem[],
|
|
236
|
+
): string {
|
|
237
|
+
const graph: Record<string, unknown>[] = [];
|
|
238
|
+
|
|
239
|
+
// WebSite
|
|
240
|
+
graph.push(generateJsonLdWebSite({
|
|
241
|
+
name: resolved.siteName,
|
|
242
|
+
url: siteUrl,
|
|
243
|
+
description: config.site.description || undefined,
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
// WebPage
|
|
247
|
+
graph.push(generateJsonLdWebPage({
|
|
248
|
+
name: resolved.title,
|
|
249
|
+
url: pageUrl,
|
|
250
|
+
description: resolved.description || undefined,
|
|
251
|
+
isPartOf: { name: resolved.siteName, url: siteUrl },
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
// Organization
|
|
255
|
+
const org = generateJsonLdOrganization(config);
|
|
256
|
+
if (org) graph.push(org);
|
|
257
|
+
|
|
258
|
+
// BreadcrumbList
|
|
259
|
+
if (breadcrumbs && breadcrumbs.length > 1) {
|
|
260
|
+
graph.push(generateJsonLdBreadcrumbList(breadcrumbs));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Article
|
|
264
|
+
const article = generateJsonLdArticle(resolved, pageUrl);
|
|
265
|
+
if (article) graph.push(article);
|
|
266
|
+
|
|
267
|
+
// Custom JSON-LD escape hatch
|
|
268
|
+
if (resolved.jsonLd) {
|
|
269
|
+
if (Array.isArray(resolved.jsonLd)) {
|
|
270
|
+
graph.push(...(resolved.jsonLd as Record<string, unknown>[]));
|
|
271
|
+
} else {
|
|
272
|
+
graph.push(resolved.jsonLd as Record<string, unknown>);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return JSON.stringify({ '@context': 'https://schema.org', '@graph': graph });
|
|
277
|
+
}
|