Template Bakery
A production-ready, full-stack bakery & restaurant management system built with Next.js 16, React 19, MongoDB, and Cloudinary. Includes a polished public website and a powerful admin dashboard.
Overview
Template Bakery is a complete, full-stack web application built specifically for bakeries, restaurants, and food businesses. It ships with everything you need out of the box:
- A beautifully designed public website with homepage sections, a product/menu showcase, an outlet/location listing, and a complete blog system.
- A feature-rich Admin Dashboard for managing products, categories, banners, testimonials, blog posts, outlets, subscribers, and site-wide settings — all without touching any code.
- A robust REST API layer with over 50 endpoints, JWT-based authentication, Zod validation, and a consistent response format.
- Cloudinary integration for image and media management, configured entirely from the admin panel.
- MongoDB as the database with Mongoose ODM, optimized with compound indexes and aggregation pipelines.
Features
Public Website
Multiple Homepages
Two distinct homepage layout variants to choose from, with dynamic sections.
Product / Menu Showcase
Dynamic menu with category and sub-category filtering, popular items, and featured sections.
Outlet Locations
Multi-branch listing with business hours, phone, and Google Maps links.
Full Blog System
Blog categories, related posts, infinite scroll, tags, and featured article flags.
Newsletter
Built-in newsletter subscription form with subscriber management in the admin.
Testimonials / Reviews
Customer testimonials with ratings, drag-and-drop ordering, and status control.
SEO Ready
Configurable metadata, Open Graph image, keywords, and page titles from the admin panel.
Dark Mode
Full dark mode support powered by next-themes, with smooth transitions.
Admin Dashboard
Secure Authentication
JWT-based admin login with bcrypt password hashing and 30-day session tokens.
Rich Text Editor
Blog content powered by SunEditor — a full-featured WYSIWYG HTML editor.
Drag-and-Drop Ordering
Reorder banners, categories, testimonials, and more with drag-and-drop (dnd-kit).
Image Upload
Direct Cloudinary uploads from every form — no manual image management needed.
Settings Panel
Configure general info, social links, SEO metadata, page banners, Cloudinary credentials, and T&C from one place.
Dashboard Statistics
At-a-glance stats: total products, blogs, outlets, and subscribers.
Technology Stack
Requirements
| Requirement | Minimum Version | Notes |
|---|---|---|
| Node.js | 20.x or later | LTS recommended |
| pnpm | 8.x or later | npm install -g pnpm |
| MongoDB | 6.0 or later | Local or MongoDB Atlas (free tier) |
| Cloudinary Account | Any | Free tier is sufficient |
Installation & Setup
Step 1 — Extract Files
unzip template-bakery.zip -d template-bakery
cd template-bakery
Step 2 — Install Dependencies
# Install pnpm globally (if not already installed)
npm install -g pnpm
# Install all project dependencies
pnpm install
Step 3 — Configure Environment Variables
cp .env.example .env.local
# Then edit .env.local with your actual values
See the Environment Variables section for all required values.
Step 4 — Seed the Database (Optional)
pnpm seed
This creates the initial admin user and populates default settings.
Step 5 — Start Development Server
pnpm dev
Open http://localhost:3000 in your browser. The admin dashboard is at http://localhost:3000/admin/login. The interactive documentation is at http://localhost:3000/documentation.
Environment Variables
Create a .env.local file in the project root with the following variables:
# ─── Database ──────────────────────────────────────────────────
# MongoDB connection string (required)
MONGODB_URI=mongodb+srv://<user>:<password>@cluster.mongodb.net/template-bakery
# ─── Authentication ────────────────────────────────────────────
# Secret key for signing NextAuth JWT tokens (required)
# Generate: openssl rand -base64 32
NEXTAUTH_SECRET=your_strong_random_secret
# ─── Application URLs ──────────────────────────────────────────
# Base URL for internal API calls (server-side fetches)
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# Public application URL (for production absolute links)
NEXT_PUBLIC_APP_URL=https://your-domain.com
# ─── Cloudinary ────────────────────────────────────────────────
# Default upload folder name
CLOUDINARY_FOLDER=uploads
# Cloudinary secure base URL
CLOUDINARY_SECURE_URL_BASE=https://res.cloudinary.com/your-cloud-name
cloud_name, api_key, and api_secret are stored and managed through Admin Dashboard → Settings → Cloudinary (saved in the database). Only the folder name and base URL need to be set as environment variables.
.env.local to version control. Add it to .gitignore.
Running the Application
| Command | Description |
|---|---|
pnpm dev | Start development server with Turbopack at localhost:3000 |
pnpm build | Create an optimized production build |
pnpm start | Serve the production build |
pnpm lint | Run ESLint code quality checks |
Project Structure
Pages & Routes
Public Pages
| Route | Description |
|---|---|
/ | Homepage — Layout 1 with hero, featured products, testimonials |
/home2 | Homepage — Layout 2 (alternate design) |
/menu | Full product/menu listing with category filter |
/outlets | All outlet/branch locations with business hours |
/all-blogs | Blog listing with category filter and infinite scroll |
/blog-list | Alternate blog list view |
/blog-details/[slug] | Individual blog post page |
/admin/login | Admin login page |
/documentation | Built-in interactive documentation |
Admin Dashboard Pages (Protected)
All admin routes are under /admin/dashboard/ and require a valid authentication session.
| Route | Description |
|---|---|
/admin/dashboard | Overview statistics |
/admin/dashboard/banner | Manage hero banners with drag-and-drop order |
/admin/dashboard/blogs | Blog list (paginated) |
/admin/dashboard/blogs/create | Create new blog post with rich text editor |
/admin/dashboard/blogs/edit/[id] | Edit existing blog post |
/admin/dashboard/blog-category | Manage blog categories |
/admin/dashboard/categories | Product category list |
/admin/dashboard/categories/create | Create product category |
/admin/dashboard/categories/edit/[slug] | Edit product category |
/admin/dashboard/products | Product/menu item management |
/admin/dashboard/outlets | Outlet list with reorder |
/admin/dashboard/outlets/create | Create new outlet |
/admin/dashboard/outlets/edit/[id] | Edit outlet details |
/admin/dashboard/testimonial | Customer testimonials |
/admin/dashboard/subscribe | Newsletter subscriber list |
/admin/dashboard/client-showcase | Client showcase section |
/admin/dashboard/video-showcase | Video showcase section |
/admin/dashboard/settings | All site settings |
API Reference — Public Endpoints
All API responses follow a consistent envelope format:
// Success
{ "success": true, "statusCode": 200, "message": "...", "data": { ... } }
// Error
{ "success": false, "statusCode": 400, "message": "...", "errors": [ ... ] }
Blogs
Returns all published blog posts with pagination.
Query Parameters: page (default: 1), limit (default: 10)
Returns a single blog post by its unique slug.
Filters and searches blog posts.
{ "category": "recipes", "search": "chocolate", "page": 1, "limit": 10 }
Returns related blog posts for the given slug.
Returns all active blog categories.
Products & Categories
Returns all active products. Supports category, subcategory, page, limit query params.
Returns all active product categories.
Other Public Endpoints
Returns all active banners ordered by position.
Returns all active testimonials ordered by position.
Returns all active outlet locations.
Subscribe a user to the newsletter.
{ "email": "user@example.com", "name": "Jane Doe" }
Returns public site settings (company info, social links, business hours).
Returns a specific home page section by its slug identifier.
API Reference — Admin Authentication
Authorization: Bearer <token>
Authenticates an admin user and returns a JWT token.
// Request
{ "email": "admin@example.com", "password": "your_password" }
// Response
{
"success": true,
"data": {
"token": "eyJhbGci...",
"user": { "name": "Admin", "email": "admin@example.com", "role": "admin" }
}
}
Returns the currently authenticated admin's profile. Requires Bearer token.
Changes the admin password. Requires Bearer token.
{ "currentPassword": "old_password", "newPassword": "new_password" }
API Reference — Banners
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/admin/banner | Create a new banner |
| GET | /api/admin/banner | List all banners |
| PUT | /api/admin/banner/[id] | Update banner by ID |
| DELETE | /api/admin/banner/[id] | Delete banner by ID |
| PATCH | /api/admin/banner/status/[id] | Toggle active status |
| POST | /api/admin/banner/sort | Reorder banners |
// Create Banner — Request Body
{
"title": "Welcome to Our Bakery",
"description": "Fresh baked goods daily",
"image": "https://res.cloudinary.com/...",
"linkUrl": "/menu",
"isActive": true
}
// Sort Banners — Request Body
{ "items": [{ "id": "banner_id", "position": 1 }, ...] }
API Reference — Blogs
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/admin/blog | Create a blog post |
| GET | /api/admin/blog | List all blogs (paginated) |
| PUT | /api/admin/blog/[id] | Update blog post |
| PATCH | /api/admin/blog/status/[id] | Toggle blog status |
// Create Blog — Request Body
{
"title": "Our New Summer Menu",
"subtitle": "Refreshing treats for the season",
"content": "<p>Rich HTML content from SunEditor...</p>",
"image": "https://res.cloudinary.com/...",
"category": "category_object_id",
"tags": ["summer", "seasonal"],
"isFeatured": false,
"isShowHomepage": true,
"status": "published"
}
Status values: pending | draft | published | archived
Blog category endpoints follow the same pattern under /api/admin/blog-category.
API Reference — Product Categories
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/admin/category | Create category |
| GET | /api/admin/category | List all categories |
| PUT | /api/admin/category/[slug] | Update category |
| PATCH | /api/admin/category/status/[id] | Toggle status |
| POST | /api/admin/category/sort | Reorder categories |
// Create Category — Request Body
{
"name": "Cakes",
"description": "All types of cakes",
"image": "https://res.cloudinary.com/...",
"parent": null // ObjectId for sub-category, null for root
}
API Reference — Products
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/admin/product | Create a product |
| GET | /api/admin/product | List all products |
| PUT | /api/admin/product/[id] | Update product |
// Create Product — Request Body
{
"name": "Chocolate Fudge Cake",
"description": "Rich chocolate cake with fudge frosting",
"image": "https://res.cloudinary.com/...",
"category": "category_object_id",
"subcategory": "subcategory_object_id",
"price": 29.99,
"isPopular": true,
"isFeatured": false,
"status": "active"
}
API Reference — Outlets
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/admin/outlet | Create outlet |
| GET | /api/admin/outlet | List all outlets |
| PUT | /api/admin/outlet/[id] | Update outlet |
| PATCH | /api/admin/outlet/status/[id] | Toggle status |
| POST | /api/admin/outlet/sort | Reorder outlets |
// Create Outlet — Request Body
{
"name": "Main Street Bakery",
"address": "123 Main Street, Springfield",
"phone": "1234567890",
"dialCode": "+1",
"image": "https://res.cloudinary.com/...",
"mapLink": "https://maps.google.com/?q=...",
"isShowHomepage": true,
"businessHours": [
{ "day": "Monday", "open": "08:00", "close": "20:00", "isClosed": false },
{ "day": "Sunday", "open": "", "close": "", "isClosed": true }
]
}
API Reference — Testimonials
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/admin/testimonial | Create testimonial |
| GET | /api/admin/testimonial | List all testimonials |
| PUT | /api/admin/testimonial/[id] | Update testimonial |
| PATCH | /api/admin/testimonial/status/[id] | Toggle status |
| POST | /api/admin/testimonial/sort | Reorder testimonials |
// Create Testimonial — Request Body
{
"name": "Sarah Johnson",
"review": "Absolutely the best bakery in town!",
"rating": 5,
"image": "https://res.cloudinary.com/...",
"authorRole": "Regular Customer",
"companyName": ""
}
API Reference — File Upload
Uploads a file to Cloudinary. Requires Bearer token. Content-Type: multipart/form-data
| Field | Type | Description |
|---|---|---|
file | File | The file to upload (required) |
folder | string | Optional subfolder within Cloudinary folder |
type | string | image | video | raw |
// Response
{
"success": true,
"data": {
"url": "https://res.cloudinary.com/your-cloud/image/upload/...",
"publicId": "uploads/filename",
"format": "jpg",
"width": 1200,
"height": 800
}
}
API Reference — Settings
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/admin/settings | Get all settings |
| POST | /api/admin/settings/general | Update general info |
| POST | /api/admin/settings/cloudinary | Update Cloudinary credentials |
| POST | /api/admin/settings/metadata | Update SEO metadata |
| POST | /api/admin/settings/page-banner | Update page banner images |
| POST | /api/admin/settings/terms | Update terms & privacy policy |
API Reference — Misc
Returns dashboard overview statistics.
{ "totalProducts": 48, "totalBlogs": 12, "totalOutlets": 3, "totalSubscribers": 210 }
Returns all newsletter subscribers.
Database Models
User
{
name: String, // Admin display name
email: String (unique), // Login credential
password: String, // Bcrypt hash
role: "admin" | "user"
}
Blog
{
title: String,
slug: String (unique),
subtitle: String,
content: String, // Rich HTML
image: String, // Cloudinary URL
status: "pending" | "draft" | "published" | "archived",
isFeatured: Boolean,
isShowHomepage: Boolean,
publishedAt: Date,
tags: [String],
category: ObjectId -> BlogCategory
}
BlogCategory
{
name: String,
slug: String (unique),
description: String,
status: Boolean,
position: Number
}
Category (Product Category)
{
name: String,
slug: String (unique),
description: String,
image: String,
parent: ObjectId | null, // Self-reference for sub-categories
status: Boolean,
position: Number
}
Product
{
name: String,
slug: String (unique),
description: String,
image: String,
category: ObjectId -> Category,
subcategory: ObjectId -> Category,
price: Number,
isPopular: Boolean,
isFeatured: Boolean,
status: "active" | "inactive"
}
Outlet
{
name: String,
slug: String (unique),
address: String,
phone: String,
dialCode: String,
image: String,
mapLink: String,
isShowHomepage: Boolean,
status: Boolean,
position: Number,
businessHours: [{ day, open, close, isClosed }]
}
Banner
{
title: String,
description: String,
image: String,
linkUrl: String,
isActive: Boolean,
status: Boolean,
position: Number,
type: String
}
Testimonial
{
name: String,
review: String,
rating: Number (1-5),
image: String,
authorRole: String,
companyName: String,
status: Boolean,
position: Number
}
Settings (Single Document)
{
general: {
companyName, phone, email, address, logo, favicon,
socialLinks: { facebook, instagram, twitter, youtube, linkedin }
},
pageBanner: { menu, location, gallery, reserveTable },
cloudinary: { cloud_name, api_key, api_secret, folderName, secureUrlBase },
metadata: { title, applicationName, description, keywords[], openGraphImage },
termsPolicy: { terms, policy },
businessHours: [{ day, open, close, isClosed }]
}
Authentication
The template uses NextAuth v5 with a Credentials provider for page-level session management, alongside a custom JWT implementation for API route protection.
Login Flow
1. POST /api/admin/auth/login → { email, password }
2. Server verifies password with bcrypt
3. Generates JWT token (30-day expiry) via jsonwebtoken
4. Client stores token in NextAuth session
5. Subsequent requests include: Authorization: Bearer <token>
6. Protected API routes verify the token via lib/authenticate.ts
Protecting Custom API Routes
import { asyncHandler } from "@/lib/async-handler";
// This route is protected — unauthenticated requests get 401
export const GET = asyncHandler(
async (req) => {
return Response.json({ success: true, data: "protected data" });
},
{ checkAuth: true }
);
Cloudinary Setup
- Create a free account at cloudinary.com
- Copy your Cloud Name, API Key, and API Secret from the Cloudinary dashboard
- Log in to your admin dashboard and go to Settings → Cloudinary
- Enter your credentials and click Save
Settings document — not in environment variables — so you can update them any time from the admin panel without redeploying.
Supported Upload Types
| Type | Supported Formats |
|---|---|
| Images | JPG, PNG, GIF, WebP, SVG |
| Videos | MP4, MOV, AVI |
| Raw | PDF, DOC, etc. |
Admin Dashboard
Access the admin dashboard at /admin/login using your admin credentials.
| Section | What You Can Do |
|---|---|
| Dashboard | View overview stats: products, blogs, subscribers, outlets |
| Banners | Add, edit, delete, activate/deactivate, drag-to-reorder hero banners |
| Products | Add menu items with images, pricing, category assignment |
| Categories | Manage product categories and sub-categories with ordering |
| Blogs | Full blog management with SunEditor rich text, tags, featured flags |
| Blog Categories | Organize blogs into categories with ordering |
| Outlets | Manage branch locations with business hours, phone, map links |
| Testimonials | Customer reviews with rating, image, drag-to-reorder |
| Subscribers | View newsletter subscriber list |
| Settings | General info, Cloudinary, SEO metadata, page banners, T&C |
State Management
The application uses Zustand for lightweight client-side state. Four stores are provided:
| Store | File | Purpose |
|---|---|---|
useBlogCategoryStore | store/useBlogCategoryStore.ts | Active blog category filter on the public blog page |
useCategoryStore | store/useCategoryStore.ts | Active product category filter on the menu page |
useTableStore | store/useTableStore.ts | Admin data table sorting, pagination, and filter state |
useUserProfile | store/useUserProfile.ts | Authenticated admin profile data |
import { useBlogCategoryStore } from "@/store/useBlogCategoryStore";
const { activeCategory, setActiveCategory } = useBlogCategoryStore();
Customization Guide
Changing Brand Colors
/* In app/globals.css */
:root {
--primary: 25 75% 40%; /* HSL value */
}
Changing Fonts
Fonts are loaded in app/layout.tsx via next/font/google. Replace the current fonts with any Google Font:
import { Montserrat } from "next/font/google";
const font = Montserrat({ subsets: ["latin"], weight: ["400", "700"] });
Adding a New Public Page
- Create a folder:
app/(public)/your-page/ - Add
page.tsxinside it - Build your page using existing components from
components/features/andcomponents/ui/
Adding a New Admin Section
- Create page:
app/(private)/admin/dashboard/your-section/page.tsx - Add API route:
app/api/admin/your-section/route.ts - Add Mongoose model:
model/YourModel.ts - Add validation: update
lib/validation-schema.ts - Add server actions:
actions/your-section/
Modifying Validation Rules
All Zod schemas are centralized in lib/validation-schema.ts:
export const createProductSchema = z.object({
name: z.string().min(2).max(200),
price: z.number().positive(),
// add your custom fields here
});
Deployment
Deploy on Vercel (Recommended)
- Push your project to a GitHub repository
- Import the repository on vercel.com
- Add all environment variables under Project Settings → Environment Variables
- Click Deploy — Vercel handles the rest automatically
Deploy on a VPS / Dedicated Server
# 1. Build the application
pnpm build
# 2. Start the production server
pnpm start
# Runs on port 3000 by default
Use Nginx or Caddy as a reverse proxy to expose the app on port 80/443:
# Sample Nginx configuration
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Process Manager (PM2)
npm install -g pm2
pm2 start "pnpm start" --name template-bakery
pm2 save
pm2 startup
Changelog
v1.0.0 — Initial Release
- Full-stack bakery management system
- Public website with two homepage variants
- Admin dashboard with complete CRUD for all entities
- JWT-based authentication with bcrypt password hashing
- Cloudinary integration for all media uploads
- MongoDB with Mongoose (indexes, aggregation pipelines)
- Full blog system with categories, tags, featured flags
- Multi-outlet management with business hours
- Testimonial management with star ratings
- Newsletter subscription system
- Site-wide settings panel (general, SEO, Cloudinary, T&C)
- Drag-and-drop reordering for all list entities
- Dark mode support
- Fully responsive design
- TypeScript strict mode throughout
- Turbopack for fast development builds
Support
For support, questions, and bug reports, please use the Comments section on the item's CodeCanyon / ThemeForest page.
When reporting an issue, please include:
- Your Node.js version (
node -v) - Your operating system
- Steps to reproduce the issue
- Any error messages from the browser console or server logs