Reduce memory use by caching API calls and using slug endpoints.

Use React cache() for list/slug fetches, hit single-item portfolio/blog routes instead of loading full lists, and dedupe metadata + page requests on news/blog detail pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-14 11:15:51 +03:30
co-authored by Cursor
parent 9455ebe7cd
commit f341b010c0
3 changed files with 79 additions and 29 deletions
+10 -9
View File
@@ -4,17 +4,20 @@ import { notFound } from "next/navigation";
import { NewsArticleDetail } from "@/components/news/NewsArticleDetail";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { getBlogs, toDisplayNewsItem } from "@/lib/api";
import { getBlogArticles, getBlogBySlug, toDisplayNewsItem } from "@/lib/api";
export const revalidate = 3600;
async function getBlogPosts() {
const posts = await getBlogs();
return posts.filter((p) => p.type !== "news");
async function getBlogArticleBySlug(slug: string) {
const item = await getBlogBySlug(slug);
if (!item || item.type === "news") {
return null;
}
return item;
}
export async function generateStaticParams() {
const posts = await getBlogPosts();
const posts = await getBlogArticles();
return posts.map((item) => ({ slug: item.slug }));
}
@@ -24,8 +27,7 @@ export async function generateMetadata({
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const posts = await getBlogPosts();
const item = posts.find((p) => p.slug === slug);
const item = await getBlogArticleBySlug(slug);
if (!item) return {};
return { title: item.titleFa || item.title, description: item.abstract || item.titleFa };
}
@@ -36,8 +38,7 @@ export default async function BlogDetailPage({
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const posts = await getBlogPosts();
const item = posts.find((p) => p.slug === slug);
const item = await getBlogArticleBySlug(slug);
if (!item) notFound();
return (
+11 -5
View File
@@ -4,10 +4,18 @@ import { notFound } from "next/navigation";
import { NewsArticleDetail } from "@/components/news/NewsArticleDetail";
import { SiteFooter } from "@/components/SiteFooter";
import { SiteHeader } from "@/components/SiteHeader";
import { getBlogs, toDisplayNewsItem } from "@/lib/api";
import { getBlogBySlug, getBlogs, toDisplayNewsItem } from "@/lib/api";
export const revalidate = 3600;
async function getNewsBySlug(slug: string) {
const item = await getBlogBySlug(slug);
if (!item || item.type !== "news") {
return null;
}
return item;
}
export async function generateStaticParams() {
const posts = await getBlogs("news");
return posts.map((item) => ({ slug: item.slug }));
@@ -19,8 +27,7 @@ export async function generateMetadata({
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const posts = await getBlogs("news");
const item = posts.find((p) => p.slug === slug);
const item = await getNewsBySlug(slug);
if (!item) return {};
return { title: item.titleFa || item.title, description: item.abstract || item.titleFa };
}
@@ -31,8 +38,7 @@ export default async function NewsDetailPage({
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const posts = await getBlogs("news");
const item = posts.find((p) => p.slug === slug);
const item = await getNewsBySlug(slug);
if (!item) notFound();
return (
+56 -13
View File
@@ -12,6 +12,8 @@
* to projects in their CMS, the filter picks them up with no code change.
*/
import { cache } from "react";
const API_BASE = "https://api.meshkee.com/api/v1";
const SITE_DOMAIN = "raoofigroup.com";
@@ -57,10 +59,9 @@ type PortfoliosResponse = {
pageSize: number;
};
async function apiGet<T>(path: string): Promise<T> {
let res: Response;
async function apiFetch(path: string): Promise<Response> {
try {
res = await fetch(`${API_BASE}${path}`, {
return await fetch(`${API_BASE}${path}`, {
headers: { Accept: "application/json" },
next: { revalidate: REVALIDATE_SECONDS },
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -68,6 +69,24 @@ async function apiGet<T>(path: string): Promise<T> {
} catch (cause) {
throw new Error(`meshkee API ${path} timed out or was unreachable`, { cause });
}
}
async function apiGet<T>(path: string): Promise<T> {
const res = await apiFetch(path);
if (!res.ok) {
throw new Error(`meshkee API ${path} responded ${res.status}`);
}
return res.json() as Promise<T>;
}
async function apiGetOptional<T>(path: string): Promise<T | null> {
const res = await apiFetch(path);
if (res.status === 404) {
return null;
}
if (!res.ok) {
throw new Error(`meshkee API ${path} responded ${res.status}`);
@@ -77,7 +96,7 @@ async function apiGet<T>(path: string): Promise<T> {
}
/** All published portfolio items, every page merged, in the CMS's own order. */
export async function getPortfolios(): Promise<ApiPortfolioItem[]> {
export const getPortfolios = cache(async (): Promise<ApiPortfolioItem[]> => {
const first = await apiGet<PortfoliosResponse>(`/tenants/${SITE_DOMAIN}/portfolios`);
const pageCount = Math.ceil(first.total / first.pageSize);
@@ -91,12 +110,21 @@ export async function getPortfolios(): Promise<ApiPortfolioItem[]> {
.flatMap((page) => page.items)
.filter((item) => item.status === "published")
.sort((a, b) => a.sortOrder - b.sortOrder);
});
/** Single portfolio by slug — avoids loading the full list on detail pages. */
export const getPortfolioBySlug = cache(async (slug: string): Promise<ApiPortfolioItem | null> => {
const data = await apiGetOptional<{ portfolio: ApiPortfolioItem }>(
`/tenants/${SITE_DOMAIN}/portfolios/${encodeURIComponent(slug)}`,
);
const item = data?.portfolio;
if (!item || item.status !== "published") {
return null;
}
export async function getPortfolioBySlug(slug: string): Promise<ApiPortfolioItem | null> {
const items = await getPortfolios();
return items.find((item) => item.slug === slug) ?? null;
}
return item;
});
export type PortfolioCategory = { id: string; name: string; count: number };
@@ -207,7 +235,7 @@ type BlogsResponse = {
};
/** All published blog/news items, every page merged. Optionally filtered by `type`. */
export async function getBlogs(type?: BlogType): Promise<ApiBlogItem[]> {
export const getBlogs = cache(async (type?: BlogType): Promise<ApiBlogItem[]> => {
const query = type ? `?type=${type}` : "";
const first = await apiGet<BlogsResponse>(`/tenants/${SITE_DOMAIN}/blogs${query}`);
const pageCount = Math.ceil(first.total / first.pageSize);
@@ -224,12 +252,27 @@ export async function getBlogs(type?: BlogType): Promise<ApiBlogItem[]> {
.flatMap((page) => page.items)
.filter((item) => item.status === "published")
.sort((a, b) => a.sortOrder - b.sortOrder);
});
/** Single blog/news item by slug — avoids loading the full list on detail pages. */
export const getBlogBySlug = cache(async (slug: string): Promise<ApiBlogItem | null> => {
const data = await apiGetOptional<{ blog: ApiBlogItem }>(
`/tenants/${SITE_DOMAIN}/blogs/${encodeURIComponent(slug)}`,
);
const item = data?.blog;
if (!item || item.status !== "published") {
return null;
}
export async function getBlogBySlug(slug: string): Promise<ApiBlogItem | null> {
const items = await getBlogs();
return items.find((item) => item.slug === slug) ?? null;
}
return item;
});
/** Blog articles only (excludes news posts). */
export const getBlogArticles = cache(async (): Promise<ApiBlogItem[]> => {
const posts = await getBlogs();
return posts.filter((item) => item.type !== "news");
});
export type DisplayNewsItem = {
slug: string;