Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ Session.vim
/.yarn/cache
/.yarn/install-state.gz

# Nextra
.nextra
.next


# Pagefind
public/_pagefind
static/_pagefind

.cursor
807 changes: 0 additions & 807 deletions .yarn/releases/yarn-3.3.0.cjs

This file was deleted.

7 changes: 0 additions & 7 deletions .yarnrc.yml

This file was deleted.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ The docs are deployed to https://docs.svix.com
## Installation

```console
yarn install
npm install
```

## Local Development

```console
yarn start
npm run dev
```
128 changes: 128 additions & 0 deletions app/[[...mdxPath]]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Footer, Layout, Navbar } from 'nextra-theme-docs'
import { Head } from 'nextra/components'
import { getPageMap } from 'nextra/page-map'
import 'nextra-theme-docs/style.css'
import '../globals.css'
import Image from 'next/image'
import Script from 'next/script'
import { PostHogProvider } from '../components/providers/PostHogProvider'
import { ReactNode } from 'react'
import { PageMapItem } from 'nextra'

const siteData = {
name: 'Svix Docs',
tagline: 'Webhooks as a service',
description: 'Svix makes sending webhooks easy and reliable. Documentation for sending, receiving, and operating webhooks with Svix.',
productionUrl: 'https://docs.svix.com',
repo: 'https://github.com/svix/svix-webhooks',
docsSource: 'https://github.com/svix/svix-docs',
} as const

export const metadata = {
metadataBase: new URL(siteData.productionUrl),
description: siteData.description,
title: {
default: `${siteData.name} – ${siteData.tagline}`,
template: `%s | ${siteData.name}`,
},
openGraph: {
url: siteData.productionUrl,
siteName: siteData.name,
locale: 'en_US',
type: 'website',
images: [{ url: '/img/socialbanner.png', width: 1200, height: 630, alt: siteData.name }],
},
}

const navbar = (
<Navbar
align="left"
logo={
<>
<Image
src="/img/brand.svg"
alt="Svix logo"
width={80}
height={22}
className="dark:hidden"
priority
/>
<Image
src="/img/brand.white.svg"
alt="Svix logo"
width={80}
height={22}
className="hidden dark:block"
priority
/>
</>
}
projectLink={siteData.repo}
>
</Navbar>
)

const footer = <Footer>Copyright © Svix</Footer>

const SECTION_ROOTS = new Set([
"stream",
"ingest",
"receiving"
]);

type DocsLayoutParams = {
mdxPath?: string[];
};

type DocsLayoutProps = {
children: ReactNode;
params: Promise<DocsLayoutParams>;
};

// This is a small hack to hide the top-level pages (for Svix dispatch) when the user is in the Ingest/Stream/Receiving sections.
// We have to do it because dispatch pages are not under its own sub-section (/dispatch/*), they live at the root, and Nextra doesn't work well with that.
async function getNestedPageMap(mdxPath: string[]): Promise<PageMapItem[]> {
const topLevelSegment = mdxPath?.[0];
const pageMapRoute =
topLevelSegment && SECTION_ROOTS.has(topLevelSegment)
? `/${topLevelSegment}`
: "/";
return await getPageMap(pageMapRoute);
}

export default async function DocsLayout({ children, params }: DocsLayoutProps) {
const resolvedParams = await params;
const pageMap = await getNestedPageMap(resolvedParams.mdxPath);

return (
<html lang="en" dir="ltr" suppressHydrationWarning>
<Head>
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<Script src="https://www.googletagmanager.com/gtag/js?id=G-Z7S16CMH3G" strategy="afterInteractive" />
<Script id="gtag-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-Z7S16CMH3G', { anonymize_ip: true });
`}
</Script>
<Script src="/js/segment.js" strategy="afterInteractive" />
<Script src="/js/apollo.js" strategy="afterInteractive" />
<Script src="/js/commonroom.js" strategy="afterInteractive" />
</Head>
<body>
<PostHogProvider>
<Layout
navbar={navbar}
pageMap={pageMap}
docsRepositoryBase={siteData.docsSource}
footer={footer}
>
{children}
</Layout>
</PostHogProvider>
</body>
</html>
)
}
28 changes: 28 additions & 0 deletions app/[[...mdxPath]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { generateStaticParamsFor, importPage } from 'nextra/pages'
import { useMDXComponents as getMDXComponents } from '../../mdx-components'

export const generateStaticParams = generateStaticParamsFor('mdxPath')

export async function generateMetadata(props: {
params: Promise<{ mdxPath?: string[] }>
}) {
const params = await props.params
const { metadata: mdxMeta } = await importPage(params.mdxPath)
return {
...mdxMeta,
title: `${mdxMeta.title} | Svix Docs`,
}
}

const Wrapper = getMDXComponents().wrapper

export default async function Page(props: { params: Promise<{ mdxPath: string[] }> }) {
const params = await props.params
const result = await importPage(params.mdxPath)
const { default: MDXContent, toc, metadata, sourceCode } = result
return (
<Wrapper toc={toc} metadata={metadata} sourceCode={sourceCode}>
<MDXContent {...props} params={params} />
</Wrapper>
)
}
Binary file added app/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions app/components/product-switcher/BrandSmallIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// We have to inline this SVG so the currentColor works
export function BrandSmallIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 230 230"
width={20}
height={20}
className={className}
aria-hidden
>
<g transform="translate(10.7,10.6)">
<g>
<path
fill="currentColor"
fillRule="evenodd"
d="M208.8 104.4c0 57.6-46.8 104.4-104.4 104.4c-57.6 0-104.4-46.8-104.4-104.4c0-57.6 46.8-104.4 104.4-104.4c57.6 0 104.4 46.8 104.4 104.4Zm-125 42.4c-7.2-16.1-23.3-26.8-41-27.2c-11.7-0.3-22.8 3-31.8 9.1c-2.1-8.3-3.2-17-3.1-26c1-53.2 44.9-95.7 98.2-94.8c35 .7 65.3 19.8 81.7 48c-4.9 5.4-12.8 8.6-21.2 8.3c-8.1-0.2-15.5-5.1-18.8-12.5c-7.3-16.1-23.4-26.8-41-27.2c-8.2-0.2-16.2 1.9-23.3 5.9c-24.5 13.7-32 47-14.3 69.3c8 10.1 20.2 16.2 34.2 17.2c7.3 .5 12.8 3.1 16.5 7.7c6.6 8.4 5.6 21.6-2.1 28.9c-4.1 3.9-9.5 6-15.1 5.8c-8.1-0.2-15.5-5.1-18.9-12.5Zm82.2-57.7c-17.3-0.8-33.5-10.4-41-27.1c-3.4-7.4-10.8-12.3-18.9-12.5c-17.1-0.5-28 21.1-17.2 34.7c3.7 4.6 9.2 7.2 16.5 7.7c14 1 26.1 7.1 34.1 17.2c14.7 18.4 12.6 46.3-4.4 62.5c-20.6 19.5-54.8 15.6-70.4-7.9c-1.4-2.1-2.6-4.3-3.7-6.6c-3.3-7.4-10.7-12.3-18.8-12.5c-8.4-0.2-16.2 3-21.2 8.3c16.4 28.2 46.7 47.4 81.7 48c53.3 .9 97.2-41.5 98.2-94.8c.1-9-1-17.7-3.1-26c.4 1.8-13.4 6.6-15 7c-5.5 1.6-11.2 2.3-16.8 2Z"
/>
</g>
</g>
<ellipse
cx={115}
cy={115}
rx={104.5}
ry={104.5}
fill="none"
stroke="currentColor"
strokeWidth={20}
/>
</svg>
)
}
132 changes: 132 additions & 0 deletions app/components/product-switcher/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use client'

import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import {
ArrowDownTrayIcon,
ChevronDownIcon,
PlayCircleIcon,
SignalIcon,
} from '@heroicons/react/24/outline'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { faGem } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { BrandSmallIcon } from './BrandSmallIcon'

type Product = {
id: string
name: string
description: string
path?: string
href?: string
hidden?: boolean
icon: React.ReactNode
}

const products: Product[] = [
{
id: 'dispatch',
name: 'Svix Dispatch',
path: '/',
description: 'Webhooks Sending',
icon: <BrandSmallIcon className="shrink-0 text-gray-500 dark:text-neutral-400" />,
},
{
id: 'stream',
name: 'Svix Stream',
path: '/stream/introduction',
description: 'Event Streaming',
icon: <SignalIcon width={20} height={20} aria-hidden />,
},
{
id: 'ingest',
name: 'Svix Ingest',
path: '/ingest/receiving-with-ingest',
description: 'Webhooks Receiving',
icon: <ArrowDownTrayIcon width={20} height={20} aria-hidden />,
},
{
id: 'play',
name: 'Svix Play',
href: 'https://svix.com/play',
description: 'Webhooks Debugger',
icon: <PlayCircleIcon width={20} height={20} aria-hidden />,
},
{
id: 'diom',
name: 'Diom',
href: 'https://diom.svix.com/docs',
description: 'Components Platform',
icon: <FontAwesomeIcon icon={faGem} width={20} height={20} />,
}
]

function getSelectedProduct(pathname: string): Product {
let p = pathname?.split('#')[0] ?? '/'
if (p.length > 1 && p.endsWith('/')) {
p = p.slice(0, -1)
}
const normalized = p === '' ? '/' : p

if (normalized.startsWith('/stream')) {
return products.find((p) => p.id === 'stream')!
}
if (normalized.startsWith('/ingest')) {
return products.find((p) => p.id === 'ingest')!
}
if (normalized.startsWith('/play')) {
return products.find((p) => p.id === 'play')!
}
return products.find((p) => p.id === 'dispatch')!
}

export function ProductSwitcher() {
const pathname = usePathname() ?? '/'
const selected = getSelectedProduct(pathname)

if (selected.hidden) {
return null
}

const visible = products.filter((p) => !p.hidden)

return (
<Menu as="div" data-product-switcher>
<MenuButton
type="button"
className="flex items-center justify-between gap-2 px-4 py-3 mt-2 md:mt-0 rounded-md hover:bg-gray-100 dark:hover:bg-neutral-800/50 cursor-pointer border nextra-border transition-all duration-300 w-full outline-none"
>
{selected.icon}
<div className="flex flex-col ml-1 text-left">
<span>{selected.name}</span>
<span className="text-xs text-gray-500 dark:text-neutral-400 font-normal">{selected.description}</span>
</div>

<ChevronDownIcon width={16} height={16} aria-hidden className="ml-auto" />
</MenuButton>

<MenuItems
anchor="bottom"
className="flex flex-col bg-[rgb(var(--nextra-bg))] outline-none mt-2 w-[16em] mx-4 shadow-md rounded-md border nextra-border"
>
<span className="text-sm text-gray-500 dark:text-neutral-400 px-4 py-2">Products</span>
{visible.map((product) => {
return (
<MenuItem
key={product.id}
as={product.href ? Link : 'a'}
href={product.href ?? product.path ?? '/'}
className="flex items-center gap-2 px-4 py-3 hover:bg-gray-100 dark:hover:bg-neutral-800/50 cursor-pointer border-t nextra-border"
>
{product.icon}
<div className="flex flex-col ml-2">
<span className="text-sm font-medium">{product.name}</span>
<span className="text-xs text-gray-500 dark:text-neutral-400 font-normal">{product.description}</span>
</div>
</MenuItem>
)
})}
</MenuItems>
</Menu >
)
}
18 changes: 18 additions & 0 deletions app/components/providers/PostHogProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

import posthog from 'posthog-js'
import { PostHogProvider as PHProvider } from 'posthog-js/react'
import { useEffect } from 'react'

export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
defaults: '2026-01-30',
})
}, [])
if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return <>{children}</>
return <PHProvider client={posthog}>{children}</PHProvider>
}
20 changes: 20 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@import "tailwindcss";

/* Match next-themes / Nextra: `dark` class on a parent (usually <html>) */
@custom-variant dark (&:where(.dark, .dark *));

@source "./**/*.{js,ts,tsx,jsx}";
@source "../src/**/*.{js,ts,tsx,jsx}";
@source "../content/**/*.mdx";
@source "../mdx-components.js";

.nextra-navbar a[href^="https"] svg.x\:inline {
display: none;
}

.nextra-sidebar li:has([data-product-switcher]),
.nextra-mobile-nav li:has([data-product-switcher]) {
margin-top: 0;
margin-bottom: -0.25rem;
padding: 0;
}
Binary file added app/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading