Using FireCMS with Next.js
It is perfectly possible to use FireCMS with Next.js. FireCMS is a React library, so you can use it with any React framework.
In the case of next.js, you are restricted to running FireCMS in the client side, as next.js does not support server side rendering of some of the React components used by FireCMS.
Start by creating your next.js project:
npx create-next-app@latest
Then install FireCMS and its dependencies:
yarn add firecms@^2.0.0 firebase@^9 @mui/material@^5 @mui/icons-material@^5 @mui/lab@latest @mui/x-date-pickers@^5.0.0-beta.1 @emotion/react @emotion/styled react-router@^6 react-router-dom@^6
Configuring the App router​
Next.js uses a file based router. In this guide, we will be creating the FireCMS
app in the /cms
route, but you can customize this to your needs.
FireCMS uses react-router, so we need to configure next.js to delegate
all the routes starting with /cms
to FireCMS.
FireCMS does not require tailwind since it relies on Material UI, so you can opt-out of it when creating the project.
In our app
folder, we create a folder called cms
and inside it another one called
[[...any]]
. This will match any route starting with /cms
.
Then create the file cms/[[...any]]/page.tsx
with the following content:
import CMS from "@/app/cms/CMS";
export default function Home() {
return <CMS/>;
}
Creating the CMS​
You can now create your CMS as you would normally do. Create the file cms/CMS.tsx
with the following content:
If you are not running FireCMS in the root path of you app, you need to set the basePath
prop to the path where you
are running it. In this case, we are running it in /cms
.
"use client";
import React, { useCallback } from "react";
import { User as FirebaseUser } from "firebase/auth";
import {
Authenticator,
buildCollection,
buildProperty,
EntityReference,
FirebaseCMSApp
} from "firecms";
import "typeface-rubik";
import "@fontsource/ibm-plex-mono";
// TODO: Replace with your config
const firebaseConfig = {
apiKey: "",
authDomain: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: ""
};
const locales = {
"en-US": "English (United States)",
"es-ES": "Spanish (Spain)",
"de-DE": "German"
};
type Product = {
name: string;
price: number;
status: string;
published: boolean;
related_products: EntityReference[];
main_image: string;
tags: string[];
description: string;
categories: string[];
publisher: {
name: string;
external_id: string;
},
expires_on: Date
}
const localeCollection = buildCollection({
path: "locale",
customId: locales,
name: "Locales",
singularName: "Locales",
properties: {
name: {
name: "Title",
validation: { required: true },
dataType: "string"
},
selectable: {
name: "Selectable",
description: "Is this locale selectable",
dataType: "boolean"
}
}
});
const productsCollection = buildCollection<Product>({
name: "Products",
singularName: "Product",
path: "products",
permissions: ({ authController }) => ({
edit: true,
create: true,
// we have created the roles object in the navigation builder
delete: false
}),
subcollections: [
localeCollection
],
properties: {
name: {
name: "Name",
validation: { required: true },
dataType: "string"
},
price: {
name: "Price",
validation: {
required: true,
requiredMessage: "You must set a price between 0 and 1000",
min: 0,
max: 1000
},
description: "Price with range validation",
dataType: "number"
},
status: {
name: "Status",
validation: { required: true },
dataType: "string",
description: "Should this product be visible in the website",
longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.",
enumValues: {
private: "Private",
public: "Public"
}
},
published: ({ values }) => buildProperty({
name: "Published",
dataType: "boolean",
columnWidth: 100,
disabled: (
values.status === "public"
? false
: {
clearOnDisabled: true,
disabledMessage: "Status must be public in order to enable this the published flag"
}
)
}),
related_products: {
dataType: "array",
name: "Related products",
description: "Reference to self",
of: {
dataType: "reference",
path: "products"
}
},
main_image: buildProperty({ // The `buildProperty` method is a utility function used for type checking
name: "Image",
dataType: "string",
storage: {
storagePath: "images",
acceptedFiles: ["image/*"]
}
}),
tags: {
name: "Tags",
description: "Example of generic array",
validation: { required: true },
dataType: "array",
of: {
dataType: "string"
}
},
description: {
name: "Description",
description: "This is the description of the product",
longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.",
dataType: "string",
columnWidth: 300
},
categories: {
name: "Categories",
validation: { required: true },
dataType: "array",
of: {
dataType: "string",
enumValues: {
electronics: "Electronics",
books: "Books",
furniture: "Furniture",
clothing: "Clothing",
food: "Food"
}
}
},
publisher: {
name: "Publisher",
description: "This is an example of a map property",
dataType: "map",
properties: {
name: {
name: "Name",
dataType: "string"
},
external_id: {
name: "External id",
dataType: "string"
}
}
},
expires_on: {
name: "Expires on",
dataType: "date"
}
}
});
export default function CMS() {
const myAuthenticator: Authenticator<FirebaseUser> = useCallback(async ({
user,
authController
}) => {
if (user?.email?.includes("flanders")) {
throw Error("Stupid Flanders!");
}
console.log("Allowing access to", user?.email);
// This is an example of retrieving async data related to the user
// and storing it in the controller's extra field.
const sampleUserRoles = await Promise.resolve(["admin"]);
authController.setExtra(sampleUserRoles);
return true;
}, []);
return <FirebaseCMSApp
name={"My Online Shop"}
basePath={"/cms"}
authentication={myAuthenticator}
collections={[productsCollection]}
firebaseConfig={firebaseConfig}
/>;
}
Run​
Then simply run:
yarn dev