Skip to content
Open
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
28 changes: 14 additions & 14 deletions content/shared/vibe-coding.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ We've built a few tools to help you Vibe Code using the knowledge of the Superwa
- [Editor AI](#editor-ai): Build and refine a paywall directly inside the visual editor using AI Chat or an external MCP-compatible agent.

And right here in the Superwall Docs:

- [Superwall AI](#superwall-ai)
- [Docs Links](#docs-links)
- [LLMs.txt](#llmstxt)



## Superwall Agents

[Superwall Agents](/agents) is the dedicated Superwall AI workspace. Use it when you want to analyze experiment results, use selected Superwall organization context, work with files available to the active hosted machine, create charts and reports, suggest new experiments, schedule recurring prompts, or connect webhook-driven workflows.
Expand Down Expand Up @@ -58,7 +57,6 @@ Use Editor AI when the task is about changing the design, layout, products, vari

Superwall AI is available in the bottom right 💬 and is a great place to start if you have a question or issue.


## Docs Links

At the top of each page of the Superwall Docs (including this one!):
Expand All @@ -70,24 +68,26 @@ Also in the **Open** dropdown menu, you can access these options:
- **View as Markdown**: to view the page in Markdown format
- **Open in ChatGPT**, **Open in Claude**: to open the page in the respective AI tool and add the page as context for your conversation

You can also add `.md` to the end of any docs page URL to open that page as Markdown. For example, `https://superwall.com/docs/ios/quickstart/install.md`.

## LLMs.txt

The Superwall Docs website has `llms.txt` and `llms-full.txt` files, in total and for each SDK, that you can use to add context to your LLMs.

`llms.txt` is a summary of the docs with links to each page.

`llms-full.txt` is the full text of all of the docs.

| SDK | Summary | Full Text |
| -------- | ------- | --------- |
| All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) |
| Dashboard | [`llms-dashboard.txt`](https://superwall.com/docs/llms-dashboard.txt) | [`llms-full-dashboard.txt`](https://superwall.com/docs/llms-full-dashboard.txt) |
| iOS | [`llms-ios.txt`](https://superwall.com/docs/llms-ios.txt) | [`llms-full-ios.txt`](https://superwall.com/docs/llms-full-ios.txt) |
| Android | [`llms-android.txt`](https://superwall.com/docs/llms-android.txt) | [`llms-full-android.txt`](https://superwall.com/docs/llms-full-android.txt) |
| Flutter | [`llms-flutter.txt`](https://superwall.com/docs/llms-flutter.txt) | [`llms-full-flutter.txt`](https://superwall.com/docs/llms-full-flutter.txt) |
| Expo | [`llms-expo.txt`](https://superwall.com/docs/llms-expo.txt) | [`llms-full-expo.txt`](https://superwall.com/docs/llms-full-expo.txt) |
| React Native (Deprecated) | [`llms-react-native.txt`](https://superwall.com/docs/llms-react-native.txt) | [`llms-full-react-native.txt`](https://superwall.com/docs/llms-full-react-native.txt) |
| Integrations | [`llms-integrations.txt`](https://superwall.com/docs/llms-integrations.txt) | [`llms-full-integrations.txt`](https://superwall.com/docs/llms-full-integrations.txt) |
| Web Checkout | [`llms-web-checkout.txt`](https://superwall.com/docs/llms-web-checkout.txt) | [`llms-full-web-checkout.txt`](https://superwall.com/docs/llms-full-web-checkout.txt) |
| SDK | Summary | Full Text |
| ------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) |
| Dashboard | [`dashboard/llms.txt`](https://superwall.com/docs/dashboard/llms.txt) | [`dashboard/llms-full.txt`](https://superwall.com/docs/dashboard/llms-full.txt) |
| iOS | [`ios/llms.txt`](https://superwall.com/docs/ios/llms.txt) | [`ios/llms-full.txt`](https://superwall.com/docs/ios/llms-full.txt) |
| Android | [`android/llms.txt`](https://superwall.com/docs/android/llms.txt) | [`android/llms-full.txt`](https://superwall.com/docs/android/llms-full.txt) |
| Flutter | [`flutter/llms.txt`](https://superwall.com/docs/flutter/llms.txt) | [`flutter/llms-full.txt`](https://superwall.com/docs/flutter/llms-full.txt) |
| Expo | [`expo/llms.txt`](https://superwall.com/docs/expo/llms.txt) | [`expo/llms-full.txt`](https://superwall.com/docs/expo/llms-full.txt) |
| React Native (Deprecated) | [`react-native/llms.txt`](https://superwall.com/docs/react-native/llms.txt) | [`react-native/llms-full.txt`](https://superwall.com/docs/react-native/llms-full.txt) |
| Integrations | [`integrations/llms.txt`](https://superwall.com/docs/integrations/llms.txt) | [`integrations/llms-full.txt`](https://superwall.com/docs/integrations/llms-full.txt) |
| Web Checkout | [`web-checkout/llms.txt`](https://superwall.com/docs/web-checkout/llms.txt) | [`web-checkout/llms-full.txt`](https://superwall.com/docs/web-checkout/llms-full.txt) |

To minimize token use, we recommend using the files specific to your SDK.
39 changes: 3 additions & 36 deletions plugins/remark-image-paths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { visit } from "unist-util-visit";
import { normalizeDocsAssetPath } from "../src/lib/docs-asset-path";

export { normalizeDocsAssetPath };

/*
Fixes the image paths
Expand All @@ -10,42 +13,6 @@ output:
<Frame>![](/docs/images/3pa_cp_2.jpeg)</Frame>
*/

const DOCS_PREFIX = "/docs";
const SPECIAL_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;

export function normalizeDocsAssetPath(url: string): string {
const trimmed = url.trim();
if (!trimmed) return trimmed;

// Keep external/special schemes and fragment-only links untouched.
if (trimmed.startsWith("#") || trimmed.startsWith("//") || SPECIAL_URL_PATTERN.test(trimmed)) {
return trimmed;
}

const [, pathPart = "", suffix = ""] = trimmed.match(/^([^?#]*)([?#].*)?$/) ?? [];
const segments = pathPart
.replace(/^\/+/, "")
.split("/")
.filter(Boolean);

const normalizedSegments: string[] = [];
for (const segment of segments) {
if (segment === "." || segment === "") continue;
if (segment === "..") {
normalizedSegments.pop();
continue;
}
normalizedSegments.push(segment);
}

const normalizedPath = `/${normalizedSegments.join("/")}`;
if (normalizedPath === DOCS_PREFIX || normalizedPath.startsWith(`${DOCS_PREFIX}/`)) {
return `${normalizedPath}${suffix}`;
}

return `${DOCS_PREFIX}${normalizedPath === "/" ? "" : normalizedPath}${suffix}`;
}

export default function remarkImagePaths() {
return (tree: any) => {
visit(tree, "image", (node) => {
Expand Down
57 changes: 57 additions & 0 deletions plugins/remark-markdown-image-map.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { describe, test } from "node:test";
import { unified } from "unified";
import remarkMdx from "remark-mdx";
import remarkParse from "remark-parse";
import remarkMarkdownImageMap from "./remark-markdown-image-map";

async function collectMarkdownImageMap(markdown: string) {
const processor = unified().use(remarkParse).use(remarkMdx).use(remarkMarkdownImageMap);
const file = { data: {} };
const tree = processor.parse(markdown);

await processor.run(tree, file as any);
return file.data as {
markdownImageMap?: Record<string, string>;
"mdx-export"?: Array<{ name: string; value: unknown }>;
};
}

describe("remarkMarkdownImageMap", () => {
test("exports normalized image URLs keyed by Fumadocs placeholders", async () => {
const data = await collectMarkdownImageMap(`
![](/images/first.png)

![Alt text](../images/second.png?size=large#preview)
`);

assert.deepEqual(data.markdownImageMap, {
__img0: "/docs/images/first.png",
__img1: "/docs/images/second.png?size=large#preview",
});
assert.deepEqual(data["mdx-export"], [
{
name: "markdownImageMap",
value: data.markdownImageMap,
},
]);
});

test("includes JSX image sources without shifting markdown image placeholders", async () => {
const data = await collectMarkdownImageMap(`
![](/images/first.png)

<Frame>
<img src="/images/frame.png" alt="Frame screenshot" />
</Frame>

![](/images/third.png)
`);

assert.deepEqual(data.markdownImageMap, {
__img0: "/docs/images/first.png",
__img1: "/docs/images/third.png",
__img2: "/docs/images/frame.png",
});
});
});
62 changes: 62 additions & 0 deletions plugins/remark-markdown-image-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { visit } from "unist-util-visit";
import { normalizeDocsAssetPath } from "../src/lib/docs-asset-path";

const imagePlaceholderPattern = /^__img\d+$/;

function getAttributeValue(node: any, name: string): string | undefined {
for (const attribute of node.attributes ?? []) {
if (attribute.name !== name) continue;

const value = attribute.value;
if (typeof value === "string") return value;
if (typeof value?.value === "string") return value.value;
}
}

function getImageUrl(node: any): string | undefined {
if (typeof node.url === "string") return normalizeDocsAssetPath(node.url);

const src = getAttributeValue(node, "src");
if (!src || imagePlaceholderPattern.test(src)) return undefined;

return normalizeDocsAssetPath(src);
}

export default function remarkMarkdownImageMap() {
return (tree: any, file: any) => {
const imageMap: Record<string, string> = {};
let imageIndex = 0;
const jsxImageUrls: string[] = [];

visit(tree, "image", (node: any) => {
const url = getImageUrl(node);
if (!url) return;

imageMap[`__img${imageIndex}`] = url;
imageIndex += 1;
});

visit(tree, ["mdxJsxFlowElement", "mdxJsxTextElement"], (node: any) => {
if (node.name !== "img") return;

const url = getImageUrl(node);
if (!url) return;

jsxImageUrls.push(url);
});
Comment thread
cursor[bot] marked this conversation as resolved.

for (const url of jsxImageUrls) {
imageMap[`__img${imageIndex}`] = url;
imageIndex += 1;
}

if (Object.keys(imageMap).length > 0) {
file.data.markdownImageMap = imageMap;
file.data["mdx-export"] ??= [];
file.data["mdx-export"].push({
name: "markdownImageMap",
value: imageMap,
});
}
};
}
49 changes: 48 additions & 1 deletion source.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,74 @@ import remarkFollowExport from "./plugins/remark-follow-export";
import remarkDirective from "remark-directive";
import { remarkInclude } from "fumadocs-mdx/config";
import remarkSdkFilter from "./plugins/remark-sdk-filter";
import remarkMarkdownImageMap from "./plugins/remark-markdown-image-map";
import type { LLMsOptions } from "fumadocs-core/mdx-plugins";

const isDevelopment = process.env.NODE_ENV === "development";
const markdownHeadingHandler: NonNullable<LLMsOptions["handlers"]>["heading"] = (
node,
_parent,
state,
info,
) => {
const depth = Math.min(Math.max(node.depth, 1), 6);
return `${"#".repeat(depth)} ${state.containerPhrasing(node, info)}`;
};

const processedMarkdownOptions: LLMsOptions = {
handlers: {
heading: markdownHeadingHandler,
},
mdxAsPlaceholder: [
"Accordion",
"AccordionGroup",
"Callout",
"Card",
"CardGroup",
"CodeGroup",
"Frame",
"Info",
"Mermaid",
"Note",
"Step",
"Steps",
"Tab",
"Tabs",
"Tip",
"Warning",
],
};

const markdownImageMapPassthroughPlugin = {
"index-file": {
serverOptions(options: any) {
options.doc ??= {};
options.doc.passthroughs ??= [];
options.doc.passthroughs.push("markdownImageMap");
},
},
};

export const docs = defineDocs({
dir: "content/docs",
docs: {
async: isDevelopment,
postprocess: {
includeProcessedMarkdown: true,
includeProcessedMarkdown: processedMarkdownOptions,
},
},
});

export default defineConfig({
plugins: [markdownImageMapPassthroughPlugin],
mdxOptions: {
// Shiki highlighting is one of the most expensive parts of the MDX pipeline.
// Keep it in builds, but skip it in local dev so first-page SSR doesn't have
// to highlight hundreds of code-heavy docs up front.
rehypeCodeOptions: isDevelopment ? false : undefined,
remarkPlugins: (existing) => [
remarkImagePaths,
remarkMarkdownImageMap,
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the image map after includes are expanded

For pages whose images come from <include>d shared MDX, this plugin runs before remarkInclude, so those included image nodes are not added to markdownImageMap even though they are present in the processed markdown later. For example, content/docs/ios/guides/advanced/observer-mode.mdx includes content/shared/observer-mode.mdx, which contains /images/om_transactions.png; that page's markdown alternate can be left with an unresolved __img... placeholder instead of the restored image URL. Move the map collection after include expansion or otherwise collect images from included content.

Useful? React with 👍 / 👎.

remarkLinkPaths,
remarkFollowExport,
...existing,
Expand Down
40 changes: 40 additions & 0 deletions src/lib/docs-asset-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const DOCS_PREFIX = "/docs";
const SPECIAL_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;

export function normalizeDocsAssetPath(url: string): string {
const trimmed = url.trim();
if (!trimmed) return trimmed;

if (trimmed.startsWith("#") || trimmed.startsWith("//") || SPECIAL_URL_PATTERN.test(trimmed)) {
return trimmed;
}

const [, pathPart = "", suffix = ""] = trimmed.match(/^([^?#]*)([?#].*)?$/) ?? [];
const segments = pathPart.replace(/^\/+/, "").split("/").filter(Boolean);

const normalizedSegments: string[] = [];
for (const segment of segments) {
if (segment === "." || segment === "") continue;
if (segment === "..") {
normalizedSegments.pop();
continue;
}
normalizedSegments.push(segment);
}

const normalizedPath = `/${normalizedSegments.join("/")}`;
if (normalizedPath === DOCS_PREFIX || normalizedPath.startsWith(`${DOCS_PREFIX}/`)) {
return `${normalizedPath}${suffix}`;
}

return `${DOCS_PREFIX}${normalizedPath === "/" ? "" : normalizedPath}${suffix}`;
}

export function resolveDocsAssetUrl(url: string, baseUrl?: string) {
const normalized = normalizeDocsAssetPath(url);
if (!baseUrl || normalized.startsWith("#") || SPECIAL_URL_PATTERN.test(normalized)) {
return normalized;
}

return new URL(normalized, baseUrl).toString();
}
Loading
Loading