mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
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
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:
parent
61b3f0d7e3
commit
7ea840dbb2
120 changed files with 25729 additions and 352 deletions
163
surfsense_web/app/(home)/blog/[slug]/page.tsx
Normal file
163
surfsense_web/app/(home)/blog/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { loader } from "fumadocs-core/source";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import { notFound } from "next/navigation";
|
||||
import { blog } from "@/.source/server";
|
||||
import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
|
||||
import { ArticleJsonLd } from "@/components/seo/json-ld";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { getMDXComponents } from "@/mdx-components";
|
||||
|
||||
const source = loader({
|
||||
baseUrl: "/blog",
|
||||
source: blog.toFumadocsSource(),
|
||||
});
|
||||
|
||||
interface BlogData {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
image?: string;
|
||||
author?: string;
|
||||
authorAvatar?: string;
|
||||
tags?: string[];
|
||||
body: React.ComponentType<{
|
||||
components?: Record<string, React.ComponentType>;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface BlogPageItem {
|
||||
url: string;
|
||||
slugs: string[];
|
||||
data: BlogData;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
slug: (page as BlogPageItem).slugs.join("/"),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await props.params;
|
||||
const page = (source.getPages() as BlogPageItem[]).find(
|
||||
(p) => p.slugs.join("/") === slug,
|
||||
);
|
||||
|
||||
if (!page) return {};
|
||||
|
||||
return {
|
||||
title: `${page.data.title} | SurfSense Blog`,
|
||||
description: page.data.description,
|
||||
alternates: {
|
||||
canonical: `https://surfsense.com/blog/${slug}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
type: "article",
|
||||
publishedTime: page.data.date,
|
||||
authors: [page.data.author ?? "SurfSense Team"],
|
||||
tags: page.data.tags,
|
||||
images: page.data.image ? [{ url: page.data.image }] : [{ url: "/og-image.png" }],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
images: page.data.image ? [page.data.image] : ["/og-image.png"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPostPage(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await props.params;
|
||||
const page = (source.getPages() as BlogPageItem[]).find(
|
||||
(p) => p.slugs.join("/") === slug,
|
||||
);
|
||||
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
const date = new Date(page.data.date);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative pt-20">
|
||||
<ArticleJsonLd
|
||||
title={page.data.title}
|
||||
description={page.data.description}
|
||||
url={`https://surfsense.com/blog/${slug}`}
|
||||
datePublished={page.data.date}
|
||||
author={page.data.author ?? "SurfSense Team"}
|
||||
image={page.data.image ? `https://surfsense.com${page.data.image}` : undefined}
|
||||
/>
|
||||
<div className="max-w-3xl mx-auto px-6 lg:px-10 pt-10 pb-20">
|
||||
<BreadcrumbNav
|
||||
items={[
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Blog", href: "/blog" },
|
||||
{ name: page.data.title, href: `/blog/${slug}` },
|
||||
]}
|
||||
className="mb-8"
|
||||
/>
|
||||
|
||||
{page.data.image && (
|
||||
<div className="relative aspect-2/1 overflow-hidden rounded-2xl mb-8">
|
||||
<Image
|
||||
src={page.data.image}
|
||||
alt={page.data.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
sizes="(max-width: 768px) 100vw, 768px"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4 mb-10">
|
||||
<h1 className="text-3xl md:text-4xl font-bold tracking-tight text-balance">
|
||||
{page.data.title}
|
||||
</h1>
|
||||
|
||||
{page.data.tags && page.data.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{page.data.tags.map((tag: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="h-6 w-fit px-2.5 text-xs font-medium bg-muted text-muted-foreground rounded-full border flex items-center justify-center"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
{page.data.authorAvatar && (
|
||||
<Image
|
||||
src={page.data.authorAvatar}
|
||||
alt={page.data.author ?? "SurfSense Team"}
|
||||
width={32}
|
||||
height={32}
|
||||
className="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<span className="font-medium text-foreground">
|
||||
{page.data.author ?? "SurfSense Team"}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<time dateTime={page.data.date}>{formatDate(date)}</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="prose dark:prose-invert max-w-none prose-headings:scroll-mt-8 prose-headings:font-semibold prose-a:no-underline prose-headings:tracking-tight prose-headings:text-balance prose-p:tracking-tight prose-p:text-balance prose-img:rounded-xl prose-img:shadow-lg">
|
||||
<MDX components={getMDXComponents()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
228
surfsense_web/app/(home)/blog/blog-magazine.tsx
Normal file
228
surfsense_web/app/(home)/blog/blog-magazine.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
"use client";
|
||||
|
||||
import { Container } from "@/components/container";
|
||||
import { format } from "date-fns";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import FuzzySearch from "fuzzy-search";
|
||||
import type { BlogEntry } from "./page";
|
||||
|
||||
function truncate(text: string, length: number) {
|
||||
return text.length > length ? `${text.slice(0, length)}…` : text;
|
||||
}
|
||||
|
||||
function SearchIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function BlogWithSearchMagazine({ blogs }: { blogs: BlogEntry[] }) {
|
||||
const featured = blogs[0];
|
||||
|
||||
if (!featured) {
|
||||
return (
|
||||
<div className="relative overflow-hidden bg-neutral-50 px-4 md:px-8 dark:bg-neutral-950">
|
||||
<Container className="relative pt-12 pb-24 md:pt-20">
|
||||
<p className="text-center text-neutral-500">No blog posts yet.</p>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden bg-neutral-50 px-4 pt-20 md:px-8 dark:bg-neutral-950">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(120,119,198,0.15),transparent)] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_-20%,rgba(120,119,198,0.12),transparent)]" />
|
||||
<Container className="relative pt-12 pb-24 md:pt-20">
|
||||
<header className="mb-10 md:mb-14">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-neutral-900 md:text-5xl dark:text-neutral-50">
|
||||
Blog
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<MagazineFeatured blog={featured} />
|
||||
|
||||
<MagazineSearchGrid blogs={blogs} featuredSlug={featured.slug} />
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MagazineFeatured({ blog }: { blog: BlogEntry }) {
|
||||
return (
|
||||
<Link
|
||||
href={blog.url}
|
||||
className="group/cover relative mb-14 block overflow-hidden rounded-3xl border border-neutral-200/80 bg-neutral-900 shadow-sm dark:border-neutral-800 dark:shadow-none"
|
||||
>
|
||||
<div className="md:aspect-[2.4/1] relative aspect-21/9 min-h-[220px]">
|
||||
{blog.image ? (
|
||||
<img
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
className="h-full w-full object-cover transition duration-500 group-hover/cover:scale-[1.03]"
|
||||
/>
|
||||
) : null}
|
||||
<div className="absolute inset-0 bg-linear-to-t from-black/85 via-black/35 to-transparent" />
|
||||
<div className="absolute inset-0 flex flex-col justify-end p-6 md:p-10">
|
||||
<span className="mb-2 inline-flex w-fit rounded-full bg-white/15 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
|
||||
Cover story
|
||||
</span>
|
||||
<h2 className="max-w-3xl font-serif text-2xl leading-tight font-medium text-white md:text-4xl">
|
||||
{blog.title}
|
||||
</h2>
|
||||
<p className="mt-3 max-w-2xl text-sm text-white/85 md:text-base">
|
||||
{truncate(blog.description, 160)}
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap items-center gap-3 text-sm text-white/90">
|
||||
<span className="flex items-center gap-2">
|
||||
<img
|
||||
src={blog.authorAvatar}
|
||||
alt={blog.author}
|
||||
width={28}
|
||||
height={28}
|
||||
className="h-7 w-7 rounded-full ring-2 ring-white/30"
|
||||
/>
|
||||
{blog.author}
|
||||
</span>
|
||||
<span className="text-white/50">·</span>
|
||||
<time dateTime={blog.date}>
|
||||
{format(new Date(blog.date), "MMMM d, yyyy")}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function MagazineSearchGrid({
|
||||
blogs: allBlogs,
|
||||
featuredSlug,
|
||||
}: {
|
||||
blogs: BlogEntry[];
|
||||
featuredSlug: string;
|
||||
}) {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const searcher = useMemo(
|
||||
() =>
|
||||
new FuzzySearch(allBlogs, ["title", "description"], {
|
||||
caseSensitive: false,
|
||||
}),
|
||||
[allBlogs],
|
||||
);
|
||||
|
||||
const [results, setResults] = useState(allBlogs);
|
||||
useEffect(() => {
|
||||
setResults(searcher.search(search));
|
||||
}, [search, searcher]);
|
||||
|
||||
const gridItems = useMemo(() => {
|
||||
if (search.trim()) {
|
||||
return results;
|
||||
}
|
||||
return results.filter((b) => b.slug !== featuredSlug);
|
||||
}, [results, search, featuredSlug]);
|
||||
|
||||
return (
|
||||
<section aria-labelledby="archive-heading">
|
||||
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2
|
||||
id="archive-heading"
|
||||
className="font-serif text-2xl font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
From the archive
|
||||
</h2>
|
||||
<label className="relative w-full sm:max-w-md">
|
||||
<span className="sr-only">Search articles</span>
|
||||
<SearchIcon className="pointer-events-none absolute top-1/2 left-4 -translate-y-1/2 text-neutral-400" />
|
||||
<input
|
||||
type="search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search by title or topic…"
|
||||
className="w-full rounded-full bg-white py-3 pr-4 pl-12 text-sm text-neutral-800 shadow-sm ring-1 shadow-black/10 ring-black/10 transition outline-none placeholder:text-neutral-400 focus:border-neutral-400 focus:ring-2 focus:ring-neutral-200/80 dark:bg-neutral-900 dark:text-neutral-100 dark:placeholder:text-neutral-500 dark:focus:border-neutral-500 dark:focus:ring-neutral-700/50"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{gridItems.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-neutral-300 py-16 text-center text-neutral-500 dark:border-neutral-700 dark:text-neutral-400">
|
||||
No articles match that search.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{gridItems.map((blog) => (
|
||||
<li key={blog.slug}>
|
||||
<MagazineCard blog={blog} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MagazineCard({ blog }: { blog: BlogEntry }) {
|
||||
return (
|
||||
<Link
|
||||
href={blog.url}
|
||||
className="group/card flex h-full flex-col overflow-hidden rounded-2xl bg-white shadow-sm ring-1 shadow-black/10 ring-black/10 transition hover:-translate-y-0.5 hover:shadow-lg dark:border-neutral-800 dark:bg-neutral-900/50 dark:hover:border-neutral-700"
|
||||
>
|
||||
<div className="relative aspect-16/10 overflow-hidden bg-neutral-100 dark:bg-neutral-800">
|
||||
{blog.image ? (
|
||||
<img
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
className="h-full w-full object-cover transition duration-300 group-hover/card:scale-105"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-neutral-400">
|
||||
No image
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col p-5">
|
||||
<time
|
||||
className="text-xs font-medium tracking-wider text-neutral-500 uppercase dark:text-neutral-400"
|
||||
dateTime={blog.date}
|
||||
>
|
||||
{format(new Date(blog.date), "MMM d, yyyy")}
|
||||
</time>
|
||||
<h3 className="mt-2 font-serif text-lg leading-snug font-medium text-neutral-900 dark:text-neutral-100">
|
||||
{blog.title}
|
||||
</h3>
|
||||
<p className="mt-2 flex-1 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
|
||||
{truncate(blog.description, 110)}
|
||||
</p>
|
||||
<div className="mt-4 flex items-center gap-2 pt-4">
|
||||
<img
|
||||
src={blog.authorAvatar}
|
||||
alt={blog.author}
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6 rounded-full object-cover"
|
||||
/>
|
||||
<span className="text-xs text-neutral-600 dark:text-neutral-300">
|
||||
{blog.author}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
61
surfsense_web/app/(home)/blog/page.tsx
Normal file
61
surfsense_web/app/(home)/blog/page.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { loader } from "fumadocs-core/source";
|
||||
import type { Metadata } from "next";
|
||||
import { blog } from "@/.source/server";
|
||||
import { BlogWithSearchMagazine } from "./blog-magazine";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Blog | SurfSense - AI Search & Knowledge Management",
|
||||
description:
|
||||
"Product updates, tutorials, and tips from the SurfSense team.",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/blog",
|
||||
},
|
||||
};
|
||||
|
||||
const source = loader({
|
||||
baseUrl: "/blog",
|
||||
source: blog.toFumadocsSource(),
|
||||
});
|
||||
|
||||
export interface BlogEntry {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
image: string;
|
||||
author: string;
|
||||
authorAvatar: string;
|
||||
}
|
||||
|
||||
export default async function BlogPage() {
|
||||
const allPages = source.getPages() as Array<{
|
||||
url: string;
|
||||
slugs: string[];
|
||||
data: {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
image?: string;
|
||||
author?: string;
|
||||
authorAvatar?: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
const blogs: BlogEntry[] = allPages
|
||||
.map((page) => ({
|
||||
title: page.data.title,
|
||||
description: page.data.description ?? "",
|
||||
date: page.data.date,
|
||||
slug: page.slugs.join("/"),
|
||||
url: page.url,
|
||||
image: page.data.image ?? "/og-image.png",
|
||||
author: page.data.author ?? "SurfSense Team",
|
||||
authorAvatar: page.data.authorAvatar ?? "/logo.png",
|
||||
}))
|
||||
.sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
);
|
||||
|
||||
return <BlogWithSearchMagazine blogs={blogs} />;
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
import { loader } from "fumadocs-core/source";
|
||||
import type { Metadata } from "next";
|
||||
import { changelog } from "@/.source/server";
|
||||
import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { getMDXComponents } from "@/mdx-components";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Changelog | SurfSense",
|
||||
description: "See what's new in SurfSense.",
|
||||
description: "See what's new in SurfSense. Latest updates, features, and improvements.",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/changelog",
|
||||
},
|
||||
};
|
||||
|
||||
const source = loader({
|
||||
|
|
@ -42,6 +46,13 @@ export default async function ChangelogPage() {
|
|||
<div className="max-w-5xl mx-auto relative">
|
||||
<div className="p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<BreadcrumbNav
|
||||
items={[
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Changelog", href: "/changelog" },
|
||||
]}
|
||||
className="mb-4"
|
||||
/>
|
||||
<h1 className="text-4xl font-bold tracking-tight bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400 bg-clip-text text-transparent">
|
||||
Changelog
|
||||
</h1>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { ContactFormGridWithDetails } from "@/components/contact/contact-form";
|
|||
|
||||
export const metadata: Metadata = {
|
||||
title: "Contact | SurfSense",
|
||||
description: "Get in touch with the SurfSense team.",
|
||||
description: "Get in touch with the SurfSense team for enterprise AI search, knowledge management, or partnership inquiries.",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/contact",
|
||||
},
|
||||
};
|
||||
|
||||
const page = () => {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,23 @@
|
|||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { HeroSection } from "@/components/homepage/hero-section";
|
||||
import { getBearerToken } from "@/lib/auth-utils";
|
||||
import { AuthRedirect } from "@/components/homepage/auth-redirect";
|
||||
import { FeaturesCards } from "@/components/homepage/features-card";
|
||||
import { FeaturesBentoGrid } from "@/components/homepage/features-bento-grid";
|
||||
|
||||
const WhySurfSense = dynamic(
|
||||
() => import("@/components/homepage/why-surfsense").then((m) => ({ default: m.WhySurfSense })),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const FeaturesCards = dynamic(
|
||||
() => import("@/components/homepage/features-card").then((m) => ({ default: m.FeaturesCards })),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const FeaturesBentoGrid = dynamic(
|
||||
() =>
|
||||
import("@/components/homepage/features-bento-grid").then((m) => ({
|
||||
default: m.FeaturesBentoGrid,
|
||||
})),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
const ExternalIntegrations = dynamic(() => import("@/components/homepage/integrations"), {
|
||||
ssr: false,
|
||||
});
|
||||
const ExternalIntegrations = dynamic(() => import("@/components/homepage/integrations"));
|
||||
|
||||
const CTAHomepage = dynamic(
|
||||
() => import("@/components/homepage/cta").then((m) => ({ default: m.CTAHomepage })),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (getBearerToken()) {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white">
|
||||
<AuthRedirect />
|
||||
<HeroSection />
|
||||
<WhySurfSense />
|
||||
<FeaturesCards />
|
||||
|
|
|
|||
|
|
@ -1,14 +1,27 @@
|
|||
import type { Metadata } from "next";
|
||||
import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
|
||||
import PricingBasic from "@/components/pricing/pricing-section";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Pricing | SurfSense",
|
||||
description: "Explore SurfSense plans and pricing options.",
|
||||
title: "Pricing | SurfSense - Free AI Search Plans",
|
||||
description:
|
||||
"Explore SurfSense plans and pricing. Use ChatGPT, Claude AI, and any AI model free. Open source NotebookLM alternative for teams.",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/pricing",
|
||||
},
|
||||
};
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="container mx-auto pt-24 px-4">
|
||||
<BreadcrumbNav
|
||||
items={[
|
||||
{ name: "Home", href: "/" },
|
||||
{ name: "Pricing", href: "/pricing" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<PricingBasic />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import type { Metadata } from "next";
|
|||
export const metadata: Metadata = {
|
||||
title: "Privacy Policy | SurfSense",
|
||||
description: "Privacy Policy for SurfSense application",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/privacy",
|
||||
},
|
||||
};
|
||||
|
||||
export default function PrivacyPolicy() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import type { Metadata } from "next";
|
|||
export const metadata: Metadata = {
|
||||
title: "Terms of Service | SurfSense",
|
||||
description: "Terms of Service for SurfSense application",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/terms",
|
||||
},
|
||||
};
|
||||
|
||||
export default function TermsOfService() {
|
||||
|
|
|
|||
BIN
surfsense_web/app/apple-icon.png
Normal file
BIN
surfsense_web/app/apple-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
|
|
@ -45,8 +45,17 @@ export async function generateMetadata(props: { params: Promise<{ slug?: string[
|
|||
const page = getDocPage(params.slug);
|
||||
if (!page) notFound();
|
||||
|
||||
const slugPath = params.slug ? params.slug.join("/") : "";
|
||||
return {
|
||||
title: page.data.title,
|
||||
title: `${page.data.title} | SurfSense Docs`,
|
||||
description: page.data.description,
|
||||
alternates: {
|
||||
canonical: `https://surfsense.com/docs${slugPath ? `/${slugPath}` : ""}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: `${page.data.title} | SurfSense Docs`,
|
||||
description: page.data.description,
|
||||
type: "article",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
BIN
surfsense_web/app/icon.png
Normal file
BIN
surfsense_web/app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
|
|
@ -13,6 +13,7 @@ import { LocaleProvider } from "@/contexts/LocaleContext";
|
|||
import { PlatformProvider } from "@/contexts/platform-context";
|
||||
import { ReactQueryClientProvider } from "@/lib/query-client/query-client.provider";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { OrganizationJsonLd, SoftwareApplicationJsonLd, WebSiteJsonLd } from "@/components/seo/json-ld";
|
||||
|
||||
const roboto = Roboto({
|
||||
subsets: ["latin"],
|
||||
|
|
@ -36,82 +37,81 @@ export const viewport: Viewport = {
|
|||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "SurfSense - Open Source NotebookLM Alternative for Teams",
|
||||
metadataBase: new URL("https://surfsense.com"),
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com",
|
||||
},
|
||||
title: "SurfSense - NotebookLM Alternative | Free ChatGPT & Claude AI",
|
||||
description:
|
||||
"An open source, privacy focused alternative to NotebookLM for teams with no data limits, built for enterprise AI search and knowledge management.",
|
||||
"Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.",
|
||||
keywords: [
|
||||
"enterprise ai",
|
||||
"enterprise search",
|
||||
"enterprise search software",
|
||||
"chatgpt alternative free",
|
||||
"ai enterprise search",
|
||||
"enterprise search solutions",
|
||||
"intranet search engine",
|
||||
"federated search",
|
||||
"enterprise search engine",
|
||||
"what is enterprise search",
|
||||
"enterprise knowledge management software",
|
||||
"free chatgpt alternative",
|
||||
"chatgpt free alternative",
|
||||
"best enterprise search software",
|
||||
"enterprise ai search",
|
||||
"enterprise knowledge management",
|
||||
"federated search engine",
|
||||
"enterprise knowledge management system",
|
||||
"free claude ai",
|
||||
"what is enterprise search engine marketing",
|
||||
"ai driven enterprise search",
|
||||
"free alternative to chatgpt",
|
||||
"free claude",
|
||||
"alternative to chatgpt free",
|
||||
"free ai chatbot like chatgpt",
|
||||
"enterprise search software comparison",
|
||||
"apps like chatgpt for free",
|
||||
"free chatgpt no login",
|
||||
"free ai chatbots like chatgpt",
|
||||
"enterprise document search",
|
||||
"search engine for intranet",
|
||||
"chatgpt online",
|
||||
"online chatgpt",
|
||||
"chat gpt free",
|
||||
"chatgpt free",
|
||||
"free chatgpt",
|
||||
"free chat gpt",
|
||||
"chatgpt no login",
|
||||
"chatgpt online free",
|
||||
"chatgpt free online",
|
||||
"chatgpt without login",
|
||||
"free chatgpt without login",
|
||||
"unified search engine",
|
||||
"free chatgpt no login",
|
||||
"chatgpt for free",
|
||||
"claude ai free",
|
||||
"claude free",
|
||||
"free claude ai",
|
||||
"free claude",
|
||||
"chatgpt alternative free",
|
||||
"free chatgpt alternative",
|
||||
"free alternative to chatgpt",
|
||||
"alternative to chatgpt free",
|
||||
"chatgpt alternative online free",
|
||||
"ai like chatgpt",
|
||||
"sites like chatgpt",
|
||||
"free ai chatbot like chatgpt",
|
||||
"apps like chatgpt for free",
|
||||
"free ai chatbots like chatgpt",
|
||||
"best free alternative to chatgpt",
|
||||
"free chatgpt alternative app",
|
||||
"free chatgpt alternative with image upload",
|
||||
"best free alternative to chatgpt",
|
||||
"enterprise search engine open source",
|
||||
"open source notebooklm alternative",
|
||||
"free ai apps",
|
||||
"ai with no restrictions",
|
||||
"notebooklm alternative",
|
||||
"notebooklm alternative for teams",
|
||||
"open source notebooklm alternative",
|
||||
"SurfSense",
|
||||
],
|
||||
openGraph: {
|
||||
title: "SurfSense - Open Source NotebookLM Alternative for Teams",
|
||||
title: "SurfSense - NotebookLM Alternative | Free ChatGPT & Claude AI",
|
||||
description:
|
||||
"An open source, privacy focused alternative to NotebookLM for teams with no data limits. Open source enterprise AI search and knowledge management.",
|
||||
"Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude, and any AI model for free.",
|
||||
url: "https://surfsense.com",
|
||||
siteName: "SurfSense",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "https://surfsense.com/og-image.png",
|
||||
url: "/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "SurfSense - Open Source NotebookLM Alternative for Teams",
|
||||
alt: "SurfSense - Open Source NotebookLM Alternative with Free ChatGPT and Claude AI",
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "SurfSense - Open Source NotebookLM Alternative for Teams",
|
||||
title: "SurfSense - NotebookLM Alternative | Free ChatGPT & Claude AI",
|
||||
description:
|
||||
"An open source, privacy focused alternative to NotebookLM for teams with no data limits. Open source enterprise AI search and knowledge management.",
|
||||
creator: "https://surfsense.com",
|
||||
site: "https://surfsense.com",
|
||||
"Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.",
|
||||
creator: "@SurfSenseAI",
|
||||
site: "@SurfSenseAI",
|
||||
images: [
|
||||
{
|
||||
url: "https://surfsense.com/og-image-twitter.png",
|
||||
url: "/og-image-twitter.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "SurfSense - Open Source NotebookLM Alternative for Teams",
|
||||
alt: "SurfSense - Open Source NotebookLM Alternative with Free ChatGPT and Claude AI",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -129,6 +129,9 @@ export default function RootLayout({
|
|||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://api.github.com" />
|
||||
<OrganizationJsonLd />
|
||||
<WebSiteJsonLd />
|
||||
<SoftwareApplicationJsonLd />
|
||||
</head>
|
||||
<body className={cn(roboto.className, "bg-white dark:bg-black antialiased h-full w-full ")}>
|
||||
<PostHogProvider>
|
||||
|
|
|
|||
54
surfsense_web/app/not-found.tsx
Normal file
54
surfsense_web/app/not-found.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Page Not Found | SurfSense",
|
||||
description: "The page you're looking for doesn't exist. Explore SurfSense - open source enterprise AI search and knowledge management.",
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center px-4 text-center">
|
||||
<h1 className="text-8xl font-bold tracking-tight text-neutral-900 dark:text-neutral-100">
|
||||
404
|
||||
</h1>
|
||||
<p className="mt-4 text-xl text-neutral-600 dark:text-neutral-400">
|
||||
The page you're looking for doesn't exist.
|
||||
</p>
|
||||
<p className="mt-2 text-base text-neutral-500 dark:text-neutral-500">
|
||||
It may have been moved, or the URL might be incorrect.
|
||||
</p>
|
||||
<div className="mt-10 flex flex-col items-center gap-4 sm:flex-row">
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-lg bg-black px-6 py-3 text-sm font-medium text-white transition hover:bg-neutral-800 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
|
||||
>
|
||||
Go Home
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs"
|
||||
className="rounded-lg border border-neutral-200 px-6 py-3 text-sm font-medium text-neutral-700 transition hover:bg-neutral-50 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Browse Docs
|
||||
</Link>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="rounded-lg border border-neutral-200 px-6 py-3 text-sm font-medium text-neutral-700 transition hover:bg-neutral-50 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Read Blog
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="mt-16 flex flex-wrap justify-center gap-x-6 gap-y-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<Link href="/pricing" className="hover:text-neutral-900 dark:hover:text-neutral-200">
|
||||
Pricing
|
||||
</Link>
|
||||
<Link href="/contact" className="hover:text-neutral-900 dark:hover:text-neutral-200">
|
||||
Contact
|
||||
</Link>
|
||||
<Link href="/changelog" className="hover:text-neutral-900 dark:hover:text-neutral-200">
|
||||
Changelog
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
22
surfsense_web/app/robots.ts
Normal file
22
surfsense_web/app/robots.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: [
|
||||
"/dashboard/",
|
||||
"/desktop/",
|
||||
"/auth/",
|
||||
"/api/",
|
||||
"/invite/",
|
||||
"/public/",
|
||||
"/verify-token/",
|
||||
],
|
||||
},
|
||||
],
|
||||
sitemap: "https://surfsense.com/sitemap.xml",
|
||||
};
|
||||
}
|
||||
|
|
@ -1,259 +1,59 @@
|
|||
import { loader } from "fumadocs-core/source";
|
||||
import type { MetadataRoute } from "next";
|
||||
import { blog, changelog } from "@/.source/server";
|
||||
import { source as docsSource } from "@/lib/source";
|
||||
|
||||
// Returns a date rounded to the current hour (updates only once per hour)
|
||||
function getHourlyDate(): Date {
|
||||
const now = new Date();
|
||||
now.setMinutes(0, 0, 0);
|
||||
return now;
|
||||
}
|
||||
const blogSource = loader({
|
||||
baseUrl: "/blog",
|
||||
source: blog.toFumadocsSource(),
|
||||
});
|
||||
|
||||
const changelogSource = loader({
|
||||
baseUrl: "/changelog",
|
||||
source: changelog.toFumadocsSource(),
|
||||
});
|
||||
|
||||
const BASE_URL = "https://surfsense.com";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const lastModified = getHourlyDate();
|
||||
const now = new Date();
|
||||
now.setMinutes(0, 0, 0);
|
||||
const lastModified = now;
|
||||
|
||||
return [
|
||||
{
|
||||
url: "https://www.surfsense.com/",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/contact",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/pricing",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/privacy",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/terms",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
// Documentation pages
|
||||
{
|
||||
url: "https://www.surfsense.com/docs",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/installation",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/prerequisites",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/docker-installation/install-script",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/docker-installation/docker-compose",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/docker-installation/updating",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/docker-installation/dev-compose",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/docker-installation/migrate-from-allinone",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/manual-installation",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.9,
|
||||
},
|
||||
// Connector documentation
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/airtable",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/bookstack",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/circleback",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/clickup",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/confluence",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/discord",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/dropbox",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/elasticsearch",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/github",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/gmail",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/google-calendar",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/google-drive",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/jira",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/linear",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/luma",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/microsoft-onedrive",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/microsoft-teams",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/notion",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/obsidian",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/slack",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/connectors/web-crawler",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
// How-to documentation
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/how-to/zero-sync",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/how-to/realtime-collaboration",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/how-to/web-search",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.8,
|
||||
},
|
||||
// Developer documentation
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/testing",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: "https://www.surfsense.com/docs/code-of-conduct",
|
||||
lastModified,
|
||||
changeFrequency: "daily",
|
||||
priority: 0.7,
|
||||
},
|
||||
const staticPages: MetadataRoute.Sitemap = [
|
||||
{ url: `${BASE_URL}/`, lastModified, changeFrequency: "daily", priority: 1 },
|
||||
{ url: `${BASE_URL}/pricing`, lastModified, changeFrequency: "weekly", priority: 0.9 },
|
||||
{ url: `${BASE_URL}/contact`, lastModified, changeFrequency: "monthly", priority: 0.7 },
|
||||
{ url: `${BASE_URL}/blog`, lastModified, changeFrequency: "daily", priority: 0.9 },
|
||||
{ url: `${BASE_URL}/changelog`, lastModified, changeFrequency: "weekly", priority: 0.7 },
|
||||
{ url: `${BASE_URL}/announcements`, lastModified, changeFrequency: "weekly", priority: 0.6 },
|
||||
{ url: `${BASE_URL}/docs`, lastModified, changeFrequency: "daily", priority: 1 },
|
||||
{ url: `${BASE_URL}/privacy`, lastModified, changeFrequency: "monthly", priority: 0.3 },
|
||||
{ url: `${BASE_URL}/terms`, lastModified, changeFrequency: "monthly", priority: 0.3 },
|
||||
{ url: `${BASE_URL}/login`, lastModified, changeFrequency: "monthly", priority: 0.5 },
|
||||
{ url: `${BASE_URL}/register`, lastModified, changeFrequency: "monthly", priority: 0.5 },
|
||||
];
|
||||
|
||||
const docsPages: MetadataRoute.Sitemap = docsSource.getPages().map((page) => ({
|
||||
url: `${BASE_URL}${page.url}`,
|
||||
lastModified,
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
|
||||
const blogPages: MetadataRoute.Sitemap = blogSource.getPages().map((page) => ({
|
||||
url: `${BASE_URL}${page.url}`,
|
||||
lastModified,
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
|
||||
const changelogPages: MetadataRoute.Sitemap = changelogSource.getPages().map((page) => ({
|
||||
url: `${BASE_URL}${page.url}`,
|
||||
lastModified,
|
||||
changeFrequency: "monthly" as const,
|
||||
priority: 0.5,
|
||||
}));
|
||||
|
||||
return [...staticPages, ...docsPages, ...blogPages, ...changelogPages];
|
||||
}
|
||||
|
|
|
|||
38
surfsense_web/blog/content/why-enterprise-search-matters.mdx
Normal file
38
surfsense_web/blog/content/why-enterprise-search-matters.mdx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: "Why Enterprise Search Matters More Than Ever"
|
||||
description: "As organizations generate more data across more tools, finding the right information at the right time has become a critical competitive advantage. Here's why enterprise search is no longer optional."
|
||||
date: "2026-04-10"
|
||||
image: "/og-image.png"
|
||||
author: "SurfSense Team"
|
||||
authorAvatar: "/logo.png"
|
||||
tags: ["Enterprise Search", "Productivity", "Knowledge Management"]
|
||||
---
|
||||
|
||||
## The Information Overload Problem
|
||||
|
||||
The average knowledge worker switches between **11 different applications** per day and spends nearly 20% of their time searching for information. That's an entire day each week lost to context-switching and hunting for answers buried in Slack threads, Google Docs, Notion pages, and email chains.
|
||||
|
||||
Enterprise search solves this by providing a **single, unified interface** to query across all your knowledge sources simultaneously.
|
||||
|
||||
## What Makes Modern Enterprise Search Different
|
||||
|
||||
Traditional enterprise search was little more than a keyword matcher slapped onto a file server. Modern solutions like SurfSense take a fundamentally different approach:
|
||||
|
||||
- **Semantic Understanding**: AI-powered search understands the *meaning* behind your query, not just the keywords
|
||||
- **Federated Architecture**: Connect dozens of data sources without migrating data out of where it lives
|
||||
- **Contextual Ranking**: Results are ranked by relevance to your role, recent activity, and team context
|
||||
- **Real-time Indexing**: New content becomes searchable within minutes, not days
|
||||
|
||||
## The ROI of Getting Search Right
|
||||
|
||||
Organizations that invest in proper enterprise search see measurable improvements:
|
||||
|
||||
- **30% reduction** in time spent searching for information
|
||||
- **25% faster** onboarding for new team members
|
||||
- **40% fewer** duplicate documents and redundant work
|
||||
|
||||
## Getting Started
|
||||
|
||||
The best time to implement enterprise search was when your team hit 20 people. The second best time is now. Start by auditing which tools your team uses daily, then look for a solution that connects to all of them natively.
|
||||
|
||||
SurfSense connects to Slack, Google Drive, Notion, Confluence, GitHub, and dozens more — all with a single search bar.
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
11
surfsense_web/components/container.tsx
Normal file
11
surfsense_web/components/container.tsx
Normal 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>;
|
||||
}
|
||||
17
surfsense_web/components/homepage/auth-redirect.tsx
Normal file
17
surfsense_web/components/homepage/auth-redirect.tsx
Normal 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;
|
||||
}
|
||||
|
|
@ -25,6 +25,10 @@ export function FooterNew() {
|
|||
title: "Pricing",
|
||||
href: "/pricing",
|
||||
},
|
||||
{
|
||||
title: "Blog",
|
||||
href: "/blog",
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
href: "/docs",
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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's most important tools
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Scrolling columns container — masked at edges so the page background shows through seamlessly */}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
52
surfsense_web/components/seo/breadcrumb-nav.tsx
Normal file
52
surfsense_web/components/seo/breadcrumb-nav.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
186
surfsense_web/components/seo/json-ld.tsx
Normal file
186
surfsense_web/components/seo/json-ld.tsx
Normal 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,
|
||||
},
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -97,6 +97,7 @@
|
|||
"fumadocs-core": "^16.3.1",
|
||||
"fumadocs-mdx": "^14.2.1",
|
||||
"fumadocs-ui": "^16.3.1",
|
||||
"fuzzy-search": "^3.2.1",
|
||||
"geist": "^1.4.2",
|
||||
"jotai": "^2.15.1",
|
||||
"jotai-tanstack-query": "^0.11.0",
|
||||
|
|
|
|||
8
surfsense_web/pnpm-lock.yaml
generated
8
surfsense_web/pnpm-lock.yaml
generated
|
|
@ -236,6 +236,9 @@ importers:
|
|||
fumadocs-ui:
|
||||
specifier: ^16.3.1
|
||||
version: 16.6.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.577.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1)
|
||||
fuzzy-search:
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1
|
||||
geist:
|
||||
specifier: ^1.4.2
|
||||
version: 1.7.0(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
|
||||
|
|
@ -5924,6 +5927,9 @@ packages:
|
|||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
fuzzy-search@3.2.1:
|
||||
resolution: {integrity: sha512-vAcPiyomt1ioKAsAL2uxSABHJ4Ju/e4UeDM+g1OlR0vV4YhLGMNsdLNvZTpEDY4JCSt0E4hASCNM5t2ETtsbyg==}
|
||||
|
||||
gaxios@6.7.1:
|
||||
resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
|
@ -14435,6 +14441,8 @@ snapshots:
|
|||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
fuzzy-search@3.2.1: {}
|
||||
|
||||
gaxios@6.7.1:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
|
|
|
|||
BIN
surfsense_web/public/logo.png
Normal file
BIN
surfsense_web/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
surfsense_web/public/og-image-twitter.png
Normal file
BIN
surfsense_web/public/og-image-twitter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 1.1 MiB |
|
|
@ -16,6 +16,19 @@ export const changelog = defineDocs({
|
|||
},
|
||||
});
|
||||
|
||||
export const blog = defineDocs({
|
||||
dir: "blog/content",
|
||||
docs: {
|
||||
schema: frontmatterSchema.extend({
|
||||
date: z.string(),
|
||||
image: z.string().optional(),
|
||||
author: z.string().default("SurfSense Team"),
|
||||
authorAvatar: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
lastModifiedTime: "git",
|
||||
mdxOptions: {
|
||||
|
|
|
|||
13
surfsense_web/types/fuzzy-search.d.ts
vendored
Normal file
13
surfsense_web/types/fuzzy-search.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
declare module "fuzzy-search" {
|
||||
interface FuzzySearchOptions {
|
||||
caseSensitive?: boolean;
|
||||
sort?: boolean;
|
||||
}
|
||||
|
||||
class FuzzySearch<T> {
|
||||
constructor(haystack: T[], keys?: string[], options?: FuzzySearchOptions);
|
||||
search(needle: string): T[];
|
||||
}
|
||||
|
||||
export default FuzzySearch;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue