- sessions: require REMIX_SECRET in prod (no "secret" fallback); base Secure cookie on NODE_ENV; add maxAge - login: safeRedirect() blocks open-redirect via redirectTo (loader + action) - SSR loaders: encodeURIComponent on user path params before internal API fetch (product, store product, seller) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
import { json, LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
|
import { useLoaderData } from "@remix-run/react";
|
|
import { ProductDetail as ProductDetailType } from "../../src/api/types";
|
|
import { ProductDetailView } from "../components/product/ProductDetailView";
|
|
import { ClientOnly } from "../components/ClientOnly";
|
|
import { getInternalApiUrl } from "~/utils/api-config";
|
|
|
|
export const meta: MetaFunction<typeof loader> = ({ data, params }) => {
|
|
const product = data?.product;
|
|
const siteUrl = data?.siteUrl;
|
|
|
|
if (!product) {
|
|
return [
|
|
{ title: "محصول یافت نشد | ویترون" },
|
|
{ name: "description", content: "متاسفانه محصول مورد نظر یافت نشد." },
|
|
{ name: "robots", content: "noindex, follow" },
|
|
];
|
|
}
|
|
|
|
const productTitle = product.title;
|
|
const productDescription =
|
|
product.description || "مشاهده جزئیات محصول در فروشگاه ویترون";
|
|
|
|
// Ensure image URL is absolute
|
|
const baseImageUrl = product.imageUrls?.[0];
|
|
const imageUrl = baseImageUrl
|
|
? baseImageUrl.startsWith("http")
|
|
? baseImageUrl
|
|
: `${siteUrl}${baseImageUrl}`
|
|
: `${siteUrl}/og-image.svg`;
|
|
|
|
return [
|
|
{ title: `${productTitle} - ویترون` },
|
|
{ name: "description", content: productDescription },
|
|
{
|
|
name: "keywords",
|
|
content: `${productTitle}، خرید آنلاین، فروشگاه آنلاین، ویترون، محصول`,
|
|
},
|
|
{ name: "robots", content: "index, follow" },
|
|
|
|
// Open Graph
|
|
{ property: "og:title", content: `${productTitle} - ویترون` },
|
|
{ property: "og:description", content: productDescription },
|
|
{ property: "og:type", content: "product" },
|
|
{ property: "og:image", content: imageUrl },
|
|
{ property: "og:image:width", content: "1200" },
|
|
{ property: "og:image:height", content: "630" },
|
|
{ property: "og:image:alt", content: productTitle },
|
|
{
|
|
property: "og:url",
|
|
content: `${siteUrl}/store/${params?.storeId}/product/${product.id}`,
|
|
},
|
|
{ property: "og:site_name", content: "ویترون" },
|
|
{ property: "og:locale", content: "fa_IR" },
|
|
|
|
// Twitter Card
|
|
{ name: "twitter:card", content: "summary_large_image" },
|
|
{ name: "twitter:title", content: `${productTitle} - ویترون` },
|
|
{ name: "twitter:description", content: productDescription },
|
|
{ name: "twitter:image", content: imageUrl },
|
|
];
|
|
};
|
|
|
|
interface LoaderData {
|
|
product: ProductDetailType;
|
|
siteUrl: string;
|
|
}
|
|
|
|
// Server-side loader
|
|
export async function loader({ params }: LoaderFunctionArgs) {
|
|
const { id } = params;
|
|
try {
|
|
const response = await fetch(
|
|
`${getInternalApiUrl()}/api/product/v1/products/${encodeURIComponent(id ?? "")}/`,
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch product: ${response.status}`);
|
|
}
|
|
|
|
const product = await response.json();
|
|
const siteUrl = process.env.VITE_SITE_URL || "https://vitrown.com";
|
|
return json<LoaderData>({ product, siteUrl });
|
|
} catch (error) {
|
|
console.error("Error fetching product:", error);
|
|
throw new Response("Error loading product", { status: 500 });
|
|
}
|
|
}
|
|
|
|
// Main component
|
|
export default function ProductDetail() {
|
|
const { product } = useLoaderData<typeof loader>();
|
|
|
|
return (
|
|
<ClientOnly>
|
|
{() => <ProductDetailView product={product} isOwner={true} />}
|
|
</ClientOnly>
|
|
);
|
|
}
|