feat: enhance page titles and link previews for improved user experience and sharing capabilities
This commit is contained in:
@@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
- **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".
|
||||
- **Faster favicon loading.** Site icons in the dashboard, referrers, and campaigns now use Next.js image optimization for better caching and lazy loading.
|
||||
- **Better page titles.** Browser tabs now show which site and page you're on (e.g. "Uptime · example.com | Pulse") instead of the same generic title everywhere.
|
||||
- **Link previews for public dashboards.** Sharing a public dashboard link on social media now shows a proper preview with the site name and description.
|
||||
|
||||
## [0.10.0-alpha] - 2026-02-21
|
||||
|
||||
|
||||
19
app/about/layout.tsx
Normal file
19
app/about/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'About | Pulse',
|
||||
description: 'Pulse is built by Ciphera — privacy-first web analytics made in Switzerland.',
|
||||
openGraph: {
|
||||
title: 'About | Pulse',
|
||||
description: 'Pulse is built by Ciphera — privacy-first web analytics made in Switzerland.',
|
||||
siteName: 'Pulse by Ciphera',
|
||||
},
|
||||
}
|
||||
|
||||
export default function AboutLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
19
app/faq/layout.tsx
Normal file
19
app/faq/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'FAQ | Pulse',
|
||||
description: 'Frequently asked questions about Pulse, privacy, GDPR compliance, and how it works.',
|
||||
openGraph: {
|
||||
title: 'FAQ | Pulse',
|
||||
description: 'Frequently asked questions about Pulse, privacy, GDPR compliance, and how it works.',
|
||||
siteName: 'Pulse by Ciphera',
|
||||
},
|
||||
}
|
||||
|
||||
export default function FaqLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
19
app/features/layout.tsx
Normal file
19
app/features/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Features | Pulse',
|
||||
description: 'Dashboards, funnels, uptime monitoring, realtime visitors, and more — all without cookies.',
|
||||
openGraph: {
|
||||
title: 'Features | Pulse',
|
||||
description: 'Dashboards, funnels, uptime monitoring, realtime visitors, and more — all without cookies.',
|
||||
siteName: 'Pulse by Ciphera',
|
||||
},
|
||||
}
|
||||
|
||||
export default function FeaturesLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
19
app/integrations/layout.tsx
Normal file
19
app/integrations/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Integrations | Pulse',
|
||||
description: 'Add Pulse analytics to Next.js, React, Vue, WordPress, and more in under a minute.',
|
||||
openGraph: {
|
||||
title: 'Integrations | Pulse',
|
||||
description: 'Add Pulse analytics to Next.js, React, Vue, WordPress, and more in under a minute.',
|
||||
siteName: 'Pulse by Ciphera',
|
||||
},
|
||||
}
|
||||
|
||||
export default function IntegrationsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
15
app/notifications/layout.tsx
Normal file
15
app/notifications/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Notifications | Pulse',
|
||||
description: 'View your alerts and activity updates.',
|
||||
robots: { index: false, follow: false },
|
||||
}
|
||||
|
||||
export default function NotificationsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
@@ -1,7 +1,18 @@
|
||||
import { Suspense } from 'react'
|
||||
import type { Metadata } from 'next'
|
||||
import PricingSection from '@/components/PricingSection'
|
||||
import { PricingCardsSkeleton } from '@/components/skeletons'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Pricing | Pulse',
|
||||
description: 'Simple, transparent pricing for privacy-first web analytics. Free tier included.',
|
||||
openGraph: {
|
||||
title: 'Pricing | Pulse',
|
||||
description: 'Simple, transparent pricing for privacy-first web analytics. Free tier included.',
|
||||
siteName: 'Pulse by Ciphera',
|
||||
},
|
||||
}
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<div className="min-h-screen pt-20">
|
||||
|
||||
72
app/share/[id]/layout.tsx
Normal file
72
app/share/[id]/layout.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8082'
|
||||
|
||||
interface SharePageParams {
|
||||
params: Promise<{ id: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: SharePageParams): Promise<Metadata> {
|
||||
const { id } = await params
|
||||
const fallback: Metadata = {
|
||||
title: 'Public Dashboard | Pulse',
|
||||
description: 'Privacy-first web analytics — view this site\'s public stats.',
|
||||
openGraph: {
|
||||
title: 'Public Dashboard | Pulse',
|
||||
description: 'Privacy-first web analytics — view this site\'s public stats.',
|
||||
siteName: 'Pulse by Ciphera',
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title: 'Public Dashboard | Pulse',
|
||||
description: 'Privacy-first web analytics — view this site\'s public stats.',
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/public/sites/${id}/dashboard?limit=1`, {
|
||||
next: { revalidate: 3600 },
|
||||
})
|
||||
if (!res.ok) return fallback
|
||||
|
||||
const data = await res.json()
|
||||
const domain = data?.site?.domain
|
||||
if (!domain) return fallback
|
||||
|
||||
const title = `${domain} analytics | Pulse`
|
||||
const description = `Live, privacy-first analytics for ${domain} — powered by Pulse.`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
siteName: 'Pulse by Ciphera',
|
||||
type: 'website',
|
||||
images: [{
|
||||
url: `https://www.google.com/s2/favicons?domain=${domain}&sz=128`,
|
||||
width: 128,
|
||||
height: 128,
|
||||
alt: `${domain} favicon`,
|
||||
}],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title,
|
||||
description,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
export default function ShareLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
15
app/sites/[id]/funnels/layout.tsx
Normal file
15
app/sites/[id]/funnels/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Funnels | Pulse',
|
||||
description: 'Track conversion funnels and user journeys.',
|
||||
robots: { index: false, follow: false },
|
||||
}
|
||||
|
||||
export default function FunnelsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
15
app/sites/[id]/layout.tsx
Normal file
15
app/sites/[id]/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard | Pulse',
|
||||
description: 'View your site analytics, traffic, and performance.',
|
||||
robots: { index: false, follow: false },
|
||||
}
|
||||
|
||||
export default function SiteLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
@@ -216,6 +216,10 @@ export default function SiteDashboardPage() {
|
||||
return () => clearInterval(interval)
|
||||
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
|
||||
|
||||
useEffect(() => {
|
||||
if (site?.domain) document.title = `${site.domain} | Pulse`
|
||||
}, [site?.domain])
|
||||
|
||||
const showSkeleton = useMinimumLoading(loading)
|
||||
|
||||
if (showSkeleton) {
|
||||
|
||||
15
app/sites/[id]/realtime/layout.tsx
Normal file
15
app/sites/[id]/realtime/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Realtime | Pulse',
|
||||
description: 'See who is on your site right now.',
|
||||
robots: { index: false, follow: false },
|
||||
}
|
||||
|
||||
export default function RealtimeLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
@@ -91,6 +91,10 @@ export default function RealtimePage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (site?.domain) document.title = `Realtime · ${site.domain} | Pulse`
|
||||
}, [site?.domain])
|
||||
|
||||
const showSkeleton = useMinimumLoading(loading)
|
||||
|
||||
if (showSkeleton) return <RealtimeSkeleton />
|
||||
|
||||
15
app/sites/[id]/settings/layout.tsx
Normal file
15
app/sites/[id]/settings/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Site Settings | Pulse',
|
||||
description: 'Configure your site tracking, privacy, and goals.',
|
||||
robots: { index: false, follow: false },
|
||||
}
|
||||
|
||||
export default function SiteSettingsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
@@ -317,6 +317,10 @@ export default function SiteSettingsPage() {
|
||||
setTimeout(() => setSnippetCopied(false), 2000)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (site?.domain) document.title = `Settings · ${site.domain} | Pulse`
|
||||
}, [site?.domain])
|
||||
|
||||
const showSkeleton = useMinimumLoading(loading)
|
||||
|
||||
if (showSkeleton) {
|
||||
|
||||
15
app/sites/[id]/uptime/layout.tsx
Normal file
15
app/sites/[id]/uptime/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Uptime | Pulse',
|
||||
description: 'Monitor your site uptime and response times.',
|
||||
robots: { index: false, follow: false },
|
||||
}
|
||||
|
||||
export default function UptimeLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return children
|
||||
}
|
||||
@@ -703,6 +703,10 @@ export default function UptimePage() {
|
||||
setShowEditModal(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (site?.domain) document.title = `Uptime · ${site.domain} | Pulse`
|
||||
}, [site?.domain])
|
||||
|
||||
const showSkeleton = useMinimumLoading(loading)
|
||||
|
||||
if (showSkeleton) return <UptimeSkeleton />
|
||||
|
||||
Reference in New Issue
Block a user