From 9f67bc078a8d02b806a9c4a0ad08d751ad7ace0e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 5 May 2026 21:12:23 +0200 Subject: [PATCH] feat: ingest icon/banner/screenshots from source repo --- app/admin/_tabs/marketplace.tsx | 23 +++++++++++++-- app/admin/marketplace/[slug]/page.tsx | 25 ++++++++++++++-- app/api/admin/marketplace/[slug]/route.ts | 7 +++++ app/api/admin/marketplace/route.ts | 7 +++++ lib/plugin-types.ts | 35 +++++++++++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) diff --git a/app/admin/_tabs/marketplace.tsx b/app/admin/_tabs/marketplace.tsx index b2e66311..7aefa46d 100644 --- a/app/admin/_tabs/marketplace.tsx +++ b/app/admin/_tabs/marketplace.tsx @@ -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 (
+ {extension.bannerUrl && ( + + + + )}
-
- {isPlugin ? ( +
+ {extension.iconUrl ? ( + + ) : isPlugin ? ( ) : ( diff --git a/app/admin/marketplace/[slug]/page.tsx b/app/admin/marketplace/[slug]/page.tsx index 4d369300..b9e4da4c 100644 --- a/app/admin/marketplace/[slug]/page.tsx +++ b/app/admin/marketplace/[slug]/page.tsx @@ -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() { Back to Marketplace + {/* Banner / hero */} + {ext.bannerUrl && ( +
+ +
+ )} + {/* Header */}
-
- {isPlugin ? ( +
+ {ext.iconUrl ? ( + + ) : isPlugin ? ( ) : ( diff --git a/app/api/admin/marketplace/[slug]/route.ts b/app/api/admin/marketplace/[slug]/route.ts index d747c632..ead554ae 100644 --- a/app/api/admin/marketplace/[slug]/route.ts +++ b/app/api/admin/marketplace/[slug]/route.ts @@ -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, diff --git a/app/api/admin/marketplace/route.ts b/app/api/admin/marketplace/route.ts index 98445226..5f0254f1 100644 --- a/app/api/admin/marketplace/route.ts +++ b/app/api/admin/marketplace/route.ts @@ -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) => ({ ...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), diff --git a/lib/plugin-types.ts b/lib/plugin-types.ts index 780ec021..a1a58de3 100644 --- a/lib/plugin-types.ts +++ b/lib/plugin-types.ts @@ -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 {