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.
|
- **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.
|
- **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
|
## [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 { Suspense } from 'react'
|
||||||
|
import type { Metadata } from 'next'
|
||||||
import PricingSection from '@/components/PricingSection'
|
import PricingSection from '@/components/PricingSection'
|
||||||
import { PricingCardsSkeleton } from '@/components/skeletons'
|
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() {
|
export default function PricingPage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen pt-20">
|
<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)
|
return () => clearInterval(interval)
|
||||||
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
|
}, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (site?.domain) document.title = `${site.domain} | Pulse`
|
||||||
|
}, [site?.domain])
|
||||||
|
|
||||||
const showSkeleton = useMinimumLoading(loading)
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
if (showSkeleton) {
|
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)
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
if (showSkeleton) return <RealtimeSkeleton />
|
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)
|
setTimeout(() => setSnippetCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (site?.domain) document.title = `Settings · ${site.domain} | Pulse`
|
||||||
|
}, [site?.domain])
|
||||||
|
|
||||||
const showSkeleton = useMinimumLoading(loading)
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
if (showSkeleton) {
|
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)
|
setShowEditModal(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (site?.domain) document.title = `Uptime · ${site.domain} | Pulse`
|
||||||
|
}, [site?.domain])
|
||||||
|
|
||||||
const showSkeleton = useMinimumLoading(loading)
|
const showSkeleton = useMinimumLoading(loading)
|
||||||
|
|
||||||
if (showSkeleton) return <UptimeSkeleton />
|
if (showSkeleton) return <UptimeSkeleton />
|
||||||
|
|||||||
Reference in New Issue
Block a user