TL;DR
The one-paragraph version before you dive in.
This prompt kit builds a minimal blog CMS with draft/publish workflow, categories, a markdown editor with image upload to Supabase Storage, and a public reader view — all in a single Lovable afternoon. The editor experience ships in under 60 credits. The SEO caveat: Lovable is a Vite SPA, so if your blog needs to rank on Google, plan to migrate the reader view to Next.js or Astro after the build phase.
Setup checklist
Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.
Cloud tab settings
Database
Stores posts, categories, post_categories junction, and author profiles. The migration creates RLS policies ensuring only published posts are visible to anonymous visitors.
- 1Click the + icon next to the preview panel in the top-right area, then click 'Cloud tab'.
- 2Click 'Database' — Lovable Cloud provisions a Supabase Postgres instance automatically.
- 3The starter prompt will generate the migration SQL; paste it into Cloud → Database → SQL Editor if Lovable does not run it automatically.
Auth
Email/password sign-in for blog authors. Anonymous readers browse without an account.
- 1In the Cloud tab, click 'Users & Auth'.
- 2Under 'Providers', confirm 'Email' is enabled.
- 3After the starter prompt runs, manually sign up as the first author via the /signin page, then run in SQL Editor: `UPDATE profiles SET role = 'admin' WHERE id = (SELECT id FROM auth.users WHERE email = 'your@email.com');`
Storage
Cover images and inline body images for posts are stored in a 'post-media' bucket with public-read access.
- 1In the Cloud tab, click 'Storage'.
- 2Create a new bucket named 'post-media' and set it to Public.
- 3The starter prompt will add Storage RLS policies. Verify by uploading a test image from the editor and confirming the public URL loads in a browser.
Preflight checklist
- You are in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded by Lovable).
- You have a Lovable account (Free is enough for the build; Pro $25/mo only if you iterate heavily).
- You have decided whether your blog needs Google SEO traffic. If yes, plan to migrate the public reader view to Next.js or Astro after the editor is built — this is an architectural decision, not a Lovable prompt.
- You know your primary author email address — you will need it to grant yourself admin access after the first sign-up.
The starter prompt
Copy this. Paste it into Lovable Build mode (the default chat at the bottom-left of the editor). Hit send.
Build a minimal blog CMS backend using Vite + React + TypeScript + Tailwind CSS + shadcn/ui, backed by Supabase (Lovable Cloud). Here is the exact schema, RLS, routes, and components you should scaffold:
**Database migration (run as 0001_blog_schema.sql):**
Create four tables:
1. `profiles` — columns: id uuid references auth.users primary key, full_name text, bio text, avatar_path text, twitter_handle text, role text default 'author'. RLS: public-read for full_name, bio, avatar_path (for author bio); per-user write own where id = auth.uid().
2. `categories` — columns: id uuid primary key default gen_random_uuid(), slug text unique not null, name text not null, description text. RLS: public-read, write only if profiles.role = 'admin' (use a SECURITY DEFINER plpgsql function `is_admin()` that checks `(SELECT role FROM profiles WHERE id = auth.uid()) = 'admin'` — never inline SQL in the policy).
3. `posts` — columns: id uuid primary key default gen_random_uuid(), author_id uuid references auth.users not null, slug text unique not null, title text not null, summary text, body_markdown text not null default '', cover_image_path text, status text check (status in ('draft','scheduled','published','archived')) default 'draft', published_at timestamptz, scheduled_for timestamptz, created_at timestamptz default now(), updated_at timestamptz default now(). RLS: public-read where `status = 'published' AND published_at <= now()`, per-author write where `author_id = auth.uid()`, admin write via is_admin(). Add a trigger `update_posts_updated_at` that sets updated_at = now() on every UPDATE. Add a partial index: `CREATE INDEX posts_published_idx ON posts(published_at DESC) WHERE status = 'published';`
4. `post_categories` — columns: post_id uuid references posts, category_id uuid references categories, primary key (post_id, category_id). RLS: inherit via post ownership for write, public-read.
Also create the `is_admin()` SECURITY DEFINER plpgsql function:
```sql
CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
RETURN (SELECT role FROM public.profiles WHERE id = auth.uid()) = 'admin';
END;
$$;
```
**Layouts:**
- `PublicLayout.tsx` — top nav with Logo / Blog / Categories / About links and a minimal footer.
- `AdminLayout.tsx` — left sidebar with: Posts, Categories, Settings. Wrap in `<AdminGuard>` that reads the Supabase auth session and the `profiles.role` field; redirect to /signin if not admin.
**Pages and routes:**
- `/` — post grid (newest published first), 3-column desktop / 1-column mobile, using PostCard components.
- `/[slug]` — post detail with PostBody (renders body_markdown via react-markdown + rehype-highlight). Show author bio from profiles. Show category chips. Show prev/next post links.
- `/category/[slug]` — filtered post grid.
- `/author/[handle]` — author bio + all their published posts.
- `/admin` — posts list with status filter chips (All / Draft / Scheduled / Published / Archived). Each row shows title, status, updated_at, Edit and Publish/Unpublish actions.
- `/admin/posts/new` and `/admin/posts/[id]/edit` — MarkdownEditor with a preview tab, cover image upload to Storage bucket 'post-media', category multi-select, status selector, and DraftSaver component that auto-saves to status='draft' every 30 seconds.
- `/signin` — Supabase Auth email/password form.
**Key components:**
- `PostCard.tsx` — image, title, summary, category chip, date, author avatar.
- `PostBody.tsx` — renders body_markdown with react-markdown + rehype-highlight. Import rehype-highlight and import 'highlight.js/styles/github.css'.
- `MarkdownEditor.tsx` — shadcn Tabs with Write and Preview tabs. Write tab: textarea bound to body_markdown. Preview tab: renders PostBody.
- `DraftSaver.tsx` — auto-saves body_markdown to the DB every 30 seconds. Show 'Saved 5s ago' using a lastSaved timestamp. Use useEffect + setInterval.
- `CategoryChip.tsx` — small pill badge linking to /category/[slug].
- `AdminGuard.tsx` — checks auth + role, shows loading spinner, redirects to /signin.
**Styling:** editorial reading experience. Use Source Serif Pro (Google Font import) for post body text. Max content width 720px for post detail. Line height 1.75. Use shadcn Card for PostCard, Tabs for MarkdownEditor, Toast for autosave feedback.
**Deliverables:** The build must produce a working public homepage with published post grid, a post detail page that renders markdown with syntax highlighting, an admin posts list with status filtering, and a markdown editor with 30-second autosave. Everything backed by the exact RLS schema above.What this prompt generates
- Generates the full Supabase migration with 4 tables, RLS policies, and the is_admin() SECURITY DEFINER function.
- Scaffolds PublicLayout and AdminLayout with AdminGuard role check.
- Creates public homepage, post detail, category, and author pages.
- Builds the markdown editor with Write/Preview tabs and 30-second DraftSaver autosave.
- Wires cover-image upload to the 'post-media' Storage bucket.
- Creates admin posts list with status filter and Edit/Publish actions.
Paste into: Lovable Build mode (the default chat at the bottom-left of the editor)
Expected output
What Lovable will generate after the starter prompt runs successfully.
Files
src/layouts/PublicLayout.tsxTop nav + footer for public reader pages
src/layouts/AdminLayout.tsxSidebar + AdminGuard wrapper for author routes
src/pages/Home.tsxPublished post grid, newest first
src/pages/PostDetail.tsxFull post with markdown render + author bio + prev/next
src/pages/Category.tsxFiltered post grid by category slug
src/pages/AuthorPage.tsxAuthor bio + all their published posts
src/pages/admin/Posts.tsxAdmin posts list with status filter chips
src/pages/admin/PostEditor.tsxMarkdown editor with autosave + image upload
src/components/PostCard.tsxCover image + title + summary card
src/components/PostBody.tsxRenders body_markdown via react-markdown
src/components/MarkdownEditor.tsxWrite/Preview tabbed editor
src/components/DraftSaver.tsx30-second autosave with 'Saved Ns ago' indicator
src/components/CategoryChip.tsxPill badge linking to /category/[slug]
src/hooks/useAutosave.tsReusable autosave hook with debounce + status
supabase/migrations/0001_blog_schema.sqlAll 4 tables + RLS + is_admin() function + indexes
Routes
Public homepage — published post grid
Post detail page with markdown render
Category-filtered post grid
Author bio + posts
Admin posts list with status filter
New post editor
Edit existing post
Author sign-in form
DB Tables
profiles| Column | Type |
|---|---|
| id | uuid primary key references auth.users |
| full_name | text |
| bio | text |
| avatar_path | text |
| twitter_handle | text |
| role | text default 'author' |
RLS: Public-read for author bio fields; per-user write own via auth.uid()=id.
categories| Column | Type |
|---|---|
| id | uuid primary key |
| slug | text unique not null |
| name | text not null |
| description | text |
RLS: Public-read; admin-write via is_admin().
posts| Column | Type |
|---|---|
| id | uuid primary key |
| author_id | uuid references auth.users not null |
| slug | text unique not null |
| title | text not null |
| summary | text |
| body_markdown | text not null default '' |
| cover_image_path | text |
| status | text check (status in ('draft','scheduled','published','archived')) default 'draft' |
| published_at | timestamptz |
| scheduled_for | timestamptz |
| created_at | timestamptz default now() |
| updated_at | timestamptz default now() |
RLS: Public-read where status='published' AND published_at <= now(). Per-author write via author_id=auth.uid(). Admin write via is_admin().
post_categories| Column | Type |
|---|---|
| post_id | uuid references posts |
| category_id | uuid references categories |
RLS: Public-read; write inherits from post author via author_id=auth.uid().
Components
PostCardCover image + title + summary card for grids
PostBodyRenders body_markdown with syntax highlighting
MarkdownEditorWrite/Preview tabs with image-paste support
DraftSaver30-second autosave with human-readable timestamp
AdminGuardChecks auth session + profiles.role, redirects if unauthorized
CategoryChipPill badge linking to category page
Follow-up prompts
Paste these into Agent Mode one by one, in order, after the starter prompt finishes.
Markdown editor with image upload and paste support
Paste-and-drag image upload directly into the markdown editor body
Upgrade MarkdownEditor.tsx to support pasting images and dragging images into the write tab. When an image is pasted or dropped:
1. Compress it client-side using browser-image-compression (target 1600px max dimension, ~300KB output).
2. Upload the compressed file to the Supabase Storage 'post-media' bucket at path `covers/{postId}/{filename}`.
3. Insert a markdown image reference `` at the cursor position.
4. Show a progress indicator (spinner + 'Uploading image...') in the toolbar during upload.
Use `supabase.storage.from('post-media').upload(path, file)` then `.getPublicUrl(path)` to get the URL. Do not store the full URL in the database — store only the path and compute the URL on read. Add error handling for failed uploads with a toast message.When to use: Right after the starter prompt finishes — markdown without image upload is half the value
Autosave drafts every 30 seconds
Prevents the 'browser crashed and lost 2 hours of writing' scenario with 30-second DB autosave
Implement robust autosave in PostEditor.tsx using a useAutosave hook. The hook should:
1. Accept the post id, the current body_markdown value, and a save function.
2. Use useEffect with setInterval to trigger a save every 30 seconds if the value has changed since the last save.
3. Track a `lastSavedAt` timestamp state.
4. Expose a `savedAgo` derived string ('Saved 5s ago', 'Saved 2m ago', etc.) that updates every 10 seconds.
5. Show the savedAgo string in a small status indicator in the editor toolbar.
6. On save, call `supabase.from('posts').update({ body_markdown, updated_at: new Date().toISOString() }).eq('id', postId)` only if status is 'draft' — never auto-publish.
7. On successful save, show a shadcn Toast with message 'Draft saved'.
Make sure the interval is cleared on component unmount to prevent memory leaks.When to use: After the image upload prompt — autosave + images together complete the editor experience
Scheduled publish via pg_cron
Posts set to 'scheduled' auto-publish at the scheduled time via pg_cron
Add scheduled publishing support:
1. Ensure the posts table has a `scheduled_for timestamptz` column (add via migration if missing).
2. In PostEditor.tsx, show a 'Schedule for...' datetime picker when status is set to 'scheduled'. Use the shadcn DateTimePicker or a combination of Calendar + TimeInput. Store the value in `posts.scheduled_for`.
3. Enable pg_cron by running in Cloud → Database → SQL Editor:
```sql
CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule(
'publish-scheduled-posts',
'* * * * *',
$$ UPDATE posts SET status = 'published', published_at = now()
WHERE status = 'scheduled' AND scheduled_for <= now(); $$
);
```
4. In the admin posts list, show 'Scheduled for [date]' in the status chip for scheduled posts.
Note: pg_cron runs inside Supabase Postgres — no edge function needed.When to use: Once you have regular publishing cadence and need to queue posts in advance
sitemap.xml and RSS feed edge functions
sitemap.xml and RSS 2.0 feed for SEO and RSS readers
Create two Supabase Edge Functions:
1. `supabase/functions/sitemap/index.ts` — returns a `text/xml` sitemap.xml listing all published posts:
```
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{posts.map(p => `<url><loc>https://SITE_URL/${p.slug}</loc><lastmod>${p.updated_at}</lastmod></url>`)}
</urlset>
```
Query posts where status='published' using the service-role key. Return with `Content-Type: text/xml`.
2. `supabase/functions/rss/index.ts` — returns RSS 2.0 feed with `<item>` per post including title, link, description (summary), and pubDate.
In vercel.json (or your hosting config), add rewrites so `/sitemap.xml` and `/feed.xml` proxy to these edge function URLs. Both functions must be deployed via the Lovable Cloud → Edge Functions panel.When to use: Before launch — non-negotiable for any SEO-conscious or RSS-readable blog
AI-assisted slug and summary generation
One-click AI-suggested URL slug and meta description from title + body
Add AI-assist to PostEditor.tsx using the Lovable AI connector (Gemini 3 Flash, the default). When the author types a title and at least 200 characters of body_markdown, show an 'AI Suggest' button in the editor toolbar. Clicking it:
1. Calls the Lovable AI panel connector with a prompt: 'Given this blog post title: "[title]" and body: "[first 500 chars]...", suggest (1) a URL slug (lowercase, hyphens, ≤60 chars) and (2) a 155-character meta description summarizing the post. Return JSON: {slug, meta_description}.'
2. Fills the slug input and summary textarea with the suggestions (user can edit before saving).
3. Show a loading spinner in the button during the AI call.
4. Handle errors with a toast: 'AI suggestion failed — enter manually.'
Never auto-save AI suggestions without user confirmation. The button should say 'Suggest slug + summary' and only be enabled after title is non-empty.When to use: Optional — pure writing UX improvement; skip if you prefer manual slugs
Common errors
Real error strings you'll see. Find yours, paste the fix prompt.
new row violates row-level security policy for table "posts"The authenticated user is signed in but their profiles.role is 'author', not 'admin', and the write policy requires is_admin().
Run in Cloud → Database → SQL Editor: `UPDATE profiles SET role = 'admin' WHERE id = (SELECT id FROM auth.users WHERE email = 'your@email.com');` Then sign out and sign back in — the session needs to refresh to pick up the role change.
Manual fix
If the is_admin() function was not created by the migration, run it manually in SQL Editor as shown in the starter prompt above.
draft post visible on public homepageThe RLS SELECT policy on posts was scaffolded with USING (true) — allowing all posts including drafts to be visible to anonymous visitors.
In Cloud → Database → SQL Editor, replace the anon SELECT policy: `DROP POLICY IF EXISTS posts_public_read ON posts; CREATE POLICY posts_public_read ON posts FOR SELECT TO anon, authenticated USING (status = 'published' AND published_at <= now());` Then open an incognito browser and visit / — only published posts should appear.
scheduled post never publishespg_cron extension is not enabled by default in Lovable Cloud, so the scheduled_for column is set but nothing polls it.
In Cloud → Database → SQL Editor, run:
```sql
CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule('publish-scheduled-posts', '* * * * *', $$ UPDATE posts SET status='published', published_at=now() WHERE status='scheduled' AND scheduled_for <= now(); $$);
```
Verify it works by setting a post's scheduled_for to 2 minutes from now and waiting.image upload returns 401 UnauthorizedThe 'post-media' Storage bucket has no write policy for authenticated users — Storage buckets default to no access.
In Cloud → Storage → post-media → Policies, add: - INSERT policy for authenticated: `FOR INSERT TO authenticated WITH CHECK (auth.role() = 'authenticated');` - SELECT policy for anon + authenticated: `FOR SELECT TO anon, authenticated USING (true);` This makes uploaded images publicly readable while restricting upload to signed-in authors.
duplicate key value violates unique constraint "posts_slug_key"Two posts have similar titles so Lovable auto-generated the same slug. The unique constraint on posts.slug enforces uniqueness.
Add a slug-collision-resistant generator: in PostEditor.tsx, when generating slug from title, append a short UUID suffix: `const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50) + '-' + crypto.randomUUID().slice(0, 6);` This makes slug collisions mathematically impossible.
Post page not indexed by Google / ChatGPT or Claude cannot find my postsLovable is a Vite SPA — when Google or LLM crawlers fetch /your-post-slug they receive an empty <div id="root"> with a JS bundle. Googlebot runs JS but deprioritizes it; LLM training crawlers often skip JS-rendered content entirely.
Manual fix
Two options — both are multi-hour projects, not a single prompt: (1) Export to GitHub, migrate the reader view (/, /[slug], /category/[slug]) to Next.js with getStaticProps + ISR, keep the editor (admin/*) in Lovable Cloud. (2) Add vite-plugin-ssg and pre-render every published post URL at build time. The editor experience stays in Lovable either way; only the public reader moves.
Cost reality
What this build actually costs — no surprises on your card.
Recommended Lovable plan
Free is enough for the build — the full prompt chain runs ~30-60 credits, well within the Free tier's ~30 credits/month cap if you run it across 2 days. Pro $25/mo only if you iterate heavily on editor UX or add the AI-assist follow-ups in one session.
Monthly run cost breakdown
~30-60 credits across starter (~25), image upload (~30), autosave (~15), sitemap/RSS (~30), plus 1-2 fix-it rounds total| Item | Cost |
|---|---|
| Lovable Stop iterating after the build is done | Free $0 |
| Supabase A blog stays under 500MB DB for years; even 100K posts with comments is fine | Free $0 |
| Resend Only needed if you add the newsletter follow-up | Free $0 (up to 3K emails/mo) |
| Custom domain Works on lovable.app subdomain without one | ~$12/yr |
| Vercel/Netlify (if SSR migration) Only if you migrate reader view to Next.js for SEO | Free hobby tier or $20/mo Pro |
Scaling notes: Supabase Free 500MB DB handles ~100K posts with comments for years. Storage 1GB Free covers ~5K cover images at 200KB each — always compress before upload. Resend Free (3K/mo) breaks first if you add a newsletter for >3K subscribers — bump to Resend Pro $20/mo.
Production checklist
Steps to take before you share the URL with real users.
Domain & SSL
Connect your custom domain to Lovable
Click Publish (top-right) → Settings → Custom Domain → enter your domain → follow the CNAME/A record instructions in your DNS provider. SSL is provisioned automatically.
SEO / SSR Decision
Decide before launch: do you need Google to index post pages?
If yes: export to GitHub (Settings → Export) and migrate the reader view to Next.js with getStaticProps, or use vite-plugin-ssg to pre-render post URLs at build time. If no (private blog, intranet, MVP): ship the Vite SPA as-is. This decision cannot be reversed with a Lovable prompt — it requires a project architecture change.
Backups
Enable Supabase daily backups
In Lovable Cloud → Database, backups are managed by Supabase. Free tier retains 1 day; Pro $25/mo retains 7 days. For a blog, 1-day is fine at MVP. Export posts as JSON monthly via Cloud → Database → SQL Editor: `SELECT * FROM posts;` → download CSV.
Monitoring
Check Lovable Cloud → Logs for edge function errors
Click + → Cloud tab → Logs. Filter by function name. Set up a Resend notification or Slack webhook in the sitemap/RSS edge function to alert on 5xx errors.
Frequently asked questions
Will Google index my Lovable blog?
Partially. Lovable builds a Vite SPA — when Google fetches /your-post-slug it gets an empty HTML shell with a JavaScript bundle. Googlebot does eventually run JavaScript, but deprioritizes JS-rendered content compared to server-rendered HTML. In practice, very popular posts with many inbound links may get indexed eventually, but a blog whose traffic model depends on Google ranking new posts within hours of publish will struggle. If SEO is important to you, migrate the public reader view (/, /[slug], /category/[slug]) to Next.js or Astro with static generation after your editor is built. The editor and admin routes can stay in Lovable Cloud.
Why are my draft posts showing up on the public homepage?
The most common cause is a too-permissive RLS SELECT policy on the posts table. Lovable's first scaffold often writes USING (true), which makes every post — including drafts — visible to anonymous visitors. Open Cloud → Database → SQL Editor and update the policy to: `USING (status = 'published' AND published_at <= now())`. Verify by opening the homepage in an incognito browser — you should only see published posts.
Can I have multiple authors with different editing permissions?
Yes. The schema includes a profiles.role column (default 'author'). Authors can edit only their own posts (via the author_id = auth.uid() RLS policy). Admins can edit any post (via is_admin()). To add a second admin, run: `UPDATE profiles SET role = 'admin' WHERE id = (SELECT id FROM auth.users WHERE email = 'colleague@email.com');` You can extend this to add an 'editor' role with its own is_editor() function if needed — that's a follow-up prompt.
How do I schedule posts to publish in the future?
Follow the 'Scheduled publish via pg_cron' prompt in the follow-up chain. The key steps are: (1) add a scheduled_for timestamptz column if not already present, (2) run CREATE EXTENSION IF NOT EXISTS pg_cron in the SQL Editor, (3) create a cron job that runs every minute and promotes posts where status='scheduled' AND scheduled_for <= now() to status='published'. Without pg_cron, scheduled_for is just a display field with no automatic promotion.
Should I just use Ghost or Substack instead?
For most people, yes — Ghost at $9/mo includes SSR, newsletters, members, and themes you do not want to rebuild. Substack is free and even simpler. Build this in Lovable when: (1) the blog is a section inside a larger Lovable app (not a standalone site), (2) you need a private or intranet blog where SEO does not matter, (3) you are learning Lovable + Supabase patterns and a blog is a good practice project, or (4) you want your own Postgres for data compliance reasons that prevent using Ghost Pro.
How do I add comments to posts?
Two paths. The quick path: embed Disqus or Giscus (GitHub Discussions-based) — zero Supabase schema change, just add the embed to PostBody.tsx. The owned path: paste the 'Lovable Prompts for Building a Community Forum' starter prompt and link each post to a forum thread. The owned path gives you moderation control and keeps all data in your Supabase instance; the embed path is faster but you lose the data.
Can I migrate my blog to Next.js later without losing the database?
Yes, and this is actually the recommended migration path. The Supabase Postgres database stays exactly where it is — nothing in the DB changes. You export the Lovable project to GitHub (Settings → Export), then port the reader components (PostBody, PostCard, etc.) to a new Next.js project that uses the same Supabase URL and anon key. The editor and admin routes can stay in Lovable or move to the Next.js app. The schema and all data remain untouched.
If your build needs more than a blog editor, what does RapidDev offer?
If your build outgrows this prompt kit and you need custom architecture, RapidDev builds production-grade Lovable apps at $13K-$25K — book a free 30-minute consultation at rapidevelopers.com.
Need a production-grade version?
RapidDev builds production-grade Lovable apps at $13K–$25K. Book a free 30-minute consultation.
Book a free consultation30-min call. No commitment.