Skip to content

Commit 35ee720

Browse files
committed
Fix docs markdown routes and image URLs
1 parent e202a5f commit 35ee720

15 files changed

Lines changed: 412 additions & 133 deletions

content/shared/vibe-coding.mdx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ We've built a few tools to help you Vibe Code using the knowledge of the Superwa
1212
- [Superwall MCP](#superwall-mcp): Expose your Superwall account (projects, paywalls, campaigns) to work with AI tools.
1313

1414
And right here in the Superwall Docs:
15+
1516
- [Superwall AI](#superwall-ai)
1617
- [Docs Links](#docs-links)
1718
- [LLMs.txt](#llmstxt)
1819

19-
20-
2120
## Superwall Agents
2221

2322
[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.
@@ -44,12 +43,10 @@ If you also want live docs access and guided SDK integration help, use the [Supe
4443

4544
See the full [Superwall MCP guide](/dashboard/guides/superwall-mcp) for installation, a step-by-step quick setup, and the complete tool reference.
4645

47-
4846
## Superwall AI
4947

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

52-
5350
## Docs Links
5451

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

61+
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`.
6462

6563
## LLMs.txt
64+
6665
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.
6766

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

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

72-
| SDK | Summary | Full Text |
73-
| -------- | ------- | --------- |
74-
| All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) |
75-
| Dashboard | [`llms-dashboard.txt`](https://superwall.com/docs/llms-dashboard.txt) | [`llms-full-dashboard.txt`](https://superwall.com/docs/llms-full-dashboard.txt) |
76-
| iOS | [`llms-ios.txt`](https://superwall.com/docs/llms-ios.txt) | [`llms-full-ios.txt`](https://superwall.com/docs/llms-full-ios.txt) |
77-
| Android | [`llms-android.txt`](https://superwall.com/docs/llms-android.txt) | [`llms-full-android.txt`](https://superwall.com/docs/llms-full-android.txt) |
78-
| Flutter | [`llms-flutter.txt`](https://superwall.com/docs/llms-flutter.txt) | [`llms-full-flutter.txt`](https://superwall.com/docs/llms-full-flutter.txt) |
79-
| Expo | [`llms-expo.txt`](https://superwall.com/docs/llms-expo.txt) | [`llms-full-expo.txt`](https://superwall.com/docs/llms-full-expo.txt) |
71+
| SDK | Summary | Full Text |
72+
| ------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
73+
| All | [`llms.txt`](https://superwall.com/docs/llms.txt) | [`llms-full.txt`](https://superwall.com/docs/llms-full.txt) |
74+
| Dashboard | [`llms-dashboard.txt`](https://superwall.com/docs/llms-dashboard.txt) | [`llms-full-dashboard.txt`](https://superwall.com/docs/llms-full-dashboard.txt) |
75+
| iOS | [`llms-ios.txt`](https://superwall.com/docs/llms-ios.txt) | [`llms-full-ios.txt`](https://superwall.com/docs/llms-full-ios.txt) |
76+
| Android | [`llms-android.txt`](https://superwall.com/docs/llms-android.txt) | [`llms-full-android.txt`](https://superwall.com/docs/llms-full-android.txt) |
77+
| Flutter | [`llms-flutter.txt`](https://superwall.com/docs/llms-flutter.txt) | [`llms-full-flutter.txt`](https://superwall.com/docs/llms-full-flutter.txt) |
78+
| Expo | [`llms-expo.txt`](https://superwall.com/docs/llms-expo.txt) | [`llms-full-expo.txt`](https://superwall.com/docs/llms-full-expo.txt) |
8079
| 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) |
81-
| Integrations | [`llms-integrations.txt`](https://superwall.com/docs/llms-integrations.txt) | [`llms-full-integrations.txt`](https://superwall.com/docs/llms-full-integrations.txt) |
82-
| 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) |
80+
| Integrations | [`llms-integrations.txt`](https://superwall.com/docs/llms-integrations.txt) | [`llms-full-integrations.txt`](https://superwall.com/docs/llms-full-integrations.txt) |
81+
| 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) |
8382

8483
To minimize token use, we recommend using the files specific to your SDK.

plugins/remark-image-paths.ts

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { visit } from "unist-util-visit";
2+
import { normalizeDocsAssetPath } from "../src/lib/docs-asset-path";
3+
4+
export { normalizeDocsAssetPath };
25

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

13-
const DOCS_PREFIX = "/docs";
14-
const SPECIAL_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
15-
16-
export function normalizeDocsAssetPath(url: string): string {
17-
const trimmed = url.trim();
18-
if (!trimmed) return trimmed;
19-
20-
// Keep external/special schemes and fragment-only links untouched.
21-
if (trimmed.startsWith("#") || trimmed.startsWith("//") || SPECIAL_URL_PATTERN.test(trimmed)) {
22-
return trimmed;
23-
}
24-
25-
const [, pathPart = "", suffix = ""] = trimmed.match(/^([^?#]*)([?#].*)?$/) ?? [];
26-
const segments = pathPart
27-
.replace(/^\/+/, "")
28-
.split("/")
29-
.filter(Boolean);
30-
31-
const normalizedSegments: string[] = [];
32-
for (const segment of segments) {
33-
if (segment === "." || segment === "") continue;
34-
if (segment === "..") {
35-
normalizedSegments.pop();
36-
continue;
37-
}
38-
normalizedSegments.push(segment);
39-
}
40-
41-
const normalizedPath = `/${normalizedSegments.join("/")}`;
42-
if (normalizedPath === DOCS_PREFIX || normalizedPath.startsWith(`${DOCS_PREFIX}/`)) {
43-
return `${normalizedPath}${suffix}`;
44-
}
45-
46-
return `${DOCS_PREFIX}${normalizedPath === "/" ? "" : normalizedPath}${suffix}`;
47-
}
48-
4916
export default function remarkImagePaths() {
5017
return (tree: any) => {
5118
visit(tree, "image", (node) => {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import assert from "node:assert/strict";
2+
import { describe, test } from "node:test";
3+
import { unified } from "unified";
4+
import remarkMdx from "remark-mdx";
5+
import remarkParse from "remark-parse";
6+
import remarkMarkdownImageMap from "./remark-markdown-image-map";
7+
8+
async function collectMarkdownImageMap(markdown: string) {
9+
const processor = unified().use(remarkParse).use(remarkMdx).use(remarkMarkdownImageMap);
10+
const file = { data: {} };
11+
const tree = processor.parse(markdown);
12+
13+
await processor.run(tree, file as any);
14+
return file.data as {
15+
markdownImageMap?: Record<string, string>;
16+
"mdx-export"?: Array<{ name: string; value: unknown }>;
17+
};
18+
}
19+
20+
describe("remarkMarkdownImageMap", () => {
21+
test("exports normalized image URLs keyed by Fumadocs placeholders", async () => {
22+
const data = await collectMarkdownImageMap(`
23+
![](/images/first.png)
24+
25+
![Alt text](../images/second.png?size=large#preview)
26+
`);
27+
28+
assert.deepEqual(data.markdownImageMap, {
29+
__img0: "/docs/images/first.png",
30+
__img1: "/docs/images/second.png?size=large#preview",
31+
});
32+
assert.deepEqual(data["mdx-export"], [
33+
{
34+
name: "markdownImageMap",
35+
value: data.markdownImageMap,
36+
},
37+
]);
38+
});
39+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { visit } from "unist-util-visit";
2+
import { normalizeDocsAssetPath } from "../src/lib/docs-asset-path";
3+
4+
const imagePlaceholderPattern = /^__img\d+$/;
5+
6+
function getAttributeValue(node: any, name: string): string | undefined {
7+
for (const attribute of node.attributes ?? []) {
8+
if (attribute.name !== name) continue;
9+
10+
const value = attribute.value;
11+
if (typeof value === "string") return value;
12+
if (typeof value?.value === "string") return value.value;
13+
}
14+
}
15+
16+
function getImagePlaceholder(node: any): string | undefined {
17+
const src = getAttributeValue(node, "src");
18+
return src && imagePlaceholderPattern.test(src) ? src : undefined;
19+
}
20+
21+
function getImageUrl(node: any): string | undefined {
22+
if (typeof node.url === "string") return normalizeDocsAssetPath(node.url);
23+
24+
const src = getAttributeValue(node, "src");
25+
if (!src || imagePlaceholderPattern.test(src)) return undefined;
26+
27+
return normalizeDocsAssetPath(src);
28+
}
29+
30+
export default function remarkMarkdownImageMap() {
31+
return (tree: any, file: any) => {
32+
const imageMap: Record<string, string> = {};
33+
let imageIndex = 0;
34+
35+
visit(tree, "image", (node: any) => {
36+
if (typeof node.url !== "string") return;
37+
38+
imageMap[`__img${imageIndex}`] = normalizeDocsAssetPath(node.url);
39+
imageIndex += 1;
40+
});
41+
42+
visit(tree, ["mdxJsxFlowElement", "mdxJsxTextElement"], (node: any) => {
43+
if (node.name !== "img") return;
44+
45+
const placeholder = getImagePlaceholder(node);
46+
const url = getImageUrl(node);
47+
if (placeholder && url) imageMap[placeholder] = url;
48+
});
49+
50+
if (Object.keys(imageMap).length > 0) {
51+
file.data.markdownImageMap = imageMap;
52+
file.data["mdx-export"] ??= [];
53+
file.data["mdx-export"].push({
54+
name: "markdownImageMap",
55+
value: imageMap,
56+
});
57+
}
58+
};
59+
}

source.config.ts

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,74 @@ import remarkFollowExport from "./plugins/remark-follow-export";
88
import remarkDirective from "remark-directive";
99
import { remarkInclude } from "fumadocs-mdx/config";
1010
import remarkSdkFilter from "./plugins/remark-sdk-filter";
11+
import remarkMarkdownImageMap from "./plugins/remark-markdown-image-map";
12+
import type { LLMsOptions } from "fumadocs-core/mdx-plugins";
1113

1214
const isDevelopment = process.env.NODE_ENV === "development";
15+
const markdownHeadingHandler: NonNullable<LLMsOptions["handlers"]>["heading"] = (
16+
node,
17+
_parent,
18+
state,
19+
info,
20+
) => {
21+
const depth = Math.min(Math.max(node.depth, 1), 6);
22+
return `${"#".repeat(depth)} ${state.containerPhrasing(node, info)}`;
23+
};
24+
25+
const processedMarkdownOptions: LLMsOptions = {
26+
handlers: {
27+
heading: markdownHeadingHandler,
28+
},
29+
mdxAsPlaceholder: [
30+
"Accordion",
31+
"AccordionGroup",
32+
"Callout",
33+
"Card",
34+
"CardGroup",
35+
"CodeGroup",
36+
"Frame",
37+
"Info",
38+
"Mermaid",
39+
"Note",
40+
"Step",
41+
"Steps",
42+
"Tab",
43+
"Tabs",
44+
"Tip",
45+
"Warning",
46+
],
47+
};
48+
49+
const markdownImageMapPassthroughPlugin = {
50+
"index-file": {
51+
serverOptions(options: any) {
52+
options.doc ??= {};
53+
options.doc.passthroughs ??= [];
54+
options.doc.passthroughs.push("markdownImageMap");
55+
},
56+
},
57+
};
1358

1459
export const docs = defineDocs({
1560
dir: "content/docs",
1661
docs: {
1762
async: isDevelopment,
1863
postprocess: {
19-
includeProcessedMarkdown: {
20-
mdxAsPlaceholder: [
21-
"Accordion",
22-
"AccordionGroup",
23-
"Callout",
24-
"Card",
25-
"CardGroup",
26-
"CodeGroup",
27-
"Frame",
28-
"Info",
29-
"Mermaid",
30-
"Note",
31-
"Step",
32-
"Steps",
33-
"Tab",
34-
"Tabs",
35-
"Tip",
36-
"Warning",
37-
],
38-
},
64+
includeProcessedMarkdown: processedMarkdownOptions,
3965
},
4066
},
4167
});
4268

4369
export default defineConfig({
70+
plugins: [markdownImageMapPassthroughPlugin],
4471
mdxOptions: {
4572
// Shiki highlighting is one of the most expensive parts of the MDX pipeline.
4673
// Keep it in builds, but skip it in local dev so first-page SSR doesn't have
4774
// to highlight hundreds of code-heavy docs up front.
4875
rehypeCodeOptions: isDevelopment ? false : undefined,
4976
remarkPlugins: (existing) => [
5077
remarkImagePaths,
78+
remarkMarkdownImageMap,
5179
remarkLinkPaths,
5280
remarkFollowExport,
5381
...existing,

src/lib/docs-asset-path.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const DOCS_PREFIX = "/docs";
2+
const SPECIAL_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
3+
4+
export function normalizeDocsAssetPath(url: string): string {
5+
const trimmed = url.trim();
6+
if (!trimmed) return trimmed;
7+
8+
if (trimmed.startsWith("#") || trimmed.startsWith("//") || SPECIAL_URL_PATTERN.test(trimmed)) {
9+
return trimmed;
10+
}
11+
12+
const [, pathPart = "", suffix = ""] = trimmed.match(/^([^?#]*)([?#].*)?$/) ?? [];
13+
const segments = pathPart.replace(/^\/+/, "").split("/").filter(Boolean);
14+
15+
const normalizedSegments: string[] = [];
16+
for (const segment of segments) {
17+
if (segment === "." || segment === "") continue;
18+
if (segment === "..") {
19+
normalizedSegments.pop();
20+
continue;
21+
}
22+
normalizedSegments.push(segment);
23+
}
24+
25+
const normalizedPath = `/${normalizedSegments.join("/")}`;
26+
if (normalizedPath === DOCS_PREFIX || normalizedPath.startsWith(`${DOCS_PREFIX}/`)) {
27+
return `${normalizedPath}${suffix}`;
28+
}
29+
30+
return `${DOCS_PREFIX}${normalizedPath === "/" ? "" : normalizedPath}${suffix}`;
31+
}
32+
33+
export function resolveDocsAssetUrl(url: string, baseUrl?: string) {
34+
const normalized = normalizeDocsAssetPath(url);
35+
if (!baseUrl || normalized.startsWith("#") || SPECIAL_URL_PATTERN.test(normalized)) {
36+
return normalized;
37+
}
38+
39+
return new URL(normalized, baseUrl).toString();
40+
}

src/lib/markdown-route.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { buildHtmlPathFromMarkdownRoute } from "./markdown-alternate";
2+
import { buildLLMSummaryText } from "./llms";
3+
import { getPageMarkdownText, source } from "./source";
4+
5+
export function slugsFromMarkdownSplat(splat?: string) {
6+
return (splat ?? "").replace(/\/+$/, "").split("/").filter(Boolean);
7+
}
8+
9+
export async function getMarkdownForPageSlugs(slugs: string[], request: Request) {
10+
const page = source.getPage(slugs);
11+
if (!page) return undefined;
12+
13+
return getPageMarkdownText(page, { baseUrl: request.url });
14+
}
15+
16+
export async function getMarkdownRouteBody(slugs: string[], request: Request) {
17+
if (slugs.length === 0) return buildLLMSummaryText();
18+
return getMarkdownForPageSlugs(slugs, request);
19+
}
20+
21+
export function buildMarkdownRouteResponse(body: BodyInit | null, slugs: string[]) {
22+
return new Response(body, {
23+
headers: {
24+
"Content-Type": "text/markdown; charset=utf-8",
25+
"Access-Control-Allow-Origin": "*",
26+
Vary: "Accept",
27+
Link: `<${buildHtmlPathFromMarkdownRoute(slugs)}>; rel="canonical"; type="text/html"`,
28+
},
29+
});
30+
}

0 commit comments

Comments
 (0)