Compare commits

...

5 Commits

Author SHA1 Message Date
Thomas Camlong
ee297afbf4 Merge branch 'main' into feat/structured-seo 2025-04-24 18:03:39 +02:00
Thomas Camlong
038e4dc73d chore(web): Update Carbon ID 2025-04-24 16:31:45 +02:00
Thomas Camlong
6001d195a6 feat(ci): Specify ID's
Signed-off-by: Thomas Camlong <thomas@ajnart.fr>

Update add_normal_icon.yml

Signed-off-by: Thomas Camlong <thomas@ajnart.fr>

Update add_normal_icon.yml

Signed-off-by: Thomas Camlong <thomas@ajnart.fr>
2025-04-24 16:31:07 +02:00
Bjorn Lammers
40482771fa fix(web): Run Biome checks and apply fixes 2025-04-24 16:07:29 +02:00
Bjorn Lammers
34fef44222 feat(web): Add structured data and improve SEO metadata 2025-04-24 16:01:03 +02:00
10 changed files with 368 additions and 146 deletions

View File

@@ -22,6 +22,9 @@
"recommended": true, "recommended": true,
"suspicious": { "suspicious": {
"noArrayIndexKey": "off" "noArrayIndexKey": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
} }
} }
}, },

6
web/public/robots.txt Normal file
View File

@@ -0,0 +1,6 @@
# Allow all user agents
User-agent: *
Allow: /
# Sitemap location (adjust if needed)
Sitemap: https://dashboardicons.com/sitemap.xml

View File

@@ -1,5 +1,6 @@
import { readFile } from "node:fs/promises" import { readFile } from "node:fs/promises"
import { join } from "node:path" import { join } from "node:path"
import { SITE_NAME, SITE_TAGLINE, WEB_URL, getIconDescription } from "@/constants"
import { getAllIcons } from "@/lib/api" import { getAllIcons } from "@/lib/api"
import { ImageResponse } from "next/og" import { ImageResponse } from "next/og"
@@ -32,10 +33,9 @@ export default async function Image({ params }: { params: { icon: string } }) {
let iconData: Buffer | null = null let iconData: Buffer | null = null
try { try {
const iconPath = join(process.cwd(), `../png/${icon}.png`) 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) iconData = await readFile(iconPath)
} catch (error) { } catch (error) {
console.error(`Icon ${icon} was not found locally`) // Icon file might not be found, fallback handled below
} }
// Convert the image data to a data URL or use placeholder // Convert the image data to a data URL or use placeholder
@@ -52,9 +52,9 @@ export default async function Image({ params }: { params: { icon: string } }) {
position: "relative", position: "relative",
fontFamily: "Inter, system-ui, sans-serif", fontFamily: "Inter, system-ui, sans-serif",
overflow: "hidden", overflow: "hidden",
backgroundColor: "white", backgroundColor: "#0f172a", // Dark background (slate-900)
backgroundImage: backgroundImage:
"radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%)", "radial-gradient(circle at 25px 25px, #1e293b 2%, transparent 0%), radial-gradient(circle at 75px 75px, #1e293b 2%, transparent 0%)",
backgroundSize: "100px 100px", backgroundSize: "100px 100px",
}} }}
> >
@@ -67,7 +67,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
width: 400, width: 400,
height: 400, height: 400,
borderRadius: "50%", 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.15) 0%, rgba(59, 130, 246, 0.15) 100%)",
filter: "blur(80px)", filter: "blur(80px)",
zIndex: 2, zIndex: 2,
}} }}
@@ -80,7 +80,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
width: 500, width: 500,
height: 500, height: 500,
borderRadius: "50%", 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.15) 0%, rgba(234, 88, 12, 0.15) 100%)",
filter: "blur(100px)", filter: "blur(100px)",
zIndex: 2, zIndex: 2,
}} }}
@@ -109,8 +109,8 @@ export default async function Image({ params }: { params: { icon: string } }) {
width: 320, width: 320,
height: 320, height: 320,
borderRadius: 32, borderRadius: 32,
background: "white", background: "#1e293b", // Dark container (slate-800)
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.5), 0 0 0 1px rgba(255, 255, 255, 0.1)",
padding: 30, padding: 30,
flexShrink: 0, flexShrink: 0,
position: "relative", position: "relative",
@@ -121,7 +121,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
style={{ style={{
position: "absolute", position: "absolute",
inset: 0, inset: 0,
background: "linear-gradient(145deg, #ffffff 0%, #f8fafc 100%)", background: "linear-gradient(145deg, #1e293b 0%, #0f172a 100%)",
zIndex: 0, zIndex: 0,
}} }}
/> />
@@ -134,7 +134,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
objectFit: "contain", objectFit: "contain",
position: "relative", position: "relative",
zIndex: 1, zIndex: 1,
filter: "drop-shadow(0 10px 15px rgba(0, 0, 0, 0.1))", filter: "drop-shadow(0 10px 15px rgba(0, 0, 0, 0.3))",
}} }}
/> />
</div> </div>
@@ -154,7 +154,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex", display: "flex",
fontSize: 64, fontSize: 64,
fontWeight: 800, fontWeight: 800,
color: "#0f172a", color: "#f8fafc", // Light text for dark background (slate-50)
lineHeight: 1.1, lineHeight: 1.1,
letterSpacing: "-0.02em", letterSpacing: "-0.02em",
}} }}
@@ -167,14 +167,14 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex", display: "flex",
fontSize: 32, fontSize: 32,
fontWeight: 500, fontWeight: 500,
color: "#64748b", color: "#94a3b8", // Muted text (slate-400)
lineHeight: 1.4, lineHeight: 1.4,
position: "relative", position: "relative",
paddingLeft: 16, paddingLeft: 16,
borderLeft: "4px solid #94a3b8", borderLeft: "4px solid #64748b", // slate-500
}} }}
> >
Amongst {totalIcons} other high-quality dashboard icons {getIconDescription(formattedIconName, totalIcons)}
</div> </div>
<div <div
@@ -191,14 +191,14 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
backgroundColor: "#f1f5f9", backgroundColor: "#334155", // slate-700
color: "#475569", color: "#e2e8f0", // slate-200
border: "2px solid #e2e8f0", border: "2px solid #475569", // slate-600
borderRadius: 12, borderRadius: 12,
padding: "8px 16px", padding: "8px 16px",
fontSize: 18, fontSize: 18,
fontWeight: 600, fontWeight: 600,
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)", boxShadow: "0 1px 2px rgba(0, 0, 0, 0.2)",
}} }}
> >
{format} {format}
@@ -219,8 +219,8 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
background: "#ffffff", background: "#1e293b", // slate-800
borderTop: "2px solid rgba(0, 0, 0, 0.05)", borderTop: "2px solid rgba(255, 255, 255, 0.1)",
zIndex: 20, zIndex: 20,
}} }}
> >
@@ -229,7 +229,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex", display: "flex",
fontSize: 24, fontSize: 24,
fontWeight: 600, fontWeight: 600,
color: "#334155", color: "#e2e8f0", // slate-200
alignItems: "center", alignItems: "center",
gap: 10, gap: 10,
}} }}
@@ -239,11 +239,11 @@ export default async function Image({ params }: { params: { icon: string } }) {
width: 8, width: 8,
height: 8, height: 8,
borderRadius: "50%", borderRadius: "50%",
backgroundColor: "#3b82f6", backgroundColor: "#3b82f6", // blue-500
marginRight: 4, marginRight: 4,
}} }}
/> />
dashboardicons.com {WEB_URL.replace("https://", "")}
</div> </div>
</div> </div>
</div>, </div>,

View File

@@ -1,8 +1,20 @@
import { IconDetails } from "@/components/icon-details" import { IconDetails } from "@/components/icon-details"
import { BASE_URL, WEB_URL } from "@/constants" import { StructuredData } from "@/components/structured-data"
import {
BASE_URL,
GITHUB_URL,
ICON_DETAIL_KEYWORDS,
SITE_NAME,
SITE_TAGLINE,
TITLE_SEPARATOR,
WEB_URL,
getIconDescription,
getIconSchema,
} from "@/constants"
import { getAllIcons, getAuthorData } from "@/lib/api" import { getAllIcons, getAuthorData } from "@/lib/api"
import type { Metadata, ResolvingMetadata } from "next" import type { Metadata, ResolvingMetadata } from "next"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import Script from "next/script"
export const dynamicParams = false export const dynamicParams = false
@@ -16,12 +28,12 @@ export async function generateStaticParams() {
export const dynamic = "force-static" export const dynamic = "force-static"
type Props = { type Props = {
params: Promise<{ icon: string }> params: { icon: string }
searchParams: Promise<{ [key: string]: string | string[] | undefined }> searchParams: { [key: string]: string | string[] | undefined }
} }
export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> { export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
const { icon } = await params const { icon } = params
const iconsData = await getAllIcons() const iconsData = await getAllIcons()
if (!iconsData[icon]) { if (!iconsData[icon]) {
notFound() notFound()
@@ -31,8 +43,6 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
const updateDate = new Date(iconsData[icon].update.timestamp) const updateDate = new Date(iconsData[icon].update.timestamp)
const totalIcons = Object.keys(iconsData).length const totalIcons = Object.keys(iconsData).length
console.debug(`Generated metadata for ${icon} by ${authorName} (${authorData.html_url}) updated at ${updateDate.toLocaleString()}`)
const iconImageUrl = `${BASE_URL}/png/${icon}.png` const iconImageUrl = `${BASE_URL}/png/${icon}.png`
const pageUrl = `${WEB_URL}/icons/${icon}` const pageUrl = `${WEB_URL}/icons/${icon}`
const formattedIconName = icon const formattedIconName = icon
@@ -40,43 +50,39 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ") .join(" ")
const title = `${formattedIconName} Icon ${TITLE_SEPARATOR} ${SITE_NAME}`
const fullTitle = `${formattedIconName} Icon ${TITLE_SEPARATOR} ${SITE_NAME} ${TITLE_SEPARATOR} ${SITE_TAGLINE}`
const description = getIconDescription(formattedIconName, totalIcons)
return { return {
title: `${formattedIconName} Icon | Dashboard Icons`, title,
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.`, description,
assets: [iconImageUrl], assets: [iconImageUrl],
category: "icons", category: "Icons",
keywords: [ keywords: ICON_DETAIL_KEYWORDS(formattedIconName),
`${formattedIconName} icon`,
"dashboard icon",
"service icon",
"application icon",
"tool icon",
"web dashboard",
"app directory",
],
icons: { icons: {
icon: iconImageUrl, icon: iconImageUrl,
}, },
abstract: `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.`, abstract: description,
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
}, },
openGraph: { openGraph: {
title: `${formattedIconName} Icon | Dashboard Icons`, title: title,
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.`, description,
type: "article", type: "article",
url: pageUrl, url: pageUrl,
authors: [authorName], authors: [authorName],
publishedTime: updateDate.toISOString(), publishedTime: updateDate.toISOString(),
modifiedTime: updateDate.toISOString(), modifiedTime: updateDate.toISOString(),
section: "Icons", section: "Icons",
tags: [formattedIconName, "dashboard icon", "service icon", "application icon", "tool icon", "web dashboard", "app directory"], tags: [formattedIconName, ...ICON_DETAIL_KEYWORDS(formattedIconName)],
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: `${formattedIconName} Icon | Dashboard Icons`, title: title,
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.`, description,
images: [iconImageUrl], images: [iconImageUrl],
}, },
alternates: { alternates: {
@@ -90,8 +96,8 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
} }
} }
export default async function IconPage({ params }: { params: Promise<{ icon: string }> }) { export default async function IconPage({ params }: { params: { icon: string } }) {
const { icon } = await params const { icon } = params
const iconsData = await getAllIcons() const iconsData = await getAllIcons()
const originalIconData = iconsData[icon] const originalIconData = iconsData[icon]
@@ -100,6 +106,26 @@ export default async function IconPage({ params }: { params: Promise<{ icon: str
} }
const authorData = await getAuthorData(originalIconData.update.author.id) const authorData = await getAuthorData(originalIconData.update.author.id)
const updateDate = new Date(originalIconData.update.timestamp)
const authorName = authorData.name || authorData.login
const formattedIconName = icon
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
return <IconDetails icon={icon} iconData={originalIconData} authorData={authorData} /> const imageSchema = getIconSchema(
formattedIconName,
icon,
authorName,
authorData.html_url,
updateDate.toISOString(),
Object.keys(iconsData).length,
)
return (
<>
<StructuredData data={imageSchema} id="image-schema" />
<IconDetails icon={icon} iconData={originalIconData} authorData={authorData} />
</>
)
} }

View File

@@ -1,4 +1,17 @@
import { BASE_URL } from "@/constants" import { StructuredData } from "@/components/structured-data"
import {
BASE_URL,
BROWSE_KEYWORDS,
DEFAULT_OG_IMAGE,
GITHUB_URL,
ORGANIZATION_NAME,
ORGANIZATION_SCHEMA,
SITE_NAME,
SITE_TAGLINE,
TITLE_SEPARATOR,
WEB_URL,
getBrowseDescription,
} from "@/constants"
import { getIconsArray } from "@/lib/api" import { getIconsArray } from "@/lib/api"
import type { Metadata } from "next" import type { Metadata } from "next"
import { IconSearch } from "./components/icon-search" import { IconSearch } from "./components/icon-search"
@@ -7,42 +20,28 @@ export async function generateMetadata(): Promise<Metadata> {
const icons = await getIconsArray() const icons = await getIconsArray()
const totalIcons = icons.length const totalIcons = icons.length
const title = `Browse Icons ${TITLE_SEPARATOR} ${SITE_NAME}`
const description = getBrowseDescription(totalIcons)
return { return {
title: "Browse Icons | Free Dashboard Icons", title,
description: `Search and browse through our collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`, description,
keywords: [ keywords: BROWSE_KEYWORDS,
"browse icons",
"dashboard icons",
"icon search",
"service icons",
"application icons",
"tool icons",
"web dashboard",
"app directory",
],
openGraph: { openGraph: {
title: "Browse Icons | Free Dashboard Icons", title: title,
description: `Search and browse through our collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`, description,
type: "website", type: "website",
url: `${BASE_URL}/icons`, url: `${WEB_URL}/icons`,
images: [ images: [DEFAULT_OG_IMAGE],
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Browse Dashboard Icons Collection",
type: "image/png",
},
],
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: "Browse Icons | Free Dashboard Icons", title: title,
description: `Search and browse through our collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`, description,
images: ["/og-image-browse.png"], images: [DEFAULT_OG_IMAGE.url],
}, },
alternates: { alternates: {
canonical: `${BASE_URL}/icons`, canonical: `${WEB_URL}/icons`,
}, },
} }
} }
@@ -51,20 +50,40 @@ export const dynamic = "force-static"
export default async function IconsPage() { export default async function IconsPage() {
const icons = await getIconsArray() const icons = await getIconsArray()
return (
<div className="isolate overflow-hidden">
<div className="py-8">
<div className="space-y-4 mb-8 mx-auto max-w-7xl">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold">Browse icons</h1>
<p className="text-muted-foreground">Search through our collection of {icons.length} beautiful icons.</p>
</div>
</div>
<IconSearch icons={icons} /> const gallerySchema = {
"@context": "https://schema.org",
"@type": "ImageGallery",
name: `${SITE_NAME} - Browse ${icons.length} Icons - ${SITE_TAGLINE}`,
description: getBrowseDescription(icons.length),
url: `${WEB_URL}/icons`,
numberOfItems: icons.length,
creator: {
"@type": "Organization",
name: ORGANIZATION_NAME,
url: GITHUB_URL,
},
}
return (
<>
<StructuredData data={gallerySchema} id="gallery-schema" />
<div className="isolate overflow-hidden">
<div className="py-8">
<div className="space-y-4 mb-8 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold">Icons</h1>
<p className="text-muted-foreground">
Search our collection of {icons.length} icons - {SITE_TAGLINE}.
</p>
</div>
</div>
<IconSearch icons={icons} />
</div>
</div> </div>
</div> </div>
</div> </>
) )
} }

View File

@@ -2,12 +2,26 @@ import { PostHogProvider } from "@/components/PostHogProvider"
import { Footer } from "@/components/footer" import { Footer } from "@/components/footer"
import { HeaderWrapper } from "@/components/header-wrapper" import { HeaderWrapper } from "@/components/header-wrapper"
import { LicenseNotice } from "@/components/license-notice" import { LicenseNotice } from "@/components/license-notice"
import { WebsiteStructuredData } from "@/components/structured-data"
import { getTotalIcons } from "@/lib/api" import { getTotalIcons } from "@/lib/api"
import type { Metadata, Viewport } from "next" import type { Metadata, Viewport } from "next"
import { Inter } from "next/font/google" import { Inter } from "next/font/google"
import { Toaster } from "sonner" import { Toaster } from "sonner"
import "./globals.css" import "./globals.css"
import { getDescription, websiteTitle } from "@/constants" import {
DEFAULT_KEYWORDS,
DEFAULT_OG_IMAGE,
GITHUB_URL,
ORGANIZATION_NAME,
ORGANIZATION_SCHEMA,
SITE_NAME,
SITE_TAGLINE,
WEB_URL,
getDescription,
getWebsiteSchema,
websiteFullTitle,
websiteTitle,
} from "@/constants"
import { ThemeProvider } from "./theme-provider" import { ThemeProvider } from "./theme-provider"
const inter = Inter({ const inter = Inter({
@@ -27,12 +41,16 @@ export const viewport: Viewport = {
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const { totalIcons } = await getTotalIcons() const { totalIcons } = await getTotalIcons()
const description = getDescription(totalIcons)
return { return {
metadataBase: new URL("https://dashboardicons.com"), metadataBase: new URL(WEB_URL),
title: websiteTitle, title: {
description: getDescription(totalIcons), default: websiteTitle,
keywords: ["dashboard icons", "service icons", "application icons", "tool icons", "web dashboard", "app directory"], template: `%s | ${websiteTitle}`,
},
description,
keywords: DEFAULT_KEYWORDS,
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
@@ -42,33 +60,23 @@ export async function generateMetadata(): Promise<Metadata> {
googleBot: "index, follow", googleBot: "index, follow",
}, },
openGraph: { openGraph: {
siteName: "Dashboard Icons", siteName: SITE_NAME,
type: "website", type: "website",
locale: "en_US", locale: "en_US",
title: websiteTitle, title: websiteFullTitle,
description: getDescription(totalIcons), description,
url: "https://dashboardicons.com", url: WEB_URL,
images: [ images: [DEFAULT_OG_IMAGE],
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Dashboard Icons",
type: "image/png",
},
],
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
site: "@homarr_app", title: websiteFullTitle,
creator: "@homarr_app", description,
title: websiteTitle, images: [DEFAULT_OG_IMAGE.url],
description: getDescription(totalIcons),
images: ["/og-image.png"],
}, },
applicationName: "Dashboard Icons", applicationName: SITE_NAME,
appleWebApp: { appleWebApp: {
title: "Dashboard Icons", title: SITE_NAME,
statusBarStyle: "default", statusBarStyle: "default",
capable: true, capable: true,
}, },
@@ -88,14 +96,26 @@ export async function generateMetadata(): Promise<Metadata> {
], ],
}, },
manifest: "/site.webmanifest", manifest: "/site.webmanifest",
authors: [{ name: ORGANIZATION_NAME, url: GITHUB_URL }],
creator: ORGANIZATION_NAME,
publisher: ORGANIZATION_NAME,
category: "Icons",
classification: "Dashboard Design Resources",
other: {
"revisit-after": "7 days",
},
} }
} }
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
const { totalIcons } = await getTotalIcons()
const websiteSchema = getWebsiteSchema(totalIcons)
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} antialiased bg-background flex flex-col min-h-screen`}> <body className={`${inter.variable} antialiased bg-background flex flex-col min-h-screen`}>
<PostHogProvider> <PostHogProvider>
<WebsiteStructuredData websiteSchema={websiteSchema} organizationSchema={ORGANIZATION_SCHEMA} />
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange> <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<HeaderWrapper /> <HeaderWrapper />
<main className="flex-grow">{children}</main> <main className="flex-grow">{children}</main>

View File

@@ -1,42 +1,50 @@
import { HeroSection } from "@/components/hero" import { HeroSection } from "@/components/hero"
import { RecentlyAddedIcons } from "@/components/recently-added-icons" import { RecentlyAddedIcons } from "@/components/recently-added-icons"
import { BASE_URL, REPO_NAME, getDescription, websiteTitle } from "@/constants" import {
BASE_URL,
DEFAULT_KEYWORDS,
DEFAULT_OG_IMAGE,
GITHUB_URL,
ORGANIZATION_NAME,
ORGANIZATION_SCHEMA,
REPO_NAME,
SITE_NAME,
SITE_TAGLINE,
WEB_URL,
getHomeDescription,
websiteFullTitle,
websiteTitle,
} from "@/constants"
import { getRecentlyAddedIcons, getTotalIcons } from "@/lib/api" import { getRecentlyAddedIcons, getTotalIcons } from "@/lib/api"
import type { Metadata } from "next" import type { Metadata } from "next"
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const { totalIcons } = await getTotalIcons() const { totalIcons } = await getTotalIcons()
const description = getHomeDescription(totalIcons)
return { return {
title: websiteTitle, title: websiteTitle,
description: getDescription(totalIcons), description,
keywords: ["dashboard icons", "service icons", "application icons", "tool icons", "web dashboard", "app directory"], keywords: DEFAULT_KEYWORDS,
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
}, },
openGraph: { openGraph: {
title: websiteTitle, title: websiteFullTitle,
description: getDescription(totalIcons), description,
type: "website", type: "website",
url: BASE_URL, url: WEB_URL,
images: [ images: [DEFAULT_OG_IMAGE],
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "Dashboard Icons",
},
],
}, },
twitter: { twitter: {
title: websiteTitle, title: websiteFullTitle,
description: getDescription(totalIcons), description,
card: "summary_large_image", card: "summary_large_image",
images: ["/og-image.png"], images: [DEFAULT_OG_IMAGE.url],
}, },
alternates: { alternates: {
canonical: BASE_URL, canonical: WEB_URL,
}, },
} }
} }
@@ -44,7 +52,7 @@ export async function generateMetadata(): Promise<Metadata> {
async function getGitHubStars() { async function getGitHubStars() {
const response = await fetch(`https://api.github.com/repos/${REPO_NAME}`) const response = await fetch(`https://api.github.com/repos/${REPO_NAME}`)
const data = await response.json() const data = await response.json()
console.log(`GitHub stars: ${data.stargazers_count}`) // TODO: Consider caching this result or fetching at build time to avoid rate limits.
return data.stargazers_count return data.stargazers_count
} }
@@ -54,9 +62,11 @@ export default async function Home() {
const stars = await getGitHubStars() const stars = await getGitHubStars()
return ( return (
<div className="flex flex-col min-h-screen"> <>
<HeroSection totalIcons={totalIcons} stars={stars} /> <div className="flex flex-col min-h-screen">
<RecentlyAddedIcons icons={recentIcons} /> <HeroSection totalIcons={totalIcons} stars={stars} />
</div> <RecentlyAddedIcons icons={recentIcons} />
</div>
</>
) )
} }

View File

@@ -1,3 +0,0 @@
User-Agent: *
Allow: /
Sitemap: https://dashboardicons.com/sitemap.xml

View File

@@ -0,0 +1,22 @@
type StructuredDataProps = {
data: Record<string, unknown>
id?: string
}
export const StructuredData = ({ data, id }: StructuredDataProps) => {
return <script id={id} type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }} />
}
type WebsiteStructuredDataProps = {
websiteSchema: Record<string, unknown>
organizationSchema: Record<string, unknown>
}
export const WebsiteStructuredData = ({ websiteSchema, organizationSchema }: WebsiteStructuredDataProps) => {
return (
<>
<StructuredData data={websiteSchema} id="website-schema" />
<StructuredData data={organizationSchema} id="organization-schema" />
</>
)
}

View File

@@ -4,7 +4,126 @@ export const METADATA_URL = "https://raw.githubusercontent.com/homarr-labs/dashb
export const WEB_URL = "https://dashboardicons.com" export const WEB_URL = "https://dashboardicons.com"
export const REPO_NAME = "homarr-labs/dashboard-icons" export const REPO_NAME = "homarr-labs/dashboard-icons"
export const getDescription = (totalIcons: number) => // Site-wide metadata constants
`A collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.` export const SITE_NAME = "Dashboard Icons"
export const TITLE_SEPARATOR = " | "
export const SITE_TAGLINE = "Your definitive source for dashboard icons"
export const ORGANIZATION_NAME = "Homarr Labs"
export const websiteTitle = "Free Dashboard Icons - Download High-Quality UI & App Icons" export const getDescription = (totalIcons: number) =>
`A curated collection of ${totalIcons} free icons for dashboards and app directories. Available in SVG, PNG, and WEBP formats. ${SITE_TAGLINE}.`
export const getHomeDescription = (totalIcons: number) =>
`Discover our curated collection of ${totalIcons} icons designed specifically for dashboards and app directories. ${SITE_TAGLINE}.`
export const getBrowseDescription = (totalIcons: number) =>
`Browse, search and download from our collection of ${totalIcons} curated icons. All icons available in SVG, PNG, and WEBP formats. ${SITE_TAGLINE}.`
export const getIconDescription = (iconName: string, totalIcons: number) =>
`Download the ${iconName} icon in SVG, PNG, and WEBP formats. Part of our curated collection of ${totalIcons} free icons for dashboards. ${SITE_TAGLINE}.`
export const websiteTitle = `${SITE_NAME} ${TITLE_SEPARATOR} Free, Curated Icons for Apps & Services`
export const websiteFullTitle = `${SITE_NAME} ${TITLE_SEPARATOR} Free, Curated Icons for Apps & Services ${TITLE_SEPARATOR} ${SITE_TAGLINE}`
// Various keyword sets for different pages
export const DEFAULT_KEYWORDS = [
"dashboard icons",
"app icons",
"service icons",
"curated icons",
"free icons",
"SVG icons",
"web dashboard",
"app directory",
]
export const BROWSE_KEYWORDS = [
"browse icons",
"search icons",
"download icons",
"minimal icons",
"dashboard design",
"UI icons",
...DEFAULT_KEYWORDS,
]
// Add format-specific keywords
export const ICON_DETAIL_KEYWORDS = (iconName: string): string[] => [
`${iconName} icon`, // e.g., "Homarr icon"
`${iconName} logo`, // e.g., "Homarr logo"
`${iconName} svg icon`, // e.g., "Homarr svg icon"
`${iconName} png icon`, // e.g., "Homarr png icon"
`${iconName} webp icon`, // e.g., "Homarr webp icon"
`${iconName} download`, // e.g., "Homarr download"
`${iconName} dashboard icon`, // e.g., "Homarr dashboard icon"
...DEFAULT_KEYWORDS,
]
// Core structured data for the website (JSON-LD)
export const getWebsiteSchema = (totalIcons: number) => ({
"@context": "https://schema.org",
"@type": "WebSite",
name: SITE_NAME,
url: WEB_URL,
description: getDescription(totalIcons),
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: `${WEB_URL}/icons?q={search_term_string}`,
},
"query-input": "required name=search_term_string",
},
slogan: SITE_TAGLINE,
})
// Organization schema
export const ORGANIZATION_SCHEMA = {
"@context": "https://schema.org",
"@type": "Organization",
name: ORGANIZATION_NAME,
url: `https://github.com/${REPO_NAME}`,
logo: `${WEB_URL}/og-image.png`,
sameAs: [`https://github.com/${REPO_NAME}`, "https://homarr.dev"],
slogan: SITE_TAGLINE,
}
// Social media
export const GITHUB_URL = `https://github.com/${REPO_NAME}`
// Image schemas
export const getIconSchema = (
iconName: string,
iconId: string,
authorName: string,
authorUrl: string,
updateDate: string,
totalIcons: number,
) => ({
"@context": "https://schema.org",
"@type": "ImageObject",
name: `${iconName} Icon`,
description: getIconDescription(iconName, totalIcons),
contentUrl: `${BASE_URL}/png/${iconId}.png`,
thumbnailUrl: `${BASE_URL}/png/${iconId}.png`,
uploadDate: updateDate,
author: {
"@type": "Person",
name: authorName,
url: authorUrl,
},
encodingFormat: ["image/png", "image/svg+xml", "image/webp"],
contentSize: "Variable",
representativeOfPage: true,
creditText: `Icon contributed by ${authorName} to the ${SITE_NAME} collection by ${ORGANIZATION_NAME}`,
embedUrl: `${WEB_URL}/icons/${iconId}`,
})
// OpenGraph defaults
export const DEFAULT_OG_IMAGE = {
url: "/og-image.png",
width: 1200,
height: 630,
alt: `${SITE_NAME} - ${SITE_TAGLINE}`,
type: "image/png",
}