বেশিরভাগ পোর্টফোলিও ও মার্কেটিং পেজ তাদের section একটি ক্রমাগত বাড়তে থাকা component-এ hardcode করে। এটা কাজ করে — যতক্ষণ না আপনি জিনিস পুনর্বিন্যাস করতে, একটা A/B টেস্টের জন্য একটা section লুকাতে বা একটা block অন্য কোথাও পুনর্ব্যবহার করতে চান। প্রতিটি পরিবর্তন মানে JSX সম্পাদনা।
একটা পরিচ্ছন্ন উপায় আছে: পেজকে data হিসেবে দেখুন। এই লেখা এই সাইটেরই homepage যে প্যাটার্নে চলে সেটার মধ্য দিয়ে হাঁটবে — src/config/sections.ts, src/features/home/registry.tsx ও src/app/page.tsx।
মূল ধারণা
section ক্রমানুসারে লেখার বদলে একটা config object-এ বর্ণনা করুন ও সেখান থেকে render করুন। তিনটি অংশ:
- একটা settings map — কোন section চালু, কোন ক্রমে ও তাদের metadata (nav label, scroll anchor, ঐচ্ছিক standalone route)।
- একটা registry — প্রতিটি section id-কে তার component-এ map করা।
- একটা পাতলা পেজ যা settings পড়ে, sort করে ও render করে — এবং কনটেন্ট বদলালেও কখনো বদলায় না।
settings map
export interface SectionSetting {
enabled: boolean;
order: number;
label: string;
anchor: string;
route?: string;
}
export const sectionSettings: Record<SectionId, SectionSetting> = {
hero: { enabled: true, order: 10, label: "Intro", anchor: "hero" },
"featured-projects": {
enabled: true,
order: 40,
label: "Projects",
anchor: "projects",
},
certifications: {
enabled: false,
order: 100,
label: "Certifications",
anchor: "certifications",
route: "/certifications",
},
// …
};
export const getOrderedSectionIds = (): SectionId[] =>
(Object.keys(sectionSettings) as SectionId[])
.filter((id) => sectionSettings[id].enabled)
.sort((a, b) => sectionSettings[a].order - sectionSettings[b].order);যেহেতু key-গুলো একটা typed union (SectionId), TypeScript নিশ্চিত করে যে আপনি এমন একটা section রেফার করতে পারবেন না যেটা নেই — একটা নতুন section যোগ করা শুরু হয় সেই union বাড়ানো দিয়ে, তাই compiler প্রতিটি config জায়গাকে ধরতে বাধ্য করে।
label আর anchor সাজসজ্জা নয় — এগুলো অন্য কোথাও ব্যবহৃত হয়: anchor section-এর DOM id হয়ে যায় (scroll-mt-24 navigation লক্ষ্য ও in-page search-palette jump-এর লক্ষ্য) ও label সাইটের nav config-এ যায়।
registry
registry id-গুলোকে component-এ map করে। এটাকে Partial চিহ্নিত করলে configuration বাস্তবায়নের আগে চলতে পারে — একটা section তার component থাকার আগেই enable করা যায় ও পেজ throw করার বদলে কেবল এটা এড়িয়ে যায়।
type SectionComponent = ComponentType | (() => Promise<ReactNode>);
export const sectionRegistry: Partial<Record<SectionId, SectionComponent>> = {
hero: Hero,
"featured-projects": FeaturedProjects,
// …
};component type ইচ্ছে করে একটা async function component অনুমোদন করে, শুধু একটা সাধারণ ComponentType নয়। এখানকার কয়েকটা section সার্ভার-সাইডে নিজেদের ডেটা fetch করে (GitHub activity, সাম্প্রতিক ব্লগ লেখা) — registry গ্রাহ্য করে না section sync না async, এটা কেবল যা ফিরে আসে তা render করে।
পেজ render করা
const HomePage = () => {
const sectionIds = getOrderedSectionIds();
return (
<main id="main">
{sectionIds.map((id) => {
const Section = sectionRegistry[id];
return Section ? (
<Suspense key={id} fallback={null}>
<Section />
</Suspense>
) : null;
})}
</main>
);
};প্রতিটি section নিজের Suspense boundary-র ভেতরে stream করে। একটা ধীর ডেটা-fetching section (ধরুন, একটা GitHub API-তে যাচ্ছে) উপরের বা নিচের section-গুলোকে paint হওয়া থেকে আটকায় না — পেজ স্বাধীনভাবে-streaming slice দিয়ে গঠিত হয়, একটা একচেটিয়া render নয়।
এটাই পুরো পেজ। কনটেন্ট বদলালেও এটা কখনো বদলায় না।
standalone-route escape hatch
প্রতিটি section শুধু homepage-এ থাকতে চায় না। Certifications, যেমন, /certifications-এ তার নিজস্ব ডেডিকেটেড পেজ আছে — কিন্তু এটা এখন homepage feed থেকে toggle করে বন্ধ রাখা (enabled: false)। SectionSetting-এর ঐচ্ছিক route field সেজন্যই।
দুটি guard function একই config পড়ে যাতে homepage, nav ও standalone page সবাই সিঙ্কে থাকে:
export const isSectionEnabled = (id: SectionId): boolean =>
sectionSettings[id].enabled;
const disabledSectionRoutes = new Set(
(Object.keys(sectionSettings) as SectionId[])
.map((id) => sectionSettings[id])
.filter((s) => !s.enabled && s.route)
.map((s) => s.route as string),
);
export const isRouteEnabled = (href: string): boolean =>
!disabledSectionRoutes.has(href);isRouteEnabled nav config filter করে — তাই একটা disabled section-এর standalone route header ও footer থেকে চুপচাপ অদৃশ্য হয়ে যায়, একটা মৃত লিংক রেখে যাওয়ার বদলে। isSectionEnabled পেজটাকেই gate করে:
const CertificationsPage = () => {
if (!isSectionEnabled("certifications")) {
notFound();
}
// …
};একটা config ফাইলে একটা boolean একটা পুরো ফিচার — homepage tile, nav link ও standalone page — চালু বা বন্ধ করে। চেক করার কথা মনে রাখার আর কোনো জায়গা নেই।
এটা কেন scale করে
এটা কাজে Open/Closed Principle: পেজ extension-এর জন্য খোলা (config + registry-তে একটা section যোগ করুন) কিন্তু modification-এর জন্য বন্ধ (আপনি পেজটা নিজে কখনো সম্পাদনা করেন না)। এটা layout সিদ্ধান্তগুলোকেও একটা পর্যালোচনাযোগ্য জায়গায় রাখে, যা ছয় মাস পর ফিরে এলে একটা উপহার।
পেজকে data হিসেবে দেখুন: একটা config object-এ section বর্ণনা করুন ও সেখান থেকে render করুন। পেজ extension-এর জন্য খোলা কিন্তু modification-এর জন্য বন্ধ — কনটেন্ট বদলালে আপনি কখনো এটা সম্পাদনা করেন না।
একই প্যাটার্ন dashboard, settings screen ও যেকোনো surface-এ সাধারণীকৃত হয় যেখানে block-গুলোর বিন্যাস ও প্রাপ্যতা নিজেই একটা পণ্য সিদ্ধান্ত।