mirror of
				https://github.com/walkxcode/dashboard-icons.git
				synced 2025-10-26 13:09:04 +08:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			icons/add-
			...
			feat/struc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ee297afbf4 | ||
|   | 40482771fa | ||
|   | 34fef44222 | ||
|   | 3e2709e7a8 | ||
|   | 245033befc | ||
|   | 9949f663eb | ||
|   | a579d41f45 | 
| @@ -22,6 +22,9 @@ | ||||
| 			"recommended": true, | ||||
| 			"suspicious": { | ||||
| 				"noArrayIndexKey": "off" | ||||
| 			}, | ||||
| 			"security": { | ||||
| 				"noDangerouslySetInnerHtml": "off" | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|   | ||||
							
								
								
									
										6
									
								
								web/public/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								web/public/robots.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| # Allow all user agents | ||||
| User-agent: * | ||||
| Allow: / | ||||
|  | ||||
| # Sitemap location (adjust if needed) | ||||
| Sitemap: https://dashboardicons.com/sitemap.xml | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { readFile } from "node:fs/promises" | ||||
| import { join } from "node:path" | ||||
| import { SITE_NAME, SITE_TAGLINE, WEB_URL, getIconDescription } from "@/constants" | ||||
| import { getAllIcons } from "@/lib/api" | ||||
| import { ImageResponse } from "next/og" | ||||
|  | ||||
| @@ -32,10 +33,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 +52,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 +67,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 +80,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 +109,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 +121,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 +134,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))", | ||||
| 						}} | ||||
| 					/> | ||||
| 				</div> | ||||
| @@ -154,7 +154,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 +167,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)} | ||||
| 					</div> | ||||
|  | ||||
| 					<div | ||||
| @@ -191,14 +191,14 @@ export default async function Image({ params }: { params: { icon: string } }) { | ||||
| 									display: "flex", | ||||
| 									alignItems: "center", | ||||
| 									justifyContent: "center", | ||||
| 									backgroundColor: "#f1f5f9", | ||||
| 									color: "#475569", | ||||
| 									border: "2px solid #e2e8f0", | ||||
| 									backgroundColor: "#334155", // slate-700 | ||||
| 									color: "#e2e8f0", // slate-200 | ||||
| 									border: "2px solid #475569", // slate-600 | ||||
| 									borderRadius: 12, | ||||
| 									padding: "8px 16px", | ||||
| 									fontSize: 18, | ||||
| 									fontWeight: 600, | ||||
| 									boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)", | ||||
| 									boxShadow: "0 1px 2px rgba(0, 0, 0, 0.2)", | ||||
| 								}} | ||||
| 							> | ||||
| 								{format} | ||||
| @@ -219,8 +219,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 +229,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 +239,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://", "")} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div>, | ||||
|   | ||||
| @@ -1,8 +1,20 @@ | ||||
| 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 { notFound } from "next/navigation" | ||||
| import Script from "next/script" | ||||
|  | ||||
| export const dynamicParams = false | ||||
|  | ||||
| @@ -16,12 +28,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<Metadata> { | ||||
| 	const { icon } = await params | ||||
| 	const { icon } = params | ||||
| 	const iconsData = await getAllIcons() | ||||
| 	if (!iconsData[icon]) { | ||||
| 		notFound() | ||||
| @@ -31,8 +43,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 +50,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 +96,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 +106,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 <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} /> | ||||
| 		</> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -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 type { Metadata } from "next" | ||||
| import { IconSearch } from "./components/icon-search" | ||||
| @@ -7,42 +20,28 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
| 	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 +50,40 @@ export const dynamic = "force-static" | ||||
|  | ||||
| export default async function IconsPage() { | ||||
| 	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> | ||||
| 		</> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -2,12 +2,26 @@ 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 +41,16 @@ export const viewport: Viewport = { | ||||
|  | ||||
| export async function generateMetadata(): Promise<Metadata> { | ||||
| 	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 +60,23 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
| 			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 +96,26 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
| 			], | ||||
| 		}, | ||||
| 		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 ( | ||||
| 		<html lang="en" suppressHydrationWarning> | ||||
| 			<body className={`${inter.variable} antialiased bg-background flex flex-col min-h-screen`}> | ||||
| 				<PostHogProvider> | ||||
| 					<WebsiteStructuredData websiteSchema={websiteSchema} organizationSchema={ORGANIZATION_SCHEMA} /> | ||||
| 					<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange> | ||||
| 						<HeaderWrapper /> | ||||
| 						<main className="flex-grow">{children}</main> | ||||
|   | ||||
| @@ -1,42 +1,50 @@ | ||||
| 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, | ||||
| 	REPO_NAME, | ||||
| 	SITE_NAME, | ||||
| 	SITE_TAGLINE, | ||||
| 	WEB_URL, | ||||
| 	getHomeDescription, | ||||
| 	websiteFullTitle, | ||||
| 	websiteTitle, | ||||
| } from "@/constants" | ||||
| import { getRecentlyAddedIcons, getTotalIcons } from "@/lib/api" | ||||
| import type { Metadata } from "next" | ||||
|  | ||||
| export async function generateMetadata(): Promise<Metadata> { | ||||
| 	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 +52,7 @@ export async function generateMetadata(): Promise<Metadata> { | ||||
| 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 +62,11 @@ export default async function Home() { | ||||
| 	const stars = await getGitHubStars() | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="flex flex-col min-h-screen"> | ||||
| 			<HeroSection totalIcons={totalIcons} stars={stars} /> | ||||
| 			<RecentlyAddedIcons icons={recentIcons} /> | ||||
| 		</div> | ||||
| 		<> | ||||
| 			<div className="flex flex-col min-h-screen"> | ||||
| 				<HeroSection totalIcons={totalIcons} stars={stars} /> | ||||
| 				<RecentlyAddedIcons icons={recentIcons} /> | ||||
| 			</div> | ||||
| 		</> | ||||
| 	) | ||||
| } | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| User-Agent: * | ||||
| Allow: / | ||||
| Sitemap: https://dashboardicons.com/sitemap.xml | ||||
							
								
								
									
										22
									
								
								web/src/components/structured-data.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								web/src/components/structured-data.tsx
									
									
									
									
									
										Normal 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" /> | ||||
| 		</> | ||||
| 	) | ||||
| } | ||||
| @@ -4,7 +4,126 @@ 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", | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user