diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e86d4e..dbdea1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Improved - **Cleaner page paths in your reports.** Pages like `/products?_t=123456` or `/about?session=abc` now correctly show as `/products` and `/about`. Only marketing attribution parameters (like UTM tags) are preserved for traffic source tracking — all other junk parameters are automatically removed, so your Top Pages and Journeys stay clean without us having to chase down every new parameter format. -- **Refreshed chart background.** The dashboard chart now has subtle horizontal lines instead of the old dotted background, giving the chart area a cleaner look with soft faded edges. -- **Smoother loading transitions.** When your data finishes loading, the page now fades in smoothly instead of appearing all at once. This applies across Dashboard, Journeys, Funnels, Uptime, Settings, Notifications, and shared dashboards. If your data was already cached from a previous visit, it still loads instantly with no animation — the fade only kicks in when you're actually waiting for fresh data. +- **Easier to hover country dots on the map.** The orange location markers on the world map are now much easier to interact with — you no longer need pixel-perfect aim to see the tooltip. +- **Smoother chart curves and filled area.** The dashboard chart line now flows with natural curves instead of sharp flat tops at peaks. The area beneath the line is filled with a soft transparent orange gradient that fades toward the bottom, making trends easier to read at a glance. +- **Refreshed chart background.** The dashboard chart now has subtle horizontal lines instead of the old dotted background, giving the chart area a cleaner look. +- **Smoother loading transitions.** When your data finishes loading, the page now fades in smoothly instead of appearing all at once. This applies across Dashboard, Journeys, Funnels, Behavior, Uptime, Settings, Notifications, and shared dashboards. If your data was already cached from a previous visit, it still loads instantly with no animation — the fade only kicks in when you're actually waiting for fresh data. - **Faster tab switching across the board.** Switching between Settings, Funnels, Uptime, and other tabs now shows your data instantly instead of flashing a loading skeleton every time. Previously visited tabs remember their data and show it right away, while quietly refreshing in the background so you always see the latest numbers without the wait. - **Smoother loading on the Journeys page.** The Journeys tab now shows a polished skeleton placeholder while data loads, matching the loading experience on other tabs. - **Consistent chart colors.** All dashboard charts — Unique Visitors, Total Pageviews, Bounce Rate, and Visit Duration — now use the same brand orange color for a cleaner, more cohesive look. diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..888f8bb --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,50 @@ +import type { MetadataRoute } from 'next' + +export default function robots(): MetadataRoute.Robots { + return { + rules: [ + { + userAgent: '*', + allow: [ + '/', + '/about', + '/features', + '/pricing', + '/faq', + '/changelog', + '/installation', + '/integrations', + ], + disallow: [ + '/api/', + '/admin/', + '/sites/', + '/notifications/', + '/onboarding/', + '/org-settings/', + '/welcome/', + '/auth/', + '/actions/', + '/share/', + ], + }, + { + userAgent: 'GPTBot', + disallow: ['/'], + }, + { + userAgent: 'ChatGPT-User', + disallow: ['/'], + }, + { + userAgent: 'Google-Extended', + disallow: ['/'], + }, + { + userAgent: 'CCBot', + disallow: ['/'], + }, + ], + sitemap: 'https://pulse.ciphera.net/sitemap.xml', + } +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..0a31eda --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,38 @@ +import type { MetadataRoute } from 'next' + +const BASE_URL = 'https://pulse.ciphera.net' + +export default function sitemap(): MetadataRoute.Sitemap { + const integrationSlugs = [ + 'nextjs', + 'react', + 'vue', + 'wordpress', + ] + + const publicRoutes = [ + { url: '', priority: 1.0, changeFrequency: 'weekly' as const }, + { url: '/about', priority: 0.8, changeFrequency: 'monthly' as const }, + { url: '/features', priority: 0.9, changeFrequency: 'monthly' as const }, + { url: '/pricing', priority: 0.9, changeFrequency: 'monthly' as const }, + { url: '/faq', priority: 0.7, changeFrequency: 'monthly' as const }, + { url: '/changelog', priority: 0.6, changeFrequency: 'weekly' as const }, + { url: '/installation', priority: 0.8, changeFrequency: 'monthly' as const }, + { url: '/integrations', priority: 0.8, changeFrequency: 'monthly' as const }, + ] + + const integrationRoutes = integrationSlugs.map((slug) => ({ + url: `/integrations/${slug}`, + priority: 0.7, + changeFrequency: 'monthly' as const, + })) + + const allRoutes = [...publicRoutes, ...integrationRoutes] + + return allRoutes.map((route) => ({ + url: `${BASE_URL}${route.url}`, + lastModified: new Date(), + changeFrequency: route.changeFrequency, + priority: route.priority, + })) +} diff --git a/app/sites/[id]/behavior/page.tsx b/app/sites/[id]/behavior/page.tsx index a172a39..eab1f50 100644 --- a/app/sites/[id]/behavior/page.tsx +++ b/app/sites/[id]/behavior/page.tsx @@ -11,6 +11,7 @@ import FrustrationTable from '@/components/behavior/FrustrationTable' import FrustrationByPageTable from '@/components/behavior/FrustrationByPageTable' import FrustrationTrend from '@/components/behavior/FrustrationTrend' import { useDashboard, useBehavior } from '@/lib/swr/dashboard' +import { BehaviorSkeleton, useMinimumLoading, useSkeletonFade } from '@/components/skeletons' const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth')) @@ -42,6 +43,9 @@ export default function BehaviorPage() { // Fetch dashboard data for scroll depth (goal_counts + stats) const { data: dashboard } = useDashboard(siteId, dateRange.start, dateRange.end) + const showSkeleton = useMinimumLoading(loading && !behavior) + const fadeClass = useSkeletonFade(showSkeleton) + useEffect(() => { const domain = dashboard?.site?.domain document.title = domain ? `Behavior · ${domain} | Pulse` : 'Behavior | Pulse' @@ -63,8 +67,10 @@ export default function BehaviorPage() { const deadClicks = behavior?.dead_clicks ?? { items: [], total: 0 } const byPage = behavior?.by_page ?? [] + if (showSkeleton) return + return ( -
+
{/* Header */}
diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx index 50062ab..a685190 100644 --- a/components/dashboard/Chart.tsx +++ b/components/dashboard/Chart.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useRef, useCallback, useEffect } from 'react' import { useTheme } from '@ciphera-net/ui' -import { CartesianGrid, Line, LineChart, XAxis, YAxis, ReferenceLine } from 'recharts' +import { Area, CartesianGrid, ComposedChart, Line, XAxis, YAxis, ReferenceLine } from 'recharts' import { ChartContainer, ChartTooltip, type ChartConfig } from '@/components/ui/line-charts-6' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { formatNumber, formatDuration, formatUpdatedAgo, DatePicker } from '@ciphera-net/ui' @@ -456,12 +456,16 @@ export default function Chart({ config={chartConfig} className="h-96 w-full overflow-visible [&_.recharts-curve.recharts-tooltip-cursor]:stroke-[initial]" > - + + + + - +
)} diff --git a/components/dashboard/DottedMap.tsx b/components/dashboard/DottedMap.tsx index a9be59a..8d6ecf6 100644 --- a/components/dashboard/DottedMap.tsx +++ b/components/dashboard/DottedMap.tsx @@ -118,30 +118,30 @@ export default function DottedMap({ data, className }: DottedMapProps) { const rowIndex = _stagger.yToRowIndex.get(marker.y) ?? 0 const offsetX = rowIndex % 2 === 1 ? _stagger.xStep / 2 : 0 const info = markerData[index] + const cx = marker.x + offsetX + const cy = marker.y return ( - { if (info) { - const rect = (e.target as SVGCircleElement).closest('svg')!.getBoundingClientRect() - const svgX = marker.x + offsetX - const svgY = marker.y + const rect = (e.target as SVGElement).closest('svg')!.getBoundingClientRect() setTooltip({ - x: rect.left + (svgX / MAP_WIDTH) * rect.width, - y: rect.top + (svgY / MAP_HEIGHT) * rect.height, + x: rect.left + (cx / MAP_WIDTH) * rect.width, + y: rect.top + (cy / MAP_HEIGHT) * rect.height, country: info.country, pageviews: info.pageviews, }) } }} onMouseLeave={() => setTooltip(null)} - /> + > + {/* Invisible larger hitbox */} + + {/* Visible dot */} + + ) })} diff --git a/components/skeletons.tsx b/components/skeletons.tsx index 8e9483a..52d6e92 100644 --- a/components/skeletons.tsx +++ b/components/skeletons.tsx @@ -370,6 +370,55 @@ export function PricingCardsSkeleton() { ) } +// ─── Behavior page skeleton ───────────────────────────────── + +export function BehaviorSkeleton() { + return ( +
+ {/* Header */} +
+
+ + +
+ +
+ + {/* Summary cards (3 cols) */} +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ + + +
+ ))} +
+ + {/* Rage clicks + Dead clicks side by side */} +
+ + +
+ + {/* By-page table */} +
+
+ + + +
+
+ + {/* Scroll depth + Frustration trend */} +
+ + +
+
+ ) +} + // ─── Organization settings skeleton (members, billing, etc) ─ export function MembersListSkeleton() { diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..3aa4d2b --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,50 @@ +# Pulse by Ciphera +> Privacy-first web analytics. No cookies, no fingerprinting, no personal data collection. GDPR compliant by architecture. + +Pulse is a lightweight, privacy-focused website analytics platform built by Ciphera. It provides meaningful traffic insights without compromising visitor privacy. The tracking script is under 2KB and requires no cookie banners. + +## Key Features +- No cookies, no fingerprinting, no personal data collection +- GDPR, CCPA, and PECR compliant by design — no consent banners needed +- Lightweight tracking script (under 2KB, no impact on page speed) +- Real-time dashboard with pageviews, visitors, referrers, and geographic data +- UTM campaign tracking and custom event tracking +- Public/shared dashboard support +- Organization and team management +- Scheduled email reports + +## Integrations +- [Next.js](https://pulse.ciphera.net/integrations/nextjs) +- [React](https://pulse.ciphera.net/integrations/react) +- [Vue](https://pulse.ciphera.net/integrations/vue) +- [WordPress](https://pulse.ciphera.net/integrations/wordpress) +- Works with any website via a single script tag + +## Installation +Add one script tag to your site: +```html + +``` + +## Pages +- [Home](https://pulse.ciphera.net): Product overview and dashboard +- [Features](https://pulse.ciphera.net/features): Full feature list +- [Pricing](https://pulse.ciphera.net/pricing): Plans and pricing +- [Installation](https://pulse.ciphera.net/installation): Setup guide +- [Integrations](https://pulse.ciphera.net/integrations): Framework-specific guides +- [FAQ](https://pulse.ciphera.net/faq): Frequently asked questions +- [About](https://pulse.ciphera.net/about): About Pulse and Ciphera +- [Changelog](https://pulse.ciphera.net/changelog): Release notes and updates +- [Documentation](https://pulse.ciphera.net/docs): Full API and usage docs + +## About Ciphera +- Founded: 2024, Diegem, Belgium +- Infrastructure: Swiss-hosted servers (FADP protected) +- Open source: https://github.com/ciphera-net +- Website: https://ciphera.net +- Contact: hello@ciphera.net + +## Policies +- Privacy policy: https://ciphera.net/privacy +- Terms of service: https://ciphera.net/terms +- AI training on this site's content is not permitted. Search indexing is allowed.