feat(website): Next.js 16 marketing website with RTL/Farsi
Marketing website for Meezi platform: - Server-side rendered pages: home, demo, blog, pricing - RTL/Farsi layout with Vazirmatn font - SEO metadata and Open Graph tags - proxy.ts for Next.js 16 middleware convention - MEEZI_API_URL internal Docker network routing Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
const API_URL = process.env.MEEZI_API_URL ?? "http://localhost:5001";
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) {
|
||||
const { slug } = await Promise.resolve(params);
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_URL}/api/public/website/posts/${encodeURIComponent(slug)}/comments`,
|
||||
{ next: { revalidate: 60 } }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ comments: [] });
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
// Unwrap ApiResponse<T>: { success, data } → normalize to { comments: [...] }
|
||||
const payload = json?.data ?? json;
|
||||
const comments = Array.isArray(payload) ? payload : [];
|
||||
return NextResponse.json({ comments });
|
||||
} catch {
|
||||
// API not available — return empty list gracefully
|
||||
return NextResponse.json({ comments: [] });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { slug: string } }
|
||||
) {
|
||||
const { slug } = await Promise.resolve(params);
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { authorName, authorEmail, content } = body as {
|
||||
authorName?: string;
|
||||
authorEmail?: string;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
if (!authorName || !content) {
|
||||
return NextResponse.json(
|
||||
{ error: "authorName and content are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_URL}/api/public/website/posts/${encodeURIComponent(slug)}/comments`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ authorName, authorEmail, content }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
return NextResponse.json(
|
||||
{ error: text || "Upstream error" },
|
||||
{ status: res.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data, { status: 201 });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to reach API" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
interface DemoRequest {
|
||||
name: string;
|
||||
business: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
branches: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = (await req.json()) as DemoRequest;
|
||||
|
||||
// Validate required fields
|
||||
if (!body.name?.trim() || !body.business?.trim() || !body.phone?.trim()) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Missing required fields" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Basic phone validation (Iranian format)
|
||||
const phoneClean = body.phone.replace(/\s|-/g, "");
|
||||
if (phoneClean.length < 10) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Invalid phone number" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// In production: send email via SMTP / save to DB / forward to CRM
|
||||
// For now: forward to Meezi API
|
||||
const apiBase = process.env.MEEZI_API_URL ?? "https://api.meezi.ir";
|
||||
try {
|
||||
await fetch(`${apiBase}/api/public/demo-requests`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
contactName: body.name.trim(),
|
||||
businessName: body.business.trim(),
|
||||
phone: phoneClean,
|
||||
email: body.email?.trim() || null,
|
||||
branchCount: body.branches,
|
||||
notes: body.message?.trim() || null,
|
||||
source: "website",
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// Don't fail the request if backend is unreachable — log and continue
|
||||
console.error("[demo] Failed to forward to API");
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
/** Strip non-Latin characters to avoid satori font shaping issues with Arabic/Persian. */
|
||||
function safeText(str: string, fallback: string): string {
|
||||
const cleaned = str.replace(/[^ -ɏ\s]/g, "").trim();
|
||||
return cleaned.length > 2 ? cleaned : fallback;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const rawTitle = searchParams.get("t") ?? "Meezi";
|
||||
const rawSub = searchParams.get("s") ?? "Smart Cafe & Restaurant Management";
|
||||
const page = searchParams.get("page") ?? "meezi.ir";
|
||||
|
||||
const title = safeText(rawTitle, "Meezi");
|
||||
const sub = safeText(rawSub, "Smart Cafe & Restaurant Management");
|
||||
const tag = safeText(page, "meezi.ir");
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "linear-gradient(140deg, #052e16 0%, #14532d 55%, #166534 100%)",
|
||||
padding: "72px 80px",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Top bar */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "14px" }}>
|
||||
{/* Logo mark */}
|
||||
<div
|
||||
style={{
|
||||
width: "52px",
|
||||
height: "52px",
|
||||
borderRadius: "14px",
|
||||
background: "rgba(255,255,255,0.12)",
|
||||
border: "1px solid rgba(255,255,255,0.18)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "26px",
|
||||
fontWeight: "900",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
M
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "26px",
|
||||
fontWeight: "700",
|
||||
color: "rgba(255,255,255,0.9)",
|
||||
letterSpacing: "-0.3px",
|
||||
}}
|
||||
>
|
||||
Meezi
|
||||
</span>
|
||||
|
||||
{/* Spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Right badge */}
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(134,239,172,0.15)",
|
||||
border: "1px solid rgba(134,239,172,0.3)",
|
||||
borderRadius: "999px",
|
||||
padding: "6px 16px",
|
||||
fontSize: "13px",
|
||||
color: "#86efac",
|
||||
fontWeight: "600",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{/* Tag line */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
marginBottom: "24px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255,255,255,0.08)",
|
||||
border: "1px solid rgba(255,255,255,0.14)",
|
||||
borderRadius: "999px",
|
||||
padding: "6px 16px",
|
||||
fontSize: "14px",
|
||||
color: "rgba(255,255,255,0.55)",
|
||||
fontWeight: "500",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
Cafe & Restaurant Management Platform
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontSize: title.length > 40 ? "52px" : title.length > 28 ? "62px" : "70px",
|
||||
fontWeight: "900",
|
||||
color: "white",
|
||||
lineHeight: 1.15,
|
||||
marginBottom: "20px",
|
||||
letterSpacing: "-1px",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "22px",
|
||||
color: "rgba(255,255,255,0.55)",
|
||||
lineHeight: 1.5,
|
||||
maxWidth: "740px",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{sub}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom accent bar */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "5px",
|
||||
background: "linear-gradient(90deg, #22c55e 0%, #16a34a 50%, #15803d 100%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
{ width: 1200, height: 630 }
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user