Tuscany TEFL School (website redesign)
I’m redesigning the website for Tuscany TEFL School, a Florence-based institution offering on-site TEFL courses.
The previous site was slow, outdated, and not mobile-friendly, making navigation difficult for potential students. By adopting a Jamstack architecture, which combines static site generation with headless CMS and client-side JavaScript, I’m using Next.js, Sanity.io, and SASS to create a modern, fast, and user-friendly platform with improved performance and accessibility.
I’m also working on implementing user authentication and payment processing, ensuring a seamless and secure experience for students enrolling in courses.
Creating a Dynamic and Scalable Page Architecture with Sanity CMS
When integrating Sanity CMS with Next.js, I faced a key challenge: how to structure page data in a way that balances flexibility and consistency. The website needed to support both: Unique pages, which have specific layouts and are not reusable. Universal pages, which share a common structure but allow for different content and styling. A rigid system would make content management tedious, while a fully dynamic system could lead to inconsistencies. I needed a scalable and reusable approach that would keep content organized without limiting future changes.
To address this, I designed a modular page system with three key components: commonTopPage – Handles global elements like background images, titles, and subtitles. page – A flexible schema that supports different page types dynamically. pageSection – A modular approach that allows infinite sections, making content creation more adaptable. By structuring pages this way, I ensured greater flexibility, reusability, and scalability, allowing the site to grow without major restructuring. In the next sections, I’ll break down the implementation in detail.
Designing a Scalable and Flexible Page System with Sanity and Next.js
___________________________________________________________________
The Challenge
When integrating Sanity CMS with Next.js, I needed a flexible yet structured approach to managing pages. The website had to support:
- Unique pages, each with a specific structure.
- Universal pages, reusable but with different content and styles.
To avoid a rigid system that complicates content updates while preventing inconsistencies, I designed a modular page architecture.
The Solution: A Modular Page System
To achieve scalability and reusability, I structured my Sanity schema into three core parts:
commonTopPage– Defines global page elements.page– Represents a reusable page structure.pageSection– Allows infinite, dynamic sections.
Let’s break them down.
___________________________________________________________________
1. Global Page Elements: commonTopPage
This schema stores common page elements, like background images, titles, and subtitles.
export default {
name: 'commonTopPage',
title: 'Common Top Page',
type: 'document',
fields: [
{
name: 'backgroundImage',
title: 'Background Image',
type: 'image',
options: { hotspot: true },
},
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'subtitle',
title: 'Subtitle',
type: 'string',
},
],
};
🟪 Why? This ensures a consistent page layout while keeping content easily editable.
___________________________________________________________________
2. The Core Page Structure: page
Each page consists of a main image, title, type (slug), and dynamic sections.
export default {
name: 'page',
title: 'Page',
type: 'document',
fields: [
{
name: 'mainImage',
title: 'Main Image',
type: 'image',
options: { hotspot: true },
},
{
name: 'mainTitle',
title: 'Main Title',
type: 'string',
},
{
name: 'pageType',
type: 'slug',
title: 'Page Type',
options: {
source: 'mainTitle',
slugify: (input) =>
input.toLowerCase().replace(/[^a-zA-Z0-9]+/g, ' ')
.split(' ')
.map((word, i) => (i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)))
.join(''),
},
},
{
name: 'sections',
title: 'Sections',
type: 'array',
of: [{ type: 'pageSection' }], // Allows adding multiple sections
},
],
};
🟪 Why?
- The slug dynamically generates from the title, keeping URLs clean.
- The sections array makes the page structure highly flexible, allowing endless content variations.
___________________________________________________________________
3. Modular Page Sections: pageSection
Each page can have multiple sections, each with its own title, subtitle, text, image, and rich content.
export default {
name: 'pageSection',
title: 'Page Section',
type: 'object',
fields: [
{
name: 'order',
title: 'Order',
type: 'number',
},
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'subtitle',
title: 'Subtitle',
type: 'string',
},
{
name: 'text',
title: 'Text',
type: 'text',
},
{
name: 'image',
title: 'Image',
type: 'image',
options: { hotspot: true },
},
{
name: 'content',
type: 'array',
title: 'Content',
of: [{ type: 'block' }], // Supports rich text content
},
],
};
🟪 Why?
- Sections can be reordered dynamically using the
orderfield. - Allows adding unlimited sections, making content fully customizable.
____________________________________________________________
Final Thoughts
This modular approach provides:
🟪 Scalability – New pages can be added effortlessly.
🟪 Reusability – A single schema supports multiple layouts.
🟪 Flexibility – Sections allow infinite content variations.
This structure ensures an efficient, adaptable CMS, making content updates easy while keeping a consistent design.
Rendering the Common Top Page Dynamically with ID-Based Content
The Common Top Page is used across multiple pages, but each instance needs to display different data (title, subtitle, background image). We need a way to: 🟪 Filter the correct content based on an ID. 🟪 Apply dynamic styles depending on the context. 🟪 Ensure efficient rendering without unnecessary re-fetching.
We achieve this by: 1) Fetching all commonTopPage entries from Sanity. 2) Filtering the correct entry using the provided ID. 3) Passing a dynamic SCSS class to apply different styles. 4) Rendering the component with a flexible layout.
Detailed Solution with Code
Step 1: Fetching Data from Sanity
The query fetches all available Common Top Pages:
export async function getCommonTopPage(): Promise<CommonTop[]> {
return client.fetch(
groq`*[_type == 'commonTopPage']{
_id,
_rev,
title,
subtitle,
backgroundImage{
asset->{
_id,
url
},
crop,
hotspot
}
}`
);
}
Step 2: Creating the Reusable Component
This component:
🟪 Accepts data, dynamicStyle, and an ID.
🟪 Filters the correct entry using the provided ID.
🟪 Uses urlFor() to generate a valid image URL.
🟪 Applies a dynamic SCSS class for customization.
CommonTopPage Component (React & Next.js)
import styles from "./style.module.scss";
import { urlFor } from "@/sanity/sanity.client";
import Image from "next/image";
import { CommonTop as CommonTopType } from "@/Types/CommonTop";
interface CommonTopTypeProps {
data: CommonTopType[];
dynamicStyle?: string;
id: string; // The unique ID to select the right content
}
export default function CommonTopPage({
data,
dynamicStyle = "defaultContainer",
id,
}: CommonTopTypeProps) {
// Find the correct entry based on the provided ID
const specificItem = data.find((item) => item._id === id);
if (!specificItem) {
return null; // If no matching entry is found, return nothing
}
const imageUrl = urlFor(specificItem.backgroundImage).url();
const { title, subtitle } = specificItem;
return (
<div className={`${styles[dynamicStyle]}`}>
{/* Background Image */}
<Image
className={styles.imageBackground}
src={imageUrl}
alt={title}
sizes="100vw"
style={{
width: "100%",
height: "auto",
}}
width={1000}
height={400}
/>
{/* Title & Subtitle */}
<div className={styles.topTextContainer}>
<h1 className={styles.mainTitle}>{title}</h1>
<p className={styles.mainText}>{subtitle}</p>
</div>
{/* Decorative Elements */}
<span className={styles.square1}></span>
<span className={styles.square2}></span>
</div>
);
}
Step 3: Rendering the Component with Dynamic Styling
In the TopPageCourses component, we:
🟪 Fetch the Common Top Page data.
🟪 Render CommonTopPage with:
🟪 A specific ID to select the right content.
🟪 A custom dynamic SCSS class for styling.
TopPageCourses.tsx
import React from 'react';
import { getCommonTopPage } from '@/sanity/sanity.query';
import OurCoursesCommonTopPage from '@/common-components/topPage';
import './TopPageCourse.module.scss';
import TextForTopPage from './TextForTopPage';
const TopPageCourses = async () => {
const dataTopPage = await getCommonTopPage();
return (
<>
<OurCoursesCommonTopPage
data={dataTopPage}
dynamicStyle="ourcoursesTopPage"
id="97d80791-1917-490a-8aaa-a922d5427434"
/>
<div className="fixed-container">
<TextForTopPage />
</div>
</>
);
};
export default TopPageCourses;
Step 4: Applying Custom SCSS Styling
Each instance of CommonTopPage can have unique styles by passing a different dynamicStyle class.
SCSS File (TopPageCourse.module.scss)
@import "../app/page.module.scss";
// Top Page Image and Titles
.ourcoursesTopPage {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(4, auto);
position: relative;
// Background Image
.imageBackground {
grid-column: 1 / 7;
grid-row: 1 / 2;
}
// Title and Subtitle Styling
.topTextContainer {
grid-column: 2 / 6;
grid-row: 1 / 2;
align-self: center;
text-align: center;
background: rgba(0, 0, 0, 0.5);
padding: 2rem;
border-radius: 8px;
h1 {
color: #fff;
font-size: 2.5rem;
}
p {
color: #d4f9fd;
font-size: 1.2rem;
}
}
// Decorative Squares
.square1, .square2 {
width: 50px;
height: 50px;
position: absolute;
background-color: #9175ba;
}
.square1 {
top: 10%;
left: 5%;
}
.square2 {
bottom: 10%;
right: 5%;
}
}
Conclusion
By structuring CommonTopPage in this way, we achieve:
🟪 Flexible, reusable content with unique data per instance.
🟪 Dynamic styling, allowing easy customization.
🟪 Efficient rendering by filtering content with an ID.
This system ensures that any page can have a customized top section while maintaining a consistent structure.