• Toggle theme

Β© 2026 Moises Lugo. All rights reserved.

Friendly URLs

Friendly URLs

πŸ‘€ Moises Lugo🏷️ SEO, Friendly URLs, prisma, TypeScriptπŸ“… Nov 4, 2025
⏱️ 5 min read

🧠 How I Built SEO-Friendly URLs in My E-commerce with Prisma + Next.js

Short version: Friendly URLs make links easier to read for humans and better for SEO.
I designed a small database structure with Department β†’ Category β†’ Product, and used it to create hierarchical URLs like:

https://e-commer.com/market/fruits/apples/red-apple

In this post, I’ll explain why this matters, how I implemented it with Prisma and Next.js, SEO benefits, and some practical tips you can use in your own project.


πŸš€ Why Friendly URLs Matter

  • Better user experience (UX) β€” They are easy to read and understand before clicking.
  • Improved SEO β€” Search engines like Google prefer clean, descriptive URLs without random IDs.
  • Keyword context β€” Words in the URL give search engines extra context about the page.
  • Easy to share and remember β€” Clean URLs look professional and are easier to share.
  • Logical site structure β€” Helps search engines crawl and index your site better.

πŸ‘‰ Tip: always use hyphens (-) instead of underscores (_). Google treats hyphens as spaces, which makes your URLs more readable.


🧩 Database Design (Prisma)

My structure follows this logic:
Department groups main areas, Category can have subcategories (hierarchy), and Product belongs to a single category.

model Department {
  id         Int         @id @default(autoincrement())
  name       String       @unique
  slug       String       @unique
  categories Category[]
}

model Category { 
  id           Int         @id @default(autoincrement())
  name         String
  slug         String       @unique
  parentId     Int?        
  parent       Category?    @relation("CategoryHierarchy", fields: [parentId], references: [id])
  children     Category[]   @relation("CategoryHierarchy")
  departmentId Int
  department   Department   @relation(fields: [departmentId], references: [id])
  products     Product[]
}



  model Product  {
    id Int @id @default(autoincrement())
    name String 
    slug String  @unique
    price Decimal   @db.Decimal(10, 2)
    description String
    imageUrl String
    imageAlt String?
    createdAt DateTime @default(now())
    updatedAt DateTime  @updatedAt
    published Boolean @default(true)
    categoryId  Int
    category    Category    @relation(fields: [categoryId], references: [id])
    @@index([name])
    @@index([description])
    @@index([slug])
    @@index([price])
    @@map("products")
  }


πŸ’‘ Explanation:

  • A Department has many Categories.
  • A Category can have child categories (using parentId).
  • A Product belongs to only one category.

That’s how I can generate URLs like:

/maket/fruits/apples/red-apple

🌐 Example URLs

Page TypeExample URL
Department/maket/
Category/market/fruits/
Subcategory/market/fruits/apples
Product/market/fruits/apples/red-apple

Rule: keep URLs short, clean, and descriptive.


βš™οΈ Next.js Routing (Dynamic & Catch-All)

To support hierarchical URLs, I use a catch-all route:

app/[...slug]/page.tsx

This captures any level of depth, like ["market", "fruits", "apples", "red-apple"].
Inside page.tsx, I get params.slug and use it to fetch the right data from the database.



πŸ” Resolving Products or Categories by URL

  1. Split the path into segments (["market", "fruits", "apples", "red-apple"]).
  2. Try to find the product with the last slug.
  3. If not found, try to find the category.
  4. If nothing matches, return a 404.

Example (TypeScript + Prisma)

const segments = params.slug;
const last = segments[segments.length - 1];

const product = await prisma.product.findFirst({
  where: {
    slug: last,
    published: true,
  },
  include: { category: { include: { parent: true } } }
});


🧠 Best Practices for Slugs

  • Use a library like slugify to generate them.
  • Keep slugs unique (@unique).
  • Don’t change them often (it hurts SEO).
  • If you must change one, add a 301 redirect from the old URL to the new one.

⚑ Performance Tips

  • Add indexes on all slug fields.
  • Consider adding a fullPath field like "fruits/apples/red-apple" for faster lookups.
  • Update fullPath automatically if a parent slug changes.

🧭 SEO Tips

  • Add <link rel="canonical" ...> on each page.
  • Generate a sitemap.xml that includes all products and categories.
  • Use JSON-LD structured data (Schema.org) for product details.
  • Keep URLs stable and consistent.

🧩 Breadcrumbs

Breadcrumbs still help users navigate and provide context for search engines.
Even if Google doesn’t always show them on mobile, they are still a good UX element.

Generated breadcrumbs

export async function generatedBreadcrumbs(
  slugs: string[]
): Promise<{ name: string; href: string }[]> {
  const breadCrumbs = [{ name: 'Home', href: '/' }]
  let currentPath = ''

  for (let i = 0; i < slugs.length; i++) {
    currentPath += `/${slugs[i]}`
    const slug = slugs[i]
    let name = slug
    const product = await getProductBySlug(slug)
    try {
      if (product) name = product.name
      const category = await getCategoryBySlug(slug)
      if (category) name = category.name
      const department = await getDepartmentBySlug(slug)
      if (department) name = department.name
    } catch (error) {
      console.error(`Error Loading breadcrumb for ${slug}:`, error)
      name = (slug)
    }
    breadCrumbs.({ name, : currentPath })
  }
   breadCrumbs
}

Render Breadcrumb

export function BreadCrumbs({ links }: BreadCrumbsProps) {
  return (
    <nav className='w-full flex justify-start' aria-label='Breadcrumb'>
      <ol className='flex flex-warp items-center gap-2 py-4 text-sm'>
        {links.map(({ name, href }, index) => {
          const isLast = index == links.length - 1
          return (
            <li key={href} className='flex items-center gap-2'>
              {index > 0 && (
                <span className='text-gray-400 select-none' aria-hidden>
                  /
                </span>
              )}

              {isLast ? (
                <span className='text-gray-700 font-semibold'>{name}</span>
              ) : (
                <Link
                  href={href}
                  className='text-blue-500 hover:text-blue-600 hover:underline transition-colors'
                >
                  {name}
                </Link>
              )}
            </li>
          )
        })}
      </>
    
  )
}

🧱 Example: Category or Product Resolution in Next.js

export default async function Page({ params }: { params: { slug: string[] }}) {
  const segments = params.slug;
  const last = segments[segments.length - 1];

  const product = await findProductByPath(segments);
  if (product) return <ProductPage product={product} />;

  const category = await findCategoryByPath(segments);
  if (category) return <CategoryPage category={category} />;

  return notFound();
}

πŸ”— References

  • Google: URL structure best practices
  • Next.js Docs: Dynamic Routes
  • Search Engine Land β€” SEO-friendly URLs: What you need to know

✨ Final Thoughts

Friendly URLs may look like a small detail, but they make a big impact on SEO and user experience.
With Prisma and Next.js, it’s easy to build a professional and scalable structure for your online store.

formatName
push
href
return
ol
</nav>