This commit is contained in:
jarek committed 2026-06-27 15:15:54 +02:00
1 parent ce12d7ae9f
commit 685132ff89
63 files changed
+1324 -281

No files matched your search

+55 -2
View File
@@ -20,6 +20,47 @@ export interface TemplateItem {
stars?: number;
pulls?: number;
network?: string;
/** Free-form maintainer / install notes from the upstream catalog. */
note?: string;
/** Project / homepage URL the user can click to learn more or report issues (#1211). */
projectUrl?: string;
}
/**
* Best-effort first-URL extraction from a template's description or note.
* Order matters: markdown link first, then HTML href, then a bare URL.
*/
function extractFirstUrl(text: string | undefined | null): string | null {
if (!text) return null;
const md = text.match(/\[[^\]]+\]\((https?:\/\/[^)]+)\)/);
if (md) return md[1];
const html = text.match(/<a\s+[^>]*href=["']([^"']+)["']/i);
if (html) return html[1];
const plain = text.match(/(https?:\/\/[^\s<>"')]+)/);
if (plain) return plain[1];
return null;
}
/**
* Resolve the "Project" URL surfaced on the card.
* Priority:
* 1. URL embedded in `description` — LSIO-style templates have a markdown
* link to the actual project repo here, which is what users want most.
* 2. URL embedded in `note` — Portainer v3 catalogs often put a homepage
* link in the install notes.
* 3. `maintainer` field — Lissy93's templates carry a per-template
* maintainer URL; not always the project itself but usually close.
* 4. `repository.url` — only for stack-type templates, points to the repo
* hosting the compose file (closest thing we have).
*/
function resolveProjectUrl(entry: any, description: string, note: string): string | undefined {
return (
extractFirstUrl(description) ||
extractFirstUrl(note) ||
(typeof entry.maintainer === 'string' ? entry.maintainer.trim() : null) ||
(entry.repository && typeof entry.repository.url === 'string' ? entry.repository.url : null) ||
undefined
);
}
// Server-side cache: url → { data, fetchedAt }
@@ -43,14 +84,18 @@ function normalizePortainerTemplate(entry: any, sourceName: string): TemplateIte
if (entry.type === 2) return null;
const title = entry.title || entry.name || '';
const description = entry.description || '';
const note = typeof entry.note === 'string' ? entry.note : '';
const template: TemplateItem = {
id: hashId(sourceName, title),
type: entry.type === 3 ? 'stack' : 'container',
title,
description: entry.description || '',
description,
logo: entry.logo || '',
categories: Array.isArray(entry.categories) ? entry.categories : [],
source: sourceName,
note: note || undefined,
projectUrl: resolveProjectUrl(entry, description, note)
};
if (entry.type === 3 && entry.repository) {
@@ -77,11 +122,18 @@ function normalizePortainerTemplate(entry: any, sourceName: string): TemplateIte
function normalizeLinuxServerTemplate(entry: any): TemplateItem | null {
if (!entry.name || entry.deprecated) return null;
const description = entry.description || '';
// LSIO fleet entries sometimes carry a project_url; prefer it over regex sniffing.
const projectUrl =
(typeof entry.project_url === 'string' && entry.project_url) ||
extractFirstUrl(description) ||
`https://github.com/linuxserver/docker-${entry.name}`;
return {
id: hashId('LinuxServer.io', entry.name),
type: 'container',
title: entry.name,
description: entry.description || '',
description,
logo: entry.project_logo || '',
categories: entry.category ? entry.category.split(',').map((c: string) => c.trim()).filter(Boolean) : [],
source: 'LinuxServer.io',
@@ -94,6 +146,7 @@ function normalizeLinuxServerTemplate(entry: any): TemplateItem | null {
restartPolicy: 'unless-stopped',
stars: entry.stars,
pulls: entry.monthly_pulls,
projectUrl
};
}