feat: ingest icon/banner/screenshots from source repo

This commit is contained in:
Linus Rath
2026-05-05 21:12:23 +02:00
parent da411af6d3
commit 9f67bc078a
5 changed files with 93 additions and 4 deletions
+21 -2
View File
@@ -18,6 +18,8 @@ interface Extension {
minAppVersion: string | null;
latestVersion: string | null;
installed: boolean;
iconUrl: string | null;
bannerUrl: string | null;
author: {
displayName: string;
githubLogin: string;
@@ -259,10 +261,27 @@ function ExtensionCard({
return (
<div className="group relative border border-border rounded-lg overflow-hidden hover:border-ring/30 transition-colors">
{extension.bannerUrl && (
<Link href={previewHref} className="block focus:outline-none">
<img
src={extension.bannerUrl}
alt=""
className="block h-24 w-full object-cover border-b border-border"
loading="lazy"
/>
</Link>
)}
<Link href={previewHref} className="block p-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 rounded-lg">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-md bg-muted flex items-center justify-center shrink-0">
{isPlugin ? (
<div className="w-10 h-10 rounded-md bg-muted flex items-center justify-center shrink-0 overflow-hidden">
{extension.iconUrl ? (
<img
src={extension.iconUrl}
alt=""
className="w-10 h-10 object-cover"
loading="lazy"
/>
) : isPlugin ? (
<Puzzle className="w-5 h-5 text-muted-foreground" />
) : (
<SwatchBook className="w-5 h-5 text-muted-foreground" />
+23 -2
View File
@@ -37,6 +37,8 @@ interface PreviewData {
githubRepo: string | null;
license: string | null;
minAppVersion: string | null;
iconUrl: string | null;
bannerUrl: string | null;
author: {
displayName: string;
githubLogin: string;
@@ -209,11 +211,30 @@ export default function MarketplacePreviewPage() {
<ArrowLeft className="w-4 h-4" /> Back to Marketplace
</Link>
{/* Banner / hero */}
{ext.bannerUrl && (
<div className="mb-6 overflow-hidden rounded-lg border border-border bg-muted">
<img
src={ext.bannerUrl}
alt=""
className="block w-full max-h-64 object-cover"
loading="lazy"
/>
</div>
)}
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
<div className="flex items-start gap-4 flex-1 min-w-0">
<div className="w-14 h-14 rounded-lg bg-muted flex items-center justify-center shrink-0">
{isPlugin ? (
<div className="w-14 h-14 rounded-lg bg-muted flex items-center justify-center shrink-0 overflow-hidden">
{ext.iconUrl ? (
<img
src={ext.iconUrl}
alt=""
className="w-14 h-14 object-cover"
loading="lazy"
/>
) : isPlugin ? (
<Puzzle className="w-7 h-7 text-muted-foreground" />
) : (
<SwatchBook className="w-7 h-7 text-muted-foreground" />
@@ -168,6 +168,11 @@ export async function GET(
}))
: [];
const fileUrl = (path: unknown): string | null =>
typeof path === 'string' && path
? new URL(`/api/v1/files/${path}`, DIRECTORY_URL).toString()
: null;
return NextResponse.json(
{
extension: {
@@ -184,6 +189,8 @@ export async function GET(
githubRepo: extension.githubRepo ?? null,
license: extension.license ?? null,
minAppVersion: extension.minAppVersion ?? null,
iconUrl: fileUrl(extension.iconPath),
bannerUrl: fileUrl(extension.bannerPath),
author: extension.author ?? null,
latestVersion,
versions,
+7
View File
@@ -60,9 +60,16 @@ export async function GET(request: NextRequest) {
const installedPlugins = new Set(pluginRegistry.plugins.map(p => p.id));
const installedThemes = new Set(themeRegistry.themes.map(t => t.id));
const fileUrl = (path: unknown): string | null =>
typeof path === 'string' && path
? new URL(`/api/v1/files/${path}`, DIRECTORY_URL).toString()
: null;
if (data.data) {
data.data = data.data.map((ext: Record<string, unknown>) => ({
...ext,
iconUrl: fileUrl(ext.iconPath),
bannerUrl: fileUrl(ext.bannerPath),
installed: ext.type === 'theme'
? installedThemes.has(ext.slug as string)
: installedPlugins.has(ext.slug as string),
+35
View File
@@ -53,7 +53,23 @@ export interface ThemeManifest {
author: string;
description: string;
type: 'theme';
/** @deprecated kept as alias for `banner` so existing themes still work. */
preview?: string;
/**
* Path inside the source repo (relative to manifest.json) to a square
* brand icon shown in marketplace cards and the host's theme picker.
*/
icon?: string;
/**
* Path to a wide promo image shown as the hero on the theme detail
* page. PNG/JPG/WebP, ≤512 KB.
*/
banner?: string;
/**
* Up to 6 screenshot paths shown in the gallery on the detail page.
* Themes typically use this to show light + dark variants.
*/
screenshots?: string[];
variants: ThemeVariant[];
minAppVersion?: string;
@@ -100,6 +116,25 @@ export interface PluginManifest {
* Validated at install time and merged into the host CSP `frame-src`.
*/
frameOrigins?: string[];
// ─── Marketplace media (NOT shipped in the runtime zip) ──────
/**
* Path inside the source repo (relative to manifest.json) to a square
* brand icon. PNG/SVG/WebP, ≤256 KB, 128×128 or larger recommended.
* The extension directory ingests this from git and serves it on
* marketplace cards and the host's plugin admin UI.
*/
icon?: string;
/**
* Path to a wide promo image (16:9 recommended), shown as the hero on
* the extension detail page. PNG/JPG/WebP, ≤512 KB.
*/
banner?: string;
/**
* Up to 6 screenshot paths shown in the gallery on the detail page.
* Each ≤512 KB; total ≤2 MB. Order is preserved.
*/
screenshots?: string[];
}
export interface SettingFieldSchema {