Build a CMS-driven landing page in FlutterFlow by storing section content in Firestore landing_sections and landing_settings collections. A Backend Query fetches sections ordered by a numeric order field. Reusable Section Components render Hero, Features, Testimonials, and CTA sections dynamically. An admin page lets you edit all copy without touching the FlutterFlow editor or republishing the app.
CMS-Driven Landing Page Without Republishing
A hardcoded landing page means every copy change — a new headline, an updated price, a fresh testimonial — requires opening FlutterFlow, editing widget properties, and republishing the app. For a web app this takes minutes; for mobile it takes App Store review time. This tutorial solves the problem by storing all landing page content in Firestore. The landing page fetches sections on load, renders them using a set of reusable Section Components (Hero, Features, Testimonials, CTA), and reflects content updates immediately. An admin page with TextFields and Toggle widgets lets you or your marketing team update any text without touching FlutterFlow.
Prerequisites
- FlutterFlow account with Firebase connected and Firestore enabled
- Firebase Authentication enabled (for admin page access control)
- Basic familiarity with FlutterFlow Components and Backend Queries
- An existing FlutterFlow project to add the landing page to
Step-by-step guide
Design the Firestore Schema for Landing Page Sections
Design the Firestore Schema for Landing Page Sections
In Firestore, create two collections. The first is landing_sections with fields: sectionType (String — 'hero', 'features', 'testimonials', 'cta'), order (Integer — controls display sequence), isActive (Boolean — toggle sections on/off), headline (String), subheadline (String), bodyText (String), ctaLabel (String), ctaUrl (String), imageUrl (String), items (Array of Maps — for features list or testimonials). The second collection is landing_settings with a single document (ID: config) containing: appName (String), primaryColor (String hex), logoUrl (String), footerText (String), contactEmail (String). In FlutterFlow's Firestore panel, import both schemas and create Document Types. Create three initial landing_sections documents with sectionType hero (order 1), features (order 2), and cta (order 3).
Expected result: Two Firestore collections are created with sample data. FlutterFlow shows the Document Types in its Firestore panel.
Build Reusable Section Components
Build Reusable Section Components
In FlutterFlow, open the Components panel and create four components. The first component is HeroSection — add a full-width Column with a large Text widget bound to the headline parameter, a smaller Text for subheadline, a body Text, and a Button bound to ctaLabel whose On Tap action navigates to ctaUrl. The second component is FeaturesSection — add a headline Text and a GridView with 2 columns. Inside the GridView, show each item's icon (from items[].icon), title (items[].title), and description (items[].description). The third component is TestimonialsSection — a horizontal ListView of cards each showing items[].quote and items[].author. The fourth component is CTASection — a gradient background Column with headline, bodyText, and a prominent Button. Each component accepts the full landing_sections Document as a parameter.
Expected result: Four components appear in the FlutterFlow Components panel, each rendering correctly with test data.
Build the Landing Page with Dynamic Section Rendering
Build the Landing Page with Dynamic Section Rendering
Create a new page called LandingPage. Add a Backend Query at the page level that queries the landing_sections collection ordered by the order field (ascending) with an isActive equals true filter. This ensures sections appear in the correct order and deactivated sections are hidden. Add a SingleChildScrollView containing a Column. Inside the Column, add a ListView or use Generate Dynamic Children to iterate over the query results. For each section, use a Conditional Builder (or if-else action) based on the sectionType field to render the appropriate component: if sectionType equals 'hero' show HeroSection, if 'features' show FeaturesSection, if 'testimonials' show TestimonialsSection, if 'cta' show CTASection. Pass the full section document to the component.
Expected result: The landing page renders all active Firestore sections in the correct order. Changing Firestore data updates the page on refresh.
Create the Admin Editor for Content Updates
Create the Admin Editor for Content Updates
Create a new page called AdminEditor. Protect it with an On Page Load action that checks if currentUser.email is in an admin allowlist stored in landing_settings — if not, navigate back to Home. Add a Backend Query streaming all landing_sections documents. Display them in a ListView. Each item shows the sectionType, order number, and an isActive Toggle. Tapping a section opens a Bottom Sheet with TextFields pre-filled with the section's headline, subheadline, bodyText, ctaLabel, ctaUrl, and imageUrl values. The Save button fires a Firestore Update Document action. Add a New Section button at the top that opens a creation dialog where the admin picks a sectionType, sets the order, and writes a new document.
Expected result: The admin page shows all sections with live toggles. Editing a section and saving immediately updates the live landing page for all users.
Add Section Reordering with Drag-and-Drop
Add Section Reordering with Drag-and-Drop
To make the admin page more powerful, add drag-and-drop section reordering. Create a Custom Widget named ReorderableSectionList using Flutter's built-in ReorderableListView widget. The widget accepts a list of section documents and an onReorder callback. When the user drags sections to reorder them, onReorder fires with the old and new indices. In the callback, calculate new order values and write them back to Firestore using a batch update — update each moved section's order field. In FlutterFlow, place the ReorderableSectionList Custom Widget on the AdminEditor page replacing the standard ListView.
1// Custom Widget: ReorderableSectionList2// Accepts: sections (List<dynamic>), onReorder callback3import 'package:flutter/material.dart';45class ReorderableSectionList extends StatefulWidget {6 final List<dynamic> sections;7 final Future<dynamic> Function(int oldIndex, int newIndex) onReorder;8 final double width;9 final double height;1011 const ReorderableSectionList({12 Key? key,13 required this.sections,14 required this.onReorder,15 required this.width,16 required this.height,17 }) : super(key: key);1819 @override20 State<ReorderableSectionList> createState() => _State();21}2223class _State extends State<ReorderableSectionList> {24 late List<dynamic> _sections;2526 @override27 void initState() {28 super.initState();29 _sections = List.from(widget.sections);30 }3132 @override33 Widget build(BuildContext context) {34 return SizedBox(35 width: widget.width,36 height: widget.height,37 child: ReorderableListView(38 onReorder: (oldIndex, newIndex) {39 setState(() {40 if (newIndex > oldIndex) newIndex--;41 final item = _sections.removeAt(oldIndex);42 _sections.insert(newIndex, item);43 });44 widget.onReorder(oldIndex, newIndex);45 },46 children: _sections.map((s) {47 final data = s.data() as Map<String, dynamic>? ?? {};48 return ListTile(49 key: ValueKey(s.id),50 leading: const Icon(Icons.drag_handle),51 title: Text(data['sectionType'] ?? 'Unknown'),52 subtitle: Text('Order: ${data['order'] ?? 0}'),53 );54 }).toList(),55 ),56 );57 }58}Expected result: The admin can drag sections up and down in the list. Releasing the drag saves new order values to Firestore and the landing page updates immediately.
Complete working example
1// Custom Widget: ReorderableSectionList for FlutterFlow Admin Editor2// Allows drag-and-drop reordering of landing page sections34import 'package:flutter/material.dart';56class ReorderableSectionList extends StatefulWidget {7 final List<dynamic> sections;8 final Future<dynamic> Function(int oldIndex, int newIndex) onReorder;9 final Future<dynamic> Function(String sectionId) onToggleActive;10 final double width;11 final double height;1213 const ReorderableSectionList({14 Key? key,15 required this.sections,16 required this.onReorder,17 required this.onToggleActive,18 required this.width,19 required this.height,20 }) : super(key: key);2122 @override23 State<ReorderableSectionList> createState() => _ReorderableSectionListState();24}2526class _ReorderableSectionListState extends State<ReorderableSectionList> {27 late List<dynamic> _sections;2829 @override30 void initState() {31 super.initState();32 _sections = List.from(widget.sections);33 }3435 @override36 void didUpdateWidget(covariant ReorderableSectionList oldWidget) {37 super.didUpdateWidget(oldWidget);38 if (widget.sections != oldWidget.sections) {39 setState(() => _sections = List.from(widget.sections));40 }41 }4243 Color _typeColor(String type) {44 switch (type) {45 case 'hero': return Colors.purple.shade100;46 case 'features': return Colors.blue.shade100;47 case 'testimonials': return Colors.green.shade100;48 case 'cta': return Colors.orange.shade100;49 default: return Colors.grey.shade100;50 }51 }5253 @override54 Widget build(BuildContext context) {55 return SizedBox(56 width: widget.width,57 height: widget.height,58 child: ReorderableListView(59 padding: const EdgeInsets.symmetric(vertical: 8),60 onReorder: (int oldIndex, int newIndex) {61 setState(() {62 if (newIndex > oldIndex) newIndex--;63 final item = _sections.removeAt(oldIndex);64 _sections.insert(newIndex, item);65 });66 widget.onReorder(oldIndex, newIndex);67 },68 children: List.generate(_sections.length, (i) {69 final s = _sections[i];70 final data = s.data() as Map<String, dynamic>? ?? {};71 final type = data['sectionType'] as String? ?? 'unknown';72 final isActive = data['isActive'] as bool? ?? true;73 return Card(74 key: ValueKey(s.id),75 color: _typeColor(type),76 margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),77 child: ListTile(78 leading: const Icon(Icons.drag_handle, color: Colors.grey),79 title: Text(80 type.toUpperCase(),81 style: const TextStyle(fontWeight: FontWeight.bold),82 ),83 subtitle: Text(data['headline'] ?? '(no headline)'),84 trailing: Switch(85 value: isActive,86 onChanged: (_) => widget.onToggleActive(s.id),87 ),88 ),89 );90 }),91 ),92 );93 }94}Common mistakes when creating a Custom Landing Page with Dynamic Content in FlutterFlow
Why it's a problem: Hardcoding all landing page text directly in FlutterFlow widget properties
How to avoid: Store all content strings in Firestore landing_sections documents. Bind Text widget values to Firestore fields. Updates take effect immediately for all users with no republish required.
Why it's a problem: Creating a separate Firestore document for every individual text string on the page
How to avoid: Group all text for a section (headline, subheadline, body, CTA) into a single Firestore document. One document read provides all the text for an entire section.
Why it's a problem: Not protecting the admin editor page with an authentication and role check
How to avoid: Add an On Page Load action that checks currentUser.email against an admin allowlist in landing_settings, or check for an admin custom claim. Redirect non-admins to the home page immediately.
Best practices
- Use integer order values with gaps of 10 so you can insert new sections between existing ones without renumbering.
- Add an isActive toggle to each section so you can stage content and activate it on launch day without deleting and recreating documents.
- Cache landing_settings in App State on app start since this document changes rarely and avoids a Firestore read on every page visit.
- Build each section type as a separate FlutterFlow Component rather than inline widgets — components are reusable across multiple pages.
- Add a createdAt and updatedAt timestamp to each landing_sections document for audit purposes.
- Store image URLs in Firestore pointing to Firebase Storage — never hardcode image asset paths in Firestore since Storage URLs can change.
- For A/B testing, add an abVariant field to sections and show different variants to different users based on their userId hash.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a landing page in FlutterFlow where all the text content is stored in Firestore so my marketing team can update it without republishing the app. I have a landing_sections collection with fields: sectionType, order, headline, subheadline, ctaLabel, ctaUrl, and isActive. How do I build a FlutterFlow page that fetches all active sections ordered by the order field and conditionally renders different FlutterFlow Components based on the sectionType value?
Create a FlutterFlow Backend Query on a page that fetches documents from the landing_sections Firestore collection, filters by isActive equals true, and orders by the order field ascending. Then show me how to use a Conditional Builder to render different Components based on the sectionType field value.
Frequently asked questions
How long does it take for Firestore content changes to appear on the landing page?
If you use a streaming Backend Query (the default in FlutterFlow), changes appear within 1-2 seconds in real time without the user refreshing. If you use a one-time Backend Query, changes appear the next time the user navigates to the page.
Can I add new section types without updating the FlutterFlow app?
No — adding a new section type requires building a new Component in FlutterFlow and republishing. What the CMS approach avoids is changing existing content (text, images, URLs). If you want fully dynamic section types, consider a Custom Widget that renders section content from a JSON schema.
How do I handle images in the landing sections?
Store image URLs in the imageUrl field of each landing_sections document. Upload images to Firebase Storage, get the download URL, and paste it into Firestore. The Image widget in your Section Component is bound to this field. To update an image, upload a new file to Storage, get the new URL, and update the Firestore document — no republish needed.
Can my marketing team update content without a FlutterFlow or Firebase account?
Yes — that is the purpose of the admin editor page built in Step 4. The admin page is part of your FlutterFlow app and is protected by Firebase Authentication. Give your marketing team an account with admin role, and they can log in and edit content through the admin page without any Firebase Console access.
How do I add a features list with icons to the FeaturesSection component?
Store the features as an Array of Maps in the items field of the Firestore document. Each map has fields: icon (String — icon name or emoji), title (String), and description (String). In the FeaturesSection Component, add a ListView or GridView bound to the items array and display each item's fields.
Is this approach suitable for SEO if I am building a web app?
FlutterFlow web apps use Flutter's web rendering which is not ideal for SEO since content is rendered client-side. For maximum SEO, consider using a static section for the above-the-fold hero content and loading dynamic content below. For apps where SEO is the primary concern, a Next.js or Webflow solution may be more appropriate.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation