feat: no login experience and prem tokens
Some checks are pending
Build and Push Docker Images / tag_release (push) Waiting to run
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Blocked by required conditions

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-15 17:02:00 -07:00
parent 87452bb315
commit ff4e0f9b62
68 changed files with 5914 additions and 121 deletions

View file

@ -0,0 +1,24 @@
"use client";
import type { ReactNode } from "react";
import { use } from "react";
import { FreeLayoutDataProvider } from "@/components/layout/providers/FreeLayoutDataProvider";
import { AnonymousModeProvider } from "@/contexts/anonymous-mode";
import { LoginGateProvider } from "@/contexts/login-gate";
interface FreeModelLayoutProps {
children: ReactNode;
params: Promise<{ model_slug: string }>;
}
export default function FreeModelLayout({ children, params }: FreeModelLayoutProps) {
const { model_slug } = use(params);
return (
<AnonymousModeProvider initialModelSlug={model_slug}>
<LoginGateProvider>
<FreeLayoutDataProvider>{children}</FreeLayoutDataProvider>
</LoginGateProvider>
</AnonymousModeProvider>
);
}

View file

@ -0,0 +1,254 @@
import { SquareArrowOutUpRight } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { FreeChatPage } from "@/components/free-chat/free-chat-page";
import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
import { FAQJsonLd, JsonLd } from "@/components/seo/json-ld";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import type { AnonModel } from "@/contracts/types/anonymous-chat.types";
import { BACKEND_URL } from "@/lib/env-config";
interface PageProps {
params: Promise<{ model_slug: string }>;
}
async function getModel(slug: string): Promise<AnonModel | null> {
try {
const res = await fetch(
`${BACKEND_URL}/api/v1/public/anon-chat/models/${encodeURIComponent(slug)}`,
{ next: { revalidate: 300 } }
);
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
async function getAllModels(): Promise<AnonModel[]> {
try {
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
next: { revalidate: 300 },
});
if (!res.ok) return [];
return res.json();
} catch {
return [];
}
}
function buildSeoTitle(model: AnonModel): string {
if (model.seo_title) return model.seo_title;
return `${model.name} Free Online Without Login | No Sign-Up AI Chat | SurfSense`;
}
function buildSeoDescription(model: AnonModel): string {
if (model.seo_description) return model.seo_description;
return `Use ${model.name} free online without login. No sign-up required. Chat with ${model.name} by ${model.provider} instantly on SurfSense, the open source ChatGPT alternative with no login.`;
}
function buildModelFaq(model: AnonModel) {
return [
{
question: `Can I use ${model.name} without login?`,
answer: `Yes. ${model.name} is available on SurfSense without login. No account creation, no email, no password. Just open the page and start chatting with ${model.name} for free.`,
},
{
question: `Is ${model.name} really free on SurfSense?`,
answer: `Yes! You can use ${model.name} completely free without login or sign-up. SurfSense gives you 1 million free tokens to use across any model, including ${model.name}.`,
},
{
question: `How do I use ${model.name} with no login?`,
answer: `Just start typing in the chat box above. ${model.name} will respond instantly. No login wall, no sign-up form, no verification. Your conversations are not stored in any database.`,
},
{
question: `What can I do with ${model.name} on SurfSense?`,
answer: `You can ask questions, get explanations, write content, brainstorm ideas, debug code, and more. ${model.name} is a powerful AI assistant available for free without login on SurfSense.`,
},
{
question: `How is SurfSense different from using ${model.name} directly?`,
answer: `SurfSense gives you free access without login to ${model.name} and many other AI models in one place. Create a free account to unlock document Q&A, team collaboration, and integrations with Slack, Google Drive, Notion, and more.`,
},
];
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { model_slug } = await params;
const model = await getModel(model_slug);
if (!model) return { title: "Model Not Found | SurfSense" };
const title = buildSeoTitle(model);
const description = buildSeoDescription(model);
const canonicalUrl = `https://surfsense.com/free/${model.seo_slug}`;
const modelNameLower = model.name.toLowerCase();
return {
title,
description,
alternates: { canonical: canonicalUrl },
keywords: [
`${modelNameLower} free`,
`free ${modelNameLower}`,
`${modelNameLower} online`,
`${modelNameLower} online free`,
`${modelNameLower} without login`,
`${modelNameLower} no login`,
`${modelNameLower} no sign up`,
`${modelNameLower} login free`,
`${modelNameLower} free without login`,
`${modelNameLower} free no login`,
`${modelNameLower} chat free`,
`${modelNameLower} free online`,
`use ${modelNameLower} for free`,
`use ${modelNameLower} without login`,
`${modelNameLower} alternative`,
`${modelNameLower} alternative free`,
"chatgpt no login",
"chatgpt without login",
"free ai chat no login",
"ai chat without login",
"free ai apps",
],
openGraph: {
title,
description,
url: canonicalUrl,
siteName: "SurfSense",
type: "website",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: `${model.name} Free Chat on SurfSense`,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: ["/og-image.png"],
},
};
}
export async function generateStaticParams() {
const models = await getAllModels();
return models.filter((m) => m.seo_slug).map((m) => ({ model_slug: m.seo_slug! }));
}
export default async function FreeModelPage({ params }: PageProps) {
const { model_slug } = await params;
const [model, allModels] = await Promise.all([getModel(model_slug), getAllModels()]);
if (!model) notFound();
const description = buildSeoDescription(model);
const faqItems = buildModelFaq(model);
const relatedModels = allModels
.filter((m) => m.seo_slug && m.seo_slug !== model.seo_slug)
.slice(0, 4);
return (
<>
{/* Invisible SEO metadata */}
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "WebApplication",
name: `${model.name} Free Chat Without Login - SurfSense`,
description,
url: `https://surfsense.com/free/${model.seo_slug}`,
applicationCategory: "ChatApplication",
operatingSystem: "Web",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
description: `Free access to ${model.name} AI chat without login`,
},
provider: {
"@type": "Organization",
name: "SurfSense",
url: "https://surfsense.com",
},
isPartOf: {
"@type": "WebSite",
name: "SurfSense",
url: "https://surfsense.com",
},
}}
/>
<FAQJsonLd questions={faqItems} />
{/* Chat fills the entire viewport area inside LayoutShell */}
<div className="h-full">
<FreeChatPage />
</div>
{/* SEO content: in DOM for crawlers, clipped by parent overflow-hidden */}
<div className="border-t bg-background">
<article className="container mx-auto px-4 py-10 max-w-3xl">
<BreadcrumbNav
items={[
{ name: "Home", href: "/" },
{ name: "Free AI Chat", href: "/free" },
{ name: model.name, href: `/free/${model.seo_slug}` },
]}
/>
<header className="mt-6 mb-6">
<h1 className="text-2xl font-bold mb-2">Chat with {model.name} Free, No Login</h1>
<p className="text-sm text-muted-foreground leading-relaxed">
Use <strong>{model.name}</strong> free online without login or sign-up. No account, no
email, no password needed. Powered by SurfSense.
</p>
</header>
<Separator className="my-8" />
<section>
<h2 className="text-xl font-bold mb-4">
{model.name} Free: Frequently Asked Questions
</h2>
<dl className="flex flex-col gap-3">
{faqItems.map((item) => (
<div key={item.question} className="rounded-lg border bg-card p-4">
<dt className="font-medium text-sm">{item.question}</dt>
<dd className="mt-1.5 text-sm text-muted-foreground leading-relaxed">
{item.answer}
</dd>
</div>
))}
</dl>
</section>
{relatedModels.length > 0 && (
<>
<Separator className="my-8" />
<nav aria-label="Other free AI models">
<h2 className="text-xl font-bold mb-4">Try Other Free AI Models</h2>
<div className="flex flex-wrap gap-2">
{relatedModels.map((m) => (
<Button key={m.id} variant="outline" size="sm" asChild>
<Link href={`/free/${m.seo_slug}`}>
{m.name}
<SquareArrowOutUpRight className="size-3" />
</Link>
</Button>
))}
<Button variant="outline" size="sm" asChild>
<Link href="/free">View All Models</Link>
</Button>
</div>
</nav>
</>
)}
</article>
</div>
</>
);
}

View file

@ -0,0 +1,387 @@
import { SquareArrowOutUpRight } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
import { FAQJsonLd, JsonLd } from "@/components/seo/json-ld";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { AnonModel } from "@/contracts/types/anonymous-chat.types";
import { BACKEND_URL } from "@/lib/env-config";
export const metadata: Metadata = {
title: "ChatGPT Free Online Without Login | Chat GPT No Login, Claude AI Free | SurfSense",
description:
"Use ChatGPT free online without login. Chat with GPT-4, Claude AI, Gemini and more for free. No sign-up required. Open source NotebookLM alternative with free AI chat and document Q&A.",
keywords: [
"chatgpt free",
"chat gpt free",
"free chatgpt",
"free chat gpt",
"chatgpt online",
"chat gpt online",
"online chatgpt",
"chatgpt free online",
"chatgpt online free",
"chat gpt free online",
"chatgpt no login",
"chatgpt without login",
"chat gpt login free",
"chat gpt login",
"free chatgpt without login",
"free chatgpt no login",
"ai chat no login",
"ai chat without login",
"claude ai without login",
"claude no login",
"chatgpt for free",
"gpt chat free",
"claude ai free",
"claude free",
"free claude ai",
"free claude",
"chatgpt alternative free",
"free chatgpt alternative",
"chatgpt free alternative",
"free alternative to chatgpt",
"alternative to chatgpt free",
"ai like chatgpt",
"sites like chatgpt",
"free ai chatbot like chatgpt",
"free ai chatbots like chatgpt",
"apps like chatgpt for free",
"best free alternative to chatgpt",
"free ai apps",
"ai with no restrictions",
"notebooklm alternative",
],
alternates: {
canonical: "https://surfsense.com/free",
},
openGraph: {
title: "ChatGPT Free Online Without Login | Claude AI Free No Login | 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",
siteName: "SurfSense",
type: "website",
images: [
{
url: "/og-image.png",
width: 1200,
height: 630,
alt: "SurfSense - ChatGPT Free Online, Claude AI Free, No Login Required",
},
],
},
twitter: {
card: "summary_large_image",
title: "ChatGPT Free Online Without Login | Claude AI Free No Login | SurfSense",
description:
"Use ChatGPT free online without login. Chat with GPT-4, Claude AI, Gemini and more. No sign-up needed.",
images: ["/og-image.png"],
},
};
async function getModels(): Promise<AnonModel[]> {
try {
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
next: { revalidate: 300 },
});
if (!res.ok) return [];
return res.json();
} catch {
return [];
}
}
const FAQ_ITEMS = [
{
question: "Can I use ChatGPT without login?",
answer:
"Yes. SurfSense lets you use ChatGPT without login or any sign-up. Just pick a model and start chatting. No email, no password, no account needed. You get 1 million free tokens to use across ChatGPT, Claude AI, Gemini, and other models.",
},
{
question: "Is ChatGPT really free on SurfSense?",
answer:
"Yes. SurfSense gives you free access to ChatGPT (GPT-4), Claude AI, Gemini, and other models without login. You get 1 million free tokens across any model with no sign-up required.",
},
{
question: "How do I use ChatGPT no login?",
answer:
"Go to any model page on SurfSense and start typing your message. There is no login wall, no account creation, and no verification step. ChatGPT no login works instantly in your browser.",
},
{
question: "What AI models can I use for free without login?",
answer:
"SurfSense offers free access without login to models from OpenAI (GPT-4, GPT-4 Turbo), Anthropic (Claude 3, Claude free), Google (Gemini), DeepSeek, Mistral, Llama, and more. All available as a free ChatGPT alternative online with no login required.",
},
{
question: "What happens after I use 1 million free tokens?",
answer:
"After your free tokens, create a free SurfSense account to unlock 5 million more. Premium model tokens can be purchased at $1 per million tokens. Non-premium models remain unlimited for registered users.",
},
{
question: "Is Claude AI available without login?",
answer:
"Yes. You can use Claude AI free without login on SurfSense. Both Claude 3 and other Anthropic models are available with no sign-up, alongside ChatGPT and Gemini.",
},
{
question: "How is SurfSense different from ChatGPT?",
answer:
"SurfSense is an open source NotebookLM alternative that gives you access to multiple AI models in one place without login. Unlike ChatGPT alone, SurfSense includes document Q&A with citations, integrations with Slack, Google Drive, Notion, and Confluence, plus team collaboration features.",
},
{
question: "Is SurfSense a free ChatGPT alternative?",
answer:
"Yes. SurfSense is a free, open source alternative to ChatGPT that works without login. It gives you access to Claude AI free, Gemini, and other AI models alongside document Q&A with citations, team collaboration, and 30+ integrations.",
},
{
question: "Is my data private when using free AI chat without login?",
answer:
"Anonymous chat sessions are not stored in any database. No account means no personal data collected. SurfSense is open source, so you can self-host for complete data control and privacy.",
},
];
export default async function FreeHubPage() {
const models = await getModels();
const seoModels = models.filter((m) => m.seo_slug);
return (
<main className="min-h-screen pt-20">
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "CollectionPage",
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" },
mainEntity: {
"@type": "ItemList",
numberOfItems: seoModels.length,
itemListElement: seoModels.map((m, i) => ({
"@type": "ListItem",
position: i + 1,
name: m.name,
url: `https://surfsense.com/free/${m.seo_slug}`,
})),
},
}}
/>
<FAQJsonLd questions={FAQ_ITEMS} />
<article className="container mx-auto px-4 pb-20">
<BreadcrumbNav
items={[
{ name: "Home", href: "/" },
{ name: "Free AI Chat", href: "/free" },
]}
/>
{/* Hero */}
<section className="mt-8 text-center max-w-3xl mx-auto">
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">
ChatGPT Free Online Without Login
</h1>
<p className="mt-4 text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
Use <strong>ChatGPT</strong>, <strong>Claude AI</strong>, <strong>Gemini</strong>, and
other AI models free online without login. No sign-up, no email, no password. Pick a
model and start chatting instantly.
</p>
<div className="flex flex-wrap items-center justify-center gap-3 mt-6">
<Badge variant="secondary" className="px-3 py-1.5 text-sm">
No login required
</Badge>
<Badge variant="secondary" className="px-3 py-1.5 text-sm">
1M free tokens
</Badge>
<Badge variant="secondary" className="px-3 py-1.5 text-sm">
{seoModels.length} AI models
</Badge>
<Badge variant="secondary" className="px-3 py-1.5 text-sm">
Open source
</Badge>
</div>
</section>
<Separator className="my-12 max-w-4xl mx-auto" />
{/* Model Table */}
{seoModels.length > 0 ? (
<section
className="max-w-4xl mx-auto"
aria-label="Free AI models available without login"
>
<h2 className="text-2xl font-bold mb-2">Free AI Models Available Without Login</h2>
<p className="text-sm text-muted-foreground mb-6">
All models below work without login or sign-up. Click any model to start a free AI
chat instantly.
</p>
<div className="overflow-hidden rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[45%]">Model</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Tier</TableHead>
<TableHead className="text-right w-[100px]" />
</TableRow>
</TableHeader>
<TableBody>
{seoModels.map((model) => (
<TableRow key={model.id}>
<TableCell>
<Link
href={`/free/${model.seo_slug}`}
className="group flex flex-col gap-0.5"
>
<span className="font-medium group-hover:underline">{model.name}</span>
{model.description && (
<span className="text-xs text-muted-foreground line-clamp-1">
{model.description}
</span>
)}
</Link>
</TableCell>
<TableCell>
<Badge variant="outline">{model.provider}</Badge>
</TableCell>
<TableCell>
{model.is_premium ? (
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300 border-0">
Premium
</Badge>
) : (
<Badge variant="secondary">Free</Badge>
)}
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" asChild>
<Link href={`/free/${model.seo_slug}`}>
Chat
<SquareArrowOutUpRight className="size-3" />
</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</section>
) : (
<section className="mt-12 text-center max-w-4xl mx-auto">
<p className="text-muted-foreground">
No models are currently available. Please check back later.
</p>
</section>
)}
<Separator className="my-12 max-w-4xl mx-auto" />
{/* Why SurfSense */}
<section className="max-w-4xl mx-auto">
<h2 className="text-2xl font-bold mb-6">
Why Use SurfSense as Your Free ChatGPT Alternative
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="rounded-lg border bg-card p-5">
<h3 className="font-semibold mb-1.5">Multiple AI Models in One Place</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
Access ChatGPT, Claude AI free, Gemini, DeepSeek, and more. Works like sites like
ChatGPT but with all AI models available, not just GPT. A true free AI chatbot like
ChatGPT and beyond.
</p>
</div>
<div className="rounded-lg border bg-card p-5">
<h3 className="font-semibold mb-1.5">No Login, No Sign-Up Required</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
Start using ChatGPT free online immediately. No email, no password, no verification.
Get ChatGPT no login access and Claude AI free access from one platform. AI with no
restrictions on which model you can use.
</p>
</div>
<div className="rounded-lg border bg-card p-5">
<h3 className="font-semibold mb-1.5">Open Source NotebookLM Alternative</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
SurfSense is a free, open source NotebookLM alternative with document Q&A and
citations, integrations with Slack, Google Drive, Notion, and Confluence, plus team
collaboration and self-hosting support.
</p>
</div>
</div>
</section>
<Separator className="my-12 max-w-4xl mx-auto" />
{/* CTA */}
<section className="max-w-3xl mx-auto text-center">
<h2 className="text-2xl font-bold mb-3">Want More Features?</h2>
<p className="text-muted-foreground mb-6 leading-relaxed">
Create a free SurfSense account to unlock 5 million tokens, document uploads with
citations, team collaboration, and integrations with Slack, Google Drive, Notion, and
30+ more tools.
</p>
<Button size="lg" asChild>
<Link href="/register">Create Free Account</Link>
</Button>
</section>
<Separator className="my-12 max-w-4xl mx-auto" />
{/* FAQ */}
<section className="max-w-3xl mx-auto">
<h2 className="text-2xl font-bold text-center mb-8">Frequently Asked Questions</h2>
<dl className="flex flex-col gap-4">
{FAQ_ITEMS.map((item) => (
<div key={item.question} className="rounded-lg border bg-card p-5">
<dt className="font-medium text-sm">{item.question}</dt>
<dd className="mt-2 text-sm text-muted-foreground leading-relaxed">
{item.answer}
</dd>
</div>
))}
</dl>
</section>
{/* Internal links */}
<nav aria-label="Related pages" className="mt-16 max-w-3xl mx-auto">
<h2 className="text-lg font-semibold mb-3">Explore SurfSense</h2>
<ul className="flex flex-wrap gap-2">
<li>
<Button variant="outline" size="sm" asChild>
<Link href="/pricing">Pricing</Link>
</Button>
</li>
<li>
<Button variant="outline" size="sm" asChild>
<Link href="/docs">Documentation</Link>
</Button>
</li>
<li>
<Button variant="outline" size="sm" asChild>
<Link href="/blog">Blog</Link>
</Button>
</li>
<li>
<Button variant="outline" size="sm" asChild>
<Link href="/register">Sign Up Free</Link>
</Button>
</li>
</ul>
</nav>
</article>
</main>
);
}

View file

@ -7,6 +7,11 @@ import { Navbar } from "@/components/homepage/navbar";
export default function HomePageLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const isAuthPage = pathname === "/login" || pathname === "/register";
const isFreeModelChat = /^\/free\/[^/]+$/.test(pathname);
if (isFreeModelChat) {
return <>{children}</>;
}
return (
<main className="min-h-screen bg-linear-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white overflow-x-hidden">

View file

@ -0,0 +1,49 @@
"use client";
import { motion } from "motion/react";
import { useState } from "react";
import { BuyPagesContent } from "@/components/settings/buy-pages-content";
import { BuyTokensContent } from "@/components/settings/buy-tokens-content";
import { cn } from "@/lib/utils";
const TABS = [
{ id: "pages", label: "Pages" },
{ id: "tokens", label: "Premium Tokens" },
] as const;
type TabId = (typeof TABS)[number]["id"];
export default function BuyMorePage() {
const [activeTab, setActiveTab] = useState<TabId>("pages");
return (
<div className="flex min-h-[calc(100vh-64px)] select-none items-center justify-center px-4 py-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full max-w-md space-y-6"
>
<div className="flex items-center justify-center rounded-lg border bg-muted/30 p-1">
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
activeTab === tab.id
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
{tab.label}
</button>
))}
</div>
{activeTab === "pages" ? <BuyPagesContent /> : <BuyTokensContent />}
</motion.div>
</div>
);
}

View file

@ -1,19 +1,16 @@
"use client";
import { motion } from "motion/react";
import { BuyPagesContent } from "@/components/settings/buy-pages-content";
import { useParams, useRouter } from "next/navigation";
import { useEffect } from "react";
export default function BuyPagesPage() {
return (
<div className="flex min-h-[calc(100vh-64px)] select-none items-center justify-center px-4 py-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full max-w-md space-y-6"
>
<BuyPagesContent />
</motion.div>
</div>
);
const router = useRouter();
const params = useParams();
const searchSpaceId = params?.search_space_id ?? "";
useEffect(() => {
router.replace(`/dashboard/${searchSpaceId}/buy-more`);
}, [router, searchSpaceId]);
return null;
}

View file

@ -0,0 +1,16 @@
"use client";
import { useParams, useRouter } from "next/navigation";
import { useEffect } from "react";
export default function BuyTokensPage() {
const router = useRouter();
const params = useParams();
const searchSpaceId = params?.search_space_id ?? "";
useEffect(() => {
router.replace(`/dashboard/${searchSpaceId}/buy-more`);
}, [router, searchSpaceId]);
return null;
}

View file

@ -23,16 +23,14 @@ export default function PurchaseCancelPage() {
<CardHeader className="text-center">
<CircleSlash2 className="mx-auto h-10 w-10 text-muted-foreground" />
<CardTitle className="text-2xl">Checkout canceled</CardTitle>
<CardDescription>
No charge was made and your current pages are unchanged.
</CardDescription>
<CardDescription>No charge was made and your account is unchanged.</CardDescription>
</CardHeader>
<CardContent className="text-center text-sm text-muted-foreground">
You can return to the pricing options and try again whenever you&apos;re ready.
</CardContent>
<CardFooter className="flex flex-col gap-2 sm:flex-row">
<Button asChild className="w-full">
<Link href={`/dashboard/${searchSpaceId}/more-pages`}>Back to Buy Pages</Link>
<Link href={`/dashboard/${searchSpaceId}/buy-more`}>Back to Pricing</Link>
</Button>
<Button asChild variant="outline" className="w-full">
<Link href={`/dashboard/${searchSpaceId}/new-chat`}>Back to Dashboard</Link>

View file

@ -23,6 +23,7 @@ export default function PurchaseSuccessPage() {
useEffect(() => {
void queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });
void queryClient.invalidateQueries({ queryKey: ["token-status"] });
}, [queryClient]);
return (
@ -31,13 +32,11 @@ export default function PurchaseSuccessPage() {
<CardHeader className="text-center">
<CheckCircle2 className="mx-auto h-10 w-10 text-emerald-500" />
<CardTitle className="text-2xl">Purchase complete</CardTitle>
<CardDescription>
Your additional pages are being applied to your account now.
</CardDescription>
<CardDescription>Your purchase is being applied to your account now.</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-center">
<p className="text-sm text-muted-foreground">
Your sidebar usage meter should refresh automatically in a moment.
Your usage meters should refresh automatically in a moment.
</p>
</CardContent>
<CardFooter className="flex flex-col gap-2">
@ -45,7 +44,7 @@ export default function PurchaseSuccessPage() {
<Link href={`/dashboard/${searchSpaceId}/new-chat`}>Back to Dashboard</Link>
</Button>
<Button asChild variant="outline" className="w-full">
<Link href={`/dashboard/${searchSpaceId}/more-pages`}>Buy More Pages</Link>
<Link href={`/dashboard/${searchSpaceId}/buy-more`}>Buy More</Link>
</Button>
</CardFooter>
</Card>

View file

@ -14,14 +14,31 @@ const changelogSource = loader({
});
const BASE_URL = "https://surfsense.com";
const BACKEND_URL = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
export default function sitemap(): MetadataRoute.Sitemap {
async function getFreeModelSlugs(): Promise<string[]> {
try {
const res = await fetch(`${BACKEND_URL}/api/v1/public/anon-chat/models`, {
next: { revalidate: 3600 },
});
if (!res.ok) return [];
const models = await res.json();
return models
.filter((m: { seo_slug?: string }) => m.seo_slug)
.map((m: { seo_slug: string }) => m.seo_slug);
} catch {
return [];
}
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const now = new Date();
now.setMinutes(0, 0, 0);
const lastModified = now;
const staticPages: MetadataRoute.Sitemap = [
{ url: `${BASE_URL}/`, lastModified, changeFrequency: "daily", priority: 1 },
{ url: `${BASE_URL}/free`, lastModified, changeFrequency: "daily", priority: 0.95 },
{ 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 },
@ -34,6 +51,14 @@ export default function sitemap(): MetadataRoute.Sitemap {
{ url: `${BASE_URL}/register`, lastModified, changeFrequency: "monthly", priority: 0.5 },
];
const slugs = await getFreeModelSlugs();
const freeModelPages: MetadataRoute.Sitemap = slugs.map((slug) => ({
url: `${BASE_URL}/free/${slug}`,
lastModified,
changeFrequency: "daily" as const,
priority: 0.9,
}));
const docsPages: MetadataRoute.Sitemap = docsSource.getPages().map((page) => ({
url: `${BASE_URL}${page.url}`,
lastModified,
@ -55,5 +80,5 @@ export default function sitemap(): MetadataRoute.Sitemap {
priority: 0.5,
}));
return [...staticPages, ...docsPages, ...blogPages, ...changelogPages];
return [...staticPages, ...freeModelPages, ...docsPages, ...blogPages, ...changelogPages];
}