10
SEO dan Metadata
Semua metadata situs berakar pada satu modul bebas dependensi, src/lib/seo.ts. Modul itu menyimpan identitas situs dan menurunkannya menjadi Metadata, BreadcrumbList, serta URL absolut yang dipakai bersama oleh layout, halaman, sitemap, robots, dan manifest src/lib/seo.ts:5-91.
Peta identitas situs ke keluaran metadata
graph TD
URL["SITE_URL"]
NAME["SITE_NAME"]
TAG["SITE_TAGLINE"]
DESC["SITE_DESCRIPTION"]
OG["baseOpenGraph"]
ABS["absoluteUrl(path)"]
PM["pageMetadata()"]
LAY["metadata di layout.tsx"]
SMM["sitemap.ts"]
RBT["robots.ts"]
MFT["manifest.ts"]
JLD["JsonLd dan objek JSON-LD"]
URL --> ABS
URL --> LAY
ABS --> SMM
ABS --> RBT
ABS --> JLD
NAME --> OG
NAME --> PM
NAME --> MFT
NAME --> LAY
NAME --> JLD
TAG --> LAY
DESC --> LAY
DESC --> MFT
OG --> PM
OG --> LAY
PM --> LAY
style URL fill:#f59e0b,color:#000
style NAME fill:#f59e0b,color:#000
style TAG fill:#f59e0b,color:#000
style DESC fill:#f59e0b,color:#000
style OG fill:#8b5cf6,color:#fff
style ABS fill:#8b5cf6,color:#fff
style PM fill:#8b5cf6,color:#fff
style LAY fill:#2d7d46,color:#fff
style SMM fill:#2d7d46,color:#fff
style RBT fill:#2d7d46,color:#fff
style MFT fill:#2d7d46,color:#fff
style JLD fill:#2d7d46,color:#fff
%% fig: Identitas situs dan keluaran metadata
%% desc: Empat konstanta identitas situs dipakai langsung oleh layout, manifest, JSON-LD, dan pageMetadata, lalu absoluteUrl menurunkan URL absolut untuk sitemap, robots, dan BreadcrumbList.Diagram itu menunjukkan arah ketergantungan yang sebenarnya: tidak ada keluaran metadata yang menulis nama situs atau domain sendiri, semuanya membaca konstanta di src/lib/seo.ts src/lib/seo.ts:5-91. Tabel berikut memerinci nilai tiap konstanta dan pemakainya.
| Konstanta | Nilai | Pemakai utama |
|---|---|---|
SITE_URL | https://quran-academy.alfath7.workers.dev | metadataBase di layout, absoluteUrl, @id JSON-LD |
SITE_NAME | Fathway Quran | template judul layout, baseOpenGraph, manifest, provider JSON-LD |
SITE_TAGLINE | Belajar Tajwid & Baca Al-Qur'an Online | homeTitle di layout, alt gambar Open Graph |
SITE_DESCRIPTION | kalimat pembuka Belajar membaca Al-Qur'an dari huruf hijaiyah… | deskripsi layout, deskripsi manifest, deskripsi EducationalOrganization |
pageMetadata: satu halaman, satu objek Metadata
pageMetadata menerima title, description, path, dan noIndex opsional, lalu mengembalikan objek Metadata yang lengkap src/lib/seo.ts:33-452. Isinya: title apa adanya, description yang sudah dipotong, alternates.canonical berisi path, openGraph hasil sebaran baseOpenGraph ditambah judul penuh, twitter dengan kartu summary_large_image, dan blok robots hanya bila noIndex dikirim.
| Field hasil | Nilai yang ditulis | Sumber |
|---|---|---|
title | title dari pemanggil, tanpa nama situs | nama situs ditambahkan template layout |
description | truncate(description), bawaan maksimum 160 karakter | truncate |
alternates.canonical | path relatif | diselesaikan terhadap metadataBase |
openGraph.title | ${title} · ${SITE_NAME} | fullTitle |
openGraph.description | summary yang sama dengan description | truncate |
twitter.card | summary_large_image | tetap |
robots | { index: false, follow: true } | hanya bila noIndex benar |
Judul sengaja dikembalikan tanpa nama situs karena template root layout %s · ${SITE_NAME} yang menambahkannya, sedangkan kartu sosial membawa nama situs sendiri lewat fullTitle src/lib/seo.ts:33-452. Template itu dipasang di metadata layout bersama default: homeTitle, metadataBase: new URL(SITE_URL), daftar keywords, authors, category: "education", serta robots yang mengizinkan index dan follow dengan max-image-preview: "large" dan max-snippet: -1 src/app/layout.tsx:28-413.
Pemotongan deskripsi dilakukan truncate, yang lebih dulu merapatkan spasi, mengembalikan teks apa adanya bila panjangnya tidak melewati batas, dan bila melewati batas memotong di batas kata lalu menambahkan … src/lib/seo.ts:22-274.
Siklus satu halaman yang punya data dinamis berjalan seperti ini.
sequenceDiagram
participant GM as generateMetadata
participant PM as pageMetadata
participant TR as truncate
participant NX as Metadata Next
GM->>PM: title, description, path
PM->>TR: description
TR-->>PM: summary maksimum 160 karakter
PM-->>NX: title, description, canonical, openGraph, twitter
NX->>NX: template %s ditambah nama situs dari layout
%% fig: Siklus pembentukan metadata satu halaman
%% desc: generateMetadata menyerahkan judul dan deskripsi, pageMetadata memotong deskripsi lewat truncate, lalu Next menyusun tag akhir dengan template judul dari root layout.Pemanggilnya terbagi dua pola. Halaman daftar menetapkan metadata statis: /pemula src/app/pemula/page.tsx:11-155, /lanjutan src/app/lanjutan/page.tsx:11-156, dan /tes src/app/tes/page.tsx:11-157. Halaman detail memakai generateMetadata karena judul dan deskripsi diambil dari data: latihan Pemula src/app/pemula/[slug]/page.tsx:20-248, materi Lanjutan src/app/lanjutan/[slug]/page.tsx:26-309, dan kategori tes src/app/tes/[kategori]/page.tsx:20-3010. Beranda adalah pengecualian: ia hanya menambah alternates: { canonical: "/" } dan mewarisi sisanya dari layout src/app/page.tsx:1311.
Kartu sosial dibentuk dari baseOpenGraph yang memuat siteName, locale: "id_ID", type: "website", dan satu gambar og-image.png berukuran 1200×627 dengan alt gabungan nama situs dan tagline src/lib/seo.ts:1112. Objek itu dipakai ulang oleh layout dan oleh setiap halaman lewat pageMetadata src/lib/seo.ts:14-1913.
⚠️ Objek
openGraphmilik satu halaman mengganti seluruh objek induknya, bukan menimpanya sebagian. Karena itubaseOpenGraphwajib disebar ulang di setiap pemanggil; menghapus sebaran itu membuatsiteName,locale, dan gambar Open Graph hilang src/lib/seo.ts:14-1913.
⚠️
SITE_DESCRIPTIONdi root layout dan manifest tidak melewatitruncate, jadi panjangnya utuh. Hanya deskripsi yang lewatpageMetadatayang dipotong ke 160 karakter src/lib/seo.ts:22-274.
Canonical dan URL absolut
Nilai canonical dan openGraph.url selalu ditulis sebagai path relatif, misalnya /lanjutan/${lesson.slug}, dan diselesaikan terhadap metadataBase di layout src/app/layout.tsx:28-413. Untuk tempat yang butuh string utuh, absoluteUrl menggabungkan path dengan SITE_URL memakai new URL src/lib/seo.ts:4714. Fungsi itu dipakai sitemap.ts, robots.ts, logo JSON-LD beranda, dan item pada BreadcrumbList.
sitemap, robots, dan manifest
sitemap() membangun entri dari helper entry(path, priority) yang menyimpan url: absoluteUrl(path) dan priority saja — tanpa lastModified maupun changeFrequency src/app/sitemap.ts:7-1715.
| Path di sitemap | priority | Asal entri |
|---|---|---|
/ | 1 | literal |
/lanjutan | 0.9 | literal |
/pemula | 0.9 | literal |
/tes | 0.6 | literal |
/tes/{kategori} | 0.5 | materiCategories, saat ini hanya tajwid |
/pemula/{slug} dan /lanjutan/{slug} | 0.8 | listAllLessons() lewat lessonHref |
Daftar kategori materi berasal dari materiCategories = ["tajwid"] src/lib/categories.ts:716, sedangkan lessonHref memilih /pemula/{slug} untuk latihan dan /lanjutan/{slug} untuk tajwid src/lib/categories.ts:31-3317. Sumber seluruh materi adalah listAllLessons() yang menggabungkan latihan lebih dulu, lalu pertemuan tajwid src/lib/lessons.ts:33-3518.
robots() mengizinkan seluruh path untuk userAgent: "*" dan mencantumkan sitemap sebagai absoluteUrl("/sitemap.xml") src/app/robots.ts:4-919. manifest() memakai SITE_NAME sebagai name, Fathway sebagai short_name, SITE_DESCRIPTION sebagai description, lang: "id", start_url: "/", display: "standalone", warna #f7f2e8 dan #0d4a3a, serta dua ikon /icon.svg dan /apple-icon.png berukuran 512×512 src/app/manifest.ts:4-1820. Warna tema untuk browser diatur terpisah lewat viewport, dengan #f7f2e8 untuk mode terang dan #0e1714 untuk mode gelap src/app/layout.tsx:43-4821.
JSON-LD
Semua structured data dicetak oleh komponen JsonLd, yang menulis <script type="application/ld+json"> dan meng-escape setiap < menjadi \u003c supaya isi konten tidak bisa menutup tag skrip src/components/json-ld.tsx:1-422.
| Halaman | Tipe schema.org | Objek sumber |
|---|---|---|
| Beranda | EducationalOrganization dan WebSite dalam satu @graph | siteJsonLd, dengan @id #organization dan #website |
| Beranda | FAQPage | faqJsonLd dari daftar faqs |
| Materi Lanjutan | LearningResource dan BreadcrumbList | jsonLd di halaman detail |
Beranda menyusun EducationalOrganization dengan logo: absoluteUrl("/apple-icon.png") dan description: SITE_DESCRIPTION, lalu WebSite yang menunjuk publisher ke @id organisasi; keduanya dirender bersama faqJsonLd sebagai dua pemanggilan JsonLd di awal halaman src/app/page.tsx:15-3523 src/app/page.tsx:125-13324 src/app/page.tsx:143-14425.
Halaman materi menambahkan LearningResource berisi name, description, url absolut, inLanguage: "id-ID", learningResourceType: "Materi", educationalLevel: "Pemula", teaches dari label kategori, isAccessibleForFree, dan provider yang memakai SITE_NAME serta SITE_URL src/app/lanjutan/[slug]/page.tsx:50-6726. Kedua objek itu dikirim sekaligus dalam satu array ke JsonLd.
⚠️ Nilai
educationalLevelpada halaman materi ditulis tetap"Pemula"walaupun halaman berada di bawah/lanjutan, jadi nilai itu tidak membedakan tingkat kesulitan materi src/app/lanjutan/[slug]/page.tsx:50-6726.
Breadcrumb dibangun breadcrumbJsonLd dari pasangan [name, path] dengan beranda di urutan pertama; setiap path diubah menjadi URL absolut lewat absoluteUrl dan position dihitung dari indeks src/lib/seo.ts:50-5527. Halaman materi mengirim tiga pasang: Beranda, label kategori, dan judul materi src/app/lanjutan/[slug]/page.tsx:6528.
Yang perlu dijaga saat menambah halaman
- Halaman baru yang publik dan boleh diindeks sebaiknya memakai
pageMetadataagarcanonical,openGraph, dantwitterkonsisten; contoh pemakaiannya ada di Kurikulum Pemula dan Kurikulum Lanjutan src/lib/seo.ts:33-452. - Setiap halaman baru juga perlu ditambahkan ke
sitemap(); tanpa itu halaman tetap terindeks lewat tautan, tetapi tidak muncul di/sitemap.xmlsrc/app/sitemap.ts:7-1715. SITE_URLharus sama dengan domain hasil deploy, karena nilai itu menjadimetadataBasedan dasarabsoluteUrlsrc/lib/seo.ts:5-91. Hubungannya dengan proses rilis dibahas di Pengujian dan Deploy.- Tes memakai
dynamicParams = false, sehingga kategori di luarmateriCategoriestidak dirender sebagai halaman baru src/app/tes/[kategori]/page.tsx:20-3010. Topik tesnya sendiri dibahas di Tes Pemahaman Tajwid.