Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
423
docs/content/supabase.mdoc
Normal file
423
docs/content/supabase.mdoc
Normal file
@@ -0,0 +1,423 @@
|
||||
---
|
||||
status: "published"
|
||||
title: "Supabase CMS Plugin for the Next.js Supabase SaaS Kit"
|
||||
label: "Supabase"
|
||||
description: "Store content in your Supabase database with optional Supamode integration for a visual admin interface."
|
||||
order: 4
|
||||
---
|
||||
|
||||
The Supabase CMS plugin stores content directly in your Supabase database. This gives you full control over your content schema, row-level security policies, and the ability to query content alongside your application data.
|
||||
|
||||
This approach works well when you want content in the same database as your app, need RLS policies on content, or want to use [Supamode](/supabase-cms) as your admin interface.
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install the Plugin
|
||||
|
||||
Run the Makerkit CLI from your app directory:
|
||||
|
||||
```bash
|
||||
npx @makerkit/cli plugins install
|
||||
```
|
||||
|
||||
Select **Supabase CMS** when prompted.
|
||||
|
||||
### 2. Add the Package Dependency
|
||||
|
||||
Add the plugin to your CMS package:
|
||||
|
||||
```bash
|
||||
pnpm --filter "@kit/cms" add "@kit/supabase-cms@workspace:*"
|
||||
```
|
||||
|
||||
### 3. Register the CMS Type
|
||||
|
||||
Update the CMS type definition to include Supabase:
|
||||
|
||||
```tsx {% title="packages/cms/types/src/cms.type.ts" %}
|
||||
export type CmsType = 'wordpress' | 'keystatic' | 'supabase';
|
||||
```
|
||||
|
||||
### 4. Register the Client
|
||||
|
||||
Add the Supabase client to the CMS registry:
|
||||
|
||||
```tsx {% title="packages/cms/core/src/create-cms-client.ts" %}
|
||||
import { CmsClient, CmsType } from '@kit/cms-types';
|
||||
import { createRegistry } from '@kit/shared/registry';
|
||||
|
||||
const CMS_CLIENT = process.env.CMS_CLIENT as CmsType;
|
||||
const cmsRegistry = createRegistry<CmsClient, CmsType>();
|
||||
|
||||
// Existing registrations...
|
||||
cmsRegistry.register('wordpress', async () => {
|
||||
const { createWordpressClient } = await import('@kit/wordpress');
|
||||
return createWordpressClient();
|
||||
});
|
||||
|
||||
cmsRegistry.register('keystatic', async () => {
|
||||
const { createKeystaticClient } = await import('@kit/keystatic');
|
||||
return createKeystaticClient();
|
||||
});
|
||||
|
||||
// Add Supabase registration
|
||||
cmsRegistry.register('supabase', async () => {
|
||||
const { createSupabaseCmsClient } = await import('@kit/supabase-cms');
|
||||
return createSupabaseCmsClient();
|
||||
});
|
||||
|
||||
export async function createCmsClient(type: CmsType = CMS_CLIENT) {
|
||||
return cmsRegistry.get(type);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Register the Content Renderer
|
||||
|
||||
Add the Supabase content renderer:
|
||||
|
||||
```tsx {% title="packages/cms/core/src/content-renderer.tsx" %}
|
||||
cmsContentRendererRegistry.register('supabase', async () => {
|
||||
return function SupabaseContentRenderer({ content }: { content: unknown }) {
|
||||
return content as React.ReactNode;
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
The default renderer returns content as-is. If you store HTML, it renders as HTML. For Markdown, add a Markdown renderer.
|
||||
|
||||
### 6. Run the Migration
|
||||
|
||||
Create a new migration file:
|
||||
|
||||
```bash
|
||||
pnpm --filter web supabase migration new cms
|
||||
```
|
||||
|
||||
Copy the contents of `packages/plugins/supabase-cms/migration.sql` to the new migration file, then apply it:
|
||||
|
||||
```bash
|
||||
pnpm --filter web supabase migration up
|
||||
```
|
||||
|
||||
### 7. Generate Types
|
||||
|
||||
Regenerate TypeScript types to include the new tables:
|
||||
|
||||
```bash
|
||||
pnpm run supabase:web:typegen
|
||||
```
|
||||
|
||||
### 8. Set the Environment Variable
|
||||
|
||||
Switch to the Supabase CMS:
|
||||
|
||||
```bash
|
||||
# apps/web/.env
|
||||
CMS_CLIENT=supabase
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
The plugin creates three tables:
|
||||
|
||||
### content_items
|
||||
|
||||
Stores all content (posts, pages, docs):
|
||||
|
||||
```sql
|
||||
create table public.content_items (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
title text not null,
|
||||
slug text not null unique,
|
||||
description text,
|
||||
content text,
|
||||
image text,
|
||||
status text not null default 'draft',
|
||||
collection text not null default 'posts',
|
||||
published_at timestamp with time zone,
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now(),
|
||||
parent_id uuid references public.content_items(id),
|
||||
"order" integer default 0,
|
||||
language text,
|
||||
metadata jsonb default '{}'::jsonb
|
||||
);
|
||||
```
|
||||
|
||||
### categories
|
||||
|
||||
Content categories:
|
||||
|
||||
```sql
|
||||
create table public.categories (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
slug text not null unique,
|
||||
created_at timestamp with time zone default now()
|
||||
);
|
||||
```
|
||||
|
||||
### tags
|
||||
|
||||
Content tags:
|
||||
|
||||
```sql
|
||||
create table public.tags (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
slug text not null unique,
|
||||
created_at timestamp with time zone default now()
|
||||
);
|
||||
```
|
||||
|
||||
### Junction Tables
|
||||
|
||||
Many-to-many relationships:
|
||||
|
||||
```sql
|
||||
create table public.content_items_categories (
|
||||
content_item_id uuid references public.content_items(id) on delete cascade,
|
||||
category_id uuid references public.categories(id) on delete cascade,
|
||||
primary key (content_item_id, category_id)
|
||||
);
|
||||
|
||||
create table public.content_items_tags (
|
||||
content_item_id uuid references public.content_items(id) on delete cascade,
|
||||
tag_id uuid references public.tags(id) on delete cascade,
|
||||
primary key (content_item_id, tag_id)
|
||||
);
|
||||
```
|
||||
|
||||
## Using Supamode as Admin
|
||||
|
||||
{% img src="/assets/images/supamode-cms-plugin-posts.webp" width="2970" height="2028" alt="Supamode CMS Posts Interface" /%}
|
||||
|
||||
[Supamode](/supabase-cms) provides a visual interface for managing content in Supabase tables. It's built specifically for Supabase and integrates with RLS policies.
|
||||
|
||||
{% alert type="default" title="Supamode is optional" %}
|
||||
Supamode is a separate product. You can use any Postgres admin tool, build your own admin, or manage content via SQL.
|
||||
{% /alert %}
|
||||
|
||||
### Setting Up Supamode
|
||||
|
||||
1. Install Supamode following the [installation guide](/docs/supamode/installation)
|
||||
2. Sync the CMS tables to Supamode:
|
||||
- Run the following SQL commands in Supabase Studio's SQL Editor:
|
||||
```sql
|
||||
-- Run in Supabase Studio's SQL Editor
|
||||
select supamode.sync_managed_tables('public', 'content_items');
|
||||
select supamode.sync_managed_tables('public', 'categories');
|
||||
select supamode.sync_managed_tables('public', 'tags');
|
||||
```
|
||||
3. Configure table views in the Supamode UI under **Resources**
|
||||
|
||||
### Content Editing
|
||||
|
||||
With Supamode, you can:
|
||||
- Create and edit content with a form-based UI
|
||||
- Upload images to Supabase Storage
|
||||
- Manage categories and tags
|
||||
- Preview content before publishing
|
||||
- Filter and search content
|
||||
|
||||
## Querying Content
|
||||
|
||||
The Supabase CMS client implements the standard CMS interface:
|
||||
|
||||
```tsx
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
|
||||
const client = await createCmsClient();
|
||||
|
||||
// Get all published posts
|
||||
const { items, total } = await client.getContentItems({
|
||||
collection: 'posts',
|
||||
status: 'published',
|
||||
limit: 10,
|
||||
sortBy: 'publishedAt',
|
||||
sortDirection: 'desc',
|
||||
});
|
||||
|
||||
// Get a specific post
|
||||
const post = await client.getContentItemBySlug({
|
||||
slug: 'getting-started',
|
||||
collection: 'posts',
|
||||
});
|
||||
```
|
||||
|
||||
### Direct Supabase Queries
|
||||
|
||||
For complex queries, use the Supabase client directly:
|
||||
|
||||
```tsx
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
async function getPostsWithCustomQuery() {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data, error } = await client
|
||||
.from('content_items')
|
||||
.select(`
|
||||
*,
|
||||
categories:content_items_categories(
|
||||
category:categories(*)
|
||||
),
|
||||
tags:content_items_tags(
|
||||
tag:tags(*)
|
||||
)
|
||||
`)
|
||||
.eq('collection', 'posts')
|
||||
.eq('status', 'published')
|
||||
.order('published_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
## Row-Level Security
|
||||
|
||||
Add RLS policies to control content access:
|
||||
|
||||
```sql
|
||||
-- Allow public read access to published content
|
||||
create policy "Public can read published content"
|
||||
on public.content_items
|
||||
for select
|
||||
using (status = 'published');
|
||||
|
||||
-- Allow authenticated users to read all content
|
||||
create policy "Authenticated users can read all content"
|
||||
on public.content_items
|
||||
for select
|
||||
to authenticated
|
||||
using (true);
|
||||
|
||||
-- Allow admins to manage content
|
||||
create policy "Admins can manage content"
|
||||
on public.content_items
|
||||
for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1 from public.accounts
|
||||
where accounts.id = auth.uid()
|
||||
and accounts.is_admin = true
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Content Format
|
||||
|
||||
The `content` field stores text. Common formats:
|
||||
|
||||
### HTML
|
||||
|
||||
Store rendered HTML directly:
|
||||
|
||||
```tsx
|
||||
const post = {
|
||||
title: 'Hello World',
|
||||
content: '<p>This is <strong>HTML</strong> content.</p>',
|
||||
};
|
||||
```
|
||||
|
||||
Render with `dangerouslySetInnerHTML` or a sanitizing library.
|
||||
|
||||
### Markdown
|
||||
|
||||
Store Markdown and render at runtime:
|
||||
|
||||
```tsx
|
||||
import { marked } from 'marked';
|
||||
import type { Cms } from '@kit/cms-types';
|
||||
|
||||
function renderContent(markdown: string) {
|
||||
return { __html: marked(markdown) };
|
||||
}
|
||||
|
||||
function Post({ post }: { post: Cms.ContentItem }) {
|
||||
return (
|
||||
<article>
|
||||
<h1>{post.title}</h1>
|
||||
<div dangerouslySetInnerHTML={renderContent(post.content as string)} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### JSON
|
||||
|
||||
Store structured content as JSON in the `metadata` field:
|
||||
|
||||
```tsx
|
||||
const post = {
|
||||
title: 'Product Comparison',
|
||||
content: '', // Optional summary
|
||||
metadata: {
|
||||
products: [
|
||||
{ name: 'Basic', price: 9 },
|
||||
{ name: 'Pro', price: 29 },
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Customizing the Schema
|
||||
|
||||
Extend the schema by modifying the migration:
|
||||
|
||||
```sql
|
||||
-- Add custom fields
|
||||
alter table public.content_items
|
||||
add column author_id uuid references auth.users(id),
|
||||
add column reading_time integer,
|
||||
add column featured boolean default false;
|
||||
|
||||
-- Add indexes
|
||||
create index content_items_featured_idx
|
||||
on public.content_items(featured)
|
||||
where status = 'published';
|
||||
```
|
||||
|
||||
Update the Supabase client to handle custom fields.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `CMS_CLIENT` | Yes | Set to `supabase` |
|
||||
|
||||
The plugin uses your existing Supabase connection (no additional configuration needed).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Migration fails
|
||||
|
||||
Check that you have the latest Supabase CLI and your local database is running:
|
||||
|
||||
```bash
|
||||
pnpm --filter web supabase start
|
||||
pnpm --filter web supabase migration up
|
||||
```
|
||||
|
||||
### TypeScript errors after migration
|
||||
|
||||
Regenerate types:
|
||||
|
||||
```bash
|
||||
pnpm run supabase:web:typegen
|
||||
```
|
||||
|
||||
### Content not appearing
|
||||
|
||||
Verify:
|
||||
- The `status` field is set to `published`
|
||||
- The `collection` field matches your query
|
||||
- RLS policies allow access
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [CMS API Reference](/docs/next-supabase-turbo/content/cms-api): Full API documentation
|
||||
- [Supamode Documentation](/docs/supamode/installation): Set up the admin interface
|
||||
- [CMS Overview](/docs/next-supabase-turbo/content/cms): Compare CMS providers
|
||||
Reference in New Issue
Block a user