This commit is contained in:
payacom
2026-10-09 15:44:12 +03:30
parent f364f989ed
commit 551260a836
14 changed files with 308 additions and 120 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "modstagram-next",
"version": "4.2.0",
"version": "4.4.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack --port 3004",

View File

@@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import AuthHead from "@/components/auth/AuthHead";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import useAxios from "@/hooks/useAxios";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
@@ -14,6 +14,7 @@ import toast from "react-hot-toast";
import { getSafeRedirectPath } from "@/lib/auth/postLogin";
import { useTranslation } from "react-i18next";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IProfileData } from "@/types/types";
function RegisterCompletePage() {
@@ -21,6 +22,16 @@ function RegisterCompletePage() {
const router = useRouter();
const { request, loading } = useAxios();
const [action, setAction] = useState<"app" | "continue" | null>(null);
const [user, setUser] = useState<IProfileData | null>(null);
useEffect(() => {
request<{ user: IProfileData }>("GET", "/profile")
.then((res) => setUser(res?.user ?? null))
.catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ");
// مطابق درخواست: عکس پروفایل حالا زودتر، توی خودِ صفحه‌ی register/avatar گرفته
// می‌شه (قبل از این صفحه) — پس دیگه لازم نیست ویزارد تایید هویت با verify/avatar
@@ -49,7 +60,10 @@ function RegisterCompletePage() {
if (next === "continue") {
await goToVerifyWizard();
} else {
router.push(getSafeRedirectPath());
// navigation سخت — همون دلیلِ Logout.tsx: router.push ممکنه نسخه‌ی
// کش‌شده‌ی فید رو از زمانی که هنوز مهمان بودیم (قبل از تکمیلِ ثبت‌نام،
// ریدایرکت‌شده به ورود) دوباره نشون بده، بدون اجرای دوباره‌ی میدل‌ور
window.location.href = getSafeRedirectPath();
}
} catch (err: any) {
toast.error(
@@ -65,32 +79,53 @@ function RegisterCompletePage() {
<AuthPageLayout>
<div className="flex w-full flex-1 flex-col items-center">
<AuthPageContent>
<AuthHead title={t("auth.registerCompleteTitle")} />
<p className="text-sm text-gray-500 text-center max-w-sm mt-2">
<div className="flex flex-col items-center">
<Image
src={
user?.profile_image
? IMAGE_BASE_URL + user.profile_image
: "/images/fake-avatar.png"
}
alt=""
width={72}
height={72}
unoptimized
className="mb-2 h-[72px] w-[72px] rounded-full object-cover bg-gray-100"
/>
{name ? (
<span className="text-base font-bold text-foreground">
{name}
</span>
) : null}
{user?.user_name ? (
<span className="text-sm text-gray-500">{user.user_name}</span>
) : null}
</div>
<p className="text-sm text-gray-500 text-center max-w-sm mt-6">
{t("auth.registerCompleteDesc")}
</p>
</AuthPageContent>
<AuthFormFooter>
<div className="flex flex-col sm:flex-row gap-3 w-full max-w-sm justify-center items-center">
<AuthNextButton
type="button"
onClick={() => finishRegistration("continue")}
className="!border-[#16a34a] !text-[#16a34a] w-full sm:w-auto"
disabled={loading}
loading={loading && action === "continue"}
>
{t("auth.continueRegistration")}
</AuthNextButton>
<div className="flex flex-row gap-3 w-full max-w-sm justify-center items-center">
<AuthNextButton
type="button"
onClick={() => finishRegistration("app")}
className="!border-[#FF0000] !text-[#FF0000] w-full sm:w-auto"
className="!border-[#16a34a] !text-[#16a34a] flex-1"
disabled={loading}
loading={loading && action === "app"}
>
{t("auth.enterApp")}
</AuthNextButton>
<AuthNextButton
type="button"
onClick={() => finishRegistration("continue")}
className="!border-[#FF0000] !text-[#FF0000] flex-1"
disabled={loading}
loading={loading && action === "continue"}
>
{t("auth.continueRegistration")}
</AuthNextButton>
</div>
</AuthFormFooter>
</div>

View File

@@ -3,6 +3,7 @@ import { Suspense, cache } from "react";
import { cookies } from "next/headers";
import ProjectDetailClient from "./ProjectDetailClient";
import PageLoader from "@/components/ui/PageLoader";
import GuestAuthPromptModal from "@/components/auth/GuestAuthPromptModal";
import { generatePageMetadata } from "@/utils/generatePageMetadata";
import { fetchProject } from "@/api/fetchProject";
import {
@@ -67,6 +68,7 @@ export default async function ProjectDetailPage({
<Suspense fallback={<PageLoader className="min-h-[50vh]" />}>
<ProjectDetailClient id={id} />
</Suspense>
<GuestAuthPromptModal />
</>
);
}

View File

@@ -1,5 +1,6 @@
import { BASE_URL, IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import BillboardDetailContent from "@/components/billboards/BillboardDetailContent";
import GuestAuthPromptModal from "@/components/auth/GuestAuthPromptModal";
import { IAdvertising, IRate } from "@/types/types";
import { cookies } from "next/headers";
import { cache } from "react";
@@ -135,6 +136,7 @@ async function BillboardPage({ params }: IBillboardProps) {
}}
/>
<BillboardDetailContent billboard={billboard} rate={rate} />
<GuestAuthPromptModal />
</>
);
}

View File

@@ -8,6 +8,7 @@ import {
buildPostSeoTitle,
} from "@/lib/postSlug";
import ExploreReelClient from "./ExploreReelClient";
import GuestAuthPromptModal from "@/components/auth/GuestAuthPromptModal";
import { getServerLanguage } from "@/lib/i18n/server";
import { translateCommon } from "@/lib/i18n/translate";
@@ -44,5 +45,10 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
export default async function ExploreReelPage({ params }: Props) {
const { id } = await params;
return <ExploreReelClient id={id} />;
return (
<>
<ExploreReelClient id={id} />
<GuestAuthPromptModal />
</>
);
}

View File

@@ -182,6 +182,14 @@
html {
scroll-behavior: smooth;
/* بدون این، Chrome روی گوشی‌هایی که «Force dark برای محتوای وب» روشنه (پیش‌فرض بعضی
بیلدهای شیائومی/سامسونگ) فکر می‌کنه این سایت از تم تیره پشتیبانی نمی‌کنه و خودش
رنگ‌های input/text رو الگوریتمی معکوس می‌کنه — دقیقاً همون چیزی که باعث می‌شد نام/
نام‌خانوادگی توی تم تیره ناخوانا بشه. اعلامِ صریحِ color-scheme این رفتار رو خاموش می‌کنه */
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
body {
direction: rtl;

View File

@@ -2,6 +2,7 @@ import { Metadata } from "next";
import { cache } from "react";
import { fetchPostById } from "@/api/fetchPostById";
import PostFeedView, { PostFeedSearchParams } from "@/components/posts/PostFeedView";
import GuestAuthPromptModal from "@/components/auth/GuestAuthPromptModal";
import { buildStorageUrl } from "@/components/main/BaseUrl";
import { generatePageMetadata } from "@/utils/generatePageMetadata";
import {
@@ -125,6 +126,7 @@ export default async function PostPage({ params, searchParams }: Props) {
showClose
searchParams={feedSearchParams}
/>
<GuestAuthPromptModal />
</main>
);
}

View File

@@ -14,18 +14,24 @@ type GeoCheckResult = {
isProxy: boolean;
};
const DISMISSED_KEY = "vpn_warning_dismissed";
// عینِ VpnWarningGate.tsx اندروید: هر بار اپِ وب لود می‌شه (mount ریشه‌ی layout.tsx)
// چک می‌شه که کاربر با آی‌پیِ غیرایران یا VPN/پراکسی وصل شده یا نه؛ اگه بله، هم یه
// toast (معادلِ نوتیفِ اندروید — چون وب پرمیشنِ Notification API لازم داره) هم یه
// مودالِ غیراجباری نشون داده می‌شه
// مودالِ غیراجباری با دکمه‌ی تایید + تیکِ «دیگر نشان نده» کنارش نشون داده می‌شه. با
// زدنِ اون تیک، دیگه هیچ‌وقت (حتی خودِ چکِ شبکه‌ش) دوباره اجرا نمی‌شه
export default function VpnWarningGate() {
const { t } = useTranslation("common");
const [open, setOpen] = useState(false);
const [checked, setChecked] = useState(false);
useEffect(() => {
let cancelled = false;
(async () => {
try {
if (localStorage.getItem(DISMISSED_KEY) === "1") return;
const res = await apiClient.get<GeoCheckResult>(
`${getApiBaseUrl()}/geo/check`
);
@@ -42,20 +48,38 @@ export default function VpnWarningGate() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const close = () => {
setOpen(false);
if (checked) {
localStorage.setItem(DISMISSED_KEY, "1");
}
};
return (
<Modal isOpen={open} onClose={() => setOpen(false)} height="auto" elevated>
<Modal isOpen={open} onClose={close} height="auto" elevated>
<div className="flex flex-col items-center gap-4 py-2 text-center">
<h2 className="text-lg font-bold">{t("settings.vpnWarningTitle")}</h2>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t("settings.vpnWarningMessage")}
</p>
<RoundedButton
variant="primary"
className="mt-2 h-10 w-40"
onClick={() => setOpen(false)}
>
{t("settings.vpnWarningAction")}
</RoundedButton>
<div className="mt-2 flex w-full max-w-xs items-center justify-center gap-4">
<RoundedButton
variant="primary"
className="h-10 flex-1"
onClick={close}
>
{t("settings.vpnWarningAction")}
</RoundedButton>
<label className="flex shrink-0 cursor-pointer items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400">
<input
type="checkbox"
className="scale-110"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
{t("settings.vpnWarningDontShowAgain")}
</label>
</div>
</div>
</Modal>
);

View File

@@ -0,0 +1,57 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import Modal from "@/components/elements/Modal";
import RoundedButton from "@/components/elements/RoundedButton";
import { isAuthenticated } from "@/lib/auth/session";
// روی صفحه‌ی تکی‌ِ پست/ریلز/بیلبورد/پروژه (قابل‌اشتراک‌گذاری و قابل‌دیدن برای گوگل
// و مهمان، مثل اینستاگرام) — اگه کاربر لاگین نکرده، همین مودالِ پایین‌صفحه (قابل
// بستن) رو نشون بده با دو دکمه‌ی ورود/ثبت‌نام؛ خودِ محتوا و رفتارهایی مثل لایک/کامنت
// دست‌نخورده می‌مونه (لایک از قبل با useRequireAuth کاربرِ مهمان رو به ورود می‌فرسته)
export default function GuestAuthPromptModal() {
const { t } = useTranslation("common");
const router = useRouter();
const [open, setOpen] = useState(false);
useEffect(() => {
if (!isAuthenticated()) setOpen(true);
}, []);
const goTo = (path: "/login" | "/register") => {
const current =
typeof window !== "undefined"
? window.location.pathname + window.location.search
: "/";
setOpen(false);
router.push(`${path}?redirect=${encodeURIComponent(current)}`);
};
return (
<Modal isOpen={open} onClose={() => setOpen(false)} height="fit" elevated>
<div className="mx-auto w-full max-w-sm">
<p className="mb-4 text-center text-sm">{t("auth.guestPromptMessage")}</p>
<div className="flex flex-row gap-3">
<RoundedButton
type="button"
variant="primary"
className="h-10 flex-1"
onClick={() => goTo("/login")}
>
{t("auth.guestPromptLogin")}
</RoundedButton>
<RoundedButton
type="button"
variant="default"
className="h-10 flex-1"
onClick={() => goTo("/register")}
>
{t("auth.guestPromptRegister")}
</RoundedButton>
</div>
</div>
</Modal>
);
}

View File

@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
import { useRouter } from "next/navigation";
import { btnPrimary, btnDefault } from "@/lib/ui/buttonStyles";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
@@ -27,7 +26,6 @@ export default function AccountActionModals({
}: AccountActionModalsProps) {
const { t } = useTranslation("common");
const { request } = useAxios();
const router = useRouter();
const [step, setStep] = useState<Step>("confirm");
const [typed, setTyped] = useState("");
const [loading, setLoading] = useState(false);
@@ -59,7 +57,8 @@ export default function AccountActionModals({
await request("POST", "/account/delete", { user_name: typed.trim() });
await clearAuthSession();
toast.success(t("settings.accountActions.deleted"));
router.push("/login");
// navigation سخت — همون دلیلِ Logout.tsx، کَشِ کلاینتیِ روتر رو دور می‌زنه
window.location.href = "/login";
}
} catch (err) {
toast.error(

View File

@@ -1,5 +1,4 @@
"use client";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import Modal from "../../elements/Modal";
import RoundedButton from "../../elements/RoundedButton";
@@ -10,11 +9,12 @@ import { useTranslation } from "react-i18next";
function Logout() {
const { t } = useTranslation("common");
const [showModal, setShowModal] = useState<boolean>(false);
const router = useRouter();
const logoutHandler = async () => {
await clearAuthSession();
router.push("/login");
router.refresh();
// router.push (soft navigation) می‌تونه کَشِ کلاینتیِ روتر (RSC payload قبلیِ
// صفحه‌ی اصلی، از وقتی لاگین بودیم) رو دوباره نشون بده بدون اینکه میدل‌ور
// دوباره اجرا بشه — navigation سخت، درخواست تازه به سرور می‌زنه
window.location.href = "/login";
};
return (
<>

View File

@@ -215,8 +215,9 @@
},
"settings": {
"vpnWarningTitle": "VPN usage detected",
"vpnWarningMessage": "For a better experience on the Modstagram platform, please turn off your VPN.",
"vpnWarningAction": "Got it",
"vpnWarningMessage": "Turn off your VPN for a better experience with Modstagram",
"vpnWarningAction": "Confirm",
"vpnWarningDontShowAgain": "Don't show again",
"title": "Settings",
"metaTitle": "Settings | Modstagram",
"metaDescription": "Account settings on Modstagram",
@@ -1183,6 +1184,9 @@
"savePasswordFailed": "Could not save password",
"registerCompleteTitle": "Initial registration complete",
"registerCompleteDesc": "You can enter the app or continue full registration (identity verification and profile).",
"guestPromptMessage": "Log in or sign up to see more and use all of Modstagram's features",
"guestPromptLogin": "Log in",
"guestPromptRegister": "Sign up",
"identityVerification": "Identity verification",
"forIdentityVerification": "For identity verification",
"nationalCodePlaceholder": "National ID",

View File

@@ -215,8 +215,9 @@
},
"settings": {
"vpnWarningTitle": "استفاده از VPN شناسایی شد",
"vpnWarningMessage": "برای تجربه بهتر از پلتفرم مدستاگرام، لطفاً VPN خود را خاموش کنید.",
"vpnWarningAction": "متوجه شدم",
"vpnWarningMessage": "برای تجربه بهتر از پلتفرم مدستاگرام VPN خود را خاموش کنید",
"vpnWarningAction": "تایید",
"vpnWarningDontShowAgain": "دیگر نشان نده",
"title": "تنظیمات",
"metaTitle": "تنظیمات | مدستاگرام",
"metaDescription": "تنظیمات حساب کاربری در مدستاگرام",
@@ -1183,6 +1184,9 @@
"savePasswordFailed": "ذخیره کلمه عبور ناموفق بود",
"registerCompleteTitle": "ثبت‌نام اولیه تکمیل شد",
"registerCompleteDesc": "می‌توانید وارد برنامه شوید یا ثبت‌نام کامل (تایید هویت و پروفایل) را ادامه دهید.",
"guestPromptMessage": "برای دیدن کامل و استفاده از امکانات مدستاگرام وارد شوید یا ثبت‌نام کنید",
"guestPromptLogin": "ورود",
"guestPromptRegister": "ثبت‌نام",
"identityVerification": "احراز هویت",
"forIdentityVerification": "جهت احراز هویت",
"nationalCodePlaceholder": "کد ملی",

View File

@@ -1,82 +1,127 @@
import { NextRequest, NextResponse } from "next/server";
import {
buildLoginRedirect,
requiresRegistrationToken,
} from "@/lib/auth/registrationRoutes";
import { LANGUAGE_COOKIE } from "@/lib/i18n/registry";
// فعلاً فقط en به‌صورت prefix مسیر پشتیبانی می‌شود؛ fa همچنان بدون prefix
// (روش سبک: بدون تغییر ساختار src/app، فقط rewrite داخلی + هدر زبان)
const LOCALE_PREFIXES = ["en"] as const;
type LocalePrefix = (typeof LOCALE_PREFIXES)[number];
function stripLocalePrefix(pathname: string): {
locale: LocalePrefix | null;
path: string;
} {
for (const locale of LOCALE_PREFIXES) {
if (pathname === `/${locale}`) return { locale, path: "/" };
if (pathname.startsWith(`/${locale}/`)) {
return { locale, path: pathname.slice(locale.length + 1) };
}
}
return { locale: null, path: pathname };
}
export function middleware(req: NextRequest) {
const isPrefetch =
req.headers.get("next-router-prefetch") === "1" ||
req.headers.get("purpose") === "prefetch";
const { locale, path: pathname } = stripLocalePrefix(req.nextUrl.pathname);
const localePrefix = locale ? `/${locale}` : "";
// prefetch بدون کوکی نباید redirect به /login کش شود
if (!isPrefetch) {
const token = req.cookies.get("token")?.value?.trim();
// صفحاتی که ذاتاً نیازمند ورود هستند (حساب کاربری و ساخت محتوا)
const protectedPaths = ["/settings", "/new-post", "/new-project"];
const needsAuth = protectedPaths.some((p) => pathname.startsWith(p));
if (needsAuth && !token) {
const loginUrl = new URL(`${localePrefix}/login`, req.url);
loginUrl.searchParams.set("redirect", pathname);
return NextResponse.redirect(loginUrl);
}
// مراحل میانی ثبت‌نام بدون توکن → ورود
if (requiresRegistrationToken(pathname) && !token) {
return NextResponse.redirect(
new URL(`${localePrefix}${buildLoginRedirect(pathname)}`, req.url)
);
}
}
if (locale) {
const rewriteUrl = req.nextUrl.clone();
rewriteUrl.pathname = pathname;
const requestHeaders = new Headers(req.headers);
requestHeaders.set("x-app-language", locale);
const res = NextResponse.rewrite(rewriteUrl, {
request: { headers: requestHeaders },
});
// برای اینکه ناوبری‌های بعدی (بدون prefix) هم انگلیسی بمونن، مثل انتخاب
// دستی زبان از سوییچر عمل می‌کنیم
res.cookies.set(LANGUAGE_COOKIE, locale, {
path: "/",
maxAge: 60 * 60 * 24 * 365,
sameSite: "lax",
});
return res;
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)"],
};
import { NextRequest, NextResponse } from "next/server";
import {
buildLoginRedirect,
requiresRegistrationToken,
} from "@/lib/auth/registrationRoutes";
import { LANGUAGE_COOKIE } from "@/lib/i18n/registry";
// فعلاً فقط en به‌صورت prefix مسیر پشتیبانی می‌شود؛ fa همچنان بدون prefix
// (روش سبک: بدون تغییر ساختار src/app، فقط rewrite داخلی + هدر زبان)
const LOCALE_PREFIXES = ["en"] as const;
type LocalePrefix = (typeof LOCALE_PREFIXES)[number];
function stripLocalePrefix(pathname: string): {
locale: LocalePrefix | null;
path: string;
} {
for (const locale of LOCALE_PREFIXES) {
if (pathname === `/${locale}`) return { locale, path: "/" };
if (pathname.startsWith(`/${locale}/`)) {
return { locale, path: pathname.slice(locale.length + 1) };
}
}
return { locale: null, path: pathname };
}
// صفحاتی که کاربر مهمان (بدون توکن) همیشه باید بتونه ببینه: خودِ صفحات
// ورود/ثبت‌نام/قانونی، و callback اکانتِ گوگل/اینستاگرام
const PUBLIC_EXACT_PATHS = new Set([
"/login",
"/login-with-username",
"/login-2fa",
"/forget-password",
"/forget-password-otp",
"/change-passowrd",
"/verify-otp",
"/register",
"/register-otp",
"/about-us",
"/privacy",
"/instagram-data-deletion",
]);
const PUBLIC_PREFIXES = ["/auth/", "/sitemaps/", "/storage/"];
// صفحه‌ی تکی‌ِ یک پست/ریلز/بیلبورد/پروژه که به‌اشتراک گذاشته می‌شه — باید عین
// اینستاگرام برای گوگل و کاربرِ مهمان قابل‌دیدن بمونه (SEO + پیش‌نمایشِ لینکِ
// اشتراک‌گذاری)؛ خودِ مودالِ ورود/ثبت‌نام و قفل‌بودنِ اقدام‌هایی مثل لایک سمتِ
// کلاینت مدیریت می‌شه، نه اینجا
const RESERVED_FIRST_SEGMENT: Record<string, Set<string>> = {
billboards: new Set(["new", "payment", "profile"]),
projects: new Set(["new", "payment", "specialists"]),
};
function isSharedDetailPath(pathname: string, root: "billboards" | "projects"): boolean {
const match = pathname.match(new RegExp(`^/${root}/([^/]+)/([^/]+)$`));
if (!match) return false;
return !RESERVED_FIRST_SEGMENT[root].has(match[1]);
}
function isPublicGuestPath(pathname: string): boolean {
if (PUBLIC_EXACT_PATHS.has(pathname)) return true;
if (PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) return true;
if (/^\/posts\/[^/]+(\/.*)?$/.test(pathname)) return true;
if (/^\/explore\/[^/]+$/.test(pathname)) return true;
if (isSharedDetailPath(pathname, "billboards")) return true;
if (isSharedDetailPath(pathname, "projects")) return true;
return false;
}
export function middleware(req: NextRequest) {
const isPrefetch =
req.headers.get("next-router-prefetch") === "1" ||
req.headers.get("purpose") === "prefetch";
const { locale, path: pathname } = stripLocalePrefix(req.nextUrl.pathname);
const localePrefix = locale ? `/${locale}` : "";
// prefetch بدون کوکی نباید redirect به /login کش شود
if (!isPrefetch) {
const token = req.cookies.get("token")?.value?.trim();
if (!token) {
// مراحل میانی ثبت‌نام: نیاز به توکنِ مرحله‌ایِ خودِ ثبت‌نام (redirect با پارامترِ
// بازگشتِ دقیق به همون مرحله)
if (requiresRegistrationToken(pathname)) {
return NextResponse.redirect(
new URL(`${localePrefix}${buildLoginRedirect(pathname)}`, req.url)
);
}
// کاربر مهمان مستقیم وارد خودِ اپ نمی‌شه — اول باید ورود/ثبت‌نام کنه؛ فقط
// صفحاتِ عمومی (ورود/ثبت‌نام/قانونی) و صفحه‌ی تکی‌ِ اشتراک‌گذاری‌شده
// (پست/ریلز/بیلبورد/پروژه) از این قاعده مستثنی‌اند
if (!isPublicGuestPath(pathname)) {
const loginUrl = new URL(`${localePrefix}/login`, req.url);
loginUrl.searchParams.set("redirect", pathname);
return NextResponse.redirect(loginUrl);
}
}
}
if (locale) {
const rewriteUrl = req.nextUrl.clone();
rewriteUrl.pathname = pathname;
const requestHeaders = new Headers(req.headers);
requestHeaders.set("x-app-language", locale);
const res = NextResponse.rewrite(rewriteUrl, {
request: { headers: requestHeaders },
});
// برای اینکه ناوبری‌های بعدی (بدون prefix) هم انگلیسی بمونن، مثل انتخاب
// دستی زبان از سوییچر عمل می‌کنیم
res.cookies.set(LANGUAGE_COOKIE, locale, {
path: "/",
maxAge: 60 * 60 * 24 * 365,
sameSite: "lax",
});
return res;
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)"],
};