feat: optimize favicon loading across the application using Next.js image component for better performance and caching
This commit is contained in:
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||||||
- **Smoother loading experience.** Pages now show a subtle preview of the layout while data loads instead of a blank screen or spinner. This applies everywhere — dashboards, settings, uptime, funnels, notifications, billing, and detail modals.
|
- **Smoother loading experience.** Pages now show a subtle preview of the layout while data loads instead of a blank screen or spinner. This applies everywhere — dashboards, settings, uptime, funnels, notifications, billing, and detail modals.
|
||||||
- **No more loading flicker.** Fast-loading pages no longer flash a loading state for a split second before showing content.
|
- **No more loading flicker.** Fast-loading pages no longer flash a loading state for a split second before showing content.
|
||||||
- **Clearer error messages.** When something goes wrong, the error message now tells you what failed (e.g. "Failed to load uptime monitors") instead of a generic "Failed to load data".
|
- **Clearer error messages.** When something goes wrong, the error message now tells you what failed (e.g. "Failed to load uptime monitors") instead of a generic "Failed to load data".
|
||||||
|
- **Faster favicon loading.** Site icons in the dashboard, referrers, and campaigns now use Next.js image optimization for better caching and lazy loading.
|
||||||
|
|
||||||
## [0.10.0-alpha] - 2026-02-21
|
## [0.10.0-alpha] - 2026-02-21
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
import { useParams, useSearchParams, useRouter } from 'next/navigation'
|
import { useParams, useSearchParams, useRouter } from 'next/navigation'
|
||||||
import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, getPublicPerformanceByPage, type DashboardData, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats'
|
import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, getPublicPerformanceByPage, type DashboardData, type Stats, type DailyStat, type PerformanceByPageStat } from '@/lib/api/stats'
|
||||||
import { toast } from '@ciphera-net/ui'
|
import { toast } from '@ciphera-net/ui'
|
||||||
@@ -282,13 +283,16 @@ export default function PublicDashboardPage() {
|
|||||||
<span className="text-sm font-medium text-brand-orange uppercase tracking-wider">Public Dashboard</span>
|
<span className="text-sm font-medium text-brand-orange uppercase tracking-wider">Public Dashboard</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white flex items-center gap-3">
|
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white flex items-center gap-3">
|
||||||
<img
|
<Image
|
||||||
src={`https://www.google.com/s2/favicons?domain=${site.domain}&sz=64`}
|
src={`https://www.google.com/s2/favicons?domain=${site.domain}&sz=64`}
|
||||||
alt={site.name}
|
alt={site.name}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
className="w-8 h-8 rounded-lg"
|
className="w-8 h-8 rounded-lg"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).src = '/globe.svg'
|
(e.target as HTMLImageElement).src = '/globe.svg'
|
||||||
}}
|
}}
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
{site.domain}
|
{site.domain}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
import { formatNumber } from '@ciphera-net/ui'
|
import { formatNumber } from '@ciphera-net/ui'
|
||||||
import { Modal, ArrowRightIcon, Button } from '@ciphera-net/ui'
|
import { Modal, ArrowRightIcon, Button } from '@ciphera-net/ui'
|
||||||
import { TableSkeleton } from '@/components/skeletons'
|
import { TableSkeleton } from '@/components/skeletons'
|
||||||
@@ -111,11 +112,14 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
|
|||||||
const useFavicon = faviconUrl && !faviconFailed.has(source)
|
const useFavicon = faviconUrl && !faviconFailed.has(source)
|
||||||
if (useFavicon) {
|
if (useFavicon) {
|
||||||
return (
|
return (
|
||||||
<img
|
<Image
|
||||||
src={faviconUrl}
|
src={faviconUrl}
|
||||||
alt=""
|
alt=""
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
className="w-5 h-5 flex-shrink-0 rounded object-contain"
|
className="w-5 h-5 flex-shrink-0 rounded object-contain"
|
||||||
onError={() => setFaviconFailed((prev) => new Set(prev).add(source))}
|
onError={() => setFaviconFailed((prev) => new Set(prev).add(source))}
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
import { formatNumber } from '@ciphera-net/ui'
|
import { formatNumber } from '@ciphera-net/ui'
|
||||||
import { getReferrerDisplayName, getReferrerFavicon, getReferrerIcon, mergeReferrersByDisplayName } from '@/lib/utils/icons'
|
import { getReferrerDisplayName, getReferrerFavicon, getReferrerIcon, mergeReferrersByDisplayName } from '@/lib/utils/icons'
|
||||||
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
import { Modal, GlobeIcon } from '@ciphera-net/ui'
|
||||||
@@ -39,11 +40,14 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
|
|||||||
const useFavicon = faviconUrl && !faviconFailed.has(referrer)
|
const useFavicon = faviconUrl && !faviconFailed.has(referrer)
|
||||||
if (useFavicon) {
|
if (useFavicon) {
|
||||||
return (
|
return (
|
||||||
<img
|
<Image
|
||||||
src={faviconUrl}
|
src={faviconUrl}
|
||||||
alt=""
|
alt=""
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
className="w-5 h-5 flex-shrink-0 rounded object-contain"
|
className="w-5 h-5 flex-shrink-0 rounded object-contain"
|
||||||
onError={() => setFaviconFailed((prev) => new Set(prev).add(referrer))}
|
onError={() => setFaviconFailed((prev) => new Set(prev).add(referrer))}
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import Image from 'next/image'
|
||||||
import { Site } from '@/lib/api/sites'
|
import { Site } from '@/lib/api/sites'
|
||||||
import type { Stats } from '@/lib/api/stats'
|
import type { Stats } from '@/lib/api/stats'
|
||||||
import { formatNumber } from '@ciphera-net/ui'
|
import { formatNumber } from '@ciphera-net/ui'
|
||||||
@@ -34,10 +35,13 @@ function SiteCard({ site, stats, statsLoading, onDelete, canDelete }: SiteCardPr
|
|||||||
<div className="flex items-start justify-between mb-6">
|
<div className="flex items-start justify-between mb-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="h-12 w-12 overflow-hidden rounded-lg border border-neutral-100 bg-neutral-50 p-1 dark:border-neutral-800 dark:bg-neutral-800">
|
<div className="h-12 w-12 overflow-hidden rounded-lg border border-neutral-100 bg-neutral-50 p-1 dark:border-neutral-800 dark:bg-neutral-800">
|
||||||
<img
|
<Image
|
||||||
src={`https://www.google.com/s2/favicons?domain=${site.domain}&sz=64`}
|
src={`https://www.google.com/s2/favicons?domain=${site.domain}&sz=64`}
|
||||||
alt={site.name}
|
alt={site.name}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
className="h-full w-full object-contain"
|
className="h-full w-full object-contain"
|
||||||
|
unoptimized
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ const nextConfig: NextConfig = {
|
|||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
// * Privacy-first: Disable analytics and telemetry
|
// * Privacy-first: Disable analytics and telemetry
|
||||||
productionBrowserSourceMaps: false,
|
productionBrowserSourceMaps: false,
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'www.google.com',
|
||||||
|
pathname: '/s2/favicons**',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user