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
|
|
@ -124,7 +124,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
handleStartEdit,
|
||||
handleSaveConnector,
|
||||
handleDisconnectConnector,
|
||||
handleDisconnectFromList,
|
||||
handleBackFromEdit,
|
||||
handleBackFromConnect,
|
||||
handleBackFromYouTube,
|
||||
|
|
@ -233,9 +232,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
indexingConnectorIds={indexingConnectorIds}
|
||||
onBack={handleBackFromMCPList}
|
||||
onManage={handleStartEdit}
|
||||
onDisconnect={(connector) =>
|
||||
handleDisconnectFromList(connector, () => refreshConnectors())
|
||||
}
|
||||
onAddAccount={handleAddNewMCPFromList}
|
||||
addButtonText="Add New MCP Server"
|
||||
/>
|
||||
|
|
@ -247,9 +243,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
indexingConnectorIds={indexingConnectorIds}
|
||||
onBack={handleBackFromAccountsList}
|
||||
onManage={handleStartEdit}
|
||||
onDisconnect={(connector) =>
|
||||
handleDisconnectFromList(connector, () => refreshConnectors())
|
||||
}
|
||||
onAddAccount={() => {
|
||||
// Check both OAUTH_CONNECTORS and COMPOSIO_CONNECTORS
|
||||
const oauthConnector =
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { CheckCircle2 } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import type { ConnectorConfigProps } from "../index";
|
||||
import { MCPTrustedTools } from "./mcp-trusted-tools";
|
||||
|
||||
export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
|
||||
const serviceName = connector.config?.mcp_service as string | undefined;
|
||||
|
|
@ -11,7 +12,7 @@ export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
|
|||
: "this service";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border border-border bg-emerald-500/5 p-4 flex items-start gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10 shrink-0 mt-0.5">
|
||||
<CheckCircle2 className="size-4 text-emerald-500" />
|
||||
|
|
@ -23,6 +24,8 @@ export const MCPServiceConfig: FC<ConnectorConfigProps> = ({ connector }) => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{connector.id > 0 && <MCPTrustedTools connector={connector} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
"use client";
|
||||
|
||||
import { ShieldCheck, Trash2 } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
|
||||
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
|
||||
|
||||
interface MCPTrustedToolsProps {
|
||||
connector: SearchSourceConnector;
|
||||
}
|
||||
|
||||
/** Audit + revoke surface for tools promoted via in-chat "Always Allow". */
|
||||
export const MCPTrustedTools: FC<MCPTrustedToolsProps> = ({ connector }) => {
|
||||
const trustedTools = readTrustedTools(connector.config);
|
||||
const [pending, setPending] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleRevoke = async (toolName: string) => {
|
||||
setPending((prev) => new Set(prev).add(toolName));
|
||||
try {
|
||||
await connectorsApiService.untrustMCPTool(connector.id, toolName);
|
||||
toast.success(`Removed ${toolName} from trusted tools`);
|
||||
} catch {
|
||||
toast.error(`Failed to remove ${toolName} from trusted tools`);
|
||||
} finally {
|
||||
setPending((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(toolName);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="font-medium text-sm sm:text-base flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
Trusted Tools
|
||||
</h3>
|
||||
|
||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-4">
|
||||
<p className="text-[10px] sm:text-xs text-muted-foreground">
|
||||
Tools listed here skip the approval prompt during chat. Trust is granted by clicking
|
||||
"Always Allow" on an approval card; revoke it here to require approval again.
|
||||
</p>
|
||||
|
||||
{trustedTools.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground/70 italic">
|
||||
No trusted tools yet for this connector.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{trustedTools.map((toolName) => {
|
||||
const isPending = pending.has(toolName);
|
||||
return (
|
||||
<li
|
||||
key={toolName}
|
||||
className="flex items-center justify-between gap-3 rounded-lg px-3 py-2 hover:bg-muted/40 transition-colors"
|
||||
>
|
||||
<span className="text-xs font-mono break-all">{toolName}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-muted-foreground hover:text-destructive shrink-0"
|
||||
onClick={() => handleRevoke(toolName)}
|
||||
disabled={isPending}
|
||||
aria-label={`Revoke trust for ${toolName}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<span className="ml-1 hidden sm:inline">Revoke</span>
|
||||
</Button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function readTrustedTools(config: Record<string, unknown> | undefined | null): string[] {
|
||||
const raw = config?.trusted_tools;
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.filter((item): item is string => typeof item === "string");
|
||||
}
|
||||
|
|
@ -1288,25 +1288,6 @@ export const useConnectorDialog = () => {
|
|||
[editingConnector, searchSpaceId, deleteConnector, cameFromMCPList, setIsOpen]
|
||||
);
|
||||
|
||||
const handleDisconnectFromList = useCallback(
|
||||
async (connector: SearchSourceConnector, refreshConnectors: () => void) => {
|
||||
if (!searchSpaceId) return;
|
||||
try {
|
||||
await deleteConnector({ id: connector.id });
|
||||
trackConnectorDeleted(Number(searchSpaceId), connector.connector_type, connector.id);
|
||||
toast.success(`${connector.name} disconnected successfully`);
|
||||
refreshConnectors();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error disconnecting connector:", error);
|
||||
toast.error("Failed to disconnect connector");
|
||||
}
|
||||
},
|
||||
[searchSpaceId, deleteConnector]
|
||||
);
|
||||
|
||||
// Handle quick index (index with selected date range, or backend defaults if none selected)
|
||||
const handleQuickIndexConnector = useCallback(
|
||||
async (
|
||||
|
|
@ -1480,7 +1461,6 @@ export const useConnectorDialog = () => {
|
|||
handleStartEdit,
|
||||
handleSaveConnector,
|
||||
handleDisconnectConnector,
|
||||
handleDisconnectFromList,
|
||||
handleBackFromEdit,
|
||||
handleBackFromConnect,
|
||||
handleBackFromYouTube,
|
||||
|
|
|
|||
|
|
@ -277,14 +277,14 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
|
|||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-8 text-[11px] px-3 font-medium shrink-0"
|
||||
onClick={() => setConfirmDisconnectId(connector.id)}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-8 text-[11px] px-3 font-medium shrink-0"
|
||||
onClick={() => setConfirmDisconnectId(connector.id)}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -47,10 +47,7 @@ export interface InlineMentionEditorRef {
|
|||
setText: (text: string) => void;
|
||||
getText: () => string;
|
||||
getMentionedDocuments: () => MentionedDocument[];
|
||||
insertMentionChip: (
|
||||
mention: MentionChipInput,
|
||||
options?: { removeTriggerText?: boolean }
|
||||
) => void;
|
||||
insertMentionChip: (mention: MentionChipInput, options?: { removeTriggerText?: boolean }) => void;
|
||||
/**
|
||||
* @deprecated Use ``insertMentionChip``. Kept for one transition
|
||||
* cycle so we don't break ad-hoc callers; prefer the new name.
|
||||
|
|
@ -364,8 +361,7 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
|
|||
const selection = editor.selection;
|
||||
const kind: MentionKind = mention.kind ?? "doc";
|
||||
const document_type =
|
||||
mention.document_type ??
|
||||
(kind === "folder" ? FOLDER_MENTION_DOCUMENT_TYPE : undefined);
|
||||
mention.document_type ?? (kind === "folder" ? FOLDER_MENTION_DOCUMENT_TYPE : undefined);
|
||||
const mentionNode: MentionElementNode = {
|
||||
type: MENTION_TYPE,
|
||||
id: mention.id,
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ import {
|
|||
} from "@/components/ui/table";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser";
|
||||
import { getVirtualPathDisplay } from "@/lib/chat/virtual-path-display";
|
||||
import { type CitationUrlMap, preprocessCitationMarkdown } from "@/lib/citations/citation-parser";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function MarkdownCodeBlockSkeleton() {
|
||||
|
|
@ -222,11 +222,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
|
|||
: undefined;
|
||||
|
||||
const { displayName, isFolder } = getVirtualPathDisplay(path);
|
||||
const icon = isFolder ? (
|
||||
<FolderIcon className="size-3.5" />
|
||||
) : (
|
||||
<FileIcon className="size-3.5" />
|
||||
);
|
||||
const icon = isFolder ? <FolderIcon className="size-3.5" /> : <FileIcon className="size-3.5" />;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
|
|
|
|||
|
|
@ -111,11 +111,7 @@ const UserTextPart: FC = () => {
|
|||
icon={icon}
|
||||
label={segment.doc.title}
|
||||
tooltip={isFolder ? `Folder: ${segment.doc.title}` : segment.doc.title}
|
||||
onClick={
|
||||
isFolder
|
||||
? undefined
|
||||
: () => handleOpenDoc(segment.doc.id, segment.doc.title)
|
||||
}
|
||||
onClick={isFolder ? undefined : () => handleOpenDoc(segment.doc.id, segment.doc.title)}
|
||||
className="mx-0.5"
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export interface PlateEditorProps {
|
|||
editorVariant?: "default" | "demo" | "fullWidth" | "none";
|
||||
/** Additional className for the container */
|
||||
className?: string;
|
||||
/** Save callback. When provided, ⌘+S / Ctrl+S shortcut is registered and save button appears. */
|
||||
/** Save callback. When provided, ⌘+Shift+S / Ctrl+Shift+S shortcut is registered (avoiding the browser's ⌘+S / Ctrl+S "Save Page As" conflict) and a save button appears in the toolbar. */
|
||||
onSave?: () => void;
|
||||
/** Whether there are unsaved changes */
|
||||
hasUnsavedChanges?: boolean;
|
||||
|
|
@ -170,16 +170,10 @@ export function PlateEditor({
|
|||
: markdown
|
||||
? (editor) => {
|
||||
if (!enableCitations) {
|
||||
return safeDeserializeMarkdown(
|
||||
editor,
|
||||
escapeMdxExpressions(markdown)
|
||||
) as Value;
|
||||
return safeDeserializeMarkdown(editor, escapeMdxExpressions(markdown)) as Value;
|
||||
}
|
||||
const { content: rewritten, urlMap } = preprocessCitationMarkdown(markdown);
|
||||
const value = safeDeserializeMarkdown(
|
||||
editor,
|
||||
escapeMdxExpressions(rewritten)
|
||||
);
|
||||
const value = safeDeserializeMarkdown(editor, escapeMdxExpressions(rewritten));
|
||||
return injectCitationNodes(value, urlMap) as Value;
|
||||
}
|
||||
: undefined,
|
||||
|
|
@ -203,10 +197,7 @@ export function PlateEditor({
|
|||
let newValue: Descendant[];
|
||||
if (enableCitations) {
|
||||
const { content: rewritten, urlMap } = preprocessCitationMarkdown(markdown);
|
||||
const deserialized = safeDeserializeMarkdown(
|
||||
editor,
|
||||
escapeMdxExpressions(rewritten)
|
||||
);
|
||||
const deserialized = safeDeserializeMarkdown(editor, escapeMdxExpressions(rewritten));
|
||||
newValue = injectCitationNodes(deserialized, urlMap);
|
||||
} else {
|
||||
newValue = safeDeserializeMarkdown(editor, escapeMdxExpressions(markdown));
|
||||
|
|
|
|||
|
|
@ -49,10 +49,7 @@ export function safeDeserializeMarkdown(
|
|||
return api.deserialize(markdown, { remarkPlugins: STRICT_PLUGINS }) as Descendant[];
|
||||
} catch (mdxError) {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.warn(
|
||||
"[plate-editor] MDX parse failed, retrying without remark-mdx:",
|
||||
mdxError
|
||||
);
|
||||
console.warn("[plate-editor] MDX parse failed, retrying without remark-mdx:", mdxError);
|
||||
}
|
||||
try {
|
||||
return api.deserialize(markdown, { remarkPlugins: LENIENT_PLUGINS }) as Descendant[];
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const Navbar = ({ scrolledBgClassName }: NavbarProps = {}) => {
|
|||
const navItems = [
|
||||
{ name: "Free\u00A0AI", link: "/free" },
|
||||
{ name: "Pricing", link: "/pricing" },
|
||||
// { name: "Blog", link: "/blog" },
|
||||
{ name: "Blog", link: "/blog" },
|
||||
{ name: "Changelog", link: "/changelog" },
|
||||
{ name: "Docs", link: "/docs" },
|
||||
{ name: "Contact\u00A0Us", link: "/contact" },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
|
|
@ -49,6 +48,7 @@ import {
|
|||
searchThreads,
|
||||
updateThread,
|
||||
} from "@/lib/chat/thread-persistence";
|
||||
import { formatThreadTimestamp } from "@/lib/format-date";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
||||
|
|
@ -393,8 +393,7 @@ export function AllPrivateChatsSidebarContent({
|
|||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start">
|
||||
<p>
|
||||
{t("updated") || "Updated"}:{" "}
|
||||
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
||||
{t("updated") || "Updated"}: {formatThreadTimestamp(thread.updatedAt)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import { useSetAtom } from "jotai";
|
||||
import {
|
||||
ArchiveIcon,
|
||||
|
|
@ -49,6 +48,7 @@ import {
|
|||
searchThreads,
|
||||
updateThread,
|
||||
} from "@/lib/chat/thread-persistence";
|
||||
import { formatThreadTimestamp } from "@/lib/format-date";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
||||
|
||||
|
|
@ -392,8 +392,7 @@ export function AllSharedChatsSidebarContent({
|
|||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="start">
|
||||
<p>
|
||||
{t("updated") || "Updated"}:{" "}
|
||||
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
|
||||
{t("updated") || "Updated"}: {formatThreadTimestamp(thread.updatedAt)}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -24,10 +24,7 @@ import type React from "react";
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom";
|
||||
import {
|
||||
makeFolderMention,
|
||||
mentionedDocumentsAtom,
|
||||
} from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { makeFolderMention, mentionedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
|
||||
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
||||
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
|
||||
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const THEMES = [
|
|||
];
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -302,8 +302,7 @@ export const DocumentMentionPicker = forwardRef<
|
|||
// folder entries lift the existing kind-aware key so the same
|
||||
// matchers used by the chip atom apply unchanged.
|
||||
const selectedKeys = useMemo(
|
||||
() =>
|
||||
new Set(initialSelectedDocuments.map((d) => getMentionDocKey(d))),
|
||||
() => new Set(initialSelectedDocuments.map((d) => getMentionDocKey(d))),
|
||||
[initialSelectedDocuments]
|
||||
);
|
||||
|
||||
|
|
@ -586,9 +585,7 @@ export const DocumentMentionPicker = forwardRef<
|
|||
{(surfsenseDocsList.length > 0 || userDocsList.length > 0) && (
|
||||
<div className="mx-2 my-4 border-t border-popover-border" />
|
||||
)}
|
||||
<div className="px-3 py-2 text-xs font-bold text-muted-foreground/55">
|
||||
Folders
|
||||
</div>
|
||||
<div className="px-3 py-2 text-xs font-bold text-muted-foreground/55">Folders</div>
|
||||
{folderMentions.map((folder) => {
|
||||
const folderKey = getMentionDocKey(folder);
|
||||
const isAlreadySelected = selectedKeys.has(folderKey);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ interface BreadcrumbNavProps {
|
|||
export function BreadcrumbNav({ items, className }: BreadcrumbNavProps) {
|
||||
const jsonLdItems = items.map((item) => ({
|
||||
name: item.name,
|
||||
url: `https://surfsense.com${item.href}`,
|
||||
url: `https://www.surfsense.com${item.href}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ export function OrganizationJsonLd() {
|
|||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: "SurfSense",
|
||||
url: "https://surfsense.com",
|
||||
logo: "https://surfsense.com/logo.png",
|
||||
url: "https://www.surfsense.com",
|
||||
logo: "https://www.surfsense.com/logo.png",
|
||||
description:
|
||||
"Open source NotebookLM alternative for teams with no data limits. Use ChatGPT, Claude AI, and any AI model for free.",
|
||||
sameAs: ["https://github.com/MODSetter/SurfSense", "https://discord.gg/Cg2M4GUJ"],
|
||||
|
|
@ -38,14 +38,14 @@ export function WebSiteJsonLd() {
|
|||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: "SurfSense",
|
||||
url: "https://surfsense.com",
|
||||
url: "https://www.surfsense.com",
|
||||
description:
|
||||
"Open source NotebookLM alternative for teams with no data limits. Free ChatGPT, Claude AI, and any AI model.",
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: {
|
||||
"@type": "EntryPoint",
|
||||
urlTemplate: "https://surfsense.com/docs?search={search_term_string}",
|
||||
urlTemplate: "https://www.surfsense.com/docs?search={search_term_string}",
|
||||
},
|
||||
"query-input": "required name=search_term_string",
|
||||
},
|
||||
|
|
@ -71,7 +71,7 @@ export function SoftwareApplicationJsonLd() {
|
|||
},
|
||||
description:
|
||||
"Open source NotebookLM alternative with free access to ChatGPT, Claude AI, and any model. Connect Slack, Google Drive, Notion, Confluence, GitHub, and dozens more data sources.",
|
||||
url: "https://surfsense.com",
|
||||
url: "https://www.surfsense.com",
|
||||
downloadUrl: "https://github.com/MODSetter/SurfSense/releases",
|
||||
featureList: [
|
||||
"Free access to ChatGPT, Claude AI, and any AI model",
|
||||
|
|
@ -95,6 +95,7 @@ export function ArticleJsonLd({
|
|||
description,
|
||||
url,
|
||||
datePublished,
|
||||
dateModified,
|
||||
author,
|
||||
image,
|
||||
}: {
|
||||
|
|
@ -102,6 +103,7 @@ export function ArticleJsonLd({
|
|||
description: string;
|
||||
url: string;
|
||||
datePublished: string;
|
||||
dateModified?: string;
|
||||
author: string;
|
||||
image?: string;
|
||||
}) {
|
||||
|
|
@ -114,6 +116,7 @@ export function ArticleJsonLd({
|
|||
description,
|
||||
url,
|
||||
datePublished,
|
||||
...(dateModified ? { dateModified } : {}),
|
||||
author: {
|
||||
"@type": "Organization",
|
||||
name: author,
|
||||
|
|
@ -123,10 +126,10 @@ export function ArticleJsonLd({
|
|||
name: "SurfSense",
|
||||
logo: {
|
||||
"@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: {
|
||||
"@type": "WebPage",
|
||||
"@id": url,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue