Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/docs-lint-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ jobs:
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: 9fd8c8fa7487a3d454a676e6e3d7409af34fc715
key: e23c860111349d8bac17b78b1fe632afe84223f9
- name: install linter
if: steps.cache-cargo.outputs.cache-hit != 'true'
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev 9fd8c8fa7487a3d454a676e6e3d7409af34fc715
run: cargo install --locked --git https://github.com/supabase-community/supa-mdx-lint --rev e23c860111349d8bac17b78b1fe632afe84223f9
- name: install reviewdog
uses: reviewdog/action-setup@3f401fe1d58fe77e10d665ab713057375e39b887 # v1.3.0
with:
Expand All @@ -41,4 +41,4 @@ jobs:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o pipefail
supa-mdx-lint apps/docs/content/guides/getting-started --format rdf | reviewdog -f=rdjsonl -reporter=github-pr-review
supa-mdx-lint apps/docs/content/guides/getting-started apps/docs/content/guides/ai --format rdf | reviewdog -f=rdjsonl -reporter=github-pr-review
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { Code, Heading, Root } from 'mdast'
import { fromMarkdown } from 'mdast-util-from-markdown'
import { toMarkdown } from 'mdast-util-to-markdown'
import { readdir, readFile, stat } from 'node:fs/promises'
import { basename, extname, join } from 'node:path'
import { cache } from 'react'
import { visit, EXIT } from 'unist-util-visit'

import { EXAMPLES_DIRECTORY } from '~/lib/docs'

const PROMPTS_DIRECTORY = join(EXAMPLES_DIRECTORY, 'prompts')

function parseMarkdown(markdown: string) {
const mdast = fromMarkdown(markdown)

let heading = ''
visit(mdast, 'heading', (node: Heading) => {
if (node.depth === 1) {
if ('value' in node.children[0]) {
heading = node.children[0].value
}
return EXIT
}
})

const codeBlock: Code = {
type: 'code',
lang: 'markdown',
value: markdown,
}
const root: Root = {
type: 'root',
children: [codeBlock],
}
const content = toMarkdown(root)

return { heading, content }
}

async function getAiPromptsImpl() {
const directoryContents = await readdir(PROMPTS_DIRECTORY)

const prompts = directoryContents
.filter(async (file) => {
if (extname(file) !== '.md') {
return false
}

const fileStats = await stat(join(PROMPTS_DIRECTORY, file))
const isFile = fileStats.isFile()
return isFile
})
.map(async (filename) => {
const rawContent = await readFile(join(PROMPTS_DIRECTORY, filename), 'utf-8')
const { heading, content } = parseMarkdown(rawContent)

return {
filename: basename(filename, '.md'),
heading,
content,
}
})

return (await Promise.all(prompts)).sort((a, b) => b.filename.localeCompare(a.filename))
}
export const getAiPrompts = cache(getAiPromptsImpl)

async function getAiPromptImpl(prompt: string) {
const filePath = join(PROMPTS_DIRECTORY, `${prompt}.md`)
try {
const rawContent = await readFile(filePath, 'utf-8')
const { heading, content } = parseMarkdown(rawContent)
return { heading, content }
} catch (err) {
console.error('Failed to fetch prompt from repo: %o', err)
}
}
export const getAiPrompt = cache(getAiPromptImpl)

export async function generateAiPromptMetadata({ params: { slug } }: { params: { slug: string } }) {
const prompt = await getAiPrompt(slug)

return {
title: `AI Prompt: ${prompt.heading} | Supabase Docs`,
}
}

export async function generateAiPromptsStaticParams() {
const prompts = await getAiPrompts()

return prompts.map((prompt) => {
return {
slug: prompt.filename,
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from 'next/link'

import { GlassPanel } from 'ui-patterns'

import { getAiPrompts } from './AiPrompts.utils'

export async function AiPromptsIndex() {
const prompts = await getAiPrompts()

return (
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-6 not-prose">
{prompts.map((prompt) => (
<Link
key={prompt.filename}
href={`/guides/getting-started/ai-prompts/${prompt.filename}`}
passHref
>
<GlassPanel
key={prompt.filename}
title={prompt.heading ?? prompt.filename.replaceAll('-', ' ')}
/>
</Link>
))}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import {
generateAiPromptMetadata,
generateAiPromptsStaticParams,
getAiPrompt,
} from './AiPrompts.utils'

export const dynamicParams = false

export default async function AiPromptsPage({ params: { slug } }: { params: { slug: string } }) {
let { heading, content } = await getAiPrompt(slug)
content = `
## How to use

Copy the prompt to a file in your repo.

Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use \`#<filename>\`, in Cursor, use \`@Files\`, and in Zed, use \`/file\`.

## Prompt

${content}
`.trim()

return (
<GuideTemplate
meta={{ title: `API Prompt: ${heading}` }}
content={content}
editLink={newEditLink(`supabase/supabase/blob/master/examples/prompts/${slug}.md`)}
/>
)
}

export const generateMetadata = generateAiPromptMetadata
export const generateStaticParams = generateAiPromptsStaticParams
40 changes: 32 additions & 8 deletions apps/docs/app/guides/(with-sidebar)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,41 @@ import { cache, type PropsWithChildren } from 'react'

import { IS_PLATFORM } from 'common'

import { supabaseMisc } from '~/lib/supabaseMisc'
import { NavMenuSection } from '~/components/Navigation/Navigation.types'
import Layout from '~/layouts/guides'
import { supabaseMisc } from '~/lib/supabaseMisc'
import { getAiPrompts } from './getting-started/ai-prompts/[slug]/AiPrompts.utils'

// Revalidate occasionally to pick up changes to partners
// 60 seconds/minute * 60 minutes/hour * 24 hours/day
export const revalidate = 86_400

const GuidesLayout = async ({ children }: PropsWithChildren) => {
const partners = IS_PLATFORM ? await getPartners() : []
const partnerNavItems = partners.map((partner) => ({
name: partner.title,
url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`,
}))
const [partnerNavItems, promptNavItems] = await Promise.all([getPartners(), getPrompts()])

return <Layout additionalNavItems={partnerNavItems}>{children}</Layout>
const additionalNavItems =
partnerNavItems.length > 0 || promptNavItems.length > 0
? { integrations: partnerNavItems, prompts: promptNavItems }
: undefined

return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
}

async function getPrompts() {
const prompts = await getAiPrompts()
return prompts.map(
(prompt) =>
({
name: prompt.heading,
url: `/guides/getting-started/ai-prompts/${prompt.filename}`,
}) as Partial<NavMenuSection>
)
}

const getPartners = cache(getPartnersImpl)
async function getPartnersImpl() {
if (!IS_PLATFORM) return []

const { data, error } = await supabaseMisc()
.from('partners')
.select('slug, title')
Expand All @@ -31,7 +47,15 @@ async function getPartnersImpl() {
console.error(new Error('Error fetching partners', { cause: error }))
}

return data ?? []
const partnerNavItems = (data ?? []).map(
(partner) =>
({
name: partner.title,
url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`,
}) as Partial<NavMenuSection>
)

return partnerNavItems
}

export default GuidesLayout
9 changes: 9 additions & 0 deletions apps/docs/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ function useBreadcrumbs() {
return breadcrumbs
}

const isAiPromptsPage = pathname.startsWith('/guides/getting-started/ai-prompts')
if (isAiPromptsPage) {
const breadcrumbs = [
{ name: 'Getting started', url: '/guides/getting-started' },
{ name: 'AI Prompts', url: '/guides/getting-started/ai-prompts' },
]
return breadcrumbs
}

const menuId = getMenuId(pathname)
const menu = NavItems[menuId]
return findMenuItemByUrl(menu, pathname, [])
Expand Down
56 changes: 40 additions & 16 deletions apps/docs/components/HomePageCover.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
'use client'

import { useBreakpoint } from 'common'
import { ChevronRight, Play, Sparkles } from 'lucide-react'
import Link from 'next/link'
import { IconBackground } from 'ui'

import { useBreakpoint } from 'common'
import { cn, IconBackground } from 'ui'
import { IconPanel } from 'ui-patterns/IconPanel'

import DocsCoverLogo from './DocsCoverLogo'
import { Play } from 'lucide-react'

function AiPrompt({ className }: { className?: string }) {
return (
<Link
className={cn(
'group',
'w-fit rounded-full border px-3 py-1 flex gap-2 items-center text-foreground-light text-sm',
'hover:border-brand hover:text-brand focus-visible:text-brand',
'transition-colors',
className
)}
href="/guides/getting-started/ai-prompts"
>
<Sparkles size={14} />
Start with Supabase AI prompts
<ChevronRight size={14} className="group-hover:translate-x-1 transition-transform" />
</Link>
)
}

const HomePageCover = (props) => {
const isXs = useBreakpoint(639)
Expand Down Expand Up @@ -74,7 +95,7 @@ const HomePageCover = (props) => {
p-5 md:p-8
"
>
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-3">
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-3">
<div className="md:max-w-xs shrink w-fit xl:max-w-none">
<div className="flex items-center gap-3 mb-3">
<IconBackground>
Expand All @@ -83,20 +104,23 @@ const HomePageCover = (props) => {
<h2 className="text-2xl m-0 text-foreground">Getting Started</h2>
</div>
<p className="text-foreground-light text-sm">
Discover how to set up a database to an app making queries in just a few minutes.
Set up and connect a database in just a few minutes.
</p>
</div>
<div className="flex shrink-0 flex-wrap md:grid md:grid-cols-5 gap-2 sm:gap-3">
{frameworks.map((framework, i) => (
<Link key={i} href={framework.href} passHref className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip={framework.tooltip}
icon={framework.icon}
/>
</Link>
))}
<div className="shrink-0">
<div className="flex flex-wrap md:grid md:grid-cols-5 gap-2 sm:gap-3">
{frameworks.map((framework, i) => (
<Link key={i} href={framework.href} passHref className="no-underline">
<IconPanel
iconSize={iconSize}
hideArrow
tooltip={framework.tooltip}
icon={framework.icon}
/>
</Link>
))}
</div>
<AiPrompt className="mt-6" />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ export const gettingstarted: NavMenuConstant = {
},
],
},
{
name: 'AI Prompts',
url: undefined,
items: [
{
name: 'Overview',
url: '/guides/getting-started/ai-prompts',
},
],
},
],
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ const NavigationMenu = ({
additionalNavItems,
}: {
menuId: MenuId
additionalNavItems?: Partial<NavMenuSection>[]
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
}) => {
const level = menuId
const menu = getMenuById(level)
Expand Down
Loading
Loading