April 30, 2026 at 5:00 PM
Membangun Route SEO untuk Website Bergaya Desktop di Next.js
Bagaimana saya menyusun konten MDX, full-page routes, metadata, sitemap, robots.txt, dan file AI-readable untuk portfolio bergaya desktop di Next.js.

Catatan tentang Mengubah Website Bergaya Aplikasi Menjadi Halaman yang Bisa Di-crawl
Di catatan sebelumnya, saya menulis tentang mengapa website bergaya desktop tetap bisa memiliki SEO yang kuat.
Tulisan ini adalah lanjutan yang lebih teknis.
Bukan sebagai tutorial yang kaku, tetapi sebagai gambaran praktis tentang bagaimana saya biasanya memikirkan arsitektur di baliknya: bagaimana sebuah website yang terasa seperti desktop tetap bisa menyediakan route yang bersih, konten yang bisa dibaca, metadata, sitemap entries, structured data, dan teks yang mudah dipahami AI.
Bagian yang menarik bukan hanya visual layer-nya.
Bagian yang menarik adalah batas di antara layer-layer itu.
Di mana desktop experience berakhir? Di mana semantic web dimulai? Dan bagaimana keduanya memakai konten yang sama tanpa menduplikasi semuanya?
Itulah masalah utama yang ingin saya pecahkan.
1. Mulai dari Konten, Bukan dari Window
Ketika membangun website bergaya desktop, kita mudah tergoda untuk memulai dari komponen window.
AboutWindow. ReadmeWindow. PreviewWindow. NotesWindow. FinderWindow.
Ini wajar, karena UI adalah bagian yang paling terlihat.
Namun jika konten hanya hidup di dalam komponen-komponen itu, arsitekturnya menjadi rapuh. Website bisa terlihat bagus, tetapi kontennya menjadi lebih sulit digunakan ulang.
Untuk SEO, arah seperti ini biasanya kurang sehat.
Saya lebih suka memulai dari file konten.
Sebagai contoh:
public/
about/
page.mdx
documents/
sandi.mdx/
page.mdx
desktop/
items/
pharmart/
page.mdx
pharmart.webp
notes/
insights/
desktop-style-website-with-powerful-seo/
page.mdxIde pentingnya sederhana:
File konten memiliki makna. Window memiliki experience. Route memiliki URL publik.
Pemisahan ini membuat sistem lebih mudah berkembang.
Ketika saya menambahkan project baru, saya tidak ingin mengedit lima komponen UI berbeda hanya agar project itu muncul di Finder, Preview, halaman SEO, dan file AI-readable. Saya ingin file kontennya membawa informasi utama, lalu setiap layer merendernya sesuai tanggung jawab masing-masing.
2. Gunakan MDX Frontmatter sebagai Kontrak Bersama
MDX berguna karena ia memberi konten sebuah kontrak yang bisa diprediksi.
Sebuah file bisa menyimpan metadata dan body content di tempat yang sama:
---
title: Pharmart
description: A healthcare commerce project built with modern web architecture.
language: en
category: Work
source: pharmart.webp
static: pharmart.webp
---
## Overview
Pharmart is a digital healthcare commerce project...Frontmatter tidak perlu dibuat rumit.
Justru saya lebih suka ketika ia tetap membosankan.
Field seperti title, description, language, category, source, dan static sudah cukup untuk banyak kasus. Nanti, ketika konten membutuhkan kemampuan tambahan, field seperti speech, order, atau tags bisa ditambahkan dengan hati-hati.
Kuncinya adalah konsistensi.
Jika setiap desktop item memiliki bentuk frontmatter yang bisa diprediksi, loader bisa membangun banyak pengalaman dari data yang sama:
- desktop icon data.
- Finder row data.
- Preview window data.
- SEO route metadata.
- sitemap entry.
- structured data.
- AI-readable summary.
Di titik ini, frontmatter bukan sekadar metadata.
Ia menjadi kontrak kecil untuk konten.
3. Tulis Content Loader yang Scoped dan Mudah Diprediksi
Di Next.js, membaca konten MDX lokal biasanya berarti menggunakan fs, path, dan gray-matter.
Loader sederhana bisa terlihat seperti ini:
import fs from "node:fs/promises";
import path from "node:path";
import matter from "gray-matter";
const itemsDirectory = path.join(
process.cwd(),
"public",
"desktop",
"items",
);
export async function loadDesktopItems() {
const entries = await fs.readdir(itemsDirectory, { withFileTypes: true });
const items = await Promise.all(
entries
.filter((entry) => entry.isDirectory())
.map(async (entry) => {
const itemDirectory = path.join(itemsDirectory, entry.name);
const sourcePath = path.join(itemDirectory, "page.mdx");
const raw = await fs.readFile(sourcePath, "utf8");
const { data, content } = matter(raw);
return {
slug: entry.name,
title: String(data.title ?? entry.name),
description: String(data.description ?? ""),
content,
};
}),
);
return items;
}Ada dua detail yang saya perhatikan di sini.
Pertama, path-nya scoped.
Loader membaca dari:
path.join(process.cwd(), "public", "desktop", "items")bukan dari path project yang sepenuhnya dinamis.
Kedua, loader melakukan normalisasi data.
UI tidak seharusnya menebak apakah title ada, apakah description kosong, atau apakah slug harus diambil dari frontmatter atau nama folder. Pekerjaan seperti itu lebih tepat berada di dekat content loader.
Ini bukan hanya soal kerapian.
Ini juga mengurangi kesalahan SEO.
Metadata yang buruk sering muncul dari kontrak konten yang tidak jelas.
4. Render Konten yang Sama di Desktop Windows
Setelah loader mengembalikan data yang bisa diprediksi, desktop UI bisa tetap fokus pada experience.
Sebagai contoh, Preview window tidak perlu tahu bagaimana file MDX disimpan di disk. Ia hanya membutuhkan sebuah item:
type PreviewItem = {
title: string;
description: string;
imageSrc?: string;
content: string;
};Lalu desktop bisa membukanya:
<PreviewWindow
item={{
title: project.title,
description: project.description,
imageSrc: project.staticSrc,
content: project.content,
}}
/>Ide yang sama berlaku untuk dokumen bergaya TextEdit.
Window seharusnya merender dokumen, bukan memiliki dokumen.
Ini terdengar kecil, tetapi mengubah cara codebase berkembang. Window baru bisa ditambahkan tanpa mengubah format konten. Konten baru bisa ditambahkan tanpa menulis ulang logika window.
Pemisahan seperti ini yang saya sukai dalam website bergaya desktop.
UI tetap playful.
Data flow tetap membosankan.
Biasanya, itu kombinasi yang baik.
5. Buat Full-Page Routes untuk Konten Penting
Desktop experience sangat baik untuk eksplorasi.
Tetapi untuk SEO, konten penting tetap membutuhkan route.
Di Next.js App Router, route About yang localized bisa terlihat seperti ini:
src/
app/
about/
page.tsx
[lang]/
page.tsxRoute default bisa diarahkan ke bahasa utama:
import { redirect } from "next/navigation";
export default function AboutIndexPage() {
redirect("/about/en");
}Kemudian route bahasa bisa dirender secara statis:
import { notFound } from "next/navigation";
import { loadAboutContent } from "@/lib/content/about";
import { AboutFullPage } from "@/features/seo/components/AboutFullPage";
const languages = ["en", "id"] as const;
type AboutLanguage = (typeof languages)[number];
export function generateStaticParams() {
return languages.map((lang) => ({ lang }));
}
export default async function AboutPage({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
if (!languages.includes(lang as AboutLanguage)) {
notFound();
}
const aboutContent = await loadAboutContent();
return (
<AboutFullPage
aboutContent={aboutContent}
lang={lang as AboutLanguage}
/>
);
}Pola ini memberi konten URL publik yang normal:
/about/en
/about/idAbout window di desktop tetap bisa ada.
Tetapi sekarang mesin pencari, external links, dan crawler AI juga memiliki route yang bersih untuk dibaca.
Inilah hybrid model yang saya sukai.
Window untuk experience. Route untuk indexing. Konten yang sama di balik keduanya.
6. Generate Metadata dari Konten yang Sama
Full-page route tidak cukup hanya merender konten.
Ia juga perlu menjelaskan dirinya.
Di sinilah generateMetadata menjadi penting:
import type { Metadata } from "next";
import { absoluteUrl, site } from "@/lib/seo/site";
import { loadAboutContent } from "@/lib/content/about";
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
}): Promise<Metadata> {
const { lang } = await params;
const aboutContent = await loadAboutContent();
const content = lang === "id" ? aboutContent.idContent : aboutContent.en;
return {
title: content.title,
description: content.description,
alternates: {
canonical: `/about/${lang}`,
languages: {
en: "/about/en",
id: "/about/id",
},
},
openGraph: {
type: "profile",
url: absoluteUrl(`/about/${lang}`),
siteName: site.name,
title: content.title,
description: content.description,
},
};
}Ini salah satu bagian yang menurut saya penting.
Metadata tidak seharusnya menjadi cerita hardcoded yang terpisah.
Jika konten berubah, metadata sebaiknya ikut berubah.
Tentu, tidak semuanya harus otomatis. Kadang metadata membutuhkan penyesuaian editorial. Tetapi sebagai pola dasar, sumber konten sebaiknya tetap dekat dengan sumber metadata.
Ini mengurangi drift.
Dan drift adalah salah satu masalah SEO yang diam-diam sering terjadi.
Halamannya mengatakan satu hal. Title mengatakan hal lain. Open Graph description masih memakai versi lama. Sitemap menunjuk ke tempat lain.
Ketidakkonsistenan kecil seperti itu membuat website terasa kurang terawat, baik untuk manusia maupun mesin.
7. Buat Sitemap yang Cukup Eksplisit untuk Dipercaya
Untuk website yang kaya konten, sitemap generation tidak seharusnya menjadi sesuatu yang dipikirkan belakangan.
Sitemap sederhana bisa menggabungkan static routes dan dynamic content:
import type { MetadataRoute } from "next";
import { absoluteUrl } from "@/lib/seo/site";
import { loadNotes } from "@/lib/content/notes";
import { loadDesktopItems } from "@/lib/content/desktopItems";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const [notes, desktopItems] = await Promise.all([
loadNotes(),
loadDesktopItems(),
]);
return [
{
url: absoluteUrl("/"),
priority: 1,
},
{
url: absoluteUrl("/about/en"),
priority: 0.9,
},
{
url: absoluteUrl("/about/id"),
priority: 0.9,
},
...notes.map((note) => ({
url: absoluteUrl(`/insights/${note.slug}`),
lastModified: note.date,
priority: 0.75,
})),
...desktopItems.map((item) => ({
url: absoluteUrl(`/work/${item.slug}`),
lastModified: item.modifiedAt,
priority: 0.8,
})),
];
}Angka priority yang persis bukan bagian paling penting.
Yang penting adalah konten penting bisa ditemukan.
Untuk website bergaya desktop, ini penting karena tidak semua konten secara alami dijangkau melalui navigasi halaman biasa. Sebagian konten mungkin dibuka melalui dock, desktop icon, window, atau Spotlight.
Itu baik untuk user.
Tetapi crawler tetap terbantu oleh sitemap yang bersih.
Sitemap menjadi peta formal dari website.
8. Robots.txt Harus Jelas, Bukan Terlalu Pintar
Saya lebih suka robots.txt yang sederhana.
Jika niatnya adalah mengizinkan indexing, katakan dengan jelas:
import type { MetadataRoute } from "next";
import { absoluteUrl } from "@/lib/seo/site";
const AI_AND_SEARCH_AGENTS = [
"Googlebot",
"Bingbot",
"Applebot",
"GPTBot",
"ChatGPT-User",
"OAI-SearchBot",
"ClaudeBot",
"Claude-User",
"PerplexityBot",
] as const;
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
},
{
userAgent: [...AI_AND_SEARCH_AGENTS],
allow: "/",
},
],
sitemap: absoluteUrl("/sitemap.xml"),
};
}Secara teknis, wildcard rule sudah mengizinkan semuanya.
Daftar agent yang eksplisit lebih berfungsi sebagai documentation layer. Ia membuat niatnya terlihat: website ini terbuka untuk mesin pencari dan crawler AI yang menghormati aturan robots.
Saya tidak suka membuat bagian ini terlalu rumit.
Jika sebuah website ingin ditemukan, robots file seharusnya tidak terasa seperti teka-teki.
9. Tambahkan Structured Data untuk Makna yang Lebih Kuat
Structured data membantu menjelaskan apa representasi dari sebuah halaman.
Untuk personal portfolio, ini bisa mencakup Person, WebSite, AboutPage, Article, atau CreativeWork.
Blok JSON-LD sederhana bisa terlihat seperti ini:
import Script from "next/script";
type JsonLdProps = {
data: Record<string, unknown>;
};
export function JsonLd({ data }: JsonLdProps) {
return (
<Script
id="structured-data"
type="application/ld+json"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: JSON.stringify(data),
}}
/>
);
}Lalu halaman bisa mengirim structured data:
<JsonLd
data={{
"@context": "https://schema.org",
"@type": "AboutPage",
name: "About Sandi Maulana Juhana",
url: "https://sandimaulanajuhana.com/about/en",
about: {
"@type": "Person",
name: "Sandi Maulana Juhana",
jobTitle: "Full-Stack Engineer",
},
}}
/>Structured data tidak menggantikan konten yang baik.
Ia memperjelasnya.
Perbedaan itu penting.
Jika halamannya tipis, structured data tidak akan secara ajaib membuatnya kuat. Tetapi jika halaman sudah memiliki konten yang jelas, structured data membantu mesin memahami peran halaman itu dengan lebih percaya diri.
10. Buat File AI-Readable dari Konten yang Sama
Untuk keterbacaan AI, saya menyukai gagasan merawat llms.txt dan llms-full.txt.
Keduanya bukan pengganti sitemap atau structured data.
Keduanya lebih seperti panduan yang mudah dibaca.
Sebagai contoh:
# Sandi Maulana Juhana
Personal website and desktop-style portfolio of Sandi Maulana Juhana.
## Key Pages
- Home: /
- About English: /about/en
- About Indonesian: /about/id
- Profile English: /profile/en
- Profile Indonesian: /profile/id
## Topics
- Full-stack engineering
- Next.js architecture
- Desktop-style web interfaces
- SEO and AI-readable contentYang penting bukan hanya memiliki file-nya.
Yang penting adalah menjaga file itu tetap selaras dengan konten asli.
Jika route baru ditambahkan, AI-readable map sebaiknya ikut diperbarui. Jika konten About berubah, ringkasannya tidak seharusnya tertinggal dengan versi lama selamanya.
Sekali lagi, ini soal mengurangi drift.
SEO yang baik sering kali tidak sedramatis yang dibayangkan.
Sebagian besar hanya tentang menjaga makna publik tetap konsisten di banyak permukaan kecil.
11. Hindari Menduplikasi Konten Itu Sendiri
Ada satu jebakan dalam arsitektur seperti ini:
Menduplikasi konten terlalu banyak.
Itu bisa terjadi pelan-pelan.
About text di dalam AboutWindow. About text lain di dalam /about/en. Summary lain di dalam schema. Versi lain di dalam llms.txt. Versi lain lagi di dalam Open Graph metadata.
Pada awalnya, ini terasa tidak berbahaya.
Tetapi seiring waktu, satu versi berubah dan versi lain tidak.
Karena itu saya lebih suka alur seperti ini:
MDX content
-> desktop window
-> full-page route
-> metadata
-> sitemap
-> structured data
-> llms.txtTidak semua layer membutuhkan konten penuh.
Tetapi setiap layer sebaiknya diturunkan dari, atau setidaknya selaras dengan, source of truth yang sama.
Inilah yang membuat sistem terasa bersih.
Website bisa memiliki banyak wajah tanpa memiliki banyak cerita yang saling bertentangan.
Refleksi Penutup
Website bergaya desktop mudah sekali berubah menjadi kumpulan UI yang mengesankan.
Itu tidak selalu buruk.
Tetapi jika tujuannya adalah discoverability jangka panjang, arsitekturnya perlu lebih dalam daripada sekadar metafora visual.
Window bukan konten. Dock bukan strategi navigasi. Animasi bukan information architecture. Desktop bukan pengganti web.
Ia adalah experience layer.
Web tetap membutuhkan routes. Search tetap membutuhkan metadata. AI tetap membutuhkan konteks yang bisa dibaca. User tetap membutuhkan link yang bisa dibuka, dibagikan, dan dikunjungi kembali.
Bagi saya, versi terkuat dari website bergaya desktop bukanlah website yang menyembunyikan web sepenuhnya.
Tetapi website yang memahami web dengan cukup baik untuk membelokkan interface-nya tanpa merusak fondasinya.
Itulah keseimbangan yang terus saya tuju.
Playful di permukaan.
Terstruktur di bawahnya.