Build a meta-builder in FlutterFlow where your users can create their own landing pages. Store all sections in a single Firestore document as an ordered Array of section objects (not separate documents) to make reordering a single write. Use ReorderableListView for drag-and-drop section ordering, a BottomSheet editor per section type, a ColorFiltered preview, and a published boolean toggle for one-click deployment.
Let Your Users Build Their Own Pages
A landing page builder inside your FlutterFlow app lets end users create and customize their own promotional pages, product listings, or profile pages without needing a developer. The critical design decision is data modeling: storing each section as a separate Firestore document makes reordering require N writes and creates race conditions. Instead, store all sections as an ordered Array on a single page document — reordering becomes one atomic write, and the entire page loads in a single Firestore read.
Prerequisites
- FlutterFlow Pro plan (Custom Widgets required for drag-and-drop)
- Firebase and Firestore configured in your project
- Authentication set up so pages belong to specific users
- Basic Dart knowledge for Custom Widget implementation
Step-by-step guide
Design the Page Document Schema in Firestore
Design the Page Document Schema in Firestore
Create a 'pages' Firestore collection. Each page document contains: user_id, title (String), slug (String — URL identifier), published (Boolean), created_at, updated_at, and sections (Array of Maps). Each section Map in the Array has: id (UUID), type (String — 'hero', 'text', 'image', 'cta', 'spacer'), order (Integer), and a content Map whose keys vary by type. For a 'hero' section, content has heading, subheading, background_color, and button_text. For 'text', it has body and alignment. For 'image', it has image_url and caption. For 'cta', it has headline, button_text, and button_url. Storing sections as an Array on the parent document means the entire page loads in one read and reordering is one write.
Expected result: Firestore shows a 'pages' collection where each document contains a sections Array with multiple section Maps, each with a type and content object.
Build the Section List with ReorderableListView
Build the Section List with ReorderableListView
Create a Custom Widget called SectionReorderList that wraps Flutter's ReorderableListView. Each item in the list renders a section preview card based on the section type, with a drag handle icon on the right side. When the user finishes dragging, the ReorderableListView's onReorder callback fires with the old and new index. In the callback, create a new List with the section moved to the new position, regenerate the order integers, and write the updated sections Array to Firestore in one update() call. Show a drag-mode indicator bar at the top when dragging is active.
1import 'package:flutter/material.dart';23class SectionReorderList extends StatefulWidget {4 final List<Map<String, dynamic>> sections;5 final ValueChanged<List<Map<String, dynamic>>> onReorder;6 final ValueChanged<Map<String, dynamic>> onEditSection;7 final ValueChanged<String> onDeleteSection;89 const SectionReorderList({10 super.key,11 required this.sections,12 required this.onReorder,13 required this.onEditSection,14 required this.onDeleteSection,15 });1617 @override18 State<SectionReorderList> createState() => _SectionReorderListState();19}2021class _SectionReorderListState extends State<SectionReorderList> {22 late List<Map<String, dynamic>> _sections;2324 @override25 void initState() {26 super.initState();27 _sections = List.from(widget.sections);28 }2930 @override31 Widget build(BuildContext context) {32 return ReorderableListView.builder(33 shrinkWrap: true,34 physics: const NeverScrollableScrollPhysics(),35 itemCount: _sections.length,36 onReorder: (oldIndex, newIndex) {37 setState(() {38 if (newIndex > oldIndex) newIndex--;39 final item = _sections.removeAt(oldIndex);40 _sections.insert(newIndex, item);41 // Reindex order field42 for (int i = 0; i < _sections.length; i++) {43 _sections[i] = {..._sections[i], 'order': i};44 }45 });46 widget.onReorder(_sections);47 },48 itemBuilder: (context, index) {49 final section = _sections[index];50 return SectionPreviewCard(51 key: ValueKey(section['id']),52 section: section,53 onEdit: () => widget.onEditSection(section),54 onDelete: () => widget.onDeleteSection(section['id'] as String),55 );56 },57 );58 }59}Expected result: Long-pressing and dragging a section card moves it to a new position in the list. Releasing saves the new order to Firestore as a single write.
Create BottomSheet Editors for Each Section Type
Create BottomSheet Editors for Each Section Type
When a user taps the Edit button on a section card, show a BottomSheet with the appropriate editor for that section type. Use a switch on section type to determine which editor to show. The Hero BottomSheet contains a TextField for heading, a TextField for subheading, a ColorPicker for background color, and a TextField for button text. The Text BottomSheet has a multiline TextField and an alignment selector. The Image BottomSheet has an image upload button and a TextField for caption. Each BottomSheet has Save and Cancel buttons. On Save, find the section by its id in the sections Array, replace it with the updated version, and write the whole sections Array back to Firestore.
1import 'package:cloud_firestore/cloud_firestore.dart';23Future<void> updateSection(4 String pageId,5 String sectionId,6 Map<String, dynamic> updatedContent,7) async {8 final docRef = FirebaseFirestore.instance.collection('pages').doc(pageId);910 await FirebaseFirestore.instance.runTransaction((tx) async {11 final snap = await tx.get(docRef);12 final sections = List<Map<String, dynamic>>.from(13 (snap.data()?['sections'] as List? ?? []).map((s) => Map<String, dynamic>.from(s as Map)),14 );1516 final index = sections.indexWhere((s) => s['id'] == sectionId);17 if (index == -1) return;1819 sections[index] = {20 ...sections[index],21 'content': updatedContent,22 'updated_at': DateTime.now().millisecondsSinceEpoch,23 };2425 tx.update(docRef, {26 'sections': sections,27 'updated_at': FieldValue.serverTimestamp(),28 });29 });30}Expected result: Tapping Edit on a Hero section opens a bottom sheet with the current heading and subheading pre-populated. Saving updates only that section in the Firestore document.
Add New Sections with a Section Type Picker
Add New Sections with a Section Type Picker
Add an Add Section button at the bottom of the sections list. Tapping it shows a BottomSheet with a grid of section type options: Hero, Text, Image, and CTA — each with an icon and label. Tapping a type creates a new section Map with default content for that type, assigns a UUID as the id, sets order to sections.length, and appends it to the sections Array using FieldValue.arrayUnion(). Show a brief animation (AnimatedList or FadeTransition) as the new section appears. New sections always appear at the bottom and can be dragged to the desired position.
Expected result: Tapping Add Section shows a type picker. Choosing Hero adds a new hero section card at the bottom of the list with placeholder text visible.
Implement the Publish Toggle and Live Preview
Implement the Publish Toggle and Live Preview
Add a Publish toggle switch at the top of the page editor, linked to the page document's published Boolean field. When toggled on, update published to true and set published_at to the current timestamp. Your public-facing page renderer reads the page by slug and checks published — unpublished pages show a 404 or a preview-only message. Add a Preview button that opens the page in a WebView or a Flutter route that renders the sections as real UI (not just the editor cards). The preview renders each section type using its actual content: a full-width Container with color for Hero, a Text widget for Text sections, and an Image.network for Image sections.
1import 'package:cloud_firestore/cloud_firestore.dart';23Future<void> togglePublish(String pageId, bool publish) async {4 final update = <String, dynamic>{5 'published': publish,6 'updated_at': FieldValue.serverTimestamp(),7 };89 if (publish) {10 update['published_at'] = FieldValue.serverTimestamp();11 }1213 await FirebaseFirestore.instance14 .collection('pages')15 .doc(pageId)16 .update(update);17}Expected result: Toggling Published on makes the page accessible at its slug URL. Toggling off removes it from public access. The toggle state reflects the current Firestore value instantly via the real-time listener.
Complete working example
1// Complete landing page builder service for FlutterFlow2import 'package:cloud_firestore/cloud_firestore.dart';3import 'package:firebase_auth/firebase_auth.dart';4import 'package:uuid/uuid.dart';56const _uuid = Uuid();78class PageBuilderService {9 final _db = FirebaseFirestore.instance;10 String? get _uid => FirebaseAuth.instance.currentUser?.uid;1112 // ─── Create a new blank page ──────────────────────────────────────────────1314 Future<String?> createPage(String title, String slug) async {15 if (_uid == null) return null;1617 final docRef = await _db.collection('pages').add({18 'user_id': _uid,19 'title': title,20 'slug': slug.toLowerCase().replaceAll(' ', '-'),21 'published': false,22 'sections': [23 _defaultSection('hero'),24 ],25 'created_at': FieldValue.serverTimestamp(),26 'updated_at': FieldValue.serverTimestamp(),27 });28 return docRef.id;29 }3031 // ─── Add a section ────────────────────────────────────────────────────────3233 Future<void> addSection(String pageId, String type) async {34 final section = _defaultSection(type);35 await _db.collection('pages').doc(pageId).update({36 'sections': FieldValue.arrayUnion([section]),37 'updated_at': FieldValue.serverTimestamp(),38 });39 }4041 // ─── Reorder sections (one atomic write) ─────────────────────────────────4243 Future<void> reorderSections(44 String pageId,45 List<Map<String, dynamic>> reorderedSections,46 ) async {47 await _db.collection('pages').doc(pageId).update({48 'sections': reorderedSections,49 'updated_at': FieldValue.serverTimestamp(),50 });51 }5253 // ─── Update a section by ID ──────────────────────────────────────────────5455 Future<void> updateSection(56 String pageId,57 String sectionId,58 Map<String, dynamic> newContent,59 ) async {60 final ref = _db.collection('pages').doc(pageId);61 await _db.runTransaction((tx) async {62 final snap = await tx.get(ref);63 final sections = _parseSections(snap.data()?['sections']);64 final idx = sections.indexWhere((s) => s['id'] == sectionId);65 if (idx == -1) return;66 sections[idx] = {...sections[idx], 'content': newContent};67 tx.update(ref, {'sections': sections, 'updated_at': FieldValue.serverTimestamp()});68 });69 }7071 // ─── Delete a section ────────────────────────────────────────────────────7273 Future<void> deleteSection(String pageId, String sectionId) async {74 final ref = _db.collection('pages').doc(pageId);75 await _db.runTransaction((tx) async {76 final snap = await tx.get(ref);77 final sections = _parseSections(snap.data()?['sections'])78 .where((s) => s['id'] != sectionId).toList();79 tx.update(ref, {'sections': sections, 'updated_at': FieldValue.serverTimestamp()});80 });81 }8283 // ─── Publish / Unpublish ──────────────────────────────────────────────────8485 Future<void> setPublished(String pageId, bool published) async {86 await _db.collection('pages').doc(pageId).update({87 'published': published,88 if (published) 'published_at': FieldValue.serverTimestamp(),89 'updated_at': FieldValue.serverTimestamp(),90 });91 }9293 // ─── Helpers ──────────────────────────────────────────────────────────────9495 Map<String, dynamic> _defaultSection(String type) {96 final defaults = <String, Map<String, dynamic>>{97 'hero': {'heading': 'Your Headline Here', 'subheading': 'Supporting text', 'background_color': '#4F46E5', 'button_text': 'Get Started'},98 'text': {'body': 'Add your paragraph text here.', 'alignment': 'left'},99 'image': {'image_url': '', 'caption': ''},100 'cta': {'headline': 'Ready to get started?', 'button_text': 'Sign Up Now', 'button_url': ''},101 };102 return {'id': _uuid.v4(), 'type': type, 'order': 0, 'content': defaults[type] ?? {}};103 }104105 List<Map<String, dynamic>> _parseSections(dynamic raw) =>106 ((raw as List?) ?? []).map((s) => Map<String, dynamic>.from(s as Map)).toList();107}Common mistakes when creating a Custom Landing Page Builder Within FlutterFlow
Why it's a problem: Storing each section as a separate Firestore document instead of an Array
How to avoid: Store all sections as an ordered Array in a single page document. Reordering is one atomic array replacement. Loading is one document read. The 1MB document limit supports hundreds of rich text sections before becoming a concern.
Why it's a problem: Not using a transaction when updating a single section in the Array
How to avoid: Wrap all Array-modify-and-write operations in a Firestore transaction. The transaction retries automatically on contention and guarantees the read and write are atomic.
Why it's a problem: Updating the published URL without checking for slug uniqueness
How to avoid: Before saving a new slug, query Firestore for existing pages with that slug and show a validation error if one exists. Consider scoping slugs by user ID in the URL: /page/{userId}/{slug}.
Best practices
- Always include a unique id field inside each section Map — you need it to identify which section to update without relying on fragile Array index positions.
- Cap the maximum number of sections per page (e.g., 20) to prevent the document from approaching the 1MB Firestore limit.
- Add a lastSaved indicator showing when the page was last saved, so users know their changes are persisted.
- Implement autosave: debounce content changes by 2 seconds and save automatically rather than requiring users to tap a Save button.
- Store a screenshot or thumbnail of the published page in Firebase Storage for the page list view — generating it client-side with RepaintBoundary is the same technique as the photo editor guide.
- Add a duplicate page feature: copy the entire page document with a new ID and a modified slug suffix (-copy) for fast page cloning.
- Gate the publish action behind a completion check: validate that required fields (heading, at least one section) are populated before allowing publish.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I am building a landing page builder feature in FlutterFlow where end users create their own pages. Show me the correct Firestore data model storing sections as an ordered Array, how to implement drag-to-reorder with ReorderableListView in a Custom Widget, how to update a single section in a Firestore Array using a transaction, and how to implement a publish toggle. Include Dart code for all components.
In my FlutterFlow app, create a Custom Action called updatePageSection(pageId, sectionId, newContent) that reads the current sections Array from the Firestore pages collection, finds the section with the matching sectionId, replaces its content map with newContent, and writes the updated sections Array back to Firestore using a transaction. Also create addPageSection(pageId, sectionType) that appends a default section Map to the Array using FieldValue.arrayUnion.
Frequently asked questions
What is the maximum number of sections I can store in a single Firestore document?
Firestore documents have a 1MB size limit. A typical section Map with text content is 500 bytes to 2KB. This means you can store 500-2,000 sections before hitting the limit. For a landing page builder, capping at 20-30 sections per page is a reasonable UX limit that keeps the document well within bounds even with image URLs and rich text.
How do I give pages a public URL that anyone can view without logging in?
Create a PublicPageView route in FlutterFlow that accepts a slug as a URL parameter. This page uses a Firestore query with no authentication requirement to fetch the page by slug (where published == true). Configure Firestore Security Rules to allow unauthenticated reads of published page documents. In FlutterFlow, set this page as accessible without login in the authentication settings.
Can I add video sections to the landing page builder?
Yes. Add 'video' as a section type with a video_url field in the content Map. In the section renderer, detect the YouTube or Vimeo URL format and use the youtube_player_flutter or webview_flutter package to embed the video. Add a poster_image_url field for the thumbnail shown before the video plays.
How do I let users preview their page before publishing?
Create a PagePreviewRoute that accepts the pageId and renders all sections using the same rendering logic as the public page view, but fetches unpublished pages and shows a 'Preview Mode' banner. In FlutterFlow, add a Preview button on the editor page that navigates to this route with the current pageId. This shows the user exactly what their published page will look like.
What happens to existing pages if I add a new section type later?
Existing pages simply do not have any sections of the new type — they are unaffected. New section types only appear when users add them. In your section renderer, add a default case that shows a simple 'Unsupported section type' placeholder for any unknown types, so old pages do not break if you ever remove a section type.
How do I implement undo/redo for section edits?
Maintain a Page State variable editHistory as a List of complete sections Array snapshots (the whole Array, not diffs). Before each edit, push the current state to the history stack. Undo pops the last snapshot and writes it back to Firestore. Limit the history to 10-20 snapshots to control memory usage. This simple snapshot-based undo works for landing page editors where sections change infrequently.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation