Project Deep Dive

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.

Challenges & solutions
01

Creating a Dynamic and Scalable Page Architecture with Sanity CMS

Challenge

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.

Solution

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:

  1. commonTopPage – Defines global page elements.
  2. page – Represents a reusable page structure.
  3. 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.

javascript
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.

javascript
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.

javascript
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 order field.
  • 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.

02

Rendering the Common Top Page Dynamically with ID-Based Content

Challenge

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.

Solution

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:

javascript
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)

javascript
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

javascript
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)

javascript
@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.