diff --git a/web/public/robots.txt b/web/public/robots.txt
new file mode 100644
index 00000000..17c23e78
--- /dev/null
+++ b/web/public/robots.txt
@@ -0,0 +1,6 @@
+# Allow all user agents
+User-agent: *
+Allow: /
+
+# Sitemap location (adjust if needed)
+Sitemap: https://dashboardicons.com/sitemap.xml
\ No newline at end of file
diff --git a/web/src/app/icons/[icon]/opengraph-image.tsx b/web/src/app/icons/[icon]/opengraph-image.tsx
index 03e2ab49..82d62c5f 100644
--- a/web/src/app/icons/[icon]/opengraph-image.tsx
+++ b/web/src/app/icons/[icon]/opengraph-image.tsx
@@ -2,6 +2,12 @@ import { readFile } from "node:fs/promises"
import { join } from "node:path"
import { getAllIcons } from "@/lib/api"
import { ImageResponse } from "next/og"
+import {
+ SITE_NAME,
+ SITE_TAGLINE,
+ getIconDescription,
+ WEB_URL
+} from "@/constants"
export const dynamic = "force-static"
@@ -32,10 +38,9 @@ export default async function Image({ params }: { params: { icon: string } }) {
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)
} 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
@@ -52,9 +57,9 @@ export default async function Image({ params }: { params: { icon: string } }) {
position: "relative",
fontFamily: "Inter, system-ui, sans-serif",
overflow: "hidden",
- backgroundColor: "white",
+ backgroundColor: "#0f172a", // Dark background (slate-900)
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",
}}
>
@@ -67,7 +72,7 @@ export default async function Image({ params }: { params: { 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.15) 0%, rgba(59, 130, 246, 0.15) 100%)",
filter: "blur(80px)",
zIndex: 2,
}}
@@ -80,7 +85,7 @@ export default async function Image({ params }: { params: { 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.15) 0%, rgba(234, 88, 12, 0.15) 100%)",
filter: "blur(100px)",
zIndex: 2,
}}
@@ -109,8 +114,8 @@ export default async function Image({ params }: { params: { icon: string } }) {
width: 320,
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)",
+ background: "#1e293b", // Dark container (slate-800)
+ boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1)",
padding: 30,
flexShrink: 0,
position: "relative",
@@ -121,7 +126,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
style={{
position: "absolute",
inset: 0,
- background: "linear-gradient(145deg, #ffffff 0%, #f8fafc 100%)",
+ background: "linear-gradient(145deg, #1e293b 0%, #0f172a 100%)",
zIndex: 0,
}}
/>
@@ -134,7 +139,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
objectFit: "contain",
position: "relative",
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))",
}}
/>
@@ -154,7 +159,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex",
fontSize: 64,
fontWeight: 800,
- color: "#0f172a",
+ color: "#f8fafc", // Light text for dark background (slate-50)
lineHeight: 1.1,
letterSpacing: "-0.02em",
}}
@@ -167,14 +172,14 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex",
fontSize: 32,
fontWeight: 500,
- color: "#64748b",
+ color: "#94a3b8", // Muted text (slate-400)
lineHeight: 1.4,
position: "relative",
paddingLeft: 16,
- borderLeft: "4px solid #94a3b8",
+ borderLeft: "4px solid #64748b", // slate-500
}}
>
- Amongst {totalIcons} other high-quality dashboard icons
+ {getIconDescription(formattedIconName, totalIcons)}
{format}
@@ -219,8 +224,8 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex",
alignItems: "center",
justifyContent: "center",
- background: "#ffffff",
- borderTop: "2px solid rgba(0, 0, 0, 0.05)",
+ background: "#1e293b", // slate-800
+ borderTop: "2px solid rgba(255, 255, 255, 0.1)",
zIndex: 20,
}}
>
@@ -229,7 +234,7 @@ export default async function Image({ params }: { params: { icon: string } }) {
display: "flex",
fontSize: 24,
fontWeight: 600,
- color: "#334155",
+ color: "#e2e8f0", // slate-200
alignItems: "center",
gap: 10,
}}
@@ -239,11 +244,11 @@ export default async function Image({ params }: { params: { icon: string } }) {
width: 8,
height: 8,
borderRadius: "50%",
- backgroundColor: "#3b82f6",
+ backgroundColor: "#3b82f6", // blue-500
marginRight: 4,
}}
/>
- dashboardicons.com
+ {WEB_URL.replace("https://", "")}
,
diff --git a/web/src/app/icons/[icon]/page.tsx b/web/src/app/icons/[icon]/page.tsx
index 186ca02f..ce3a50bd 100644
--- a/web/src/app/icons/[icon]/page.tsx
+++ b/web/src/app/icons/[icon]/page.tsx
@@ -1,7 +1,9 @@
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 type { Metadata, ResolvingMetadata } from "next"
+import Script from "next/script"
import { notFound } from "next/navigation"
export const dynamicParams = false
@@ -16,12 +18,12 @@ export async function generateStaticParams() {
export const dynamic = "force-static"
type Props = {
- params: Promise<{ icon: string }>
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>
+ params: { icon: string }
+ searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise {
- const { icon } = await params
+ const { icon } = params
const iconsData = await getAllIcons()
if (!iconsData[icon]) {
notFound()
@@ -31,8 +33,6 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
const updateDate = new Date(iconsData[icon].update.timestamp)
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 pageUrl = `${WEB_URL}/icons/${icon}`
const formattedIconName = icon
@@ -40,43 +40,39 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.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 {
- 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.`,
+ title,
+ description,
assets: [iconImageUrl],
- category: "icons",
- keywords: [
- `${formattedIconName} icon`,
- "dashboard icon",
- "service icon",
- "application icon",
- "tool icon",
- "web dashboard",
- "app directory",
- ],
+ category: "Icons",
+ keywords: ICON_DETAIL_KEYWORDS(formattedIconName),
icons: {
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: {
index: true,
follow: true,
},
openGraph: {
- 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.`,
+ title: title,
+ description,
type: "article",
url: pageUrl,
authors: [authorName],
publishedTime: updateDate.toISOString(),
modifiedTime: updateDate.toISOString(),
section: "Icons",
- tags: [formattedIconName, "dashboard icon", "service icon", "application icon", "tool icon", "web dashboard", "app directory"],
+ tags: [formattedIconName, ...ICON_DETAIL_KEYWORDS(formattedIconName)],
},
twitter: {
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.`,
+ title: title,
+ description,
images: [iconImageUrl],
},
alternates: {
@@ -90,8 +86,8 @@ export async function generateMetadata({ params, searchParams }: Props, parent:
}
}
-export default async function IconPage({ params }: { params: Promise<{ icon: string }> }) {
- const { icon } = await params
+export default async function IconPage({ params }: { params: { icon: string } }) {
+ const { icon } = params
const iconsData = await getAllIcons()
const originalIconData = iconsData[icon]
@@ -100,6 +96,26 @@ export default async function IconPage({ params }: { params: Promise<{ icon: str
}
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
+ const imageSchema = getIconSchema(
+ formattedIconName,
+ icon,
+ authorName,
+ authorData.html_url,
+ updateDate.toISOString(),
+ Object.keys(iconsData).length
+ )
+
+ return (
+ <>
+
+
+ >
+ )
}
diff --git a/web/src/app/icons/page.tsx b/web/src/app/icons/page.tsx
index a218be25..1aa7d5f5 100644
--- a/web/src/app/icons/page.tsx
+++ b/web/src/app/icons/page.tsx
@@ -1,48 +1,35 @@
-import { BASE_URL } from "@/constants"
+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 type { Metadata } from "next"
+import { StructuredData } from "@/components/structured-data"
import { IconSearch } from "./components/icon-search"
export async function generateMetadata(): Promise {
const icons = await getIconsArray()
const totalIcons = icons.length
+ const title = `Browse Icons ${TITLE_SEPARATOR} ${SITE_NAME}`
+ const description = getBrowseDescription(totalIcons)
+
return {
- title: "Browse Icons | Free Dashboard Icons",
- description: `Search and browse through our collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`,
- keywords: [
- "browse icons",
- "dashboard icons",
- "icon search",
- "service icons",
- "application icons",
- "tool icons",
- "web dashboard",
- "app directory",
- ],
+ title,
+ description,
+ keywords: BROWSE_KEYWORDS,
openGraph: {
- title: "Browse Icons | Free Dashboard Icons",
- description: `Search and browse through our collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`,
+ title: title,
+ description,
type: "website",
- url: `${BASE_URL}/icons`,
- images: [
- {
- url: "/og-image.png",
- width: 1200,
- height: 630,
- alt: "Browse Dashboard Icons Collection",
- type: "image/png",
- },
- ],
+ url: `${WEB_URL}/icons`,
+ images: [DEFAULT_OG_IMAGE],
},
twitter: {
card: "summary_large_image",
- title: "Browse Icons | Free Dashboard Icons",
- description: `Search and browse through our collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`,
- images: ["/og-image-browse.png"],
+ title: title,
+ description,
+ images: [DEFAULT_OG_IMAGE.url],
},
alternates: {
- canonical: `${BASE_URL}/icons`,
+ canonical: `${WEB_URL}/icons`,
},
}
}
@@ -51,20 +38,38 @@ export const dynamic = "force-static"
export default async function IconsPage() {
const icons = await getIconsArray()
- return (
-
-
-
-
-
-
Browse icons
-
Search through our collection of {icons.length} beautiful 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 (
+ <>
+
+
+
+
+
+
+
Icons
+
Search our collection of {icons.length} icons - {SITE_TAGLINE}.
+
+
+
+
+
-
+ >
)
}
diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx
index 910ef52b..f5e3dd96 100644
--- a/web/src/app/layout.tsx
+++ b/web/src/app/layout.tsx
@@ -2,12 +2,13 @@ import { PostHogProvider } from "@/components/PostHogProvider"
import { Footer } from "@/components/footer"
import { HeaderWrapper } from "@/components/header-wrapper"
import { LicenseNotice } from "@/components/license-notice"
+import { WebsiteStructuredData } from "@/components/structured-data"
import { getTotalIcons } from "@/lib/api"
import type { Metadata, Viewport } from "next"
import { Inter } from "next/font/google"
import { Toaster } from "sonner"
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"
const inter = Inter({
@@ -27,12 +28,16 @@ export const viewport: Viewport = {
export async function generateMetadata(): Promise
{
const { totalIcons } = await getTotalIcons()
+ const description = getDescription(totalIcons)
return {
- metadataBase: new URL("https://dashboardicons.com"),
- title: websiteTitle,
- description: getDescription(totalIcons),
- keywords: ["dashboard icons", "service icons", "application icons", "tool icons", "web dashboard", "app directory"],
+ metadataBase: new URL(WEB_URL),
+ title: {
+ default: websiteTitle,
+ template: `%s | ${websiteTitle}`,
+ },
+ description,
+ keywords: DEFAULT_KEYWORDS,
robots: {
index: true,
follow: true,
@@ -42,33 +47,23 @@ export async function generateMetadata(): Promise {
googleBot: "index, follow",
},
openGraph: {
- siteName: "Dashboard Icons",
+ siteName: SITE_NAME,
type: "website",
locale: "en_US",
- title: websiteTitle,
- description: getDescription(totalIcons),
- url: "https://dashboardicons.com",
- images: [
- {
- url: "/og-image.png",
- width: 1200,
- height: 630,
- alt: "Dashboard Icons",
- type: "image/png",
- },
- ],
+ title: websiteFullTitle,
+ description,
+ url: WEB_URL,
+ images: [DEFAULT_OG_IMAGE],
},
twitter: {
card: "summary_large_image",
- site: "@homarr_app",
- creator: "@homarr_app",
- title: websiteTitle,
- description: getDescription(totalIcons),
- images: ["/og-image.png"],
+ title: websiteFullTitle,
+ description,
+ images: [DEFAULT_OG_IMAGE.url],
},
- applicationName: "Dashboard Icons",
+ applicationName: SITE_NAME,
appleWebApp: {
- title: "Dashboard Icons",
+ title: SITE_NAME,
statusBarStyle: "default",
capable: true,
},
@@ -88,14 +83,29 @@ export async function generateMetadata(): Promise {
],
},
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 (
+
{children}
diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx
index 2682ff6c..70f008e1 100644
--- a/web/src/app/page.tsx
+++ b/web/src/app/page.tsx
@@ -1,42 +1,36 @@
import { HeroSection } from "@/components/hero"
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, SITE_NAME, SITE_TAGLINE, WEB_URL, REPO_NAME, getHomeDescription, websiteFullTitle, websiteTitle } from "@/constants"
import { getRecentlyAddedIcons, getTotalIcons } from "@/lib/api"
import type { Metadata } from "next"
export async function generateMetadata(): Promise {
const { totalIcons } = await getTotalIcons()
+ const description = getHomeDescription(totalIcons)
return {
title: websiteTitle,
- description: getDescription(totalIcons),
- keywords: ["dashboard icons", "service icons", "application icons", "tool icons", "web dashboard", "app directory"],
+ description,
+ keywords: DEFAULT_KEYWORDS,
robots: {
index: true,
follow: true,
},
openGraph: {
- title: websiteTitle,
- description: getDescription(totalIcons),
+ title: websiteFullTitle,
+ description,
type: "website",
- url: BASE_URL,
- images: [
- {
- url: "/og-image.png",
- width: 1200,
- height: 630,
- alt: "Dashboard Icons",
- },
- ],
+ url: WEB_URL,
+ images: [DEFAULT_OG_IMAGE],
},
twitter: {
- title: websiteTitle,
- description: getDescription(totalIcons),
+ title: websiteFullTitle,
+ description,
card: "summary_large_image",
- images: ["/og-image.png"],
+ images: [DEFAULT_OG_IMAGE.url],
},
alternates: {
- canonical: BASE_URL,
+ canonical: WEB_URL,
},
}
}
@@ -44,7 +38,7 @@ export async function generateMetadata(): Promise {
async function getGitHubStars() {
const response = await fetch(`https://api.github.com/repos/${REPO_NAME}`)
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
}
@@ -54,9 +48,11 @@ export default async function Home() {
const stars = await getGitHubStars()
return (
-
-
-
-
+ <>
+
+
+
+
+ >
)
}
diff --git a/web/src/app/robots.txt b/web/src/app/robots.txt
deleted file mode 100644
index d52321a0..00000000
--- a/web/src/app/robots.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-User-Agent: *
-Allow: /
-Sitemap: https://dashboardicons.com/sitemap.xml
\ No newline at end of file
diff --git a/web/src/components/structured-data.tsx b/web/src/components/structured-data.tsx
new file mode 100644
index 00000000..dc1593eb
--- /dev/null
+++ b/web/src/components/structured-data.tsx
@@ -0,0 +1,31 @@
+type StructuredDataProps = {
+ data: any
+ id?: string
+}
+
+export const StructuredData = ({ data, id }: StructuredDataProps) => {
+ return (
+
+ )
+}
+
+type WebsiteStructuredDataProps = {
+ websiteSchema: any
+ organizationSchema: any
+}
+
+export const WebsiteStructuredData = ({
+ websiteSchema,
+ organizationSchema
+}: WebsiteStructuredDataProps) => {
+ return (
+ <>
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/web/src/constants.ts b/web/src/constants.ts
index 56f09708..f9aa092c 100644
--- a/web/src/constants.ts
+++ b/web/src/constants.ts
@@ -4,7 +4,122 @@ export const METADATA_URL = "https://raw.githubusercontent.com/homarr-labs/dashb
export const WEB_URL = "https://dashboardicons.com"
export const REPO_NAME = "homarr-labs/dashboard-icons"
-export const getDescription = (totalIcons: number) =>
- `A collection of ${totalIcons} curated icons for services, applications and tools, designed specifically for dashboards and app directories.`
+// Site-wide metadata constants
+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"
+}