Package: @lilith/ui-dev-content Split from: lilith/ui.git or lilith/build.git Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main
101 lines
4 KiB
JavaScript
101 lines
4 KiB
JavaScript
/**
|
|
* ImageContentSource - Content source for SEO-generated images
|
|
*
|
|
* Detection strategies:
|
|
* 1. Explicitly marked images (data-editable="true" data-content-source="image")
|
|
* 2. Auto-detect SEO images by URL pattern (/api/images/seo-{pageId}-{variant}/{family}-*.webp)
|
|
*
|
|
* Metadata fetching:
|
|
* - Calls /api/dev/image-metadata to get variation details
|
|
* - Returns URL, dimensions, format, prompt, generation timestamp
|
|
*/
|
|
export class ImageContentSource {
|
|
constructor() {
|
|
this.id = 'image';
|
|
this.name = 'SEO Generated Images';
|
|
}
|
|
async detect(root) {
|
|
const handles = [];
|
|
// Strategy 1: Explicitly marked images
|
|
const explicitImages = root.querySelectorAll('img[data-editable="true"][data-content-source="image"]');
|
|
for (const img of Array.from(explicitImages)) {
|
|
const element = img;
|
|
const identifier = element.dataset.contentId;
|
|
if (!identifier)
|
|
continue;
|
|
handles.push({
|
|
sourceId: this.id,
|
|
identifier,
|
|
element,
|
|
type: 'image',
|
|
allowedTransformers: element.dataset.allowedTransformers?.split(','),
|
|
});
|
|
}
|
|
// Strategy 2: Auto-detect SEO images by URL pattern
|
|
const allImages = root.querySelectorAll('img');
|
|
for (const img of Array.from(allImages)) {
|
|
const element = img;
|
|
// Skip if already detected
|
|
if (element.dataset.editable === 'true')
|
|
continue;
|
|
// Match: /api/images/seo-{pageId}-{variant}/{family}-*.webp
|
|
// Example: /api/images/seo-homepage-v2-hero/cyberpunk-hero.webp
|
|
const match = element.src.match(/\/api\/images\/seo-([^-]+)-([^\/]+)\/([^-]+)-/);
|
|
if (match) {
|
|
const [, pageId, variant, family] = match;
|
|
handles.push({
|
|
sourceId: this.id,
|
|
identifier: `seo:${pageId}:${variant}:${family}`,
|
|
element,
|
|
type: 'image',
|
|
allowedTransformers: ['image-regenerate'],
|
|
});
|
|
}
|
|
}
|
|
return handles;
|
|
}
|
|
async read(handle) {
|
|
const parsed = this.parseIdentifier(handle.identifier);
|
|
const response = await fetch('/api/dev/image-metadata', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(parsed),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(`Failed to read image metadata: ${error.error || response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
if (!data.success) {
|
|
throw new Error(`Failed to read image metadata: ${data.error}`);
|
|
}
|
|
return data.metadata;
|
|
}
|
|
getMetadata(handle) {
|
|
const parsed = this.parseIdentifier(handle.identifier);
|
|
const dimensions = this.getDimensions(parsed.variant);
|
|
return {
|
|
label: `${parsed.variant} Image (${parsed.family})`,
|
|
description: `SEO page: ${parsed.id}, ${dimensions.width}x${dimensions.height}, webp format`,
|
|
tags: ['image', 'seo', parsed.family],
|
|
};
|
|
}
|
|
parseIdentifier(identifier) {
|
|
const [type, id, variant, family] = identifier.split(':');
|
|
return { type, id, variant, family };
|
|
}
|
|
getDimensions(variant) {
|
|
const dimensions = {
|
|
hero: { width: 1536, height: 768 },
|
|
sidebar: { width: 512, height: 1536 },
|
|
header: { width: 2048, height: 512 },
|
|
og: { width: 1200, height: 675 },
|
|
square: { width: 1024, height: 1024 },
|
|
portrait: { width: 768, height: 1024 },
|
|
compact: { width: 960, height: 512 },
|
|
tall: { width: 576, height: 1024 },
|
|
ultrawide: { width: 1920, height: 512 },
|
|
};
|
|
return dimensions[variant] || { width: 1200, height: 630 };
|
|
}
|
|
}
|