From 59219c06d6085e8f903ba094f1b8da6c3b1de400 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 27 Sep 2026 20:13:11 +0400 Subject: [PATCH 1/2] docs(web): include feature cards in introduction Markdown --- apps/web/content/docs/introduction.mdx | 56 +++++++++- apps/web/source.config.ts | 11 +- .../components/docs/docs-mdx-components.tsx | 3 +- apps/web/src/components/docs/features.tsx | 105 +++++++----------- 4 files changed, 106 insertions(+), 69 deletions(-) diff --git a/apps/web/content/docs/introduction.mdx b/apps/web/content/docs/introduction.mdx index 746f739e..846d4eef 100644 --- a/apps/web/content/docs/introduction.mdx +++ b/apps/web/content/docs/introduction.mdx @@ -9,4 +9,58 @@ PayKit is an embedded Stripe billing framework for TypeScript apps. Define plans PayKit aims to be a complete billing framework. It provides a wide range of features out of the box and allows you to extend it with plugins. - + + +**Products in Code** + +Define plans and features as typed primitives. + + + +**Webhooks Handled** + +Verified, deduplicated, synced to your database automatically. + + + +**Usage Billing** + +Metered features with check() and report(). + + + +**Built For Stripe** + +Stripe subscriptions, webhooks, portal, and product sync built in. + + + +**Plugin Ecosystem** + +Dashboard, analytics, or build your own plugin. + + + +**Local Billing State** + +Billing state in your Postgres, joinable with your tables. + + + +**CLI** + +Init, push, and status. Scaffold, migrate, validate. + + + +**Client SDK** + +Browser-side billing calls with full type inference. + + + +**Type-safe** + +Plan IDs, feature IDs, events — all inferred from your schema. + + diff --git a/apps/web/source.config.ts b/apps/web/source.config.ts index 3a6ea149..f8e105d8 100644 --- a/apps/web/source.config.ts +++ b/apps/web/source.config.ts @@ -7,7 +7,16 @@ export const docs = defineDocs({ dir: "./content/docs", docs: { postprocess: { - includeProcessedMarkdown: true, + includeProcessedMarkdown: { + stringify(node, _parent, state, info) { + if ( + node.type === "mdxJsxFlowElement" && + (node.name === "Features" || node.name === "FeatureCard") + ) { + return state.containerFlow(node, info); + } + }, + }, }, }, }); diff --git a/apps/web/src/components/docs/docs-mdx-components.tsx b/apps/web/src/components/docs/docs-mdx-components.tsx index 64831709..2074b3a6 100644 --- a/apps/web/src/components/docs/docs-mdx-components.tsx +++ b/apps/web/src/components/docs/docs-mdx-components.tsx @@ -9,7 +9,7 @@ import { RiLinkM, } from "react-icons/ri"; -import { Features } from "@/components/docs/features"; +import { FeatureCard, Features } from "@/components/docs/features"; import { MdxTab, MdxTabs, MdxTabsList, MdxTabsPanel, MdxTabsTab } from "@/components/docs/mdx-tabs"; import { Anchor, @@ -308,5 +308,6 @@ export const docsMdxComponents = { TabsList: MdxTabsList, TabsPanel: MdxTabsPanel, TabsTab: MdxTabsTab, + FeatureCard, Features, } satisfies MDXComponents; diff --git a/apps/web/src/components/docs/features.tsx b/apps/web/src/components/docs/features.tsx index a90944d0..e264715a 100644 --- a/apps/web/src/components/docs/features.tsx +++ b/apps/web/src/components/docs/features.tsx @@ -1,6 +1,5 @@ -"use client"; - import type { ReactNode } from "react"; +import { Children, isValidElement } from "react"; import { RiBox3Line, RiComputerLine, @@ -13,75 +12,49 @@ import { RiWebhookLine, } from "react-icons/ri"; +import { extractText } from "@/components/docs/react-node-text"; import { FrameCorners } from "@/components/ui/frame-corners"; -const features: { icon: ReactNode; title: string; description: string }[] = [ - { - icon: , - title: "Products in Code", - description: "Define plans and features as typed primitives.", - }, - { - icon: , - title: "Webhooks Handled", - description: "Verified, deduplicated, synced to your database automatically.", - }, - { - icon: , - title: "Usage Billing", - description: "Metered features with check() and report().", - }, - { - icon: , - title: "Built For Stripe", - description: "Stripe subscriptions, webhooks, portal, and product sync built in.", - }, - { - icon: , - title: "Plugin Ecosystem", - description: "Dashboard, analytics, or build your own plugin.", - }, - { - icon: , - title: "Local Billing State", - description: "Billing state in your Postgres, joinable with your tables.", - }, - { - icon: , - title: "CLI", - description: "Init, push, and status. Scaffold, migrate, validate.", - }, - { - icon: , - title: "Client SDK", - description: "Browser-side billing calls with full type inference.", - }, - { - icon: , - title: "Type-safe", - description: "Plan IDs, feature IDs, events — all inferred from your schema.", - }, -]; +const featureIcons: Record = { + "Products in Code": RiBox3Line, + "Webhooks Handled": RiWebhookLine, + "Usage Billing": RiSpeedUpLine, + "Built For Stripe": RiPlug2Line, + "Plugin Ecosystem": RiPuzzle2Line, + "Local Billing State": RiDatabase2Line, + CLI: RiTerminalBoxLine, + "Client SDK": RiComputerLine, + "Type-safe": RiShieldCheckLine, +}; + +export function Features({ children }: { children: ReactNode }) { + return ( +
{children}
+ ); +} + +/** Renders a feature's Markdown title and description in the card layout. */ +export function FeatureCard({ children }: { children: ReactNode }) { + const [titleNode, descriptionNode] = Children.toArray(children).filter( + isValidElement<{ children?: ReactNode }>, + ); + const title = extractText(titleNode?.props.children); + const Icon = featureIcons[title] ?? RiBox3Line; -export function Features() { return ( -
- {features.map((feature) => ( -
- -
- - {feature.icon} - -
-

{feature.title}

-

- {feature.description} -

-
-
+
+ +
+ + + +
+

{title}

+

+ {descriptionNode?.props.children} +

- ))} +
); } From 458f2ff55cf65aa35b47a680a1015826c26b2259 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 27 Sep 2026 20:20:18 +0400 Subject: [PATCH 2/2] codex: address PR review feedback (#219) --- apps/web/src/components/docs/features.tsx | 24 +++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/docs/features.tsx b/apps/web/src/components/docs/features.tsx index e264715a..bce1cf9c 100644 --- a/apps/web/src/components/docs/features.tsx +++ b/apps/web/src/components/docs/features.tsx @@ -35,11 +35,23 @@ export function Features({ children }: { children: ReactNode }) { /** Renders a feature's Markdown title and description in the card layout. */ export function FeatureCard({ children }: { children: ReactNode }) { - const [titleNode, descriptionNode] = Children.toArray(children).filter( - isValidElement<{ children?: ReactNode }>, - ); - const title = extractText(titleNode?.props.children); - const Icon = featureIcons[title] ?? RiBox3Line; + const blocks = Children.toArray(children); + const [titleNode, descriptionNode] = blocks; + + if ( + blocks.length !== 2 || + !isValidElement<{ children?: ReactNode }>(titleNode) || + !isValidElement<{ children?: ReactNode }>(descriptionNode) + ) { + throw new Error("FeatureCard needs a Markdown title and description paragraph."); + } + + const title = extractText(titleNode.props.children).trim().replace(/\s+/g, " "); + const Icon = featureIcons[title]; + + if (!Icon || !extractText(descriptionNode.props.children).trim()) { + throw new Error(`FeatureCard has an unknown title or empty description: ${title}`); + } return (
@@ -51,7 +63,7 @@ export function FeatureCard({ children }: { children: ReactNode }) {

{title}

- {descriptionNode?.props.children} + {descriptionNode.props.children}