Unit 1 · Module 5

Route Groups, Dynamic Routes & Sitemaps

Folder conventions that shape your URLs

A quick note before the brackets

This page covers two folder-naming tricks: wrapping folders in (parentheses) and wrapping them in [square brackets]. Both are used by Next.js. But the idea of giving folder names special meaning isn't unique to Next.js — almost every modern framework does some version of this. We'll note where the conventions overlap so you can recognize them later in other projects.

Parentheses are invisible folders

Look at Opsette's app/ folder:

app/

app/
├── (auth)/
├── (dashboard)/
├── (marketing)/
├── (pos)/
├── (storefront)/
├── api/
├── auth/
├── b/
├── booking/
├── embed/
├── f/
├── hub/
├── portal/
├── products/
├── providers/
├── sign/
├── GoogleAnalytics.tsx
├── globals.css
├── layout.tsx
├── page.tsx
└── sitemap.ts

Notice the folders wrapped in parentheses — (auth), (dashboard), (marketing), (pos), (storefront). Those are route groups.

Normally, a folder called dashboard in Next.js becomes /dashboard. The folder name is the route. But if you wrap that folder in parentheses — (dashboard) — the framework sees those and says "this folder is just for organization. Don't put its name in the URL."

So everything inside (dashboard) is still routed. The pages still work. The URL just never has the word "dashboard" in it. The user sees /bookings, not /(dashboard)/bookings.

Why route groups exist

Route groups solve a specific problem: you want to group pages together without that grouping becoming visible in the URL.

You might want all authenticated pages under (dashboard) so they share a layout with a sidebar. You might want all public marketing pages under (marketing) so they share a different layout with a nav bar. You might want a checkout flow grouped under (pos) so all its pages use the point-of-sale layout.

Route groups let you share layouts across sections of the app without mangling your URLs.

Square brackets are variables

Now look at Opsette's app/b/[org]/ folder:

app/b/[org]/

app/b/[org]/
├── [service]/
└── page.tsx

Those square brackets around org and service mean the folder name is a variable, not a fixed word. These are called dynamic routes.

So app/b/[org]/[service]/page.tsx doesn't mean there's a literal folder called org on your server. It means: whatever the org slug is, put it here. Whatever the service is, put it there.

When someone visits opsette.io/b/dr-smith/haircut:

That's how one page.tsx file can render thousands of different URLs — one for every provider, every service, every booking. The structure is fixed. The values inside are dynamic.

Why b and f instead of booking and forms

In Opsette, the public booking interface lives at app/b/[org]/[service] instead of app/booking/[org]/[service]. Same with app/f/ for public forms.

That's not just a shortcut — it's actively keeping the URL short and shareable.

A booking link that says opsette.io/b/dr-smith/consultation is a lot friendlier than opsette.io/booking/dr-smith/consultation. Same page, cleaner share. The single-letter folder is a deliberate architectural choice to separate the short public-facing route from the longer internal /booking/* admin routes.

sitemap.ts — telling Google what exists

At the bottom of Opsette's app/ folder sits a file called sitemap.ts. It's not a page. It's not a layout. It's a special file Next.js looks for — if it's there, the framework automatically serves its output at /sitemap.xml, which is the standard location search engines look for.

A sitemap is a list of every page that exists on your site. Google's crawler visits your domain, finds your sitemap, and uses it as a map of what to index. Without one, Google has to discover your pages by following links — slower, and pages with no links into them can get missed entirely.

This file is a great example because it pulls together almost everything you've learned so far — imports, functions, types, async/await, objects, and the file structure that lets it all sit in one place. Let's break it down piece by piece.

1. The imports — what this file needs to do its job

app/sitemap.ts — imports

import type { MetadataRoute } from 'next'
import supabaseServer from '@/integrations/supabase/server'
import client as sanityClient from '../sanity/lib/client'

Three imports, three different jobs:

2. The function signature — a labeled entry point

app/sitemap.ts — function declaration

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  // ...body goes here
}

Read that line one word at a time:

3. Fetching from two sources in parallel

app/sitemap.ts — data fetching

const [{ data: providers }, blogPosts] = await Promise.all([
  supabaseServer
    .from('public_company_profiles')
    .select('organization_slug')
    .not('organization_slug', 'is', null)
    .eq('is_deleted', false)
    .order('company_name'),
  sanityClient.fetch(/* ...blog posts query... */)
])

Two data sources, fetched at the same time to save time. Let's break it down:

4. Mapping each row into a URL object

app/sitemap.ts — building URLs

const providerUrls = (providers || []).map((p) => ({
  url: `https://opsette.io/providers/${p.organization_slug}`,
  changeFrequency: 'weekly',
  priority: 0.8,
}))

const blogUrls = (blogPosts || []).map((post) => ({
  url: `https://opsette.io/blog/${post.slug}`,
  lastModified: post.publishedAt,
  changeFrequency: 'monthly',
  priority: 0.7,
}))

Each block does the same job for a different data source. Reading providerUrls:

5. Returning the combined list

app/sitemap.ts — the return

  return [...providerUrls, ...blogUrls]
}

The ... is the spread operator. It takes the contents of one array and drops them into another. So this line says: "return a new array containing all the provider URLs, followed by all the blog URLs." The final shape matches the MetadataRoute.Sitemap blueprint TypeScript was enforcing at the top.

What this file actually does, end to end

That's not a static file someone typed out. It's generated fresh from the database every time it's requested. When a new provider signs up, they automatically appear in the sitemap the next time Google asks for it. No manual update. No cron job. The file is the map.

Three conventions, one system

Inside the app/ folder, three folder-naming patterns shape your URLs:

Once you can read those three, you can look at any Next.js project's app/ folder and know what its URL map looks like without running the app. And when you jump to a different framework in the next unit, you'll recognize the same ideas even if the syntax shifts to dollar signs or underscores.