fix: update URLs to use the "www" subdomain across the application

This commit modifies various metadata and canonical URLs in the SurfSense application to ensure consistency by using "https://www.surfsense.com" instead of "https://surfsense.com". Changes were made in layout files, blog posts, and SEO components to reflect this update.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-05-15 12:35:15 -07:00
parent dc88ce0277
commit 219a5977b7
18 changed files with 105 additions and 39 deletions

View file

@ -5,12 +5,12 @@ export const metadata: Metadata = {
title: "Announcements | SurfSense", title: "Announcements | SurfSense",
description: "Latest product updates, feature releases, and news from SurfSense.", description: "Latest product updates, feature releases, and news from SurfSense.",
alternates: { alternates: {
canonical: "https://surfsense.com/announcements", canonical: "https://www.surfsense.com/announcements",
}, },
openGraph: { openGraph: {
title: "Announcements | SurfSense", title: "Announcements | SurfSense",
description: "Latest product updates, feature releases, and news from SurfSense.", description: "Latest product updates, feature releases, and news from SurfSense.",
url: "https://surfsense.com/announcements", url: "https://www.surfsense.com/announcements",
type: "website", type: "website",
}, },
twitter: { twitter: {

View file

@ -4,7 +4,8 @@ import Image from "next/image";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { blog } from "@/.source/server"; import { blog } from "@/.source/server";
import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav"; 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 { formatDate } from "@/lib/utils";
import { getMDXComponents } from "@/mdx-components"; import { getMDXComponents } from "@/mdx-components";
@ -21,6 +22,8 @@ interface BlogData {
author?: string; author?: string;
authorAvatar?: string; authorAvatar?: string;
tags?: string[]; tags?: string[];
// Populated by Fumadocs when `lastModifiedTime: "git"` is set in source.config.ts.
lastModified?: Date;
body: React.ComponentType<{ body: React.ComponentType<{
components?: Record<string, React.ComponentType>; components?: Record<string, React.ComponentType>;
}>; }>;
@ -50,7 +53,7 @@ export async function generateMetadata(props: {
title: `${page.data.title} | SurfSense Blog`, title: `${page.data.title} | SurfSense Blog`,
description: page.data.description, description: page.data.description,
alternates: { alternates: {
canonical: `https://surfsense.com/blog/${slug}`, canonical: `https://www.surfsense.com/blog/${slug}`,
}, },
openGraph: { openGraph: {
title: page.data.title, title: page.data.title,
@ -78,17 +81,23 @@ export default async function BlogPostPage(props: { params: Promise<{ slug: stri
const MDX = page.data.body; const MDX = page.data.body;
const date = new Date(page.data.date); 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 ( return (
<div className="min-h-screen relative pt-20"> <div className="min-h-screen relative pt-20">
<ArticleJsonLd <ArticleJsonLd
title={page.data.title} title={page.data.title}
description={page.data.description} description={page.data.description}
url={`https://surfsense.com/blog/${slug}`} url={`https://www.surfsense.com/blog/${slug}`}
datePublished={page.data.date} datePublished={page.data.date}
dateModified={dateModified}
author={page.data.author ?? "SurfSense Team"} 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"> <div className="max-w-3xl mx-auto px-6 lg:px-10 pt-10 pb-20">
<BreadcrumbNav <BreadcrumbNav
items={[ items={[

View file

@ -7,7 +7,7 @@ export const metadata: Metadata = {
title: "Blog | SurfSense - AI Search & Knowledge Management", title: "Blog | SurfSense - AI Search & Knowledge Management",
description: "Product updates, tutorials, and tips from the SurfSense team.", description: "Product updates, tutorials, and tips from the SurfSense team.",
alternates: { alternates: {
canonical: "https://surfsense.com/blog", canonical: "https://www.surfsense.com/blog",
}, },
}; };

View file

@ -9,7 +9,7 @@ export const metadata: Metadata = {
title: "Changelog | SurfSense", title: "Changelog | SurfSense",
description: "See what's new in SurfSense. Latest updates, features, and improvements.", description: "See what's new in SurfSense. Latest updates, features, and improvements.",
alternates: { alternates: {
canonical: "https://surfsense.com/changelog", canonical: "https://www.surfsense.com/changelog",
}, },
}; };

View file

@ -6,7 +6,7 @@ export const metadata: Metadata = {
description: description:
"Get in touch with the SurfSense team for enterprise AI search, knowledge management, or partnership inquiries.", "Get in touch with the SurfSense team for enterprise AI search, knowledge management, or partnership inquiries.",
alternates: { alternates: {
canonical: "https://surfsense.com/contact", canonical: "https://www.surfsense.com/contact",
}, },
}; };

View file

@ -81,7 +81,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
const title = buildSeoTitle(model); const title = buildSeoTitle(model);
const description = buildSeoDescription(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(); const modelNameLower = model.name.toLowerCase();
return { return {
@ -161,7 +161,7 @@ export default async function FreeModelPage({ params }: PageProps) {
"@type": "WebApplication", "@type": "WebApplication",
name: `${model.name} Free Chat Without Login - SurfSense`, name: `${model.name} Free Chat Without Login - SurfSense`,
description, description,
url: `https://surfsense.com/free/${model.seo_slug}`, url: `https://www.surfsense.com/free/${model.seo_slug}`,
applicationCategory: "ChatApplication", applicationCategory: "ChatApplication",
operatingSystem: "Web", operatingSystem: "Web",
offers: { offers: {
@ -173,12 +173,12 @@ export default async function FreeModelPage({ params }: PageProps) {
provider: { provider: {
"@type": "Organization", "@type": "Organization",
name: "SurfSense", name: "SurfSense",
url: "https://surfsense.com", url: "https://www.surfsense.com",
}, },
isPartOf: { isPartOf: {
"@type": "WebSite", "@type": "WebSite",
name: "SurfSense", name: "SurfSense",
url: "https://surfsense.com", url: "https://www.surfsense.com",
}, },
}} }}
/> />

View file

@ -64,13 +64,13 @@ export const metadata: Metadata = {
"notebooklm alternative", "notebooklm alternative",
], ],
alternates: { alternates: {
canonical: "https://surfsense.com/free", canonical: "https://www.surfsense.com/free",
}, },
openGraph: { openGraph: {
title: "Free AI Chat, No Login Required | SurfSense", title: "Free AI Chat, No Login Required | SurfSense",
description: description:
"Use ChatGPT free online without login. Chat with GPT-4, Claude AI, Gemini and 100+ AI models. Open source NotebookLM alternative.", "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", siteName: "SurfSense",
type: "website", type: "website",
images: [ images: [
@ -164,8 +164,8 @@ export default async function FreeHubPage() {
name: "ChatGPT Free Online Without Login - SurfSense", name: "ChatGPT Free Online Without Login - SurfSense",
description: 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.", "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", url: "https://www.surfsense.com/free",
isPartOf: { "@type": "WebSite", name: "SurfSense", url: "https://surfsense.com" }, isPartOf: { "@type": "WebSite", name: "SurfSense", url: "https://www.surfsense.com" },
mainEntity: { mainEntity: {
"@type": "ItemList", "@type": "ItemList",
numberOfItems: seoModels.length, numberOfItems: seoModels.length,
@ -173,7 +173,7 @@ export default async function FreeHubPage() {
"@type": "ListItem", "@type": "ListItem",
position: i + 1, position: i + 1,
name: m.name, name: m.name,
url: `https://surfsense.com/free/${m.seo_slug}`, url: `https://www.surfsense.com/free/${m.seo_slug}`,
})), })),
}, },
}} }}

View file

@ -7,7 +7,7 @@ export const metadata: Metadata = {
description: 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.", "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: { alternates: {
canonical: "https://surfsense.com/pricing", canonical: "https://www.surfsense.com/pricing",
}, },
}; };

View file

@ -4,7 +4,7 @@ export const metadata: Metadata = {
title: "Privacy Policy | SurfSense", title: "Privacy Policy | SurfSense",
description: "Privacy Policy for SurfSense application", description: "Privacy Policy for SurfSense application",
alternates: { alternates: {
canonical: "https://surfsense.com/privacy", canonical: "https://www.surfsense.com/privacy",
}, },
}; };

View file

@ -4,7 +4,7 @@ export const metadata: Metadata = {
title: "Terms of Service | SurfSense", title: "Terms of Service | SurfSense",
description: "Terms of Service for SurfSense application", description: "Terms of Service for SurfSense application",
alternates: { alternates: {
canonical: "https://surfsense.com/terms", canonical: "https://www.surfsense.com/terms",
}, },
}; };

View file

@ -50,7 +50,7 @@ export async function generateMetadata(props: { params: Promise<{ slug?: string[
title: `${page.data.title} | SurfSense Docs`, title: `${page.data.title} | SurfSense Docs`,
description: page.data.description, description: page.data.description,
alternates: { alternates: {
canonical: `https://surfsense.com/docs${slugPath ? `/${slugPath}` : ""}`, canonical: `https://www.surfsense.com/docs${slugPath ? `/${slugPath}` : ""}`,
}, },
openGraph: { openGraph: {
title: `${page.data.title} | SurfSense Docs`, title: `${page.data.title} | SurfSense Docs`,

View file

@ -41,9 +41,9 @@ export const viewport: Viewport = {
}; };
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL("https://surfsense.com"), metadataBase: new URL("https://www.surfsense.com"),
alternates: { alternates: {
canonical: "https://surfsense.com", canonical: "https://www.surfsense.com",
}, },
title: "SurfSense Open Source, Privacy-Focused NotebookLM Alternative for Teams", title: "SurfSense Open Source, Privacy-Focused NotebookLM Alternative for Teams",
description: description:
@ -90,7 +90,7 @@ export const metadata: Metadata = {
title: "SurfSense Open Source, Privacy-Focused NotebookLM Alternative for Teams", title: "SurfSense Open Source, Privacy-Focused NotebookLM Alternative for Teams",
description: description:
"Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude, and any AI model for free.", "Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude, and any AI model for free.",
url: "https://surfsense.com", url: "https://www.surfsense.com",
siteName: "SurfSense", siteName: "SurfSense",
type: "website", type: "website",
images: [ images: [

View file

@ -17,6 +17,6 @@ export default function robots(): MetadataRoute.Robots {
], ],
}, },
], ],
sitemap: "https://surfsense.com/sitemap.xml", sitemap: "https://www.surfsense.com/sitemap.xml",
}; };
} }

View file

@ -40,7 +40,7 @@ The promise of the search query "AI without login" is simple: someone wants to c
Whatever the reason, the answer in 2026 is mostly: **yes, you can do this**, but the options are scattered across product pages, browser features, and a long tail of wrapper sites that look identical. This guide is the cleanest map we could draw, organised so a casual user gets value in the first three minutes and a developer or privacy-conscious reader can keep going deeper. Whatever the reason, the answer in 2026 is mostly: **yes, you can do this**, but the options are scattered across product pages, browser features, and a long tail of wrapper sites that look identical. This guide is the cleanest map we could draw, organised so a casual user gets value in the first three minutes and a developer or privacy-conscious reader can keep going deeper.
We're going to cite primary sources for everything that touches privacy or product behavior, so you can verify (and so the article ages well as the products change). The two cluster posts that go deeper into [Claude specifically](/blog/use-claude-without-login-2026) and into [a tested comparison of 12 free AI chats](/blog/tested-no-login-ai-chats-2026) will sit alongside this one when they're ready. We're going to cite primary sources for everything that touches privacy or product behavior, so you can verify (and so the article ages well as the products change).
## Where to use each major AI model without an account ## Where to use each major AI model without an account
@ -70,9 +70,9 @@ Anthropic does require an account on `claude.ai` itself, but the picture is much
**Path 3: Brave Leo (no account, browser-side).** Install the [Brave browser](https://brave.com), open the sidebar, click the Leo icon, pick Claude Haiku from the model dropdown. No signup. Brave doesn't collect identifiers tied to you ([per their docs](https://brave.com/leo/)). The trade-off is that you have to use Brave as your browser, and you're limited to Haiku on the free tier (Sonnet and Opus require Brave Leo Premium at $14.99/month). **Path 3: Brave Leo (no account, browser-side).** Install the [Brave browser](https://brave.com), open the sidebar, click the Leo icon, pick Claude Haiku from the model dropdown. No signup. Brave doesn't collect identifiers tied to you ([per their docs](https://brave.com/leo/)). The trade-off is that you have to use Brave as your browser, and you're limited to Haiku on the free tier (Sonnet and Opus require Brave Leo Premium at $14.99/month).
**Path 4: Multi-model aggregator pages.** These wrap the Anthropic API and serve Claude responses without an account on Anthropic. The pick we'd recommend (with the obvious disclosure that we made it) is **[SurfSense /free](/free)**: it lists Claude alongside ChatGPT, Gemini, DeepSeek, Mistral, and Llama in one chat UI, the source code is open on [GitHub](https://github.com/MODSetter/SurfSense) so the privacy and quota behavior is verifiable, and the 500K free token quota is shared across any model you pick (so you can spend the budget on Claude if that's what you came for). The closed-source alternatives ([HIX.AI](https://hix.ai/claude), [EaseMate](https://www.easemate.ai/ai-chat/ask-claude), [Eye2.ai](https://eye2.ai), [NoteGPT](https://notegpt.io/ai-models/claude-sonnet-4-5)) work too, but quality and message limits vary widely; we [tested 12 of these](/blog/tested-no-login-ai-chats-2026) in a separate post. **Path 4: Multi-model aggregator pages.** These wrap the Anthropic API and serve Claude responses without an account on Anthropic. The pick we'd recommend (with the obvious disclosure that we made it) is **[SurfSense /free](/free)**: it lists Claude alongside ChatGPT, Gemini, DeepSeek, Mistral, and Llama in one chat UI, the source code is open on [GitHub](https://github.com/MODSetter/SurfSense) so the privacy and quota behavior is verifiable, and the 500K free token quota is shared across any model you pick (so you can spend the budget on Claude if that's what you came for). The closed-source alternatives ([HIX.AI](https://hix.ai/claude), [EaseMate](https://www.easemate.ai/ai-chat/ask-claude), [Eye2.ai](https://eye2.ai), [NoteGPT](https://notegpt.io/ai-models/claude-sonnet-4-5)) work too, but quality and message limits vary widely.
For the developer-specific paths (Claude Code with Bedrock or Vertex AI authentication, the Claude for Open Source program, the `/passes` Guest Pass system), see our [Claude-specific deep dive](/blog/use-claude-without-login-2026). For the developer-specific paths (Claude Code with Bedrock or Vertex AI authentication, the Claude for Open Source program), see Anthropic's [Claude Code authentication docs](https://code.claude.com/docs/en/authentication).
### Gemini, the awkward one ### Gemini, the awkward one
@ -242,7 +242,7 @@ Yes. Open `chatgpt.com` in a private tab and you'll get guest mode automatically
### Can I use Claude without an account? ### Can I use Claude without an account?
Not on `claude.ai` itself, which still requires signup. The closest no-account paths are [Duck.ai](https://duck.ai) (Claude Haiku 4.5, free, anonymised), [Brave Leo](https://brave.com/leo/) (Claude Haiku in the Brave browser sidebar), and aggregator pages like our open-source [SurfSense /free](/free), which lists Claude among the models you can pick with no Anthropic account and a 500K free token budget shared across the whole page. For more, see our [Claude-specific guide](/blog/use-claude-without-login-2026). Not on `claude.ai` itself, which still requires signup. The closest no-account paths are [Duck.ai](https://duck.ai) (Claude Haiku 4.5, free, anonymised), [Brave Leo](https://brave.com/leo/) (Claude Haiku in the Brave browser sidebar), and aggregator pages like our open-source [SurfSense /free](/free), which lists Claude among the models you can pick with no Anthropic account and a 500K free token budget shared across the whole page.
### Can I use Gemini without a Google account? ### Can I use Gemini without a Google account?

View file

@ -53,7 +53,7 @@ const THEMES = [
]; ];
const LEARN_MORE_LINKS = [ const LEARN_MORE_LINKS = [
{ key: "documentation" as const, href: "https://surfsense.com/docs" }, { key: "documentation" as const, href: "https://www.surfsense.com/docs" },
{ key: "github" as const, href: "https://github.com/MODSetter/SurfSense" }, { key: "github" as const, href: "https://github.com/MODSetter/SurfSense" },
]; ];

View file

@ -15,7 +15,7 @@ interface BreadcrumbNavProps {
export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) { export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
const jsonLdItems = items.map((item) => ({ const jsonLdItems = items.map((item) => ({
name: item.name, name: item.name,
url: `https://surfsense.com${item.href}`, url: `https://www.surfsense.com${item.href}`,
})); }));
return ( return (

View file

@ -16,8 +16,8 @@ export function OrganizationJsonLd() {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Organization", "@type": "Organization",
name: "SurfSense", name: "SurfSense",
url: "https://surfsense.com", url: "https://www.surfsense.com",
logo: "https://surfsense.com/logo.png", logo: "https://www.surfsense.com/logo.png",
description: description:
"Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.", "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"], sameAs: ["https://github.com/MODSetter/SurfSense", "https://discord.gg/Cg2M4GUJ"],
@ -38,14 +38,14 @@ export function WebSiteJsonLd() {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebSite", "@type": "WebSite",
name: "SurfSense", name: "SurfSense",
url: "https://surfsense.com", url: "https://www.surfsense.com",
description: description:
"Open source NotebookLM alternative for teams with no data limits. Free ChatGPT, Claude AI, and any AI model.", "Open source NotebookLM alternative for teams with no data limits. Free ChatGPT, Claude AI, and any AI model.",
potentialAction: { potentialAction: {
"@type": "SearchAction", "@type": "SearchAction",
target: { target: {
"@type": "EntryPoint", "@type": "EntryPoint",
urlTemplate: "https://surfsense.com/docs?search={search_term_string}", urlTemplate: "https://www.surfsense.com/docs?search={search_term_string}",
}, },
"query-input": "required name=search_term_string", "query-input": "required name=search_term_string",
}, },
@ -71,7 +71,7 @@ export function SoftwareApplicationJsonLd() {
}, },
description: 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.", "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", url: "https://www.surfsense.com",
downloadUrl: "https://github.com/MODSetter/SurfSense/releases", downloadUrl: "https://github.com/MODSetter/SurfSense/releases",
featureList: [ featureList: [
"Free access to ChatGPT, Claude AI, and any AI model", "Free access to ChatGPT, Claude AI, and any AI model",
@ -95,6 +95,7 @@ export function ArticleJsonLd({
description, description,
url, url,
datePublished, datePublished,
dateModified,
author, author,
image, image,
}: { }: {
@ -102,6 +103,7 @@ export function ArticleJsonLd({
description: string; description: string;
url: string; url: string;
datePublished: string; datePublished: string;
dateModified?: string;
author: string; author: string;
image?: string; image?: string;
}) { }) {
@ -114,6 +116,7 @@ export function ArticleJsonLd({
description, description,
url, url,
datePublished, datePublished,
...(dateModified ? { dateModified } : {}),
author: { author: {
"@type": "Organization", "@type": "Organization",
name: author, name: author,
@ -123,10 +126,10 @@ export function ArticleJsonLd({
name: "SurfSense", name: "SurfSense",
logo: { logo: {
"@type": "ImageObject", "@type": "ImageObject",
url: "https://surfsense.com/logo.png", url: "https://www.surfsense.com/logo.png",
}, },
}, },
image: image || "https://surfsense.com/og-image.png", image: image || "https://www.surfsense.com/og-image.png",
mainEntityOfPage: { mainEntityOfPage: {
"@type": "WebPage", "@type": "WebPage",
"@id": url, "@id": url,

View file

@ -0,0 +1,54 @@
import { promises as fs } from "node:fs";
import path from "node:path";
export interface FaqEntry {
question: string;
answer: string;
}
/**
* Extracts FAQ items from a blog post MDX file by parsing the `## FAQ` section.
*
* The FAQ section is bounded by `## FAQ` and the next H2 heading. Each H3 inside
* is treated as a question, with the body until the next H3 (or the end of the
* FAQ section) as its answer. Common Markdown decorations (links, bold, inline
* code) are stripped so the JSON-LD output contains plain text suitable for
* Google's FAQ rich-result eligibility checks.
*
* Returns an empty array when the post has no FAQ section, or when the file
* cannot be read (e.g. for posts that do not yet exist on disk).
*/
export async function extractFaqFromBlogPost(slug: string): Promise<FaqEntry[]> {
try {
const filepath = path.join(process.cwd(), "blog", "content", `${slug}.mdx`);
const content = await fs.readFile(filepath, "utf-8");
return extractFaqFromContent(content);
} catch {
return [];
}
}
export function extractFaqFromContent(content: string): FaqEntry[] {
const faqHeading = content.match(/^##\s+FAQ\s*$/m);
if (!faqHeading || faqHeading.index === undefined) return [];
const afterFaq = content.slice(faqHeading.index + faqHeading[0].length);
const nextH2 = afterFaq.match(/^##\s+/m);
const faqBody = nextH2 ? afterFaq.slice(0, nextH2.index) : afterFaq;
const blocks = faqBody.split(/^###\s+/m).slice(1);
return blocks
.map((block) => {
const newlineIdx = block.indexOf("\n");
const question = (newlineIdx === -1 ? block : block.slice(0, newlineIdx)).trim();
const answer = (newlineIdx === -1 ? "" : block.slice(newlineIdx + 1))
.trim()
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/`([^`]+)`/g, "$1")
.replace(/\s+/g, " ");
return { question, answer };
})
.filter((item) => item.question.length > 0 && item.answer.length > 0);
}