| 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 | } |