/g)].map(
+ (match) => `/reference/glossary#${match[1]}`,
+ )
+ return [...markdown, ...terms]
+}
diff --git a/apps/docs/scripts/generate-faq.ts b/apps/docs/scripts/generate-faq.ts
new file mode 100644
index 00000000..e4fe300b
--- /dev/null
+++ b/apps/docs/scripts/generate-faq.ts
@@ -0,0 +1,46 @@
+import { resolve } from "node:path"
+
+import { headingEntries, loadPages } from "./content"
+
+const faq = (await loadPages()).find((page) => page.route === "/resources/faq")
+if (!faq) throw new Error("FAQ source is missing")
+const landing = new Set(faq.frontmatter.landing ?? [])
+const entries = headingEntries(faq.body)
+ .filter((entry) => landing.has(entry.id))
+ .map((entry) => {
+ const link = entry.answer.match(/\[([^\]]+)\]\(([^)]+)\)\.$/)
+ if (!link)
+ throw new Error(`FAQ ${entry.id} must end with one documentation link`)
+ return {
+ id: entry.id,
+ question: entry.title,
+ answer: entry.answer.slice(0, link.index).trim(),
+ linkLabel: link[1],
+ href: link[2],
+ }
+ })
+
+if (entries.length !== landing.size)
+ throw new Error("landing frontmatter names an unknown FAQ entry")
+
+const output = `// Generated from apps/docs/content/resources/faq.mdx. Do not edit.\nexport const LANDING_FAQS = ${JSON.stringify(entries, null, 2)} as const\n`
+const target = resolve(
+ import.meta.dir,
+ "../../web/src/ui/landing/faq.generated.ts",
+)
+
+if (process.argv.includes("--check")) {
+ const current = await Bun.file(target)
+ .text()
+ .catch(() => "")
+ if (current !== output) {
+ console.error(
+ "Landing FAQ data is stale. Run: bun run --cwd apps/docs generate:faq",
+ )
+ process.exit(1)
+ }
+ console.log("Landing FAQ data matches its MDX source.")
+} else {
+ await Bun.write(target, output)
+ console.log(`Generated ${entries.length} landing FAQ entries.`)
+}
diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json
new file mode 100644
index 00000000..67b15dca
--- /dev/null
+++ b/apps/docs/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "types": ["bun"],
+ "noEmit": true
+ },
+ "include": ["scripts/**/*.ts"]
+}
diff --git a/apps/web/src/lib/glv-router-client.ts b/apps/web/src/lib/glv-router-client.ts
index f028e0f5..c30987db 100644
--- a/apps/web/src/lib/glv-router-client.ts
+++ b/apps/web/src/lib/glv-router-client.ts
@@ -27,7 +27,7 @@ function getGlvClient(): GlvRouterClient {
glvClient ??= new GlvRouterClient({
contractId: CONTRACTS.glvRouter,
networkPassphrase: NETWORK.networkPassphrase,
- rpcUrl: import.meta.env.VITE_RPC_URL,
+ rpcUrl: NETWORK.rpcUrl,
})
return glvClient
diff --git a/apps/web/src/ui/landing/faq-section.test.tsx b/apps/web/src/ui/landing/faq-section.test.tsx
new file mode 100644
index 00000000..536bc1fd
--- /dev/null
+++ b/apps/web/src/ui/landing/faq-section.test.tsx
@@ -0,0 +1,24 @@
+import { render, screen } from "@testing-library/react"
+import userEvent from "@testing-library/user-event"
+import { describe, expect, it } from "vitest"
+
+import { FaqSection } from "./faq-section"
+import { LANDING_FAQS } from "./faq.generated"
+
+describe("FaqSection", () => {
+ it("renders every generated entry and reveals its documentation link", async () => {
+ const user = userEvent.setup()
+ render()
+ for (const item of LANDING_FAQS) {
+ expect(
+ screen.getByRole("button", { name: item.question }),
+ ).toBeInTheDocument()
+ }
+ await user.click(
+ screen.getByRole("button", { name: LANDING_FAQS[0].question }),
+ )
+ expect(
+ screen.getByRole("link", { name: LANDING_FAQS[0].linkLabel }),
+ ).toHaveAttribute("href", `https://docs.so4.market${LANDING_FAQS[0].href}`)
+ })
+})
diff --git a/apps/web/src/ui/landing/faq-section.tsx b/apps/web/src/ui/landing/faq-section.tsx
index 3a57a359..0d756447 100644
--- a/apps/web/src/ui/landing/faq-section.tsx
+++ b/apps/web/src/ui/landing/faq-section.tsx
@@ -4,66 +4,10 @@ import {
AccordionItem,
AccordionTrigger,
} from "@workspace/ui/components/accordion"
-import type { ReactNode } from "react"
-// Same shape as GMX (first answer bulleted, second numbered). Every claim
-// here is checked against what the app actually does — see README.md and
-// the linked routes — rather than adapted from GMX's own answers, since SO4
-// is a different protocol with a different feature set.
-const FAQS: Array<{ id: string; question: string; answer: ReactNode }> = [
- {
- id: "yield",
- question: "What makes SO4 a good place to earn yield on my crypto?",
- answer: (
-
- - Provide liquidity to the GM pools and earn trading fees and funding, paid in real time.
- - Stake SO4 for GLV exposure — a diversified position across every pool, one token.
- - No lockups: withdraw whenever the pool has capacity, same block.
-
- ),
- },
- {
- id: "get-started",
- question: "How do I get started on SO4?",
- answer: (
-
- - Connect a Stellar wallet — no signup, no email.
- - Open Trade and pick BTC, ETH, or XLM to go long or short.
- - Or open Earn to deposit into a pool instead of trading directly.
-
- ),
- },
- {
- id: "cost-efficiency",
- question: "What makes SO4 cost-efficient compared to other perpetual platforms?",
- answer: (
-
- Every position is filled against a single unified pool instead of a fragmented order book,
- so fills don't depend on order book depth. Fees go to the liquidity that backs your
- trade, not to a separate market maker spread.
-
- ),
- },
- {
- id: "integrate",
- question: "Can I build on top of SO4 or integrate it into my DeFi app?",
- answer: (
-
- SO4 runs on public Soroban contracts — ExchangeRouter, DataStore, SyntheticsReader, and
- OrderVault. There's no published SDK or integration docs yet; check the{" "}
-
- source
- {" "}
- in the meantime.
-
- ),
- },
-]
+import { LANDING_FAQS } from "./faq.generated"
+
+const DOCS_ORIGIN = "https://docs.so4.market"
export function FaqSection() {
return (
@@ -76,20 +20,28 @@ export function FaqSection() {
the aria-controls/labelledby wiring and focus-visible ring. Only
the landing's typography and hairline rules are restyled here. */}
- {FAQS.map(({ id, question, answer }) => (
+ {LANDING_FAQS.map((item) => (
- {question}
+ {item.question}
- {answer}
+
+ {item.answer}{" "}
+
+ {item.linkLabel}
+
+
))}
diff --git a/apps/web/src/ui/landing/faq.generated.ts b/apps/web/src/ui/landing/faq.generated.ts
new file mode 100644
index 00000000..069abae8
--- /dev/null
+++ b/apps/web/src/ui/landing/faq.generated.ts
@@ -0,0 +1,38 @@
+// Generated from apps/docs/content/resources/faq.mdx. Do not edit.
+export const LANDING_FAQS = [
+ {
+ "id": "what-is-so4",
+ "question": "What is SO4?",
+ "answer": "SO4 is a perpetual-markets protocol on Stellar with a web interface for preparing and submitting transactions.",
+ "linkLabel": "Read the protocol risk overview",
+ "href": "/concepts/risk"
+ },
+ {
+ "id": "self-custody",
+ "question": "Do I keep custody of my assets?",
+ "answer": "The interface does not hold wallet keys; your wallet signs transactions and protocol contracts account for deposited collateral.",
+ "linkLabel": "Read the interface and protocol distinction",
+ "href": "/resources/terms#interface-and-protocol"
+ },
+ {
+ "id": "liquidation",
+ "question": "What can cause liquidation?",
+ "answer": "A position becomes liquidatable when its remaining collateral no longer meets the protocol's maintenance-margin requirement.",
+ "linkLabel": "Read the liquidation mechanics",
+ "href": "/concepts/liquidation"
+ },
+ {
+ "id": "fees",
+ "question": "What fees will I pay?",
+ "answer": "Costs can include opening, closing, funding, borrowing, network, and price-impact amounts, depending on the transaction.",
+ "linkLabel": "Read about funding and fees",
+ "href": "/concepts/funding-and-fees"
+ },
+ {
+ "id": "availability",
+ "question": "Is the interface always available?",
+ "answer": "No; deployments, incidents, RPC failures, congestion, and local connectivity can interrupt the website even while the protocol remains deployed.",
+ "linkLabel": "Read the risk overview",
+ "href": "/concepts/risk"
+ }
+] as const
diff --git a/bun.lock b/bun.lock
index fa9a914b..41c0aaf1 100644
--- a/bun.lock
+++ b/bun.lock
@@ -13,6 +13,15 @@
"typescript": "5.9.3",
},
},
+ "apps/docs": {
+ "name": "docs",
+ "version": "0.0.1",
+ "devDependencies": {
+ "@types/bun": "^1.3.0",
+ "prettier": "^3.8.1",
+ "typescript": "^5.9.3",
+ },
+ },
"apps/s03-indexer": {
"name": "s03-indexer",
"version": "0.0.1",
@@ -1161,6 +1170,8 @@
"@types/bn.js": ["@types/bn.js@5.2.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q=="],
+ "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
+
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
@@ -1547,6 +1558,8 @@
"bufferutil": ["bufferutil@4.1.0", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw=="],
+ "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
+
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -1749,6 +1762,8 @@
"dns-over-http-resolver": ["dns-over-http-resolver@1.2.3", "", { "dependencies": { "debug": "^4.3.1", "native-fetch": "^3.0.0", "receptacle": "^1.3.2" } }, "sha512-miDiVSI6KSNbi4SVifzO/reD8rMnxgrlnkrlkugOLQpWQTe2qMdHsZp5DmfKjxNE+/T3VAAYLQUZMv9SMr6+AA=="],
+ "docs": ["docs@workspace:apps/docs"],
+
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
"dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="],
diff --git a/docs/dx_1/001_docs_site.md b/docs/dx_1/001_docs_site.md
index 0aa78df8..7200ced5 100644
--- a/docs/dx_1/001_docs_site.md
+++ b/docs/dx_1/001_docs_site.md
@@ -14,6 +14,15 @@ issue index in [`dx_issues.md`](./dx_issues.md).
**Questions?** Reach out to the maintainer at [t.me/ibrahimijai](https://t.me/ibrahimijai).
+> **Implementation note (2026-08-24):** DX-097–DX-100 introduced a dependency-light
+> static documentation workspace while the wider DX1 foundation remains deferred.
+> It keeps the specified `apps/docs/content` boundary, typed frontmatter checks,
+> explicit navigation, link validation, printable HTML, and generated FAQ data.
+> TanStack Start, runtime MDX compilation, Pagefind, syntax highlighting, Mermaid,
+> generated reference pages, deployment configuration, and the complete docs chrome
+> remain deferred because their prerequisite issues have not landed. This note records
+> the built state rather than presenting the target architecture below as complete.
+
---
## 1. Why a separate app
diff --git a/docs/dx_1/002_changelog.md b/docs/dx_1/002_changelog.md
index a976841c..9eb3877a 100644
--- a/docs/dx_1/002_changelog.md
+++ b/docs/dx_1/002_changelog.md
@@ -12,6 +12,12 @@ release, and how they are published in the app. Companion to
**Questions?** Reach out to the maintainer at [t.me/ibrahimijai](https://t.me/ibrahimijai).
+> **Implementation note (2026-08-24):** The DX1 resource batch adds an unreleased
+> `docs` entry and `/resources/changelog`, which points to the canonical product
+> history. The parser, release command, feeds, and `/changelog` application route
+> described below remain deferred to DX-002 and its dependent issues; they were not
+> recreated as part of documentation close-out work.
+
---
## 1. Why the changelog lives in `apps/web`
diff --git a/docs/dx_1/003_content_map.md b/docs/dx_1/003_content_map.md
index 33c49f16..742d5750 100644
--- a/docs/dx_1/003_content_map.md
+++ b/docs/dx_1/003_content_map.md
@@ -6,6 +6,17 @@ content issue in [`dx_issues.md`](./dx_issues.md). Companion to
**Questions?** Reach out to the maintainer at [t.me/ibrahimijai](https://t.me/ibrahimijai).
+> **Implementation status (2026-08-24):** `/concepts/risk`,
+> `/concepts/funding-and-fees`, `/concepts/liquidation`, `/reference/glossary`,
+> `/resources/terms`, `/resources/faq`, `/resources/roadmap`, and
+> `/resources/changelog` are implemented and reachable from the sidebar. All other
+> pages in §2 are explicitly deferred because their prerequisite DX1 content or
+> generated-reference issues have not landed. The three intended reader journeys
+> therefore cannot yet be walked end to end: trader onboarding stops before
+> quickstart/trading, liquidity-provider onboarding stops before pools, and the
+> integrator journey stops before local setup and contract clients. This is the
+> audited built state, not a claim that deferred paths exist.
+
---
## 1. Who we are writing for
diff --git a/e2e/landing-a11y-check.spec.ts b/e2e/landing-a11y-check.spec.ts
index 34c20664..4c37cf05 100644
--- a/e2e/landing-a11y-check.spec.ts
+++ b/e2e/landing-a11y-check.spec.ts
@@ -5,7 +5,7 @@ test.use({ viewport: { width: 390, height: 844 } })
test("faq accordion opens via keyboard and is wired to its panel", async ({ page }) => {
await page.goto("/")
await page.waitForLoadState("networkidle")
- const trigger = page.getByRole("button", { name: /good place to earn yield/i })
+ const trigger = page.getByRole("button", { name: /what is so4/i })
await expect(trigger).toHaveAttribute("aria-expanded", "false")
const controls = await trigger.getAttribute("aria-controls")
expect(controls).toBeTruthy()
diff --git a/package.json b/package.json
index 1dfd0621..52bbbce0 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,9 @@
"indexer:dev": "bun run --cwd apps/s03-indexer dev",
"indexer:start": "bun run --cwd apps/s03-indexer start",
"check:integration": "bash scripts/check-integration.sh",
- "check:tokens": "bun run scripts/check-design-tokens.ts"
+ "check:tokens": "bun run scripts/check-design-tokens.ts",
+ "check:content": "bun run --cwd apps/docs check:content",
+ "check:links": "bun run --cwd apps/docs check:links"
},
"devDependencies": {
"@playwright/test": "^1.61.1",