mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat(kb): Expose markdown alternatives for articles and topics pages (#50135)
This commit is contained in:
1 parent
c145f3e046
commit
fd863ee15d
8 files changed
+477
-25
No files matched your search
@@ -1,6 +1,9 @@
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated markdown export (scripts/generate-markdown.mjs)
|
||||
public/markdown/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
|
||||
+37
-9
@@ -10,6 +10,19 @@ pnpm dev:kb
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Astro documentation
|
||||
|
||||
Full documentation: https://docs.astro.build
|
||||
|
||||
Consult these guides before working on related tasks:
|
||||
|
||||
- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/)
|
||||
- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/)
|
||||
- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/)
|
||||
- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/)
|
||||
- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/)
|
||||
- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/)
|
||||
|
||||
## Build
|
||||
|
||||
To run a full production build, run:
|
||||
@@ -22,15 +35,30 @@ pnpm build:kb
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Documentation
|
||||
## Authoring content
|
||||
|
||||
Full documentation: https://docs.astro.build
|
||||
Content lives under `src/content/guides/*.{md,mdx}`. Keep it plain GitHub-Flavored Markdown:
|
||||
|
||||
Consult these guides before working on related tasks:
|
||||
- No custom/JSX components in guide bodies — content is also exported as plain `.md` (see below), and a
|
||||
component wouldn't survive that export.
|
||||
- For callouts, use GitHub's native alert syntax (`> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`,
|
||||
`> [!CAUTION]`) — the build renders these into the `Admonition` component for you
|
||||
(`src/lib/mdx/rehype-admonitions.ts`). Don't import or use `Admonition` directly in content.
|
||||
- Frontmatter requires `title`, `description`, and `topics` (values must match `TOPIC_NAMES` in
|
||||
`src/lib/topics.ts`); `pinned` and `github_url` are optional. See `src/content.config.ts` for the full schema.
|
||||
|
||||
- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/)
|
||||
- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/)
|
||||
- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/)
|
||||
- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/)
|
||||
- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/)
|
||||
- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/)
|
||||
## Pages and markdown export
|
||||
|
||||
Each content collection renders through a matching catch-all page — e.g. `src/content/guides/**` →
|
||||
`src/pages/guides/[...slug].astro` → `GuideLayout`. Topic pages (`src/pages/topics/[topic].astro`) are
|
||||
generated from the `TOPICS` list in `src/lib/topics.ts`, not from content files.
|
||||
|
||||
Separately, `scripts/generate-markdown.mjs` runs as a `prebuild` step and exports every content file — plus
|
||||
one page per topic, listing its guides — as a plain `.md` file under the gitignored `public/markdown/`,
|
||||
mirroring the page's URL with a `.md` extension. `vercel.json` permanently redirects `<page>.md` requests to
|
||||
these generated files, one redirect entry per route section (`guides`, `topics`).
|
||||
|
||||
If you add a new content collection or top-level route, add a matching redirect in `vercel.json`
|
||||
(`/kb/<section>/:path+.md` → `/kb/markdown/<section>/:path+.md`), and check whether `generate-markdown.mjs`
|
||||
needs updating too — the content export falls out of its generic `src/content/**` walk automatically, but
|
||||
per-topic-style listing pages don't.
|
||||
@@ -7,7 +7,9 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev --port 3008",
|
||||
"prebuild": "pnpm run generate:markdown",
|
||||
"build": "astro build",
|
||||
"generate:markdown": "node --experimental-strip-types ./scripts/generate-markdown.mjs",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"test": "vitest --run"
|
||||
@@ -20,8 +22,14 @@
|
||||
"lucide-react": "*",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"ui": "workspace:*",
|
||||
"ui-patterns": "workspace:*"
|
||||
"ui-patterns": "workspace:*",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"yaml": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.2.4",
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env node
|
||||
// Pre-build step: exports every src/content/**/*.{md,mdx} file as a plain
|
||||
// .md file under public/markdown/, for agents/tools that want the raw
|
||||
// content instead of the rendered page (same idea as apps/docs' guides
|
||||
// markdown export, recreated here at a much smaller scale — see
|
||||
// vercel.json for the redirect that serves it at `<page>.md`).
|
||||
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkStringify from 'remark-stringify'
|
||||
import { unified } from 'unified'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import YAML from 'yaml'
|
||||
|
||||
import { TOPICS, topicToSlug } from '../src/lib/topics.ts'
|
||||
|
||||
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const CONTENT_DIR = path.join(SCRIPT_DIR, '..', 'src', 'content')
|
||||
const OUTPUT_DIR = path.join(SCRIPT_DIR, '..', 'public', 'markdown')
|
||||
|
||||
// Production origin + Astro `base` — used to turn root-relative links into
|
||||
// full URLs so the exported file still makes sense read on its own.
|
||||
const SITE_ORIGIN = 'https://supabase.com'
|
||||
const BASE_PATH = '/kb'
|
||||
|
||||
// Single processor reused for every file: parses GFM markdown to an mdast
|
||||
// tree and serializes it back, same extensions on both ends so nothing
|
||||
// (tables, strikethrough, alert blockquotes) gets mangled in the round trip.
|
||||
// `emphasis`/`rule` match the markers content already uses (see
|
||||
// src/content/guides/sample-guide.mdx) so the round trip doesn't normalize
|
||||
// authors' `_italic_`/`---` into the serializer's own `*italic*`/`***`.
|
||||
const processor = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkStringify, { bullet: '-', listItemIndent: 'one', emphasis: '_', rule: '-' })
|
||||
|
||||
/**
|
||||
* Absolute base URL to prepend to root-relative links, mirroring apps/docs'
|
||||
* `getInternalLinkBaseUrl()`. Empty in local dev/CI (outside Vercel), which
|
||||
* keeps links relative there — resolved instead against whatever host is
|
||||
* serving the build.
|
||||
*
|
||||
* Resolution order:
|
||||
* - `VERCEL_ENV=production` → `https://supabase.com`
|
||||
* - `VERCEL_ENV=preview` → `https://${VERCEL_URL}`
|
||||
* - anything else → ''
|
||||
*/
|
||||
export function getInternalLinkBaseUrl() {
|
||||
const env = process.env.VERCEL_ENV
|
||||
if (env === 'production') return SITE_ORIGIN
|
||||
if (env === 'preview' && process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a `.md`/`.mdx` file's raw text into its YAML frontmatter (parsed
|
||||
* to a plain object) and the remaining body. The delimiters are located
|
||||
* with plain string ops, and the frontmatter itself is parsed with a real
|
||||
* YAML parser — no regex involved in extracting values.
|
||||
*
|
||||
* @param {string} raw
|
||||
*/
|
||||
export function parseFrontmatter(raw) {
|
||||
if (!raw.startsWith('---\n')) return { data: {}, body: raw }
|
||||
|
||||
const frontmatterEnd = raw.indexOf('\n---', 4)
|
||||
if (frontmatterEnd === -1) return { data: {}, body: raw }
|
||||
|
||||
const yamlText = raw.slice(4, frontmatterEnd)
|
||||
const bodyStart = raw.indexOf('\n', frontmatterEnd + 1)
|
||||
const body = bodyStart === -1 ? '' : raw.slice(bodyStart + 1)
|
||||
|
||||
return { data: YAML.parse(yamlText) ?? {}, body }
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites root-relative markdown links into full, absolute URLs by parsing
|
||||
* the body to an mdast tree, visiting its `link` nodes, and serializing it
|
||||
* back — the AST equivalent of apps/docs' `addBaseUrlPrefix()`, so this file
|
||||
* still makes sense read outside of the site (no regex over the raw text).
|
||||
*
|
||||
* @param {string} body
|
||||
*/
|
||||
export function absolutizeLinks(body) {
|
||||
const tree = processor.parse(body)
|
||||
const baseUrl = getInternalLinkBaseUrl()
|
||||
|
||||
visit(tree, 'link', (node) => {
|
||||
if (!node.url.startsWith('/') || node.url.startsWith('//')) return
|
||||
const withBase =
|
||||
node.url === BASE_PATH || node.url.startsWith(`${BASE_PATH}/`)
|
||||
? node.url
|
||||
: `${BASE_PATH}${node.url}`
|
||||
node.url = `${baseUrl}${withBase}`
|
||||
})
|
||||
|
||||
return String(processor.stringify(tree))
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the simplified export: the frontmatter title as an h1, the
|
||||
* description as the paragraph beneath it, then the (link-absolutized)
|
||||
* body.
|
||||
*
|
||||
* @param {{ title?: string; description?: string }} data
|
||||
* @param {string} body
|
||||
*/
|
||||
export function renderMarkdown(data, body) {
|
||||
const heading = data.title ? `# ${data.title}\n\n` : ''
|
||||
const lead = data.description ? `${data.description}\n\n` : ''
|
||||
// absolutizeLinks() already normalizes to exactly one trailing newline
|
||||
// (remark-stringify's doing, not ours) — no extra "\n" needed here.
|
||||
return `${heading}${lead}${absolutizeLinks(body.trim())}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a topic index: its name as an h1, its description as the
|
||||
* paragraph beneath it, then a bullet list linking to every guide tagged
|
||||
* with that topic — same data `src/pages/topics/[topic].astro` renders,
|
||||
* as a plain markdown page.
|
||||
*
|
||||
* @param {{ name: string; description: string }} topic
|
||||
* @param {{ title: string; url: string }[]} guides
|
||||
*/
|
||||
export function renderTopicMarkdown(topic, guides) {
|
||||
const list = guides.length
|
||||
? guides.map((guide) => `- [${guide.title}](${guide.url})`).join('\n')
|
||||
: 'No guides for this topic'
|
||||
return `# ${topic.name}\n\n${topic.description}\n\n${list}\n`
|
||||
}
|
||||
|
||||
// file.mdx -> file.md, file.md -> file.md — no regex, just a suffix swap.
|
||||
function toMdExtension(fileName) {
|
||||
if (fileName.endsWith('.mdx')) return `${fileName.slice(0, -'.mdx'.length)}.md`
|
||||
return fileName
|
||||
}
|
||||
|
||||
// Strips the .md/.mdx extension off a content-relative path and turns it
|
||||
// into the full, absolute URL of that page's markdown export.
|
||||
function toGuideUrl(relativePath) {
|
||||
const posixPath = relativePath.split(path.sep).join('/')
|
||||
const withoutExt = toMdExtension(posixPath).slice(0, -'.md'.length)
|
||||
return `${SITE_ORIGIN}${BASE_PATH}/${withoutExt}.md`
|
||||
}
|
||||
|
||||
async function findContentFiles(dir) {
|
||||
const entries = await readdir(dir, { withFileTypes: true })
|
||||
const files = await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) return findContentFiles(fullPath)
|
||||
if (entry.name.endsWith('.md') || entry.name.endsWith('.mdx')) return [fullPath]
|
||||
return []
|
||||
})
|
||||
)
|
||||
return files.flat()
|
||||
}
|
||||
|
||||
async function writeFileEnsuringDir(outputPath, contents) {
|
||||
await mkdir(path.dirname(outputPath), { recursive: true })
|
||||
await writeFile(outputPath, contents)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const files = await findContentFiles(CONTENT_DIR)
|
||||
|
||||
// Only entries with a `topics` array (currently just the `guides`
|
||||
// collection) feed the per-topic index pages below.
|
||||
const guidesByTopic = new Map(TOPICS.map((topic) => [topic.name, []]))
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const raw = await readFile(file, 'utf-8')
|
||||
const { data, body } = parseFrontmatter(raw)
|
||||
const relativePath = path.relative(CONTENT_DIR, file)
|
||||
|
||||
const markdown = renderMarkdown(data, body)
|
||||
const outputPath = path.join(OUTPUT_DIR, toMdExtension(relativePath))
|
||||
await writeFileEnsuringDir(outputPath, markdown)
|
||||
|
||||
if (!Array.isArray(data.topics)) return
|
||||
const guide = { title: data.title, url: toGuideUrl(relativePath) }
|
||||
for (const topicName of data.topics) {
|
||||
guidesByTopic.get(topicName)?.push(guide)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
TOPICS.map((topic) => {
|
||||
const markdown = renderTopicMarkdown(topic, guidesByTopic.get(topic.name))
|
||||
const outputPath = path.join(OUTPUT_DIR, 'topics', `${topicToSlug(topic.name)}.md`)
|
||||
return writeFileEnsuringDir(outputPath, markdown)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
main()
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
absolutizeLinks,
|
||||
getInternalLinkBaseUrl,
|
||||
parseFrontmatter,
|
||||
renderMarkdown,
|
||||
renderTopicMarkdown,
|
||||
} from './generate-markdown.mjs'
|
||||
|
||||
describe('parseFrontmatter', () => {
|
||||
it('parses title/description out of the YAML frontmatter and returns the rest as body', () => {
|
||||
const raw = `---\ntitle: 'Sample guide'\ndescription: 'A short summary.'\n---\n\nBody text here.\n`
|
||||
|
||||
const { data, body } = parseFrontmatter(raw)
|
||||
|
||||
expect(data.title).toBe('Sample guide')
|
||||
expect(data.description).toBe('A short summary.')
|
||||
// The blank line separating the closing `---` from the body is kept as-is
|
||||
// here — renderMarkdown() is what trims it before composing the output.
|
||||
expect(body).toBe('\nBody text here.\n')
|
||||
})
|
||||
|
||||
it('parses values that would trip up a naive regex (colons and quotes in the text)', () => {
|
||||
const raw = `---\ntitle: "Note: this has a colon"\ndescription: "Quoted 'inner' text: still one value"\n---\nBody\n`
|
||||
|
||||
const { data } = parseFrontmatter(raw)
|
||||
|
||||
expect(data.title).toBe('Note: this has a colon')
|
||||
expect(data.description).toBe("Quoted 'inner' text: still one value")
|
||||
})
|
||||
|
||||
it('returns an empty data object and the raw text untouched when there is no frontmatter', () => {
|
||||
const raw = 'Just a plain paragraph, no frontmatter.\n'
|
||||
|
||||
const { data, body } = parseFrontmatter(raw)
|
||||
|
||||
expect(data).toEqual({})
|
||||
expect(body).toBe(raw)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getInternalLinkBaseUrl', () => {
|
||||
const ORIGINAL_ENV = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...ORIGINAL_ENV }
|
||||
delete process.env.VERCEL_ENV
|
||||
delete process.env.VERCEL_URL
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = ORIGINAL_ENV
|
||||
})
|
||||
|
||||
it('returns the production origin when VERCEL_ENV=production', () => {
|
||||
process.env.VERCEL_ENV = 'production'
|
||||
expect(getInternalLinkBaseUrl()).toBe('https://supabase.com')
|
||||
})
|
||||
|
||||
it('returns the deployment URL when VERCEL_ENV=preview', () => {
|
||||
process.env.VERCEL_ENV = 'preview'
|
||||
process.env.VERCEL_URL = 'kb-git-fork-supabase.vercel.app'
|
||||
expect(getInternalLinkBaseUrl()).toBe('https://kb-git-fork-supabase.vercel.app')
|
||||
})
|
||||
|
||||
it('returns empty when preview is set but VERCEL_URL is missing', () => {
|
||||
process.env.VERCEL_ENV = 'preview'
|
||||
expect(getInternalLinkBaseUrl()).toBe('')
|
||||
})
|
||||
|
||||
it('returns empty when VERCEL_ENV is not set (local dev/CI)', () => {
|
||||
expect(getInternalLinkBaseUrl()).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('absolutizeLinks', () => {
|
||||
const ORIGINAL_ENV = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...ORIGINAL_ENV, VERCEL_ENV: 'production' }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = ORIGINAL_ENV
|
||||
})
|
||||
|
||||
it('leaves already-absolute links untouched', () => {
|
||||
expect(absolutizeLinks('See [the docs](https://example.com/guide) for more.')).toBe(
|
||||
'See [the docs](https://example.com/guide) for more.\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('rewrites a root-relative link into a full URL under the kb base path', () => {
|
||||
expect(absolutizeLinks('See [another guide](/guides/other-guide) for more.')).toBe(
|
||||
'See [another guide](https://supabase.com/kb/guides/other-guide) for more.\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not double up the base path when a link already includes it', () => {
|
||||
expect(absolutizeLinks('[another guide](/kb/guides/other-guide)')).toBe(
|
||||
'[another guide](https://supabase.com/kb/guides/other-guide)\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves the link relative (base path only, no origin) outside of Vercel', () => {
|
||||
process.env.VERCEL_ENV = undefined
|
||||
expect(absolutizeLinks('[another guide](/guides/other-guide)')).toBe(
|
||||
'[another guide](/kb/guides/other-guide)\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not rewrite image URLs', () => {
|
||||
expect(absolutizeLinks('')).toBe('\n')
|
||||
})
|
||||
|
||||
it('skips link-like text inside fenced code blocks', () => {
|
||||
expect(absolutizeLinks('```\n[x](/x)\n```\n\n[y](/y)')).toBe(
|
||||
'```\n[x](/x)\n```\n\n[y](https://supabase.com/kb/y)\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves a root-relative URL inside a fenced code block untouched, even outside link syntax', () => {
|
||||
const body = '```bash\ncurl -X GET /guides/foo\n```'
|
||||
expect(absolutizeLinks(body)).toBe('```bash\ncurl -X GET /guides/foo\n```\n')
|
||||
})
|
||||
|
||||
it('leaves a root-relative URL inside a fenced markdown code block untouched', () => {
|
||||
const body = '```md\n[Guides](/guides/foo)\n```'
|
||||
expect(absolutizeLinks(body)).toBe('```md\n[Guides](/guides/foo)\n```\n')
|
||||
})
|
||||
|
||||
it('leaves a root-relative URL inside an inline code span untouched', () => {
|
||||
expect(absolutizeLinks('Run `GET /guides/foo` to fetch it.')).toBe(
|
||||
'Run `GET /guides/foo` to fetch it.\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('round-trips GFM tables and strikethrough without mangling them', () => {
|
||||
const body = '| a | b |\n| - | - |\n| 1 | 2 |\n\n~~gone~~'
|
||||
expect(absolutizeLinks(body)).toBe('| a | b |\n| - | - |\n| 1 | 2 |\n\n~~gone~~\n')
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderMarkdown', () => {
|
||||
it('turns the title into an h1 and the description into the paragraph beneath it', () => {
|
||||
const data = { title: 'Sample guide', description: 'A short summary.' }
|
||||
|
||||
expect(renderMarkdown(data, 'Body text.')).toBe(
|
||||
'# Sample guide\n\nA short summary.\n\nBody text.\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('skips the heading/lead lines gracefully when title or description are missing', () => {
|
||||
expect(renderMarkdown({}, 'Body text.')).toBe('Body text.\n')
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderTopicMarkdown', () => {
|
||||
const topic = { name: 'Tutorial', description: 'Step-by-step walkthroughs.' }
|
||||
|
||||
it('renders the topic as an h1/description followed by a bullet list of its guides', () => {
|
||||
const guides = [
|
||||
{ title: 'Sample guide', url: 'https://supabase.com/kb/guides/sample-guide.md' },
|
||||
]
|
||||
|
||||
expect(renderTopicMarkdown(topic, guides)).toBe(
|
||||
'# Tutorial\n\nStep-by-step walkthroughs.\n\n- [Sample guide](https://supabase.com/kb/guides/sample-guide.md)\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to "No guides for this topic" when the list is empty', () => {
|
||||
expect(renderTopicMarkdown(topic, [])).toBe(
|
||||
'# Tutorial\n\nStep-by-step walkthroughs.\n\nNo guides for this topic\n'
|
||||
)
|
||||
})
|
||||
})
|
||||
+15
-11
@@ -8,33 +8,37 @@
|
||||
export const TOPICS = [
|
||||
{
|
||||
name: 'Migration',
|
||||
description: 'Moving data, schemas, or projects onto Supabase',
|
||||
description: 'Moving data, schemas, or projects onto Supabase.',
|
||||
pinned: false,
|
||||
},
|
||||
{
|
||||
name: 'Comparison',
|
||||
description: 'How Supabase compares to other databases and platforms',
|
||||
description: 'How Supabase compares to other databases and platforms.',
|
||||
pinned: false,
|
||||
},
|
||||
{ name: 'Troubleshooting', description: 'Common errors and how to resolve them', pinned: false },
|
||||
{ name: 'Troubleshooting', description: 'Common errors and how to resolve them.', pinned: false },
|
||||
{
|
||||
name: 'Tutorial',
|
||||
description: 'Step-by-step walkthroughs for building with Supabase',
|
||||
description: 'Step-by-step walkthroughs for building with Supabase.',
|
||||
pinned: true,
|
||||
},
|
||||
{ name: 'Storage', description: 'Uploading, managing, and serving files', pinned: false },
|
||||
{ name: 'Auth', description: 'Authentication, authorization, and user management', pinned: true },
|
||||
{ name: 'Database', description: 'Postgres schemas, queries, and performance', pinned: true },
|
||||
{ name: 'Storage', description: 'Uploading, managing, and serving files.', pinned: false },
|
||||
{
|
||||
name: 'Auth',
|
||||
description: 'Authentication, authorization, and user management.',
|
||||
pinned: true,
|
||||
},
|
||||
{ name: 'Database', description: 'Postgres schemas, queries, and performance.', pinned: true },
|
||||
{
|
||||
name: 'Edge Functions',
|
||||
description: 'Deploying and running serverless functions',
|
||||
description: 'Deploying and running serverless functions.',
|
||||
pinned: false,
|
||||
},
|
||||
{ name: 'Queues', description: 'Background jobs and message processing', pinned: false },
|
||||
{ name: 'Realtime', description: 'Broadcast, presence, and database changes', pinned: false },
|
||||
{ name: 'Queues', description: 'Background jobs and message processing.', pinned: false },
|
||||
{ name: 'Realtime', description: 'Broadcast, presence, and database changes.', pinned: false },
|
||||
{
|
||||
name: 'Supabase Platform',
|
||||
description: 'Project settings, billing, and infrastructure',
|
||||
description: 'Project settings, billing, and infrastructure.',
|
||||
pinned: false,
|
||||
},
|
||||
] as const
|
||||
|
||||
+13
-1
@@ -1,6 +1,18 @@
|
||||
{
|
||||
"buildCommand": "pnpm build",
|
||||
"redirects": [{ "source": "/", "destination": "/kb", "permanent": false }],
|
||||
"redirects": [
|
||||
{ "source": "/", "destination": "/kb", "permanent": false },
|
||||
{
|
||||
"source": "/kb/guides/:path+.md",
|
||||
"destination": "/kb/markdown/guides/:path+.md",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/kb/topics/:path+.md",
|
||||
"destination": "/kb/markdown/topics/:path+.md",
|
||||
"permanent": true
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{ "source": "/kb", "destination": "/index.html" },
|
||||
{ "source": "/kb/:path*", "destination": "/:path*" }
|
||||
|
||||
Generated
+21
-3
@@ -750,12 +750,30 @@ importers:
|
||||
react-dom:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.6(react@19.2.6)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(supports-color@8.1.1)
|
||||
remark-parse:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0(supports-color@8.1.1)
|
||||
remark-stringify:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
ui:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
ui-patterns:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui-patterns
|
||||
unified:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
unist-util-visit:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
yaml:
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0
|
||||
devDependencies:
|
||||
'@tailwindcss/vite':
|
||||
specifier: 4.2.4
|
||||
@@ -31261,7 +31279,7 @@ snapshots:
|
||||
'@types/mdast': 4.0.4
|
||||
escape-string-regexp: 5.0.0
|
||||
unist-util-is: 6.0.0
|
||||
unist-util-visit-parents: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
mdast-util-from-markdown@1.3.1(supports-color@8.1.1):
|
||||
dependencies:
|
||||
@@ -36267,7 +36285,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.0
|
||||
unist-util-visit-parents: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
unist-util-stringify-position@2.0.3:
|
||||
dependencies:
|
||||
@@ -36316,7 +36334,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
unist-util-is: 6.0.0
|
||||
unist-util-visit-parents: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
universal-github-app-jwt@2.2.0: {}
|
||||
|
||||
|
||||
Reference in new issue
Block a user