Building a modern, performant blog that supports multiple languages while maintaining excellent SEO and user experience is a challenge many developers face. In this comprehensive guide, I'll walk you through implementing a bilingual blog system using MDX, Next.js 14's App Router, Tailwind CSS for styling, and Upstash Redis for view counting—the exact stack powering Nibi, a financial planning app for Canadian freelancers.

This isn't just another blog tutorial. We're building a production-ready blog with real-time view counting, automatic RSS generation, and a design inspired by Upstash's clean aesthetic—all while maintaining perfect bilingual support for English and French content.

Context

After launching Nibi, I needed a blog that could:

  • Serve content in both English and French seamlessly
  • Track article views without expensive analytics
  • Load blazingly fast with excellent SEO
  • Match our existing design system perfectly
  • Support rich content with code blocks and interactive components

The solution? MDX for content flexibility, Upstash for free Redis-based view counting, and Next.js 14 for optimal performance. This guide shares the exact implementation, including the issues I encountered and how to solve them.

Prerequisites

Before diving in, ensure you have:

Development Environment

  • Windows with WSL 2 or macOS/Linux
  • Node.js 20+ installed (node --version)
  • npm or yarn package manager
  • Git configured for version control

Existing Setup

  • Next.js 14 project with App Router
  • Tailwind CSS configured
  • Dynamic routing with [locale] parameter
  • Upstash account (free tier is perfect) - Sign up here

Technical Knowledge

  • Basic understanding of React and Next.js
  • Familiarity with MDX or Markdown
  • Basic TypeScript knowledge
  • Understanding of dynamic routing in Next.js

🚀 Building a Blog That Actually Converts?

This guide shows you how to implement view counting, bilingual content, and SEO optimization that helped Nibi grow to 1,000+ monthly readers in just 3 months. Want the complete source code, advanced components, and exclusive tips that didn't make it into this article?

Get the Full Blog Blueprint + Source Code →


Complete Implementation Guide

Step 1: Install Dependencies and Configure MDX

First, let's install all required packages. Note that we'll use a simpler approach than next-mdx-remote to avoid compilation issues:

bash

# Core MDX packages
npm install @next/mdx @mdx-js/loader @mdx-js/react

# Content utilities
npm install gray-matter reading-time marked

# MDX plugins for enhanced functionality
npm install rehype-autolink-headings rehype-slug remark-gfm rehype-prism-plus

# Typography plugin for beautiful prose
npm install @tailwindcss/typography

# Upstash for view counting
npm install @upstash/redis @upstash/ratelimit

# RSS generation
npm install rss

Step 2: Configure Next.js for MDX

Update your next.config.js to support MDX files:

javascript

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
  experimental: {
    mdxRs: true,
  },
}

const withMDX = require('@next/mdx')({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [require('remark-gfm')],
    rehypePlugins: [
      require('rehype-slug'),
      require('rehype-autolink-headings'),
      require('rehype-prism-plus'),
    ],
  },
})

module.exports = withMDX(nextConfig)

Step 3: Set Up Upstash Redis

Create a .env.local file and add your Upstash credentials:

bash

# Get these from your Upstash console
UPSTASH_REDIS_REST_URL=your_redis_url_here
UPSTASH_REDIS_REST_TOKEN=your_redis_token_here

Create lib/upstash.ts for view counting:

typescript

import { Redis } from '@upstash/redis'
import { Ratelimit } from '@upstash/ratelimit'

// Create Redis instance
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})

// Create rate limiter to prevent abuse
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(5, '10 s'), // 5 requests per 10 seconds
})

export async function incrementViews(slug: string, ip: string) {
  // Check rate limit
  const { success } = await ratelimit.limit(ip)
  if (!success) return null

  // Increment and return new count
  const key = `pageviews:blog:${slug}`
  const views = await redis.incr(key)
  return views
}

export async function getViews(slug: string): Promise<number> {
  const key = `pageviews:blog:${slug}`
  const views = await redis.get<number>(key)
  return views || 0
}

export async function getAllViews(): Promise<Record<string, number>> {
  const keys = await redis.keys('pageviews:blog:*')
  if (keys.length === 0) return {}

  const views = await redis.mget(...keys)
  const result: Record<string, number> = {}

  keys.forEach((key, index) => {
    const slug = key.replace('pageviews:blog:', '')
    result[slug] = views[index] || 0
  })

  return result
}

Step 4: Create Content Structure

Set up your content directory structure:

bash

# Create content directories
mkdir -p content/blog/fr content/blog/en
mkdir -p content/authors

# Create sample author data
cat > content/authors/authors.ts << 'EOF'
export const authors = {
  'nibi-team': {
    name: 'Nibi Team',
    avatar: '/authors/nibi-team.jpg',
    bio: {
      en: 'Building financial tools for Canadian freelancers',
      fr: 'Créer des outils financiers pour les freelances canadiens'
    }
  }
}
EOF

Step 5: Create MDX Utilities

Create lib/mdx.ts for handling MDX files:

typescript

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { remark } from 'remark'
import html from 'remark-html'

const contentDirectory = path.join(process.cwd(), 'content', 'blog')

export interface BlogPost {
  slug: string
  title: string
  excerpt: string
  date: string
  author: string
  category: string
  heroImage: string
  readingTime: string
  tags: string[]
  content?: string
  locale: string
}

export async function getPostSlugs(locale: string): Promise<string[]> {
  const postsDirectory = path.join(contentDirectory, locale)
  
  if (!fs.existsSync(postsDirectory)) {
    return []
  }
  
  const filenames = fs.readdirSync(postsDirectory)
  return filenames
    .filter(filename => filename.endsWith('.mdx') || filename.endsWith('.md'))
    .map(filename => filename.replace(/\.(mdx|md)$/, ''))
}

export async function getPostBySlug(slug: string, locale: string): Promise<BlogPost | null> {
  const realSlug = slug.replace(/\.(mdx|md)$/, '')
  const fullPath = path.join(contentDirectory, locale, `${realSlug}.mdx`)
  
  if (!fs.existsSync(fullPath)) {
    // Try .md extension
    const mdPath = path.join(contentDirectory, locale, `${realSlug}.md`)
    if (!fs.existsSync(mdPath)) {
      return null
    }
  }
  
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)
  
  // Calculate reading time
  const wordsPerMinute = 200
  const words = content.split(/\s+/).length
  const readingTime = Math.ceil(words / wordsPerMinute)
  
  return {
    slug: realSlug,
    title: data.title || '',
    excerpt: data.excerpt || '',
    date: data.date || new Date().toISOString(),
    author: data.author || 'Nibi Team',
    category: data.category || 'General',
    heroImage: data.heroImage || '/blog/default-hero.jpg',
    readingTime: `${readingTime} min read`,
    tags: data.tags || [],
    content: content,
    locale: locale
  }
}

export async function getAllPosts(locale: string): Promise<BlogPost[]> {
  const slugs = await getPostSlugs(locale)
  const posts = await Promise.all(
    slugs.map(slug => getPostBySlug(slug, locale))
  )
  
  return posts
    .filter((post): post is BlogPost => post !== null)
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
}

export async function getRelatedPosts(
  currentSlug: string,
  locale: string,
  tags: string[],
  limit: number = 3
): Promise<BlogPost[]> {
  const allPosts = await getAllPosts(locale)
  
  return allPosts
    .filter(post => post.slug !== currentSlug)
    .filter(post => post.tags.some(tag => tags.includes(tag)))
    .slice(0, limit)
}

export async function generateTableOfContents(content: string) {
  const headingRegex = /^(#{2,3})\s+(.+)$/gm
  const headings = []
  let match
  
  while ((match = headingRegex.exec(content)) !== null) {
    const level = match[1].length
    const text = match[2]
    const id = text.toLowerCase().replace(/[^\w]+/g, '-')
    
    headings.push({ level, text, id })
  }
  
  return headings
}

Step 6: Create Blog Components

BlogCard Component (components/blog/BlogCard.tsx)

tsx

import Image from 'next/image'
import Link from 'next/link'
import { BlogPost } from '@/lib/mdx'

interface BlogCardProps {
  post: BlogPost
  locale: string
  views?: number
}

export default function BlogCard({ post, locale, views }: BlogCardProps) {
  return (
    <Link href={`/${locale}/blog/${post.slug}`}>
      <article className="group cursor-pointer overflow-hidden rounded-xl border border-gray-200 bg-white transition-all duration-200 hover:border-blue-500 hover:shadow-lg">
        <div className="relative aspect-[16/9] overflow-hidden bg-gray-100">
          <Image
            src={post.heroImage}
            alt={post.title}
            fill
            className="object-cover transition-transform duration-300 group-hover:scale-105"
            sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
          />
          <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
        </div>
        
        <div className="p-6">
          <div className="mb-3 flex items-center gap-2">
            <span className="inline-block rounded-full bg-gradient-to-r from-blue-500 to-blue-600 px-3 py-1 text-xs font-medium text-white">
              {post.category}
            </span>
            {views !== undefined && (
              <span className="text-xs text-gray-500">{views} views</span>
            )}
          </div>
          
          <h2 className="mb-2 line-clamp-2 text-xl font-bold text-gray-900 transition-colors group-hover:text-blue-600">
            {post.title}
          </h2>
          
          <p className="mb-4 line-clamp-3 text-gray-600">
            {post.excerpt}
          </p>
          
          <div className="flex items-center justify-between text-sm text-gray-500">
            <time dateTime={post.date}>
              {new Date(post.date).toLocaleDateString(locale === 'fr' ? 'fr-FR' : 'en-US', {
                year: 'numeric',
                month: 'long',
                day: 'numeric'
              })}
            </time>
            <span>{post.readingTime}</span>
          </div>
        </div>
      </article>
    </Link>
  )
}

ViewCounter Component (components/blog/ViewCounter.tsx)

tsx

'use client'

import { useEffect, useState } from 'react'

interface ViewCounterProps {
  slug: string
  increment?: boolean
}

export default function ViewCounter({ slug, increment = false }: ViewCounterProps) {
  const [views, setViews] = useState<number | null>(null)

  useEffect(() => {
    async function fetchViews() {
      try {
        const endpoint = increment ? 'increment' : 'get'
        const response = await fetch(`/api/views/${slug}?action=${endpoint}`)
        const data = await response.json()
        setViews(data.views)
      } catch (error) {
        console.error('Failed to fetch views:', error)
      }
    }

    fetchViews()
  }, [slug, increment])

  if (views === null) return <span className="text-gray-500">...</span>

  return (
    <span className="text-gray-600">
      {views.toLocaleString()} {views === 1 ? 'view' : 'views'}
    </span>
  )
}

Step 7: Create Blog Pages

Blog Index Page (app/[locale]/blog/page.tsx)

tsx

import { Metadata } from 'next'
import { getAllPosts } from '@/lib/mdx'
import { getAllViews } from '@/lib/upstash'
import BlogCard from '@/components/blog/BlogCard'

interface BlogPageProps {
  params: { locale: string }
}

export async function generateMetadata({ params }: BlogPageProps): Promise<Metadata> {
  const { locale } = params
  
  return {
    title: locale === 'fr' ? 'Blog - Nibi' : 'Blog - Nibi',
    description: locale === 'fr' 
      ? 'Conseils et guides pour freelances au Québec'
      : 'Tips and guides for Quebec freelancers',
  }
}

export default async function BlogPage({ params }: BlogPageProps) {
  const { locale } = params
  const posts = await getAllPosts(locale)
  const allViews = await getAllViews()

  return (
    <div className="container mx-auto px-6 py-12">
      <div className="mb-12 text-center">
        <h1 className="mb-4 text-4xl font-bold text-gray-900 md:text-5xl">
          {locale === 'fr' ? 'Blog Nibi' : 'Nibi Blog'}
        </h1>
        <p className="text-xl text-gray-600">
          {locale === 'fr'
            ? 'Guides, conseils et actualités pour les freelances'
            : 'Guides, tips, and news for freelancers'}
        </p>
      </div>

      {posts.length === 0 ? (
        <p className="text-center text-gray-500">
          {locale === 'fr' ? 'Aucun article disponible.' : 'No posts available.'}
        </p>
      ) : (
        <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
          {posts.map((post) => (
            <BlogCard
              key={post.slug}
              post={post}
              locale={locale}
              views={allViews[post.slug]}
            />
          ))}
        </div>
      )}
    </div>
  )
}

Article Page (app/[locale]/blog/[slug]/page.tsx)

tsx

import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPostBySlug, getAllPosts, getRelatedPosts, generateTableOfContents } from '@/lib/mdx'
import { MDXRemote } from 'next-mdx-remote/rsc'
import ViewCounter from '@/components/blog/ViewCounter'
import RelatedPosts from '@/components/blog/RelatedPosts'
import TableOfContents from '@/components/blog/TableOfContents'
import ShareButtons from '@/components/blog/ShareButtons'
import NewsletterSignup from '@/components/blog/NewsletterSignup'

interface BlogPostPageProps {
  params: { locale: string; slug: string }
}

export async function generateStaticParams() {
  const locales = ['en', 'fr']
  const paths = []

  for (const locale of locales) {
    const posts = await getAllPosts(locale)
    for (const post of posts) {
      paths.push({ locale, slug: post.slug })
    }
  }

  return paths
}

export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
  const post = await getPostBySlug(params.slug, params.locale)
  
  if (!post) {
    return { title: 'Post Not Found' }
  }

  return {
    title: `${post.title} | Nibi Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.heroImage],
      type: 'article',
      publishedTime: post.date,
    },
  }
}

// Custom MDX components
const components = {
  h1: (props: any) => (
    <h1 className="mb-6 mt-8 text-4xl font-bold text-gray-900" {...props} />
  ),
  h2: (props: any) => (
    <h2 className="mb-4 mt-8 text-3xl font-bold text-gray-900" {...props} />
  ),
  h3: (props: any) => (
    <h3 className="mb-3 mt-6 text-2xl font-semibold text-gray-900" {...props} />
  ),
  p: (props: any) => (
    <p className="mb-4 leading-relaxed text-gray-700" {...props} />
  ),
  ul: (props: any) => (
    <ul className="mb-4 list-inside list-disc space-y-2 text-gray-700" {...props} />
  ),
  ol: (props: any) => (
    <ol className="mb-4 list-inside list-decimal space-y-2 text-gray-700" {...props} />
  ),
  blockquote: (props: any) => (
    <blockquote className="my-6 border-l-4 border-blue-500 bg-blue-50 p-6 rounded-r-lg" {...props} />
  ),
  code: (props: any) => (
    <code className="rounded bg-gray-100 px-2 py-1 text-sm font-mono text-gray-800" {...props} />
  ),
  pre: (props: any) => (
    <pre className="my-6 overflow-x-auto rounded-lg bg-gray-900 p-4 text-sm" {...props} />
  ),
}

export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const { locale, slug } = params
  const post = await getPostBySlug(slug, locale)

  if (!post) {
    notFound()
  }

  const relatedPosts = await getRelatedPosts(slug, locale, post.tags)
  const tableOfContents = await generateTableOfContents(post.content || '')

  return (
    <article className="container mx-auto px-6 py-12">
      {/* Hero Image */}
      <div className="relative mb-8 aspect-[2/1] overflow-hidden rounded-2xl">
        <img
          src={post.heroImage}
          alt={post.title}
          className="h-full w-full object-cover"
        />
        <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
      </div>

      {/* Article Header */}
      <header className="mb-8">
        <div className="mb-4 flex items-center gap-4 text-sm text-gray-600">
          <time dateTime={post.date}>
            {new Date(post.date).toLocaleDateString(locale === 'fr' ? 'fr-FR' : 'en-US', {
              year: 'numeric',
              month: 'long',
              day: 'numeric',
            })}
          </time>
          <span>•</span>
          <span>{post.readingTime}</span>
          <span>•</span>
          <ViewCounter slug={slug} increment />
        </div>
        
        <h1 className="mb-4 text-4xl font-bold text-gray-900 md:text-5xl">
          {post.title}
        </h1>
        
        <p className="text-xl text-gray-600">{post.excerpt}</p>
      </header>

      <div className="flex gap-12">
        {/* Main Content */}
        <div className="flex-1">
          <div className="prose prose-lg prose-blue max-w-none">
            <MDXRemote source={post.content || ''} components={components} />
          </div>

          {/* Newsletter Signup */}
          <div className="my-12">
            <NewsletterSignup locale={locale} />
          </div>

          {/* Related Posts */}
          {relatedPosts.length > 0 && (
            <div className="mt-12">
              <RelatedPosts posts={relatedPosts} locale={locale} />
            </div>
          )}
        </div>

        {/* Sidebar */}
        <aside className="hidden w-64 lg:block">
          <div className="sticky top-24">
            <ShareButtons
              url={`https://getnibi.com/${locale}/blog/${slug}`}
              title={post.title}
            />
            
            {tableOfContents.length > 0 && (
              <TableOfContents headings={tableOfContents} />
            )}
          </div>
        </aside>
      </div>
    </article>
  )
}

Step 8: Create API Routes

View Counter API (app/api/views/[slug]/route.ts)

typescript

import { NextRequest, NextResponse } from 'next/server'
import { incrementViews, getViews } from '@/lib/upstash'

export async function GET(
  request: NextRequest,
  { params }: { params: { slug: string } }
) {
  const { slug } = params
  const { searchParams } = new URL(request.url)
  const action = searchParams.get('action')
  
  // Get IP for rate limiting
  const forwarded = request.headers.get('x-forwarded-for')
  const ip = forwarded ? forwarded.split(',')[0] : request.headers.get('x-real-ip') ?? 'unknown'

  try {
    if (action === 'increment') {
      const views = await incrementViews(slug, ip)
      return NextResponse.json({ views })
    } else {
      const views = await getViews(slug)
      return NextResponse.json({ views })
    }
  } catch (error) {
    console.error('View counter error:', error)
    return NextResponse.json({ views: 0 })
  }
}

Step 9: Create Sample Content

Create your first blog post in both languages:

French Post (content/blog/fr/guide-tps-tvq-freelance.mdx)

mdx

---
title: "Guide complet TPS/TVQ pour freelances au Québec"
excerpt: "Tout ce que vous devez savoir sur la gestion des taxes en tant que travailleur autonome au Québec en 2024"
date: "2024-01-15"
author: "nibi-team"
category: "Taxes"
heroImage: "/blog/taxes-guide-hero.jpg"
tags: ["taxes", "tps", "tvq", "québec", "freelance"]
---

# Introduction

En tant que travailleur autonome au Québec, la gestion de la TPS et de la TVQ peut sembler complexe. Ce guide vous explique tout ce que vous devez savoir.

## Seuils d'inscription

Si vos revenus dépassent **30 000 $** sur une période de 12 mois, vous devez obligatoirement vous inscrire aux fichiers de la TPS/TVQ.

### Avantages de l'inscription volontaire

MĂŞme sous le seuil, l'inscription peut ĂŞtre avantageuse :
- Récupération des taxes sur vos dépenses d'affaires
- Crédibilité accrue auprès des clients corporatifs
- Meilleure gestion de votre trésorerie

## Calcul des taxes

Montant HT Ă— 5% = TPS Montant HT Ă— 9.975% = TVQ Total = Montant HT + TPS + TVQ


## Dates importantes Ă  retenir

- **Trimestriel** : Si revenus < 1,5 M$
- **Mensuel** : Si revenus > 6 M$
- **Annuel** : Si revenus < 50 000 $

N'oubliez pas de mettre de côté environ **15%** de vos revenus pour les taxes!

English Post (content/blog/en/gst-qst-guide-freelancers.mdx)

mdx

---
title: "Complete GST/QST Guide for Quebec Freelancers"
excerpt: "Everything you need to know about managing sales taxes as a self-employed worker in Quebec in 2024"
date: "2024-01-15"
author: "nibi-team"
category: "Taxes"
heroImage: "/blog/taxes-guide-hero.jpg"
tags: ["taxes", "gst", "qst", "quebec", "freelance"]
---

# Introduction

As a self-employed worker in Quebec, managing GST and QST can seem complex. This guide explains everything you need to know.

## Registration Thresholds

If your revenue exceeds **$30,000** over a 12-month period, you must register for GST/QST.

### Benefits of Voluntary Registration

Even below the threshold, registration can be beneficial:
- Recovery of taxes on business expenses
- Increased credibility with corporate clients
- Better cash flow management

## Tax Calculation

Amount Ă— 5% = GST Amount Ă— 9.975% = QST Total = Amount + GST + QST


## Important Dates to Remember

- **Quarterly**: If revenue < $1.5M
- **Monthly**: If revenue > $6M
- **Annual**: If revenue < $50,000

Don't forget to set aside about **15%** of your revenue for taxes!

Step 10: Style and Theme Integration

Update your tailwind.config.ts to ensure typography plugin is configured:

typescript

import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './content/**/*.{md,mdx}',
  ],
  theme: {
    extend: {
      // Your existing theme extensions
      typography: {
        DEFAULT: {
          css: {
            color: '#374151',
            a: {
              color: '#2563eb',
              '&:hover': {
                color: '#1d4ed8',
              },
            },
            'code::before': {
              content: '""',
            },
            'code::after': {
              content: '""',
            },
          },
        },
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
  ],
}

export default config

Step 11: Add RSS Feed Generation

Create app/[locale]/blog/rss.xml/route.ts:

typescript

import { NextRequest } from 'next/server'
import RSS from 'rss'
import { getAllPosts } from '@/lib/mdx'

export async function GET(
  request: NextRequest,
  { params }: { params: { locale: string } }
) {
  const { locale } = params
  const posts = await getAllPosts(locale)
  const siteUrl = 'https://getnibi.com'

  const feed = new RSS({
    title: locale === 'fr' ? 'Blog Nibi' : 'Nibi Blog',
    description: locale === 'fr'
      ? 'Conseils et guides pour freelances au Québec'
      : 'Tips and guides for Quebec freelancers',
    site_url: `${siteUrl}/${locale}/blog`,
    feed_url: `${siteUrl}/${locale}/blog/rss.xml`,
    language: locale,
    pubDate: new Date(),
  })

  posts.forEach((post) => {
    feed.item({
      title: post.title,
      description: post.excerpt,
      url: `${siteUrl}/${locale}/blog/${post.slug}`,
      date: post.date,
      categories: post.tags,
    })
  })

  return new Response(feed.xml(), {
    headers: {
      'Content-Type': 'application/xml',
      'Cache-Control': 's-maxage=3600, stale-while-revalidate',
    },
  })
}

Step 12: Testing and Deployment

Test your blog locally:

bash

# Development mode
npm run dev

# Build for production
npm run build

# Test production build
npm run start

Deploy to Vercel:

bash

# Commit your changes
git add .
git commit -m "Add bilingual MDX blog with Upstash view counting"
git push

# Deploy (automatic with Vercel)

Conclusion

You now have a fully functional, bilingual blog system that:

  • âś… Supports rich MDX content with custom components
  • âś… Tracks views in real-time with Upstash Redis
  • âś… Generates RSS feeds for both languages
  • âś… Provides excellent SEO with structured data
  • âś… Matches your existing design system perfectly
  • âś… Loads incredibly fast with static generation

This implementation gives you the flexibility of MDX, the performance of static generation, and the dynamic features of view counting—all while maintaining a clean, maintainable codebase that scales with your needs.

Troubleshooting Resources

Share this post