diff --git a/.github/workflows/docs-lint-v2.yml b/.github/workflows/docs-lint-v2.yml index 31cfe6ea4fd73..1536658780fa6 100644 --- a/.github/workflows/docs-lint-v2.yml +++ b/.github/workflows/docs-lint-v2.yml @@ -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: @@ -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 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 0000000000000..935bd6d388e4c --- /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 0000000000000..ed32afb8c671a --- /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 0000000000000..c2b7c84e67ec6 --- /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 7c57707cdee62..532645a070d8b 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 398a5112c2aaa..d8d2e4856fb6d 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 4e8bff257349b..dd67ee4e641d1 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 719c333152d81..c0e952b351b45 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 d47562c92298a..ffb21177637b5 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 af40ab7e9e869..e9e211a826598 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/ai/examples/semantic-image-search-amazon-titan.mdx b/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx index c2dd616366eaa..373720b7797aa 100644 --- a/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx +++ b/apps/docs/content/guides/ai/examples/semantic-image-search-amazon-titan.mdx @@ -28,7 +28,7 @@ Then initialize a new project: poetry new aws_bedrock_image_search ``` -## Spin up a Postgres Database with pgvector +## Spin up a Postgres database with pgvector If you haven't already, head over to [database.new](https://database.new) and create a new project. Every Supabase project comes with a full Postgres database and the [pgvector extension](/docs/guides/database/extensions/pgvector) preconfigured. diff --git a/apps/docs/content/guides/ai/integrations/amazon-bedrock.mdx b/apps/docs/content/guides/ai/integrations/amazon-bedrock.mdx index d18f9a6bc83dc..02699d3271de0 100644 --- a/apps/docs/content/guides/ai/integrations/amazon-bedrock.mdx +++ b/apps/docs/content/guides/ai/integrations/amazon-bedrock.mdx @@ -10,7 +10,7 @@ tocVideo: 'A3uND5sgiO0' This guide will walk you through an example using Amazon Bedrock SDK with `vecs`. We will create embeddings using the Amazon Titan Embeddings G1 – Text v1.2 (amazon.titan-embed-text-v1) model, insert these embeddings into a PostgreSQL database using vecs, and then query the collection to find the most similar sentences to a given query sentence. -## Create an Environment +## Create an environment First, you need to set up your environment. You will need Python 3.7+ with the `vecs` and `boto3` libraries installed. @@ -25,7 +25,7 @@ You'll also need: - [Credentials to your AWS account](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) - [A Postgres Database with the pgvector extension](hosting.md) -## Create Embeddings +## Create embeddings Next, we will use Amazon’s Titan Embedding G1 - Text v1.2 model to create embeddings for a set of sentences. @@ -67,7 +67,7 @@ for sentence in dataset: ``` -### Store the Embeddings with vecs +### Store the embeddings with vecs Now that we have our embeddings, we can insert them into a PostgreSQL database using vecs. @@ -90,7 +90,7 @@ sentences.upsert(records=embeddings) sentences.create_index() ``` -### Querying for Most Similar Sentences +### Querying for most similar sentences Now, we query the `sentences` collection to find the most similar sentences to a sample query sentence. First need to create an embedding for the query sentence. Next, we query the collection we created earlier to find the most similar sentences. 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 0000000000000..412364c2bfb62 --- /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 1bda6f1f0b1f4..20413a1cc7d0b 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 62bf27160f8c2..9e116e883c9a7 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 0a2695833c93a..c6ba38d293a22 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 725dd6f245153..40c3d68096c27 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 9cf9730847556..857ab7ba745c6 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 ed3cb254a30d2..45e55811dac3b 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 0000000000000..b10a4d63c3d30 --- /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 8d9b9e74f34a7..2bdf6f2e7eab5 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", diff --git a/supa-mdx-lint.config.toml b/supa-mdx-lint.config.toml index 1eba0db28cce7..745fe5912332b 100644 --- a/supa-mdx-lint.config.toml +++ b/supa-mdx-lint.config.toml @@ -5,32 +5,42 @@ ignore_patterns = ["**/_*.mdx"] # Words that may be uppercased even if they are not the first word in the sentence. # Can also specify a regex that is compatible with the [Rust regex crate](https://docs.rs/regex/latest/regex/). may_uppercase = [ - "[A-Z]{3,5}", + "[A-Z0-9]{2,5}", "Android", "Angular", "Apple", "Auth", + "ChatGPT", "Content Delivery Network", "Dart", - "Edge Functions", + "Edge Functions?", "Flutter", "GoTrue", "Google", "GraphQL", + "Hugging Face", + "I", + "IVFFlat", "Ionic Angular", "Ionic React", "Ionic Vue", "JavaScript", "Kotlin", + "Navigable Small World", "Next.js", "Nuxt", "OpenAI", + "Poetry", "Postgres", "PostgreSQL", "PostgREST", + "Python", "React", "React Native", + "Reciprocal Ranked Fusion", "RedwoodJS", + "Retrieval Plugin", + "Roboflow Inference", "Row Level Security", "Server-Side Auth", "Single Sign-On", @@ -40,8 +50,11 @@ may_uppercase = [ "SvelteKit", "Swift", "SwiftUI", + "TypeScript", "Xcode", + "Vecs", "Vue", + "Wrappers", ] # Words that may be lowercased even if they are the first word in the sentence. # Can also specify a regex that is compatible with the [Rust regex crate](https://docs.rs/regex/latest/regex/).