diff --git a/src/components/docs-page-actions.tsx b/src/components/docs-page-actions.tsx deleted file mode 100644 index 09345dc..0000000 --- a/src/components/docs-page-actions.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import { Button, buttonVariants } from '@/components/ui/button'; -import { useCopyButton } from '@/hooks/use-copy-button'; -import { cn } from '@/lib/utils'; -import { ViewOptionsPopover as FumaViewOptionsPopover } from 'fumadocs-ui/layouts/docs/page'; -import { Check, Copy } from 'lucide-react'; -import { useState, type ComponentProps } from 'react'; -import { useTranslations } from 'fumadocs-ui/contexts/i18n'; - -const markdownCache = new Map>(); - -export const docsActionButtonClass = cn( - buttonVariants({ variant: 'default', size: 'sm' }), - 'gap-2 [&_svg]:size-3.5 [&_svg]:text-muted-foreground data-[state=open]:bg-foreground/15', -); - -export function MarkdownCopyButton({ - markdownUrl, - className, - children, - ...props -}: ComponentProps<'button'> & { markdownUrl: string }) { - const t = useTranslations(); - const [isLoading, setLoading] = useState(false); - const [checked, onClick] = useCopyButton(async () => { - const cached = markdownCache.get(markdownUrl); - if (cached) return navigator.clipboard.writeText(await cached); - - setLoading(true); - try { - const promise = fetch(markdownUrl).then((res) => res.text()); - markdownCache.set(markdownUrl, promise); - await navigator.clipboard.write([ - new ClipboardItem({ 'text/plain': promise }), - ]); - } finally { - setLoading(false); - } - }); - - return ( - - ); -} - -export function ViewOptionsPopover( - props: ComponentProps, -) { - return ( - - ); -} diff --git a/src/components/landing-page.tsx b/src/components/landing-page.tsx index 0cc2a15..1ef2b05 100644 --- a/src/components/landing-page.tsx +++ b/src/components/landing-page.tsx @@ -1,8 +1,11 @@ "use client"; import { LandingShardCanvas } from "@/components/landing-shard-canvas"; +import { MarkdownActions } from "@/components/markdown-actions"; import { SiteNav } from "@/components/site-nav"; import { Button } from "@/components/ui/button"; +import { posthogClient } from "@/lib/posthog-client"; +import { siteUrl } from "@/lib/shared"; import { pageHeadingClassName } from "@/lib/typography"; import { cn } from "@/lib/utils"; import { GithubIcon } from "@/components/github-icon"; @@ -129,12 +132,19 @@ export default function Home() { href="https://github.com/multigres/multigres" target="_blank" rel="noreferrer" + onClick={() => posthogClient.capture('multigres_github_clicked', { source: 'hero' })} > GitHub + + diff --git a/src/components/markdown-actions.tsx b/src/components/markdown-actions.tsx new file mode 100644 index 0000000..4bea18b --- /dev/null +++ b/src/components/markdown-actions.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { Button, buttonVariants } from '@/components/ui/button'; +import { useCopyButton } from '@/hooks/use-copy-button'; +import { posthogClient } from '@/lib/posthog-client'; +import { cn } from '@/lib/utils'; +import { Check, Copy } from 'lucide-react'; +import { useState, type ComponentProps, type SVGProps } from 'react'; +import { useTranslations } from 'fumadocs-ui/contexts/i18n'; + +const markdownCache = new Map>(); + +const actionButtonClass = cn( + buttonVariants({ variant: 'default', size: 'sm' }), + 'gap-2 [&_svg]:size-3.5 [&_svg]:text-muted-foreground', +); + +const askLinkClass = cn( + buttonVariants({ variant: 'secondary', size: 'sm' }), + 'gap-2 [&_svg]:size-3.5', +); + +export function MarkdownCopyButton({ + markdownUrl, + className, + children, + ...props +}: ComponentProps<'button'> & { markdownUrl: string }) { + const t = useTranslations(); + const [isLoading, setLoading] = useState(false); + const [checked, onClick] = useCopyButton(async () => { + const cached = markdownCache.get(markdownUrl); + if (cached) { + await navigator.clipboard.writeText(await cached); + posthogClient.capture('copy_as_markdown_clicked'); + return; + } + + setLoading(true); + try { + const promise = fetch(markdownUrl).then((res) => res.text()); + markdownCache.set(markdownUrl, promise); + await navigator.clipboard.write([ + new ClipboardItem({ 'text/plain': promise }), + ]); + posthogClient.capture('copy_as_markdown_clicked'); + } finally { + setLoading(false); + } + }); + + return ( + + ); +} + +function OpenAiIcon(props: SVGProps) { + return ( + + + + ); +} + +function AnthropicIcon(props: SVGProps) { + return ( + + + + ); +} + +export function MarkdownActions({ + markdownUrl, + pageUrl, + className, +}: { + markdownUrl: string; + pageUrl: string; + className?: string; +}) { + const prompt = `Read ${pageUrl}, I want to ask questions about it.`; + return ( + + ); +} diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx index c6c6004..395f852 100644 --- a/src/components/site-footer.tsx +++ b/src/components/site-footer.tsx @@ -1,5 +1,6 @@ import { Link } from '@tanstack/react-router'; import { GithubIcon } from '@/components/github-icon'; +import { posthogClient } from '@/lib/posthog-client'; import { appName, blogRoute, docsRoute, gitConfig } from '@/lib/shared'; import { cn } from '@/lib/utils'; @@ -39,6 +40,11 @@ export function SiteFooter({ className }: SiteFooterProps) { target="_blank" rel="noreferrer" className="inline-flex items-center gap-1.5 hover:text-foreground" + onClick={ + item.label === 'GitHub' + ? () => posthogClient.capture('multigres_github_clicked', { source: 'footer' }) + : undefined + } > {item.label === 'GitHub' ? : null} {item.label} diff --git a/src/components/site-nav.tsx b/src/components/site-nav.tsx index db54e20..6984ed3 100644 --- a/src/components/site-nav.tsx +++ b/src/components/site-nav.tsx @@ -2,6 +2,7 @@ import { GithubIcon } from '@/components/github-icon'; import { MultigresLogo } from '@/components/multigres-logo'; +import { posthogClient } from '@/lib/posthog-client'; import { cn } from '@/lib/utils'; import { appName } from '@/lib/shared'; import { siteNavLinks } from '@/lib/site-nav'; @@ -56,6 +57,11 @@ export function SiteNav({ trailing, className }: SiteNavProps) { target="_blank" rel="noreferrer" className="inline-flex items-center gap-1.5 text-sm font-semibold text-foreground" + onClick={ + item.label === 'GitHub' + ? () => posthogClient.capture('multigres_github_clicked', { source: 'nav' }) + : undefined + } > {item.label === 'GitHub' ? ( diff --git a/src/routes/blog/$slug.tsx b/src/routes/blog/$slug.tsx index ff11034..1f5f798 100644 --- a/src/routes/blog/$slug.tsx +++ b/src/routes/blog/$slug.tsx @@ -7,7 +7,9 @@ import { BlogLayout } from '@/components/blog-layout'; import { BlogShardPlaceholder } from '@/components/blog-shard-placeholder'; import { useMDXComponents } from '@/components/mdx'; import { parseAuthorKeys, resolveAuthors } from '@/lib/authors'; +import { MarkdownActions } from '@/components/markdown-actions'; import { blogSource } from '@/lib/blog-source.server'; +import { siteUrl } from '@/lib/shared'; import { docPageHeadingClassName } from '@/lib/typography'; import { useFumadocsLoader } from 'fumadocs-core/source/client'; import { Suspense } from 'react'; @@ -60,7 +62,10 @@ export const Route = createFileRoute('/blog/$slug')({ { name: 'twitter:description', content: description }, { name: 'twitter:image', content: image }, ], - links: [{ rel: 'canonical', href: canonicalUrl }], + links: [ + { rel: 'canonical', href: canonicalUrl }, + { rel: 'alternate', type: 'text/markdown', href: `${canonicalUrl}.md` }, + ], scripts: [ { type: 'application/ld+json', @@ -91,11 +96,16 @@ const serverLoader = createServerFn({ method: 'GET' }) image: page.data.image, datePublished: page.data.date ? page.data.date.toISOString() : undefined, authorName: firstAuthor?.name, + markdownUrl: `${page.url}.md`, + pageUrl: page.url, }; }); const clientLoader = browserCollections.blog.createClientLoader({ - component({ default: MDX, frontmatter }) { + component( + { default: MDX, frontmatter }, + { markdownUrl, pageUrl }: { markdownUrl: string; pageUrl: string }, + ) { const postAuthors = resolveAuthors( parseAuthorKeys(frontmatter.authors ?? frontmatter.author), ); @@ -127,6 +137,11 @@ const clientLoader = browserCollections.blog.createClientLoader({ series={frontmatter.series} seriesPart={frontmatter.seriesPart} /> +
@@ -137,11 +152,13 @@ const clientLoader = browserCollections.blog.createClientLoader({ }); function BlogPostPage() { - const { path } = useFumadocsLoader(Route.useLoaderData()); + const { path, markdownUrl, pageUrl } = useFumadocsLoader( + Route.useLoaderData(), + ); return ( - {clientLoader.useContent(path)} + {clientLoader.useContent(path, { markdownUrl, pageUrl })} ); } diff --git a/src/routes/docs/$.tsx b/src/routes/docs/$.tsx index 981746f..7d63fd1 100644 --- a/src/routes/docs/$.tsx +++ b/src/routes/docs/$.tsx @@ -9,14 +9,11 @@ import { DocsPage, DocsTitle, } from 'fumadocs-ui/layouts/docs/page'; -import { - MarkdownCopyButton, - ViewOptionsPopover, -} from '@/components/docs-page-actions'; +import { MarkdownActions } from '@/components/markdown-actions'; import { SiteDocsContainer } from '@/components/site-docs-container'; import { baseOptions } from '@/lib/layout.shared'; import { docPageHeadingClassName } from '@/lib/typography'; -import { gitConfig } from '@/lib/shared'; +import { siteUrl } from '@/lib/shared'; import { useFumadocsLoader } from 'fumadocs-core/source/client'; import { Suspense } from 'react'; import { useMDXComponents } from '@/components/mdx'; @@ -48,7 +45,18 @@ export const Route = createFileRoute('/docs/$')({ { name: 'twitter:description', content: description }, { name: 'twitter:image', content: '/img/og-image.png' }, ], - links: [{ rel: 'canonical', href: canonicalUrl }], + links: [ + { rel: 'canonical', href: canonicalUrl }, + ...(loaderData?.markdownUrl + ? [ + { + rel: 'alternate', + type: 'text/markdown', + href: `${siteUrl}${loaderData.markdownUrl}`, + }, + ] + : []), + ], scripts: [ { type: 'application/ld+json', @@ -80,6 +88,7 @@ const serverLoader = createServerFn({ title: page.data.title, description: page.data.description, markdownUrl: slugsToMarkdownPath(page.slugs).url, + pageUrl: page.url, pageTree: await source.serializePageTree(source.getPageTree()), }; }); @@ -87,7 +96,7 @@ const serverLoader = createServerFn({ const clientLoader = browserCollections.docs.createClientLoader({ component( { toc, frontmatter, default: MDX }, - { markdownUrl, path }: { markdownUrl: string; path: string }, + { markdownUrl, pageUrl }: { markdownUrl: string; pageUrl: string }, ) { return ( @@ -102,13 +111,11 @@ const clientLoader = browserCollections.docs.createClientLoader({ ) : null}
-
- - -
+ @@ -119,7 +126,9 @@ const clientLoader = browserCollections.docs.createClientLoader({ }); function Page() { - const { path, pageTree, markdownUrl } = useFumadocsLoader(Route.useLoaderData()); + const { path, pageTree, markdownUrl, pageUrl } = useFumadocsLoader( + Route.useLoaderData(), + ); return ( null, }} > - {clientLoader.useContent(path, { markdownUrl, path })} + {clientLoader.useContent(path, { markdownUrl, pageUrl })} ); } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 85a1468..fbb87ef 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,6 +1,7 @@ import { createFileRoute } from '@tanstack/react-router'; import LandingPage from '@/components/landing-page'; import { serializeJsonLd } from '@/lib/json-ld'; +import { siteUrl } from '@/lib/shared'; const description = 'A horizontally scalable Postgres architecture supporting multi-tenant, highly available, and globally distributed deployments.'; @@ -72,6 +73,11 @@ export const Route = createFileRoute('/')({ rel: 'canonical', href: 'https://multigres.com/', }, + { + rel: 'alternate', + type: 'text/markdown', + href: `${siteUrl}/index.md`, + }, ], scripts: [ { diff --git a/src/routes/llms-full[.]txt.ts b/src/routes/llms-full[.]txt.ts index afb79d3..10a0e82 100644 --- a/src/routes/llms-full[.]txt.ts +++ b/src/routes/llms-full[.]txt.ts @@ -7,7 +7,9 @@ export const Route = createFileRoute('/llms-full.txt')({ GET: async () => { const scan = source.getPages().map(getLLMText); const scanned = await Promise.all(scan); - return new Response(scanned.join('\n\n')); + return new Response(scanned.join('\n\n'), { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); }, }, }, diff --git a/src/routes/llms[.]txt.ts b/src/routes/llms[.]txt.ts index 59ae413..f2c5eb8 100644 --- a/src/routes/llms[.]txt.ts +++ b/src/routes/llms[.]txt.ts @@ -35,7 +35,9 @@ export const Route = createFileRoute('/llms.txt')({ const body = `# Multigres\n\n> ${TAGLINE}\n\n${docsIndex}\n\n${blogSection}\n`; - return new Response(body); + return new Response(body, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); }, }, }, diff --git a/src/start.ts b/src/start.ts index 6f0dfe0..ab6d7a3 100644 --- a/src/start.ts +++ b/src/start.ts @@ -34,7 +34,7 @@ const llmMiddleware = createMiddleware().server(async ({ next, request }) => { const path = explicitMarkdown ? pathname.slice(0, -MARKDOWN_EXT.length) : pathname; - if (path === '/' || path === '') { + if (path === '/' || path === '' || path === '/index') { return markdownResponse(homepageMarkdown); }