meno-core 1.0.15 → 1.0.18
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/bin/cli.ts +13 -4
- package/build-static.ts +40 -11
- package/lib/client/core/ComponentBuilder.ts +1 -1
- package/lib/client/templateEngine.ts +18 -1
- package/lib/server/routes/api/pages.ts +2 -1
- package/lib/server/services/pageService.ts +24 -0
- package/lib/server/ssr/attributeBuilder.ts +1 -1
- package/lib/server/ssr/htmlGenerator.ts +17 -2
- package/lib/server/ssr/imageMetadata.ts +1 -1
- package/lib/server/ssr/ssrRenderer.ts +64 -18
- package/lib/server/ssrRenderer.test.ts +0 -39
- package/lib/shared/cssProperties.ts +84 -7
- package/lib/shared/itemTemplateUtils.ts +24 -0
- package/lib/shared/types/api.ts +2 -0
- package/lib/shared/types/cms.ts +1 -1
- package/lib/shared/utilityClassConfig.ts +70 -4
- package/lib/shared/validation/schemas.ts +2 -0
- package/package.json +1 -1
- /package/lib/shared/registry/nodeTypes/{ObjectLinkNodeType.ts → LinkNodeType.ts} +0 -0
package/bin/cli.ts
CHANGED
|
@@ -27,9 +27,13 @@ Commands:
|
|
|
27
27
|
serve Serve built files from ./dist on port 8080
|
|
28
28
|
init Initialize a new project
|
|
29
29
|
|
|
30
|
+
Options:
|
|
31
|
+
build --dev Include draft pages in build (for local preview)
|
|
32
|
+
|
|
30
33
|
Examples:
|
|
31
34
|
meno dev Start dev server in current directory
|
|
32
|
-
meno build Build static files to ./dist
|
|
35
|
+
meno build Build static files to ./dist (excludes drafts)
|
|
36
|
+
meno build --dev Build including draft pages
|
|
33
37
|
meno serve Serve built files on port 8080
|
|
34
38
|
meno init my-project Create new project in ./my-project
|
|
35
39
|
`);
|
|
@@ -167,7 +171,7 @@ async function runDev() {
|
|
|
167
171
|
await import('../entries/server-router');
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
async function runBuild() {
|
|
174
|
+
async function runBuild(isDev: boolean = false) {
|
|
171
175
|
const projectRoot = process.cwd();
|
|
172
176
|
|
|
173
177
|
// Validate project structure
|
|
@@ -179,7 +183,12 @@ async function runBuild() {
|
|
|
179
183
|
// Set project root for path resolution
|
|
180
184
|
setProjectRoot(projectRoot);
|
|
181
185
|
|
|
182
|
-
|
|
186
|
+
// Set dev mode environment variable (drafts are built in dev mode)
|
|
187
|
+
if (isDev) {
|
|
188
|
+
process.env.MENO_DEV_BUILD = 'true';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`📁 Building project: ${projectRoot}${isDev ? ' (dev mode - including drafts)' : ''}`);
|
|
183
192
|
|
|
184
193
|
// Import and run build
|
|
185
194
|
await import('../build-static.ts');
|
|
@@ -397,7 +406,7 @@ switch (command) {
|
|
|
397
406
|
runDev();
|
|
398
407
|
break;
|
|
399
408
|
case 'build':
|
|
400
|
-
runBuild();
|
|
409
|
+
runBuild(args.includes('--dev'));
|
|
401
410
|
break;
|
|
402
411
|
case 'serve':
|
|
403
412
|
runServe();
|
package/build-static.ts
CHANGED
|
@@ -319,6 +319,13 @@ async function buildCMSTemplates(
|
|
|
319
319
|
try {
|
|
320
320
|
const pageData = parseJSON<JSONPage>(templateContent);
|
|
321
321
|
|
|
322
|
+
// Skip draft templates in production (not in dev mode)
|
|
323
|
+
const isDevBuild = process.env.MENO_DEV_BUILD === 'true';
|
|
324
|
+
if (pageData.meta?.draft === true && !isDevBuild) {
|
|
325
|
+
console.log(`⏭️ Skipping draft template: ${file}`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
322
329
|
if (!isCMSPage(pageData)) {
|
|
323
330
|
console.warn(`⚠️ ${file} is in templates/ but missing meta.source: "cms"`);
|
|
324
331
|
continue;
|
|
@@ -423,18 +430,33 @@ async function buildStaticPages(): Promise<void> {
|
|
|
423
430
|
copyDirectory(functionsDir, join(distDir, "functions"));
|
|
424
431
|
}
|
|
425
432
|
|
|
426
|
-
// Copy
|
|
433
|
+
// Copy user-created root files for static hosting
|
|
427
434
|
const hostingFiles: string[] = [];
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
435
|
+
const rootFilesToCopy = [
|
|
436
|
+
'_headers', // Netlify/Cloudflare headers
|
|
437
|
+
'_redirects', // Netlify/Cloudflare redirects
|
|
438
|
+
'llms.txt', // LLM context
|
|
439
|
+
'humans.txt', // Team credits
|
|
440
|
+
'ads.txt', // Ad verification
|
|
441
|
+
'security.txt', // Security contact
|
|
442
|
+
'CNAME', // GitHub Pages domain
|
|
443
|
+
'manifest.json', // PWA manifest
|
|
444
|
+
'site.webmanifest', // PWA manifest (alt)
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
for (const file of rootFilesToCopy) {
|
|
448
|
+
const filePath = join(projectPaths.project, file);
|
|
449
|
+
if (existsSync(filePath)) {
|
|
450
|
+
copyFileSync(filePath, join(distDir, file));
|
|
451
|
+
hostingFiles.push(file);
|
|
452
|
+
}
|
|
434
453
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
454
|
+
|
|
455
|
+
// Copy .well-known directory if exists
|
|
456
|
+
const wellKnownDir = join(projectPaths.project, '.well-known');
|
|
457
|
+
if (existsSync(wellKnownDir)) {
|
|
458
|
+
copyDirectory(wellKnownDir, join(distDir, '.well-known'));
|
|
459
|
+
hostingFiles.push('.well-known/');
|
|
438
460
|
}
|
|
439
461
|
|
|
440
462
|
const parts = ['Assets'];
|
|
@@ -508,6 +530,13 @@ async function buildStaticPages(): Promise<void> {
|
|
|
508
530
|
try {
|
|
509
531
|
const pageData = parseJSON<JSONPage>(pageContent);
|
|
510
532
|
|
|
533
|
+
// Skip draft pages in production (not in dev mode)
|
|
534
|
+
const isDevBuild = process.env.MENO_DEV_BUILD === 'true';
|
|
535
|
+
if (pageData.meta?.draft === true && !isDevBuild) {
|
|
536
|
+
console.log(`⏭️ Skipping draft: ${basePath}`);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
511
540
|
// Get translated slugs from page meta (if available)
|
|
512
541
|
const slugs = pageData.meta?.slugs;
|
|
513
542
|
|
|
@@ -607,7 +636,7 @@ async function buildStaticPages(): Promise<void> {
|
|
|
607
636
|
}
|
|
608
637
|
|
|
609
638
|
// Run build
|
|
610
|
-
buildStaticPages().catch((error) => {
|
|
639
|
+
await buildStaticPages().catch((error) => {
|
|
611
640
|
console.error("❌ Build failed:", error);
|
|
612
641
|
process.exit(1);
|
|
613
642
|
});
|
|
@@ -387,7 +387,7 @@ export class ComponentBuilder {
|
|
|
387
387
|
* Filter internal props from node props
|
|
388
388
|
*/
|
|
389
389
|
private filterInternalProps(nodeProps: Record<string, unknown>, tag: string | undefined): Record<string, unknown> {
|
|
390
|
-
const imageOnlyProps = ['src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority'
|
|
390
|
+
const imageOnlyProps = ['src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority'];
|
|
391
391
|
const internalProps = ['type', 'tag', 'component', 'props', 'children', 'html', 'style', ...imageOnlyProps];
|
|
392
392
|
const props: Record<string, unknown> = {};
|
|
393
393
|
|
|
@@ -339,6 +339,11 @@ export function processStructure(
|
|
|
339
339
|
if (result === undefined || result === null) {
|
|
340
340
|
return '';
|
|
341
341
|
}
|
|
342
|
+
// Return objects as-is (e.g., link objects { href, target })
|
|
343
|
+
// The caller (e.g., href handling) will process them appropriately
|
|
344
|
+
if (typeof result === 'object') {
|
|
345
|
+
return result as any;
|
|
346
|
+
}
|
|
342
347
|
return String(result);
|
|
343
348
|
}
|
|
344
349
|
|
|
@@ -640,7 +645,19 @@ export function processStructure(
|
|
|
640
645
|
// Regular href value - process as template
|
|
641
646
|
const processedValue = processStructure(value as ComponentNode | ComponentNode[] | string | number | null | undefined, context, viewportWidth, instanceChildren, preserveResponsiveStyles, depth + 1);
|
|
642
647
|
if (processedValue !== null && processedValue !== undefined) {
|
|
643
|
-
(
|
|
648
|
+
// Check if result is a link object (from link-type prop like {{link}})
|
|
649
|
+
if (typeof processedValue === 'object' && processedValue !== null && 'href' in processedValue) {
|
|
650
|
+
const linkObj = processedValue as { href: string; target?: string };
|
|
651
|
+
(processed as any).href = linkObj.href;
|
|
652
|
+
if (linkObj.target) {
|
|
653
|
+
(processed as any).attributes = {
|
|
654
|
+
...((processed as any).attributes || {}),
|
|
655
|
+
target: linkObj.target
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
(processed as any).href = processedValue;
|
|
660
|
+
}
|
|
644
661
|
}
|
|
645
662
|
}
|
|
646
663
|
} else if (key === 'attributes' && typeof value === 'object' && value !== null) {
|
|
@@ -11,9 +11,10 @@ import { jsonResponse } from './shared';
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Handle pages API endpoint - GET /api/pages
|
|
14
|
+
* Returns pages with draft status info
|
|
14
15
|
*/
|
|
15
16
|
export function handlePagesRoute(pageService: PageService): Response {
|
|
16
|
-
const pages = pageService.
|
|
17
|
+
const pages = pageService.getAllPagesWithInfo();
|
|
17
18
|
return jsonResponse({ pages });
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -185,6 +185,30 @@ export class PageService {
|
|
|
185
185
|
return this.pageCache.keys();
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Get all pages with their info (including draft status)
|
|
190
|
+
*
|
|
191
|
+
* Returns an array of page info objects including path and draft status.
|
|
192
|
+
*
|
|
193
|
+
* @returns Array of page info objects
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* const pages = pageService.getAllPagesWithInfo();
|
|
198
|
+
* // [{ path: "/", isDraft: false }, { path: "/about", isDraft: true }]
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
getAllPagesWithInfo(): Array<{ path: string; isDraft: boolean }> {
|
|
202
|
+
const paths = this.pageCache.keys();
|
|
203
|
+
return paths.map(path => {
|
|
204
|
+
const pageData = this.getPageData(path);
|
|
205
|
+
return {
|
|
206
|
+
path,
|
|
207
|
+
isDraft: pageData?.meta?.draft === true
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
188
212
|
/**
|
|
189
213
|
* Get line map for a page
|
|
190
214
|
*
|
|
@@ -21,7 +21,7 @@ export function escapeHtml(unsafe: string): string {
|
|
|
21
21
|
export function buildAttributes(props: Record<string, unknown>, exclude: string[] = []): string {
|
|
22
22
|
const attrs: string[] = [];
|
|
23
23
|
// Internal props that should never be rendered as HTML attributes
|
|
24
|
-
const internalProps = ['tag', 'component', 'props', 'children', 'src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority'
|
|
24
|
+
const internalProps = ['tag', 'component', 'props', 'children', 'src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority'];
|
|
25
25
|
const defaultExclude = [...internalProps, ...exclude];
|
|
26
26
|
|
|
27
27
|
// Regex to detect unresolved template strings like {{link.target}}
|
|
@@ -15,9 +15,21 @@ import { generateUtilityCSS, extractUtilityClassesFromHTML, generateAllInteracti
|
|
|
15
15
|
import { printMissingStyleWarnings } from '../validateStyleCoverage';
|
|
16
16
|
import { formHandlerScript, needsFormHandler } from '../../client/scripts/formHandler';
|
|
17
17
|
import { escapeHtml } from './attributeBuilder';
|
|
18
|
-
import { renderPageSSR } from './ssrRenderer';
|
|
18
|
+
import { renderPageSSR, type PreloadImage } from './ssrRenderer';
|
|
19
19
|
import type { CMSContext } from './cmsSSRProcessor';
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Generate image preload link tags for high-priority images
|
|
23
|
+
* Uses imagesrcset and imagesizes for responsive image preloading
|
|
24
|
+
*/
|
|
25
|
+
function generateImagePreloadTags(preloadImages: PreloadImage[]): string {
|
|
26
|
+
if (preloadImages.length === 0) return '';
|
|
27
|
+
|
|
28
|
+
return preloadImages
|
|
29
|
+
.map(img => `<link rel="preload" as="image" type="${img.type}" imagesrcset="${escapeHtml(img.srcset)}" imagesizes="${escapeHtml(img.sizes)}" fetchpriority="high">`)
|
|
30
|
+
.join('\n ');
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
/**
|
|
22
34
|
* Minify CSS code using regex-based minification
|
|
23
35
|
* Removes comments, unnecessary whitespace, and optimizes values
|
|
@@ -302,6 +314,9 @@ img {
|
|
|
302
314
|
? `<link rel="preload" href="${extScriptPath}" as="script">`
|
|
303
315
|
: '';
|
|
304
316
|
|
|
317
|
+
// Image preload tags for high-priority images (LCP optimization)
|
|
318
|
+
const imagePreloadTags = generateImagePreloadTags(rendered.preloadImages);
|
|
319
|
+
|
|
305
320
|
// In production, output minified CSS on single line; in dev, preserve formatting
|
|
306
321
|
const styleContent = useBundled
|
|
307
322
|
? finalCSS
|
|
@@ -312,7 +327,7 @@ img {
|
|
|
312
327
|
<head>
|
|
313
328
|
<meta charset="UTF-8">
|
|
314
329
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
315
|
-
${iconTags ? iconTags + '\n ' : ''}${scriptPreloadTag ? scriptPreloadTag + '\n ' : ''}${fontPreloadTags ? fontPreloadTags + '\n ' : ''}${rendered.meta}
|
|
330
|
+
${iconTags ? iconTags + '\n ' : ''}${scriptPreloadTag ? scriptPreloadTag + '\n ' : ''}${imagePreloadTags ? imagePreloadTags + '\n ' : ''}${fontPreloadTags ? fontPreloadTags + '\n ' : ''}${rendered.meta}
|
|
316
331
|
${configInlineScript}${cmsInlineScript}<style>${styleContent}</style>
|
|
317
332
|
</head>
|
|
318
333
|
<body>
|
|
@@ -35,7 +35,7 @@ export const RESPONSIVE_WIDTHS = [500, 800, 1080, 1600, 2400] as const;
|
|
|
35
35
|
/**
|
|
36
36
|
* Default sizes attribute for responsive images
|
|
37
37
|
*/
|
|
38
|
-
export const DEFAULT_SIZES = '
|
|
38
|
+
export const DEFAULT_SIZES = '100vw';
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Build image metadata map from manifest
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ComponentNode, ComponentDefinition, JSONPage, CMSListNode, CMSItem } from '../../shared/types';
|
|
7
7
|
import type { TemplateContext } from '../../shared/types/cms';
|
|
8
|
-
import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, buildTemplateContext, resolveItemsTemplate, type ValueResolver } from '../../shared/itemTemplateUtils';
|
|
8
|
+
import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, buildTemplateContext, resolveItemsTemplate, resolveTemplateRawValue, type ValueResolver } from '../../shared/itemTemplateUtils';
|
|
9
9
|
import { singularize } from '../../shared/types/cms';
|
|
10
10
|
import type { ResponsiveStyleObject, StyleObject } from '../../shared/types';
|
|
11
11
|
import type { BreakpointConfig } from '../../shared/breakpoints';
|
|
@@ -37,6 +37,18 @@ import { collectComponentJavaScript } from './jsCollector';
|
|
|
37
37
|
import { CMSContext, processCMSTemplate, processCMSPropsTemplate, createI18nResolver, RAW_HTML_PREFIX } from './cmsSSRProcessor';
|
|
38
38
|
import { ImageMetadataMap, DEFAULT_SIZES, buildImageMetadataMap } from './imageMetadata';
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Image preload info for generating <link rel="preload"> tags in head
|
|
42
|
+
*/
|
|
43
|
+
export interface PreloadImage {
|
|
44
|
+
/** Best available srcset (AVIF preferred, then WebP) */
|
|
45
|
+
srcset: string;
|
|
46
|
+
/** Image type (image/avif or image/webp) */
|
|
47
|
+
type: 'image/avif' | 'image/webp';
|
|
48
|
+
/** Sizes attribute */
|
|
49
|
+
sizes: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
// Re-export types for external consumers
|
|
41
53
|
export type { CMSContext } from './cmsSSRProcessor';
|
|
42
54
|
export type { PageMeta } from './metaTagGenerator';
|
|
@@ -65,6 +77,8 @@ interface SSRContext {
|
|
|
65
77
|
componentContext?: string;
|
|
66
78
|
/** Resolved component props for interactive style mapping resolution */
|
|
67
79
|
componentResolvedProps?: Record<string, unknown>;
|
|
80
|
+
/** Array to collect high-priority images for preloading */
|
|
81
|
+
preloadImages?: PreloadImage[];
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
/**
|
|
@@ -101,11 +115,13 @@ async function buildComponentHTML(
|
|
|
101
115
|
pagePath?: string,
|
|
102
116
|
cmsContext?: CMSContext,
|
|
103
117
|
cmsService?: CMSService
|
|
104
|
-
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles
|
|
118
|
+
): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[] }> {
|
|
105
119
|
// Create map to collect interactive styles during render
|
|
106
120
|
const interactiveStylesMap = new Map<string, InteractiveStyles>();
|
|
121
|
+
// Create array to collect high-priority images for preloading
|
|
122
|
+
const preloadImages: PreloadImage[] = [];
|
|
107
123
|
|
|
108
|
-
if (!node) return { html: '', interactiveStylesMap };
|
|
124
|
+
if (!node) return { html: '', interactiveStylesMap, preloadImages };
|
|
109
125
|
|
|
110
126
|
// Register components for this render
|
|
111
127
|
ssrComponentRegistry.merge(globalComponents);
|
|
@@ -133,11 +149,12 @@ async function buildComponentHTML(
|
|
|
133
149
|
cmsService,
|
|
134
150
|
elementPath: [0], // Initialize path tracking for interactive styles
|
|
135
151
|
interactiveStylesMap, // Collect interactive styles during render
|
|
152
|
+
preloadImages, // Collect high-priority images for preloading
|
|
136
153
|
};
|
|
137
154
|
|
|
138
155
|
const html = await renderNode(node, ctx);
|
|
139
156
|
|
|
140
|
-
return { html, interactiveStylesMap };
|
|
157
|
+
return { html, interactiveStylesMap, preloadImages };
|
|
141
158
|
}
|
|
142
159
|
|
|
143
160
|
/**
|
|
@@ -460,11 +477,22 @@ async function renderNode(
|
|
|
460
477
|
// Handle object link nodes (render as <a> tag in SSR)
|
|
461
478
|
if (isObjectLinkNode(node)) {
|
|
462
479
|
let href: string = typeof node.href === 'string' ? node.href : '#';
|
|
480
|
+
let targetFromLink: string | undefined;
|
|
463
481
|
|
|
464
482
|
// Process item templates in href (for CMSList context)
|
|
465
483
|
const templateCtx = getTemplateContext(ctx);
|
|
466
484
|
if (templateCtx && hasItemTemplates(href)) {
|
|
467
|
-
|
|
485
|
+
// Get raw value first to check if it's a link object (like { href: "/path", target: "_blank" })
|
|
486
|
+
const rawValue = resolveTemplateRawValue(href, templateCtx);
|
|
487
|
+
if (rawValue && typeof rawValue === 'object' && 'href' in rawValue) {
|
|
488
|
+
// Link object - extract href and target
|
|
489
|
+
const linkObj = rawValue as { href: string; target?: string };
|
|
490
|
+
href = String(linkObj.href);
|
|
491
|
+
targetFromLink = linkObj.target;
|
|
492
|
+
} else {
|
|
493
|
+
// Regular value - use string processing
|
|
494
|
+
href = processItemTemplate(href, templateCtx, getI18nResolver(ctx));
|
|
495
|
+
}
|
|
468
496
|
}
|
|
469
497
|
|
|
470
498
|
// Localize internal page links to current locale
|
|
@@ -542,6 +570,11 @@ async function renderNode(
|
|
|
542
570
|
delete nodeAttributes.className;
|
|
543
571
|
delete nodeAttributes.class;
|
|
544
572
|
|
|
573
|
+
// Add target from link object if present (and not already set in attributes)
|
|
574
|
+
if (targetFromLink && !nodeAttributes.target) {
|
|
575
|
+
nodeAttributes.target = targetFromLink;
|
|
576
|
+
}
|
|
577
|
+
|
|
545
578
|
const attrs = buildAttributes(nodeAttributes, ['href']);
|
|
546
579
|
const classAttr = ` class="${escapeHtml(classNames.filter(Boolean).join(' '))}"`;
|
|
547
580
|
|
|
@@ -898,7 +931,6 @@ function renderImageElement(
|
|
|
898
931
|
const loading = imgProps.loading as string | undefined;
|
|
899
932
|
const sizes = imgProps.sizes as string | undefined;
|
|
900
933
|
const fetchpriority = imgProps.fetchpriority as string | undefined;
|
|
901
|
-
const priority = imgProps.priority as boolean | undefined;
|
|
902
934
|
let width = imgProps.width as string | number | undefined;
|
|
903
935
|
let height = imgProps.height as string | number | undefined;
|
|
904
936
|
|
|
@@ -914,18 +946,31 @@ function renderImageElement(
|
|
|
914
946
|
// Determine sizes attribute (prop > default)
|
|
915
947
|
const sizesAttr = sizes || DEFAULT_SIZES;
|
|
916
948
|
|
|
949
|
+
// Collect high-priority images for preloading in head
|
|
950
|
+
if (fetchpriority === 'high' && metadata && ctx.preloadImages) {
|
|
951
|
+
// Prefer AVIF, fallback to WebP
|
|
952
|
+
if (metadata.avifSrcset) {
|
|
953
|
+
ctx.preloadImages.push({
|
|
954
|
+
srcset: metadata.avifSrcset,
|
|
955
|
+
type: 'image/avif',
|
|
956
|
+
sizes: sizesAttr,
|
|
957
|
+
});
|
|
958
|
+
} else if (metadata.srcset) {
|
|
959
|
+
ctx.preloadImages.push({
|
|
960
|
+
srcset: metadata.srcset,
|
|
961
|
+
type: 'image/webp',
|
|
962
|
+
sizes: sizesAttr,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
917
967
|
// Build img attributes
|
|
918
968
|
let imgAttrs = '';
|
|
919
969
|
if (src) imgAttrs += ` src="${escapeHtml(String(src))}"`;
|
|
920
970
|
if (alt !== undefined) imgAttrs += ` alt="${escapeHtml(String(alt))}"`;
|
|
921
|
-
|
|
922
|
-
if (
|
|
923
|
-
|
|
924
|
-
imgAttrs += ' loading="eager"';
|
|
925
|
-
} else {
|
|
926
|
-
if (fetchpriority) imgAttrs += ` fetchpriority="${escapeHtml(String(fetchpriority))}"`;
|
|
927
|
-
if (loading) imgAttrs += ` loading="${escapeHtml(String(loading))}"`;
|
|
928
|
-
}
|
|
971
|
+
if (fetchpriority) imgAttrs += ` fetchpriority="${escapeHtml(String(fetchpriority))}"`;
|
|
972
|
+
if (loading) imgAttrs += ` loading="${escapeHtml(String(loading))}"`;
|
|
973
|
+
|
|
929
974
|
if (width !== undefined) imgAttrs += ` width="${escapeHtml(String(width))}"`;
|
|
930
975
|
if (height !== undefined) imgAttrs += ` height="${escapeHtml(String(height))}"`;
|
|
931
976
|
|
|
@@ -1164,7 +1209,7 @@ export async function renderPageSSR(
|
|
|
1164
1209
|
slugMappings?: SlugMap[],
|
|
1165
1210
|
cmsContext?: CMSContext,
|
|
1166
1211
|
cmsService?: CMSService
|
|
1167
|
-
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles
|
|
1212
|
+
): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[] }> {
|
|
1168
1213
|
// Extract page content
|
|
1169
1214
|
const rootNode = pageData?.root || undefined;
|
|
1170
1215
|
if (!rootNode) {
|
|
@@ -1199,10 +1244,10 @@ export async function renderPageSSR(
|
|
|
1199
1244
|
const pageComponents = pageData?.components || {};
|
|
1200
1245
|
|
|
1201
1246
|
// Render the component tree to HTML with i18n and CMS support
|
|
1202
|
-
// Also collect interactive styles during render
|
|
1203
|
-
const { html: contentHTML, interactiveStylesMap } = rootNode
|
|
1247
|
+
// Also collect interactive styles and preload images during render
|
|
1248
|
+
const { html: contentHTML, interactiveStylesMap, preloadImages } = rootNode
|
|
1204
1249
|
? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService)
|
|
1205
|
-
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>() };
|
|
1250
|
+
: { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [] };
|
|
1206
1251
|
|
|
1207
1252
|
// Collect JavaScript and CSS from all components
|
|
1208
1253
|
const javascript = collectComponentJavaScript(globalComponents, pageComponents);
|
|
@@ -1231,5 +1276,6 @@ export async function renderPageSSR(
|
|
|
1231
1276
|
componentCSS,
|
|
1232
1277
|
locale: effectiveLocale,
|
|
1233
1278
|
interactiveStylesMap,
|
|
1279
|
+
preloadImages,
|
|
1234
1280
|
};
|
|
1235
1281
|
}
|
|
@@ -986,45 +986,6 @@ describe("SSR Renderer - Security (XSS Prevention)", () => {
|
|
|
986
986
|
expect(result.html).toContain('fetchpriority="high"');
|
|
987
987
|
});
|
|
988
988
|
|
|
989
|
-
test("should render priority prop as fetchpriority=high and loading=eager", async () => {
|
|
990
|
-
const pageData: JSONPage = {
|
|
991
|
-
root: {
|
|
992
|
-
type: "node",
|
|
993
|
-
tag: "img",
|
|
994
|
-
attributes: {
|
|
995
|
-
src: "/test.jpg",
|
|
996
|
-
alt: "test",
|
|
997
|
-
priority: true
|
|
998
|
-
}
|
|
999
|
-
} as any
|
|
1000
|
-
};
|
|
1001
|
-
|
|
1002
|
-
const result = await renderPageSSR(pageData);
|
|
1003
|
-
|
|
1004
|
-
expect(result.html).toContain('fetchpriority="high"');
|
|
1005
|
-
expect(result.html).toContain('loading="eager"');
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
test("priority should override explicit loading=lazy", async () => {
|
|
1009
|
-
const pageData: JSONPage = {
|
|
1010
|
-
root: {
|
|
1011
|
-
type: "node",
|
|
1012
|
-
tag: "img",
|
|
1013
|
-
attributes: {
|
|
1014
|
-
src: "/test.jpg",
|
|
1015
|
-
alt: "test",
|
|
1016
|
-
priority: true,
|
|
1017
|
-
loading: "lazy"
|
|
1018
|
-
}
|
|
1019
|
-
} as any
|
|
1020
|
-
};
|
|
1021
|
-
|
|
1022
|
-
const result = await renderPageSSR(pageData);
|
|
1023
|
-
|
|
1024
|
-
// priority=true should set loading="eager", ignoring the lazy value
|
|
1025
|
-
expect(result.html).toContain('loading="eager"');
|
|
1026
|
-
expect(result.html).not.toContain('loading="lazy"');
|
|
1027
|
-
});
|
|
1028
989
|
});
|
|
1029
990
|
|
|
1030
991
|
describe("Embed content sanitization", () => {
|
|
@@ -24,6 +24,7 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
24
24
|
right: { type: 'string' },
|
|
25
25
|
bottom: { type: 'string' },
|
|
26
26
|
left: { type: 'string' },
|
|
27
|
+
inset: { type: 'string' },
|
|
27
28
|
zIndex: { type: 'number' },
|
|
28
29
|
|
|
29
30
|
// Dimensions
|
|
@@ -33,6 +34,7 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
33
34
|
maxWidth: { type: 'string' },
|
|
34
35
|
minHeight: { type: 'string' },
|
|
35
36
|
maxHeight: { type: 'string' },
|
|
37
|
+
aspectRatio: { type: 'string' },
|
|
36
38
|
|
|
37
39
|
// Spacing
|
|
38
40
|
margin: { type: 'string' },
|
|
@@ -109,6 +111,10 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
109
111
|
values: ['flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'stretch'],
|
|
110
112
|
type: 'select',
|
|
111
113
|
},
|
|
114
|
+
alignSelf: {
|
|
115
|
+
values: ['auto', 'flex-start', 'flex-end', 'center', 'stretch', 'baseline'],
|
|
116
|
+
type: 'select',
|
|
117
|
+
},
|
|
112
118
|
flexGrow: { type: 'number' },
|
|
113
119
|
flexShrink: { type: 'number' },
|
|
114
120
|
flexBasis: { type: 'string' },
|
|
@@ -118,13 +124,28 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
118
124
|
grid: { type: 'string' },
|
|
119
125
|
gridTemplateColumns: { type: 'string' },
|
|
120
126
|
gridTemplateRows: { type: 'string' },
|
|
127
|
+
gridTemplateAreas: { type: 'string' },
|
|
121
128
|
gridGap: { type: 'string' },
|
|
122
129
|
gridColumn: { type: 'string' },
|
|
123
130
|
gridRow: { type: 'string' },
|
|
131
|
+
gridArea: { type: 'string' },
|
|
124
132
|
gridAutoFlow: {
|
|
125
133
|
values: ['row', 'column', 'row dense', 'column dense'],
|
|
126
134
|
type: 'select',
|
|
127
135
|
},
|
|
136
|
+
gridAutoColumns: { type: 'string' },
|
|
137
|
+
gridAutoRows: { type: 'string' },
|
|
138
|
+
justifyItems: {
|
|
139
|
+
values: ['start', 'end', 'center', 'stretch'],
|
|
140
|
+
type: 'select',
|
|
141
|
+
},
|
|
142
|
+
justifySelf: {
|
|
143
|
+
values: ['auto', 'start', 'end', 'center', 'stretch'],
|
|
144
|
+
type: 'select',
|
|
145
|
+
},
|
|
146
|
+
placeContent: { type: 'string' },
|
|
147
|
+
placeItems: { type: 'string' },
|
|
148
|
+
placeSelf: { type: 'string' },
|
|
128
149
|
|
|
129
150
|
// Text & Font
|
|
130
151
|
fontSize: { type: 'string' },
|
|
@@ -152,14 +173,38 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
152
173
|
},
|
|
153
174
|
letterSpacing: { type: 'string' },
|
|
154
175
|
wordSpacing: { type: 'string' },
|
|
176
|
+
wordBreak: {
|
|
177
|
+
values: ['normal', 'break-all', 'keep-all', 'break-word'],
|
|
178
|
+
type: 'select',
|
|
179
|
+
},
|
|
180
|
+
overflowWrap: {
|
|
181
|
+
values: ['normal', 'break-word', 'anywhere'],
|
|
182
|
+
type: 'select',
|
|
183
|
+
},
|
|
184
|
+
textIndent: { type: 'string' },
|
|
185
|
+
verticalAlign: {
|
|
186
|
+
values: ['baseline', 'top', 'middle', 'bottom', 'text-top', 'text-bottom', 'sub', 'super'],
|
|
187
|
+
type: 'select',
|
|
188
|
+
},
|
|
155
189
|
|
|
156
190
|
// Box Shadow & Effects
|
|
157
191
|
boxShadow: { type: 'string' },
|
|
158
192
|
textShadow: { type: 'string' },
|
|
159
193
|
filter: { type: 'string' },
|
|
194
|
+
backdropFilter: { type: 'string' },
|
|
160
195
|
transform: { type: 'string' },
|
|
196
|
+
transformOrigin: { type: 'string' },
|
|
161
197
|
transition: { type: 'string' },
|
|
162
198
|
animation: { type: 'string' },
|
|
199
|
+
backfaceVisibility: {
|
|
200
|
+
values: ['visible', 'hidden'],
|
|
201
|
+
type: 'select',
|
|
202
|
+
},
|
|
203
|
+
mixBlendMode: {
|
|
204
|
+
values: ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', 'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'],
|
|
205
|
+
type: 'select',
|
|
206
|
+
},
|
|
207
|
+
clipPath: { type: 'string' },
|
|
163
208
|
|
|
164
209
|
// Overflow & Content
|
|
165
210
|
overflow: {
|
|
@@ -202,6 +247,27 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
202
247
|
type: 'select',
|
|
203
248
|
},
|
|
204
249
|
|
|
250
|
+
// Outline
|
|
251
|
+
outline: { type: 'string' },
|
|
252
|
+
outlineWidth: { type: 'string' },
|
|
253
|
+
outlineStyle: {
|
|
254
|
+
values: ['none', 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'inset', 'outset'],
|
|
255
|
+
type: 'select',
|
|
256
|
+
},
|
|
257
|
+
outlineColor: { type: 'string' },
|
|
258
|
+
outlineOffset: { type: 'string' },
|
|
259
|
+
|
|
260
|
+
// Lists
|
|
261
|
+
listStyle: { type: 'string' },
|
|
262
|
+
listStyleType: {
|
|
263
|
+
values: ['none', 'disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman', 'lower-alpha', 'upper-alpha'],
|
|
264
|
+
type: 'select',
|
|
265
|
+
},
|
|
266
|
+
listStylePosition: {
|
|
267
|
+
values: ['inside', 'outside'],
|
|
268
|
+
type: 'select',
|
|
269
|
+
},
|
|
270
|
+
|
|
205
271
|
// Miscellaneous
|
|
206
272
|
float: {
|
|
207
273
|
values: ['left', 'right', 'none'],
|
|
@@ -223,6 +289,15 @@ export const CSS_PROPERTIES_DEFINITION: Record<string, CSSPropertyDefinition> =
|
|
|
223
289
|
values: ['top', 'bottom', 'left', 'right', 'center'],
|
|
224
290
|
type: 'select',
|
|
225
291
|
},
|
|
292
|
+
resize: {
|
|
293
|
+
values: ['none', 'both', 'horizontal', 'vertical'],
|
|
294
|
+
type: 'select',
|
|
295
|
+
},
|
|
296
|
+
scrollBehavior: {
|
|
297
|
+
values: ['auto', 'smooth'],
|
|
298
|
+
type: 'select',
|
|
299
|
+
},
|
|
300
|
+
accentColor: { type: 'string' },
|
|
226
301
|
};
|
|
227
302
|
|
|
228
303
|
/**
|
|
@@ -236,18 +311,20 @@ export const CSS_PROPERTIES = Object.keys(CSS_PROPERTIES_DEFINITION);
|
|
|
236
311
|
* Order determines display order in the UI
|
|
237
312
|
*/
|
|
238
313
|
export const CSS_PROPERTY_GROUPS: Record<string, string[]> = {
|
|
239
|
-
'Layout': ['display', 'position', 'top', 'right', 'bottom', 'left', 'zIndex'],
|
|
240
|
-
'Dimensions': ['width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight'],
|
|
314
|
+
'Layout': ['display', 'position', 'top', 'right', 'bottom', 'left', 'inset', 'zIndex'],
|
|
315
|
+
'Dimensions': ['width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'aspectRatio'],
|
|
241
316
|
'Spacing': ['margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'gap', 'rowGap', 'columnGap'],
|
|
242
|
-
'Flexbox': ['flex', 'flexDirection', 'flexWrap', 'flexFlow', 'justifyContent', 'alignItems', 'alignContent', 'flexGrow', 'flexShrink', 'flexBasis', 'order'],
|
|
243
|
-
'Grid': ['grid', 'gridTemplateColumns', 'gridTemplateRows', 'gridGap', 'gridColumn', 'gridRow', 'gridAutoFlow'],
|
|
244
|
-
'Typography': ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'lineHeight', 'textAlign', 'textDecoration', 'textTransform', 'letterSpacing', 'wordSpacing', 'color'],
|
|
317
|
+
'Flexbox': ['flex', 'flexDirection', 'flexWrap', 'flexFlow', 'justifyContent', 'alignItems', 'alignContent', 'alignSelf', 'flexGrow', 'flexShrink', 'flexBasis', 'order'],
|
|
318
|
+
'Grid': ['grid', 'gridTemplateColumns', 'gridTemplateRows', 'gridTemplateAreas', 'gridGap', 'gridColumn', 'gridRow', 'gridArea', 'gridAutoFlow', 'gridAutoColumns', 'gridAutoRows', 'justifyItems', 'justifySelf', 'placeContent', 'placeItems', 'placeSelf'],
|
|
319
|
+
'Typography': ['fontSize', 'fontWeight', 'fontFamily', 'fontStyle', 'lineHeight', 'textAlign', 'textDecoration', 'textTransform', 'letterSpacing', 'wordSpacing', 'wordBreak', 'overflowWrap', 'textIndent', 'verticalAlign', 'color'],
|
|
245
320
|
'Background': ['background', 'backgroundColor', 'backgroundImage', 'backgroundSize', 'backgroundPosition', 'backgroundRepeat', 'opacity'],
|
|
246
321
|
'Borders': ['border', 'borderWidth', 'borderStyle', 'borderColor', 'borderTop', 'borderRight', 'borderBottom', 'borderLeft', 'borderRadius', 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomLeftRadius', 'borderBottomRightRadius'],
|
|
247
|
-
'
|
|
322
|
+
'Outline': ['outline', 'outlineWidth', 'outlineStyle', 'outlineColor', 'outlineOffset'],
|
|
323
|
+
'Effects': ['boxShadow', 'textShadow', 'filter', 'backdropFilter', 'transform', 'transformOrigin', 'transition', 'animation', 'backfaceVisibility', 'mixBlendMode', 'clipPath'],
|
|
248
324
|
'Overflow': ['overflow', 'overflowX', 'overflowY', 'whiteSpace', 'textOverflow', 'visibility', 'content'],
|
|
249
325
|
'Interaction': ['cursor', 'pointerEvents', 'userSelect'],
|
|
250
|
-
'
|
|
326
|
+
'Lists': ['listStyle', 'listStyleType', 'listStylePosition'],
|
|
327
|
+
'Other': ['float', 'clear', 'boxSizing', 'objectFit', 'objectPosition', 'resize', 'scrollBehavior', 'accentColor'],
|
|
251
328
|
};
|
|
252
329
|
|
|
253
330
|
/**
|
|
@@ -267,6 +267,30 @@ export function resolveItemsTemplate(
|
|
|
267
267
|
return String(value);
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Resolve a template expression to its raw value (without stringification).
|
|
272
|
+
* Returns undefined if template cannot be resolved.
|
|
273
|
+
* Useful for checking if a value is an object (like link-type fields).
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* resolveTemplateRawValue("{{article.link}}", { article: { link: { href: "/about", target: "_blank" } } })
|
|
277
|
+
* // Returns: { href: "/about", target: "_blank" }
|
|
278
|
+
*/
|
|
279
|
+
export function resolveTemplateRawValue(
|
|
280
|
+
template: string,
|
|
281
|
+
ctx: TemplateContext
|
|
282
|
+
): unknown {
|
|
283
|
+
// Match single template expression: {{varName.field}}
|
|
284
|
+
const match = template.match(/^\{\{(\w+)\.([^}]+)\}\}$/);
|
|
285
|
+
if (!match) return undefined;
|
|
286
|
+
|
|
287
|
+
const [, varName, fieldPath] = match;
|
|
288
|
+
const item = ctx[varName];
|
|
289
|
+
if (!item || typeof item !== 'object') return undefined;
|
|
290
|
+
|
|
291
|
+
return getNestedValue(item as Record<string, unknown>, fieldPath.trim());
|
|
292
|
+
}
|
|
293
|
+
|
|
270
294
|
/**
|
|
271
295
|
* Build a template context for a CMS item with the given variable name.
|
|
272
296
|
* Includes both named context and legacy context for backward compatibility.
|
package/lib/shared/types/api.ts
CHANGED
|
@@ -55,5 +55,7 @@ export interface PageMetaData {
|
|
|
55
55
|
source?: 'static' | 'cms';
|
|
56
56
|
/** CMS configuration with embedded schema (required when source: 'cms') */
|
|
57
57
|
cms?: PageCmsConfig;
|
|
58
|
+
/** Draft pages are excluded from static build, sitemap, and robots.txt */
|
|
59
|
+
draft?: boolean;
|
|
58
60
|
}
|
|
59
61
|
|
package/lib/shared/types/cms.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export type CMSFieldType =
|
|
8
8
|
| 'string' // Single-line text
|
|
9
9
|
| 'text' // Multi-line text
|
|
10
|
-
| 'rich-text' // Rich text with formatting (HTML)
|
|
10
|
+
| 'rich-text' // Rich text with formatting (stored as Tiptap JSON, converted to HTML for SSR)
|
|
11
11
|
| 'number' // Numeric value
|
|
12
12
|
| 'boolean' // True/false
|
|
13
13
|
| 'image' // Image URL/path
|
|
@@ -42,6 +42,7 @@ export const propertyMap: Record<string, string> = {
|
|
|
42
42
|
maxHeight: 'mh',
|
|
43
43
|
minWidth: 'miw',
|
|
44
44
|
minHeight: 'mih',
|
|
45
|
+
aspectRatio: 'ar',
|
|
45
46
|
|
|
46
47
|
// Colors & Background
|
|
47
48
|
backgroundColor: 'bgc',
|
|
@@ -69,14 +70,27 @@ export const propertyMap: Record<string, string> = {
|
|
|
69
70
|
textDecoration: 'td',
|
|
70
71
|
lineHeight: 'lh',
|
|
71
72
|
letterSpacing: 'ls',
|
|
73
|
+
wordBreak: 'wb',
|
|
74
|
+
overflowWrap: 'ow',
|
|
75
|
+
textIndent: 'ti',
|
|
76
|
+
verticalAlign: 'va',
|
|
77
|
+
|
|
78
|
+
// Lists
|
|
72
79
|
listStyle: 'lst',
|
|
80
|
+
listStyleType: 'lstt',
|
|
81
|
+
listStylePosition: 'lstp',
|
|
73
82
|
|
|
74
83
|
// Transform & Effects
|
|
75
84
|
opacity: 'o',
|
|
76
85
|
transform: 'tm',
|
|
77
|
-
|
|
86
|
+
transformOrigin: 'tmo',
|
|
87
|
+
boxShadow: 'bsh',
|
|
78
88
|
textShadow: 'ts',
|
|
79
89
|
filter: 'flt',
|
|
90
|
+
backdropFilter: 'bdf',
|
|
91
|
+
backfaceVisibility: 'bfv',
|
|
92
|
+
mixBlendMode: 'mbm',
|
|
93
|
+
clipPath: 'cp',
|
|
80
94
|
|
|
81
95
|
// Positioning
|
|
82
96
|
position: 'pos',
|
|
@@ -84,23 +98,39 @@ export const propertyMap: Record<string, string> = {
|
|
|
84
98
|
right: 'r',
|
|
85
99
|
bottom: 'bo',
|
|
86
100
|
left: 'l',
|
|
101
|
+
inset: 'ins',
|
|
87
102
|
zIndex: 'z',
|
|
88
103
|
|
|
89
104
|
// Grid Layout
|
|
90
105
|
gridTemplateColumns: 'gtc',
|
|
91
106
|
gridTemplateRows: 'gtr',
|
|
107
|
+
gridTemplateAreas: 'gta',
|
|
92
108
|
gridGap: 'gg',
|
|
93
109
|
gridAutoFlow: 'gaf',
|
|
94
110
|
gridColumn: 'gc',
|
|
95
111
|
gridRow: 'gr',
|
|
112
|
+
gridArea: 'ga',
|
|
96
113
|
gridAutoRows: 'gar',
|
|
97
114
|
gridAutoColumns: 'gac',
|
|
115
|
+
justifyItems: 'ji',
|
|
116
|
+
justifySelf: 'jse',
|
|
117
|
+
placeContent: 'plc',
|
|
118
|
+
placeItems: 'pli',
|
|
119
|
+
placeSelf: 'pls',
|
|
98
120
|
|
|
99
121
|
// Flexbox extras
|
|
100
122
|
flexGrow: 'fg',
|
|
101
|
-
flexShrink: '
|
|
123
|
+
flexShrink: 'fsh',
|
|
102
124
|
flexBasis: 'fb',
|
|
103
125
|
order: 'ord',
|
|
126
|
+
alignSelf: 'as',
|
|
127
|
+
|
|
128
|
+
// Outline
|
|
129
|
+
outline: 'ol',
|
|
130
|
+
outlineWidth: 'olw',
|
|
131
|
+
outlineStyle: 'ols',
|
|
132
|
+
outlineColor: 'olc',
|
|
133
|
+
outlineOffset: 'olo',
|
|
104
134
|
|
|
105
135
|
// Other
|
|
106
136
|
overflow: 'ov',
|
|
@@ -110,8 +140,11 @@ export const propertyMap: Record<string, string> = {
|
|
|
110
140
|
transition: 'tn',
|
|
111
141
|
objectFit: 'objf',
|
|
112
142
|
objectPosition: 'objp',
|
|
113
|
-
boxSizing: '
|
|
143
|
+
boxSizing: 'bsz',
|
|
114
144
|
visibility: 'vis',
|
|
145
|
+
resize: 'rsz',
|
|
146
|
+
scrollBehavior: 'scb',
|
|
147
|
+
accentColor: 'acc',
|
|
115
148
|
};
|
|
116
149
|
|
|
117
150
|
// Mapping of prefixes to CSS property names for dynamic rule generation
|
|
@@ -142,6 +175,7 @@ export const prefixToCSSProperty: Record<string, string> = {
|
|
|
142
175
|
mh: 'max-height',
|
|
143
176
|
miw: 'min-width',
|
|
144
177
|
mih: 'min-height',
|
|
178
|
+
ar: 'aspect-ratio',
|
|
145
179
|
|
|
146
180
|
// Colors & Background
|
|
147
181
|
bgc: 'background-color',
|
|
@@ -169,36 +203,57 @@ export const prefixToCSSProperty: Record<string, string> = {
|
|
|
169
203
|
td: 'text-decoration',
|
|
170
204
|
lh: 'line-height',
|
|
171
205
|
ls: 'letter-spacing',
|
|
206
|
+
wb: 'word-break',
|
|
207
|
+
ow: 'overflow-wrap',
|
|
208
|
+
ti: 'text-indent',
|
|
209
|
+
va: 'vertical-align',
|
|
210
|
+
|
|
211
|
+
// Lists
|
|
172
212
|
lst: 'list-style',
|
|
213
|
+
lstt: 'list-style-type',
|
|
214
|
+
lstp: 'list-style-position',
|
|
173
215
|
|
|
174
216
|
// Flexbox
|
|
175
217
|
fd: 'flex-direction',
|
|
176
218
|
jc: 'justify-content',
|
|
177
219
|
ai: 'align-items',
|
|
178
220
|
ac: 'align-content',
|
|
221
|
+
as: 'align-self',
|
|
179
222
|
flex: 'flex',
|
|
180
223
|
fw: 'flex-wrap',
|
|
181
224
|
fg: 'flex-grow',
|
|
182
|
-
|
|
225
|
+
fsh: 'flex-shrink',
|
|
183
226
|
fb: 'flex-basis',
|
|
184
227
|
ord: 'order',
|
|
185
228
|
|
|
186
229
|
// Grid
|
|
187
230
|
gtc: 'grid-template-columns',
|
|
188
231
|
gtr: 'grid-template-rows',
|
|
232
|
+
gta: 'grid-template-areas',
|
|
189
233
|
gg: 'grid-gap',
|
|
190
234
|
gaf: 'grid-auto-flow',
|
|
191
235
|
gc: 'grid-column',
|
|
192
236
|
gr: 'grid-row',
|
|
237
|
+
ga: 'grid-area',
|
|
193
238
|
gar: 'grid-auto-rows',
|
|
194
239
|
gac: 'grid-auto-columns',
|
|
240
|
+
ji: 'justify-items',
|
|
241
|
+
jse: 'justify-self',
|
|
242
|
+
plc: 'place-content',
|
|
243
|
+
pli: 'place-items',
|
|
244
|
+
pls: 'place-self',
|
|
195
245
|
|
|
196
246
|
// Effects
|
|
197
247
|
o: 'opacity',
|
|
198
248
|
tm: 'transform',
|
|
249
|
+
tmo: 'transform-origin',
|
|
199
250
|
bsh: 'box-shadow',
|
|
200
251
|
ts: 'text-shadow',
|
|
201
252
|
flt: 'filter',
|
|
253
|
+
bdf: 'backdrop-filter',
|
|
254
|
+
bfv: 'backface-visibility',
|
|
255
|
+
mbm: 'mix-blend-mode',
|
|
256
|
+
cp: 'clip-path',
|
|
202
257
|
|
|
203
258
|
// Positioning
|
|
204
259
|
pos: 'position',
|
|
@@ -206,8 +261,16 @@ export const prefixToCSSProperty: Record<string, string> = {
|
|
|
206
261
|
r: 'right',
|
|
207
262
|
bo: 'bottom',
|
|
208
263
|
l: 'left',
|
|
264
|
+
ins: 'inset',
|
|
209
265
|
z: 'z-index',
|
|
210
266
|
|
|
267
|
+
// Outline
|
|
268
|
+
ol: 'outline',
|
|
269
|
+
olw: 'outline-width',
|
|
270
|
+
ols: 'outline-style',
|
|
271
|
+
olc: 'outline-color',
|
|
272
|
+
olo: 'outline-offset',
|
|
273
|
+
|
|
211
274
|
// Other
|
|
212
275
|
ov: 'overflow',
|
|
213
276
|
ovx: 'overflow-x',
|
|
@@ -218,6 +281,9 @@ export const prefixToCSSProperty: Record<string, string> = {
|
|
|
218
281
|
objp: 'object-position',
|
|
219
282
|
bsz: 'box-sizing',
|
|
220
283
|
vis: 'visibility',
|
|
284
|
+
rsz: 'resize',
|
|
285
|
+
scb: 'scroll-behavior',
|
|
286
|
+
acc: 'accent-color',
|
|
221
287
|
};
|
|
222
288
|
|
|
223
289
|
// Special value mappings for specific properties
|
|
@@ -291,6 +291,7 @@ const PageMetaDataBaseSchema = z.object({
|
|
|
291
291
|
ogImage: z.string().optional(),
|
|
292
292
|
ogType: z.string().optional(),
|
|
293
293
|
slugs: z.record(z.string(), z.string()).optional(),
|
|
294
|
+
draft: z.boolean().optional(),
|
|
294
295
|
}).passthrough();
|
|
295
296
|
|
|
296
297
|
// Temporary export for backward compatibility - will be replaced with full schema below
|
|
@@ -469,4 +470,5 @@ export const PageMetaDataWithCMSSchema = z.object({
|
|
|
469
470
|
slugs: z.record(z.string(), z.string()).optional(),
|
|
470
471
|
source: z.enum(['static', 'cms']).optional(),
|
|
471
472
|
cms: PageCmsConfigSchema.optional(),
|
|
473
|
+
draft: z.boolean().optional(),
|
|
472
474
|
}).passthrough();
|
package/package.json
CHANGED
|
File without changes
|