Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Nextjs
  4. /Intercepting Routes
Your Progress0%
0 of 70 completed

NextJS Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication

Intercepting Routes

Advanced modal patterns with URL preservation and navigation

Imagine clicking a photo in a feed and seeing it in a modal—but the URL updates to /photo/123. If you share that URL or refresh, the photo loads as a full page. This is the magic of intercepting routes. They let you "intercept" navigation to show overlays or modals while maintaining proper URLs that work with refresh, sharing, and back button navigation. It's the final piece of Next.js's advanced routing puzzle, and it's incredibly powerful for building modern web experiences.

What Are Intercepting Routes?

Intercepting routes allow you to show a route in a different context (like a modal) while keeping the URL updated and maintaining the ability to:

  • Share the URL - Links work correctly
  • Refresh the page - Shows full page version
  • Navigate with back/forward - History works naturally
  • Load directly - URL works when visited directly

The Problem They Solve

Traditional modal implementations have issues:

❌ Traditional Modals

  • URL doesn't change
  • Can't share or bookmark
  • Back button closes entire page
  • Refresh loses modal state
  • Not accessible via direct URL

✅ Intercepting Routes

  • URL updates naturally
  • Shareable and bookmarkable
  • Back button closes modal
  • Refresh shows full page
  • Works with direct access

Best of Both Worlds

Intercepting routes give you the UX of modals (quick, contextual overlay) with the benefits of proper routing (shareable URLs, browser navigation, refresh support).

The (..) Convention

Intercepting routes use a special convention similar to relative file paths:

ConventionMatchesExample
(.)Same levelLike ./file
(..)One level upLike ../file
(..)(..)Two levels upLike ../../file
(...)Root segmentsFrom app directory

📁 Think Like File Paths

The convention mirrors relative paths: (.) is current directory, (..) is parent directory, etc. It's based on route segments, not file structure!

Basic Example: Photo Gallery Modal

Let's build a classic use case—clicking a photo in a feed shows it in a modal:

Photo Gallery with Intercepting Routes

Click from feed shows modal, refresh shows full page

appImportant

Select a file or folder to see details

Step 1: Create the Full Page Route

First, create the regular route that shows the full page:

app/photo/[id]/page.tsx
// Full page photo view
// URL: /photo/123
export default async function PhotoPage({
  params,
}: {
  params: { id: string };
}) {
  const photo = await fetch(`https://api.example.com/photos/${params.id}`)
    .then(r => r.json());

  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-4xl mx-auto">
        <a href="/feed" className="text-blue-600 mb-4 inline-block">
          ← Back to Feed
        </a>
        
        <div className="bg-white rounded-lg shadow-lg p-8">
          <img
            src={photo.url}
            alt={photo.title}
            className="w-full h-auto rounded-lg mb-6"
          />
          <h1 className="text-3xl font-bold mb-4">{photo.title}</h1>
          <p className="text-gray-600 mb-4">{photo.description}</p>
          <div className="text-sm text-gray-500">
            By {photo.author} • {photo.date}
          </div>
        </div>
      </div>
    </div>
  );
}

Step 2: Create the Feed Page

app/feed/page.tsx
import Link from 'next/link';

export default async function FeedPage() {
  const photos = await fetch('https://api.example.com/photos')
    .then(r => r.json());

  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Photo Feed</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        {photos.map((photo: any) => (
          <Link
            key={photo.id}
            href={`/photo/${photo.id}`}
            className="group"
          >
            <div className="aspect-square overflow-hidden rounded-lg">
              <img
                src={photo.thumbnail}
                alt={photo.title}
                className="w-full h-full object-cover group-hover:scale-105 transition"
              />
            </div>
            <h3 className="mt-2 font-semibold">{photo.title}</h3>
          </Link>
        ))}
      </div>
    </div>
  );
}

Step 3: Create the Intercepting Route

Now create the intercepting route that shows the modal:

app/feed/(.)photo/[id]/page.tsx
import Modal from '@/components/Modal';

// This intercepts /photo/[id] when navigating from /feed
export default async function PhotoModal({
  params,
}: {
  params: { id: string };
}) {
  const photo = await fetch(`https://api.example.com/photos/${params.id}`)
    .then(r => r.json());

  return (
    <Modal>
      <div className="relative">
        <img
          src={photo.url}
          alt={photo.title}
          className="w-full h-auto rounded-lg"
        />
        <div className="mt-4">
          <h2 className="text-2xl font-bold">{photo.title}</h2>
          <p className="text-gray-600 mt-2">{photo.description}</p>
        </div>
      </div>
    </Modal>
  );
}

Step 4: Create the Modal Component

components/Modal.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useEffect, useRef } from 'react';

export default function Modal({
  children,
}: {
  children: React.ReactNode;
}) {
  const router = useRouter();
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    dialogRef.current?.showModal();
  }, []);

  const handleClose = () => {
    dialogRef.current?.close();
    router.back();
  };

  return (
    <dialog
      ref={dialogRef}
      onClose={handleClose}
      className="backdrop:bg-black/50 rounded-lg p-0 max-w-4xl w-full"
    >
      {/* Close button */}
      <button
        onClick={handleClose}
        className="absolute top-4 right-4 text-white bg-black/50 rounded-full w-10 h-10 flex items-center justify-center hover:bg-black/70"
      >
        ✕
      </button>
      
      {/* Modal content */}
      <div className="p-8">
        {children}
      </div>
    </dialog>
  );
}

How It Works

When navigating from /feed:

  1. User clicks photo link to /photo/123
  2. Next.js finds the intercepting route (.)photo/[id]
  3. Shows photo in modal overlay
  4. URL updates to /photo/123
  5. Back button closes modal and returns to feed

When refreshing or visiting directly:

  1. User visits /photo/123 directly
  2. Next.js uses the original app/photo/[id]/page.tsx
  3. Shows full page photo view
  4. No interception occurs!

Understanding Route Matching

The matching is based on route segments, not folder structure:

Example 1: Same Level (.)

PLAINTEXT
app/
  feed/
    page.tsx                  → /feed
    (.)photo/                 ← Intercepts routes at same level
      [id]/
        page.tsx              ← Intercepts /photo/[id]
  photo/
    [id]/
      page.tsx                → /photo/[id] (original)

(.)photo intercepts /photo/[id] because they're at the same level (both are direct children of the root).

Example 2: One Level Up (..)

PLAINTEXT
app/
  dashboard/
    projects/
      page.tsx                → /dashboard/projects
      (.)new/                 ← Intercepts /dashboard/new (same level)
        page.tsx
      (..)settings/           ← Intercepts /dashboard/settings (one up)
        page.tsx
    new/
      page.tsx                → /dashboard/new
    settings/
      page.tsx                → /dashboard/settings

Example 3: Root Level (...)

PLAINTEXT
app/
  dashboard/
    projects/
      page.tsx                → /dashboard/projects
      (...)login/             ← Intercepts /login (from root)
        page.tsx
  login/
    page.tsx                  → /login

Key Insight

The convention is based on URL segments, not physical folders. Think about where the route you want to intercept lives in the URL structure.

Complex Example: Multi-Level Interception

Complex Intercepting Routes

Multiple interceptions at different levels

appImportant

Select a file or folder to see details

Dashboard with New Project Modal

app/dashboard/projects/(.)new/page.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Modal from '@/components/Modal';

// Intercepts /dashboard/new when navigating from /dashboard/projects
export default function NewProjectModal() {
  const router = useRouter();
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    // Create project
    await fetch('/api/projects', {
      method: 'POST',
      body: JSON.stringify({ name, description }),
    });
    
    // Close modal and refresh
    router.back();
    router.refresh();
  };

  return (
    <Modal>
      <div className="max-w-md">
        <h2 className="text-2xl font-bold mb-6">Create New Project</h2>
        
        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <label className="block text-sm font-semibold mb-2">
              Project Name
            </label>
            <input
              type="text"
              value={name}
              onChange={(e) => setName(e.target.value)}
              className="w-full px-4 py-2 border rounded-lg"
              required
            />
          </div>
          
          <div>
            <label className="block text-sm font-semibold mb-2">
              Description
            </label>
            <textarea
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              className="w-full px-4 py-2 border rounded-lg"
              rows={4}
            />
          </div>
          
          <div className="flex gap-4">
            <button
              type="submit"
              className="flex-1 px-6 py-2 bg-blue-600 text-white rounded-lg"
            >
              Create Project
            </button>
            <button
              type="button"
              onClick={() => router.back()}
              className="flex-1 px-6 py-2 bg-gray-200 rounded-lg"
            >
              Cancel
            </button>
          </div>
        </form>
      </div>
    </Modal>
  );
}
app/dashboard/new/page.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

// Full page version - shown on direct access or refresh
export default function NewProjectPage() {
  const router = useRouter();
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    await fetch('/api/projects', {
      method: 'POST',
      body: JSON.stringify({ name, description }),
    });
    
    router.push('/dashboard/projects');
  };

  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-2xl mx-auto">
        <div className="bg-white rounded-lg shadow-lg p-8">
          <h1 className="text-3xl font-bold mb-8">Create New Project</h1>
          
          <form onSubmit={handleSubmit} className="space-y-6">
            <div>
              <label className="block text-sm font-semibold mb-2">
                Project Name
              </label>
              <input
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                className="w-full px-4 py-3 border rounded-lg text-lg"
                placeholder="Enter project name"
                required
              />
            </div>
            
            <div>
              <label className="block text-sm font-semibold mb-2">
                Description
              </label>
              <textarea
                value={description}
                onChange={(e) => setDescription(e.target.value)}
                className="w-full px-4 py-3 border rounded-lg text-lg"
                rows={6}
                placeholder="Describe your project..."
              />
            </div>
            
            <div className="flex gap-4">
              <button
                type="submit"
                className="flex-1 px-8 py-3 bg-blue-600 text-white rounded-lg font-semibold"
              >
                Create Project
              </button>
              <button
                type="button"
                onClick={() => router.back()}
                className="px-8 py-3 bg-gray-200 rounded-lg font-semibold"
              >
                Cancel
              </button>
            </div>
          </form>
        </div>
      </div>
    </div>
  );
}

Combining with Parallel Routes

Intercepting routes work great with parallel routes for advanced patterns:

PLAINTEXT
app/
  @modal/
    (.)photo/
      [id]/
        page.tsx              ← Photo modal in slot
    default.tsx               ← Empty default
  layout.tsx                  ← Receives modal slot
  feed/
    page.tsx
  photo/
    [id]/
      page.tsx                ← Full page photo
app/layout.tsx
export default function RootLayout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html>
      <body>
        {children}
        {modal}  {/* Modal slot renders on top */}
      </body>
    </html>
  );
}

This pattern keeps the modal completely separate from page content—perfect for complex applications!

Common Intercepting Route Patterns

1. Image Galleries

PLAINTEXT
app/
  gallery/
    page.tsx                  → Grid of images
    (.)image/
      [id]/
        page.tsx              → Modal with image
  image/
    [id]/
      page.tsx                → Full page image view

2. Product Quick View

PLAINTEXT
app/
  shop/
    page.tsx                  → Product grid
    (.)product/
      [id]/
        page.tsx              → Quick view modal
  product/
    [id]/
      page.tsx                → Full product page

3. Login Modal

PLAINTEXT
app/
  page.tsx                    → Homepage
  (...)login/
    page.tsx                  → Login modal from anywhere
  login/
    page.tsx                  → Full login page

4. Comments Overlay

PLAINTEXT
app/
  posts/
    [id]/
      page.tsx                → Post detail
      (.)comments/
        page.tsx              → Comments modal
      comments/
        page.tsx              → Full comments page

Best Practices

1. Always Provide Both Routes

Create both the intercepting route (modal) and the original route (full page):

PLAINTEXT
✅ Good:
app/
  feed/(.)photo/[id]/page.tsx    ← Modal
  photo/[id]/page.tsx            ← Full page

❌ Bad:
app/
  feed/(.)photo/[id]/page.tsx    ← Modal only!
  (no full page route)

2. Keep Shared Logic in Components

Extract shared UI into components to avoid duplication:

TYPESCRIPT
// components/PhotoView.tsx
export function PhotoView({ photo }: { photo: Photo }) {
  return (
    <div>
      <img src={photo.url} alt={photo.title} />
      <h2>{photo.title}</h2>
      <p>{photo.description}</p>
    </div>
  );
}

// Use in both modal and full page
// app/feed/(.)photo/[id]/page.tsx
import { PhotoView } from '@/components/PhotoView';
export default function PhotoModal({ params }) {
  const photo = await fetchPhoto(params.id);
  return <Modal><PhotoView photo={photo} /></Modal>;
}

// app/photo/[id]/page.tsx
import { PhotoView } from '@/components/PhotoView';
export default function PhotoPage({ params }) {
  const photo = await fetchPhoto(params.id);
  return <div className="container"><PhotoView photo={photo} /></div>;
}

3. Handle Closing Gracefully

TYPESCRIPT
'use client';

import { useRouter } from 'next/navigation';

export default function Modal({ children }) {
  const router = useRouter();
  
  const handleClose = () => {
    // Go back in history
    router.back();
  };
  
  // Close on backdrop click
  const handleBackdropClick = (e: React.MouseEvent) => {
    if (e.target === e.currentTarget) {
      handleClose();
    }
  };
  
  // Close on Escape key
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') handleClose();
    };
    window.addEventListener('keydown', handleEscape);
    return () => window.removeEventListener('keydown', handleEscape);
  }, []);
  
  return (
    <div onClick={handleBackdropClick}>
      {children}
    </div>
  );
}

4. Consider Mobile Experience

On mobile, consider whether modals make sense or if full pages work better:

TYPESCRIPT
'use client';

import { useMediaQuery } from '@/hooks/useMediaQuery';
import { useRouter } from 'next/navigation';

export default function AdaptiveView({ children }) {
  const isMobile = useMediaQuery('(max-width: 768px)');
  const router = useRouter();
  
  // On mobile, redirect to full page
  if (isMobile) {
    router.push('/photo/123');  // Redirect to full page
    return null;
  }
  
  // On desktop, show modal
  return <Modal>{children}</Modal>;
}

5. Preserve Scroll Position

When closing modal, preserve the user's scroll position in the feed:

TYPESCRIPT
// This is handled automatically by Next.js!
// router.back() preserves scroll position by default

Common Issues and Solutions

Issue 1: Interception Not Working

Problem: Modal doesn't show, goes directly to full page

Solutions:

  • Check the convention - is it (.), (..), or (...)?
  • Verify you're navigating from the right place
  • Ensure both routes exist (intercepting and original)
  • Restart dev server

Issue 2: Modal Shows on Refresh

Problem: Refreshing shows modal instead of full page

Solution: This means you don't have the original route. Create app/photo/[id]/page.tsx for the full page version.

Issue 3: Back Button Doesn't Work

Problem: Clicking back doesn't close modal

Solution: Use router.back() to close modal, not router.push(). The back function respects browser history.

When to Use Intercepting Routes

✅ Use Intercepting Routes When:

  • Building image galleries or lightboxes
  • Creating quick view modals for products
  • Showing forms in overlays (login, sign up, create)
  • Displaying content that should be shareable
  • You want modal UX with proper URL support
  • SEO matters for the modal content

❌ Don't Use Intercepting Routes When:

  • Simple confirmations or alerts (use regular modals)
  • Content doesn't need its own URL
  • The modal is purely temporary UI state
  • Adding complexity without clear benefit

Practice Exercise

🎯 Build a Product Gallery

Create an e-commerce product gallery with intercepting routes:

  1. Create app/products/page.tsx - product grid
  2. Create app/products/[id]/page.tsx - full product page
  3. Create app/products/(.)product/[id]/page.tsx - quick view modal
  4. Add a Modal component with close functionality
  5. Test navigation, refresh, and direct access

Reusable Modal Component

Use this component for intercepting routes

Modal.tsx

Output Preview

Click "Run Code" to see the output

Key Takeaways

  • Intercepting routes show modals with proper URLs - best of both worlds
  • Use (..) convention for matching - like relative file paths
  • Always provide both routes - intercepting and original
  • Refresh shows full page - interception is client-side only
  • Perfect for galleries and quick views - shareable modal content
  • Works with parallel routes - for complex patterns
  • Handle closing gracefully - back button, escape key, backdrop
  • Extract shared components - avoid duplicating logic

You've Mastered Advanced Routing!

Congratulations! You've completed the routing fundamentals section and learned every advanced routing pattern in Next.js 15:

What You've Learned

📁 Core Routing

  • File-based routing
  • Creating pages
  • Dynamic routes [slug]
  • Catch-all routes [...slug]

🎨 Advanced Patterns

  • Route groups (folder)
  • Parallel routes @folder
  • Intercepting routes (.)
  • Modal patterns

With these routing skills, you can build sophisticated applications with complex navigation patterns, multiple layouts, and advanced UI behaviors—all while maintaining clean, SEO-friendly URLs and great user experience.

🚀 What's Next?

Now that you've mastered routing, it's time to explore other essential Next.js concepts like data fetching, rendering strategies, Server Actions, and more. Each concept builds on the routing foundation you've established!

Test Your Understanding

Question 1 of 4

What does the (.) convention mean in intercepting routes?

Master intercepting routes in Next.js! Learn how to show modals while preserving URLs for sharing and refresh.

Previous
Parallel Routes
Next
Understanding Layouts

Continue Your Next.js Journey

Join 2,000+ developers mastering Next.js. Get more advanced tutorials delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

NextJS Tutorials

0 of 70 completed

Your Progress0%

Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo