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"
},
"dependencies": {
"@astrojs/sitemap": "^3.7.0",
"@astrojs/svelte": "^7.2.5",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/svelte": "^8.0.4",
"@fontsource-variable/lora": "^5.2.8",
"@fontsource-variable/rubik": "^5.2.8",
"@fontsource/inter": "^5.2.8",
"@iconify/svelte": "^5.2.1",
"@tailwindcss/vite": "^4.1.18",
"astro": "^5.17.1",
"astro": "^6.1.1",
"json5": "^2.2.3",
"marked": "^17.0.2",
"svelte": "^5.51.2",
+9 -5
View File
@@ -225,9 +225,12 @@ try {
})) ??
(await getPageConfigBySlug("default", { locale: "de", resolve: "logo" }));
const rawLogo = pageConfig?.logo;
/** Logo-URL: String (z. B. `/api/assets/…`) oder aufgelöstes img-Objekt. */
const logoFileUrl =
typeof rawLogo === "string" && rawLogo.startsWith("http")
? rawLogo
typeof rawLogo === "string" && rawLogo.trim()
? rawLogo.trim().startsWith("//")
? `https:${rawLogo.trim()}`
: rawLogo.trim()
: ((
rawLogo as
| { src?: string; _slug?: string; file?: { url?: string } }
@@ -237,9 +240,10 @@ try {
?._slug ??
(rawLogo as { file?: { url?: string } } | undefined)?.file?.url);
if (logoFileUrl) {
const u = logoFileUrl.startsWith("//")
? `https:${logoFileUrl}`
: logoFileUrl;
const u =
typeof logoFileUrl === "string" && logoFileUrl.startsWith("//")
? `https:${logoFileUrl}`
: logoFileUrl;
logoUrl = await ensureTransformedImage(u, {
height: 56,
fit: "contain",
+361 -2
View File
@@ -375,6 +375,23 @@ export interface paths {
patch?: 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": {
parameters: {
query?: never;
@@ -1639,6 +1656,63 @@ export interface paths {
patch?: 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": {
parameters: {
query?: never;
@@ -3025,7 +3099,7 @@ export interface components {
/** @description Entry identifier (filename) */
readonly _slug?: string;
/** @description Banner headline */
headline: string;
headline?: string;
/** @description Banner image (optional) */
image?: string | {
/** @description Alt text / description for accessibility */
@@ -3050,7 +3124,7 @@ export interface components {
/** @description URL slug (used as filename) */
_slug: string;
/** @description Banner headline */
headline: string;
headline?: string;
/** @description Banner image (optional) */
image?: string | {
/** @description Alt text / description for accessibility */
@@ -3745,6 +3819,26 @@ export interface components {
/** @description Internal component name */
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: {
/** @description Entry identifier (filename) */
readonly _slug?: string;
@@ -3855,6 +3949,8 @@ export interface components {
seoDescription: string;
/** @description Default SEO title for website */
seoTitle: string;
/** @description Site name */
siteName: string;
/** @description Website URL */
website: string;
};
@@ -3880,6 +3976,8 @@ export interface components {
seoDescription: string;
/** @description Default SEO title for website */
seoTitle: string;
/** @description Site name */
siteName: string;
/** @description Website URL */
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: {
parameters: {
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: {
parameters: {
query?: {
@@ -10585,6 +10942,8 @@ export interface operations {
_locale?: string;
/** @description Filter by logo */
logo?: string;
/** @description Filter by siteName */
siteName?: string;
/** @description Filter by website */
website?: string;
/** @description Filter by footerText1 */
+47 -15
View File
@@ -35,6 +35,19 @@ function getCmsBaseUrl(): string {
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. */
function formatToExt(format: string): string {
switch (format.toLowerCase()) {
@@ -79,9 +92,10 @@ export async function ensureTransformedImage(
url: string,
params: RustyImageTransformParams = {},
): Promise<string> {
const absoluteUrl = normalizeMediaUrl(url);
const format = params.format ?? DEFAULT_FORMAT;
const ext = formatToExt(format);
const hash = hashForTransform(url, params);
const hash = hashForTransform(absoluteUrl, params);
const { width, height } = params;
const dimSuffix =
width != null && height != null
@@ -130,7 +144,7 @@ export async function ensureTransformedImage(
const baseUrl = getCmsBaseUrl();
const searchParams = new URLSearchParams();
searchParams.set("url", url);
searchParams.set("url", absoluteUrl);
if (params.width != null) searchParams.set("w", String(params.width));
if (params.height != null) searchParams.set("h", String(params.height));
if (params.ar != null) searchParams.set("ar", params.ar);
@@ -144,7 +158,7 @@ export async function ensureTransformedImage(
const res = await fetch(transformUrl);
if (!res.ok) {
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 };
function toImageUrl(item: ImageArrayItem): string | null {
if (typeof item === "string")
return item.startsWith("//") ? `https:${item}` : item;
if (item && typeof item === "object" && typeof item.src === "string")
return item.src.startsWith("//") ? `https:${item.src}` : item.src;
if (typeof item === "string") {
const s = item.trim();
if (!s) return null;
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;
}
@@ -182,7 +202,7 @@ export async function resolveImageUrls(
const result: string[] = [];
for (const item of items) {
const u = toImageUrl(item);
if (!u || !u.startsWith("http")) continue;
if (!u) continue;
try {
result.push(await ensureTransformedImage(u, params));
} catch (e) {
@@ -198,11 +218,16 @@ function getImageBlockUrl(block: {
img?: string | { src?: string; file?: { url?: string }; title?: string };
}): string | null {
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") {
const u =
"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;
}
@@ -211,8 +236,14 @@ function getImageBlockUrl(block: {
function getGalleryImageUrl(
img: { src?: string; file?: { url?: string } },
): string | null {
if (typeof img.src === "string") return img.src.startsWith("//") ? `https:${img.src}` : img.src;
if (img.file?.url) return img.file.url.startsWith("//") ? `https:${img.file.url}` : img.file.url;
if (typeof img.src === "string" && img.src.trim()) {
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;
}
@@ -256,9 +287,10 @@ export async function processMarkdownHtmlImages(
for (const m of matches) {
const rawSrc = m[2];
const url = rawSrc.startsWith("//") ? `https:${rawSrc}` : rawSrc;
if (!url.startsWith("http")) continue;
const absolute = normalizeMediaUrl(url);
if (!absolute.startsWith("http")) continue;
try {
const transformedPath = await ensureTransformedImage(url, transformParams);
const transformedPath = await ensureTransformedImage(absolute, transformParams);
const href = transformedPath.startsWith("/") ? transformedPath : `/${transformedPath}`;
const newImg = m[0].replace(rawSrc, transformedPath);
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) {
if (!img || typeof img !== "object") continue;
const url = getGalleryImageUrl(img);
if (!url || !url.startsWith("http")) continue;
if (!url) continue;
try {
(img as { resolvedSrc?: string }).resolvedSrc =
await ensureTransformedImage(url, galleryParams);
+528 -625
View File
File diff suppressed because it is too large Load Diff