feat: enhance SurfSense with new skills, blog section, and improve SEO metadata
Some checks failed
Build and Push Docker Images / tag_release (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Has been cancelled
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Has been cancelled

- Added multiple new skills to skills-lock.json from the repository `aaron-he-zhu/seo-geo-claude-skills`.
- Introduced `fuzzy-search` dependency in package.json for improved search functionality.
- Updated pnpm-lock.yaml to include the new `fuzzy-search` package.
- Enhanced SEO metadata across various pages, including canonical links and descriptions for better search visibility.
- Improved layout and structure of several components, including the homepage and changelog, to enhance user experience.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-11 23:38:12 -07:00
parent 61b3f0d7e3
commit 7ea840dbb2
120 changed files with 25729 additions and 352 deletions

View file

@ -15,7 +15,7 @@ export const Logo = ({
<Image
src="/icon-128.svg"
className={cn("select-none dark:invert", className)}
alt="logo"
alt="SurfSense"
width={128}
height={128}
priority={priority}

View file

@ -10,7 +10,6 @@ import {
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import type { AnnouncementCategory } from "@/contracts/types/announcement.types";
import type { AnnouncementWithState } from "@/hooks/use-announcements";
@ -67,7 +66,7 @@ export function AnnouncementCard({ announcement }: { announcement: AnnouncementW
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<CardTitle className="text-base leading-tight">{announcement.title}</CardTitle>
<h2 className="text-base font-semibold leading-tight tracking-tight">{announcement.title}</h2>
<Badge variant={config.badgeVariant} className="text-[10px] px-1.5 py-0">
{config.label}
</Badge>

View file

@ -16,9 +16,9 @@ export function ContactFormGridWithDetails() {
<IconMailFilled className="h-6 w-6 text-blue-500" />
</FeatureIconContainer>
</div>
<h2 className="mt-9 bg-gradient-to-b from-neutral-800 to-neutral-900 bg-clip-text text-center text-xl font-bold text-transparent md:text-3xl lg:text-5xl dark:from-neutral-200 dark:to-neutral-300">
Contact
</h2>
<h1 className="mt-9 bg-gradient-to-b from-neutral-800 to-neutral-900 bg-clip-text text-center text-xl font-bold text-transparent md:text-3xl lg:text-5xl dark:from-neutral-200 dark:to-neutral-300">
Contact
</h1>
<p className="mt-8 max-w-lg text-center text-base text-neutral-600 dark:text-neutral-400">
We'd love to hear from you!
</p>

View file

@ -0,0 +1,11 @@
import { cn } from "@/lib/utils";
export function Container({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return <div className={cn("mx-auto max-w-7xl px-4 sm:px-6 lg:px-8", className)}>{children}</div>;
}

View file

@ -0,0 +1,17 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { getBearerToken } from "@/lib/auth-utils";
export function AuthRedirect() {
const router = useRouter();
useEffect(() => {
if (getBearerToken()) {
router.replace("/dashboard");
}
}, [router]);
return null;
}

View file

@ -25,6 +25,10 @@ export function FooterNew() {
title: "Pricing",
href: "/pricing",
},
{
title: "Blog",
href: "/blog",
},
{
title: "Docs",
href: "/docs",

View file

@ -153,14 +153,13 @@ export function HeroSection() {
</h1>
<div className="mt-4 flex w-full flex-col items-start justify-between gap-4 md:mt-12 md:flex-row md:items-end md:gap-10">
<div>
<h2
className={cn(
"relative mb-8 max-w-2xl text-left text-sm tracking-wide text-neutral-600 antialiased sm:text-base md:text-xl dark:text-neutral-400"
)}
>
An open source, privacy focused alternative to NotebookLM for teams with no data
limits.
</h2>
<p
className={cn(
"relative mb-8 max-w-2xl text-left text-sm tracking-wide text-neutral-600 antialiased sm:text-base md:text-xl dark:text-neutral-400"
)}
>
A free, open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.
</p>
<div className="relative mb-4 flex w-full flex-col justify-center gap-y-2 sm:flex-row sm:justify-start sm:space-y-0 sm:space-x-4">
<DownloadButton />

View file

@ -180,11 +180,11 @@ export default function ExternalIntegrations() {
>
{/* Heading */}
<div className="text-center mb-12 md:mb-16 relative z-20 px-4">
<h3 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white leading-[1.1] tracking-tight">
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white leading-[1.1] tracking-tight">
Integrate with your
<br />
team&apos;s most important tools
</h3>
</h2>
</div>
{/* Scrolling columns container — masked at edges so the page background shows through seamlessly */}

View file

@ -36,6 +36,7 @@ export const Navbar = ({ scrolledBgClassName }: NavbarProps = {}) => {
const navItems = [
{ name: "Pricing", link: "/pricing" },
{ name: "Blog", link: "/blog" },
{ name: "Changelog", link: "/changelog" },
{ name: "Docs", link: "/docs" },
{ name: "Contact\u00A0Us", link: "/contact" },

View file

@ -72,7 +72,7 @@ export function Pricing({
return (
<div className="container mx-auto py-20">
<div className="text-center space-y-4 mb-12">
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">{title}</h2>
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">{title}</h1>
<p className="text-muted-foreground text-lg whitespace-pre-line">{description}</p>
</div>

View file

@ -0,0 +1,52 @@
import { ChevronRight } from "lucide-react";
import Link from "next/link";
import { BreadcrumbJsonLd } from "./json-ld";
interface BreadcrumbItem {
name: string;
href: string;
}
interface BreadcrumbNavProps {
items: BreadcrumbItem[];
className?: string;
}
export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
const jsonLdItems = items.map((item) => ({
name: item.name,
url: `https://surfsense.com${item.href}`,
}));
return (
<>
<BreadcrumbJsonLd items={jsonLdItems} />
<nav aria-label="Breadcrumb" className={className}>
<ol className="flex items-center gap-1.5 text-sm text-muted-foreground">
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<li key={item.href} className="flex items-center gap-1.5">
{index > 0 && (
<ChevronRight className="h-3.5 w-3.5 shrink-0" aria-hidden />
)}
{isLast ? (
<span className="font-medium text-foreground" aria-current="page">
{item.name}
</span>
) : (
<Link
href={item.href}
className="transition-colors hover:text-foreground"
>
{item.name}
</Link>
)}
</li>
);
})}
</ol>
</nav>
</>
);
}

View file

@ -0,0 +1,186 @@
interface JsonLdProps {
data: Record<string, unknown>;
}
export function JsonLd({ data }: JsonLdProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
export function OrganizationJsonLd() {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "Organization",
name: "SurfSense",
url: "https://surfsense.com",
logo: "https://surfsense.com/logo.png",
description:
"Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.",
sameAs: [
"https://github.com/MODSetter/SurfSense",
"https://discord.gg/Cg2M4GUJ",
],
contactPoint: {
"@type": "ContactPoint",
email: "rohan@surfsense.com",
contactType: "sales",
},
}}
/>
);
}
export function WebSiteJsonLd() {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "WebSite",
name: "SurfSense",
url: "https://surfsense.com",
description:
"Open source NotebookLM alternative for teams with no data limits. Free ChatGPT, Claude AI, and any AI model.",
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: "https://surfsense.com/docs?search={search_term_string}",
},
"query-input": "required name=search_term_string",
},
}}
/>
);
}
export function SoftwareApplicationJsonLd() {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
name: "SurfSense",
applicationCategory: "BusinessApplication",
operatingSystem: "Windows, macOS, Linux, Web",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
description: "Free plan with 500 pages included",
},
description:
"Open source NotebookLM alternative with free access to ChatGPT, Claude AI, and any model. Connect Slack, Google Drive, Notion, Confluence, GitHub, and dozens more data sources.",
url: "https://surfsense.com",
downloadUrl: "https://github.com/MODSetter/SurfSense/releases",
featureList: [
"Free access to ChatGPT, Claude AI, and any AI model",
"AI-powered semantic search across all connected tools",
"Federated search across Slack, Google Drive, Notion, Confluence, GitHub",
"No data limits with open source self-hosting",
"Real-time collaborative team chats",
"Document Q&A with citations",
"Report generation",
"Podcast and video generation from sources",
"Enterprise knowledge management",
"Self-hostable and privacy-focused",
],
}}
/>
);
}
export function ArticleJsonLd({
title,
description,
url,
datePublished,
author,
image,
}: {
title: string;
description: string;
url: string;
datePublished: string;
author: string;
image?: string;
}) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "Article",
headline: title,
description,
url,
datePublished,
author: {
"@type": "Organization",
name: author,
},
publisher: {
"@type": "Organization",
name: "SurfSense",
logo: {
"@type": "ImageObject",
url: "https://surfsense.com/logo.png",
},
},
image: image || "https://surfsense.com/og-image.png",
mainEntityOfPage: {
"@type": "WebPage",
"@id": url,
},
}}
/>
);
}
export function BreadcrumbJsonLd({
items,
}: {
items: { name: string; url: string }[];
}) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
name: item.name,
item: item.url,
})),
}}
/>
);
}
export function FAQJsonLd({
questions,
}: {
questions: { question: string; answer: string }[];
}) {
return (
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: questions.map((q) => ({
"@type": "Question",
name: q.question,
acceptedAnswer: {
"@type": "Answer",
text: q.answer,
},
})),
}}
/>
);
}