Web

Next.js Dynamic Routing: Fix Slug Update Issues

Solve Next.js dynamic routing problems when slug changes. Learn full SSR implementation, generateMetadata for SEO, and disable client caching.

1 answer 1 view

How to fix Next.js dynamic routing not updating on slug change? When building a dynamic website like a movie archive platform, the component wasn’t re-rendering when the slug changed, causing SEO metadata and content to stay outdated. What are the solutions to ensure dynamic pages update correctly when the slug changes? Specifically, how to implement full SSR with [slug]/page.js, use generateMetadata() for dynamic SEO, sync canonical URLs and JSON-LD with the final slug, and disable client caching to force fresh renders?

Next.js dynamic routing can fail to update when slugs change due to client-side caching and improper SSR implementation. To fix this issue, you need to implement full server-side rendering for dynamic pages, use generateMetadata() for dynamic SEO metadata, properly sync canonical URLs and structured data with the slug changes, and configure caching settings to ensure fresh content delivery. The complete solution involves proper data fetching strategies, metadata configuration, and cache control headers for dynamic pages.

Contents

Understanding Next.js Dynamic Routing

Next.js dynamic routing allows you to create pages based on dynamic parameters, such as movie IDs or slugs. When you create a file like app/movies/[slug]/page.js, Next.js automatically generates routes for any slug value passed to this component. However, the default behavior may not always update content when slugs change, especially when navigating between different movie pages.

The core issue arises from Next.js’s client-side routing system. When users navigate between pages with different slugs, the framework attempts to optimize performance by reusing components and data. This optimization can cause your dynamic page content to remain static if not properly configured.

In a movie archive platform, this means that when users click from one movie to another, the page might not update, showing outdated SEO metadata, stale content, and incorrect canonical URLs. This not only harms user experience but also negatively impacts SEO performance.

Common Causes of Dynamic Routing Issues

Several factors contribute to dynamic routing problems in Next.js:

  1. Client-side navigation: Next.js Router often performs client-side navigation, which doesn’t trigger full server renders
  2. Data caching: React’s Suspense and Next.js caching may preserve stale data between page transitions
  3. Static generation: Pages may be statically generated at build time and not updated on demand
  4. Improper data fetching: Using fetch() without proper cache control headers can lead to stale data

Understanding these causes helps us implement targeted solutions to ensure your dynamic pages update correctly when slugs change.

Implementing Full SSR with Dynamic Slugs

To ensure your Next.js dynamic pages update correctly when slugs change, you need to implement full server-side rendering (SSR). This approach forces the server to generate fresh content for each unique slug, preventing stale data from being served.

Using Server Components for Dynamic Data

Start by converting your dynamic page to a Server Component, which runs exclusively on the server. This ensures that all data fetching occurs on the server side, providing fresh content for each slug:

javascript
// app/movies/[slug]/page.js
import { Suspense } from 'react';
import MovieDetails from '@/components/MovieDetails';
import MovieSkeleton from '@/components/MovieSkeleton';

export default function MoviePage({ params }) {
  return (
    <Suspense fallback={<MovieSkeleton />}>
      <MovieDetails slug={params.slug} />
    </Suspense>
  );
}

Fetching Data on the Server

For your MovieDetails component, implement server-side data fetching:

javascript
// components/MovieDetails.js
import { notFound } from 'next/navigation';

export default async function MovieDetails({ slug }) {
  // Fetch movie data on the server
  const movie = await fetchMovieData(slug);
  
  if (!movie) {
    notFound();
  }

  return (
    <div>
      <h1>{movie.title}</h1>
      <p>{movie.description}</p>
      {/* Other movie content */}
    </div>
  );
}

async function fetchMovieData(slug) {
  const res = await fetch(`${process.env.API_BASE_URL}/movies/${slug}`, {
    cache: 'no-store', // Disable caching
    headers: {
      'Cache-Control': 'no-cache, no-store, must-revalidate',
    },
  });
  
  if (!res.ok) {
    return null;
  }
  
  return res.json();
}

Using Dynamic Route Parameters

In Next.js 13+ with App Router, dynamic route parameters are available through the params prop. Ensure you’re accessing these parameters correctly to fetch the right data for each slug:

javascript
export async function generateStaticParams() {
  // For static generation of known paths
  const movies = await fetchMovies();
  return movies.map((movie) => ({
    slug: movie.slug,
  }));
}

Implementing Incremental Static Regeneration (ISR)

If you prefer static generation with periodic updates, ISR is an excellent solution:

javascript
export async function generateStaticParams() {
  const movies = await fetchMovies();
  return movies.map((movie) => ({
    slug: movie.slug,
  }));
}

export const dynamicParams = true;
export const revalidate = 3600; // Revalidate every hour

export default async function MoviePage({ params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    notFound();
  }

  return (
    <div>
      <h1>{movie.title}</h1>
      {/* Movie content */}
    </div>
  );
}

ISR provides the best of both worlds: fast static pages with automatic updates at specified intervals.

Using generateMetadata() for Dynamic SEO

Dynamic SEO metadata is crucial for movie archive platforms, as each movie should have its own title, description, and other metadata. Next.js provides the generateMetadata() function to dynamically generate metadata based on route parameters.

Implementing Dynamic Metadata

Create a metadata function that fetches movie data and generates appropriate metadata:

javascript
// app/movies/[slug]/page.js
import { notFound } from 'next/navigation';

export async function generateMetadata({ params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    return {
      title: 'Movie Not Found',
      description: 'The requested movie could not be found.',
    };
  }

  return {
    title: `${movie.title} - Movie Archive`,
    description: movie.description,
    keywords: movie.genres.join(', '),
    openGraph: {
      title: movie.title,
      description: movie.description,
      images: movie.posterUrl,
      type: 'video.movie',
    },
    twitter: {
      card: 'summary_large_image',
      title: movie.title,
      description: movie.description,
      images: movie.posterUrl,
    },
  };
}

export default async function MoviePage({ params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    notFound();
  }

  return (
    <div>
      {/* Movie content */}
    </div>
  );
}

Handling Metadata for Missing Pages

Always implement proper handling for missing or invalid slugs:

javascript
export async function generateMetadata({ params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    return {
      title: 'Movie Not Found',
      description: 'The requested movie could not be found on our archive.',
    };
  }

  // Return metadata for existing movie
  return {
    title: `${movie.title} (${movie.year}) - Movie Archive`,
    description: `${movie.title}: ${movie.description.substring(0, 160)}...`,
  };
}

Optimizing Metadata for Movie Archives

For movie archive platforms, consider additional metadata fields relevant to movies:

javascript
export async function generateMetadata({ params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    return {
      title: 'Movie Not Found | Movie Archive',
      description: 'The requested movie could not be found. Browse our collection of thousands of movies.',
    };
  }

  return {
    title: `${movie.title} (${movie.year}) | Movie Archive`,
    description: `${movie.title}: ${movie.description.substring(0, 160)}... Starring ${movie.cast.join(', ')}. Directed by ${movie.director}.`,
    keywords: `${movie.title}, ${movie.year}, ${movie.genres.join(', ')}, ${movie.director}, ${movie.cast.join(', ')}`,
    other: {
      'article:author': movie.director,
      'article:section': 'Movies',
      'article:published_time': movie.releaseDate,
      'article:modified_time': movie.updatedAt,
      'article:tag': movie.genres.join(','),
    },
  };
}

This approach ensures each movie page has unique, relevant SEO metadata that updates correctly when slugs change.

Syncing Canonical URLs and JSON-LD

Canonical URLs and structured data (JSON-LD) are essential for SEO and must be properly synchronized with the current slug. When users navigate between movie pages, these elements must update to reflect the new content.

Implementing Dynamic Canonical URLs

Add a canonical URL to your metadata that matches the current slug:

javascript
export async function generateMetadata({ params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    return {
      title: 'Movie Not Found | Movie Archive',
      description: 'The requested movie could not be found. Browse our collection of thousands of movies.',
    };
  }

  return {
    title: `${movie.title} (${movie.year}) | Movie Archive`,
    description: movie.description,
    alternates: {
      canonical: `/movies/${params.slug}`,
    },
  };
}

Adding JSON-LD Structured Data

Include JSON-LD structured data for better search engine understanding:

javascript
// app/movies/[slug]/page.js
import { notFound } from 'next/navigation';

export async function generateMetadata({ params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    return {
      title: 'Movie Not Found | Movie Archive',
      description: 'The requested movie could not be found. Browse our collection of thousands of movies.',
    };
  }

  return {
    title: `${movie.title} (${movie.year}) | Movie Archive`,
    description: movie.description,
    alternates: {
      canonical: `/movies/${params.slug}`,
    },
    other: {
      'jsonld': getMovieJsonLd(movie),
    },
  };
}

function getMovieJsonLd(movie) {
  return {
    '@context': 'https://schema.org',
    '@type': 'Movie',
    name: movie.title,
    url: `https://yourdomain.com/movies/${movie.slug}`,
    image: movie.posterUrl,
    description: movie.description,
    datePublished: movie.releaseDate,
    director: {
      '@type': 'Person',
      name: movie.director,
    },
    actor: movie.cast.map(actor => ({
      '@type': 'Person',
      name: actor,
    })),
    genre: movie.genres,
    aggregateRating: {
      '@type': 'AggregateRating',
      ratingValue: movie.rating,
      ratingCount: movie.ratingCount,
    },
  };
}

Handling Multiple URLs for Same Content

If your movie can be accessed through multiple URLs (with and without trailing slashes), implement a canonical URL to prevent duplicate content issues:

javascript
export async function generateMetadata({ params }) {
  // ... fetch movie data
  
  return {
    // ... other metadata
    alternates: {
      canonical: `/movies/${params.slug}`,
      languages: {
        'en-US': `/movies/${params.slug}`,
      },
    },
  };
}

This ensures search engines understand which URL is the canonical version of your movie page, preventing content duplication issues.

Disabling Client Caching for Fresh Renders

Client-side caching can prevent dynamic pages from updating when slugs change. To ensure fresh content, you need to implement proper cache control headers and React caching strategies.

Implementing Cache Control Headers

Set appropriate cache control headers in your API responses:

javascript
// API route example: pages/api/movies/[slug].js
export default function handler(req, res) {
  const { slug } = req.query;
  
  fetchMovieData(slug)
    .then(movie => {
      res.setHeader('Cache-Control', 'no-store, must-revalidate');
      res.setHeader('Pragma', 'no-cache');
      res.setHeader('Expires', '0');
      res.status(200).json(movie);
    })
    .catch(error => {
      res.setHeader('Cache-Control', 'no-store, must-revalidate');
      res.status(404).json({ error: 'Movie not found' });
    });
}

Using React Query for Data Fetching

React Query (TanStack Query) provides excellent caching mechanisms that can be configured for dynamic pages:

javascript
// components/MovieDetails.js
import { useQuery } from '@tanstack/react-query';
import { notFound } from 'next/navigation';

export default function MovieDetails({ slug }) {
  const { data: movie, isLoading, error } = useQuery({
    queryKey: ['movie', slug],
    queryFn: () => fetchMovieData(slug),
    staleTime: 0, // Data becomes stale immediately
    gcTime: 0, // Disable garbage collection of cache
    retry: 1,
  });

  if (error) {
    notFound();
  }

  if (isLoading) {
    return <MovieSkeleton />;
  }

  return (
    <div>
      <h1>{movie.title}</h1>
      {/* Movie content */}
    </div>
  );
}

Disabling Next.js Data Cache

In your Server Components, fetch data with appropriate cache control:

javascript
export default async function MoviePage({ params }) {
  const movie = await fetch(`${process.env.API_BASE_URL}/movies/${params.slug}`, {
    cache: 'no-store', // Disable caching
    headers: {
      'Cache-Control': 'no-cache, no-store, must-revalidate',
    },
  });
  
  if (!movie.ok) {
    notFound();
  }
  
  const movieData = await movie.json();

  return (
    <div>
      <h1>{movieData.title}</h1>
      {/* Movie content */}
    </div>
  );
}

Implementing Forced Revalidation

For scenarios where you need to force a revalidation of data when slugs change:

javascript
export default async function MoviePage({ params }) {
  // Add a timestamp to prevent caching
  const timestamp = Date.now();
  
  const movie = await fetch(`${process.env.API_BASE_URL}/movies/${params.slug}?t=${timestamp}`, {
    cache: 'no-store',
  });
  
  if (!movie.ok) {
    notFound();
  }
  
  const movieData = await movie.json();

  return (
    <div>
      <h1>{movieData.title}</h1>
      {/* Movie content */}
    </div>
  );
}

This approach ensures that each request fetches fresh data, solving the stale content problem in dynamic routing.

Advanced Techniques for Dynamic Pages

Beyond the basic solutions, several advanced techniques can further improve dynamic page performance and user experience.

Implementing Prefetching with Suspense

Use Next.js’s built-in prefetching to improve navigation performance while maintaining fresh content:

javascript
// components/MovieCard.js
import Link from 'next/link';
import { Suspense } from 'react';

export default function MovieCard({ movie }) {
  return (
    <Link 
      href={`/movies/${movie.slug}`}
      prefetch={true} // Prefetch the page on hover
    >
      <div className="movie-card">
        <img src={movie.posterUrl} alt={movie.title} />
        <h3>{movie.title}</h3>
        <p>{movie.year}</p>
      </div>
    </Link>
  );
}

Using Route Handlers for API-like Responses

Create API-like route handlers for dynamic data fetching:

javascript
// app/movies/[slug]/route.js
import { NextResponse } from 'next/server';

export async function GET(request, { params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    return NextResponse.json(
      { error: 'Movie not found' },
      { status: 404, headers: { 'Cache-Control': 'no-store' } }
    );
  }

  return NextResponse.json(movie, {
    headers: {
      'Cache-Control': 'no-store, must-revalidate',
    },
  });
}

Implementing Client-Side Data Revalidation

Add a revalidation button for users who want to ensure they’re seeing the latest content:

javascript
// components/MovieDetails.js
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';

export default function MovieDetails({ slug }) {
  const [refetchKey, setRefetchKey] = useState(0);
  const { data: movie, isLoading, error, refetch } = useQuery({
    queryKey: ['movie', slug, refetchKey],
    queryFn: () => fetchMovieData(slug),
    staleTime: 0,
    gcTime: 0,
  });

  const handleRefresh = () => {
    setRefetchKey(prev => prev + 1);
    refetch();
  };

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  if (isLoading) {
    return <MovieSkeleton />;
  }

  return (
    <div>
      <h1>{movie.title}</h1>
      <button onClick={handleRefresh} disabled={isLoading}>
        {isLoading ? 'Refreshing...' : 'Refresh Data'}
      </button>
      {/* Movie content */}
    </div>
  );
}

Optimizing Image Loading for Dynamic Pages

Next.js Image component works well with dynamic pages but requires careful configuration:

javascript
// components/MoviePoster.js
import Image from 'next/image';

export default function MoviePoster({ movie }) {
  return (
    <div className="relative aspect-[2/3] w-full">
      <Image
        src={movie.posterUrl}
        alt={movie.title}
        fill
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        priority={true} // Prioritize loading for current page
        unoptimized={process.env.NODE_ENV === 'development'} // Disable optimization in dev
      />
    </div>
  );
}

This ensures that images load efficiently even on dynamically generated pages.

Best Practices for Movie Archive Platforms

When implementing dynamic routing for a movie archive platform, follow these best practices to ensure optimal performance and SEO:

Structuring URLs and Slugs

Create clean, readable URLs that include movie titles and years:

javascript
// Generate slugs from movie titles
function generateSlug(title, year) {
  return `${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${year}`;
}

// Example usage
const movie = {
  title: "The Shawshank Redemption",
  year: 1994
};
const slug = generateSlug(movie.title, movie.year); // "the-shawshank-redemption-1994"

Implementing Breadcrumbs for Navigation

Add breadcrumbs to help search engines understand page hierarchy:

javascript
// components/Breadcrumbs.js
import Link from 'next/link';

export default function Breadcrumbs({ movie }) {
  return (
    <nav aria-label="Breadcrumb">
      <ol className="flex items-center space-x-2 text-sm">
        <li>
          <Link href="/" className="text-gray-500 hover:text-gray-700">
            Home
          </Link>
        </li>
        <li>/</li>
        <li>
          <Link href="/movies" className="text-gray-500 hover:text-gray-700">
            Movies
          </Link>
        </li>
        <li>/</li>
        <li className="text-gray-900">{movie.title}</li>
      </ol>
    </nav>
  );
}

Implement cross-linking between related movies to improve navigation and SEO:

javascript
// components/RelatedMovies.js
import Link from 'next/link';

export default function RelatedMovies({ movie }) {
  const relatedMovies = findRelatedMovies(movie);
  
  return (
    <div className="mt-8">
      <h2 className="text-xl font-bold mb-4">Related Movies</h2>
      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
        {relatedMovies.map(related => (
          <Link 
            key={related.id} 
            href={`/movies/${related.slug}`}
            className="block"
          >
            <div className="related-movie-card">
              <img 
                src={related.posterUrl} 
                alt={related.title}
                width={200}
                height={300}
              />
              <h3 className="text-sm mt-2">{related.title}</h3>
              <p className="text-xs text-gray-500">{related.year}</p>
            </div>
          </Link>
        ))}
      </div>
    </div>
  );
}

Implementing Schema Markup for Movie Pages

Add comprehensive schema markup for better search engine understanding:

javascript
// components/JsonLdScript.js
import Script from 'next/script';

export default function JsonLdScript({ movie }) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Movie',
    name: movie.title,
    url: `https://yourdomain.com/movies/${movie.slug}`,
    image: movie.posterUrl,
    description: movie.description,
    datePublished: movie.releaseDate,
    director: {
      '@type': 'Person',
      name: movie.director,
    },
    actor: movie.cast.map(actor => ({
      '@type': 'Person',
      name: actor,
    })),
    genre: movie.genres,
    aggregateRating: {
      '@type': 'AggregateRating',
      ratingValue: movie.rating,
      ratingCount: movie.ratingCount,
    },
    contentRating: movie.contentRating,
    duration: movie.duration,
  };

  return (
    <Script
      id="movie-jsonld"
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(jsonLd),
      }}
    />
  );
}

Optimizing Page Load Performance

Implement performance optimizations specific to movie archive pages:

javascript
// app/movies/[slug]/page.js
import { Suspense } from 'react';
import MovieDetails from '@/components/MovieDetails';
import MovieHeader from '@/components/MovieHeader';
import MovieCast from '@/components/MovieCast';
import MovieGallery from '@/components/MovieGallery';
import JsonLdScript from '@/components/JsonLdScript';
import MovieSkeleton from '@/components/MovieSkeleton';

export async function generateMetadata({ params }) {
  // ... metadata implementation
}

export default async function MoviePage({ params }) {
  const movie = await fetchMovieData(params.slug);
  
  if (!movie) {
    notFound();
  }

  return (
    <>
      <JsonLdScript movie={movie} />
      <Suspense fallback={<MovieSkeleton />}>
        <MovieHeader movie={movie} />
      </Suspense>
      <div className="container mx-auto px-4 py-8">
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
          <div className="lg:col-span-2">
            <Suspense fallback={<MovieSkeleton />}>
              <MovieDetails movie={movie} />
            </Suspense>
            <Suspense fallback={<MovieSkeleton />}>
              <MovieCast movie={movie} />
            </Suspense>
          </div>
          <div className="lg:col-span-1">
            <Suspense fallback={<MovieSkeleton />}>
              <MovieGallery movie={movie} />
            </Suspense>
          </div>
        </div>
      </div>
    </>
  );
}

This implementation uses Suspense boundaries to load different parts of the page independently, improving perceived performance.

Sources

Conclusion

Implementing proper Next.js dynamic routing requires a comprehensive approach that addresses server-side rendering, dynamic metadata, canonical URLs, and cache control. By following the solutions outlined in this guide, you can ensure that your movie archive platform’s dynamic pages update correctly when slugs change, providing users with fresh content and search engines with accurate SEO information.

The key takeaways include using full server-side rendering with Server Components, implementing generateMetadata() for dynamic SEO metadata, properly syncing canonical URLs and structured data with the current slug, and configuring appropriate cache control headers to prevent stale content delivery. These techniques work together to create a dynamic website that performs well both for users and search engines.

For movie archive platforms specifically, additional considerations like clean URL structures, comprehensive schema markup, and related content suggestions further enhance the user experience and SEO performance. By implementing these best practices, you’ll create a dynamic website that scales well and provides an excellent user experience regardless of how many movies are in your archive.

Authors
Verified by moderation
Moderation
Next.js Dynamic Routing: Fix Slug Update Issues