Law Firm Template

Complete documentation for installation, configuration, and customization.

Overview

This is a professional, production-ready law firm website template built with the modern Next.js App Router. It ships with a fully-featured admin dashboard that lets you manage every piece of content — from attorney profiles and blog posts to site settings and SEO metadata — without ever touching code.

📄

Public Website

Home, About Us, Blogs, Services, and dynamic Practice Area pages.

🔐

Admin Dashboard

Secure, role-based admin panel with full CRUD for all content sections.

📷

Cloudinary Images

Automatic image upload, CDN delivery, and cleanup on delete.

✏️

Rich Text Editor

SunEditor WYSIWYG for blog posts and policy content.

🔄

Drag & Drop

Reorder testimonials with drag-and-drop using DnD Kit.

🔍

SEO Ready

Configurable metadata, Open Graph image, and keywords per-site.

📊

Data Tables

TanStack Table with search, pagination, sorting, and status toggle.

🌞

Dark / Light Mode

next-themes integration for user-preferred color scheme.

Requirements

Requirement Minimum Version Notes
Node.js 18.17+ LTS recommended
npm / pnpm / yarn Any recent version pnpm recommended for speed
MongoDB 6.0+ Atlas free tier is sufficient
Cloudinary Account Free tier Required for all image uploads

Quick Start

  1. Install dependencies
    npm install
    # or
    pnpm install
  2. Configure environment variables

    Create a .env file at the project root. See Environment Variables for all required keys.

  3. Create the first admin user

    Insert a user document into your MongoDB users collection with a bcrypt-hashed password:

    // In mongosh or MongoDB Compass:
    db.users.insertOne({
      name: "Admin",
      email: "admin@yourdomain.com",
      password: "<bcrypt-hashed-password>",
      role: "admin",
      createdAt: new Date(),
      updatedAt: new Date()
    })
  4. Configure Cloudinary

    Start the dev server, log in at /admin/login, then go to Settings → Cloudinary and enter your credentials.

  5. Start the development server
    npm run dev

    Site: http://localhost:3000  |  Admin: http://localhost:3000/admin/login

Available Scripts

Command Description
npm run dev Start development server with Turbopack
npm run build Build for production (Turbopack)
npm run start Start production server
npm run lint Run ESLint

Environment Variables

# ── Application ───────────────────────────────────────────
AUTH_TRUST_HOST="true"
NEXTAUTH_URL="http://localhost:3000"
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
NEXTAUTH_SECRET="your-strong-random-secret"

# ── Database ───────────────────────────────────────────────
MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/dbname?retryWrites=true&w=majority"
Variable Required Description
AUTH_TRUST_HOST Yes Set to "true" for all environments to trust the host header
NEXTAUTH_URL Yes Full URL of your site. Must match your domain exactly (no trailing slash)
NEXT_PUBLIC_BASE_URL Yes Same as NEXTAUTH_URL. Exposed to the browser for API calls
NEXTAUTH_SECRET Yes Random string for encrypting sessions. Generate with openssl rand -base64 32
MONGODB_URI Yes MongoDB connection string including database name
ℹ️
Cloudinary credentials (Cloud Name, API Key, API Secret, Folder, Secure URL Base) are stored in the database via Admin → Settings → Cloudinary. You do not need to add them to .env.

Tech Stack

Next.js
Full-stack React framework
v16
React
UI library
v19
TypeScript
Type safety
v5
MongoDB + Mongoose
Database & ODM
v9
Tailwind CSS
Utility-first styling
v4
Next-Auth
Session management
v5 beta
Cloudinary
Image storage & CDN
v2
Radix UI
Accessible UI primitives
latest
TanStack Table
Data tables
v8
SunEditor
Rich text editor
v2
DnD Kit
Drag-and-drop
v6
Zustand
Client state management
v5
React Hook Form
Form handling
v7
Zod
Schema validation
v4
bcrypt
Password hashing
v6
date-fns
Date utilities
v4

Project Structure

law-firm/ ├── public/ # Static assets served at root URL │ ├── fonts/ # Custom web fonts (guminert.ttf) │ ├── images/ # Local/placeholder images │ ├── sample-data/ # Seed data reference │ └── documentation.html # Standalone documentation file │ ├── src/ │ ├── actions/ # Next.js Server Actions (one folder per feature) │ │ ├── about-section/ │ │ ├── admin-auth/ │ │ ├── attorney/ │ │ ├── awards/ │ │ ├── banner/ │ │ ├── blog-categories/ │ │ ├── blogs/ │ │ ├── dashboard/ │ │ ├── faq/ │ │ ├── practices/ │ │ ├── profile/ │ │ ├── services/ │ │ ├── settings/ │ │ ├── taglines/ │ │ └── testimonials/ │ │ │ ├── app/ # Next.js App Router root │ │ ├── (admin)/ # Unauthenticated admin group │ │ │ └── admin/login/ # Route: /admin/login │ │ │ │ │ ├── (private)/ # Protected dashboard group │ │ │ └── admin/dashboard/ # Route: /admin/dashboard/* │ │ │ ├── about-section/ │ │ │ ├── attorneys/ │ │ │ ├── awards/ │ │ │ ├── banner/ │ │ │ ├── blog-categories/ │ │ │ ├── blogs/ │ │ │ ├── faq/ │ │ │ ├── practices/ │ │ │ ├── services/ │ │ │ ├── settings/ │ │ │ ├── taglines/ │ │ │ └── testimonials/ │ │ │ │ │ ├── (public)/ # Public website group (Header + Footer) │ │ │ ├── (home)/ # Route: / │ │ │ ├── about-us/ # Route: /about-us │ │ │ ├── blogs/ # Route: /blogs & /blogs/[slug] │ │ │ ├── services/ # Route: /services │ │ │ └── [slug]/ # Route: /[practice-area-slug] │ │ │ │ │ ├── (docs)/ # Docs group (no Header/Footer) │ │ │ └── documentation/ # Route: /documentation │ │ │ │ │ └── api/ # REST API handlers │ │ ├── admin/ # Protected admin API (JWT required) │ │ │ ├── about-section/ │ │ │ ├── attorneys/ │ │ │ ├── auth/ │ │ │ ├── awards/ │ │ │ ├── banner/ │ │ │ ├── blog-category/ │ │ │ ├── blogs/ │ │ │ ├── dashboard/ │ │ │ ├── faq/ │ │ │ ├── practices/ │ │ │ ├── services/ │ │ │ ├── settings/ │ │ │ ├── taglines/ │ │ │ └── testimonials/ │ │ └── auth/ # NextAuth handlers │ │ │ ├── components/ │ │ ├── custom/ # Domain-specific UI components │ │ ├── form/ # Reusable form field components │ │ ├── shared/ # Header, Footer, shared layout │ │ └── ui/ # Radix UI / shadcn base components │ │ │ ├── config/ │ │ ├── cloudinary.ts # Upload, delete, URL helpers │ │ ├── constants.ts # Enums: ROLE, BlogTag, etc. │ │ ├── database.ts # MongoDB connection singleton │ │ └── routes.ts # Centralized route path definitions │ │ │ ├── db/ # Server-side data fetch helpers │ │ ├── attorneys.ts │ │ ├── blogs.ts │ │ ├── faqs.ts │ │ ├── milestones.ts │ │ ├── nav-items.ts │ │ ├── practice-areas.ts │ │ ├── settings.ts │ │ └── testimonials.ts │ │ │ ├── hooks/ │ │ └── use-mobile.ts # Mobile breakpoint detection hook │ │ │ ├── lib/ │ │ ├── api-client.ts # Typed fetch wrapper for client components │ │ ├── async-handler.ts # API route try/catch wrapper │ │ ├── async-formdata-handler.ts │ │ ├── authenticate.ts # JWT middleware for admin routes │ │ ├── date-format.ts # Date formatting utilities │ │ ├── file-validator.ts # Image/file validation helper │ │ ├── metadata.ts # Next.js generateMetadata helper │ │ ├── schema.ts # All Zod validation schemas │ │ ├── server-utils.ts # apiResponse(), makePaginate() helpers │ │ ├── types.ts # Shared TypeScript interfaces │ │ └── utils.ts # General utility functions │ │ │ ├── models/ # Mongoose schema + model definitions │ │ ├── about-section.ts │ │ ├── attorneys.ts │ │ ├── awards.ts │ │ ├── banner.ts │ │ ├── blog-categories.ts │ │ ├── blogs.ts │ │ ├── faq.ts │ │ ├── practices.ts │ │ ├── services.ts │ │ ├── settings.ts │ │ ├── taglines.ts │ │ ├── testimonials.ts │ │ └── user.ts │ │ │ ├── providers/ │ │ ├── auth-session-provider.tsx │ │ └── dashboard-provider.tsx │ │ │ ├── store/ │ │ ├── useTableStore.ts # Zustand store for data tables │ │ └── useUserProfile.ts # Zustand store for admin profile │ │ │ └── types/ │ ├── global.ts │ ├── index.ts │ ├── next-auth.d.ts # NextAuth session type extensions │ └── next.d.ts │ ├── .env # Environment variables (never commit) ├── components.json # shadcn/ui configuration ├── next.config.ts # Next.js config (image domains, server actions) ├── postcss.config.mjs ├── tailwind.config.js ├── tsconfig.json └── package.json

Public Pages

Route Description
/ Home page — Banner, About preview, Practice Areas, Attorneys, Testimonials, Blogs, FAQ, Contact
/about-us Full About Us page with team milestones, core values, and author quote
/blogs Paginated blog listing with category and tag filters
/blogs/[slug] Individual blog post with rich text content and share buttons
/services Services section overview page
/[slug] Dynamic practice area detail page (driven by Practices data)
/documentation This documentation page
ℹ️
Navigation items are automatically generated from active Practice Areas. Any practice area with status: true appears in the site navigation with its slug as the URL path.

Admin Dashboard

Access the admin panel at /admin/login. After successful login you are redirected to /admin/dashboard.

Section Route Description
Overview /admin/dashboard Stats: total attorneys, blogs, practice areas, FAQs
Banner /admin/dashboard/banner Hero heading, background image, person image, success rate & cases count
Taglines /admin/dashboard/taglines Section taglines for Practice Areas, Attorneys, Testimonials, Blog, FAQ
Attorneys /admin/dashboard/attorneys Attorney profiles with photo, name, and designation. Toggle visibility.
Blog Categories /admin/dashboard/blog-categories Manage categories used to filter blog posts
Blogs /admin/dashboard/blogs Full blog management: create, edit, delete, toggle status
Practice Areas /admin/dashboard/practices Full practice area pages with multiple images and rich descriptions
Services /admin/dashboard/services Services section content with tagline, descriptions, and images
FAQ /admin/dashboard/faq Question & answer pairs. Toggle visibility.
Testimonials /admin/dashboard/testimonials Client testimonials. Drag-and-drop to reorder display sequence.
Awards /admin/dashboard/awards Award titles and images. Toggle visibility.
About Section /admin/dashboard/about-section About page content: tagline, description, core values, quote, milestones
Settings /admin/dashboard/settings General info, Metadata/SEO, Terms & Policy, Cloudinary config

API — Authentication

Admin API routes require a valid JWT token in the Authorization header as Bearer <token>.

Method Endpoint Description Auth
POST /api/admin/auth/login Admin login. Body: { email, password }. Returns JWT token. Public
GET /api/admin/auth/me Get current admin profile Required
PUT /api/admin/auth/password Change admin password. Body: { currentPassword, newPassword } Required

API — Dashboard

Method Endpoint Description
GET /api/admin/dashboard Returns aggregate counts: attorneys, blogs, practices, FAQs

API — Attorneys

Method Endpoint Description
GET /api/admin/attorneys List attorneys. Query: page, limit, search
POST /api/admin/attorneys Create attorney. multipart/form-data: name, designation, image (file)
GET /api/admin/attorneys/[id] Get single attorney by ID
PUT /api/admin/attorneys/[id] Update attorney
DELETE /api/admin/attorneys/[id] Delete attorney and remove image from Cloudinary
PUT /api/admin/attorneys/status/[id] Toggle attorney visibility (status boolean)

API — Awards

Method Endpoint Description
GET /api/admin/awards List all awards
POST /api/admin/awards Create award. multipart/form-data: title, image (file)
GET /api/admin/awards/[id] Get single award
PUT /api/admin/awards/[id] Update award
DELETE /api/admin/awards/[id] Delete award
PUT /api/admin/awards/status/[id] Toggle visibility

API — Blog Categories

Method Endpoint Description
GET /api/admin/blog-category List all categories
POST /api/admin/blog-category Create category. Body: { title }
PUT /api/admin/blog-category/[id] Update category title
DELETE /api/admin/blog-category/[id] Delete category
PUT /api/admin/blog-category/status/[id] Toggle category visibility

API — Blogs

Method Endpoint Description
GET /api/admin/blogs List blogs. Query: page, limit, search, category, tag, status
POST /api/admin/blogs Create blog. multipart/form-data with image file and all text fields
GET /api/admin/blogs/[id] Get single blog by ID
PUT /api/admin/blogs/[id] Update blog
DELETE /api/admin/blogs/[id] Delete blog and remove image from Cloudinary
GET /api/admin/blogs/slug/[slug] Get blog by URL slug
PUT /api/admin/blogs/status/[id] Toggle blog visibility

API — Banner

Method Endpoint Description
GET /api/admin/banner Get current banner content
PUT /api/admin/banner Update banner. multipart/form-data: heading, successRate, successCases, image, backgroundImage

API — About Section

Method Endpoint Description
GET /api/admin/about-section Get full about section content including milestones and core values
PUT /api/admin/about-section Update about section. multipart/form-data with image fields and nested arrays

API — FAQ

Method Endpoint Description
GET /api/admin/faq List all FAQs
POST /api/admin/faq Create FAQ. Body: { question, answer }
PUT /api/admin/faq/[id] Update FAQ
DELETE /api/admin/faq/[id] Delete FAQ
PUT /api/admin/faq/status/[id] Toggle FAQ visibility

API — Practice Areas

Method Endpoint Description
GET /api/admin/practices List practice areas. Query: page, limit, search
POST /api/admin/practices Create practice area. multipart/form-data: displayImage, firstImage, expertiseImage (files) + all text fields
GET /api/admin/practices/[id] Get single practice area
PUT /api/admin/practices/[id] Update practice area
DELETE /api/admin/practices/[id] Delete practice area and all 3 Cloudinary images
PUT /api/admin/practices/status/[id] Toggle practice area visibility

API — Services

Method Endpoint Description
GET /api/admin/services Get services section content
PUT /api/admin/services/[id] Update services. multipart/form-data with optional imageOne and imageTwo

API — Settings

Method Endpoint Description
GET /api/admin/settings/general Get general settings (company info, logo, social links)
PUT /api/admin/settings/general Update general settings. multipart/form-data: optional logo, favicon, contactUsImage
GET /api/admin/settings/metadata Get SEO metadata
PUT /api/admin/settings/metadata Update SEO metadata. multipart/form-data: optional openGraphImage
GET /api/admin/settings/terms Get Terms of Service and Privacy Policy HTML
PUT /api/admin/settings/terms Update terms and policy rich text
GET /api/admin/settings/cloudinary Get Cloudinary credentials
PUT /api/admin/settings/cloudinary Update Cloudinary credentials

API — Taglines

Method Endpoint Description
GET /api/admin/taglines Get all section taglines
PUT /api/admin/taglines Update taglines. Body: { practiceAreaTagline, attorneyTagline, testimonialTagline, blogTagline, faqTagline }

API — Testimonials

Method Endpoint Description
GET /api/admin/testimonials List testimonials ordered by order field
POST /api/admin/testimonials Create testimonial. multipart/form-data: name, testimonial, image (file)
PUT /api/admin/testimonials/[id] Update testimonial
DELETE /api/admin/testimonials/[id] Delete testimonial
PUT /api/admin/testimonials/status/[id] Toggle visibility
PUT /api/admin/testimonials/sort Update display order after drag-and-drop. Body: [{ id, order }]

API — Public Endpoints

These endpoints are unauthenticated and used by the public-facing frontend. They only return records with status: true.

Method Endpoint Description
GET /api/attorneys Get all active attorneys
GET /api/awards Get all active awards
GET /api/faq Get all active FAQs
GET /api/practices Get all active practice areas
GET /api/practices/slug/[slug] Get a single practice area by URL slug
GET /api/testimonials Get active testimonials ordered by order field

Database Schema

User

Collection: users
name
String
Required. Admin display name.
email
String
Required. Unique. Used for login.
password
String
Required. bcrypt hash (cost factor 12 recommended).
role
Enum
"admin" | "user"

Attorney

Collection: attorneys
name
String
Required.
image
String
Required. Cloudinary public_id.
designation
String
Required. e.g. "Senior Partner"
status
Boolean
Default: true. Controls public visibility.

Awards

Collection: awards
title
String
Required.
image
String
Required. Cloudinary public_id.
status
Boolean
Default: true.

Banner

Collection: banners
heading
String
Required. Hero heading text.
backgroundImage
String
Required. Cloudinary public_id.
image
String
Required. Foreground person image. Cloudinary public_id.
successRate
Number
Default: 0. Displayed as a percentage stat.
successCases
Number
Default: 0. Total case count displayed on banner.

Blog

Collection: blogs
title
String
Required.
image
String
Required. Cover image. Cloudinary public_id.
slug
String
Required. URL-friendly identifier. e.g. "understanding-business-law"
shortDescription
String
Required. Shown on listing cards.
author
String
Required.
content
String
Required. HTML content from SunEditor.
tag
Enum
BlogTag enum values. Default: "general".
category
ObjectId
Ref: BlogCategory.
status
Boolean
Default: true.
minutesToRead
Number
Default: 0. Estimated reading time.

Blog Category

Collection: blogCategories
title
String
Required.
status
Boolean
Default: true.

FAQ

Collection: faqs
question
String
Required.
answer
String
Required.
status
Boolean
Default: true.

Practice Areas

Collection: practices
title
String
Required. Unique.
slug
String
Required. Used as URL path.
displayImage
String
Required. Card thumbnail. Cloudinary public_id.
startingDescription
String
Required. Intro description on detail page.
firstImage
String
Required. First content image. Cloudinary public_id.
choosingReasonDescription
String
Required. "Why choose us" section text.
expertise
[String]
Required. List of expertise bullet points.
expertiseImage
String
Required. Cloudinary public_id.
endingDescription
String
Required. Closing description.
discussionText
String
Required. CTA text for consultation.
status
Boolean
Default: true. Also controls nav visibility.

Services

Collection: services
tagline
String
Optional. Default: "Your partners in law, your advocates in life."
description
String
Required.
shortDescriptionOne
String
Required.
imageOne
String
Required. Cloudinary public_id.
shortDescriptionTwo
String
Required.
imageTwo
String
Required. Cloudinary public_id.

Settings

Collection: settings — Embedded: general
companyName
String
Required.
companyDialCode
String
Required. Phone dial code e.g. "+1"
companyPhone
String
Required.
companyAddress
String
Required.
logo
String
Required. Cloudinary public_id.
favicon
String
Optional. Cloudinary public_id.
contactUsImage
String
Required. Cloudinary public_id.
supportEmail
String
Required.
ownerName / ownerEmail
String
Required.
facebook / instagram / twitter / youtube
String
Required. Social media URLs.
Embedded: metadata
title
String
Site <title> tag.
applicationName
String
App name for meta tags.
description
String
Meta description.
keywords
[String]
Meta keywords array.
openGraphImage
String
Cloudinary public_id for og:image.
Embedded: cloudinary
cloudName
String
Your Cloudinary cloud name.
apiKey
String
Cloudinary API key.
apiSecret
String
Cloudinary API secret.
folder
String
Base Cloudinary folder. e.g. "my-law-firm"
secureUrlBase
String
Full CDN URL base. e.g. "https://res.cloudinary.com/cloud/image/upload"

Taglines

Collection: taglines
practiceAreaTagline
String
Default: "Comprehensive legal support, tailored to your needs."
attorneyTagline
String
Default tagline for the attorneys section.
testimonialTagline
String
Default: "The voices of those we've stood beside."
blogTagline
String
Default tagline for the blog section.
faqTagline
String
Default: "Helping You Understand Your Rights and Options"

Testimonials

Collection: testimonials
name
String
Required. Client name.
image
String
Required. Cloudinary public_id.
testimonial
String
Required. Client quote text.
status
Boolean
Default: true.
order
Number
Auto-assigned on create. Updated via drag-and-drop sort.

About Section

Collection: aboutSections
headingImage
String
Required. Cloudinary public_id.
tagline
String
Required.
description
String
Required.
features
[{title, description}]
Core values / features list.
quote
String
Required. Featured quote text.
authorName
String
Required. Quote attribution name.
authorRole
String
Required. Quote attribution role.
authorImage
String
Required. Cloudinary public_id.
discussionText
String
Required. CTA text.
journeyDescription
String
Required.
milestones
[{heading, description, year}]
Timeline entries.

Image Management — Cloudinary

All images in this template are stored and served via Cloudinary. The server stores only the Cloudinary public_id in MongoDB and constructs full URLs at query time using the configured secureUrlBase.

Configuration

After starting the dev server, go to Admin → Settings → Cloudinary and fill in:

Field Description Example
Cloud Name Your Cloudinary account cloud name my-cloud
API Key Cloudinary API key 123456789012345
API Secret Cloudinary API secret AbCdEfGhIjKlMnOpQrStUvWxYz
Folder Base folder for all uploads law-firm
Secure URL Base Full CDN base URL https://res.cloudinary.com/my-cloud/image/upload

Folder Structure in Cloudinary

{folder}/
├── attorneys/
├── awards/
├── blog/
├── practices/
└── settings/
⚠️
Deleting a record through the admin dashboard also deletes its image(s) from Cloudinary. This is irreversible. Make sure you have backups if needed.

Authentication

Admin Login (JWT)

The admin panel uses custom JWT-based authentication. On POST /api/admin/auth/login a signed JWT is returned and stored client-side. All admin API routes verify this token via the authenticate middleware.

Session Management (NextAuth)

Next-Auth v5 wraps the protected dashboard layout group (private) to manage session persistence across page navigations.

Creating the First Admin User

There is no sign-up page. You must insert the first admin user directly into MongoDB:

// Using mongosh
use your-database-name

db.users.insertOne({
  name: "Admin",
  email: "admin@yourdomain.com",
  password: "<bcrypt-hash-of-your-password>",
  role: "admin",
  createdAt: new Date(),
  updatedAt: new Date()
})

Generate a bcrypt hash in Node.js:

const bcrypt = require('bcrypt');
const hash = await bcrypt.hash('YourSecurePassword123!', 12);
 // paste this as the password value

Customization

Change Brand Colors

Update the color tokens in src/app/globals.css and extend the Tailwind theme in tailwind.config.js. All component color references use CSS custom properties for easy theming.

Change Logo / Favicon

Go to Admin → Settings → General and upload a new logo and favicon image.

Add Practice Areas

Go to Admin → Practice Areas → Create. Each practice area requires a title, slug (used as the URL path), three images, several description fields, and an expertise list. Setting status: true makes it appear in the site navigation automatically.

Update SEO

Go to Admin → Settings → Metadata to configure the site title, meta description, keywords, and Open Graph image used across all pages.

Modify Navigation Items

Navigation is dynamically generated from Practice Areas with status: true. To add or remove nav items, activate or deactivate the corresponding practice area.

Add New Blog Posts

Go to Admin → Blogs → Create. Write content using the built-in SunEditor WYSIWYG editor. Assign a category, reading time, and publish status.

Deployment

Vercel (Recommended)

  1. Push to GitHub / GitLab / Bitbucket

    Push your repository to any Git provider.

  2. Import in Vercel

    Go to vercel.com/new, import the repository, and select the Next.js framework preset.

  3. Set environment variables

    Add all five environment variables in the Vercel project settings under Settings → Environment Variables.

  4. Deploy

    Vercel will build and deploy automatically on every push to your main branch.

Self-Hosted (Railway, Render, VPS)

# Build and start
npm run build
npm run start
⚠️
For all non-Vercel deployments, set AUTH_TRUST_HOST=true and make sure NEXTAUTH_URL matches your production domain exactly (including protocol, no trailing slash).

Production Environment Variables

AUTH_TRUST_HOST=true
NEXTAUTH_URL=https://yourdomain.com
NEXT_PUBLIC_BASE_URL=https://yourdomain.com
NEXTAUTH_SECRET=<strong-random-secret>
MONGODB_URI=<production-mongodb-connection-string>

Support

For questions, bug reports, or customization requests, please reach out via the support channel associated with your purchase.

  • View this documentation online at /documentation
  • Refer to public/sample-data/ for example data structures
  • Check the README.md for a quick reference in markdown format
Thank you for using this template. We hope it helps you build a great law firm website quickly and professionally.