feature/branding
tsx 338 lines 12.7 KB
Raw
1 import type { Metadata } from "next";
2 import { allKtis } from "contentlayer/generated";
3 import { notFound } from "next/navigation";
4 import { Mdx } from "@/components/mdx-components";
5 import Link from "next/link";
6 import { StructuredData } from "@/components/structured-data";
7 import siteMetadata from "@/content/siteMetadata";
8 import { absoluteUrl, genPageMetadata } from "../../seo";
9
10 export async function generateStaticParams() {
11 return allKtis.map((kti) => ({
12 term: kti.term,
13 }));
14 }
15
16 export async function generateMetadata({
17 params,
18 }: {
19 params: Promise<{ term: string }>;
20 }): Promise<Metadata> {
21 const { term } = await params;
22 const kti = allKtis.find((kti) => kti.term === term);
23
24 if (!kti) return {};
25
26 const keywords = [
27 kti.term,
28 kti.category,
29 ...(kti.tags || []),
30 "kamus teknologi informasi",
31 "istilah IT",
32 "bahasa Indonesia",
33 ];
34
35 if (kti.englishTerm) {
36 keywords.splice(1, 0, kti.englishTerm);
37 }
38
39 return {
40 ...genPageMetadata({
41 title: `${kti.title} - Kamus Teknologi Informasi`,
42 description: kti.description,
43 path: kti.slug,
44 locale: "id_ID",
45 type: "article",
46 openGraph: {
47 title: `${kti.title} - KTI | ${siteMetadata.title}`,
48 description: kti.description,
49 url: absoluteUrl(kti.slug),
50 siteName: siteMetadata.title,
51 locale: "id_ID",
52 type: "article",
53 publishedTime: kti.lastmod,
54 modifiedTime: kti.lastmod,
55 authors: [siteMetadata.author],
56 },
57 }),
58 keywords,
59 };
60 }
61
62 const difficultyConfig = {
63 pemula: {
64 label: "Pemula",
65 badge:
66 "bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/20",
67 tag: "bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-500/20",
68 },
69 menengah: {
70 label: "Menengah",
71 badge:
72 "bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-500/20",
73 tag: "bg-amber-50 dark:bg-amber-500/10 text-amber-700 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-500/20",
74 },
75 lanjut: {
76 label: "Lanjut",
77 badge:
78 "bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 border border-indigo-200 dark:border-indigo-500/20",
79 tag: "bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 hover:bg-indigo-100 dark:hover:bg-indigo-500/20",
80 },
81 } as const;
82
83 type Difficulty = keyof typeof difficultyConfig;
84
85 export default async function KtiTermPage({
86 params,
87 }: {
88 params: Promise<{ term: string }>;
89 }) {
90 const { term } = await params;
91 const kti = allKtis.find((kti) => kti.term === term);
92
93 if (!kti || !kti.published) notFound();
94
95 const relatedKtis = kti.relatedTerms
96 ? allKtis.filter(
97 (k) =>
98 k.published &&
99 k.term !== kti.term &&
100 kti.relatedTerms?.includes(k.term),
101 )
102 : [];
103
104 const sameCategoryKtis = allKtis
105 .filter(
106 (k) =>
107 k.published &&
108 k.term !== kti.term &&
109 k.category === kti.category &&
110 !kti.relatedTerms?.includes(k.term),
111 )
112 .slice(0, 8);
113
114 const difficulty = kti.difficulty as Difficulty;
115 const config = difficultyConfig[difficulty] ?? difficultyConfig.pemula;
116
117 const termStructuredData = {
118 "@context": "https://schema.org",
119 "@graph": [
120 {
121 "@type": "DefinedTerm",
122 name: kti.title,
123 description: kti.description,
124 url: absoluteUrl(kti.slug),
125 inLanguage: "id-ID",
126 termCode: kti.term,
127 alternateName: [kti.englishTerm, kti.acronym].filter(Boolean),
128 inDefinedTermSet: absoluteUrl("/kti"),
129 },
130 {
131 "@type": "BreadcrumbList",
132 itemListElement: [
133 {
134 "@type": "ListItem",
135 position: 1,
136 name: "Home",
137 item: absoluteUrl("/"),
138 },
139 {
140 "@type": "ListItem",
141 position: 2,
142 name: "KTI",
143 item: absoluteUrl("/kti"),
144 },
145 {
146 "@type": "ListItem",
147 position: 3,
148 name: kti.title,
149 item: absoluteUrl(kti.slug),
150 },
151 ],
152 },
153 ],
154 };
155
156 return (
157 <div className="mx-auto max-w-3xl px-6 py-16 lg:px-8">
158 <StructuredData data={termStructuredData} />
159 {/* ── Breadcrumb ──────────────────────────────────── */}
160 <nav className="mb-10 flex items-center gap-2 text-xs text-slate-400 dark:text-slate-600">
161 <Link
162 href="/"
163 className="transition-colors hover:text-slate-700 dark:hover:text-slate-300"
164 >
165 Home
166 </Link>
167 <span>/</span>
168 <Link
169 href="/kti"
170 className="transition-colors hover:text-slate-700 dark:hover:text-slate-300"
171 >
172 KTI
173 </Link>
174 <span>/</span>
175 <span className="text-slate-600 dark:text-slate-400">{kti.title}</span>
176 </nav>
177
178 {/* ── Page header ─────────────────────────────────── */}
179 <div className="mb-10">
180 <div className="mb-5 flex flex-wrap items-center gap-2">
181 <span
182 className={`rounded-full px-2.5 py-0.5 text-xs font-semibold ${config.badge}`}
183 >
184 {config.label}
185 </span>
186 <span className="rounded-full border border-slate-200 dark:border-white/[0.08] bg-slate-100 dark:bg-white/[0.04] px-2.5 py-0.5 text-xs font-medium text-slate-600 dark:text-slate-400">
187 {kti.category}
188 </span>
189 {kti.acronym && (
190 <span className="rounded-full border border-slate-200 dark:border-white/[0.08] bg-slate-100 dark:bg-white/[0.04] px-2.5 py-0.5 font-mono text-xs text-slate-500 dark:text-slate-500">
191 {kti.acronym}
192 </span>
193 )}
194 </div>
195
196 <h1 className="text-4xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-5xl">
197 {kti.title}
198 </h1>
199 {kti.englishTerm && (
200 <p className="mt-2 text-lg text-slate-400 dark:text-slate-500">
201 {kti.englishTerm}
202 </p>
203 )}
204
205 {/* Description callout */}
206 <div className="mt-6 rounded-xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] p-5">
207 <p className="text-base leading-relaxed text-slate-700 dark:text-slate-300">
208 {kti.description}
209 </p>
210 </div>
211
212 {/* Tags */}
213 {kti.tags && kti.tags.length > 0 && (
214 <div className="mt-4 flex flex-wrap items-center gap-1.5">
215 {kti.tags.map((tag) => (
216 <span
217 key={tag}
218 className="rounded-md bg-slate-100 dark:bg-white/[0.05] px-2 py-0.5 text-xs text-slate-500 dark:text-slate-500"
219 >
220 #{tag}
221 </span>
222 ))}
223 </div>
224 )}
225 </div>
226
227 {/* ── MDX body ────────────────────────────────────── */}
228 <article className="prose dark:prose-invert prose-slate max-w-none prose-headings:font-semibold prose-headings:tracking-tight prose-a:text-indigo-600 dark:prose-a:text-indigo-400 prose-code:rounded prose-code:bg-slate-100 dark:prose-code:bg-white/[0.06] prose-code:px-1 prose-code:py-0.5 prose-code:text-slate-800 dark:prose-code:text-slate-200 prose-code:before:content-none prose-code:after:content-none prose-pre:rounded-xl prose-pre:border prose-pre:border-slate-200 dark:prose-pre:border-white/[0.06] prose-pre:bg-[#0D1117] mb-16">
229 <Mdx code={kti.body.code} />
230 </article>
231
232 {/* ── Related terms ───────────────────────────────── */}
233 {relatedKtis.length > 0 && (
234 <section className="mb-12 border-t border-slate-200 dark:border-white/[0.06] pt-10">
235 <p className="mb-6 text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
236 Istilah Terkait
237 </p>
238 <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
239 {relatedKtis.map((rel) => {
240 const relConfig =
241 difficultyConfig[rel.difficulty as Difficulty] ??
242 difficultyConfig.pemula;
243 return (
244 <Link
245 key={rel.term}
246 href={rel.slug}
247 className="group rounded-xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] p-4 transition-colors hover:border-indigo-300 dark:hover:border-indigo-700/50 hover:bg-slate-50 dark:hover:bg-white/[0.04]"
248 >
249 <div className="mb-2 flex items-start justify-between gap-2">
250 <h3 className="font-semibold text-slate-900 dark:text-white transition-colors group-hover:text-indigo-600 dark:group-hover:text-indigo-400">
251 {rel.title}
252 </h3>
253 <span
254 className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold ${relConfig.badge}`}
255 >
256 {relConfig.label}
257 </span>
258 </div>
259 {rel.englishTerm && (
260 <p className="mb-1 text-xs text-slate-400 dark:text-slate-600">
261 {rel.englishTerm}
262 </p>
263 )}
264 <p className="line-clamp-2 text-sm leading-relaxed text-slate-500 dark:text-slate-400">
265 {rel.description}
266 </p>
267 </Link>
268 );
269 })}
270 </div>
271 </section>
272 )}
273
274 {/* ── Same category ───────────────────────────────── */}
275 {sameCategoryKtis.length > 0 && (
276 <section className="mb-12 border-t border-slate-200 dark:border-white/[0.06] pt-10">
277 <p className="mb-5 text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
278 Lainnya dalam {kti.category}
279 </p>
280 <div className="flex flex-wrap gap-1.5">
281 {sameCategoryKtis.map((cat) => {
282 const catConfig =
283 difficultyConfig[cat.difficulty as Difficulty] ??
284 difficultyConfig.pemula;
285 return (
286 <Link
287 key={cat.term}
288 href={cat.slug}
289 className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${catConfig.tag}`}
290 >
291 {cat.title}
292 </Link>
293 );
294 })}
295 </div>
296 </section>
297 )}
298
299 {/* ── Back link ───────────────────────────────────── */}
300 <div className="mb-12 border-t border-slate-200 dark:border-white/[0.06] pt-8">
301 <Link
302 href="/kti"
303 className="inline-flex items-center gap-1.5 text-sm font-medium text-slate-500 dark:text-slate-400 transition-colors hover:text-slate-900 dark:hover:text-white"
304 >
305 ← Kembali ke Daftar Istilah
306 </Link>
307 </div>
308
309 {/* ── CTA ─────────────────────────────────────────── */}
310 <div className="relative overflow-hidden rounded-2xl bg-indigo-600 px-8 py-12 text-center shadow-2xl shadow-indigo-500/20">
311 <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.1),transparent)]" />
312 <h2 className="relative text-xl font-bold tracking-tight text-white">
313 Siap membangun dengan smbCloud?
314 </h2>
315 <p className="relative mt-2 text-sm text-indigo-200">
316 Deploy NodeJS, Swift, dan Ruby dengan satu perintah.
317 </p>
318 <Link
319 href="https://console.smbcloud.xyz"
320 target="_blank"
321 className="relative mt-6 inline-block rounded-lg bg-white px-6 py-2.5 text-sm font-semibold text-indigo-700 shadow-sm transition-colors hover:bg-indigo-50"
322 >
323 Mulai dengan smbCloud →
324 </Link>
325 </div>
326
327 {/* ── Last updated ────────────────────────────────── */}
328 <p className="mt-8 text-center text-xs text-slate-400 dark:text-slate-600">
329 Terakhir diperbarui:{" "}
330 {new Date(kti.lastmod).toLocaleDateString("id-ID", {
331 year: "numeric",
332 month: "long",
333 day: "numeric",
334 })}
335 </p>
336 </div>
337 );
338 }