mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(Docs): Add copy as markdown and AI tools to guide (#43355)
This commit is contained in:
1 parent
af3b8971c5
commit
8b4bf646fc
12 files changed
+372
-79
No files matched your search
@@ -30,6 +30,8 @@ public/sitemap.xml
|
||||
# llms.txt (generated)
|
||||
public/llms.txt
|
||||
public/llms/
|
||||
# Generated guide markdown files
|
||||
public/docs/
|
||||
|
||||
# Copied examples folder
|
||||
/examples/
|
||||
|
||||
@@ -22,6 +22,19 @@ For a complete run-down on how all of our tools work together, see the main DEVE
|
||||
4. Visit http://localhost:3001/docs in your browser - don't forget to append the `/docs` to the end
|
||||
5. Your local site should look exactly like [https://supabase.com/docs](https://supabase.com/docs)
|
||||
|
||||
## AI friendly documentation
|
||||
|
||||
This project generates Markdown files for each page under `/docs/guides/..` path.
|
||||
|
||||
To test locally, within the `apps/docs` directory:
|
||||
|
||||
1. Run `pnpm build:guides-markdown`
|
||||
2. Run `pnpm dev`
|
||||
|
||||
This creates Markdown files for all routes under the `public/docs/guides` directory, ignored by Git.
|
||||
|
||||
For production this setup runs as a `prebuild` task to allow Vercel to bundle these files with middleware and functions.
|
||||
|
||||
## Contributing
|
||||
|
||||
For repo organization and style guide, see the [contributing guide](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md).
|
||||
@@ -0,0 +1,22 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string[] }> }) {
|
||||
const { slug } = await params
|
||||
const baseDir = path.join(process.cwd(), 'public/docs/guides')
|
||||
const filePath = path.join(baseDir, `${slug.join('/')}.md`)
|
||||
|
||||
if (!filePath.startsWith(baseDir + path.sep) && filePath !== baseDir) {
|
||||
return new NextResponse('Not found', { status: 404 })
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8')
|
||||
return new NextResponse(content, {
|
||||
headers: { 'Content-Type': 'text/markdown; charset=utf-8' },
|
||||
})
|
||||
} catch {
|
||||
return new NextResponse('Not found', { status: 404 })
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { CodeBlock } from 'ui'
|
||||
import { Heading } from 'ui/src/components/CustomHTMLElements'
|
||||
import { type TOCHeader } from '~/components/GuidesTableOfContents'
|
||||
import { type TOCHeader } from '~/components/GuidesSidebar'
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import type { Parameter } from '~/lib/refGenerator/refTypes'
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
|
||||
import { Check, Copy, ExternalLink } from 'lucide-react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { isFeatureEnabled } from 'common'
|
||||
import { cn } from 'ui'
|
||||
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
|
||||
import { Toc, TOCItems, TOCScrollArea } from 'ui-patterns/Toc'
|
||||
import { Feedback } from '~/components/Feedback'
|
||||
import { useTocAnchors } from '../features/docs/GuidesMdx.state'
|
||||
|
||||
interface TOCHeader {
|
||||
id?: string
|
||||
text: string
|
||||
link: string
|
||||
level: number
|
||||
}
|
||||
|
||||
function AiTools({ className }: { className?: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
let url = ''
|
||||
|
||||
// Safe check for server side rendering.
|
||||
try {
|
||||
const urlParts = new URL(`${window.location}`)
|
||||
url = urlParts.origin + urlParts.pathname
|
||||
} catch (error) {}
|
||||
|
||||
async function copyMarkdown() {
|
||||
const mdUrl = `${url}.md`
|
||||
|
||||
try {
|
||||
const res = await fetch(mdUrl)
|
||||
const text = await res.text()
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch (error) {
|
||||
console.error('Failed to copy markdown', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={cn(className)} aria-labelledby="ask-ai-title">
|
||||
<h3
|
||||
id="ask-ai-title"
|
||||
className="block font-mono uppercase text-xs text-foreground-light mb-3"
|
||||
>
|
||||
AI Tools
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={copyMarkdown}
|
||||
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground text-left transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<Check size={14} strokeWidth={1.5} className="text-brand" />
|
||||
) : (
|
||||
<Copy size={14} strokeWidth={1.5} />
|
||||
)}
|
||||
{copied ? 'Copied!' : 'Copy as Markdown'}
|
||||
</button>
|
||||
<a
|
||||
href={`https://chatgpt.com/?hint=search&q=Read from ${url} so I can ask questions about its contents`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
|
||||
>
|
||||
<ExternalLink size={14} strokeWidth={1.5} />
|
||||
Ask ChatGPT
|
||||
</a>
|
||||
<a
|
||||
href={`https://claude.ai/new?q=Read from ${url} so I can ask questions about its contents`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
|
||||
>
|
||||
<ExternalLink size={14} strokeWidth={1.5} />
|
||||
Ask Claude
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const GuidesSidebar = ({
|
||||
className,
|
||||
video,
|
||||
hideToc,
|
||||
}: {
|
||||
className?: string
|
||||
video?: string
|
||||
hideToc?: boolean
|
||||
}) => {
|
||||
const pathname = usePathname()
|
||||
const { toc } = useTocAnchors()
|
||||
const showFeedback = isFeatureEnabled('feedback:docs')
|
||||
const tocVideoPreview = `https://img.youtube.com/vi/${video}/0.jpg`
|
||||
|
||||
return (
|
||||
<div className={cn('thin-scrollbar overflow-y-auto h-fit', 'px-px', className)}>
|
||||
<div className="w-full relative border-l flex flex-col gap-6 lg:gap-8 px-2 h-fit">
|
||||
{video && (
|
||||
<div className="relative pl-5">
|
||||
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
|
||||
</div>
|
||||
)}
|
||||
{showFeedback && (
|
||||
<div className="pl-5">
|
||||
<Feedback key={pathname} />
|
||||
</div>
|
||||
)}
|
||||
<div className="pl-5">
|
||||
<AiTools key={pathname} />
|
||||
</div>
|
||||
{!hideToc && toc.length !== 0 && (
|
||||
<Toc className="-ml-[calc(0.25rem+6px)]">
|
||||
<h3 className="inline-flex items-center gap-1.5 font-mono text-xs uppercase text-foreground pl-[calc(1.5rem+6px)]">
|
||||
On this page
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
<TOCItems items={toc} />
|
||||
</TOCScrollArea>
|
||||
</Toc>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GuidesSidebar
|
||||
export { GuidesSidebar }
|
||||
export type { TOCHeader }
|
||||
@@ -1,55 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { isFeatureEnabled } from 'common'
|
||||
import { cn } from 'ui'
|
||||
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
|
||||
import { Toc, TOCItems, TOCScrollArea } from 'ui-patterns/Toc'
|
||||
import { Feedback } from '~/components/Feedback'
|
||||
import { useTocAnchors } from '../features/docs/GuidesMdx.state'
|
||||
|
||||
interface TOCHeader {
|
||||
id?: string
|
||||
text: string
|
||||
link: string
|
||||
level: number
|
||||
}
|
||||
|
||||
const GuidesTableOfContents = ({ className, video }: { className?: string; video?: string }) => {
|
||||
const pathname = usePathname()
|
||||
const { toc } = useTocAnchors()
|
||||
|
||||
const showFeedback = isFeatureEnabled('feedback:docs')
|
||||
|
||||
const tocVideoPreview = `https://img.youtube.com/vi/${video}/0.jpg`
|
||||
|
||||
return (
|
||||
<div className={cn('thin-scrollbar overflow-y-auto h-fit', 'px-px', className)}>
|
||||
<div className="w-full relative border-l flex flex-col gap-6 lg:gap-8 px-2 h-fit">
|
||||
{video && (
|
||||
<div className="relative pl-5">
|
||||
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
|
||||
</div>
|
||||
)}
|
||||
{showFeedback && (
|
||||
<div className="pl-5">
|
||||
<Feedback key={pathname} />
|
||||
</div>
|
||||
)}
|
||||
{toc.length !== 0 && (
|
||||
<Toc className="-ml-[calc(0.25rem+6px)]">
|
||||
<h3 className="inline-flex items-center gap-1.5 font-mono text-xs uppercase text-foreground pl-[calc(1.5rem+6px)]">
|
||||
On this page
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
<TOCItems items={toc} />
|
||||
</TOCScrollArea>
|
||||
</Toc>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GuidesTableOfContents
|
||||
export type { TOCHeader }
|
||||
@@ -5,7 +5,7 @@ import ReactMarkdown from 'react-markdown'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import Breadcrumbs from '~/components/Breadcrumbs'
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import GuidesSidebar from '~/components/GuidesSidebar'
|
||||
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
|
||||
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
||||
import type { WithRequired } from '~/features/helpers.types'
|
||||
@@ -71,7 +71,7 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
|
||||
'relative',
|
||||
'transition-all ease-out',
|
||||
'duration-100',
|
||||
hideToc ? 'col-span-12' : 'col-span-12 md:col-span-9'
|
||||
'col-span-12 md:col-span-9'
|
||||
)}
|
||||
>
|
||||
<Breadcrumbs className="mb-2" />
|
||||
@@ -114,24 +114,23 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
{!hideToc && (
|
||||
<GuidesTableOfContents
|
||||
video={meta?.tocVideo}
|
||||
className={cn(
|
||||
'hidden md:flex',
|
||||
'col-span-3 self-start',
|
||||
'sticky',
|
||||
/**
|
||||
* --header-height: height of nav
|
||||
* 1px: height of nav border
|
||||
* 2rem: content padding
|
||||
*/
|
||||
'top-[calc(var(--header-height)+1px+2rem)]',
|
||||
// 3rem accounts for 2rem of top padding + 1rem of extra breathing room
|
||||
'max-h-[calc(100vh-var(--header-height)-3rem)]'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<GuidesSidebar
|
||||
video={meta?.tocVideo}
|
||||
hideToc={hideToc}
|
||||
className={cn(
|
||||
'hidden md:flex',
|
||||
'col-span-3 self-start',
|
||||
'sticky',
|
||||
/**
|
||||
* --header-height: height of nav
|
||||
* 1px: height of nav border
|
||||
* 2rem: content padding
|
||||
*/
|
||||
'top-[calc(var(--header-height)+1px+2rem)]',
|
||||
// 3rem accounts for 2rem of top padding + 1rem of extra breathing room
|
||||
'max-h-[calc(100vh-var(--header-height)-3rem)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TocAnchorsProvider>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { createContext, useContext, type ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
|
||||
import GuidesTableOfContents from '~/components/GuidesSidebar'
|
||||
import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
|
||||
import { type GuideFrontmatter } from '~/lib/docs'
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { globby } from 'globby'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
const PARTIALS_DIR = path.join(process.cwd(), 'content', '_partials')
|
||||
|
||||
/**
|
||||
* Reads <$Partial path="..." /> tags and replaces them with the file contents.
|
||||
* Recurses to handle nested partials.
|
||||
*/
|
||||
async function inlinePartials(content: string): Promise<string> {
|
||||
const partialRegex = /<\$Partial\s+path="([^"]+)"[^/]*\/>/g
|
||||
const matches = [...content.matchAll(partialRegex)]
|
||||
for (const [fullMatch, partialPath] of matches) {
|
||||
try {
|
||||
const raw = await fs.promises.readFile(path.join(PARTIALS_DIR, partialPath), 'utf8')
|
||||
const { content: partialBody } = matter(raw)
|
||||
const inlined = await inlinePartials(partialBody)
|
||||
content = content.replace(fullMatch, inlined)
|
||||
} catch {
|
||||
content = content.replace(fullMatch, '')
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
/** Remove the minimum common leading whitespace from all non-empty lines. */
|
||||
function dedentBlock(text: string): string {
|
||||
const lines = text.split('\n')
|
||||
const nonEmpty = lines.filter((l) => /\S/.test(l))
|
||||
if (!nonEmpty.length) return text
|
||||
const minIndent = Math.min(...nonEmpty.map((l) => (l.match(/^([ \t]*)/) ?? ['', ''])[1].length))
|
||||
if (!minIndent) return text
|
||||
return lines.map((l) => l.slice(minIndent)).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts StepHikeCompact components to markdown ordered lists.
|
||||
* Each step becomes a numbered item: the title (from Details) is bolded on the item
|
||||
* line, and the full step body (Details + Code) is dedented and appended below.
|
||||
* Remaining JSX tags inside the body are later stripped by stripJsxTags.
|
||||
*/
|
||||
function convertStepHike(content: string): string {
|
||||
return content.replace(/<StepHikeCompact>([\s\S]*?)<\/StepHikeCompact>/g, (_, body) => {
|
||||
const items: string[] = []
|
||||
const stepRe = /<StepHikeCompact\.Step[^>]*>([\s\S]*?)<\/StepHikeCompact\.Step>/g
|
||||
let stepNum = 1
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = stepRe.exec(body)) !== null) {
|
||||
const stepBody = m[1]
|
||||
const titleMatch = stepBody.match(/<StepHikeCompact\.Details[^>]+title="([^"]*)"/)
|
||||
const title = titleMatch ? titleMatch[1] : ''
|
||||
// Dedent the entire step body so nested JSX indentation is removed.
|
||||
// Remaining component tags (Details, Code, Admonition…) are stripped later.
|
||||
const inner = dedentBlock(stepBody).trim()
|
||||
const item = title ? `${stepNum}. **${title}**\n\n${inner}` : `${stepNum}. ${inner}`
|
||||
items.push(item)
|
||||
stepNum++
|
||||
}
|
||||
return items.join('\n\n')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips JSX component tags (capitalized names, dot-notation, or $-prefixed)
|
||||
* while keeping their inner content. Also strips wrapper div and a elements.
|
||||
* Removes MDX JSX comment blocks. Strips unnecessary leading indentation from
|
||||
* non-code-block lines.
|
||||
*/
|
||||
function stripJsxTags(content: string): string {
|
||||
// Remove MDX/JSX comments {/* ... */}
|
||||
content = content.replace(/\{\/\*[\s\S]*?\*\/\}/g, '')
|
||||
|
||||
// Remove self-closing JSX components: <Component ... /> or <$Directive ... />
|
||||
content = content.replace(/<[\$A-Z][\w.]*(?:\s[^>]*)?\s*\/>/gs, '')
|
||||
|
||||
// Remove opening JSX component tags (possibly multi-line): <Component ...>
|
||||
content = content.replace(/<[\$A-Z][\w.]*(?:\s[^>]*)?\s*>/gs, '')
|
||||
|
||||
// Remove closing JSX component tags: </Component>
|
||||
content = content.replace(/<\/[\$A-Z][\w.]*>/g, '')
|
||||
|
||||
// Remove wrapper div and a elements used structurally in MDX (carry JSX props
|
||||
// like className which are not valid HTML; inner content such as img is preserved)
|
||||
content = content.replace(/<div(?:\s[^>]*)?\s*>/g, '')
|
||||
content = content.replace(/<\/div>/g, '')
|
||||
content = content.replace(/<a(?:\s[^>]*)?\s*>/g, '')
|
||||
content = content.replace(/<\/a>/g, '')
|
||||
|
||||
// Split on fenced code blocks to handle prose and code separately.
|
||||
// For prose (even segments): strip leading whitespace (removes JSX nesting indent).
|
||||
// For code blocks (odd segments): dedent the body to remove JSX nesting indent while
|
||||
// preserving relative code structure, then normalize the closing fence.
|
||||
const segments = content.split(/(```[\s\S]*?```)/g)
|
||||
content = segments
|
||||
.map((seg, i) => {
|
||||
if (i % 2 === 0) return seg.replace(/^[ \t]+/gm, '')
|
||||
return seg.replace(
|
||||
/^(```[^\n]*\n)([\s\S]*?)(\n[ \t]*```)$/,
|
||||
(_, open, body) => open + dedentBlock(body) + '\n```'
|
||||
)
|
||||
})
|
||||
.join('')
|
||||
|
||||
// Collapse lines that are only whitespace to empty lines, then deduplicate blank lines
|
||||
content = content.replace(/^[^\S\n]+$/gm, '')
|
||||
content = content.replace(/\n{3,}/g, '\n\n').trim()
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
const files = await globby(['content/guides/**/!(_)*.mdx'])
|
||||
let warnings = 0
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (filePath) => {
|
||||
const outPath = filePath
|
||||
.replace(/^content\/guides\//, 'public/docs/guides/')
|
||||
.replace(/\.mdx$/, '.md')
|
||||
|
||||
let output: string
|
||||
try {
|
||||
const raw = await fs.promises.readFile(filePath, 'utf8')
|
||||
const { content: rawContent, data } = matter(raw)
|
||||
|
||||
const withPartials = await inlinePartials(rawContent)
|
||||
const withSteps = convertStepHike(withPartials)
|
||||
const processed = stripJsxTags(withSteps)
|
||||
|
||||
const header = [
|
||||
data.title ? `# ${data.title}` : '',
|
||||
data.subtitle || data.description ? `\n${data.subtitle ?? data.description}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
|
||||
output = header ? `${header}\n\n${processed}` : processed
|
||||
} catch (err) {
|
||||
warnings++
|
||||
console.warn(
|
||||
`[warn] Failed to process ${filePath}: ${err instanceof Error ? err.message : err}`
|
||||
)
|
||||
// Fall back to raw file content so the route still serves something
|
||||
try {
|
||||
output = await fs.promises.readFile(filePath, 'utf8')
|
||||
} catch {
|
||||
output = `<!-- failed to generate: ${filePath} -->`
|
||||
}
|
||||
}
|
||||
|
||||
// content/guides/ai/vector-columns.mdx → public/docs/guides/ai/vector-columns.md
|
||||
// Placing under public/docs/ ensures the file is served at /docs/guides/...
|
||||
// matching the exact URL of the rendered page.
|
||||
await fs.promises.mkdir(path.dirname(outPath), { recursive: true })
|
||||
await fs.promises.writeFile(outPath, output)
|
||||
})
|
||||
)
|
||||
|
||||
const summary = warnings ? ` (${warnings} with warnings)` : ''
|
||||
console.log(`Generated ${files.length} markdown files under public/docs/guides/${summary}`)
|
||||
}
|
||||
|
||||
generate()
|
||||
+12
-1
@@ -6,8 +6,19 @@ import { BASE_PATH } from '~/lib/constants'
|
||||
|
||||
const REFERENCE_PATH = `${BASE_PATH ?? ''}/reference`
|
||||
|
||||
const GUIDES_PATH = `${BASE_PATH ?? ''}/guides`
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const url = new URL(request.url)
|
||||
|
||||
// Serve pre-generated .md files before the [[...slug]] page route can intercept them
|
||||
if (url.pathname.startsWith(GUIDES_PATH + '/') && url.pathname.endsWith('.md')) {
|
||||
const slug = url.pathname.slice(GUIDES_PATH.length + 1, -'.md'.length)
|
||||
const rewriteUrl = new URL(url)
|
||||
rewriteUrl.pathname = `${BASE_PATH ?? ''}/api/guides-md/${slug}`
|
||||
return NextResponse.rewrite(rewriteUrl)
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith(REFERENCE_PATH)) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
@@ -56,5 +67,5 @@ export function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/reference/:path*',
|
||||
matcher: ['/reference/:path*', '/guides/:path*'],
|
||||
}
|
||||
@@ -60,6 +60,7 @@ const nextConfig = {
|
||||
],
|
||||
outputFileTracingIncludes: {
|
||||
'/api/crawlers': ['./features/docs/generated/**/*', './docs/ref/**/*'],
|
||||
'/api/guides-md/**/*': ['./public/docs/guides/**/*'],
|
||||
'/guides/**/*': ['./content/guides/**/*', './content/troubleshooting/**/*', './examples/**/*'],
|
||||
'/reference/**/*': ['./features/docs/generated/**/*', './docs/ref/**/*'],
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"build": "next build",
|
||||
"build:analyze": "ANALYZE=true next build",
|
||||
"build:llms": "tsx --conditions=react-server ./scripts/llms.ts",
|
||||
"build:guides-markdown": "tsx ./internals/generate-guides-markdown.ts",
|
||||
"build:sitemap": "tsx ./internals/generate-sitemap.ts",
|
||||
"clean": "rimraf .next .turbo node_modules features/docs/generated examples __generated__",
|
||||
"codegen:examples": "shx cp -r ../../examples ./examples",
|
||||
@@ -25,7 +26,7 @@
|
||||
"lint": "eslint .",
|
||||
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
|
||||
"postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh",
|
||||
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
|
||||
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples && pnpm run build:guides-markdown",
|
||||
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
|
||||
"preembeddings": "pnpm run codegen:references",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
Reference in new issue
Block a user