diff --git a/content/docs/framework/actions.mdx b/content/docs/framework/actions.mdx new file mode 100644 index 00000000..8fc4a97b --- /dev/null +++ b/content/docs/framework/actions.mdx @@ -0,0 +1,70 @@ +--- +title: "Actions" +description: "Close the paywall, restore purchases, open links, request OS permissions, and call back into your app, everything a paywall asks its host to do." +--- + +A paywall runs inside your app, and some things only the host can do: dismiss the paywall, open a link, prompt for a permission, run your app's code. All of it goes through `useActions()`: + +```tsx +import { useActions } from "superwall/hooks"; + +const { close, restore, openUrl, requestPermission, requestCallback } = useActions(); +``` + +## The actions + +| Action | Use it for | +| --- | --- | +| `close()` | Closing the paywall. The X button. Closing is not navigation. | +| `restore()` | Restore purchases. Fire-and-forget. There is no restore-result event and no value to await; success surfaces as a dismissed paywall. See [Purchases](/framework/purchases#restore). | +| `openUrl(url)` | Terms, privacy, any link. **Always this, never ``.** | +| `openExternalUrl(url)` | Open in the system browser instead of in-app. | +| `openDeepLink(link)` | Deep link into the app. | +| `customPlacement(name, params?)` | Fire a Superwall placement, which can present another paywall. | +| `requestPermission(type)` | OS permission prompt. Resolves `"granted" \| "denied" \| "unsupported"`. | +| `requestCallback(name, options?)` | Run **your app's** code and await its answer. Resolves `{ status: "success" \| "failure", data? }`. | +| `requestStoreReview("in-app" \| "external")` | Store review prompt. | + + +Links go through `openUrl`, never an ``. Inside a webview, an anchor either does nothing or navigates the paywall away from itself. `openUrl` hands the URL to the host so it opens the way the platform expects. + + +Closing works the same way: the paywall lives on a navigation stack of its own pages, but *leaving* the paywall isn't a navigation. It's `close()`. See [Pages & navigation](/framework/navigation). + +## Permissions + +```tsx +const status = await requestPermission("notification"); +// "granted" | "denied" | "unsupported" +``` + +Permission types: `notification`, `camera`, `microphone`, `location`, `background_location`, `contacts`, `read_images`, `read_video` (Android only), `tracking`. + +## Callbacks: ask your app a question + +A callback runs code *in your app* and hands the answer back to the paywall, anything the paywall cannot know on its own: does this account exist, is this referral code valid, what did the user pick during signup. + +```tsx +const result = await requestCallback<{ exists: boolean }>("checkAccount"); + +if (result.status === "success" && result.data?.exists) { + router.push("welcome-back"); +} +``` + +Type the answer with a claim, as above. The generic is your statement of what the app returns. + +### Permission vs callback + +A **permission** asks the OS; a **callback** asks your app. Both resolve from code the paywall does not control, which shapes how you use them: + +- **Show something while they run.** The OS prompt or your app's code takes as long as it takes. +- **Treat a denial as an ordinary outcome**, not an error. A user who declines notifications is still a user, design the path that continues without. + +## In development + +In `superwall dev`, actions don't reach a real host. They're logged in the studio's event log, and permission, callback, and purchase requests prompt **you** to pick the outcome. That makes both branches of every flow testable before a device ever sees it. See [The studio](/framework/studio). + + +The permissions example shows `requestPermission` and `requestCallback` side by side, with a denial treated as an outcome rather than an error. See [Examples](/framework/examples). + diff --git a/content/docs/framework/assets.mdx b/content/docs/framework/assets.mdx new file mode 100644 index 00000000..5512dc06 --- /dev/null +++ b/content/docs/framework/assets.mdx @@ -0,0 +1,122 @@ +--- +title: "Assets" +description: "Add images, video, audio, fonts, and animations to a paywall by importing files. The build handles optimization, hosting, and caching." +--- + +Add media to a paywall by importing files from an `assets/` directory. The build handles optimization, hosting, and caching; there is nothing to configure and no upload step. + +## Where assets live + +Assets follow the same two-level pattern as components and messages: shared at the project root, local inside a paywall. + +```ts +superwall/ +├── assets/ shared across every paywall +└── paywalls/pro/ + └── assets/ this paywall's own +``` + + +Every asset belongs in an `assets/` directory, `superwall/assets/` for shared files, `superwall/paywalls//assets/` for one paywall's own. If a large asset lives anywhere else, the build fails and names the file. + + +## Use an image + +Import the file and use it like any URL: + +```tsx +import hero from "@/assets/hero.jpg"; // shared: superwall/assets/ +import badge from "../assets/badge.png"; // this paywall's own + + +``` + +CSS `url()` works the same way. Imports typecheck because of the generated `superwall.d.ts`, one more reason to [commit it](/framework/project-structure). + +Supported out of the box: + +| Kind | Formats | +| --- | --- | +| Images | `png` `jpg` `jpeg` `webp` `avif` `gif` `svg` `ico` `apng` | +| Video | `mp4` `webm` `mov` `m4v` | +| Audio | `mp3` `m4a` `aac` `wav` `ogg` | +| Fonts | `woff2` `woff` `ttf` `otf` | +| Animation & 3D | `lottie` `riv` `glb` | + +`?url`, `?raw`, and `?inline` import suffixes work too, as do CSS modules. + +## How hosting works + +You never choose where an asset is served from. The build decides, and nothing about your code changes either way: + +- **Video, audio, and fonts** are always served from Superwall's CDN, whatever their size. Video streams properly instead of being carried by the paywall, and one upload is reused across every version of every paywall. +- **Images** embed in the paywall when small and move to the CDN when large. + +```tsx +import promo from "../assets/promo.mp4"; + +` either does nothing or navigates the paywall away from itself. Open links through `useActions().openUrl` instead. See [Actions](/framework/actions). + +### Controls sit in the status bar / under the home indicator + +`env(safe-area-inset-*)` resolves to 0 in previews and some webview contexts, so bare `env()` math collapses. Always wrap in `max()` with a floor. See [Styling](/framework/styling). + +### The payment sheet doesn't open in dev + +By design, `superwall dev` previews the flow and copy but doesn't mount the web checkout payment sheet. Push and open the live URL to verify the checkout itself. See [Web checkout](/framework/web-checkout). diff --git a/content/docs/framework/variables.mdx b/content/docs/framework/variables.mdx new file mode 100644 index 00000000..4e90c37c --- /dev/null +++ b/content/docs/framework/variables.mdx @@ -0,0 +1,74 @@ +--- +title: "Variables & Personalization" +description: "React to user attributes, device state, and placement parameters, and write paywalls the dashboard can experiment on without a rebuild." +--- + +Everything your app and the SDK tell a paywall about the presentation arrives through `useVariables()`: who the user is, what device they're on, and what the placement was called with. Read these defensively and a single paywall can greet a returning user by name, adapt to platform, or react to any parameter your app passes, all without a rebuild. + +## `useVariables()` + +```tsx +import { useVariables } from "superwall/hooks"; + +const { device, user, params } = useVariables(); +``` + +Three records, three sources: + +- **`device`**, filled in by the SDK: `platform`, `deviceModel`, `osVersion`, `appVersion`, `deviceLocale`, `regionCode`, `deviceCurrencyCode`, `subscriptionStatus`, `activeEntitlements`, `daysSinceInstall`, `totalPaywallViews`, and more. +- **`user`**, whatever your app set via `setUserAttributes` (`user.firstName`, `user.plan`, …). +- **`params`**, whatever the placement was called with (`params.event_name` is the placement's name; `$`-prefixed keys are SDK-set, and anything the app passed alongside comes through unprefixed). + +```tsx +const name = typeof user.firstName === "string" ? user.firstName : undefined; + +

{name ? `Welcome back, ${name}` : "Go Pro"}

+{device.platform ?? "—"} +``` + +## Guard every read + +All three records are filled in by the host, your paywall controls none of them, so every read needs a fallback: + +- For **`device`** fields, `?? "—"` (or any sensible default) suffices. The SDK guarantees the shape, just not that a value has arrived yet. +- For **`user`** and **`params`**, the host controls the *type* too, so check it before using it: `typeof params.event_name === "string"`. An attribute your app sets as a number today might be a string tomorrow, and the paywall must not crash either way. + + +`device.isSandbox` is a string, not a boolean. Compare it as one. + + +While previewing, every one of these values is editable live in the studio's **Variables** panel: user attributes, device properties, placement params, and per-product variables, all seeded from your app's real sample data. Change a value and watch the paywall react. See [The studio](/framework/studio). + +## `useUser()` + +Shorthand for when you only need the user record: + +```tsx +import { useUser } from "superwall/hooks"; + +const user = useUser(); +``` + +Identical to `useVariables().user`, reach for it when the device and params records aren't needed. + +## `useDevice()` + +The same device record as `useVariables().device`, plus **`orientation`**: + +```tsx +import { useDevice } from "superwall/hooks"; + +const { orientation, platform, deviceModel } = useDevice(); +``` + +`orientation` is `"portrait" | "landscape"`, measured in the page itself, it updates the moment the device turns, so you can build layouts that answer to rotation. The orientation example reflows to a two-column grid in landscape rather than shrinking the portrait layout; see [Examples](/framework/examples). + +## Built to be experimented on + +Notice what's missing: variables are never *declared* in code. What the paywall reads, user attributes, device state, placement params, product variables, trial eligibility, is supplied by the app and the store at runtime, and the studio overrides all of it live while previewing. + +Write every read defensively, guarded, typed, with a designed fallback, and every one of those values becomes a knob the dashboard can turn without a rebuild. A paywall that renders sensibly for any combination of inputs can be A/B tested freely. + + +The personalization example shows the full doctrine in one project: `?? "—"` for SDK-guaranteed device fields, `typeof` checks for host-controlled user and params reads, and designed fallbacks for every string. See [Examples](/framework/examples). + diff --git a/content/docs/framework/web-checkout.mdx b/content/docs/framework/web-checkout.mdx new file mode 100644 index 00000000..4f85a5ec --- /dev/null +++ b/content/docs/framework/web-checkout.mdx @@ -0,0 +1,92 @@ +--- +title: "Web Checkout" +description: "Sell the same paywall on the web with one config key, Stripe payment in a sheet, Apple Pay, or a hosted checkout page, with purchase() unchanged." +--- + +One config key sells the same paywall on the web: + +```ts +checkout: "sheet", +``` + +Native store products ignore it. Drop the same paywall into your iOS app and a store reference buys through the App Store. Your components don't change, and neither does `purchase()`. + +One exception: in `external` mode the page navigates away to the hosted checkout page, so `purchase()` never resolves. Call it, but don't `await` it or branch on its result. + + +This page covers the framework side: config, modes, and prefetching. Stripe keys, web apps, products, and campaigns are set up in the dashboard, see the [Web Checkout](/web-checkout) section for that half. + + +## Modes + +| Mode | The purchase | Use when | +| --- | --- | --- | +| `sheet` | Stripe checkout in a sheet **over the paywall**, nobody leaves mid-flow | The default choice for the web | +| `applePay` | Straight to Apple Pay where available, sheet as fallback | Apple-Pay-heavy audiences | +| `external` | Superwall's hosted checkout page, then back | You want zero payment UI in the paywall | + +Only `sheet` and `applePay` add payment UI to the paywall; `external` adds nothing. Stripe's own scripts load at runtime from `js.stripe.com` rather than being bundled. + +## Products + +Web paywalls sell Stripe products, declared with the price inside the identifier, `{environment}:{priceId}:{offer}`, where `{environment}` is exactly `test` or `live`: + +```ts +products: { + monthly: "live:price_1ABC…:7days-free", +}, +``` + +A paywall can declare store and Stripe products side by side. See [Products](/framework/products). + +## The purchase, unchanged + +With `sheet` or `applePay` and a Stripe product, the same `purchase()` call opens the payment sheet in-page, a brief loading overlay covers the session creation unless it was prefetched. The outcomes map exactly as they do natively: + +- `completed`, payment succeeded +- `abandoned`, the shopper closed the sheet +- `failed`, a payment or session error + + +The web sheet does not set `isPurchasing`, react to the awaited result, which is the right pattern everywhere anyway. A web paywall also typically drops the close button and restore link its native sibling carries: there's no host app to close back to. + + +## Prefetch: make the sheet open instantly + +Creating a checkout session takes a network round-trip. Prefetching does it before the tap, so the sheet opens with nothing to wait for. + +**Automatic:** a `sheet` paywall warms one Stripe product on load; `applePay` warms every Stripe product on the paywall, up to ten. Steer it in config: + +```ts +checkout: { mode: "sheet", prefetch: "pro" } // which product warms first +checkout: { mode: "sheet", prefetch: false } // disable auto-prefetch +``` + +**On selection, do this whenever there's a product selector.** With `sheet`, only one plan is warmed; prefetch the selected one so whichever plan is on screen opens instantly: + +```tsx +import { usePurchase, type ProductReference } from "superwall/hooks"; + +const { purchase, prefetch } = usePurchase(); +const [reference, setReference] = React.useState("monthly"); + +React.useEffect(() => { + prefetch(reference); +}, [prefetch, reference]); +``` + +`prefetch` is safe to call unconditionally. It's a no-op for store products, for paywalls without web checkout, and for already-warm sessions (sessions stay warm for about ten minutes). It's a hint; never await it. + +## The sheet is not yours to style + +It takes no colors, fonts, or spacing from the page around it, and there's no prop to change that. This is deliberate: payment UI that borrows the paywall's design stops looking like payment UI, and the payment step is the one place a shopper is entitled to see something they recognize. Safe areas, scroll locking, and Escape handling (never mid-payment) are handled for you. + +## Verify on a pushed version + +`superwall dev` previews the flow and the copy, but it does not mount the payment sheet. Push and open the live URL to verify the checkout itself, see [Push, promote & publish](/framework/push-and-promote). + +Two things gate that push: the Stripe product must already be imported into your Superwall dashboard (push validates every declared identifier, Stripe ones included), and the application needs the `headless_paywalls` feature enabled, see the [CLI reference](/framework/cli). + +## A full web funnel + +The `web-funnel` [example](/framework/examples) is the reference: question steps as pages, a typed plan selector with on-selection prefetch, then `purchase(reference)`, the whole flow in one paywall. Because `checkout` is set, the flow's step and answers live in the page URL, so it resumes from any link, in Safari after an in-app browser, or back from hosted checkout. Keep every answer in `useQueryState`. [Web Funnels](/framework/web-funnels) is the guide. diff --git a/content/docs/framework/web-funnels.mdx b/content/docs/framework/web-funnels.mdx new file mode 100644 index 00000000..0dfa1a31 --- /dev/null +++ b/content/docs/framework/web-funnels.mdx @@ -0,0 +1,119 @@ +--- +title: "Web Funnels" +description: "Quizzes and checkout funnels on the web: one paywall whose steps are pages, every answer kept in the URL so the flow survives any browser hand-off, and payment at the end." +--- + +A web funnel is a paywall with `checkout` set: a few question pages, a plan, then `purchase()`. It is served as a normal web page, and a web page has one problem a native paywall never has: **the person may change browsers halfway through.** A link opened from Instagram or TikTok runs in that app's in-app browser; tapping "Open in Safari" (or being sent there to pay with Apple Pay) hands over the URL and nothing else. `localStorage`, cookies, React state, all of it stays behind. Hosted checkout comes back to a URL too, and a reload starts from scratch. + +So a web funnel keeps its state in the URL. The router does its half automatically; your half is one rule. + +## The rule: every answer is `useQueryState` + +On a web funnel, **never hold an answer in `useState`, layout context, or a module.** Single choice, multi choice, text input, the selected plan, anything the person entered lives in `useQueryState`, so any URL resumes the flow on the same step with the same answers. + +```tsx +import { parseAsArrayOf, parseAsStringEnum, useQueryState } from "superwall/navigation"; + +const GOALS = ["focus", "habit", "catch-up"] as const; +const goalParser = parseAsStringEnum(GOALS); + +// single choice — set, then move on +const [goal, setGoal] = useQueryState("goal", goalParser); +const choose = (value: (typeof GOALS)[number]) => { + haptics.selection(); + setGoal(value); + router.push("interests"); +}; +``` + +```tsx +// multi choice — an array of enum ids, toggled in place +const [interests, setInterests] = useQueryState( + "interests", + parseAsArrayOf(parseAsStringEnum(["reading", "writing", "speaking"])).withDefault([]), +); +const toggle = (id: Interest) => + setInterests((current) => + current.includes(id) ? current.filter((one) => one !== id) : [...current, id], + ); +``` + +```tsx +// text input — a plain string; replaces are coalesced, so typing is safe +const [name, setName] = useQueryState("name"); + setName(event.target.value || null)} /> +``` + +```tsx +// the selected plan, typed against config +const [plan, setPlan] = useQueryState("plan", parseAsStringEnum(["monthly", "annual"]).withDefault("annual")); +``` + +Every later page reads the same hook: the plan page shows `goal`, the summary page lists `interests`, and `purchase(plan)` uses the selection, all with no context and no prop drilling. Guard reads on pages someone might land on directly: `goal ? COPY[goal] : COPY.default`. + +The API is [nuqs](https://nuqs.dev)'s, so its parsers read the same: `parseAsString`, `parseAsInteger`, `parseAsFloat`, `parseAsBoolean`, `parseAsStringEnum`, `parseAsArrayOf`, `createParser`, each with `.withDefault()` (removes `null` from the type and clears the key when the value equals the default) and `.withOptions({ history, clearOnDefault })`. Junk in the URL parses to the default. Full signatures are in [Hooks](/framework/hooks#usequerystatekey-parser). + + +The same hook on a native host, where there is no URL bar, is plain state shared across pages. A funnel written this way runs unchanged natively; only where the state is kept differs. + + +## What the router does on its own + +With `checkout` set, `queryState` defaults to on and the route stack is mirrored into one reserved param: + +``` +https://yourapp.superwall.app/funnel?sw_nav=index,goal,interests&goal=habit&interests=reading,speaking +``` + +- `router.push` adds a browser history entry; back, replace and dismiss rewrite in place. **The browser's back button is `router.back()`**, including Android's hardware back. +- Any URL rebuilds the stack it names, with no animation and one `entry` page view. A route that no longer exists starts the flow over at `index`. +- Writes are coalesced so a text input can't trip Safari's history rate limit, and anything pending is flushed the moment the page is hidden, the instant before a hand-off or the jump to hosted checkout. +- Foreign params (`utm_*`, attribution) are left untouched, and survive the whole flow. + +`definePaywall({ queryState: false })` turns it off for a checkout surface; `queryState: true` turns it on for a web surface without checkout. See [Config](/framework/config). + +## Branch on the answers + +Branching reads the same state, so a branch taken before a hand-off is the branch resumed after it: + +```tsx +const [goal] = useQueryState("goal", goalParser); +const next = () => router.push(goal === "catch-up" ? "backlog" : "interests"); +``` + +Because the stack is in the URL as the routes actually visited, back always retraces the branch taken. + +## Keep the URL small and clean + +About 2 kB is safe across every app and share sheet, and a question flow of twenty short answers is well under 500 bytes if you follow three habits: + +- **Enumerate.** Store ids (`"habit"`), never labels (`"Build a habit"`). `parseAsStringEnum` gives you validation for free. +- **Short keys, cleared defaults.** `goal`, not `selectedGoalOption`; leave `clearOnDefault` on so untouched answers cost nothing. +- **Nothing personal.** URLs end up in referrer and analytics logs. An email or a name belongs in the checkout sheet's own fields, not in the query string. + +Keys starting with `sw_`, plus `platform` and `transport`, are reserved, the hook throws on them. + +## Move like a funnel + +Set the funnel transition once; `shift` fades each step in as it drifts into place and drops the previous step outright, so a long flow never reads as a growing stack: + +```ts +export default definePaywall({ + name: "Onboarding", + transition: "shift", + checkout: { mode: "sheet", prefetch: "annual" }, + products: { monthly: "live:price_…:no-trial", annual: "live:price_…:7days-free" }, +}); +``` + +Then the plan page prefetches the selected product and `purchase(plan)` opens the sheet, see [Web Checkout](/framework/web-checkout). + +## Checklist + +- `checkout` set in `config.ts`; `transition: "shift"` +- every answer, selection and input is `useQueryState`, no `useState` for anything the person entered +- enum ids, short keys, defaults cleared, nothing personal +- later pages guard their reads, so a direct link never crashes +- test it: answer two questions, copy the studio's iframe URL into a new tab, and you should land on the same step with the same answers + +The `web-funnel` [example](/framework/examples) is the reference. diff --git a/content/docs/meta.json b/content/docs/meta.json index 6a470cff..79c9649c 100644 --- a/content/docs/meta.json +++ b/content/docs/meta.json @@ -6,6 +6,7 @@ "---Docs---", "dashboard", + "framework", "agents", "web-checkout", "integrations", diff --git a/package.json b/package.json index 9b342011..b0ac8a00 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev": "vite dev", "dev:port": "vite dev --port", "prebuild": "bun run generate:changelog && bun run scripts/copy-docs-images.cjs && bun run generate:og", - "build": "NODE_OPTIONS=--max-old-space-size=5120 vite build && bun run scripts/generate-static-cache.ts && bun run scripts/generate-search-index.ts", + "build": "NODE_OPTIONS=--max-old-space-size=8192 vite build && bun run scripts/generate-static-cache.ts && bun run scripts/generate-search-index.ts", "build:cf": "bun run build", "build:cf:staging": "CLOUDFLARE_ENV=staging bun run build", "sync:mixedbread": "mxbai vs sync $MIXEDBREAD_STORE_ID './content/docs' --ci", diff --git a/src/lib/llms.ts b/src/lib/llms.ts index 9b89492a..21dde9de 100644 --- a/src/lib/llms.ts +++ b/src/lib/llms.ts @@ -5,6 +5,10 @@ export const llmsSectionConfigs = { label: "Dashboard", urlPrefix: "/docs/dashboard", }, + framework: { + label: "Framework", + urlPrefix: "/docs/framework", + }, agents: { label: "Agents", urlPrefix: "/docs/agents", diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index e9a798a1..6a2bd596 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -40,6 +40,8 @@ import { Route as IosLlmsDottxtRouteImport } from './routes/ios/llms[.]txt' import { Route as IosLlmsFullDottxtRouteImport } from './routes/ios/llms-full[.]txt' import { Route as IntegrationsLlmsDottxtRouteImport } from './routes/integrations/llms[.]txt' import { Route as IntegrationsLlmsFullDottxtRouteImport } from './routes/integrations/llms-full[.]txt' +import { Route as FrameworkLlmsDottxtRouteImport } from './routes/framework/llms[.]txt' +import { Route as FrameworkLlmsFullDottxtRouteImport } from './routes/framework/llms-full[.]txt' import { Route as FlutterLlmsDottxtRouteImport } from './routes/flutter/llms[.]txt' import { Route as FlutterLlmsFullDottxtRouteImport } from './routes/flutter/llms-full[.]txt' import { Route as ExpoLlmsDottxtRouteImport } from './routes/expo/llms[.]txt' @@ -216,6 +218,16 @@ const IntegrationsLlmsFullDottxtRoute = path: '/integrations/llms-full.txt', getParentRoute: () => rootRouteImport, } as any) +const FrameworkLlmsDottxtRoute = FrameworkLlmsDottxtRouteImport.update({ + id: '/framework/llms.txt', + path: '/framework/llms.txt', + getParentRoute: () => rootRouteImport, +} as any) +const FrameworkLlmsFullDottxtRoute = FrameworkLlmsFullDottxtRouteImport.update({ + id: '/framework/llms-full.txt', + path: '/framework/llms-full.txt', + getParentRoute: () => rootRouteImport, +} as any) const FlutterLlmsDottxtRoute = FlutterLlmsDottxtRouteImport.update({ id: '/flutter/llms.txt', path: '/flutter/llms.txt', @@ -317,6 +329,8 @@ export interface FileRoutesByFullPath { '/expo/llms.txt': typeof ExpoLlmsDottxtRoute '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute '/flutter/llms.txt': typeof FlutterLlmsDottxtRoute + '/framework/llms-full.txt': typeof FrameworkLlmsFullDottxtRoute + '/framework/llms.txt': typeof FrameworkLlmsDottxtRoute '/integrations/llms-full.txt': typeof IntegrationsLlmsFullDottxtRoute '/integrations/llms.txt': typeof IntegrationsLlmsDottxtRoute '/ios/llms-full.txt': typeof IosLlmsFullDottxtRoute @@ -365,6 +379,8 @@ export interface FileRoutesByTo { '/expo/llms.txt': typeof ExpoLlmsDottxtRoute '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute '/flutter/llms.txt': typeof FlutterLlmsDottxtRoute + '/framework/llms-full.txt': typeof FrameworkLlmsFullDottxtRoute + '/framework/llms.txt': typeof FrameworkLlmsDottxtRoute '/integrations/llms-full.txt': typeof IntegrationsLlmsFullDottxtRoute '/integrations/llms.txt': typeof IntegrationsLlmsDottxtRoute '/ios/llms-full.txt': typeof IosLlmsFullDottxtRoute @@ -413,6 +429,8 @@ export interface FileRoutesById { '/expo/llms.txt': typeof ExpoLlmsDottxtRoute '/flutter/llms-full.txt': typeof FlutterLlmsFullDottxtRoute '/flutter/llms.txt': typeof FlutterLlmsDottxtRoute + '/framework/llms-full.txt': typeof FrameworkLlmsFullDottxtRoute + '/framework/llms.txt': typeof FrameworkLlmsDottxtRoute '/integrations/llms-full.txt': typeof IntegrationsLlmsFullDottxtRoute '/integrations/llms.txt': typeof IntegrationsLlmsDottxtRoute '/ios/llms-full.txt': typeof IosLlmsFullDottxtRoute @@ -463,6 +481,8 @@ export interface FileRouteTypes { | '/expo/llms.txt' | '/flutter/llms-full.txt' | '/flutter/llms.txt' + | '/framework/llms-full.txt' + | '/framework/llms.txt' | '/integrations/llms-full.txt' | '/integrations/llms.txt' | '/ios/llms-full.txt' @@ -511,6 +531,8 @@ export interface FileRouteTypes { | '/expo/llms.txt' | '/flutter/llms-full.txt' | '/flutter/llms.txt' + | '/framework/llms-full.txt' + | '/framework/llms.txt' | '/integrations/llms-full.txt' | '/integrations/llms.txt' | '/ios/llms-full.txt' @@ -558,6 +580,8 @@ export interface FileRouteTypes { | '/expo/llms.txt' | '/flutter/llms-full.txt' | '/flutter/llms.txt' + | '/framework/llms-full.txt' + | '/framework/llms.txt' | '/integrations/llms-full.txt' | '/integrations/llms.txt' | '/ios/llms-full.txt' @@ -607,6 +631,8 @@ export interface RootRouteChildren { ExpoLlmsDottxtRoute: typeof ExpoLlmsDottxtRoute FlutterLlmsFullDottxtRoute: typeof FlutterLlmsFullDottxtRoute FlutterLlmsDottxtRoute: typeof FlutterLlmsDottxtRoute + FrameworkLlmsFullDottxtRoute: typeof FrameworkLlmsFullDottxtRoute + FrameworkLlmsDottxtRoute: typeof FrameworkLlmsDottxtRoute IntegrationsLlmsFullDottxtRoute: typeof IntegrationsLlmsFullDottxtRoute IntegrationsLlmsDottxtRoute: typeof IntegrationsLlmsDottxtRoute IosLlmsFullDottxtRoute: typeof IosLlmsFullDottxtRoute @@ -848,6 +874,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IntegrationsLlmsFullDottxtRouteImport parentRoute: typeof rootRouteImport } + '/framework/llms.txt': { + id: '/framework/llms.txt' + path: '/framework/llms.txt' + fullPath: '/framework/llms.txt' + preLoaderRoute: typeof FrameworkLlmsDottxtRouteImport + parentRoute: typeof rootRouteImport + } + '/framework/llms-full.txt': { + id: '/framework/llms-full.txt' + path: '/framework/llms-full.txt' + fullPath: '/framework/llms-full.txt' + preLoaderRoute: typeof FrameworkLlmsFullDottxtRouteImport + parentRoute: typeof rootRouteImport + } '/flutter/llms.txt': { id: '/flutter/llms.txt' path: '/flutter/llms.txt' @@ -996,6 +1036,8 @@ const rootRouteChildren: RootRouteChildren = { ExpoLlmsDottxtRoute: ExpoLlmsDottxtRoute, FlutterLlmsFullDottxtRoute: FlutterLlmsFullDottxtRoute, FlutterLlmsDottxtRoute: FlutterLlmsDottxtRoute, + FrameworkLlmsFullDottxtRoute: FrameworkLlmsFullDottxtRoute, + FrameworkLlmsDottxtRoute: FrameworkLlmsDottxtRoute, IntegrationsLlmsFullDottxtRoute: IntegrationsLlmsFullDottxtRoute, IntegrationsLlmsDottxtRoute: IntegrationsLlmsDottxtRoute, IosLlmsFullDottxtRoute: IosLlmsFullDottxtRoute, diff --git a/src/routes/framework/llms-full[.]txt.ts b/src/routes/framework/llms-full[.]txt.ts new file mode 100644 index 00000000..3b10190d --- /dev/null +++ b/src/routes/framework/llms-full[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMFullResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/framework/llms-full.txt")({ + server: { + handlers: { + GET: () => buildLLMFullResponseForSection("framework"), + }, + }, +}); diff --git a/src/routes/framework/llms[.]txt.ts b/src/routes/framework/llms[.]txt.ts new file mode 100644 index 00000000..047b5555 --- /dev/null +++ b/src/routes/framework/llms[.]txt.ts @@ -0,0 +1,10 @@ +import { buildLLMSummaryResponseForSection } from "@/lib/llms"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/framework/llms.txt")({ + server: { + handlers: { + GET: () => buildLLMSummaryResponseForSection("framework"), + }, + }, +}); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 0778fb98..ef50d21d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -82,6 +82,13 @@ const docsCards: DocCard[] = [ href: buildDocsPath("dashboard"), icon: , }, + { + title: "Framework", + description: + "Build paywalls, onboarding funnels, and web checkout flows as React mini-apps in your repo.", + href: buildDocsPath("framework"), + icon: , + }, { title: "Superwall Agents", description: