ui-dev-content/dist/sources/ImageContentSource.js
autocommit 8b284e01b9 chore: initial package split from monorepo
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
2026-04-20 01:11:45 -07:00

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 };
}
}