fix: improve SEO

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-12-04 09:46:12 +01:00
parent a37e37c28b
commit 4b775ff2c5
10 changed files with 155 additions and 44 deletions

View File

@@ -1,3 +1,6 @@
# https://www.robotstxt.org/robotstxt.html # https://www.robotstxt.org/robotstxt.html
User-agent: * User-agent: *
Disallow: Allow: /share/
Allow: /login
Allow: /onboarding
Disallow: /

View File

@@ -1,4 +1,3 @@
import { LogoSquare } from '@/components/logo';
import { import {
Carousel, Carousel,
CarouselContent, CarouselContent,
@@ -64,18 +63,8 @@ const sellingPoints = [
export function LoginLeftPanel() { export function LoginLeftPanel() {
return ( return (
<div className="relative h-screen overflow-hidden"> <div className="relative h-screen overflow-hidden">
<div className="row justify-between items-center p-8">
<LogoSquare className="h-8 w-8" />
<a
href="https://openpanel.dev"
className="text-sm text-muted-foreground"
>
Back to website
</a>
</div>
{/* Carousel */} {/* Carousel */}
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full mt-24">
<Carousel <Carousel
className="w-full h-full [&>div]:h-full [&>div]:min-h-full" className="w-full h-full [&>div]:h-full [&>div]:min-h-full"
opts={{ opts={{

View File

@@ -0,0 +1,107 @@
import { useAppContext } from '@/hooks/use-app-context';
import { cn } from '@/utils/cn';
import { MenuIcon, XIcon } from 'lucide-react';
import { useState } from 'react';
import { LogoSquare } from './logo';
import { Button, LinkButton } from './ui/button';
export function LoginNavbar({ className }: { className?: string }) {
const { isSelfHosted } = useAppContext();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<div
className={cn(
'absolute top-0 left-0 w-full row justify-between items-center p-8 z-10',
className,
)}
>
<a href="https://openpanel.dev" className="row items-center gap-2">
<LogoSquare className="size-8 shrink-0" />
<span className="font-medium text-sm text-muted-foreground">
{isSelfHosted ? 'Self-hosted analytics' : 'OpenPanel.dev'}
</span>
</a>
<nav className="max-md:hidden">
<ul className="row gap-4 items-center [&>li>a]:text-sm [&>li>a]:text-muted-foreground [&>li>a]:hover:underline">
<li>
<a href="https://openpanel.dev">OpenPanel Cloud</a>
</li>
<li>
<a href="https://openpanel.dev/compare/mixpanel-alternative">
Mixpanel alternative
</a>
</li>
<li>
<a href="https://openpanel.dev/compare/mixpanel-alternative">
Posthog alternative
</a>
</li>
<li>
<a href="https://openpanel.dev/articles/open-source-web-analytics">
Open source analytics
</a>
</li>
</ul>
</nav>
<div className="md:hidden relative">
<Button
size="icon"
variant="ghost"
onClick={() => setMobileMenuOpen((prev) => !prev)}
>
{mobileMenuOpen ? <XIcon size={20} /> : <MenuIcon size={20} />}
</Button>
{mobileMenuOpen && (
<>
<button
type="button"
onClick={() => setMobileMenuOpen(false)}
className="fixed inset-0 z-40 bg-black/20 backdrop-blur-sm"
/>
<nav className="absolute right-0 top-full mt-2 z-50 bg-card border border-border rounded-md shadow-lg min-w-48 py-2">
<ul className="flex flex-col *:text-sm *:text-muted-foreground">
<li>
<a
href="https://openpanel.dev"
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground"
onClick={() => setMobileMenuOpen(false)}
>
OpenPanel Cloud
</a>
</li>
<li>
<a
href="https://openpanel.dev/compare/mixpanel-alternative"
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground"
onClick={() => setMobileMenuOpen(false)}
>
Posthog alternative
</a>
</li>
<li>
<a
href="https://openpanel.dev/compare/mixpanel-alternative"
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground"
onClick={() => setMobileMenuOpen(false)}
>
Mixpanel alternative
</a>
</li>
<li>
<a
href="https://openpanel.dev/articles/open-source-web-analytics"
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground"
onClick={() => setMobileMenuOpen(false)}
>
Open source analytics
</a>
</li>
</ul>
</nav>
</>
)}
</div>
</div>
);
}

View File

@@ -91,19 +91,8 @@ const onboardingSellingPoints = [
export function OnboardingLeftPanel() { export function OnboardingLeftPanel() {
return ( return (
<div className="sticky top-0 h-screen overflow-hidden"> <div className="sticky top-0 h-screen overflow-hidden">
<div className="row justify-between items-center p-8">
<LogoSquare className="h-8 w-8" />
<div className="text-sm text-muted-foreground">
Already have an account?{' '}
<Link to="/login" className="text-sm text-muted-foreground underline">
Sign in
</Link>
</div>
</div>
{/* Carousel */} {/* Carousel */}
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full mt-24">
<Carousel <Carousel
className="w-full h-full [&>div]:h-full [&>div]:min-h-full" className="w-full h-full [&>div]:h-full [&>div]:min-h-full"
opts={{ opts={{

View File

@@ -12,7 +12,10 @@ import { z } from 'zod';
export const Route = createFileRoute('/_login/login')({ export const Route = createFileRoute('/_login/login')({
component: LoginPage, component: LoginPage,
head: () => ({ head: () => ({
meta: [{ title: createTitle(PAGE_TITLES.LOGIN) }], meta: [
{ title: createTitle(PAGE_TITLES.LOGIN) },
{ name: 'robots', content: 'noindex, follow' },
],
}), }),
validateSearch: z.object({ validateSearch: z.object({
error: z.string().optional(), error: z.string().optional(),
@@ -26,11 +29,13 @@ function LoginPage() {
return ( return (
<div className="col gap-8 w-full text-left"> <div className="col gap-8 w-full text-left">
<div> <div>
<LogoSquare className="size-12 mb-8 md:hidden" />
<h1 className="text-3xl font-bold text-foreground mb-2">Sign in</h1> <h1 className="text-3xl font-bold text-foreground mb-2">Sign in</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Don't have an account?{' '} Don't have an account?{' '}
<a href="/onboarding" className="underline"> <a
href="/onboarding"
className="underline font-medium text-foreground"
>
Create one today Create one today
</a> </a>
</p> </p>

View File

@@ -6,7 +6,10 @@ import { z } from 'zod';
export const Route = createFileRoute('/_login/reset-password')({ export const Route = createFileRoute('/_login/reset-password')({
head: () => ({ head: () => ({
meta: [{ title: createTitle(PAGE_TITLES.RESET_PASSWORD) }], meta: [
{ title: createTitle(PAGE_TITLES.RESET_PASSWORD) },
{ name: 'robots', content: 'noindex, follow' },
],
}), }),
component: Component, component: Component,
validateSearch: z.object({ validateSearch: z.object({

View File

@@ -1,4 +1,5 @@
import { LoginLeftPanel } from '@/components/login-left-panel'; import { LoginLeftPanel } from '@/components/login-left-panel';
import { LoginNavbar } from '@/components/login-navbar';
import { SkeletonDashboard } from '@/components/skeleton-dashboard'; import { SkeletonDashboard } from '@/components/skeleton-dashboard';
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'; import { Outlet, createFileRoute, redirect } from '@tanstack/react-router';
@@ -14,6 +15,7 @@ export const Route = createFileRoute('/_login')({
function AuthLayout() { function AuthLayout() {
return ( return (
<div className="relative min-h-screen grid md:grid-cols-2"> <div className="relative min-h-screen grid md:grid-cols-2">
<LoginNavbar />
<div className="hidden md:block"> <div className="hidden md:block">
<LoginLeftPanel /> <LoginLeftPanel />
</div> </div>

View File

@@ -18,6 +18,7 @@ export const Route = createFileRoute('/_public/onboarding')({
head: () => ({ head: () => ({
meta: [ meta: [
{ title: createEntityTitle('Create an account', PAGE_TITLES.ONBOARDING) }, { title: createEntityTitle('Create an account', PAGE_TITLES.ONBOARDING) },
{ name: 'robots', content: 'noindex, follow' },
], ],
}), }),
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
@@ -56,7 +57,6 @@ function Component() {
return ( return (
<div className="col gap-8 w-full text-left"> <div className="col gap-8 w-full text-left">
<div> <div>
<LogoSquare className="size-12 mb-8 md:hidden" />
<h1 className="text-3xl font-bold text-foreground mb-2"> <h1 className="text-3xl font-bold text-foreground mb-2">
Create an account Create an account
</h1> </h1>

View File

@@ -1,3 +1,4 @@
import { LoginNavbar } from '@/components/login-navbar';
import { OnboardingLeftPanel } from '@/components/onboarding-left-panel'; import { OnboardingLeftPanel } from '@/components/onboarding-left-panel';
import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'; import { Outlet, createFileRoute, redirect } from '@tanstack/react-router';
@@ -8,6 +9,7 @@ export const Route = createFileRoute('/_public')({
function OnboardingLayout() { function OnboardingLayout() {
return ( return (
<div className="relative min-h-screen grid md:grid-cols-2"> <div className="relative min-h-screen grid md:grid-cols-2">
<LoginNavbar />
<div className="hidden md:block"> <div className="hidden md:block">
<OnboardingLeftPanel /> <OnboardingLeftPanel />
</div> </div>

View File

@@ -1,6 +1,7 @@
import { ShareEnterPassword } from '@/components/auth/share-enter-password'; import { ShareEnterPassword } from '@/components/auth/share-enter-password';
import { FullPageEmptyState } from '@/components/full-page-empty-state'; import { FullPageEmptyState } from '@/components/full-page-empty-state';
import FullPageLoadingState from '@/components/full-page-loading-state'; import FullPageLoadingState from '@/components/full-page-loading-state';
import { LoginNavbar } from '@/components/login-navbar';
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
import { LiveCounter } from '@/components/overview/live-counter'; import { LiveCounter } from '@/components/overview/live-counter';
import OverviewMetrics from '@/components/overview/overview-metrics'; import OverviewMetrics from '@/components/overview/overview-metrics';
@@ -24,11 +25,32 @@ export const Route = createFileRoute('/share/overview/$shareId')({
component: RouteComponent, component: RouteComponent,
validateSearch: shareSearchSchema, validateSearch: shareSearchSchema,
loader: async ({ context, params }) => { loader: async ({ context, params }) => {
await context.queryClient.prefetchQuery( const share = await context.queryClient.ensureQueryData(
context.trpc.share.overview.queryOptions({ context.trpc.share.overview.queryOptions({
shareId: params.shareId, shareId: params.shareId,
}), }),
); );
return { share };
},
head: ({ loaderData }) => {
if (!loaderData || !loaderData.share) {
return {
meta: [
{
title: 'Share not found - OpenPanel.dev',
},
],
};
}
return {
meta: [
{
title: `${loaderData.share.project?.name} - ${loaderData.share.organization?.name} - OpenPanel.dev`,
},
],
};
}, },
pendingComponent: FullPageLoadingState, pendingComponent: FullPageLoadingState,
errorComponent: () => ( errorComponent: () => (
@@ -78,19 +100,8 @@ function RouteComponent() {
return ( return (
<div> <div>
{isHeaderVisible && ( {isHeaderVisible && (
<div className="mx-auto max-w-7xl justify-between row gap-4 p-4 pb-0"> <div className="mx-auto max-w-7xl">
<div className="col gap-1"> <LoginNavbar className="relative p-4" />
<span className="text-sm">{share.organization?.name}</span>
<h1 className="text-xl font-medium">{share.project?.name}</h1>
</div>
<a
href="https://openpanel.dev?utm_source=openpanel.dev&utm_medium=share"
className="col gap-1 items-end"
title="OpenPanel"
>
<span className="text-xs">POWERED BY</span>
<span className="text-xl font-medium">openpanel.dev</span>
</a>
</div> </div>
)} )}
<div className=""> <div className="">