Bosia Bosia v1.2.9

Server Metadata

Atur judul halaman, meta tag, Open Graph, dan atribut lang dengan fungsi metadata().

Fungsi metadata() memungkinkan Anda mendefinisikan tag SEO dan head untuk tiap halaman, berjalan di server sebelum load().

Penggunaan Dasar

Ekspor sebuah fungsi metadata dari +page.server.ts:

import type { MetadataEvent } from "bosia";

export function metadata({ params }: MetadataEvent) {
	return {
		title: "About — My App",
		description: "Learn more about our app.",
	};
}

Ini merender sebuah tag <title> dan tag <meta name="description"> di <head> halaman.

Open Graph & Tag Sosial

Gunakan array meta untuk menambahkan Open Graph, Twitter Card, atau meta tag kustom apa pun:

export function metadata({ params }: MetadataEvent) {
	return {
		title: "Blog Post",
		description: "A great blog post.",
		meta: [
			{ property: "og:title", content: "Blog Post" },
			{ property: "og:description", content: "A great blog post." },
			{ property: "og:type", content: "article" },
			{ name: "twitter:card", content: "summary_large_image" },
		],
	};
}

Tag dengan property dirender sebagai <meta property="...">, tag dengan name dirender sebagai <meta name="...">.

Atur atribut <html lang> dan tambahkan tag <link> untuk URL kanonik, alternatif hreflang, dan lainnya:

export function metadata() {
	return {
		title: "Mon Blog",
		lang: "fr",
		link: [
			{ rel: "canonical", href: "https://example.com/blog" },
			{ rel: "alternate", href: "https://example.com/en/blog", hreflang: "en" },
			{ rel: "alternate", href: "https://example.com/fr/blog", hreflang: "fr" },
		],
	};
}

Mengoper Data ke load()

Properti data memungkinkan Anda berbagi data yang sudah di-fetch dengan load(), menghindari query ganda:

import type { MetadataEvent, LoadEvent } from "bosia";

export function metadata({ params }: MetadataEvent) {
	const post = await db.getPost(params.slug);
	return {
		title: `${post.title} — Blog`,
		description: post.excerpt,
		meta: [{ property: "og:title", content: post.title }],
		// Pass to load() — avoids a second DB query
		data: { post },
	};
}

export async function load({ params, metadata }: LoadEvent) {
	// Reuse data from metadata(), fall back to fresh query
	const post = metadata?.post ?? (await db.getPost(params.slug));
	return { post };
}

Objek data dari metadata() menjadi event.metadata di load(). Jika tidak ada fungsi metadata(), event.metadata bernilai null.

Properti MetadataEvent

Properti Tipe Deskripsi
params Record<string, string> Parameter route dinamis
url URL URL request
locals Record<string, any> Data yang diset oleh middleware hooks
cookies Cookies Baca/tulis cookie
fetch Function Helper fetch (cookie hanya diteruskan ke same-origin — lihat Server Loaders → Cookie Forwarding)

Tipe Return Metadata

Properti Tipe Deskripsi
title string Tag <title> halaman
description string Tag <meta name="description">
meta Array<{ name?: string; property?: string; content: string }> Meta tag kustom
lang string Atribut <html lang>
link Array<{ rel: string; href: string; hreflang?: string }> Tag <link> (kanonik, hreflang, dll.)
data Record<string, any> Data yang dioper ke load() sebagai event.metadata

Semua properti opsional.

Form Actions

metadata() juga berjalan saat submit <form method="POST"> merender ulang halaman, jadi title, meta tag, lang, dan data yang dioper ke load() sama persis dengan GET biasa.

Redirect & Error

Kamu bisa melempar redirect() atau error() dari dalam metadata() — perilakunya sama seperti di load():

import { error } from "bosia";
import type { MetadataEvent } from "bosia";

export async function metadata({ params }: MetadataEvent) {
	const post = await db.getPost(params.slug);
	if (!post) error(404, "Post tidak ditemukan");
	return { title: post.title };
}

Pengecekan login tempatnya di guard hook, yang berjalan sebelum metadata(). +layout.server.ts tidak bisa menghentikan metadata(): keduanya mulai bersamaan.

Error lain di dalam metadata() dicatat ke log dan halaman dirender tanpa metadata, bukan menggagalkan request.

Title vs ``

Jangan pernah menetapkan <title> di metadata() dan <svelte:head> sekaligus pada rute yang sama — keduanya tidak digabung, dan pemenangnya berbeda tergantung siapa yang membaca.

Title dari metadata() yang dikirim di HTML SSR, jadi itu yang dibaca scraper, curl, dan crawler tanpa JS. Svelte mengompilasi <svelte:head><title> menjadi penulisan document.title yang jalan saat mount dan setiap kali nilainya berubah, jadi tab browser menampilkan yang itu. Rute yang mendeklarasikan keduanya mengiklankan satu judul tapi menampilkan judul lain.

Pilih metadata() untuk apa pun yang penting bagi tautan berbagi, lalu hapus <svelte:head><title> yang bersaing.

Selama navigasi sisi-klien, Bosia mengirim title, description, meta, link, dan lang dari metadata() dalam respons data. Router klien menyusun ulang <head> dari data itu tanpa reload halaman penuh, jadi og:*, twitter:*, canonical, dan robots sama dengan hasil muat-ulang penuh URL yang sama.

metadata.data sengaja tidak dikirim — ia dipakai load() di server dan bisa berisi nilai yang tidak boleh dilihat browser.

Router hanya mengganti tag yang dihasilkan metadata() (ditandai data-bosia-meta di HTML). Tag yang kamu tambahkan lewat <svelte:head> atau fragmen head sebuah plugin tidak disentuh.

<svelte:head> dirender di server ke dalam <head> mentah, di bagian akhir, setelah markup app.html-mu. Script dan stylesheet di sana dimuat seperti yang lain di <head>: <script src> biasa menahan tampilan pertama sampai selesai diunduh, jadi tambahkan async atau defer pada script pihak ketiga. Inline script di sana juga butuh nonce CSP saat policy aktif, dan data di dalam script {@html} seperti JSON-LD wajib meng-escape < (lihat Keamanan). Isi hanya dengan tag head (title, meta, link, style, script, noscript): <div>, <img>, atau teks biasa di sana membuat browser menutup <head> lebih awal dan merusak hydration.

Saat navigasi client, halaman yang tidak punya judul sendiri mendapat judul yang sama dengan hard load: judul yang ditulis layout-nya, lalu <title> di app.html, lalu Bosia App. Svelte hanya memasang judul saat komponen dipasang atau saat judulnya berubah, jadi layout yang tetap terpasang tidak memasang judulnya lagi. Bosia mengingat judul yang ditulis tiap layout, termasuk judul dari ekspresi ({data.site}) atau komponen anak seperti <Seo>, lalu memasangnya kembali. Tetap di halaman yang sama (invalidate(), form action, ?page=2) tidak mengubah judul yang dipasang komponen anak. Ada beberapa kasus yang tidak tertangkap. Komponen yang menulis judul setelah {@render children()} di layout dianggap milik halaman, jadi judulnya diganti saat pindah halaman; taruh sebelum {@render children()}. Layout yang memasang document.title di $effect atau onMount tidak diingat; pakai <svelte:head><title>. Judul layout yang berubah di antara dua navigasi (({unread}) Inbox) kembali ke nilai saat navigasi terakhir, sampai judulnya berubah lagi. Layout yang me-render {@render children()} lebih lambat dari judulnya (di dalam {#if}, {#await}, atau setelah onMount) tidak diingat. Halaman yang menulis judul di $effect.pre atau $: lama bisa dianggap milik layout-nya; pakai <svelte:head><title> di halaman. Di antara dua halaman dari route yang sama (/blog/a ke /blog/b), judul yang hanya tampil di dalam {#if} tetap ada saat halaman berikutnya menyembunyikannya; pakai metadata().title untuk judul per item. <title> dari fragmen head plugin juga diganti default; pakai metadata().title atau <title> di app.html.

Kalau tidak ada yang mengatur judul (bukan metadata(), <svelte:head>, plugin, maupun app.html), server menambahkan <title>Bosia App</title> sebagai tag terakhir di <head>. Halaman error memakai 404 — Not Found (status dan pesan) dengan cara yang sama, jadi <svelte:head><title> di +error.svelte tetap menang. Navigasi client ke halaman error memasang judul yang sama. <title> di app.html mematikan fallback Bosia App; di halaman error, judul status tetap ditaruh sebelum <title> app.html yang ada setelah %bosia.head%.

Timeout

Fungsi metadata() punya timeout yang bisa dikonfigurasi melalui variabel environment METADATA_TIMEOUT (dalam milidetik). Jika metadata() terlalu lama, ia timeout dengan anggun dan halaman dirender tanpa metadata.

Interaksi dengan Caching

Jika metadata() memanggil cookies.get() atau cookies.getAll(), respons data otomatis ditandai dengan Cache-Control: private, no-cache. Lihat Server Loaders untuk detailnya.