mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 04:42:39 +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];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue