Update dependencies in package.json and yarn.lock for improved functionality and performance. Added new API endpoints for 'navgroup' in cms-api.generated.ts, enhanced image URL handling in rusty-image.ts, and refined logo URL processing in Layout.astro. This update includes version upgrades for several packages, ensuring compatibility with the latest features and fixes.
Deploy to Firebase Hosting / deploy (push) Failing after 14s

This commit is contained in:
Peter Meier
2026-03-28 13:39:27 +01:00
parent c688519e7e
commit b40579ee0b
5 changed files with 948 additions and 650 deletions
+3 -3
View File
@@ -17,14 +17,14 @@
"firebase-deploy:nocache": "yarn build && firebase deploy --only hosting" "firebase-deploy:nocache": "yarn build && firebase deploy --only hosting"
}, },
"dependencies": { "dependencies": {
"@astrojs/sitemap": "^3.7.0", "@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "^7.2.5", "@astrojs/svelte": "^8.0.4",
"@fontsource-variable/lora": "^5.2.8", "@fontsource-variable/lora": "^5.2.8",
"@fontsource-variable/rubik": "^5.2.8", "@fontsource-variable/rubik": "^5.2.8",
"@fontsource/inter": "^5.2.8", "@fontsource/inter": "^5.2.8",
"@iconify/svelte": "^5.2.1", "@iconify/svelte": "^5.2.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"astro": "^5.17.1", "astro": "^6.1.1",
"json5": "^2.2.3", "json5": "^2.2.3",
"marked": "^17.0.2", "marked": "^17.0.2",
"svelte": "^5.51.2", "svelte": "^5.51.2",
+7 -3
View File
@@ -225,9 +225,12 @@ try {
})) ?? })) ??
(await getPageConfigBySlug("default", { locale: "de", resolve: "logo" })); (await getPageConfigBySlug("default", { locale: "de", resolve: "logo" }));
const rawLogo = pageConfig?.logo; const rawLogo = pageConfig?.logo;
/** Logo-URL: String (z. B. `/api/assets/…`) oder aufgelöstes img-Objekt. */
const logoFileUrl = const logoFileUrl =
typeof rawLogo === "string" && rawLogo.startsWith("http") typeof rawLogo === "string" && rawLogo.trim()
? rawLogo ? rawLogo.trim().startsWith("//")
? `https:${rawLogo.trim()}`
: rawLogo.trim()
: (( : ((
rawLogo as rawLogo as
| { src?: string; _slug?: string; file?: { url?: string } } | { src?: string; _slug?: string; file?: { url?: string } }
@@ -237,7 +240,8 @@ try {
?._slug ?? ?._slug ??
(rawLogo as { file?: { url?: string } } | undefined)?.file?.url); (rawLogo as { file?: { url?: string } } | undefined)?.file?.url);
if (logoFileUrl) { if (logoFileUrl) {
const u = logoFileUrl.startsWith("//") const u =
typeof logoFileUrl === "string" && logoFileUrl.startsWith("//")
? `https:${logoFileUrl}` ? `https:${logoFileUrl}`
: logoFileUrl; : logoFileUrl;
logoUrl = await ensureTransformedImage(u, { logoUrl = await ensureTransformedImage(u, {
+361 -2
View File
@@ -375,6 +375,23 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/api/collections/navgroup": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get schema definition for 'navgroup' */
get: operations["getNavgroupSchema"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/collections/navigation": { "/api/collections/navigation": {
parameters: { parameters: {
query?: never; query?: never;
@@ -1639,6 +1656,63 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/api/content/navgroup": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** List 'navgroup' entries */
get: operations["listNavgroup"];
put?: never;
/** Create a new 'navgroup' entry */
post: operations["createNavgroup"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/content/navgroup/{slug}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get 'navgroup' entry by slug */
get: operations["getNavgroup"];
/** Update 'navgroup' entry */
put: operations["updateNavgroup"];
post?: never;
/** Delete 'navgroup' entry */
delete: operations["deleteNavgroup"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/content/navgroup/{slug}/referrers": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* List referrers of 'navgroup' entry
* @description Returns all entries that reference this entry (reverse index). Empty when not using referrer index (e.g. with RUSTYCMS_ENVIRONMENTS).
*/
get: operations["getNavgroupReferrers"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/content/navigation": { "/api/content/navigation": {
parameters: { parameters: {
query?: never; query?: never;
@@ -3025,7 +3099,7 @@ export interface components {
/** @description Entry identifier (filename) */ /** @description Entry identifier (filename) */
readonly _slug?: string; readonly _slug?: string;
/** @description Banner headline */ /** @description Banner headline */
headline: string; headline?: string;
/** @description Banner image (optional) */ /** @description Banner image (optional) */
image?: string | { image?: string | {
/** @description Alt text / description for accessibility */ /** @description Alt text / description for accessibility */
@@ -3050,7 +3124,7 @@ export interface components {
/** @description URL slug (used as filename) */ /** @description URL slug (used as filename) */
_slug: string; _slug: string;
/** @description Banner headline */ /** @description Banner headline */
headline: string; headline?: string;
/** @description Banner image (optional) */ /** @description Banner image (optional) */
image?: string | { image?: string | {
/** @description Alt text / description for accessibility */ /** @description Alt text / description for accessibility */
@@ -3745,6 +3819,26 @@ export interface components {
/** @description Internal component name */ /** @description Internal component name */
name: string; name: string;
}; };
navgroup: {
/** @description Entry identifier (filename) */
readonly _slug?: string;
/** @description Display label of the group */
label: string;
/** @description Sub-links shown in the dropdown */
links: string[];
/** @description Optional URL for the group label itself */
url?: string;
};
navgroup_input: {
/** @description URL slug (used as filename) */
_slug: string;
/** @description Display label of the group */
label: string;
/** @description Sub-links shown in the dropdown */
links: string[];
/** @description Optional URL for the group label itself */
url?: string;
};
navigation: { navigation: {
/** @description Entry identifier (filename) */ /** @description Entry identifier (filename) */
readonly _slug?: string; readonly _slug?: string;
@@ -3855,6 +3949,8 @@ export interface components {
seoDescription: string; seoDescription: string;
/** @description Default SEO title for website */ /** @description Default SEO title for website */
seoTitle: string; seoTitle: string;
/** @description Site name */
siteName: string;
/** @description Website URL */ /** @description Website URL */
website: string; website: string;
}; };
@@ -3880,6 +3976,8 @@ export interface components {
seoDescription: string; seoDescription: string;
/** @description Default SEO title for website */ /** @description Default SEO title for website */
seoTitle: string; seoTitle: string;
/** @description Site name */
siteName: string;
/** @description Website URL */ /** @description Website URL */
website: string; website: string;
}; };
@@ -5398,6 +5496,31 @@ export interface operations {
}; };
}; };
}; };
getNavgroupSchema: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Schema definition */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Collection not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getNavigationSchema: { getNavigationSchema: {
parameters: { parameters: {
query?: never; query?: never;
@@ -10068,6 +10191,240 @@ export interface operations {
}; };
}; };
}; };
listNavgroup: {
parameters: {
query?: {
/** @description Field name to sort by */
_sort?: string;
/** @description Sort order (default: asc) */
_order?: "asc" | "desc";
/** @description Page number (1-indexed) */
_page?: number;
/** @description Items per page */
_per_page?: number;
/** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */
_resolve?: string;
/** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */
_locale?: string;
/** @description Filter by label */
label?: string;
/** @description Filter by url */
url?: string;
/** @description Filter by links */
links?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Paginated list of entries */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
items?: components["schemas"]["navgroup"][];
page?: number;
per_page?: number;
total?: number;
total_pages?: number;
};
};
};
};
};
createNavgroup: {
parameters: {
query?: {
/** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */
_resolve?: string;
/** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */
_locale?: string;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["navgroup_input"];
};
};
responses: {
/** @description Entry created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["navgroup"];
};
};
/** @description Validation error */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Entry already exists */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getNavgroup: {
parameters: {
query?: {
/** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */
_resolve?: string;
/** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */
_locale?: string;
};
header?: never;
path: {
/** @description Entry identifier (filename without extension) */
slug: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description The entry */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["navgroup"];
};
};
/** @description Entry not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
updateNavgroup: {
parameters: {
query?: {
/** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */
_resolve?: string;
/** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */
_locale?: string;
};
header?: never;
path: {
/** @description Entry identifier (filename without extension) */
slug: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["navgroup"];
};
};
responses: {
/** @description Entry updated */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["navgroup"];
};
};
/** @description Validation error */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Entry not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
deleteNavgroup: {
parameters: {
query?: {
/** @description Resolve references: 'all' or comma-separated field names (e.g. row1Content,topFullwidthBanner). Resolved entries are embedded instead of { _type, _slug }. */
_resolve?: string;
/** @description Locale for content (e.g. de, en). Only used when RUSTYCMS_LOCALES is set. Default = first locale. References are resolved in the same locale. */
_locale?: string;
};
header?: never;
path: {
/** @description Entry identifier (filename without extension) */
slug: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Entry deleted */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Entry not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getNavgroupReferrers: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Entry slug */
slug: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description List of referrers (collection, slug, field, locale) */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @description Collection of the referring entry */
collection?: string;
/** @description Field that holds the reference */
field?: string;
/** @description Locale of the referring entry if applicable */
locale?: string | null;
/** @description Slug of the referring entry */
slug?: string;
}[];
};
};
};
};
listNavigation: { listNavigation: {
parameters: { parameters: {
query?: { query?: {
@@ -10585,6 +10942,8 @@ export interface operations {
_locale?: string; _locale?: string;
/** @description Filter by logo */ /** @description Filter by logo */
logo?: string; logo?: string;
/** @description Filter by siteName */
siteName?: string;
/** @description Filter by website */ /** @description Filter by website */
website?: string; website?: string;
/** @description Filter by footerText1 */ /** @description Filter by footerText1 */
+47 -15
View File
@@ -35,6 +35,19 @@ function getCmsBaseUrl(): string {
return base; return base;
} }
/**
* Relative CMS-Pfade (z. B. `/api/assets/…`) für Fetch/Transform absolut machen.
* RustyCMS liefert oft relative URLs; `/api/transform` braucht eine absolute URL.
*/
export function normalizeMediaUrl(url: string): string {
const u = url.trim();
if (!u) return u;
if (u.startsWith("//")) return `https:${u}`;
if (u.startsWith("http://") || u.startsWith("https://")) return u;
if (u.startsWith("/")) return `${getCmsBaseUrl()}${u}`;
return u;
}
/** Stabile Dateiendung aus format. */ /** Stabile Dateiendung aus format. */
function formatToExt(format: string): string { function formatToExt(format: string): string {
switch (format.toLowerCase()) { switch (format.toLowerCase()) {
@@ -79,9 +92,10 @@ export async function ensureTransformedImage(
url: string, url: string,
params: RustyImageTransformParams = {}, params: RustyImageTransformParams = {},
): Promise<string> { ): Promise<string> {
const absoluteUrl = normalizeMediaUrl(url);
const format = params.format ?? DEFAULT_FORMAT; const format = params.format ?? DEFAULT_FORMAT;
const ext = formatToExt(format); const ext = formatToExt(format);
const hash = hashForTransform(url, params); const hash = hashForTransform(absoluteUrl, params);
const { width, height } = params; const { width, height } = params;
const dimSuffix = const dimSuffix =
width != null && height != null width != null && height != null
@@ -130,7 +144,7 @@ export async function ensureTransformedImage(
const baseUrl = getCmsBaseUrl(); const baseUrl = getCmsBaseUrl();
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
searchParams.set("url", url); searchParams.set("url", absoluteUrl);
if (params.width != null) searchParams.set("w", String(params.width)); if (params.width != null) searchParams.set("w", String(params.width));
if (params.height != null) searchParams.set("h", String(params.height)); if (params.height != null) searchParams.set("h", String(params.height));
if (params.ar != null) searchParams.set("ar", params.ar); if (params.ar != null) searchParams.set("ar", params.ar);
@@ -144,7 +158,7 @@ export async function ensureTransformedImage(
const res = await fetch(transformUrl); const res = await fetch(transformUrl);
if (!res.ok) { if (!res.ok) {
throw new Error( throw new Error(
`rusty-image: transform failed ${res.status} for ${url}: ${await res.text()}`, `rusty-image: transform failed ${res.status} for ${absoluteUrl}: ${await res.text()}`,
); );
} }
@@ -163,10 +177,16 @@ export async function ensureTransformedImage(
type ImageArrayItem = string | { src?: string }; type ImageArrayItem = string | { src?: string };
function toImageUrl(item: ImageArrayItem): string | null { function toImageUrl(item: ImageArrayItem): string | null {
if (typeof item === "string") if (typeof item === "string") {
return item.startsWith("//") ? `https:${item}` : item; const s = item.trim();
if (item && typeof item === "object" && typeof item.src === "string") if (!s) return null;
return item.src.startsWith("//") ? `https:${item.src}` : item.src; return normalizeMediaUrl(s.startsWith("//") ? `https:${s}` : s);
}
if (item && typeof item === "object" && typeof item.src === "string") {
const s = item.src.trim();
if (!s) return null;
return normalizeMediaUrl(s.startsWith("//") ? `https:${s}` : s);
}
return null; return null;
} }
@@ -182,7 +202,7 @@ export async function resolveImageUrls(
const result: string[] = []; const result: string[] = [];
for (const item of items) { for (const item of items) {
const u = toImageUrl(item); const u = toImageUrl(item);
if (!u || !u.startsWith("http")) continue; if (!u) continue;
try { try {
result.push(await ensureTransformedImage(u, params)); result.push(await ensureTransformedImage(u, params));
} catch (e) { } catch (e) {
@@ -198,11 +218,16 @@ function getImageBlockUrl(block: {
img?: string | { src?: string; file?: { url?: string }; title?: string }; img?: string | { src?: string; file?: { url?: string }; title?: string };
}): string | null { }): string | null {
const source = block.image ?? block.img; const source = block.image ?? block.img;
if (typeof source === "string" && source.startsWith("http")) return source; if (typeof source === "string") {
const s = source.trim();
if (!s) return null;
return normalizeMediaUrl(s.startsWith("//") ? `https:${s}` : s);
}
if (source && typeof source === "object") { if (source && typeof source === "object") {
const u = const u =
"src" in source && typeof source.src === "string" ? source.src : source.file?.url; "src" in source && typeof source.src === "string" ? source.src : source.file?.url;
if (typeof u === "string") return u.startsWith("//") ? `https:${u}` : u; if (typeof u !== "string" || !u.trim()) return null;
return normalizeMediaUrl(u.startsWith("//") ? `https:${u}` : u);
} }
return null; return null;
} }
@@ -211,8 +236,14 @@ function getImageBlockUrl(block: {
function getGalleryImageUrl( function getGalleryImageUrl(
img: { src?: string; file?: { url?: string } }, img: { src?: string; file?: { url?: string } },
): string | null { ): string | null {
if (typeof img.src === "string") return img.src.startsWith("//") ? `https:${img.src}` : img.src; if (typeof img.src === "string" && img.src.trim()) {
if (img.file?.url) return img.file.url.startsWith("//") ? `https:${img.file.url}` : img.file.url; const s = img.src;
return normalizeMediaUrl(s.startsWith("//") ? `https:${s}` : s);
}
if (img.file?.url) {
const s = img.file.url;
return normalizeMediaUrl(s.startsWith("//") ? `https:${s}` : s);
}
return null; return null;
} }
@@ -256,9 +287,10 @@ export async function processMarkdownHtmlImages(
for (const m of matches) { for (const m of matches) {
const rawSrc = m[2]; const rawSrc = m[2];
const url = rawSrc.startsWith("//") ? `https:${rawSrc}` : rawSrc; const url = rawSrc.startsWith("//") ? `https:${rawSrc}` : rawSrc;
if (!url.startsWith("http")) continue; const absolute = normalizeMediaUrl(url);
if (!absolute.startsWith("http")) continue;
try { try {
const transformedPath = await ensureTransformedImage(url, transformParams); const transformedPath = await ensureTransformedImage(absolute, transformParams);
const href = transformedPath.startsWith("/") ? transformedPath : `/${transformedPath}`; const href = transformedPath.startsWith("/") ? transformedPath : `/${transformedPath}`;
const newImg = m[0].replace(rawSrc, transformedPath); const newImg = m[0].replace(rawSrc, transformedPath);
const newBlock = `<a href="${href}" target="_blank" rel="noopener noreferrer" class="markdown-image-link">${newImg}</a>`; const newBlock = `<a href="${href}" target="_blank" rel="noopener noreferrer" class="markdown-image-link">${newImg}</a>`;
@@ -352,7 +384,7 @@ export async function resolveContentImages(
for (const img of block.images) { for (const img of block.images) {
if (!img || typeof img !== "object") continue; if (!img || typeof img !== "object") continue;
const url = getGalleryImageUrl(img); const url = getGalleryImageUrl(img);
if (!url || !url.startsWith("http")) continue; if (!url) continue;
try { try {
(img as { resolvedSrc?: string }).resolvedSrc = (img as { resolvedSrc?: string }).resolvedSrc =
await ensureTransformedImage(url, galleryParams); await ensureTransformedImage(url, galleryParams);
+528 -625
View File
File diff suppressed because it is too large Load Diff