Back

Step setting gambar untuk astro framework

· FZH

in Astro

Astro native pada saat menghandle gambar ada dua cara, satu melalui /src/assets dan satu lagi /public yang membedakan astro default untuk gambar sudah dioptimasi ke .webp dan hanya terjadi difolder /assets jadi folder /public tidak terjadi optimasi, lalu apa yang terjadi? could be mess for Lighthouse Pagespeed.

disinilah peran stack ini yang saya develop dengan DeepSeek hari ini dari jam 2 pagi, tentu dalam perjalannya tidak selalu lancar. saya ga bilang ini stack sudah sempurna karena keterbatasan ilmu dan tools saya.

tapi kenapa saya maksa untuk pakai stack ini? ini semua karena saya akan menulis dari pages CMS yang mana untuk gambar folder output harus selalu /public dan disaat bersamaan lighthouse eksis, alasan lainnya saya mau jual ini template kepada potensialy non dev.

jadi dengan setting ini harapannya adalah:

oke jadi ini Dia stacknya.

ALUR GAMBAR DI FASTNEWS — DARI UPLOAD SAMPAI TAMPIL
====================================================

Dokumen ini menjelaskan alur lengkap gambar di proyek fastnews: stack yang
terlibat, plugin yang dipakai, dan perjalanan gambar mulai dari di-upload
lewat Pages CMS hingga dirender sebagai <img> dengan srcset AVIF/WebP di
browser. Tidak ada Sätteri untuk body gambar; Sätteri hanya dipakai untuk
mengubah <img> di HTML hasil Markdown agar srcset-nya terpasang. Sharp
bekerja sekali di prebuild.


1. STACK YANG TERLIBAT
----------------------

  Komponen              Peran
  --------------------  ------------------------------------------------
  Pages CMS             UI untuk upload gambar dan menulis post
  Sharp                 Generate varian AVIF/WebP + fallback WebP
  Manifest JSON         Peta path asli -> varian (dibaca dua konsumen)
  ResponsiveImg.astro   Konsumen manifest untuk HERO (frontmatter)
  hast-responsive-...   Konsumen manifest untuk BODY (Markdown)
  Sätteri               Prosesor Markdown Astro 7, jalankan HAST plugin
  Astro                 Framework statis, build HTML
  Cloudflare Pages      Hosting statis, sajikan /opt/* dan /uploads/*
  Browser               Pilih varian dari srcset berdasarkan viewport


2. FILE YANG TERLIBAT
---------------------

  .pages.yml
      - Konfigurasi upload gambar: media.input & media.output
      - Konfigurasi koleksi post

  scripts/build-image-manifest.mjs
      - Satu-satunya tempat Sharp dipanggil
      - Scan public/uploads/, hasilkan varian di public/opt/
      - Tulis image-manifest.json

  image-manifest.json
      - Hasil prebuild, tidak di-commit
      - Berisi entry per gambar: sources[], fallback, width, height

  public/uploads/
      - File asli dari CMS
      - URL publik: /uploads/namafile.jpg

  public/opt/
      - Output Sharp, tidak di-commit
      - URL publik: /opt/namafile-480.avif, dst

  src/components/ResponsiveImg.astro
      - Baca manifest, render <picture> untuk hero
      - Fallback ke <img> jika entry tidak ada

  src/lib/hast-responsive-images.mjs
      - Plugin HAST Sätteri
      - Baca manifest, TAMBAH srcset/sizes ke <img> yang sudah ada
      - Tidak mengganti node (menghindari srcset hilang)

  src/lib/manifest.ts
      - Fungsi getManifest() dengan cache
      - Dipakai ResponsiveImg.astro

  astro.config.mjs
      - markdown.processor: satteri({ hastPlugins: [...] })
      - image.remotePatterns (untuk URL eksternal Unsplash, opsional)


3. ALUR LENGKAP — UPLOAD VIA PAGES CMS
---------------------------------------

  LANGKAH 1 — User upload gambar di Pages CMS
  ---------------------------------------------
  Di editor post, user klik "Upload image" dan pilih file dari HP.
  Pages CMS membaca .pages.yml:

      media:
        input: public/uploads
        output: /uploads

  File disimpan ke public/uploads/namafile.webp.
  Pages CMS otomatis menulis markdown di body post:

      ![alt](/uploads/namafile.webp)

  Path yang ditulis adalah media.output (/uploads/...), tapi file fisiknya
  ada di public/uploads/. Kedua sisi konsisten karena folder public/
  disajikan apa adanya oleh Astro di URL /.


  LANGKAH 2 — prebuild jalan
  ---------------------------
  npm run build memicu prebuild:

      "scripts": {
        "prebuild": "node scripts/build-image-manifest.mjs",
        "build": "astro build"
      }

  scripts/build-image-manifest.mjs:

    a. walk() rekursif public/uploads/
       - Abaikan folder opt/ (output Sharp, bukan sumber)
       - Proses SEMUA file .jpg .jpeg .png .webp .avif
       - Tidak ada filter nama file; nama apapun diterima
         (727.jpg, Jshsh.png, 1-1.webp semuanya diproses)

    b. Untuk setiap file:
       - Baca dimensi via sharp.metadata()
       - Hitung usableWidths = [480, 960, 1440] yang <= lebar asli
       - Untuk setiap format (avif q50, webp q80):
           untuk setiap lebar:
             hasilkan public/opt/name-{w}.{ext}
             (skip jika file sudah ada dan lebih baru dari sumber)
       - Hasilkan fallback WebP di lebar maksimum (maks 1440)

    c. Tulis image-manifest.json:

       {
         "/uploads/namafile.webp": {
           "sources": [
             { "srcset": "/opt/name-480.avif 480w, ...", "type": "image/avif", ... },
             { "srcset": "/opt/name-480.webp 480w, ...", "type": "image/webp", ... }
           ],
           "fallback": "/opt/name-1440.webp",
           "width": 1440,
           "height": 960
         },
         ...
       }

  Log yang muncul:
    [images] found N source files
    [images] ✓ /uploads/namafile.webp
    [images] done — N entries in manifest


  LANGKAH 3 — astro build render HTML
  ------------------------------------
  Setelah prebuild selesai, astro build jalan. Dua jalur konsumen manifest:

  JALUR A — HERO (dari frontmatter image)
  ----------------------------------------
  Post.astro memanggil:

      <ResponsiveImg src={image} alt={title} priority={true} />

  ResponsiveImg.astro:
    1. Baca manifest via getManifest()
    2. Cari entry src = "/uploads/namafile.webp"
    3. Jika ada, render:

         <picture>
           <source srcset="/opt/name-480.avif 480w, ..." type="image/avif">
           <source srcset="/opt/name-480.webp 480w, ..." type="image/webp">
           <img src="/opt/name-1440.webp" width height
                loading="eager" fetchpriority="high" decoding="async">
         </picture>

    4. Jika tidak ada entry, fallback ke <img src={src}> biasa


  JALUR B — BODY (dari Markdown ![](/uploads/...))
  --------------------------------------------------
  Astro memproses .md lewat Sätteri. Sätteri menghasilkan HTML AST (HAST).
  Di sini, plugin hast-responsive-images.mjs berjalan:

    a. Filter: hanya node <img>
    b. Ambil src, normalisasi (terima /uploads/, uploads/, URL absolut)
    c. Cari di manifest
    d. Jika ada entry:
         - setProperty(src, entry.fallback)
         - setProperty(srcset, chosen.srcset)   // avif diutamakan
         - setProperty(sizes, chosen.sizes)
         - setProperty(loading, "lazy")
         - setProperty(decoding, "async")
         - setProperty(width, entry.width)
         - setProperty(height, entry.height)
       TIDAK mengganti node. Hanya menambah properti.
    e. Jika tidak ada entry:
         - Biarkan node apa adanya (gambar eksternal, atau belum di-generate)

  Hasil HTML:

    <img src="/opt/name-1440.avif"
         srcset="/opt/name-480.avif 480w, /opt/name-960.avif 960w, ..."
         sizes="(max-width: 640px) 100vw, 43rem"
         loading="lazy" decoding="async"
         width="1440" height="960">

  Kenapa pakai Sätteri untuk body?
    - Markdown tidak mengenal komponen .astro
    - components={{ img: ResponsiveImg }} hanya untuk MDX, bukan .md
    - Sätteri adalah satu-satunya cara Astro 7 menyisipkan plugin HAST
      yang bisa memodifikasi <img> hasil Markdown


  LANGKAH 4 — output dist/ dan deploy
  ------------------------------------
  Astro build menghasilkan:

    dist/
      index.html
      [slug]/index.html
      arsip/index.html
      opt/name-480.avif, name-480.webp, ...   (dari public/opt/)
      uploads/namafile.webp                   (dari public/uploads/)
      _astro/*                                (CSS, JS, font)
      pagefind/*                              (index pencarian)

  Cloudflare Pages menyajikan dist/ sebagai situs statis.
  public/_headers mengatur cache:
    /_astro/*  → immutable 1y
    /uploads/* → immutable 1y


  LANGKAH 5 — browser memilih varian
  -----------------------------------
  Saat user buka /post-slug/:

    a. Browser parse <img srcset="...">:
       - Cek format yang didukung (AVIF diutamakan dari <source type>)
       - Hitung lebar yang dibutuhkan dari sizes + viewport × DPR

    b. Skenario:
       - HP 360px DPR 2 → butuh 720px → unduh name-960.avif
       - HP 414px DPR 3 → butuh 1242px → unduh name-1440.avif
       - Desktop 1440px DPR 1 → unduh name-1440.avif
       - Desktop retina 1440 DPR 2 → name-1440.avif (paling besar tersedia)

    c. File yang diunduh = hasil Sharp (kecil, WebP/AVIF)
       Bukan file asli dari HP (bisa 3-5MB)


4. RINGKASAN SATU ARAH
----------------------

  Upload via CMS
    → public/uploads/x.webp
    → prebuild: Sharp
    → public/opt/x-{480,960,1440}.{avif,webp}
    → image-manifest.json
    → astro build:
        hero  : ResponsiveImg  → <picture> + <source srcset>
        body  : hast plugin    → <img srcset="...">
    → dist/
    → Cloudflare Pages
    → browser pilih varian dari srcset
    → user lihat gambar sesuai ukuran layar


5. SETTING PAGES CMS YANG WAJIB
-------------------------------

  .pages.yml:

    media:
      input: public/uploads
      output: /uploads

    content:
      - name: posts
        label: Catatan
        type: collection
        path: src/content/posts
        filename: "{primary}.md"
        fields:
          - { name: title, label: Judul, type: string, required: true }
          - { name: description, label: Deskripsi SEO, type: text, required: false }
          - { name: image, label: Cover, type: image, required: false }
          - name: date
            label: Tanggal
            type: date
            required: true
            options: { format: yyyy-MM-dd }
          - { name: body, label: Isi, type: rich-text }

  Penjelasan:
    media.input      : folder fisik tempat file disimpan (di repo)
    media.output     : path yang ditulis ke Markdown
    filename         : pola nama file post, {primary} = field title
    image (Cover)    : field untuk hero, mengisi frontmatter image:
    body             : editor rich-text, hasilnya Markdown


6. KONFIGURASI ASTRO YANG WAJIB
-------------------------------

  astro.config.mjs:

    markdown: {
      processor: satteri({
        hastPlugins: [hastResponsiveImages],
      }),
    },

  Tanpa blok ini, body Markdown tidak akan pernah menyentuh plugin,
  sehingga <img> dari CMS tidak mendapat srcset.

  scripts (package.json):

    "prebuild": "node scripts/build-image-manifest.mjs",
    "build": "astro build"

  Tanpa prebuild, image-manifest.json tidak ada, dan kedua konsumen
  (hero & body) tidak punya data varian.


7. TROUBLESHOOTING ALUR
-----------------------

  Gejala                                    Cek
  ----------------------------------------  ---------------------------------
  Hero teroptimasi, body tidak              Plugin Sätteri terdaftar di
                                            astro.config.mjs?
  Body teroptimasi, hero tidak              image frontmatter pakai
                                            /uploads/...?
  Keduanya tidak teroptimasi                image-manifest.json ada?
                                            prebuild jalan?
  Sebagian gambar body tidak teroptimasi    Path di markdown cocok dengan
                                            key manifest? Cek log [images]
  Gambar berubah jadi 404                   File asli ada di public/uploads/?
  /opt/* 404 di produksi                    public/opt/ ter-commit atau
                                            di-generate saat build?

apakah nanti saya akan ganti stack lagi karena ada yang lebih baik? bisa jadi, tapi untuk sekarang saya pakai ini saja.

tambahan stack guide astro

GUIDE ASTRO FRAMEWORK — FASTNEWS
================================

Dokumen ini adalah panduan proyek fastnews berbasis Astro 7: alur gambar
dari upload sampai render, SEO, canonical, robots, RSS, sitemap, halaman
404, arsip, Tailwind CSS, dan komponen shadcn-style. Ditulis agar mudah
dibaca manusia maupun LLM: struktur tegas, penanda jelas, tanpa asumsi
tersembunyi.

Catatan struktur folder: setiap proyek punya struktur berbeda. Bagian
"STRUKTUR FOLDER" hanya CONTOH. Yang mengikat adalah PERAN file dan
ALURNYA, bukan path literalnya. Jika proyek Anda memakai path lain,
petakan perannya (mis. "script yang memanggil Sharp", "komponen hero
responsif", "layout dasar") lalu sesuaikan.


0. RINGKASAN CEPAT (untuk LLM)
------------------------------

- Framework        : Astro 7 (static build)
- Markdown         : prosesor Sätteri + HAST plugin kustom
- Gambar           : Sharp di prebuild -> manifest JSON -> dua konsumen
                     (hero via komponen ResponsiveImg, body via HAST plugin)
- Hosting          : Cloudflare Pages
- Styling          : Tailwind CSS + komponen bergaya shadcn (lokal)
- SEO              : canonical, robots.txt, RSS, sitemap, meta OG
                     -> semua di satu sumber kebenaran (config situs)
- Komponen berulang: komponen presentasi di folder components,
                     konstanta situs di satu file config
- Aturan emas      : hal yang sering berubah (nama situs, URL, menu,
                     meta default) HANYA di satu file config.


1. STACK YANG TERLIBAT
----------------------

  Komponen                  Peran
  ------------------------  ----------------------------------------------
  Pages CMS                 UI upload gambar + tulis post
  Sharp                     Generate varian AVIF/WebP + fallback
  Manifest JSON             Peta path asli -> varian (dibaca 2 konsumen)
  Komponen ResponsiveImg    Konsumen manifest untuk HERO (frontmatter)
  Plugin HAST responsif     Konsumen manifest untuk BODY (Markdown)
  Sätteri                   Prosesor Markdown Astro 7, jalankan HAST plugin
  Astro                     Framework statis, build HTML
  Cloudflare Pages          Hosting statis, sajikan /opt/* dan /uploads/*
  Browser                   Pilih varian dari srcset berdasarkan viewport
  Tailwind CSS              Utility styling
  shadcn-style (lokal)      Komponen UI (button, card, badge, dll)


2. STRUKTUR FOLDER (CONTOH SAJA — FLEKSIBEL)
--------------------------------------------

  Setiap proyek boleh berbeda. Yang penting PERAN tiap file tersedia,
  bukan path literalnya. Contoh di bawah hanya ilustrasi:

    <root>/
      astro.config.mjs
      package.json
      .pages.yml
      image-manifest.json          (generated, tidak di-commit)
      scripts/
        build-image-manifest.mjs
      public/
        uploads/                   (file asli dari CMS)
        opt/                       (output Sharp, generated)
        _headers
      src/
        config/site.ts             (SITE, menu, meta default)
        lib/manifest.ts
        lib/hast-responsive-images.mjs
        components/
          ResponsiveImg.astro
          Seo.astro
          Header.astro
          Footer.astro
          PostCard.astro
          ui/                      (shadcn-style)
        layouts/
          BaseLayout.astro
          PostLayout.astro
        pages/
          index.astro
          [slug].astro
          arsip/index.astro
          404.astro
          rss.xml.ts
          sitemap.xml.ts
          robots.txt.ts
        content/posts/*.md
        styles/global.css
      tailwind.config.mjs

  PETUNJUK PEMETAAN JIKA STRUKTUR BERBEDA:
  - "script Sharp"        -> file apa pun yang dipanggil prebuild
  - "manifest"            -> file JSON hasil prebuild, path bebas,
                             asal dua konsumen membacanya dari sumber sama
  - "hero responsif"      -> komponen presentasi yang menerima src+alt
  - "plugin HAST body"    -> file .mjs yang didaftarkan di hastPlugins
  - "config situs"        -> file tunggal berisi nama, URL, menu, meta
  - "layout dasar"        -> layout yang membungkus SEO + header + footer

  Semua penjelasan di bawah TIDAK bergantung pada path literal di atas.


3. ALUR GAMBAR — UPLOAD SAMPAI TAMPIL
-------------------------------------

3.1 FILE YANG TERLIBAT (per peran, bukan path literal)

  Peran                     Tugas
  ------------------------  ----------------------------------------------
  Konfigurasi CMS           Atur media.input & media.output + koleksi
  Script Sharp              Satu-satunya tempat Sharp dipanggil.
                            Scan folder upload -> tulis varian ke folder
                            opt -> tulis manifest JSON
  Manifest JSON             Hasil prebuild. Entry per gambar: sources[],
                            fallback, width, height. Tidak di-commit
  Folder upload             File asli dari CMS. URL publik /uploads/nama
  Folder opt                Output Sharp. URL publik /opt/nama-{w}.{ext}
  Komponen hero responsif   Baca manifest, render <picture> + <source>
  Plugin HAST body          Baca manifest, TAMBAH srcset/sizes ke <img>,
                            tidak mengganti node (cegah srcset hilang)
  Helper manifest           getManifest() dengan cache
  Konfigurasi Astro         markdown.processor: satteri({ hastPlugins })


3.2 LANGKAH 1 — Upload via CMS

  User klik "Upload image" di editor post. CMS membaca konfigurasi media:
    input  : folder fisik di repo (contoh: public/uploads)
    output : path yang ditulis ke Markdown (contoh: /uploads)

  File tersimpan di folder input. Markdown otomatis:
    ![alt](/uploads/namafile.webp)

  Path di Markdown (/uploads/...) dan folder fisik konsisten karena
  Astro menyajikan folder public apa adanya di URL /.


3.3 LANGKAH 2 — Prebuild (Sharp)

  Build memicu prebuild:
    "prebuild": "node <script-sharp>",
    "build"   : "astro build"

  Script Sharp melakukan:
  a. Walk rekursif folder upload
     - Abaikan folder opt (output, bukan sumber)
     - Proses semua .jpg .jpeg .png .webp .avif
     - Tanpa filter nama file (727.jpg, Jshsh.png, 1-1.webp diproses)
  b. Per file:
     - sharp.metadata() -> dimensi asli
     - usableWidths = [480, 960, 1440] yang <= lebar asli
     - Untuk avif q50 dan webp q80, tiap lebar:
         hasilkan /opt/name-{w}.{ext}
         (skip jika sudah ada dan lebih baru dari sumber)
     - Fallback WebP di lebar maksimum (maks 1440)
  c. Tulis manifest JSON:
     {
       "/uploads/namafile.webp": {
         "sources": [
           { "srcset": "/opt/name-480.avif 480w, ...",
             "type": "image/avif", "sizes": "..." },
           { "srcset": "/opt/name-480.webp 480w, ...",
             "type": "image/webp", "sizes": "..." }
         ],
         "fallback": "/opt/name-1440.webp",
         "width": 1440,
         "height": 960
       }
     }

  Log yang muncul:
    [images] found N source files
    [images] ok /uploads/namafile.webp
    [images] done - N entries in manifest


3.4 LANGKAH 3 — Build render HTML

  Setelah prebuild, build Astro jalan. Dua jalur konsumen manifest:

  JALUR A — HERO (dari frontmatter image)
  ---------------------------------------
  Layout post memanggil komponen hero responsif:
    <ResponsiveImg src={image} alt={title} priority={true} />

  Komponen:
    1. getManifest()
    2. Cari entry dengan key = src
    3. Jika ada:
         <picture>
           <source srcset="/opt/name-480.avif 480w, ..."
                   type="image/avif">
           <source srcset="/opt/name-480.webp 480w, ..."
                   type="image/webp">
           <img src="/opt/name-1440.webp" width height
                loading="eager" fetchpriority="high"
                decoding="async">
         </picture>
    4. Jika tidak ada -> fallback <img src={src}> biasa

  JALUR B — BODY (dari Markdown ![](/uploads/...))
  -------------------------------------------------
  Astro memproses .md lewat Sätteri -> HAST -> plugin berjalan:
    a. Filter: hanya node <img>
    b. Normalisasi src (terima /uploads/, uploads/, URL absolut)
    c. Cari di manifest
    d. Jika ada entry:
         set src        = entry.fallback
         set srcset     = chosen.srcset (avif diutamakan)
         set sizes      = chosen.sizes
         set loading    = "lazy"
         set decoding   = "async"
         set width      = entry.width
         set height     = entry.height
       TIDAK mengganti node. Hanya menambah properti.
    e. Jika tidak ada -> biarkan node apa adanya
       (gambar eksternal / belum di-generate)

  Hasil HTML body:
    <img src="/opt/name-1440.avif"
         srcset="/opt/name-480.avif 480w, /opt/name-960.avif 960w, ..."
         sizes="(max-width: 640px) 100vw, 43rem"
         loading="lazy" decoding="async"
         width="1440" height="960">

  Kenapa pakai Sätteri untuk body?
    - Markdown tidak mengenal komponen Astro
    - components={{ img: ResponsiveImg }} hanya untuk MDX, bukan .md
    - Sätteri satu-satunya cara Astro 7 menyisipkan plugin HAST yang
      bisa memodifikasi <img> hasil Markdown


3.5 LANGKAH 4 — Output build & deploy

  Output build (ilustrasi):
    dist/
      index.html
      [slug]/index.html
      arsip/index.html
      404.html
      rss.xml
      sitemap.xml
      robots.txt
      opt/name-480.avif, name-480.webp, ...
      uploads/namafile.webp
      _astro/*       (CSS, JS, font)
      pagefind/*     (index pencarian)

  Cloudflare Pages menyajikan dist. Header cache yang disarankan:
    /_astro/*  -> immutable, max-age 1 tahun
    /uploads/* -> immutable, max-age 1 tahun
    /opt/*     -> immutable, max-age 1 tahun


3.6 LANGKAH 5 — Browser memilih varian

  - Browser parse <img srcset> + <source type>
  - Hitung lebar dibutuhkan = sizes x viewport x DPR
  - Contoh:
      HP 360 DPR 2       -> butuh 720  -> unduh name-960.avif
      HP 414 DPR 3       -> butuh 1242 -> unduh name-1440.avif
      Desktop 1440 DPR 1 -> unduh name-1440.avif
      Desktop 1440 DPR 2 -> name-1440.avif (terbesar tersedia)
  - File terunduh = hasil Sharp (kecil), bukan file asli dari HP


3.7 RINGKASAN SATU ARAH

  Upload via CMS
    -> folder upload/x.webp
    -> prebuild: Sharp
    -> opt/x-{480,960,1440}.{avif,webp}
    -> manifest JSON
    -> build Astro:
         hero : komponen hero -> <picture> + <source srcset>
         body : plugin HAST  -> <img srcset="...">
    -> dist/
    -> Cloudflare Pages
    -> browser pilih varian dari srcset
    -> user lihat gambar sesuai ukuran layar


4. KONFIGURASI WAJIB
--------------------

4.1 Konfigurasi CMS (contoh .pages.yml)

  media:
    input: public/uploads
    output: /uploads

  content:
    - name: posts
      label: Catatan
      type: collection
      path: src/content/posts
      filename: "{primary}.md"
      fields:
        - { name: title, label: Judul, type: string, required: true }
        - { name: description, label: Deskripsi SEO,
            type: text, required: false }
        - { name: image, label: Cover, type: image, required: false }
        - name: date
          label: Tanggal
          type: date
          required: true
          options: { format: yyyy-MM-dd }
        - { name: body, label: Isi, type: rich-text }

  Penjelasan:
    media.input   : folder fisik tempat file disimpan (di repo)
    media.output  : path yang ditulis ke Markdown
    filename      : pola nama file post, {primary} = field title
    image (Cover) : field untuk hero, mengisi frontmatter image:
    body          : editor rich-text, hasilnya Markdown


4.2 Konfigurasi Astro (contoh astro.config.mjs)

  import { defineConfig } from "astro/config";
  import satteri from "<paket-satteri>";
  import hastResponsiveImages from "<path-plugin-hast>";
  import tailwind from "@astrojs/tailwind";

  export default defineConfig({
    site: "https://fastnews.example.com",   // WAJIB
    integrations: [tailwind()],
    markdown: {
      processor: satteri({
        hastPlugins: [hastResponsiveImages],
      }),
    },
    image: {
      remotePatterns: [
        { protocol: "https", hostname: "images.unsplash.com" },
      ],
    },
  });

  Tanpa markdown.processor, body Markdown tidak menyentuh plugin,
  sehingga <img> dari CMS tidak mendapat srcset.
  Tanpa site, canonical/RSS/sitemap tidak akurat.


4.3 Scripts (contoh package.json)

  "scripts": {
    "dev"     : "astro dev",
    "prebuild": "node <script-sharp>",
    "build"   : "astro build",
    "preview" : "astro preview"
  }

  Tanpa prebuild, manifest tidak ada, hero & body tidak punya data
  varian.


5. SEO, CANONICAL, ROBOTS, RSS, SITEMAP
---------------------------------------

Prinsip: SATU SUMBER KEBENARAN.
  - Semua default (nama situs, URL, menu, meta) di satu file config.
  - Semua tag SEO dirender oleh satu komponen Seo.
  - Layout dasar memanggil komponen Seo sekali untuk semua halaman.


5.1 File config situs (peran)

  Berisi minimal:
    name, title, description, url, locale, author, twitter,
    ogImageDefault, menu[], buildDate

  Contoh isi (TypeScript):
    export const SITE = {
      name: "Fastnews",
      title: "Fastnews - Berita cepat, ringkas, akurat",
      description: "Kumpulan catatan dan berita ringkas.",
      url: "https://fastnews.example.com",
      locale: "id-ID",
      author: "Redaksi Fastnews",
      twitter: "@fastnews",
      ogImageDefault: "/uploads/og-default.webp",
      menu: [
        { label: "Beranda", href: "/" },
        { label: "Arsip",   href: "/arsip/" },
        { label: "RSS",     href: "/rss.xml" },
      ],
    };

  Semua komponen mengimpor dari sini. Ganti nama situs / domain cukup
  edit satu file.


5.2 Komponen Seo (peran)

  Props yang didukung minimal:
    title?, description?, image?, canonical?, type? ("website"|"article"),
    publishedTime?, noindex?

  Yang dirender:
    <title>
    <meta name="description">
    <link rel="canonical">            (absolut)
    <meta property="og:type">
    <meta property="og:title">
    <meta property="og:description">
    <meta property="og:url">
    <meta property="og:image">
    <meta property="og:site_name">
    <meta property="og:locale">
    <meta name="twitter:card">        (summary_large_image)
    <meta name="twitter:title">
    <meta name="twitter:description">
    <meta name="twitter:image">
    <meta name="robots" content="noindex,nofollow">  (jika noindex)

  Aturan canonical:
    - Selalu absolut
    - Dibangun dari URL situs di config + path halaman
    - Kecuali prop canonical diisi manual


5.3 robots.txt (peran)

  Rute yang mengembalikan text/plain berisi:
    User-agent: *
    Allow: /
    Disallow: /pagefind/
    Sitemap: <URL situs>/sitemap.xml

  Sitemap harus absolut.


5.4 sitemap.xml (peran)

  Iterasi koleksi post, emit <urlset>:
    /              -> priority 1.0
    /arsip/        -> 0.6
    /{slug}/       -> 0.8, lastmod = date, changefreq = monthly
  Aturan:
    - <loc> absolut (URL situs + path)
    - Content-Type: application/xml
    - Jangan masukkan halaman 404


5.5 rss.xml (peran)

  Pakai paket RSS resmi (contoh @astrojs/rss):
    - Sort post berdasarkan date desc
    - Item: title, pubDate, description, link absolut
    - customData: <language>{locale}</language>

  Di layout dasar sisipkan:
    <link rel="alternate" type="application/rss+xml" href="/rss.xml">
  (disisipkan sekali, bukan per halaman)


5.6 Halaman 404 (peran)

    - Pakai layout dasar dengan noindex = true
    - Judul "404 - Tidak ditemukan"
    - Link ke "/" dan "/arsip/"
    - Cloudflare Pages otomatis menyajikan 404.html untuk path asing
    - JANGAN masukkan 404 ke sitemap


5.7 Ringkasan SEO - cek cepat

  Hal                     Lokasi (peran)              Catatan
  ----------------------  --------------------------  ------------------
  Nama/URL/deskripsi      file config situs           satu-satunya sumber
  Meta + OG + canonical   komponen Seo                dipanggil layout
  Canonical absolut       dibangun dari URL situs     jangan hardcode
  Robots                  rute robots.txt             sebutkan sitemap
  Sitemap                 rute sitemap.xml            absolut, no 404
  RSS                     rute rss.xml                alternate di layout
  404                     rute 404 + noindex          disajikan otomatis


6. KOMPONEN BERULANG — SATU TEMPAT
----------------------------------

Aturan: komponen presentasi TIDAK menyimpan konstanta situs. Semua yang
berulang diatur di satu tempat.

  Jenis                    Lokasi (peran)
  -----------------------  --------------------------------------------
  Konstanta situs & menu   file config situs
  Layout dasar             layout pembungkus (SEO + header + footer)
  Layout post              layout post (hero + meta + slot body)
  SEO                      komponen Seo
  Header / Footer          komponen, impor menu dari config situs
  Kartu post               satu komponen, dipakai di beranda & arsip
  Gambar responsif (hero)  komponen ResponsiveImg
  UI primitives            komponen ui (shadcn-style)

Cara ganti: edit satu file -> semua halaman ikut.
Contoh: ubah menu di config situs -> Header semua halaman berubah.


7. ARSIP
--------

Halaman arsip (peran):

  1. Ambil semua post, sort date desc
  2. Kelompokkan per tahun (Map<tahun, Post[]>)
  3. Render: tiap tahun -> <h2>{tahun}</h2> + daftar kartu post
  4. Komponen Seo dengan title "Arsip" dan canonical "/arsip/"
  5. Daftarkan "Arsip" di menu (config situs)
  6. Masukkan "/arsip/" di sitemap (priority 0.6)

Opsional pagination:
  - Pakai paginate() Astro di rute arsip berparameter
  - Jaga konsistensi canonical halaman 2+ (path atau query)


8. TAILWIND CSS + SHADCN-STYLE
------------------------------

8.1 Setup Tailwind (contoh)

  Install:
    npm i -D tailwindcss @astrojs/tailwind postcss autoprefixer

  tailwind.config.mjs:
    export default {
      content: ["./src/**/*.{astro,html,md,mdx,ts,tsx,js,jsx}"],
      theme: { extend: {} },
      plugins: [],
    };

  styles/global.css:
    @tailwind base;
    @tailwind components;
    @tailwind utilities;

  Impor global.css di layout dasar.


8.2 shadcn-style (lokal, bukan paket npm)

  Alasan: generator shadcn berbasis React, tidak langsung kompatibel
  dengan Astro. Praktiknya komponen bergaya shadcn disalin ke folder
  komponen ui, mis:
    button.astro, card.astro, badge.astro
  (tambah sesuai kebutuhan)

  Aturan:
    - Semua varian style didaftarkan di SATU file komponen
      (mis. map variants di button)
    - Token warna di tailwind.config (theme.extend.colors)
    - Komponen ui TIDAK mengimpor config situs (murni presentasi)


8.3 Varian Tailwind untuk gambar

  Saat mengisi sizes di manifest / plugin HAST, samakan dengan
  breakpoint Tailwind. Contoh default:
    sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 43rem"


9. TROUBLESHOOTING
------------------

  Gejala                                Cek
  ------------------------------------  ---------------------------------
  Hero teroptimasi, body tidak          Plugin Sätteri terdaftar di
                                        konfigurasi markdown Astro?
  Body teroptimasi, hero tidak          Frontmatter image pakai
                                        /uploads/...?
  Keduanya tidak teroptimasi            Manifest JSON ada? Prebuild
                                        jalan?
  Sebagian gambar body tidak            Path di Markdown cocok dengan
  teroptimasi                           key manifest? Cek log [images]
  Gambar jadi 404                       File asli ada di folder upload?
  /opt/* 404 di produksi                Folder opt ter-generate saat
                                        build? (Jangan commit)
  Canonical salah domain                URL situs di config sudah benar?
                                        site: di konfigurasi Astro?
  RSS kosong                            site: di config? getCollection
                                        mengembalikan data?
  Sitemap tidak muncul di Google        robots.txt menyebut sitemap?
                                        Sitemap absolut?
  404 tampil sebagai halaman default    Rute 404 ada? Cloudflare Pages
                                        deploy dist/404.html?
  Menu berubah tapi tidak konsisten     Ada hardcode menu di komponen?
                                        Pindahkan ke config situs


10. CHECKLIST DEPLOY
--------------------

  1. URL situs di config = domain produksi
  2. site: di konfigurasi Astro = domain produksi
  3. Build -> cek di dist: manifest, opt/*, rss.xml, sitemap.xml,
     404.html
  4. Header cache mengatur /opt/*, /uploads/*, /_astro/*
  5. Cloudflare Pages: build command = build, output = dist
  6. Verifikasi manual: /, satu post, /arsip/, /rss.xml, /sitemap.xml,
     /robots.txt, path acak -> 404
  7. Cek <link rel="canonical"> di view-source setiap halaman
  8. Cek <img srcset> di body post: minimal 3 kandidat 480w/960w/1440w


11. ATURAN UNTUK LLM / KONTRIBUTOR
----------------------------------

  1. Jangan panggil Sharp di luar script Sharp
  2. Jangan ganti node <img> di plugin HAST - hanya tambah properti
  3. Jangan hardcode domain/nama situs di komponen - impor config situs
  4. Jangan commit manifest JSON dan folder opt
  5. Semua meta/canonical/OG lewat komponen Seo
  6. Menu & link situs lewat config situs
  7. Komponen ui murni presentasi (tanpa config situs)
  8. Body Markdown: gunakan path /uploads/... agar cocok key manifest
  9. 404 harus noindex dan tidak masuk sitemap
 10. Perubahan skema konten: update konfigurasi CMS DAN definisi
     koleksi konten bersamaan
 12. Saat memetakan struktur proyek baru, cocokkan PERAN file (lihat
     bagian 2), bukan path literal di contoh.


12. RINGKASAN SATU HALAMAN
--------------------------

  - Gambar: upload -> Sharp prebuild -> manifest -> hero <picture>
    + body <img srcset> -> browser pilih varian
  - SEO: satu config situs + satu komponen Seo + rute robots,
    sitemap, rss, 404
  - Komponen berulang: satu tempat, tanpa hardcode
  - Styling: Tailwind + komponen shadcn-style lokal
  - Struktur folder: FLEKSIBEL, yang mengikat adalah PERAN & ALUR
  - Aturan emas: konstanta situs hanya di satu file config