From b4c4fe26349ce703156e1f724fb4dde3571ec104 Mon Sep 17 00:00:00 2001 From: Bjorn Lammers Date: Wed, 23 Apr 2025 01:07:45 +0200 Subject: [PATCH] feat(web): improve site metadata structure --- web/src/app/icons/[icon]/page.tsx | 62 +++++++++++----- web/src/app/icons/page.tsx | 95 +++++++++++++----------- web/src/app/layout.tsx | 62 +++++++++------- web/src/app/page.tsx | 67 +++++++++++------ web/src/constants.ts | 118 +++++++++++++++++++++++++++++- 5 files changed, 293 insertions(+), 111 deletions(-) diff --git a/web/src/app/icons/[icon]/page.tsx b/web/src/app/icons/[icon]/page.tsx index ca00e909..2785acbc 100644 --- a/web/src/app/icons/[icon]/page.tsx +++ b/web/src/app/icons/[icon]/page.tsx @@ -1,7 +1,8 @@ import { IconDetails } from "@/components/icon-details" -import { BASE_URL, WEB_URL } from "@/constants" +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 @@ -40,43 +41,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. Available for free in our collection of ${totalIcons} icons for dashboards and applications.`, + 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. Available for free in our collection of ${totalIcons} icons for dashboards and applications.`, + abstract: description, robots: { index: true, follow: true, }, openGraph: { - title: `${formattedIconName} Icon | Dashboard Icons`, - description: `Download the ${formattedIconName} icon in SVG, PNG, and WEBP formats. Available for free in our collection of ${totalIcons} icons for dashboards and applications.`, + title: fullTitle, + 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. Available for free in our collection of ${totalIcons} icons for dashboards and applications.`, + title: fullTitle, + description, images: [iconImageUrl], }, alternates: { @@ -87,6 +84,9 @@ export async function generateMetadata({ params, searchParams }: Props, parent: webp: `${BASE_URL}/webp/${icon}.webp`, }, }, + other: { + "revisit-after": "7 days", + } } } @@ -100,6 +100,28 @@ 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 b429c88d..171efc8a 100644 --- a/web/src/app/icons/page.tsx +++ b/web/src/app/icons/page.tsx @@ -1,49 +1,39 @@ -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 { IconSearch } from "./components/icon-search" +import Script from "next/script" 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: `Browse our collection of ${totalIcons} icons for dashboards and applications. Available in SVG, PNG, and WEBP formats.`, - 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: `Browse our collection of ${totalIcons} icons for dashboards and applications. Available in SVG, PNG, and WEBP formats.`, + title: `Browse Icons ${TITLE_SEPARATOR} ${SITE_NAME} ${TITLE_SEPARATOR} ${SITE_TAGLINE}`, + 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: `Browse our collection of ${totalIcons} icons for dashboards and applications. Available in SVG, PNG, and WEBP formats.`, - images: ["/og-image-browse.png"], + title: `Browse Icons ${TITLE_SEPARATOR} ${SITE_NAME} ${TITLE_SEPARATOR} ${SITE_TAGLINE}`, + description, + images: [DEFAULT_OG_IMAGE.url], }, alternates: { - canonical: `${BASE_URL}/icons`, + canonical: `${WEB_URL}/icons`, }, + other: { + "revisit-after": "3 days", + } } } @@ -51,20 +41,43 @@ export const dynamic = "force-static" export default async function IconsPage() { const icons = await getIconsArray() - return ( -
-
-
-
-
-

Icons

-

Search our collection of {icons.length} 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..aec3e3e4 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -7,8 +7,9 @@ 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" +import Script from "next/script" const inter = Inter({ variable: "--font-inter", @@ -27,12 +28,13 @@ export const viewport: Viewport = { export async function generateMetadata(): Promise { const { totalIcons } = await getTotalIcons() + const description = getDescription(totalIcons) return { - metadataBase: new URL("https://dashboardicons.com"), + metadataBase: new URL(WEB_URL), 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, @@ -42,33 +44,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,12 +80,32 @@ export async function generateMetadata(): Promise { ], }, manifest: "/site.webmanifest", + authors: [{ name: ORGANIZATION_NAME, url: GITHUB_URL }], + creator: ORGANIZATION_NAME, + publisher: ORGANIZATION_NAME, + archives: [`${WEB_URL}/icons`], + 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 ( + + + + diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 2682ff6c..c498d9f3 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,42 +1,37 @@ 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" +import Script from "next/script" 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, }, } } @@ -53,10 +48,38 @@ export default async function Home() { const recentIcons = await getRecentlyAddedIcons(10) const stars = await getGitHubStars() + // Collection schema for the homepage + const collectionSchema = { + "@context": "https://schema.org", + "@type": "CollectionPage", + "name": `${SITE_NAME} Collection - ${SITE_TAGLINE}`, + "description": getHomeDescription(totalIcons), + "url": WEB_URL, + "numberOfItems": totalIcons, + "mainEntity": { + "@type": "CreativeWork", + "name": SITE_NAME, + "description": getHomeDescription(totalIcons), + "creator": { + "@type": "Organization", + "name": ORGANIZATION_NAME, + "url": GITHUB_URL + } + } + } + return ( -
- - -
+ <> + + +
+ + +
+ ) } diff --git a/web/src/constants.ts b/web/src/constants.ts index acb3d62b..5c90b6e3 100644 --- a/web/src/constants.ts +++ b/web/src/constants.ts @@ -4,7 +4,119 @@ 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) => - `Collection of ${totalIcons} icons for applications, services, and tools - designed 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 +] + +export const ICON_DETAIL_KEYWORDS = (iconName: string) => [ + `${iconName} icon`, + `${iconName} logo`, + `${iconName} svg`, + `${iconName} download`, + `${iconName} 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" +}