diff --git a/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts b/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts new file mode 100644 index 00000000000..935bd6d388e --- /dev/null +++ b/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts @@ -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, + } + }) +} diff --git a/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPromptsIndex.tsx b/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPromptsIndex.tsx new file mode 100644 index 00000000000..ed32afb8c67 --- /dev/null +++ b/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPromptsIndex.tsx @@ -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 ( +
+ {prompts.map((prompt) => ( + + + + ))} +
+ ) +} diff --git a/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/page.tsx b/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/page.tsx new file mode 100644 index 00000000000..c2b7c84e67e --- /dev/null +++ b/apps/docs/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/page.tsx @@ -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 \`#\`, in Cursor, use \`@Files\`, and in Zed, use \`/file\`. + +## Prompt + +${content} +`.trim() + + return ( + + ) +} + +export const generateMetadata = generateAiPromptMetadata +export const generateStaticParams = generateAiPromptsStaticParams diff --git a/apps/docs/app/guides/(with-sidebar)/layout.tsx b/apps/docs/app/guides/(with-sidebar)/layout.tsx index 7c57707cdee..532645a070d 100644 --- a/apps/docs/app/guides/(with-sidebar)/layout.tsx +++ b/apps/docs/app/guides/(with-sidebar)/layout.tsx @@ -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 {children} + const additionalNavItems = + partnerNavItems.length > 0 || promptNavItems.length > 0 + ? { integrations: partnerNavItems, prompts: promptNavItems } + : undefined + + return {children} +} + +async function getPrompts() { + const prompts = await getAiPrompts() + return prompts.map( + (prompt) => + ({ + name: prompt.heading, + url: `/guides/getting-started/ai-prompts/${prompt.filename}`, + }) as Partial + ) } const getPartners = cache(getPartnersImpl) async function getPartnersImpl() { + if (!IS_PLATFORM) return [] + const { data, error } = await supabaseMisc() .from('partners') .select('slug, title') @@ -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 + ) + + return partnerNavItems } export default GuidesLayout diff --git a/apps/docs/components/Breadcrumbs.tsx b/apps/docs/components/Breadcrumbs.tsx index 398a5112c2a..d8d2e4856fb 100644 --- a/apps/docs/components/Breadcrumbs.tsx +++ b/apps/docs/components/Breadcrumbs.tsx @@ -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, []) diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index 4e8bff25734..dd67ee4e641 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -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 ( + + + Start with Supabase AI prompts + + + ) +} const HomePageCover = (props) => { const isXs = useBreakpoint(639) @@ -74,7 +95,7 @@ const HomePageCover = (props) => { p-5 md:p-8 " > -
+
@@ -83,20 +104,23 @@ const HomePageCover = (props) => {

Getting Started

- 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.

-
- {frameworks.map((framework, i) => ( - - - - ))} +
+
+ {frameworks.map((framework, i) => ( + + + + ))} +
+
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 719c333152d..c0e952b351b 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -349,6 +349,16 @@ export const gettingstarted: NavMenuConstant = { }, ], }, + { + name: 'AI Prompts', + url: undefined, + items: [ + { + name: 'Overview', + url: '/guides/getting-started/ai-prompts', + }, + ], + }, ], } diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx index d47562c9229..ffb21177637 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.tsx @@ -256,7 +256,7 @@ const NavigationMenu = ({ additionalNavItems, }: { menuId: MenuId - additionalNavItems?: Partial[] + additionalNavItems?: Record[]> }) => { const level = menuId const menu = getMenuById(level) diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx index af40ab7e9e8..e9e211a8265 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx @@ -6,20 +6,23 @@ import { type NavMenuSection } from '../Navigation.types' import * as NavItems from './NavigationMenu.constants' import NavigationMenuGuideListItems from './NavigationMenuGuideListItems' import { usePathname } from 'next/navigation' +import { PropsWithChildren } from 'react' +import { MenuId } from './NavigationMenu' const NavigationMenuGuideList = ({ id, additionalNavItems, }: { id: string - additionalNavItems?: Partial[] + additionalNavItems?: Record[]> }) => { const pathname = usePathname() const firstLevelRoute = pathname?.split('/')?.slice(0, 4)?.join('/') + // eslint-disable-next-line import/namespace -- dynamic access, can't lint properly let menu = NavItems[id] - if (id === 'integrations' && additionalNavItems) { + if (id === MenuId.Integrations && additionalNavItems?.integrations) { const integrationsListIndex = menu.items.findIndex((item) => item.name === 'Integrations') if (integrationsListIndex !== -1) { menu = { @@ -28,7 +31,7 @@ const NavigationMenuGuideList = ({ ...menu.items.slice(0, integrationsListIndex), { ...menu.items[integrationsListIndex], - items: [...menu.items[integrationsListIndex].items, ...additionalNavItems], + items: [...menu.items[integrationsListIndex].items, ...additionalNavItems.integrations], }, ...menu.items.slice(integrationsListIndex + 1), ], @@ -36,6 +39,38 @@ const NavigationMenuGuideList = ({ } } + if (id === MenuId.GettingStarted && additionalNavItems?.prompts) { + const promptsSectionIndex = menu.items.findIndex((item) => item.name === 'AI Prompts') + if (promptsSectionIndex !== -1) { + menu = { + ...menu, + items: [ + ...menu.items.slice(0, promptsSectionIndex), + { + ...menu.items[promptsSectionIndex], + items: [...menu.items[promptsSectionIndex].items, ...additionalNavItems.prompts], + }, + ...menu.items.slice(promptsSectionIndex + 1), + ], + } + } + } + + return ( + + + + ) +} + +export function NavigationMenuGuideListWrapper({ + id, + firstLevelRoute, + children, +}: PropsWithChildren<{ + id: string + firstLevelRoute?: string +}>) { return ( - + {children} ) } diff --git a/apps/docs/content/guides/getting-started/ai-prompts.mdx b/apps/docs/content/guides/getting-started/ai-prompts.mdx new file mode 100644 index 00000000000..412364c2bfb --- /dev/null +++ b/apps/docs/content/guides/getting-started/ai-prompts.mdx @@ -0,0 +1,16 @@ +--- +title: AI Prompts +subtitle: Prompts for working with Supabase using AI-powered IDE tools +--- + +We've curated a selection of prompts to help you work with Supabase using your favorite AI-powered IDE tools, such as GitHub Copilot or Cursor. + +## 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 `#`, in Cursor, use `@Files`, and in Zed, use `/file`. + +## Prompts + + diff --git a/apps/docs/features/docs/MdxBase.tsx b/apps/docs/features/docs/MdxBase.tsx index 1bda6f1f0b1..20413a1cc7d 100644 --- a/apps/docs/features/docs/MdxBase.tsx +++ b/apps/docs/features/docs/MdxBase.tsx @@ -7,9 +7,14 @@ import remarkGfm from 'remark-gfm' import rehypeKatex from 'rehype-katex' import remarkMath from 'remark-math' +import { AiPromptsIndex } from '~/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPromptsIndex' import { preprocessMdxWithDefaults } from '~/features/directives/utils' import { components } from '~/features/docs/MdxBase.shared' +const serverOnlyComponents = { + AiPromptsIndex, +} + const codeHikeOptions: CodeHikeConfig = { theme: codeHikeTheme, lineNumbers: true, @@ -66,7 +71,7 @@ const MDXRemoteBase = async ({ return ( diff --git a/apps/docs/layouts/MainSkeleton.tsx b/apps/docs/layouts/MainSkeleton.tsx index 62bf27160f8..9e116e883c9 100644 --- a/apps/docs/layouts/MainSkeleton.tsx +++ b/apps/docs/layouts/MainSkeleton.tsx @@ -336,7 +336,7 @@ interface SkeletonProps extends PropsWithChildren { NavigationMenu?: ReactNode hideFooter?: boolean className?: string - additionalNavItems?: Partial[] + additionalNavItems?: Record[]> } function TopNavSkeleton({ children }) { diff --git a/apps/docs/layouts/guides/index.tsx b/apps/docs/layouts/guides/index.tsx index 0a2695833c9..c6ba38d293a 100644 --- a/apps/docs/layouts/guides/index.tsx +++ b/apps/docs/layouts/guides/index.tsx @@ -1,5 +1,5 @@ import 'katex/dist/katex.min.css' -import { type PropsWithChildren } from 'react' +import type { ReactNode, PropsWithChildren } from 'react' import { type NavMenuSection } from '~/components/Navigation/Navigation.types' import { LayoutMainContent } from '~/layouts/DefaultLayout' @@ -8,9 +8,13 @@ import { SidebarSkeleton } from '~/layouts/MainSkeleton' const Layout = ({ children, additionalNavItems, -}: PropsWithChildren<{ additionalNavItems?: Partial[] }>) => { + NavigationMenu, +}: PropsWithChildren<{ + additionalNavItems?: Record[]> + NavigationMenu?: ReactNode +}>) => { return ( - + {children} ) diff --git a/apps/docs/next-env.d.ts b/apps/docs/next-env.d.ts index 725dd6f2451..40c3d68096c 100644 --- a/apps/docs/next-env.d.ts +++ b/apps/docs/next-env.d.ts @@ -1,6 +1,5 @@ /// /// -/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/apps/docs/package.json b/apps/docs/package.json index 9cf97308475..857ab7ba745 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -101,7 +101,6 @@ "unist-builder": "^3.0.1", "unist-util-filter": "^4.0.1", "unist-util-visit": "^4.1.2", - "unist-util-visit-parents": "^5.0.0", "uuid": "^9.0.1", "valtio": "^1.12.0", "yaml": "^2.4.5", diff --git a/examples/prompts/code-format-sql.md b/examples/prompts/code-format-sql.md index ed3cb254a30..45e55811dac 100644 --- a/examples/prompts/code-format-sql.md +++ b/examples/prompts/code-format-sql.md @@ -1,4 +1,4 @@ -# PostgreSQL SQL Style Guide +# Postgres SQL Style Guide ## General @@ -12,7 +12,7 @@ - Avoid SQL reserved words and ensure names are unique and under 63 characters. - Use snake_case for tables and columns. -- Prefer plurals for table names +- Prefer plurals for table names - Prefer singular names for columns. ## Tables @@ -62,12 +62,12 @@ where employee_id = 1001; Larger queries: ```sql -select - first_name, +select + first_name, last_name -from +from employees -where +where start_date between '2021-01-01' and '2021-12-31' and status = 'employed'; @@ -80,14 +80,14 @@ and - Prefer full table names when referencing tables. This helps for readability. ```sql -select - employees.employee_name, +select + employees.employee_name, departments.department_name -from +from employees -join +join departments on employees.department_id = departments.department_id -where +where employees.start_date > '2022-01-01'; ``` @@ -104,7 +104,7 @@ where end_date is null; ## Complex queries and CTEs -- If a query is extremely complex, prefer a CTE. +- If a query is extremely complex, prefer a CTE. - Make sure the CTE is clear and linear. Prefer readability over performance. - Add comments to each block. @@ -138,4 +138,4 @@ from employee_counts order by department_name; -``` \ No newline at end of file +``` diff --git a/examples/prompts/nextjs-supabase-auth.md b/examples/prompts/nextjs-supabase-auth.md new file mode 100644 index 00000000000..b10a4d63c3d --- /dev/null +++ b/examples/prompts/nextjs-supabase-auth.md @@ -0,0 +1,108 @@ +# Bootstrap Next.js app with Supabase Auth + +Create a Next.js app that uses App Router with Supabase Auth. + +Follow Supabase's guidelines for using the `@supabase/ssr` package and Server-Side Auth. Specifically, there should be: + +- A utility function to create a client on the client side +- A utility function create a client on the server side, using the Next.js `cookies` API to access the cookies. Use the latest version of the API, where `cookies` must be awaited. +- A utility function to handle refreshing the user session in middleware. + +## Working with cookies + +Use the latest version of `@supabase/ssr`, where cookie options are defined with the `getAll` and `setAll` functions, like so: + +``` +const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ) + }, + }, + } + ) +``` + +No other cookie options should be provided. + +## Middleware + +The middleware should use the following `updateSession` function: + +``` +import { createServerClient } from '@supabase/ssr' +import { NextResponse, type NextRequest } from 'next/server' + +export async function updateSession(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }) + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value)) + supabaseResponse = NextResponse.next({ + request, + }) + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options) + ) + }, + }, + } + ) + + // IMPORTANT: Avoid writing any logic between createServerClient and + // supabase.auth.getUser(). A simple mistake could make it very hard to debug + // issues with users being randomly logged out. + + const { + data: { user }, + } = await supabase.auth.getUser() + + if ( + !user && + !request.nextUrl.pathname.startsWith('/login') && + !request.nextUrl.pathname.startsWith('/auth') + ) { + // no user, potentially respond by redirecting the user to the login page + const url = request.nextUrl.clone() + url.pathname = '/login' + return NextResponse.redirect(url) + } + + // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're + // creating a new response object with NextResponse.next() make sure to: + // 1. Pass the request in it, like so: + // const myNewResponse = NextResponse.next({ request }) + // 2. Copy over the cookies, like so: + // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll()) + // 3. Change the myNewResponse object to fit your needs, but avoid changing + // the cookies! + // 4. Finally: + // return myNewResponse + // If this is not done, you may be causing the browser and server to go out + // of sync and terminate the user's session prematurely! + + return supabaseResponse +} +``` diff --git a/package-lock.json b/package-lock.json index 8d9b9e74f34..2bdf6f2e7ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -931,7 +931,6 @@ "unist-builder": "^3.0.1", "unist-util-filter": "^4.0.1", "unist-util-visit": "^4.1.2", - "unist-util-visit-parents": "^5.0.0", "uuid": "^9.0.1", "valtio": "^1.12.0", "yaml": "^2.4.5",