Initial commit: Negin Dental Clinic website

Next.js (App Router) clone of negindentalclinic.ir, wired to the
Meshkee Website API (blog, contact form, favicon), with on-page SEO
and a scroll-reveal animation sequence on the homepage.

See context.md for full project context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 18:28:54 +03:30
co-authored by Claude Sonnet 5
commit f16b47966b
70 changed files with 4810 additions and 0 deletions
@@ -0,0 +1,76 @@
.wrap {
max-width: 820px;
margin: 0 auto;
}
.back {
display: inline-block;
margin-bottom: 24px;
font-size: 14px;
font-weight: 600;
color: var(--brand);
}
.title {
font-size: 32px;
margin: 0 0 10px;
}
.date {
color: var(--muted);
font-size: 14px;
margin: 0 0 28px;
}
.cover {
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 34px;
box-shadow: var(--shadow-sm);
}
.cover img {
width: 100%;
height: auto;
}
.content {
color: var(--text);
font-size: 16px;
line-height: 2.1;
}
.content :global(p) {
text-align: justify;
text-align-last: right;
margin: 0 0 1.4em;
}
.content :global(h2),
.content :global(h3) {
color: var(--heading);
margin: 1.6em 0 0.7em;
}
.content :global(img) {
max-width: 100%;
border-radius: var(--radius);
margin: 1.2em 0;
}
.content :global(a) {
color: var(--brand);
text-decoration: underline;
}
.content :global(ul),
.content :global(ol) {
padding-inline-start: 1.4em;
margin: 0 0 1.4em;
}
@media (max-width: 720px) {
.title {
font-size: 25px;
}
}
+67
View File
@@ -0,0 +1,67 @@
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import {
getBlogById,
blogTitle,
blogExcerpt,
blogCover,
blogDate,
blogContentHtml,
faDate,
} from "@/lib/meshkee";
import styles from "./blog-detail.module.css";
export const revalidate = 120;
export async function generateMetadata({ params }) {
const { id } = await params;
const blog = await getBlogById(id);
if (!blog) return { title: "مقاله" };
const title = blogTitle(blog);
const description = blogExcerpt(blog) || undefined;
return {
title,
description,
openGraph: {
title,
description,
images: blogCover(blog) ? [blogCover(blog)] : undefined,
},
};
}
export default async function BlogDetailPage({ params }) {
const { id } = await params;
const blog = await getBlogById(id);
if (!blog) notFound();
const title = blogTitle(blog);
const cover = blogCover(blog);
const html = blogContentHtml(blog);
return (
<article className="section">
<div className={`container ${styles.wrap}`}>
<Link href="/blog" className={styles.back}>
← بازگشت به مقالات
</Link>
<h1 className={styles.title}>{title}</h1>
{blogDate(blog) ? <p className={styles.date}>{faDate(blogDate(blog))}</p> : null}
{cover ? (
<div className={styles.cover}>
<Image src={cover} alt={title} width={1200} height={640} priority />
</div>
) : null}
{html ? (
<div className={styles.content} dangerouslySetInnerHTML={{ __html: html }} />
) : (
<p className={styles.content}>{blogExcerpt(blog)}</p>
)}
</div>
</article>
);
}