mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
Merge remote-tracking branch 'upstream/dev' into feat/ui-revamp
This commit is contained in:
commit
f65bc81509
603 changed files with 45035 additions and 4652 deletions
|
|
@ -5,12 +5,12 @@ export const metadata: Metadata = {
|
|||
title: "What's New | SurfSense",
|
||||
description: "Latest product updates, feature releases, and news from SurfSense.",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/announcements",
|
||||
canonical: "https://www.surfsense.com/announcements",
|
||||
},
|
||||
openGraph: {
|
||||
title: "What's New | SurfSense",
|
||||
description: "Latest product updates, feature releases, and news from SurfSense.",
|
||||
url: "https://surfsense.com/announcements",
|
||||
url: "https://www.surfsense.com/announcements",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ 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 { ArticleJsonLd, FAQJsonLd } from "@/components/seo/json-ld";
|
||||
import { extractFaqFromBlogPost } from "@/lib/blog-faq";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { getMDXComponents } from "@/mdx-components";
|
||||
|
||||
|
|
@ -21,6 +22,8 @@ interface BlogData {
|
|||
author?: string;
|
||||
authorAvatar?: string;
|
||||
tags?: string[];
|
||||
// Populated by Fumadocs when `lastModifiedTime: "git"` is set in source.config.ts.
|
||||
lastModified?: Date;
|
||||
body: React.ComponentType<{
|
||||
components?: Record<string, React.ComponentType>;
|
||||
}>;
|
||||
|
|
@ -50,7 +53,7 @@ export async function generateMetadata(props: {
|
|||
title: `${page.data.title} | SurfSense Blog`,
|
||||
description: page.data.description,
|
||||
alternates: {
|
||||
canonical: `https://surfsense.com/blog/${slug}`,
|
||||
canonical: `https://www.surfsense.com/blog/${slug}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: page.data.title,
|
||||
|
|
@ -78,17 +81,23 @@ export default async function BlogPostPage(props: { params: Promise<{ slug: stri
|
|||
|
||||
const MDX = page.data.body;
|
||||
const date = new Date(page.data.date);
|
||||
const dateModified = page.data.lastModified
|
||||
? new Date(page.data.lastModified).toISOString()
|
||||
: undefined;
|
||||
const faqEntries = await extractFaqFromBlogPost(slug);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative pt-20">
|
||||
<ArticleJsonLd
|
||||
title={page.data.title}
|
||||
description={page.data.description}
|
||||
url={`https://surfsense.com/blog/${slug}`}
|
||||
url={`https://www.surfsense.com/blog/${slug}`}
|
||||
datePublished={page.data.date}
|
||||
dateModified={dateModified}
|
||||
author={page.data.author ?? "SurfSense Team"}
|
||||
image={page.data.image ? `https://surfsense.com${page.data.image}` : undefined}
|
||||
image={page.data.image ? `https://www.surfsense.com${page.data.image}` : undefined}
|
||||
/>
|
||||
{faqEntries.length > 0 && <FAQJsonLd questions={faqEntries} />}
|
||||
<div className="max-w-3xl mx-auto px-6 lg:px-10 pt-10 pb-20">
|
||||
<BreadcrumbNav
|
||||
items={[
|
||||
|
|
|
|||
|
|
@ -35,9 +35,7 @@ function SearchIcon({ className }: { className?: string }) {
|
|||
}
|
||||
|
||||
export function BlogWithSearchMagazine({ blogs }: { blogs: BlogEntry[] }) {
|
||||
const featured = blogs[0];
|
||||
|
||||
if (!featured) {
|
||||
if (blogs.length === 0) {
|
||||
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">
|
||||
|
|
@ -47,6 +45,17 @@ export function BlogWithSearchMagazine({ blogs }: { blogs: BlogEntry[] }) {
|
|||
);
|
||||
}
|
||||
|
||||
// `blogs` arrives pre-sorted from the server: explicitly featured posts
|
||||
// first (ordered by `featured_order` asc, then date desc), then the rest
|
||||
// by date desc. If nothing is explicitly featured, fall back to treating
|
||||
// the newest post as the cover so the layout never feels empty up top.
|
||||
// `MagazineSearchGrid` re-filters using `heroSlugs` so the hero/featured
|
||||
// posts never duplicate into the archive grid.
|
||||
const explicitlyFeatured = blogs.filter((b) => b.featured);
|
||||
const heroBlogs = explicitlyFeatured.length > 0 ? explicitlyFeatured : blogs.slice(0, 1);
|
||||
const heroSlugs = new Set(heroBlogs.map((b) => b.slug));
|
||||
const [coverStory, ...secondaryFeatured] = heroBlogs;
|
||||
|
||||
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)]" />
|
||||
|
|
@ -57,14 +66,36 @@ export function BlogWithSearchMagazine({ blogs }: { blogs: BlogEntry[] }) {
|
|||
</h1>
|
||||
</header>
|
||||
|
||||
<MagazineFeatured blog={featured} />
|
||||
<MagazineFeatured blog={coverStory} />
|
||||
|
||||
<MagazineSearchGrid blogs={blogs} featuredSlug={featured.slug} />
|
||||
{secondaryFeatured.length > 0 ? <MoreFeatured blogs={secondaryFeatured} /> : null}
|
||||
|
||||
<MagazineSearchGrid blogs={blogs} excludedSlugs={heroSlugs} />
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MoreFeatured({ blogs }: { blogs: BlogEntry[] }) {
|
||||
return (
|
||||
<section aria-labelledby="more-featured-heading" className="mb-14">
|
||||
<h2
|
||||
id="more-featured-heading"
|
||||
className="mb-6 font-serif text-2xl font-medium text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
More featured
|
||||
</h2>
|
||||
<ul className="grid gap-6 sm:grid-cols-2">
|
||||
{blogs.map((blog) => (
|
||||
<li key={blog.slug}>
|
||||
<MagazineCard blog={blog} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MagazineFeatured({ blog }: { blog: BlogEntry }) {
|
||||
return (
|
||||
<Link
|
||||
|
|
@ -112,10 +143,11 @@ function MagazineFeatured({ blog }: { blog: BlogEntry }) {
|
|||
|
||||
function MagazineSearchGrid({
|
||||
blogs: allBlogs,
|
||||
featuredSlug,
|
||||
excludedSlugs,
|
||||
}: {
|
||||
blogs: BlogEntry[];
|
||||
featuredSlug: string;
|
||||
/** Slugs already shown above the archive (cover story + "More featured"). */
|
||||
excludedSlugs: Set<string>;
|
||||
}) {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
|
|
@ -128,12 +160,15 @@ function MagazineSearchGrid({
|
|||
);
|
||||
|
||||
const gridItems = useMemo(() => {
|
||||
// When the reader is searching, surface every match (including
|
||||
// featured posts they may be looking for); otherwise hide the posts
|
||||
// that are already rendered as featured above the archive.
|
||||
const results = search.trim() ? searcher.search(search) : allBlogs;
|
||||
if (search.trim()) {
|
||||
return results;
|
||||
}
|
||||
return results.filter((b) => b.slug !== featuredSlug);
|
||||
}, [search, searcher, allBlogs, featuredSlug]);
|
||||
return results.filter((b) => !excludedSlugs.has(b.slug));
|
||||
}, [search, searcher, allBlogs, excludedSlugs]);
|
||||
|
||||
return (
|
||||
<section aria-labelledby="archive-heading">
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ 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",
|
||||
canonical: "https://www.surfsense.com/blog",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -25,6 +25,8 @@ export interface BlogEntry {
|
|||
image: string;
|
||||
author: string;
|
||||
authorAvatar: string;
|
||||
featured: boolean;
|
||||
featuredOrder?: number;
|
||||
}
|
||||
|
||||
export default async function BlogPage() {
|
||||
|
|
@ -38,6 +40,8 @@ export default async function BlogPage() {
|
|||
image?: string;
|
||||
author?: string;
|
||||
authorAvatar?: string;
|
||||
featured?: boolean;
|
||||
featured_order?: number;
|
||||
};
|
||||
}>;
|
||||
|
||||
|
|
@ -51,8 +55,20 @@ export default async function BlogPage() {
|
|||
image: page.data.image ?? "/og-image.png",
|
||||
author: page.data.author ?? "SurfSense Team",
|
||||
authorAvatar: page.data.authorAvatar ?? "/logo.png",
|
||||
featured: page.data.featured ?? false,
|
||||
featuredOrder: page.data.featured_order,
|
||||
}))
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
.sort((a, b) => {
|
||||
// Featured first; then by `featured_order` asc within featured;
|
||||
// then by `date` desc as the universal tie-breaker.
|
||||
if (a.featured !== b.featured) return a.featured ? -1 : 1;
|
||||
if (a.featured && b.featured) {
|
||||
const aOrder = a.featuredOrder ?? Number.POSITIVE_INFINITY;
|
||||
const bOrder = b.featuredOrder ?? Number.POSITIVE_INFINITY;
|
||||
if (aOrder !== bOrder) return aOrder - bOrder;
|
||||
}
|
||||
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
||||
});
|
||||
|
||||
return <BlogWithSearchMagazine blogs={blogs} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export const metadata: Metadata = {
|
|||
title: "Changelog | SurfSense",
|
||||
description: "See what's new in SurfSense. Latest updates, features, and improvements.",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/changelog",
|
||||
canonical: "https://www.surfsense.com/changelog",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const metadata: Metadata = {
|
|||
description:
|
||||
"Get in touch with the SurfSense team for enterprise AI search, knowledge management, or partnership inquiries.",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/contact",
|
||||
canonical: "https://www.surfsense.com/contact",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||
|
||||
const title = buildSeoTitle(model);
|
||||
const description = buildSeoDescription(model);
|
||||
const canonicalUrl = `https://surfsense.com/free/${model.seo_slug}`;
|
||||
const canonicalUrl = `https://www.surfsense.com/free/${model.seo_slug}`;
|
||||
const modelNameLower = model.name.toLowerCase();
|
||||
|
||||
return {
|
||||
|
|
@ -161,7 +161,7 @@ export default async function FreeModelPage({ params }: PageProps) {
|
|||
"@type": "WebApplication",
|
||||
name: `${model.name} Free Chat Without Login - SurfSense`,
|
||||
description,
|
||||
url: `https://surfsense.com/free/${model.seo_slug}`,
|
||||
url: `https://www.surfsense.com/free/${model.seo_slug}`,
|
||||
applicationCategory: "ChatApplication",
|
||||
operatingSystem: "Web",
|
||||
offers: {
|
||||
|
|
@ -173,12 +173,12 @@ export default async function FreeModelPage({ params }: PageProps) {
|
|||
provider: {
|
||||
"@type": "Organization",
|
||||
name: "SurfSense",
|
||||
url: "https://surfsense.com",
|
||||
url: "https://www.surfsense.com",
|
||||
},
|
||||
isPartOf: {
|
||||
"@type": "WebSite",
|
||||
name: "SurfSense",
|
||||
url: "https://surfsense.com",
|
||||
url: "https://www.surfsense.com",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -64,13 +64,13 @@ export const metadata: Metadata = {
|
|||
"notebooklm alternative",
|
||||
],
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/free",
|
||||
canonical: "https://www.surfsense.com/free",
|
||||
},
|
||||
openGraph: {
|
||||
title: "Free AI Chat, No Login Required | SurfSense",
|
||||
description:
|
||||
"Use ChatGPT free online without login. Chat with GPT-4, Claude AI, Gemini and 100+ AI models. Open source NotebookLM alternative.",
|
||||
url: "https://surfsense.com/free",
|
||||
url: "https://www.surfsense.com/free",
|
||||
siteName: "SurfSense",
|
||||
type: "website",
|
||||
images: [
|
||||
|
|
@ -164,8 +164,8 @@ export default async function FreeHubPage() {
|
|||
name: "ChatGPT Free Online Without Login - SurfSense",
|
||||
description:
|
||||
"Use ChatGPT, Claude AI, Gemini and more AI models free online without login or sign-up. Open source NotebookLM alternative with no login required.",
|
||||
url: "https://surfsense.com/free",
|
||||
isPartOf: { "@type": "WebSite", name: "SurfSense", url: "https://surfsense.com" },
|
||||
url: "https://www.surfsense.com/free",
|
||||
isPartOf: { "@type": "WebSite", name: "SurfSense", url: "https://www.surfsense.com" },
|
||||
mainEntity: {
|
||||
"@type": "ItemList",
|
||||
numberOfItems: seoModels.length,
|
||||
|
|
@ -173,7 +173,7 @@ export default async function FreeHubPage() {
|
|||
"@type": "ListItem",
|
||||
position: i + 1,
|
||||
name: m.name,
|
||||
url: `https://surfsense.com/free/${m.seo_slug}`,
|
||||
url: `https://www.surfsense.com/free/${m.seo_slug}`,
|
||||
})),
|
||||
},
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export const metadata: Metadata = {
|
|||
description:
|
||||
"Explore SurfSense plans and pricing. Start free with 500 pages & $5 in premium credits. Use ChatGPT, Claude AI, and premium AI models. Pay as you go at provider cost.",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/pricing",
|
||||
canonical: "https://www.surfsense.com/pricing",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const metadata: Metadata = {
|
|||
title: "Privacy Policy | SurfSense",
|
||||
description: "Privacy Policy for SurfSense application",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/privacy",
|
||||
canonical: "https://www.surfsense.com/privacy",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const metadata: Metadata = {
|
|||
title: "Terms of Service | SurfSense",
|
||||
description: "Terms of Service for SurfSense application",
|
||||
alternates: {
|
||||
canonical: "https://surfsense.com/terms",
|
||||
canonical: "https://www.surfsense.com/terms",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue