Merge pull request #2492 from homarr-labs/feat/fix-cf-compile

This commit is contained in:
Thomas Camlong
2025-11-17 15:17:45 +01:00
committed by GitHub
7 changed files with 134 additions and 108 deletions

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

View File

@@ -2,26 +2,16 @@ import { permanentRedirect, redirect } from "next/navigation"
import { ImageResponse } from "next/og"
import { getCommunityGalleryRecord, getCommunitySubmissionByName, getCommunitySubmissions } from "@/lib/community"
export const revalidate = 21600 // 6 hours
export async function generateStaticParams() {
const icons = await getCommunitySubmissions()
const validIcons = icons.filter((icon) => icon.name)
if (process.env.CI_MODE === "false") {
return validIcons.slice(0, 5).map((icon) => ({
icon: icon.name,
}))
}
return validIcons.map((icon) => ({
icon: icon.name,
}))
}
export const dynamic = "force-dynamic";
export const size = {
width: 1200,
height: 630,
}
export const alt = "Community Icon Open Graph Image";
export const contentType = "image/png";
export default async function Image({ params }: { params: Promise<{ icon: string }> }) {
const { icon } = await params
@@ -137,7 +127,7 @@ export default async function Image({ params }: { params: Promise<{ icon: string
const iconUrl = iconDataBuffer
? `data:image/png;base64,${iconDataBuffer.toString("base64")}`
: `https://placehold.co/600x400?text=${formattedIconName}`
: `https://placehold.co/600x400?text=${formattedIconName}`;
return new ImageResponse(
<div
@@ -171,7 +161,8 @@ export default async function Image({ params }: { params: Promise<{ icon: string
fontSize: 20,
fontWeight: 700,
letterSpacing: "0.5px",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
boxShadow:
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
zIndex: 30,
textTransform: "uppercase",
}}
@@ -186,7 +177,8 @@ export default async function Image({ params }: { params: Promise<{ icon: string
width: 400,
height: 400,
borderRadius: "50%",
background: "linear-gradient(135deg, rgba(56, 189, 248, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)",
background:
"linear-gradient(135deg, rgba(56, 189, 248, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)",
filter: "blur(80px)",
zIndex: 2,
}}
@@ -199,7 +191,8 @@ export default async function Image({ params }: { params: Promise<{ icon: string
width: 500,
height: 500,
borderRadius: "50%",
background: "linear-gradient(135deg, rgba(249, 115, 22, 0.1) 0%, rgba(234, 88, 12, 0.1) 100%)",
background:
"linear-gradient(135deg, rgba(249, 115, 22, 0.1) 0%, rgba(234, 88, 12, 0.1) 100%)",
filter: "blur(100px)",
zIndex: 2,
}}
@@ -227,7 +220,8 @@ export default async function Image({ params }: { params: Promise<{ icon: string
height: 320,
borderRadius: 32,
background: "white",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)",
boxShadow:
"0 25px 50px -12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)",
padding: 30,
flexShrink: 0,
position: "relative",
@@ -362,5 +356,5 @@ export default async function Image({ params }: { params: Promise<{ icon: string
{
...size,
},
)
);
}

View File

@@ -92,6 +92,7 @@ export async function generateMetadata({ params }: Props, _parent: ResolvingMeta
width: 512,
height: 512,
alt: `${formattedIconName} icon`,
type: typeof mainIconUrl === "string" && mainIconUrl.endsWith(".png") ? "image/png" : typeof mainIconUrl === "string" && mainIconUrl.endsWith(".svg") ? "image/svg+xml" : "image/png",
},
],
},
@@ -99,6 +100,7 @@ export async function generateMetadata({ params }: Props, _parent: ResolvingMeta
card: "summary_large_image",
title: `${formattedIconName} Icon (Community) | Dashboard Icons`,
description: `Download the ${formattedIconName} community-submitted icon. Part of a collection of ${totalIcons} community icons awaiting review and addition to the Dashboard Icons collection.`,
images: [mainIconUrl],
},
alternates: {
canonical: `${WEB_URL}/community/${icon}`,

View File

@@ -1,34 +1,26 @@
import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { ImageResponse } from "next/og"
import { getAllIcons } from "@/lib/api"
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { ImageResponse } from "next/og";
import { getAllIcons } from "@/lib/api";
export const revalidate = false
export async function generateStaticParams() {
const iconsData = await getAllIcons()
if (process.env.CI_MODE === "false") {
// This is meant to speed up the build process in local development
return Object.keys(iconsData)
.slice(0, 5)
.map((icon) => ({
icon,
}))
}
return Object.keys(iconsData).map((icon) => ({
icon,
}))
}
export const dynamic = "force-dynamic";
export const size = {
width: 1200,
height: 630,
}
export default async function Image({ params }: { params: Promise<{ icon: string }> }) {
const { icon } = await params
};
export const alt = "Icon Open Graph Image";
export const contentType = "image/png";
export default async function Image({
params,
}: {
params: Promise<{ icon: string }>;
}) {
const { icon } = await params;
if (!icon) {
console.error(`[Opengraph Image] Icon not found for ${icon}`)
console.error(`[Opengraph Image] Icon not found for ${icon}`);
return new ImageResponse(
<div
style={{
@@ -46,31 +38,35 @@ export default async function Image({ params }: { params: Promise<{ icon: string
Icon not found
</div>,
{ ...size },
)
);
}
const iconsData = await getAllIcons()
const totalIcons = Object.keys(iconsData).length
const index = Object.keys(iconsData).indexOf(icon)
const iconsData = await getAllIcons();
const totalIcons = Object.keys(iconsData).length;
const index = Object.keys(iconsData).indexOf(icon);
// Format the icon name for display
const formattedIconName = icon
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
.join(" ");
// Read the icon file from local filesystem
let iconData: Buffer | null = null
let iconData: Buffer | null = null;
try {
const iconPath = join(process.cwd(), `../png/${icon}.png`)
console.log(`Generating opengraph image for ${icon} (${index + 1} / ${totalIcons}) from path ${iconPath}`)
iconData = await readFile(iconPath)
const iconPath = join(process.cwd(), `../png/${icon}.png`);
console.log(
`Generating opengraph image for ${icon} (${index + 1} / ${totalIcons}) from path ${iconPath}`,
);
iconData = await readFile(iconPath);
} catch (_error) {
console.error(`Icon ${icon} was not found locally`)
console.error(`Icon ${icon} was not found locally`);
}
// Convert the image data to a data URL or use placeholder
const iconUrl = iconData ? `data:image/png;base64,${iconData.toString("base64")}` : null
const iconUrl = iconData
? `data:image/png;base64,${iconData.toString("base64")}`
: null;
return new ImageResponse(
<div
@@ -96,7 +92,8 @@ export default async function Image({ params }: { params: Promise<{ icon: string
width: 400,
height: 400,
borderRadius: "50%",
background: "linear-gradient(135deg, rgba(56, 189, 248, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)",
background:
"linear-gradient(135deg, rgba(56, 189, 248, 0.1) 0%, rgba(59, 130, 246, 0.1) 100%)",
filter: "blur(80px)",
zIndex: 2,
}}
@@ -109,7 +106,8 @@ export default async function Image({ params }: { params: Promise<{ icon: string
width: 500,
height: 500,
borderRadius: "50%",
background: "linear-gradient(135deg, rgba(249, 115, 22, 0.1) 0%, rgba(234, 88, 12, 0.1) 100%)",
background:
"linear-gradient(135deg, rgba(249, 115, 22, 0.1) 0%, rgba(234, 88, 12, 0.1) 100%)",
filter: "blur(100px)",
zIndex: 2,
}}
@@ -139,7 +137,8 @@ export default async function Image({ params }: { params: Promise<{ icon: string
height: 320,
borderRadius: 32,
background: "white",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)",
boxShadow:
"0 25px 50px -12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)",
padding: 30,
flexShrink: 0,
position: "relative",
@@ -155,7 +154,10 @@ export default async function Image({ params }: { params: Promise<{ icon: string
}}
/>
<img
src={iconUrl || `https://placehold.co/600x400?text=${formattedIconName}`}
src={
iconUrl ||
`https://placehold.co/600x400?text=${formattedIconName}`
}
alt={formattedIconName}
width={260}
height={260}
@@ -279,5 +281,5 @@ export default async function Image({ params }: { params: Promise<{ icon: string
{
...size,
},
)
);
}

View File

@@ -79,11 +79,26 @@ export async function generateMetadata({ params, searchParams }: Props, _parent:
url: pageUrl,
siteName: "Dashboard Icons",
images: [
{
url: `${BASE_URL}/png/${icon}.png`,
width: 512,
height: 512,
alt: `${formattedIconName} icon`,
type: "image/png",
},
{
url: `${BASE_URL}/webp/${icon}.webp`,
width: 512,
height: 512,
alt: `${formattedIconName} icon`,
type: "image/webp",
},
{
url: `${BASE_URL}/svg/${icon}.svg`,
width: 512,
height: 512,
alt: `${formattedIconName} icon`,
type: "image/svg+xml",
},
],
},
@@ -91,6 +106,7 @@ export async function generateMetadata({ params, searchParams }: Props, _parent:
card: "summary_large_image",
title: `${formattedIconName} Icon | Dashboard Icons`,
description: `Download the ${formattedIconName} icon in SVG, PNG, and WEBP formats for FREE. Part of a collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`,
images: [`${BASE_URL}/png/${icon}.png`],
},
alternates: {
canonical: `${WEB_URL}/icons/${icon}`,

View File

@@ -1,7 +1,7 @@
import { unstable_cache } from "next/cache"
import { METADATA_URL } from "@/constants"
import { ApiError } from "@/lib/errors"
import type { AuthorData, IconFile, IconWithName } from "@/types/icons"
import { unstable_cache } from "next/cache";
import { METADATA_URL } from "@/constants";
import { ApiError } from "@/lib/errors";
import type { AuthorData, IconFile, IconWithName } from "@/types/icons";
/**
* Fetches all icon data from the metadata.json file
@@ -9,19 +9,22 @@ import type { AuthorData, IconFile, IconWithName } from "@/types/icons"
*/
export async function getAllIcons(): Promise<IconFile> {
try {
const response = await fetch(METADATA_URL)
const response = await fetch(METADATA_URL);
if (!response.ok) {
throw new ApiError(`Failed to fetch icons: ${response.statusText}`, response.status)
throw new ApiError(
`Failed to fetch icons: ${response.statusText}`,
response.status,
);
}
return (await response.json()) as IconFile
return (await response.json()) as IconFile;
} catch (error) {
if (error instanceof ApiError) {
throw error
throw error;
}
console.error("Error fetching icons:", error)
throw new ApiError("Failed to fetch icons data. Please try again later.")
console.error("Error fetching icons:", error);
throw new ApiError("Failed to fetch icons data. Please try again later.");
}
}
@@ -30,55 +33,57 @@ export async function getAllIcons(): Promise<IconFile> {
*/
export const getIconNames = async (): Promise<string[]> => {
try {
const iconsData = await getAllIcons()
return Object.keys(iconsData)
const iconsData = await getAllIcons();
return Object.keys(iconsData);
} catch (error) {
console.error("Error getting icon names:", error)
throw error
console.error("Error getting icon names:", error);
throw error;
}
}
};
/**
* Converts icon data to an array format for easier rendering
*/
export async function getIconsArray(): Promise<IconWithName[]> {
try {
const iconsData = await getAllIcons()
const iconsData = await getAllIcons();
return Object.entries(iconsData)
.map(([name, data]) => ({
name,
data,
}))
.sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) => a.name.localeCompare(b.name));
} catch (error) {
console.error("Error getting icons array:", error)
throw error
console.error("Error getting icons array:", error);
throw error;
}
}
/**
* Fetches data for a specific icon
*/
export async function getIconData(iconName: string): Promise<IconWithName | null> {
export async function getIconData(
iconName: string,
): Promise<IconWithName | null> {
try {
const iconsData = await getAllIcons()
const iconData = iconsData[iconName]
const iconsData = await getAllIcons();
const iconData = iconsData[iconName];
if (!iconData) {
throw new ApiError(`Icon '${iconName}' not found`, 404)
throw new ApiError(`Icon '${iconName}' not found`, 404);
}
return {
name: iconName,
data: iconData,
}
};
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
return null
return null;
}
console.error("Error getting icon data:", error)
throw error
console.error("Error getting icon data:", error);
throw error;
}
}
@@ -91,26 +96,31 @@ async function fetchAuthorData(authorId: number) {
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
})
});
if (!response.ok) {
// If unauthorized or other error, return a default user object
if (response.status === 401 || response.status === 403) {
console.warn(`GitHub API rate limit or authorization issue: ${response.statusText}`)
console.warn(
`GitHub API rate limit or authorization issue: ${response.statusText}`,
);
return {
login: "unknown",
avatar_url: "https://avatars.githubusercontent.com/u/0",
html_url: "https://github.com",
name: "Unknown User",
bio: null,
}
};
}
throw new ApiError(`Failed to fetch author data: ${response.statusText}`, response.status)
throw new ApiError(
`Failed to fetch author data: ${response.statusText}`,
response.status,
);
}
return response.json()
return response.json();
} catch (error) {
console.error("Error fetching author data:", error)
console.error("Error fetching author data:", error);
// Even for unexpected errors, return a default user to prevent page failures
return {
login: "unknown",
@@ -118,7 +128,7 @@ async function fetchAuthorData(authorId: number) {
html_url: "https://github.com",
name: "Unknown User",
bio: null,
}
};
}
}
@@ -148,32 +158,37 @@ export async function getAuthorData(authorId: number): Promise<AuthorData> {
*/
export async function getTotalIcons() {
try {
const iconsData = await getAllIcons()
const iconsData = await getAllIcons();
return {
totalIcons: Object.keys(iconsData).length,
}
};
} catch (error) {
console.error("Error getting total icons:", error)
throw error
console.error("Error getting total icons:", error);
throw error;
}
}
/**
* Fetches recently added icons sorted by timestamp
*/
export async function getRecentlyAddedIcons(limit = 8): Promise<IconWithName[]> {
export async function getRecentlyAddedIcons(
limit = 8,
): Promise<IconWithName[]> {
try {
const icons = await getIconsArray()
const icons = await getIconsArray();
return icons
.sort((a, b) => {
// Sort by timestamp in descending order (newest first)
return new Date(b.data.update.timestamp).getTime() - new Date(a.data.update.timestamp).getTime()
return (
new Date(b.data.update.timestamp).getTime() -
new Date(a.data.update.timestamp).getTime()
);
})
.slice(0, limit)
.slice(0, limit);
} catch (error) {
console.error("Error getting recently added icons:", error)
throw error
console.error("Error getting recently added icons:", error);
throw error;
}
}

View File

@@ -10,6 +10,3 @@ export class ApiError extends Error {
this.status = status
}
}