Ship local assets, portfolio filters, favicon, and UI polish.

Host CDN images locally, add top-level portfolio category filtering with nav sublinks, refresh the Meshkee favicon, and tighten title/gallery/footer presentation across the site.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-07-31 09:30:35 +03:30
co-authored by Cursor
parent 112e5eb46d
commit 4e635bb038
230 changed files with 955 additions and 174 deletions
-5
View File
@@ -3,11 +3,6 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
images: { images: {
remotePatterns: [ remotePatterns: [
{
protocol: "https",
hostname: "meshkee-templates.sas.amin.parminstorage.ir",
pathname: "/**",
},
{ {
protocol: "https", protocol: "https",
hostname: "meshkee-storage.sas.amin.parminstorage.ir", hostname: "meshkee-storage.sas.amin.parminstorage.ir",
+1
View File
@@ -0,0 +1 @@
../../src/assets/images
Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<rect width="128" height="128" fill="#000000"/>
<g fill="none" stroke="#FFFFFF" stroke-width="8" stroke-linecap="round">
<!-- top: ring left + line right -->
<circle cx="36" cy="46" r="10"/>
<line x1="46" y1="46" x2="96" y2="46"/>
<!-- bottom: line left + ring right -->
<line x1="32" y1="82" x2="82" y2="82"/>
<circle cx="92" cy="82" r="10"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 512 B

Executable → Regular
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

+1 -1
View File
@@ -59,7 +59,7 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
</Link> </Link>
{blog.categoryName ? ( {blog.categoryName ? (
<p className="latin mb-3 text-[11px] uppercase tracking-widest text-muted"> <p className="latin title-en mb-3 text-muted">
{blog.categoryName} {blog.categoryName}
</p> </p>
) : null} ) : null}
+3 -2
View File
@@ -5,8 +5,9 @@ import { Pagination } from "@/components/Pagination";
import { getBlogCoverImage, getBlogs } from "@/lib/api"; import { getBlogCoverImage, getBlogs } from "@/lib/api";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "مقالات | مشکی", title: "مقالات",
description: "آخرین مقالات آموزشی و انگیزشی تیم مشکی.", description: "آخرین مقالات آموزشی و انگیزشی تیم مشکی.",
alternates: { canonical: "/blog" },
}; };
const PAGE_SIZE = 12; const PAGE_SIZE = 12;
@@ -39,7 +40,7 @@ export default async function BlogListPage({ searchParams }: BlogPageProps) {
<h1 className="whitespace-normal text-[clamp(1.35rem,3.6vw,2.75rem)] font-normal tracking-tight text-ink"> <h1 className="whitespace-normal text-[clamp(1.35rem,3.6vw,2.75rem)] font-normal tracking-tight text-ink">
رشدنامه، آموزشی و انگیزشی رشدنامه، آموزشی و انگیزشی
</h1> </h1>
<p className="latin mt-3 text-xs uppercase tracking-[0.2em] text-muted"> <p className="latin title-en text-muted">
Our Blog Our Blog
</p> </p>
<p className="mt-5 text-sm leading-8 text-muted md:text-base"> <p className="mt-5 text-sm leading-8 text-muted md:text-base">
+12 -3
View File
@@ -9,14 +9,23 @@ import {
import { images } from "@/data/assets"; import { images } from "@/data/assets";
const CDN = const CDN =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets/files"; "/assets/images/files";
const ASSETS = const ASSETS =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets"; "/assets/images";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "وبسایت CMS | مشکی", title: "طراحی وبسایت با Meshkee CMS",
description: description:
"طراحی وبسایت روی سیستم مدیریت محتوای مشکی؛ ویرایش زنده، اتصال به پلتفرم‌ها و کدنویسی اختصاصی.", "طراحی وبسایت روی سیستم مدیریت محتوای مشکی؛ ویرایش زنده، اتصال به پلتفرم‌ها و کدنویسی اختصاصی.",
alternates: { canonical: "/cms-website" },
openGraph: {
title: "طراحی وبسایت با Meshkee CMS",
description:
"طراحی وبسایت روی سیستم مدیریت محتوای مشکی؛ ویرایش زنده، اتصال به پلتفرم‌ها و کدنویسی اختصاصی.",
url: "/cms-website",
type: "website",
locale: "fa_IR",
},
}; };
export default function CmsWebsitePage() { export default function CmsWebsitePage() {
+3 -2
View File
@@ -8,11 +8,12 @@ import {
import { images } from "@/data/assets"; import { images } from "@/data/assets";
const CDN = const CDN =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets/files"; "/assets/images/files";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "طراحی اختصاصی اپلیکیشن | مشکی", title: "طراحی اختصاصی اپلیکیشن",
description: "طراحی و توسعه اپلیکیشن موبایل iOS و Android توسط تیم اختصاصی مشکی.", description: "طراحی و توسعه اپلیکیشن موبایل iOS و Android توسط تیم اختصاصی مشکی.",
alternates: { canonical: "/dedicated-application" },
}; };
export default function DedicatedApplicationPage() { export default function DedicatedApplicationPage() {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 660 B

+21
View File
@@ -85,6 +85,25 @@ h4 {
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
/* English subtitle under FA titles */
:is(h1, h2):has(+ .title-en) {
margin-bottom: 0 !important;
}
.title-en {
margin-top: 0.25rem;
font-size: 0.8125rem;
letter-spacing: 0.28em;
text-transform: uppercase;
}
.title-en-sm {
margin-top: 0.15rem;
font-size: 0.7rem;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.text-body { .text-body {
text-align: justify; text-align: justify;
text-justify: inter-word; text-justify: inter-word;
@@ -267,6 +286,8 @@ a.group-border-hover:hover {
color: var(--ink-soft); color: var(--ink-soft);
font-size: 0.975rem; font-size: 0.975rem;
line-height: 2; line-height: 2;
text-align: justify;
text-justify: inter-word;
} }
.prose-content > *:first-child { .prose-content > *:first-child {
+4 -3
View File
@@ -10,13 +10,14 @@ import {
import { images } from "@/data/assets"; import { images } from "@/data/assets";
const CDN = const CDN =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets/files"; "/assets/images/files";
const ASSETS = const ASSETS =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets"; "/assets/images";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "طراحی گرافیک و هویت بصری | مشکی", title: "طراحی گرافیک و هویت بصری",
description: "طراحی لوگو، هویت بصری، کاتالوگ، بروشور و گرافیک شبکه‌های اجتماعی توسط تیم مشکی.", description: "طراحی لوگو، هویت بصری، کاتالوگ، بروشور و گرافیک شبکه‌های اجتماعی توسط تیم مشکی.",
alternates: { canonical: "/graphic-designer" },
}; };
export default function GraphicDesignerPage() { export default function GraphicDesignerPage() {
Executable → Regular
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

+65 -3
View File
@@ -14,13 +14,75 @@ const montserrat = Montserrat({
display: "swap", display: "swap",
}); });
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.meshkee.com";
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: {
default: "تیم برنامه‌نویسی و تجارت الکترونیک مشکی | Meshkee",
template: "%s | مشکی",
},
description:
"راهکارهای مدرن برای تجارت شما — طراحی وبسایت، اپلیکیشن، سیستم‌های مدیریتی، سئو و گرافیک توسط تیم مشکی.",
applicationName: "Meshkee",
authors: [{ name: "Meshkee", url: siteUrl }],
creator: "Meshkee",
publisher: "Meshkee",
keywords: [
"مشکی",
"Meshkee",
"طراحی وبسایت",
"طراحی اپلیکیشن",
"سئو",
"CMS",
"قم",
],
icons: {
icon: [
{ url: "/images/brand/meshkee-mark.svg", type: "image/svg+xml" },
{ url: "/favicon.ico", sizes: "any" },
{ url: "/images/brand/favicon.png", type: "image/png", sizes: "512x512" },
],
apple: [{ url: "/images/brand/favicon.png", type: "image/png", sizes: "512x512" }],
shortcut: ["/favicon.ico"],
},
openGraph: {
type: "website",
locale: "fa_IR",
url: siteUrl,
siteName: "Meshkee",
title: "تیم برنامه‌نویسی و تجارت الکترونیک مشکی | Meshkee", title: "تیم برنامه‌نویسی و تجارت الکترونیک مشکی | Meshkee",
description: description:
"راهکارهای مدرن برای تجارت شما — طراحی وبسایت، اپلیکیشن، سیستم‌های مدیریتی، سئو و گرافیک توسط تیم مشکی.", "راهکارهای مدرن برای تجارت شما — طراحی وبسایت، اپلیکیشن، سیستم‌های مدیریتی، سئو و گرافیک توسط تیم مشکی.",
icons: { images: [
icon: "/images/brand/mark-black.png", {
apple: "/images/brand/mark-black.png", url: "/images/brand/favicon.png",
width: 512,
height: 512,
alt: "لوگوی مشکی",
},
],
},
twitter: {
card: "summary",
title: "تیم برنامه‌نویسی و تجارت الکترونیک مشکی | Meshkee",
description:
"راهکارهای مدرن برای تجارت شما — طراحی وبسایت، اپلیکیشن، سیستم‌های مدیریتی، سئو و گرافیک توسط تیم مشکی.",
images: ["/images/brand/favicon.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
alternates: {
canonical: "/",
}, },
}; };
+4 -3
View File
@@ -4,14 +4,15 @@ import { GraphicBrands, cmsBrands } from "@/components/GraphicBrands";
import { images } from "@/data/assets"; import { images } from "@/data/assets";
const CDN = const CDN =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets/files"; "/assets/images/files";
const ASSETS = const ASSETS =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets"; "/assets/images";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "سیستم مدیریت یکپارچه مشکی | Meshkee CMS", title: "سیستم مدیریت یکپارچه مشکی",
description: description:
"سیستم مدیریت یکپارچه مشکی شامل فروشگاه، مالی، انبار، باشگاه مشتریان و محتوا.", "سیستم مدیریت یکپارچه مشکی شامل فروشگاه، مالی، انبار، باشگاه مشتریان و محتوا.",
alternates: { canonical: "/meshkee-cms" },
}; };
export default function MeshkeeCmsPage() { export default function MeshkeeCmsPage() {
+67 -32
View File
@@ -3,10 +3,20 @@ import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { ContentPageShell } from "@/components/ContentPageShell"; import { ContentPageShell } from "@/components/ContentPageShell";
import { HtmlContent } from "@/components/HtmlContent"; import { HtmlContent } from "@/components/HtmlContent";
import { PortfolioGallery } from "@/components/PortfolioGallery";
import {
categoryLabelFa,
findPortfolioCategory,
getRootCategories,
portfolioCategoryHref,
topPortfolioCategories,
type PortfolioCategory,
} from "@/data/portfolioCategories";
import { import {
ApiNotFoundError, ApiNotFoundError,
formatFaDate, formatFaDate,
getPortfolio, getPortfolio,
getPortfolioCategories,
getPortfolioCoverImage, getPortfolioCoverImage,
} from "@/lib/api"; } from "@/lib/api";
@@ -14,6 +24,36 @@ type PortfolioDetailPageProps = {
params: Promise<{ slug: string }>; params: Promise<{ slug: string }>;
}; };
function mapApiCategories(
items: Awaited<ReturnType<typeof getPortfolioCategories>>,
): PortfolioCategory[] {
return items.map((item) => ({
id: String(item.id),
slug: item.slug,
nameEn: item.name,
nameFa: item.nameFa || item.name,
parentId: item.parentId,
sortOrder: item.sortOrder ?? 0,
}));
}
function findRootForCategory(
categoryId: string | number | null | undefined,
categories: PortfolioCategory[],
): PortfolioCategory | undefined {
if (categoryId == null) return undefined;
let current = categories.find((c) => c.id === String(categoryId));
if (!current) return undefined;
while (current?.parentId) {
const parent = categories.find((c) => c.id === String(current?.parentId));
if (!parent) break;
current = parent;
}
return current;
}
export async function generateMetadata({ export async function generateMetadata({
params, params,
}: PortfolioDetailPageProps): Promise<Metadata> { }: PortfolioDetailPageProps): Promise<Metadata> {
@@ -47,6 +87,18 @@ export default async function PortfolioDetailPage({
const cover = getPortfolioCoverImage(portfolio); const cover = getPortfolioCoverImage(portfolio);
const date = formatFaDate(portfolio.publishedAt); const date = formatFaDate(portfolio.publishedAt);
const title = portfolio.titleFa || portfolio.title; const title = portfolio.titleFa || portfolio.title;
let allCategories: PortfolioCategory[] = topPortfolioCategories;
try {
allCategories = mapApiCategories(await getPortfolioCategories());
} catch {
allCategories = topPortfolioCategories;
}
const categoryFa = categoryLabelFa(portfolio.categoryName, allCategories);
const rootCategory =
findRootForCategory(portfolio.categoryId, allCategories) ??
findPortfolioCategory(portfolio.categoryName, getRootCategories(allCategories));
const gallery = (portfolio.gallery ?? []).filter( const gallery = (portfolio.gallery ?? []).filter(
(item) => item.url !== cover, (item) => item.url !== cover,
); );
@@ -57,16 +109,18 @@ export default async function PortfolioDetailPage({
<div className="mx-auto max-w-5xl px-5 lg:px-8"> <div className="mx-auto max-w-5xl px-5 lg:px-8">
<div className="reveal mb-10 max-w-3xl"> <div className="reveal mb-10 max-w-3xl">
<Link <Link
href="/portfolio" href={
rootCategory
? portfolioCategoryHref(rootCategory.slug)
: "/portfolio"
}
className="link-hover mb-6 inline-flex text-sm text-muted" className="link-hover mb-6 inline-flex text-sm text-muted"
> >
بازگشت به نمونه‌کارها بازگشت به نمونه‌کارها
</Link> </Link>
{portfolio.categoryName ? ( {categoryFa ? (
<p className="latin mb-3 text-[11px] uppercase tracking-widest text-muted"> <p className="mb-3 text-[11px] text-muted">{categoryFa}</p>
{portfolio.categoryName}
</p>
) : null} ) : null}
<h1 className="whitespace-normal text-[clamp(1.35rem,3.8vw,2.5rem)] font-normal leading-tight tracking-tight text-ink"> <h1 className="whitespace-normal text-[clamp(1.35rem,3.8vw,2.5rem)] font-normal leading-tight tracking-tight text-ink">
@@ -74,13 +128,13 @@ export default async function PortfolioDetailPage({
</h1> </h1>
{portfolio.titleEn ? ( {portfolio.titleEn ? (
<p className="latin mt-3 text-xs uppercase tracking-widest text-muted"> <p className="latin title-en text-muted">
{portfolio.titleEn} {portfolio.titleEn}
</p> </p>
) : null} ) : null}
{portfolio.abstract ? ( {portfolio.abstract ? (
<p className="mt-5 text-sm leading-8 text-muted md:text-base"> <p className="mt-5 text-justify text-sm leading-8 text-muted md:text-base">
{portfolio.abstract} {portfolio.abstract}
</p> </p>
) : null} ) : null}
@@ -95,32 +149,13 @@ export default async function PortfolioDetailPage({
) : null} ) : null}
</div> </div>
{cover ? ( <PortfolioGallery
<div className="reveal reveal-delay-1 mb-10 overflow-hidden rounded-2xl bg-paper-deep"> cover={cover}
<img images={gallery.map((item) => ({
src={cover} id: item.mediaId,
alt="" url: item.url,
className="mx-auto max-h-[32rem] w-full object-contain" }))}
/> />
</div>
) : null}
{gallery.length > 0 ? (
<div className="reveal reveal-delay-2 mb-12 grid gap-4 sm:grid-cols-2">
{gallery.map((item) => (
<div
key={item.mediaId}
className="overflow-hidden rounded-2xl bg-paper-deep"
>
<img
src={item.url}
alt=""
className="h-full w-full object-contain"
/>
</div>
))}
</div>
) : null}
{portfolio.mainTextHtml ? ( {portfolio.mainTextHtml ? (
<div className="reveal reveal-delay-2 mx-auto max-w-3xl"> <div className="reveal reveal-delay-2 mx-auto max-w-3xl">
+111 -14
View File
@@ -2,46 +2,104 @@ import type { Metadata } from "next";
import Link from "next/link"; import Link from "next/link";
import { ContentPageShell } from "@/components/ContentPageShell"; import { ContentPageShell } from "@/components/ContentPageShell";
import { Pagination } from "@/components/Pagination"; import { Pagination } from "@/components/Pagination";
import { getPortfolioCoverImage, getPortfolios } from "@/lib/api"; import {
categoryLabelFa,
findPortfolioCategory,
getDescendantCategoryIds,
getRootCategories,
portfolioCategoryHref,
topPortfolioCategories,
type PortfolioCategory,
} from "@/data/portfolioCategories";
import {
getPortfolioCategories,
getPortfolioCoverImage,
getPortfolios,
getPortfoliosByCategory,
} from "@/lib/api";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "نمونه‌کارها | مشکی", title: "نمونه‌کارها",
description: "نمونه پروژه‌ها و کارهای انجام‌شده توسط تیم مشکی.", description: "نمونه پروژه‌ها و کارهای انجام‌شده توسط تیم مشکی.",
alternates: { canonical: "/portfolio" },
}; };
const PAGE_SIZE = 12; const PAGE_SIZE = 12;
type PortfolioPageProps = { type PortfolioPageProps = {
searchParams: Promise<{ page?: string }>; searchParams: Promise<{ page?: string; category?: string }>;
}; };
function mapApiCategories(
items: Awaited<ReturnType<typeof getPortfolioCategories>>,
): PortfolioCategory[] {
return items.map((item) => ({
id: String(item.id),
slug: item.slug,
nameEn: item.name,
nameFa: item.nameFa || item.name,
parentId: item.parentId,
sortOrder: item.sortOrder ?? 0,
}));
}
export default async function PortfolioListPage({ export default async function PortfolioListPage({
searchParams, searchParams,
}: PortfolioPageProps) { }: PortfolioPageProps) {
const { page: pageParam } = await searchParams; const { page: pageParam, category: categoryParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1); const page = Math.max(1, Number(pageParam) || 1);
let allCategories: PortfolioCategory[] = topPortfolioCategories;
try {
allCategories = mapApiCategories(await getPortfolioCategories());
} catch {
allCategories = topPortfolioCategories;
}
const rootCategories = getRootCategories(allCategories);
const activeCategory =
findPortfolioCategory(categoryParam, allCategories) ??
findPortfolioCategory(categoryParam, rootCategories);
let items: Awaited<ReturnType<typeof getPortfolios>>["items"] = []; let items: Awaited<ReturnType<typeof getPortfolios>>["items"] = [];
let total = 0; let total = 0;
try { try {
if (activeCategory) {
const data = await getPortfoliosByCategory({
categoryId: activeCategory.id,
categoryIds: getDescendantCategoryIds(allCategories, activeCategory.id),
page,
pageSize: PAGE_SIZE,
});
items = data.items;
total = data.total;
} else {
const data = await getPortfolios({ page, pageSize: PAGE_SIZE }); const data = await getPortfolios({ page, pageSize: PAGE_SIZE });
items = data.items; items = data.items;
total = data.total; total = data.total;
}
} catch { } catch {
items = []; items = [];
total = 0; total = 0;
} }
const paginationQuery = activeCategory
? { category: activeCategory.slug }
: undefined;
const filterCategories =
rootCategories.length > 0 ? rootCategories : topPortfolioCategories;
return ( return (
<ContentPageShell> <ContentPageShell>
<section className="pb-20 lg:pb-28"> <section className="pb-20 lg:pb-28">
<div className="mx-auto max-w-7xl px-5 lg:px-8"> <div className="mx-auto max-w-7xl px-5 lg:px-8">
<div className="reveal mb-12 max-w-2xl md:mb-16"> <div className="reveal mb-10 max-w-2xl md:mb-12">
<h1 className="whitespace-normal text-[clamp(1.35rem,3.6vw,2.75rem)] font-normal tracking-tight text-ink"> <h1 className="whitespace-normal text-[clamp(1.35rem,3.6vw,2.75rem)] font-normal tracking-tight text-ink">
نمونه‌کارها نمونه‌کارها
</h1> </h1>
<p className="latin mt-3 text-xs uppercase tracking-[0.2em] text-muted"> <p className="latin title-en text-muted">
Portfolio Portfolio
</p> </p>
<p className="mt-5 text-sm leading-8 text-muted md:text-base"> <p className="mt-5 text-sm leading-8 text-muted md:text-base">
@@ -49,19 +107,55 @@ export default async function PortfolioListPage({
</p> </p>
</div> </div>
<div className="reveal mb-10 flex flex-wrap gap-2 md:mb-12">
<Link
href="/portfolio"
className={`rounded-full border px-4 py-2 text-sm transition ${
!activeCategory
? "border-ink bg-ink text-surface"
: "border-line/70 text-ink-soft hover:border-ink/30 hover:text-ink"
}`}
>
همه
</Link>
{filterCategories.map((category) => {
const active = activeCategory?.id === category.id;
return (
<Link
key={category.id}
href={portfolioCategoryHref(category.slug)}
className={`rounded-full border px-4 py-2 text-sm transition ${
active
? "border-ink bg-ink text-surface"
: "border-line/70 text-ink-soft hover:border-ink/30 hover:text-ink"
}`}
>
{category.nameFa}
</Link>
);
})}
</div>
{items.length === 0 ? ( {items.length === 0 ? (
<p className="text-sm text-muted"> <p className="text-sm text-muted">
نمونه‌کاری برای نمایش وجود ندارد. نمونه‌کاری برای نمایش وجود ندارد.
</p> </p>
) : ( ) : (
<div key={page} className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3"> <div
key={`${activeCategory?.slug ?? "all"}-${page}`}
className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3"
>
{items.map((item, index) => { {items.map((item, index) => {
const image = getPortfolioCoverImage(item); const image = getPortfolioCoverImage(item);
const categoryFa = categoryLabelFa(
item.categoryName,
allCategories,
);
return ( return (
<article <article
key={item.id} key={item.id}
className={`reveal group ${ className={`reveal group overflow-hidden rounded-2xl border border-line/70 ${
index % 3 === 1 index % 3 === 1
? "reveal-delay-1" ? "reveal-delay-1"
: index % 3 === 2 : index % 3 === 2
@@ -70,7 +164,7 @@ export default async function PortfolioListPage({
}`} }`}
> >
<Link href={`/portfolio/${item.slug}`} className="block"> <Link href={`/portfolio/${item.slug}`} className="block">
<div className="relative mb-4 overflow-hidden rounded-2xl bg-paper-deep"> <div className="relative overflow-hidden bg-paper-deep">
{image ? ( {image ? (
<img <img
src={image} src={image}
@@ -79,19 +173,21 @@ export default async function PortfolioListPage({
/> />
) : null} ) : null}
</div> </div>
{item.categoryName ? ( <div className="px-4 py-4">
<p className="latin mb-2 text-[10px] uppercase tracking-widest text-muted"> {categoryFa ? (
{item.categoryName} <p className="mb-2 text-[11px] text-muted">
{categoryFa}
</p> </p>
) : null} ) : null}
<h2 className="group-text-hover mb-2 line-clamp-2 whitespace-normal text-lg font-normal leading-8 text-ink"> <h2 className="group-text-hover mb-0.5 line-clamp-2 whitespace-normal text-lg font-normal leading-7 text-ink">
{item.titleFa || item.title} {item.titleFa || item.title}
</h2> </h2>
{item.abstract ? ( {item.abstract ? (
<p className="line-clamp-3 text-sm leading-7 text-muted"> <p className="line-clamp-3 text-sm leading-6 text-muted">
{item.abstract} {item.abstract}
</p> </p>
) : null} ) : null}
</div>
</Link> </Link>
</article> </article>
); );
@@ -104,6 +200,7 @@ export default async function PortfolioListPage({
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
total={total} total={total}
basePath="/portfolio" basePath="/portfolio"
query={paginationQuery}
/> />
</div> </div>
</section> </section>
+14
View File
@@ -0,0 +1,14 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.meshkee.com";
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: `${siteUrl}/sitemap.xml`,
host: siteUrl,
};
}
+3 -2
View File
@@ -5,11 +5,12 @@ import { ProjectsShowcase, seoProjects } from "@/components/WebsiteProjects";
import { images } from "@/data/assets"; import { images } from "@/data/assets";
const CDN = const CDN =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets/files"; "/assets/images/files";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "سئو و مدیریت وبسایت | مشکی", title: "سئو و مدیریت وبسایت",
description: "خدمات سئو حرفه‌ای، بهینه‌سازی موتور جستجو و مدیریت وبسایت توسط تیم مشکی.", description: "خدمات سئو حرفه‌ای، بهینه‌سازی موتور جستجو و مدیریت وبسایت توسط تیم مشکی.",
alternates: { canonical: "/seo" },
}; };
export default function SeoPage() { export default function SeoPage() {
+27
View File
@@ -0,0 +1,27 @@
import type { MetadataRoute } from "next";
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://www.meshkee.com";
const routes = [
"/",
"/cms-website",
"/website-qom",
"/website",
"/seo",
"/dedicated-application",
"/meshkee-cms",
"/graphic-designer",
"/portfolio",
"/blog",
];
export default function sitemap(): MetadataRoute.Sitemap {
const lastModified = new Date();
return routes.map((path) => ({
url: `${siteUrl}${path === "/" ? "" : path}`,
lastModified,
changeFrequency: path === "/" ? "weekly" : "monthly",
priority: path === "/" ? 1 : path === "/website-qom" || path === "/cms-website" ? 0.9 : 0.7,
}));
}
+17 -1
View File
@@ -8,12 +8,28 @@ import {
import { images } from "@/data/assets"; import { images } from "@/data/assets";
const CDN = const CDN =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets/files"; "/assets/images/files";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "طراحی سایت در قم توسط تیم حرفه‌ای مشکی", title: "طراحی سایت در قم توسط تیم حرفه‌ای مشکی",
description: description:
"طراحی وبسایت در قم توسط تیم مشکی با استفاده از جدیدترین متدهای کدنویسی و CMS اختصاصی با بهترین قیمت.", "طراحی وبسایت در قم توسط تیم مشکی با استفاده از جدیدترین متدهای کدنویسی و CMS اختصاصی با بهترین قیمت.",
keywords: [
"طراحی سایت در قم",
"طراحی وبسایت قم",
"شرکت طراحی سایت قم",
"مشکی",
"Meshkee",
],
alternates: { canonical: "/website-qom" },
openGraph: {
title: "طراحی سایت در قم توسط تیم حرفه‌ای مشکی",
description:
"طراحی وبسایت در قم توسط تیم مشکی با استفاده از جدیدترین متدهای کدنویسی و CMS اختصاصی با بهترین قیمت.",
url: "/website-qom",
type: "website",
locale: "fa_IR",
},
}; };
export default function WebsiteQomPage() { export default function WebsiteQomPage() {
+4 -3
View File
@@ -5,13 +5,14 @@ import { WebsiteCountries } from "@/components/WebsiteCountries";
import { images } from "@/data/assets"; import { images } from "@/data/assets";
const CDN = const CDN =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets/files"; "/assets/images/files";
const ICONS = const ICONS =
"https://meshkee-templates.sas.amin.parminstorage.ir/sites/user_themes/meshkee_SEEpb1zypk/assets/img/icons"; "/assets/images/img/icons";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "طراحی اختصاصی وبسایت | مشکی", title: "طراحی اختصاصی وبسایت",
description: "طراحی و توسعه وبسایت اختصاصی، فروشگاهی، شرکتی و خاص توسط تیم مشکی.", description: "طراحی و توسعه وبسایت اختصاصی، فروشگاهی، شرکتی و خاص توسط تیم مشکی.",
alternates: { canonical: "/website" },
}; };
export default function WebsitePage() { export default function WebsitePage() {
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Some files were not shown because too many files have changed in this diff Show More