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:
co-authored by
Cursor
parent
9455ebe7cd
commit
f341b010c0
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+58
-15
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
export async function getPortfolioBySlug(slug: string): Promise<ApiPortfolioItem | null> {
|
||||
const items = await getPortfolios();
|
||||
return items.find((item) => item.slug === slug) ?? null;
|
||||
}
|
||||
/** 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
export async function getBlogBySlug(slug: string): Promise<ApiBlogItem | null> {
|
||||
const items = await getBlogs();
|
||||
return items.find((item) => item.slug === slug) ?? null;
|
||||
}
|
||||
/** 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user