Struktur proyek & konvensi folder
Hanya dua folder di dalam src/ yang punya arti khusus bagi Astro. Sisanya konvensi — tapi konvensi yang layak diikuti kalau kamu ingin kodenya masih terbaca dua tahun lagi.
Intisari
src/pages/adalah satu-satunya folder yang menghasilkan rute. Satu berkas = satu URL.public/disalin apa adanya tanpa diproses;src/assets/dioptimasi dan diberi nama ber-hash.- Menaruh gambar artikel di
public/berarti membuang seluruh optimasi gambar Astro — kesalahan paling mahal di portal berita. src/components/,src/layouts/,src/lib/murni konvensi. Astro tidak peduli namanya.- Alias
@/*ditsconfig.jsonmenghapus../../../yang bikin refactor menyakitkan.
Struktur yang dipakai sepanjang roadmap ini
src/
├── pages/ ← MENGHASILKAN RUTE. satu berkas = satu URL
│ ├── index.astro → /
│ ├── berita/
│ │ ├── index.astro → /berita
│ │ └── [slug].astro → /berita/apa-saja
│ ├── perusahaan/
│ │ └── [kode].astro → /perusahaan/BBCA
│ └── api/
│ └── midtrans-webhook.ts → /api/midtrans-webhook
├── layouts/ ← kerangka halaman (konvensi)
├── components/ ← komponen .astro dan .vue (konvensi)
├── lib/ ← kode non-UI: db, cache, auth (konvensi)
│ ├── db.ts
│ ├── artikel.ts
│ └── midtrans.ts
├── assets/ ← DIPROSES: gambar, font
├── styles/ ← CSS global (konvensi)
└── middleware.ts ← ISTIMEWA: dijalankan di tiap request
public/ ← DISALIN APA ADANYA, tidak diproses
├── robots.txt
└── favicon.ico
Yang punya arti khusus bagi Astro cuma tiga: src/pages/,
src/middleware.ts, dan src/content.config.ts (Fase 3). Sisanya nama bebas —
kamu bisa menaruh komponen di src/potongan/ dan Astro tidak akan protes. Tapi setiap orang
yang membaca kodemu nanti mengharapkan nama yang standar, jadi pakai yang standar.
Jebakan nomor satu: public/ vs src/assets/
public/ | src/assets/ | |
|---|---|---|
| Diproses saat build | Tidak | Ya |
| Nama berkas | Persis seperti aslinya | Diberi hash isi |
| Gambar dioptimasi & diubah format | Tidak | Ya — WebP/AVIF, beberapa ukuran |
| Boleh di-cache selamanya | Tidak (nama bisa dipakai ulang untuk isi berbeda) | Ya (isi berubah → nama berubah) |
| Referensi di kode | /logo.png — string biasa | import logo from "@/assets/logo.png" |
| Salah ketik ketahuan saat | Runtime, sebagai 404 | Build, sebagai error |
---
import { Image } from "astro:assets";
import sampul from "@/assets/sampul-default.jpg";
---
<!-- Dioptimasi: WebP, ukuran tepat, lebar/tinggi terisi otomatis -->
<Image src={sampul} alt="Sampul" width={800} />
<!-- Dari public/: apa adanya, tidak dioptimasi, tidak diperiksa -->
<img src="/sampul-default.jpg" alt="Sampul" />
Aturannya sederhana: kalau gambarnya kamu import, taruh di src/assets/.
Kalau URL-nya harus tetap dan bisa ditebak orang lain, taruh di public/. Isi
public/ di portal berita seharusnya hanya robots.txt, favicon.ico,
berkas verifikasi domain, dan ads.txt — tidak lebih.
Gambar artikel dari database bukan salah satu dari keduanya. Ia tidak ada saat build, jadi
tidak bisa di-import. Ia butuh jalur ketiga: <Image> dengan URL remote
plus image.domains di konfigurasi, atau CDN gambar terpisah. Ini dibahas di Fase 8 — dan
untuk portal berita, ini keputusan yang berdampak besar ke biaya.
Alias import — pasang sekarang, bukan nanti
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
// Sebelum — rapuh, dan berubah tiap berkas dipindah
import { db } from "../../../lib/db";
// Sesudah — sama di mana pun berkasnya berada
import { db } from "@/lib/db";
Ini terlihat kosmetik sampai kamu memindahkan satu folder dan harus memperbaiki empat puluh import.
Astro dan Vite membaca paths dari tsconfig.json langsung — tidak perlu
konfigurasi tambahan di astro.config.mjs.
Batas yang layak ditegakkan sejak awal
Satu aturan yang akan menyelamatkanmu di Fase 5 dan 7:
src/pages/ tidak boleh berisi kueri database atau logika bisnis.
Halaman memanggil fungsi dari src/lib/, dan fungsi itu yang tahu soal SQL.
---
// BURUK: kueri langsung di halaman
const artikel = await db.selectFrom("artikel")
.where("slug", "=", Astro.params.slug!)
.selectAll()
.executeTakeFirst();
---
---
// BAIK: halaman tidak tahu apa-apa soal SQL
import { ambilArtikelPublik } from "@/lib/artikel";
const artikel = await ambilArtikelPublik(Astro.params.slug!);
---
Alasannya bukan kerapian. Kueri yang tersebar di halaman tidak bisa diuji, tidak bisa di-cache di satu
tempat, dan — yang paling mahal — tidak bisa dipastikan selalu menyaring status = 'terbit'.
Satu halaman yang lupa menambahkan filter itu cukup untuk menerbitkan draf ke publik. Nama fungsinya
sengaja ambilArtikelPublik, bukan ambilArtikel: batasannya ada di nama.
astro.config.mjs — yang perlu kamu kenali sekarang
import { defineConfig } from "astro/config";
import vue from "@astrojs/vue";
import node from "@astrojs/node";
export default defineConfig({
site: "https://portal.contoh.id", // wajib untuk sitemap & URL kanonik
output: "static", // diubah di Fase 1
adapter: node({ mode: "standalone" }),
integrations: [vue()],
});
site sering dilewatkan dan itu kesalahan. Tanpanya, Astro.site bernilai
undefined, sitemap tidak bisa dibuat, dan URL kanonik yang kamu tulis untuk SEO jadi relatif —
persis hal yang paling tidak boleh salah saat memigrasikan portal berita yang sudah terindeks.
Latihan: susun kerangka folder di atas di proyekmu, pasang alias @/*, lalu buat
src/lib/artikel.ts berisi satu fungsi ambilArtikelPublik(slug) yang untuk
sementara mengembalikan data palsu. Panggil dari src/pages/berita/[slug].astro memakai
alias. Terakhir: taruh satu gambar di public/ dan satu lagi di src/assets/,
render keduanya, jalankan pnpm build, lalu bandingkan nama berkas dan ukurannya di
dist/.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.