Hazem Mohammed

Next.js 16Partial PrerenderingSupabaseSSRAuthenticationServer ComponentsSuspenseNext.js Middleware

إتقان العرض المسبق الجزئي (PPR) في Next.js 16 مع مصادقة Supabase

Hazem Mohammed
٢٥ فبراير ٢٠٢٦
إتقان العرض المسبق الجزئي (PPR) في Next.js 16 مع مصادقة Supabase

مقدمة

يُحدث العرض المسبق الجزئي (PPR) في Next.js 16 ثورة في كيفية بناء تطبيقات الويب، حيث يجمع بين سرعة المواقع الثابتة وديناميكية العرض من جانب الخادم (SSR). يقدم PPR هيكلاً ثابتًا وفوريًا للصفحة، ثم يبث المكونات الديناميكية. ولكن كيف يتوافق هذا مع المحتوى المخصص للمستخدم، مثل حالة المصادقة؟

توضح هذه المقالة الفنية كيفية دمج مصادقة Supabase SSR مع PPR في Next.js 16، مما يتيح لك بناء تطبيقات سريعة للغاية وشخصية.

ما هو العرض المسبق الجزئي (PPR) ولماذا هو مهم؟

يعمل PPR عن طريق تقديم هيكل HTML ثابت للصفحة عند الطلب الأول. هذا الهيكل يتم إنشاؤه في وقت البناء (build time) ويتم تخزينه مؤقتًا على شبكات CDN، مما يؤدي إلى زمن وصول منخفض جدًا (Time to First Byte - TTFB). يتم تحديد الأجزاء الديناميكية من الصفحة باستخدام حدود <Suspense> من React. هذه "الثقوب" يتم بثها بعد ذلك من الخادم وتضاف إلى الهيكل الثابت بمجرد أن تصبح جاهزة.

التحدي الرئيسي يكمن في المصادقة. إذا كان الهيكل ثابتًا، فكيف يمكننا عرض "تسجيل الدخول" للمستخدمين غير المسجلين و "لوحة التحكم" للمستخدمين المسجلين بشكل صحيح؟ الحل يكمن في عزل منطق المصادقة داخل حدود <Suspense>.

التنفيذ: دمج Supabase SSR مع PPR

لتحقيق هذا التكامل، سنستخدم عميل Supabase SSR، وبرنامج وسيط (proxy.ts) لإدارة الجلسات، وحدود <Suspense> لعزل المكونات الديناميكية.

الخطوة 1: إعداد عميل Supabase من جانب الخادم

أولاً، نحتاج إلى طريقة لإنشاء عميل Supabase يمكنه الوصول إلى الكوكيز بأمان داخل مكونات الخادم. قم بإنشاء ملف أداة مساعدة لهذا الغرض.

// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'

export function createClient() {
  const cookieStore = cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SECRET_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
        set(name: string, value: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value, ...options })
          } catch (error) {
            // The `set` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing user sessions.
          }
        },
        remove(name: string, options: CookieOptions) {
          try {
            cookieStore.set({ name, value: '', ...options })
          } catch (error) {
            // The `delete` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing user sessions.
          }
        },
      },
    }
  )
}

الخطوة 2: إدارة الجلسة باستخدام proxy.ts (البرنامج الوسيط)

في Next.js 16، يتم التعامل مع منطق الوسيط في ملف proxy.ts. سيقوم هذا البرنامج باعتراض الطلبات الواردة، وتحديث جلسة المستخدم، والتأكد من أن حالة المصادقة متاحة في جميع أنحاء التطبيق. هذا أمر حيوي لأنه يجعل المسار ديناميكيًا عند الحاجة، وهو شرط للمصادقة.

// src/proxy.ts
import { NextResponse, type NextRequest } from 'next/server'
import { createServerClient } from '@supabase/ssr'

export async function middleware(request: NextRequest) {
  let response = NextResponse.next({ request: { headers: request.headers } })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SECRET_KEY!,
    {
      cookies: {
        get: (name) => request.cookies.get(name)?.value,
        set: (name, value, options) => {
          request.cookies.set({ name, value, ...options })
          response = NextResponse.next({ request: { headers: request.headers } })
          response.cookies.set({ name, value, ...options })
        },
        remove: (name, options) => {
          request.cookies.set({ name, value: '', ...options })
          response = NextResponse.next({ request: { headers: request.headers } })
          response.cookies.set({ name, value: '', ...options })
        },
      },
    }
  )

  // Refresh session if expired - crucial for Server Components
  await supabase.auth.getSession()

  return response
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
}

الخطوة 3: بناء الصفحة باستخدام <Suspense>

الآن، لنبني صفحتنا الرئيسية. ستكون الصفحة نفسها مكونًا ثابتًا، مما يتيح عرض الهيكل بسرعة. سنقوم بتغليف مكون حالة المصادقة الديناميكي داخل <Suspense>.

// app/page.tsx
import { Suspense } from 'react';
import AuthStatus from './components/AuthStatus';

export default function HomePage() {
  return (
    <main className="container mx-auto p-8">
      <header className="flex justify-between items-center border-b pb-4">
        <h1 className="text-2xl font-bold">PPR E-Commerce</h1>
        <Suspense fallback={<div className="h-8 w-24 bg-gray-200 rounded animate-pulse" />}>
          <AuthStatus />
        </Suspense>
      </header>

      <section className="mt-8">
        <h2 className="text-xl">Welcome to our store!</h2>
        <p>This part of the page is statically pre-rendered for maximum speed.</p>
      </section>
    </main>
  );
}

// app/components/AuthStatus.tsx
import { createClient } from '@/lib/supabase/server';
import LogoutButton from './LogoutButton';

export default async function AuthStatus() {
  const supabase = createClient();
  const { data: { user } } = await supabase.auth.getUser();

  return (
    <div className="flex items-center gap-4">
      {user ? (
        <>
          <span>Welcome, {user.email}</span>
          <LogoutButton />
        </>
      ) : (
        <a href="/login" className="bg-blue-500 text-white px-4 py-2 rounded">Login</a>
      )}
    </div>
  );
}

الخطوة 4: التعامل مع الإجراءات باستخدام Server Actions

أخيرًا، لنجعل زر تسجيل الخروج يعمل باستخدام Server Action. هذا يضمن أن منطق الخروج يعمل بأمان على الخادم دون الحاجة إلى واجهات برمجة تطبيقات من جانب العميل.

// app/components/LogoutButton.tsx
'use client';

import { createClient } from '@/lib/supabase/client'; // Note: using client component
import { useRouter } from 'next/navigation';

export default function LogoutButton() {
  const router = useRouter();
  
  const handleLogout = async () => {
    const supabase = createClient();
    await supabase.auth.signOut();
    router.refresh();
  };

  return (
    <button 
      onClick={handleLogout} 
      className="bg-red-500 text-white px-4 py-2 rounded"
    >
      Logout
    </button>
  );
}

// We need a client-side Supabase instance for this component
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

حالة استخدام واقعية

تخيل صفحة رئيسية لموقع تجارة إلكترونية. يمكن أن يكون وصف المنتج، والصور، والمراجعات جزءًا من الهيكل الثابت الذي يتم تحميله فورًا لتحسين محركات البحث (SEO) وتجربة المستخدم. وفي الوقت نفسه، يتم بث المكونات المخصصة للمستخدم مثل رأس الصفحة (الذي يعرض "مرحبًا، [اسم المستخدم]") وحالة عربة التسوق داخل حدود <Suspense>. هذا يضمن حصول المستخدمين على تجربة سريعة وسلسة مع محتوى مخصص.

الخلاصة

يعتبر العرض المسبق الجزئي (PPR) في Next.js 16 أداة قوية لتحسين الأداء. عند دمجه مع مصادقة Supabase SSR، يتطلب الأمر بنية مدروسة. باستخدام proxy.ts لإدارة الجلسات و <Suspense> لعزل المكونات الديناميكية، يمكنك تحقيق التوازن المثالي بين سرعة التحميل الفورية والتفاعلية المخصصة للمستخدم، وتقديم تجارب ويب حديثة ومتطورة.

HM

Hazem Mohammed

مطور ويب متخصص في هندسة تطبيقات حديثة، سريعة، وقابلة للتوسع.