Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions web/public/llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Dashboard Icons
> Free curated icons and logos for dashboards and app directories.

## Pages
- / — Homepage with search and recently added icons
- /icons — Browse and search the full icon collection
- /community — Community-submitted icons awaiting review
- /license — License and trademark policy
- /icons/{slug} — Download individual native icon (SVG, PNG, WEBP)
- /icons/external/{slug} — Download external source icons
- /community/{slug} — Community icon detail pages

## Facts
- License: CC BY 4.0 for native Dashboard Icons
- Maintainer: Homarr Labs
- CDN: cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons
- GitHub: github.com/homarr-labs/dashboard-icons
- Formats: SVG, PNG, WEBP
- Contact: homarr-labs@proton.me
18 changes: 6 additions & 12 deletions web/public/site.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,12 @@
"short_name": "DashIcons",
"description": "A collection of curated icons for services, applications and tools, designed specifically for dashboards and app directories.",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
],
"theme_color": "#FA5252",
"background_color": "#1B1B1D",
Expand Down
66 changes: 32 additions & 34 deletions web/src/app/community/[icon]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Metadata, ResolvingMetadata } from "next"
import { notFound, permanentRedirect } from "next/navigation"
import { IconDetails } from "@/components/icon-details"
import { JsonLd } from "@/components/seo/json-ld"
import { BASE_URL, WEB_URL } from "@/constants"
import { computeRelatedIcons, getAllIcons, getAuthorData } from "@/lib/api"
import { getCommunityGalleryRecord, getCommunitySubmissionByName, getCommunitySubmissions } from "@/lib/community"
import { buildIconPageGraph } from "@/lib/seo/schemas"

function isIconAddedToCollection(
record: Awaited<ReturnType<typeof getCommunityGalleryRecord>>,
Expand Down Expand Up @@ -102,6 +104,13 @@ export async function generateMetadata({ params }: Props, _parent: ResolvingMeta
siteName: "Dashboard Icons",
locale: "en_US",
images: [
{
url: `${WEB_URL}/og/community/${icon}`,
width: 1200,
height: 630,
alt: `${formattedIconName} community icon`,
type: "image/png",
},
{
url: mainIconUrl,
width: 512,
Expand All @@ -115,7 +124,7 @@ export async function generateMetadata({ params }: Props, _parent: ResolvingMeta
card: "summary_large_image",
title: `${formattedIconName} Icon & Logo (Community)`,
description: `Download the ${formattedIconName} community-submitted icon and logo. Part of a collection of ${totalIcons} community icons and logos awaiting review and addition to the Dashboard Icons collection.`,
images: [mainIconUrl],
images: [`${WEB_URL}/og/community/${icon}`],
},
alternates: {
canonical: `${WEB_URL}/community/${icon}`,
Expand Down Expand Up @@ -251,41 +260,30 @@ export default async function CommunityIconPage({ params }: { params: Promise<{
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")

const pageUrl = `${WEB_URL}/community/${icon}`
const pageDescription = `Download the ${formattedName} community-submitted icon and logo. Part of a collection of community icons awaiting review and addition to the Dashboard Icons collection.`
const authorName = authorData.name || authorData.login

return (
<>
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "ImageObject",
contentUrl: mainIconUrl,
license: "https://creativecommons.org/licenses/by/4.0/",
acquireLicensePage: `${WEB_URL}/license`,
creditText: `Icon by ${authorData.name || authorData.login}`,
copyrightNotice: "© Homarr Labs",
creator: {
"@type": "Person",
name: authorData.name || authorData.login,
},
}).replace(/</g, "\\u003c"),
}}
/>
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Home", item: WEB_URL },
{ "@type": "ListItem", position: 2, name: "Community Icons", item: `${WEB_URL}/community` },
{ "@type": "ListItem", position: 3, name: `${formattedName} Icon`, item: `${WEB_URL}/community/${icon}` },
],
}).replace(/</g, "\\u003c"),
}}
<JsonLd
data={buildIconPageGraph({
pageUrl,
pageName: `${formattedName} Icon & Logo (Community)`,
pageDescription,
dateModified: iconData.data.update.timestamp,
contentUrl: mainIconUrl,
licenseKey: "CC BY 4.0",
formattedName,
creator: { type: "Person", name: authorName, url: authorData.html_url || undefined },
creditText: `Icon by ${authorName}`,
copyrightNotice: "© Homarr Labs",
breadcrumbs: [
{ name: "Home", item: WEB_URL },
{ name: "Community Icons", item: `${WEB_URL}/community` },
{ name: `${formattedName} Icon`, item: pageUrl },
],
})}
/>
<IconDetails
breadcrumbItems={[
Expand Down
47 changes: 30 additions & 17 deletions web/src/app/community/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import type { Metadata } from "next"
import { Suspense } from "react"
import { CommunityIconSearch } from "@/components/community-icon-search"
import { JsonLd } from "@/components/seo/json-ld"
import { WEB_URL } from "@/constants"
import { fetchCommunitySubmissions, getCommunitySubmissions } from "@/lib/community"
import { buildDefaultOgImages, buildDefaultTwitterImages, getFilteredBrowseMetadata } from "@/lib/seo/metadata"
import { buildCommunityBrowseGraph } from "@/lib/seo/schemas"

export const revalidate = 300

export async function generateMetadata(): Promise<Metadata> {
const icons = await getCommunitySubmissions()
type Props = {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata({ searchParams }: Props): Promise<Metadata> {
const [icons, params] = await Promise.all([getCommunitySubmissions(), searchParams])
const totalIcons = icons.length
const description = `Search and browse through ${totalIcons} community-submitted icons and logos awaiting review and addition to the Dashboard Icons collection.`

return {
title: "Browse Community Icons & Logos",
description: `Search and browse through ${totalIcons} community-submitted icons and logos awaiting review and addition to the Dashboard Icons collection.`,
description,
keywords: [
"community icons",
"community logos",
Expand All @@ -27,34 +35,39 @@ export async function generateMetadata(): Promise<Metadata> {
],
openGraph: {
title: "Browse Community Icons & Logos",
description: `Search and browse through ${totalIcons} community-submitted icons and logos awaiting review and addition to the Dashboard Icons collection.`,
description,
type: "website",
url: `${WEB_URL}/community`,
images: buildDefaultOgImages("Browse Community Dashboard Icons"),
},
twitter: {
card: "summary_large_image",
title: "Browse Community Icons & Logos",
description: `Search and browse through ${totalIcons} community-submitted icons and logos awaiting review and addition to the Dashboard Icons collection.`,
},
alternates: {
canonical: `${WEB_URL}/community`,
description,
images: buildDefaultTwitterImages(),
},
...getFilteredBrowseMetadata(params, "/community"),
}
}

export default async function CommunityPage() {
const icons = await fetchCommunitySubmissions()
const description = `Search and browse through ${icons.length} community-submitted icons and logos awaiting review and addition to the Dashboard Icons collection.`

return (
<div className="isolate overflow-hidden p-2 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 community icons & logos</h1>
<p className="text-muted-foreground mb-1">Search through our collection of {icons.length} community-submitted icons and logos.</p>
<>
<JsonLd data={buildCommunityBrowseGraph({ description, totalItems: icons.length })} />
<div className="isolate overflow-hidden p-2 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 community icons & logos</h1>
<p className="text-muted-foreground mb-1">Search through our collection of {icons.length} community-submitted icons and logos.</p>
</div>
</div>
<Suspense fallback={<div className="text-muted-foreground">Loading...</div>}>
<CommunityIconSearch icons={icons as any} />
</Suspense>
</div>
<Suspense fallback={<div className="text-muted-foreground">Loading...</div>}>
<CommunityIconSearch icons={icons as any} />
</Suspense>
</div>
</>
)
}
58 changes: 25 additions & 33 deletions web/src/app/icons/[icon]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Metadata, ResolvingMetadata } from "next"
import { notFound } from "next/navigation"
import { IconDetails } from "@/components/icon-details"
import { JsonLd } from "@/components/seo/json-ld"
import { BASE_URL, WEB_URL } from "@/constants"
import { computeRelatedIcons, getAllIcons, getAuthorData } from "@/lib/api"
import { buildIconPageGraph } from "@/lib/seo/schemas"

export const dynamicParams = false
export const revalidate = false
Expand Down Expand Up @@ -134,41 +136,31 @@ export default async function IconPage({ params }: { params: Promise<{ icon: str
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")

const pageUrl = `${WEB_URL}/icons/${icon}`
const pageDescription = `Download the ${formattedName} icon and logo in SVG, PNG, and WEBP formats for FREE. Part of a collection of curated icons and logos for services, applications and tools, designed specifically for dashboards and app directories.`
const authorName = authorData.name || authorData.login

return (
<>
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "ImageObject",
contentUrl: `${BASE_URL}/png/${icon}.png`,
license: "https://creativecommons.org/licenses/by/4.0/",
acquireLicensePage: `${WEB_URL}/license`,
creditText: `Icon by ${authorData.name || authorData.login}`,
copyrightNotice: "© Homarr Labs",
creator: {
"@type": "Person",
name: authorData.name || authorData.login,
},
}).replace(/</g, "\\u003c"),
}}
/>
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Home", item: WEB_URL },
{ "@type": "ListItem", position: 2, name: "Browse Icons", item: `${WEB_URL}/icons` },
{ "@type": "ListItem", position: 3, name: `${formattedName} Icon`, item: `${WEB_URL}/icons/${icon}` },
],
}).replace(/</g, "\\u003c"),
}}
<JsonLd
data={buildIconPageGraph({
pageUrl,
pageName: `${formattedName} Icon & Logo`,
pageDescription,
dateModified: originalIconData.update.timestamp,
contentUrl: `${BASE_URL}/png/${icon}.png`,
licenseKey: "CC BY 4.0",
formattedName,
creator: { type: "Person", name: authorName, url: authorData.html_url },
creditText: `Icon by ${authorName}`,
copyrightNotice: "© Homarr Labs",
breadcrumbs: [
{ name: "Home", item: WEB_URL },
{ name: "Browse Icons", item: `${WEB_URL}/icons` },
{ name: `${formattedName} Icon`, item: pageUrl },
],
encodingFormat: "image/png",
})}
/>
<IconDetails
breadcrumbItems={[
Expand Down
62 changes: 24 additions & 38 deletions web/src/app/icons/external/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Metadata, ResolvingMetadata } from "next"
import { notFound } from "next/navigation"
import { IconDetails } from "@/components/icon-details"
import { JsonLd } from "@/components/seo/json-ld"
import { EXTERNAL_SOURCES, type ExternalSourceId, WEB_URL } from "@/constants"
import { getExternalIconPreviewUrl, resolveExternalIconUrl } from "@/lib/external-icon-urls"
import { getExternalIconBySlug, getExternalIcons } from "@/lib/external-icons"
import { buildIconPageGraph, resolveLicenseKey } from "@/lib/seo/schemas"
import type { AuthorData } from "@/types/icons"

export const dynamicParams = false
Expand Down Expand Up @@ -131,46 +133,30 @@ export default async function ExternalIconPage({ params }: { params: Promise<{ s
html_url: sourceConfig.authorUrl,
}

const pageUrl = `${WEB_URL}/icons/external/${slug}`
const pageDescription = `Download the ${icon.external.name} icon and logo from ${sourceConfig.label}. Licensed under ${sourceConfig.license}.`
const updatedAt =
icon.external.updated_at_source || icon.external.updated || icon.external.created || new Date().toISOString()

return (
<>
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "ImageObject",
contentUrl: previewUrl,
license:
sourceConfig.license === "MIT" ? "https://opensource.org/licenses/MIT" : "https://creativecommons.org/licenses/by/4.0/",
acquireLicensePage: `${WEB_URL}/license`,
creator: {
"@type": "Organization",
name: sourceConfig.authorName,
url: sourceConfig.authorUrl,
},
}).replace(/</g, "\\u003c"),
}}
/>
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD structured data
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Home", item: WEB_URL },
{ "@type": "ListItem", position: 2, name: "Browse Icons", item: `${WEB_URL}/icons` },
{
"@type": "ListItem",
position: 3,
name: `${icon.external.name} Icon`,
item: `${WEB_URL}/icons/external/${slug}`,
},
],
}).replace(/</g, "\\u003c"),
}}
<JsonLd
data={buildIconPageGraph({
pageUrl,
pageName: `${icon.external.name} Icon & Logo (${sourceConfig.label})`,
pageDescription,
dateModified: updatedAt,
contentUrl: previewUrl,
licenseKey: resolveLicenseKey(sourceConfig.license),
formattedName: icon.external.name,
creator: { type: "Organization", name: sourceConfig.authorName, url: sourceConfig.authorUrl },
breadcrumbs: [
{ name: "Home", item: WEB_URL },
{ name: "Browse Icons", item: `${WEB_URL}/icons` },
{ name: sourceConfig.label, item: `${WEB_URL}/icons?source=${sourceConfig.id}` },
{ name: `${icon.external.name} Icon`, item: pageUrl },
],
})}
/>
<IconDetails
icon={icon.external.slug}
Expand Down
Loading