Website Resmi Karang Taruna — Jekyll + GitHub Pages

Berikut adalah script lengkap untuk membangun website resmi Organisasi Karang Taruna dengan Jekyll, Material Design 3, SEO-friendly, performa tinggi, dan mudah dikelola.


📁 Struktur Proyek

karang-taruna-web/
├── _config.yml
├── Gemfile
├── index.html
├── 404.html
├── robots.txt
├── _data/
│   ├── navigation.yml
│   ├── pengurus.yml
│   ├── program_kerja.yml
│   └── social.yml
├── _layouts/
│   ├── default.html
│   ├── post.html
│   ├── page.html
│   └── archive.html
├── _includes/
│   ├── head.html
│   ├── header.html
│   ├── footer.html
│   ├── breadcrumb.html
│   ├── article-card.html
│   ├── share.html
│   ├── comments.html
│   ├── search-modal.html
│   ├── skeleton.html
│   └── schema.html
├── _sass/
│   ├── _variables.scss
│   ├── _base.scss
│   ├── _layout.scss
│   ├── _components.scss
│   ├── _md3.scss
│   └── _markdown.scss
├── _pages/
│   ├── about.md
│   ├── struktur.md
│   ├── program-kerja.md
│   ├── kegiatan.md
│   ├── galeri.md
│   ├── dokumen.md
│   ├── kontak.md
│   ├── pengumuman.md
│   ├── agenda.md
│   ├── kategori.md
│   ├── tags.md
│   ├── arsip.md
│   └── search.md
├── _posts/
│   └── 2026-06-30-selamat-datang.md
├── assets/
│   ├── css/style.scss
│   ├── js/main.js
│   └── img/
└── feed.xml

1️⃣ _config.yml

title: Karang Taruna Desa Sejahtera
description: Portal resmi Karang Taruna Desa Sejahtera — Pemuda Berkarya, Desa Berjaya.
url: https://karangtaruna-sejahtera.github.io
baseurl: ""
lang: id-ID
timezone: Asia/Jakarta

# Author & Organization
author: Karang Taruna Desa Sejahtera
email: karangtaruna.sejahtera@gmail.com
logo: /assets/img/logo.png

# Social
social:
  facebook: https://facebook.com/karangtaruna.sejahtera
  instagram: https://instagram.com/karangtaruna.sejahtera
  youtube: https://youtube.com/@karangtaruna.sejahtera
  twitter: https://twitter.com/karangtaruna
  whatsapp: https://wa.me/6281234567890

# Build
markdown: kramdown
highlighter: rouge
permalink: /berita/:year/:month/:day/:title/
paginate: 6
paginate_path: "/berita/page:num/"

kramdown:
  input: GFM
  syntax_highlighter: rouge
  hard_wrap: false
  auto_ids: true
  toc_levels: 1..3

plugins:
  - jekyll-paginate
  - jekyll-sitemap
  - jekyll-feed
  - jekyll-seo-tag
  - jekyll-gist

# Exclude
exclude:
  - Gemfile
  - Gemfile.lock
  - README.md
  - node_modules
  - vendor

# Defaults
defaults:
  - scope:
      path: ""
      type: posts
    values:
      layout: post
      author: "Redaksi Karang Taruna"
      comments: true
  - scope:
      path: "_pages"
    values:
      layout: page

# Collections
collections:
  pages:
    output: true
    permalink: /:path/

# Giscus
giscus:
  repo: karangtaruna-sejahtera/web
  repo_id: "R_xxxxx"
  category: "Announcements"
  category_id: "DIC_xxxxx"
  mapping: pathname
  reactions_enabled: 1
  emit_metadata: 0
  theme: preferred_color_scheme
  lang: id

# SEO
twitter:
  username: karangtaruna
facebook:
  publisher: https://facebook.com/karangtaruna.sejahtera

2️⃣ Gemfile

source "https://rubygems.org"

gem "github-pages", group: :jekyll_plugins
gem "jekyll-paginate"
gem "jekyll-sitemap"
gem "jekyll-feed"
gem "jekyll-seo-tag"
gem "jekyll-gist"
gem "webrick"

3️⃣ _data/navigation.yml

main:
  - title: Beranda
    url: /
    icon: home
  - title: Profil
    url: /about/
    icon: info
  - title: Struktur
    url: /struktur/
    icon: account_tree
  - title: Program Kerja
    url: /program-kerja/
    icon: task_alt
  - title: Berita
    url: /berita/
    icon: newspaper
  - title: Kegiatan
    url: /kegiatan/
    icon: event
  - title: Galeri
    url: /galeri/
    icon: photo_library
  - title: Dokumen
    url: /dokumen/
    icon: folder
  - title: Kontak
    url: /kontak/
    icon: mail

4️⃣ _data/pengurus.yml

- nama: Ahmad Fauzi, S.Sos.
  jabatan: Ketua
  foto: /assets/img/pengurus/ketua.jpg
  bidang: Pimpinan Umum
- nama: Siti Nurhaliza
  jabatan: Sekretaris
  foto: /assets/img/pengurus/sekretaris.jpg
  bidang: Administrasi
- nama: Budi Santoso
  jabatan: Bendahara
  foto: /assets/img/pengurus/bendahara.jpg
  bidang: Keuangan
- nama: Dewi Lestari
  jabatan: Koordinator Bidang Kepemudaan
  foto: /assets/img/pengurus/kepemudaan.jpg
  bidang: Kepemudaan & Olahraga
- nama: Rudi Hartono
  jabatan: Koordinator Bidang Sosial
  foto: /assets/img/pengurus/sosial.jpg
  bidang: Kesejahteraan Sosial
- nama: Maya Anggraini
  jabatan: Koordinator Bidang Seni & Budaya
  foto: /assets/img/pengurus/seni.jpg
  bidang: Seni & Budaya

5️⃣ _data/program_kerja.yml

bidang:
  - nama: Kepemudaan & Olahraga
    icon: sports_soccer
    program:
      - nama: Turnamen Antar RT
        deskripsi: Kompetisi olahraga tahunan antar RT.
      - nama: Pelatihan Kepemimpinan Pemuda
        deskripsi: Workshop kepemimpinan untuk pemuda desa.
  - nama: Kesejahteraan Sosial
    icon: volunteer_activism
    program:
      - nama: Bakti Sosial Bulanan
        deskripsi: Kegiatan sosial rutin setiap bulan.
      - nama: Santunan Anak Yatim
        deskripsi: Pemberian santunan di hari besar keagamaan.
  - nama: Seni & Budaya
    icon: palette
    program:
      - nama: Festival Budaya Desa
        deskripsi: Pementasan seni budaya lokal.
      - nama: Sanggar Seni Remaja
        deskripsi: Wadah pengembangan bakat seni.
  - nama: Lingkungan Hidup
    icon: eco
    program:
      - nama: Kerja Bakti Lingkungan
        deskripsi: Pembersihan lingkungan mingguan.
      - nama: Bank Sampah
        deskripsi: Program daur ulang sampah.

6️⃣ _data/social.yml

- name: Facebook
  icon: facebook
  url: https://facebook.com/karangtaruna.sejahtera
  color: "#1877F2"
- name: Instagram
  icon: instagram
  url: https://instagram.com/karangtaruna.sejahtera
  color: "#E4405F"
- name: YouTube
  icon: youtube
  url: https://youtube.com/@karangtaruna.sejahtera
  color: "#FF0000"
- name: WhatsApp
  icon: whatsapp
  url: https://wa.me/6281234567890
  color: "#25D366"
- name: Email
  icon: mail
  url: mailto:karangtaruna.sejahtera@gmail.com
  color: "#0F9D58"

7️⃣ _layouts/default.html

<!DOCTYPE html>
<html lang="{{ site.lang | default: 'id-ID' }}" data-theme="light">
<head>
  {% include head.html %}
</head>
<body class="md3-body">
  <a class="skip-link" href="#main">Lewati ke konten</a>

  {% include header.html %}

  <div id="reading-progress" class="reading-progress" aria-hidden="true"></div>

  <main id="main" class="md3-main">
    {% include breadcrumb.html %}
    {{ content }}
  </main>

  {% include footer.html %}
  {% include search-modal.html %}

  <button id="back-to-top" class="fab" aria-label="Kembali ke atas" title="Kembali ke atas">
    <span class="material-symbols-rounded">arrow_upward</span>
  </button>

  <script src="{{ '/assets/js/main.js' | relative_url }}" defer></script>
</body>
</html>

8️⃣ _layouts/post.html

---
layout: default
---
{% assign words = page.content | number_of_words %}
{% assign read_time = words | divided_by: 200 | plus: 1 %}

<article class="post md3-surface-1" itemscope itemtype="https://schema.org/Article">
  <header class="post-header">
    {% if page.image %}
      <img class="post-hero" src="{{ page.image | relative_url }}" alt="{{ page.title }}" loading="eager" itemprop="image">
    {% endif %}

    <div class="post-meta-container">
      {% if page.categories.size > 0 %}
        <div class="post-categories">
          {% for cat in page.categories %}
            <a href="{{ '/kategori/' | append: cat | slugify | append: '/' | relative_url }}" class="md3-chip">{{ cat }}</a>
          {% endfor %}
        </div>
      {% endif %}

      <h1 class="post-title" itemprop="headline">{{ page.title }}</h1>

      <div class="post-meta" itemprop="author" itemscope itemtype="https://schema.org/Person">
        <span class="material-symbols-rounded">person</span>
        <span itemprop="name">{{ page.author | default: site.author }}</span>
        <span class="material-symbols-rounded">calendar_today</span>
        <time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">
          {{ page.date | date: "%d %B %Y" }}
        </time>
        <span class="material-symbols-rounded">schedule</span>
        <span>{{ read_time }} menit baca</span>
      </div>
    </div>
  </header>

  <div class="post-content md3-typography" itemprop="articleBody">
    {{ content }}
  </div>

  {% if page.tags.size > 0 %}
    <footer class="post-tags">
      <span class="material-symbols-rounded">sell</span>
      {% for tag in page.tags %}
        <a href="{{ '/tags/' | append: tag | slugify | append: '/' | relative_url }}" class="md3-chip">#{{ tag }}</a>
      {% endfor %}
    </footer>
  {% endif %}

  {% include share.html %}
  {% include comments.html %}
</article>

{% include schema.html %}

9️⃣ _layouts/page.html

---
layout: default
---
<article class="page md3-surface-1">
  <header class="page-header">
    <h1 class="page-title">{{ page.title }}</h1>
    {% if page.description %}<p class="page-subtitle">{{ page.description }}</p>{% endif %}
  </header>
  <div class="page-content md3-typography">
    {{ content }}
  </div>
</article>

🔟 _layouts/archive.html

---
layout: default
---
<div class="archive">
  <header class="page-header">
    <h1 class="page-title">{{ page.title }}</h1>
    {% if page.description %}<p class="page-subtitle">{{ page.description }}</p>{% endif %}
  </header>

  <div class="filter-bar">
    <div class="md3-tabs" role="tablist">
      <a href="{{ '/kategori/' | relative_url }}" class="md3-tab {% if page.layout_type == 'kategori' %}active{% endif %}">Kategori</a>
      <a href="{{ '/tags/' | relative_url }}" class="md3-tab {% if page.layout_type == 'tags' %}active{% endif %}">Tag</a>
      <a href="{{ '/arsip/' | relative_url }}" class="md3-tab {% if page.layout_type == 'arsip' %}active{% endif %}">Arsip</a>
    </div>
  </div>

  <div class="archive-content">
    {{ content }}
  </div>
</div>

1️⃣1️⃣ _includes/head.html

<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#2E7D32" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0F1B15" media="(prefers-color-scheme: dark)">

<title>{% if page.title %}{{ page.title }} — {{ site.title }}{% else %}{{ site.title }} — {{ site.description }}{% endif %}</title>
<meta name="description" content="{{ page.description | default: site.description | strip_html | truncate: 160 }}">
<meta name="author" content="{{ page.author | default: site.author }}">
<link rel="canonical" href="{{ page.url | absolute_url }}">

<!-- SEO -->
{% seo %}

<!-- Preconnect & Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://giscus.app" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,1,0&display=swap" rel="stylesheet">

<!-- Feeds & Sitemap -->
<link rel="alternate" type="application/rss+xml" title="{{ site.title }}" href="{{ '/feed.xml' | absolute_url }}">
<link rel="sitemap" type="application/xml" href="{{ '/sitemap.xml' | absolute_url }}">

<!-- Icons -->
<link rel="icon" type="image/png" href="{{ '/assets/img/favicon.png' | relative_url }}">
<link rel="apple-touch-icon" href="{{ '/assets/img/apple-touch-icon.png' | relative_url }}">

<!-- Stylesheet -->
<link rel="stylesheet" href="{{ '/assets/css/style.css' | relative_url }}">

<!-- JSON-LD Organization -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": {{ site.title | jsonify }},
  "url": {{ site.url | jsonify }},
  "logo": {{ site.logo | absolute_url | jsonify }},
  "description": {{ site.description | jsonify }},
  "email": {{ site.email | jsonify }},
  "sameAs": [
    {{ site.social.facebook | jsonify }},
    {{ site.social.instagram | jsonify }},
    {{ site.social.youtube | jsonify }}
  ]
}
</script>

1️⃣2️⃣ _includes/header.html

<header class="md3-app-bar" role="banner">
  <div class="app-bar-inner">
    <button class="icon-btn menu-toggle" aria-label="Buka menu" aria-expanded="false" aria-controls="primary-nav">
      <span class="material-symbols-rounded">menu</span>
    </button>

    <a href="{{ '/' | relative_url }}" class="brand" aria-label="{{ site.title }}">
      <img src="{{ site.logo | relative_url }}" alt="" width="36" height="36" loading="eager">
      <span class="brand-text">{{ site.title }}</span>
    </a>

    <nav id="primary-nav" class="primary-nav" aria-label="Navigasi utama">
      <ul class="nav-list">
        {% for item in site.data.navigation.main %}
          <li>
            <a href="{{ item.url | relative_url }}" class="nav-link {% if page.url == item.url %}active{% endif %}">
              <span class="material-symbols-rounded">{{ item.icon }}</span>
              {{ item.title }}
            </a>
          </li>
        {% endfor %}
      </ul>
    </nav>

    <div class="app-bar-actions">
      <button class="icon-btn" id="search-toggle" aria-label="Cari artikel" title="Cari (Ctrl+K)">
        <span class="material-symbols-rounded">search</span>
      </button>
      <button class="icon-btn" id="theme-toggle" aria-label="Ganti tema" title="Ganti tema">
        <span class="material-symbols-rounded theme-icon">dark_mode</span>
      </button>
    </div>
  </div>
</header>

1️⃣3️⃣ _includes/footer.html

<footer class="md3-footer" role="contentinfo">
  <div class="footer-grid">
    <div class="footer-col">
      <h3 class="footer-title">{{ site.title }}</h3>
      <p class="footer-about">{{ site.description }}</p>
      <div class="social-links">
        {% for s in site.data.social %}
          <a href="{{ s.url }}" class="social-icon" aria-label="{{ s.name }}" target="_blank" rel="noopener">
            <span class="material-symbols-rounded">{{ s.icon }}</span>
          </a>
        {% endfor %}
      </div>
    </div>

    <div class="footer-col">
      <h4>Tautan Cepat</h4>
      <ul class="footer-links">
        {% for item in site.data.navigation.main limit: 6 %}
          <li><a href="{{ item.url | relative_url }}">{{ item.title }}</a></li>
        {% endfor %}
      </ul>
    </div>

    <div class="footer-col">
      <h4>Kontak</h4>
      <ul class="footer-contact">
        <li><span class="material-symbols-rounded">location_on</span> Jl. Pemuda No. 17, Desa Sejahtera</li>
        <li><span class="material-symbols-rounded">mail</span> {{ site.email }}</li>
        <li><span class="material-symbols-rounded">call</span> +62 812-3456-7890</li>
      </ul>
    </div>

    <div class="footer-col">
      <h4>Langganan</h4>
      <p>Dapatkan informasi terbaru langsung di email Anda.</p>
      <a href="{{ '/feed.xml' | relative_url }}" class="md3-button outlined">
        <span class="material-symbols-rounded">rss_feed</span> RSS Feed
      </a>
    </div>
  </div>

  <div class="footer-bottom">
    <p>&copy; {{ 'now' | date: '%Y' }} {{ site.title }}. Seluruh hak cipta dilindungi.</p>
    <p>Dibangun dengan ❤️ menggunakan <a href="https://jekyllrb.com" target="_blank" rel="noopener">Jekyll</a> & <a href="https://pages.github.com" target="_blank" rel="noopener">GitHub Pages</a>.</p>
  </div>
</footer>

1️⃣4️⃣ _includes/breadcrumb.html

{% unless page.url == '/' %}
<nav class="breadcrumb" aria-label="Breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
  <ol>
    <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
      <a itemprop="item" href="{{ '/' | relative_url }}"><span itemprop="name">Beranda</span></a>
      <meta itemprop="position" content="1">
    </li>
    {% if page.categories.size > 0 and page.layout == 'post' %}
      <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
        <a itemprop="item" href="{{ '/kategori/' | append: page.categories.first | slugify | append: '/' | relative_url }}">
          <span itemprop="name">{{ page.categories.first }}</span>
        </a>
        <meta itemprop="position" content="2">
      </li>
    {% endif %}
    <li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem" aria-current="page">
      <span itemprop="name">{{ page.title }}</span>
      <meta itemprop="position" content="3">
    </li>
  </ol>
</nav>
{% endunless %}

1️⃣5️⃣ _includes/article-card.html

<article class="article-card md3-surface-1" itemscope itemtype="https://schema.org/Article">
  <a href="{{ include.post.url | relative_url }}" class="card-media" aria-hidden="true" tabindex="-1">
    {% if include.post.image %}
      <img src="{{ include.post.image | relative_url }}" alt="{{ include.post.title }}" loading="lazy" decoding="async" itemprop="image">
    {% else %}
      <div class="card-placeholder">
        <span class="material-symbols-rounded">article</span>
      </div>
    {% endif %}
  </a>
  <div class="card-body">
    {% if include.post.categories.size > 0 %}
      <div class="card-chips">
        {% for cat in include.post.categories limit: 2 %}
          <a href="{{ '/kategori/' | append: cat | slugify | append: '/' | relative_url }}" class="md3-chip small">{{ cat }}</a>
        {% endfor %}
      </div>
    {% endif %}
    <h3 class="card-title" itemprop="headline">
      <a href="{{ include.post.url | relative_url }}" itemprop="url">{{ include.post.title }}</a>
    </h3>
    <p class="card-excerpt" itemprop="description">{{ include.post.excerpt | strip_html | truncate: 140 }}</p>
    <div class="card-meta">
      <time datetime="{{ include.post.date | date_to_xmlschema }}" itemprop="datePublished">
        {{ include.post.date | date: "%d %b %Y" }}
      </time>
      <span>·</span>
      <span>{{ include.post.content | number_of_words | divided_by: 200 | plus: 1 }} menit baca</span>
    </div>
  </div>
</article>

1️⃣6️⃣ _includes/share.html

<div class="share-bar" aria-label="Bagikan artikel">
  <span class="share-label">Bagikan:</span>
  {% assign post_url = page.url | absolute_url %}
  {% assign post_title = page.title | url_encode %}
  <a class="icon-btn share-btn" href="https://www.facebook.com/sharer/sharer.php?u={{ post_url }}" target="_blank" rel="noopener" aria-label="Facebook" title="Facebook">
    <span class="material-symbols-rounded">facebook</span>
  </a>
  <a class="icon-btn share-btn" href="https://twitter.com/intent/tweet?url={{ post_url }}&text={{ post_title }}" target="_blank" rel="noopener" aria-label="Twitter" title="Twitter">
    <span class="material-symbols-rounded">close</span>
  </a>
  <a class="icon-btn share-btn" href="https://wa.me/?text={{ post_title }}%20{{ post_url }}" target="_blank" rel="noopener" aria-label="WhatsApp" title="WhatsApp">
    <span class="material-symbols-rounded">chat</span>
  </a>
  <a class="icon-btn share-btn" href="https://t.me/share/url?url={{ post_url }}&text={{ post_title }}" target="_blank" rel="noopener" aria-label="Telegram" title="Telegram">
    <span class="material-symbols-rounded">send</span>
  </a>
  <button class="icon-btn share-btn" id="copy-link" aria-label="Salin tautan" title="Salin tautan">
    <span class="material-symbols-rounded">content_copy</span>
  </button>
</div>

1️⃣7️⃣ _includes/comments.html

{% if page.comments != false and site.giscus.repo %}
<section class="comments" aria-label="Komentar">
  <h2 class="comments-title">
    <span class="material-symbols-rounded">forum</span> Diskusi
  </h2>
  <div class="giscus-wrapper md3-surface-2"></div>
  <script src="https://giscus.app/client.js"
    data-repo="{{ site.giscus.repo }}"
    data-repo-id="{{ site.giscus.repo_id }}"
    data-category="{{ site.giscus.category }}"
    data-category-id="{{ site.giscus.category_id }}"
    data-mapping="{{ site.giscus.mapping }}"
    data-reactions-enabled="{{ site.giscus.reactions_enabled }}"
    data-emit-metadata="{{ site.giscus.emit_metadata }}"
    data-theme="{{ site.giscus.theme }}"
    data-lang="id"
    crossorigin="anonymous"
    async>
  </script>
</section>
{% endif %}

1️⃣8️⃣ _includes/search-modal.html

<div id="search-modal" class="search-modal" role="dialog" aria-modal="true" aria-labelledby="search-title" hidden>
  <div class="search-dialog md3-surface-2">
    <div class="search-input-wrap">
      <span class="material-symbols-rounded">search</span>
      <input id="search-input" type="search" placeholder="Cari artikel, kategori, atau tag..." autocomplete="off" aria-label="Pencarian">
      <kbd>ESC</kbd>
    </div>
    <div id="search-results" class="search-results" role="listbox"></div>
    <div class="search-footer">
      <span><kbd>↑↓</kbd> Navigasi</span>
      <span><kbd>↵</kbd> Buka</span>
      <span><kbd>ESC</kbd> Tutup</span>
    </div>
  </div>
</div>

1️⃣9️⃣ _includes/skeleton.html

<div class="skeleton-grid">
  {% for i in (1..6) %}
  <div class="skeleton-card md3-surface-1" aria-hidden="true">
    <div class="skeleton-img skeleton-shimmer"></div>
    <div class="skeleton-body">
      <div class="skeleton-line short skeleton-shimmer"></div>
      <div class="skeleton-line skeleton-shimmer"></div>
      <div class="skeleton-line skeleton-shimmer"></div>
      <div class="skeleton-line medium skeleton-shimmer"></div>
    </div>
  </div>
  {% endfor %}
</div>

2️⃣0️⃣ _includes/schema.html

{% if page.layout == 'post' %}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": {{ page.title | jsonify }},
  "description": {{ page.description | default: page.excerpt | strip_html | truncate: 160 | jsonify }},
  "image": {{ page.image | default: site.logo | absolute_url | jsonify }},
  "datePublished": {{ page.date | date_to_xmlschema | jsonify }},
  "dateModified": {{ page.last_modified_at | default: page.date | date_to_xmlschema | jsonify }},
  "author": {
    "@type": "Person",
    "name": {{ page.author | default: site.author | jsonify }}
  },
  "publisher": {
    "@type": "Organization",
    "name": {{ site.title | jsonify }},
    "logo": {
      "@type": "ImageObject",
      "url": {{ site.logo | absolute_url | jsonify }}
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": {{ page.url | absolute_url | jsonify }}
  }
}
</script>
{% endif %}

2️⃣1️⃣ index.html (Beranda dengan Infinite Scroll)

---
layout: default
title: Beranda
description: Portal resmi Karang Taruna Desa Sejahtera — berita, kegiatan, program kerja, dan informasi pemuda.
---

<section class="hero md3-surface-1">
  <div class="hero-inner">
    <span class="hero-badge md3-chip">
      <span class="material-symbols-rounded">eco</span> Pemuda Berkarya, Desa Berjaya
    </span>
    <h1 class="hero-title">Selamat Datang di <span class="text-gradient">Karang Taruna</span></h1>
    <p class="hero-subtitle">Organisasi sosial wadah pengembangan generasi muda yang tumbuh atas dasar kesadaran dan tanggung jawab bersama di Desa Sejahtera.</p>
    <div class="hero-actions">
      <a href="{{ '/about/' | relative_url }}" class="md3-button filled">
        <span class="material-symbols-rounded">info</span> Tentang Kami
      </a>
      <a href="{{ '/program-kerja/' | relative_url }}" class="md3-button tonal">
        <span class="material-symbols-rounded">task_alt</span> Program Kerja
      </a>
    </div>
  </div>
</section>

<section class="section" aria-labelledby="latest-title">
  <div class="section-header">
    <h2 id="latest-title" class="section-title">
      <span class="material-symbols-rounded">newspaper</span> Berita Terbaru
    </h2>
    <a href="{{ '/berita/' | relative_url }}" class="md3-button text">Lihat Semua <span class="material-symbols-rounded">arrow_forward</span></a>
  </div>

  <div id="articles-grid" class="articles-grid">
    {% include skeleton.html %}
  </div>

  <div id="infinite-sentinel" class="infinite-sentinel" aria-hidden="true"></div>
  <div id="load-status" class="load-status" hidden>
    <span class="material-symbols-rounded spin">progress_activity</span> Memuat artikel...
  </div>
</section>

<section class="section features">
  <h2 class="section-title center">
    <span class="material-symbols-rounded">hub</span> Jelajahi Portal Kami
  </h2>
  <div class="features-grid">
    {% for item in site.data.navigation.main offset:1 limit:8 %}
      <a href="{{ item.url | relative_url }}" class="feature-card md3-surface-1">
        <span class="material-symbols-rounded feature-icon">{{ item.icon }}</span>
        <h3>{{ item.title }}</h3>
        <p>Menuju halaman {{ item.title | downcase }}.</p>
      </a>
    {% endfor %}
  </div>
</section>

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": {{ site.title | jsonify }},
  "url": {{ site.url | jsonify }},
  "potentialAction": {
    "@type": "SearchAction",
    "target": "{{ site.url }}/search/?q={query}",
    "query-input": "required name=query"
  }
}
</script>

2️⃣2️⃣ 404.html

---
layout: default
title: Halaman Tidak Ditemukan
permalink: /404.html
---
<section class="error-page md3-surface-1">
  <div class="error-content">
    <span class="error-icon material-symbols-rounded">forest</span>
    <h1>404</h1>
    <h2>Oops! Halaman yang Anda cari tidak ada</h2>
    <p>Mungkin halaman telah dipindahkan atau URL yang Anda ketik salah. Mari kembali ke jalur yang benar.</p>
    <div class="error-actions">
      <a href="{{ '/' | relative_url }}" class="md3-button filled">
        <span class="material-symbols-rounded">home</span> Kembali ke Beranda
      </a>
      <button class="md3-button tonal" id="open-search-404">
        <span class="material-symbols-rounded">search</span> Cari Artikel
      </button>
    </div>
  </div>
</section>
<script>
  document.getElementById('open-search-404')?.addEventListener('click', () => {
    document.getElementById('search-toggle')?.click();
  });
</script>

2️⃣3️⃣ _pages/about.md

---
layout: page
title: Profil Organisasi
permalink: /about/
description: Mengenal lebih dekat Karang Taruna Desa Sejahtera — sejarah, visi, misi, dan nilai-nilai organisasi.
image: /assets/img/about-hero.jpg
---

## 🌱 Sejarah Singkat

Karang Taruna Desa Sejahtera didirikan pada tanggal **17 Agustus 1995** oleh sekelompok pemuda yang peduli terhadap perkembangan desa. Berawal dari kegiatan rutin arisan pemuda, organisasi ini resmi berdiri sebagai wadah pengembangan generasi muda desa sesuai amanat **Permensos RI No. 25 Tahun 2019** tentang Karang Taruna.

Selama lebih dari tiga dekade, Karang Taruna Desa Sejahtera telah menjadi motor penggerak berbagai program kepemudaan, sosial, dan pemberdayaan masyarakat.

## 👁️ Visi

> *"Terwujudnya generasi muda yang beriman, bertakwa, berakhlak mulia, sehat, cerdas, kreatif, mandiri, dan peduli sosial serta berjiwa nasionalis."*

## 🎯 Misi

1. Menumbuhkan kesadaran dan tanggung jawab sosial generasi muda.
2. Meningkatkan kualitas sumber daya pemuda melalui pelatihan dan pendidikan.
3. Mengembangkan potensi pemuda di bidang sosial, ekonomi, seni, dan olahraga.
4. Mempererat tali silaturahmi antarwarga dan pemuda desa.
5. Melestarikan budaya lokal dan menjaga kelestarian lingkungan.

## 💎 Nilai-Nilai Organisasi

- **Gotong Royong** — bekerja bersama untuk kebaikan bersama.
- **Integritas** — jujur, transparan, dan akuntabel.
- **Inovasi** — kreatif dan adaptif terhadap perubahan.
- **Inklusivitas** — terbuka untuk seluruh pemuda tanpa terkecuali.
- **Keberlanjutan** — berorientasi pada masa depan yang lebih baik.

## 🏆 Prestasi

- Juara 1 Karang Taruna Berprestasi Tingkat Kabupaten (2023)
- Juara 2 Lomba Karya Pemuda Tingkat Provinsi (2024)
- Penghargaan Desa Peduli Pemuda dari Kemensos (2025)

2️⃣4️⃣ _pages/struktur.md

---
layout: page
title: Struktur Pengurus
permalink: /struktur/
description: Susunan pengurus Karang Taruna Desa Sejahtera periode 2025–2028.
---

<div class="pengurus-grid">
{% for p in site.data.pengurus %}
  <div class="pengurus-card md3-surface-1">
    <div class="pengurus-photo">
      <img src="{{ p.foto | relative_url }}" alt="{{ p.nama }}" loading="lazy" decoding="async"
           onerror="this.onerror=null;this.src='https://api.dicebear.com/7.x/initials/svg?seed={{ p.nama | url_encode }}&backgroundColor=2E7D32';">
    </div>
    <div class="pengurus-info">
      <h3>{{ p.nama }}</h3>
      <p class="jabatan">{{ p.jabatan }}</p>
      <span class="md3-chip small">{{ p.bidang }}</span>
    </div>
  </div>
{% endfor %}
</div>

2️⃣5️⃣ _pages/program-kerja.md

---
layout: page
title: Program Kerja
permalink: /program-kerja/
description: Program kerja Karang Taruna berdasarkan bidang kepengurusan.
---

<div class="program-container">
{% for bidang in site.data.program_kerja.bidang %}
  <section class="program-bidang md3-surface-1">
    <header class="program-header">
      <span class="material-symbols-rounded program-icon">{{ bidang.icon }}</span>
      <h2>{{ bidang.nama }}</h2>
    </header>
    <ul class="program-list">
    {% for prog in bidang.program %}
      <li class="program-item">
        <span class="material-symbols-rounded">check_circle</span>
        <div>
          <h4>{{ prog.nama }}</h4>
          <p>{{ prog.deskripsi }}</p>
        </div>
      </li>
    {% endfor %}
    </ul>
  </section>
{% endfor %}
</div>

2️⃣6️⃣ _pages/kegiatan.md

---
layout: page
title: Kegiatan
permalink: /kegiatan/
description: Daftar kegiatan dan agenda Karang Taruna Desa Sejahtera.
---

## 📅 Agenda Mendatang

<div class="agenda-list">
  <article class="agenda-card md3-surface-1">
    <div class="agenda-date">
      <span class="day">15</span>
      <span class="month">Jul</span>
    </div>
    <div class="agenda-content">
      <h3>Kerja Bakti Lingkungan</h3>
      <p><span class="material-symbols-rounded">schedule</span> 07:00 – 11:00 WIB</p>
      <p><span class="material-symbols-rounded">location_on</span> Balai Desa</p>
    </div>
  </article>

  <article class="agenda-card md3-surface-1">
    <div class="agenda-date">
      <span class="day">22</span>
      <span class="month">Jul</span>
    </div>
    <div class="agenda-content">
      <h3>Pelatihan Digital Marketing</h3>
      <p><span class="material-symbols-rounded">schedule</span> 09:00 – 15:00 WIB</p>
      <p><span class="material-symbols-rounded">location_on</span> Aula Karang Taruna</p>
    </div>
  </article>
</div>

## 📰 Kegiatan Terbaru

<div class="articles-grid">
{% assign kegiatan = site.categories.kegiatan | slice: 0, 6 %}
{% for post in kegiatan %}
  {% include article-card.html post=post %}
{% endfor %}
</div>

2️⃣7️⃣ _pages/galeri.md

---
layout: page
title: Galeri
permalink: /galeri/
description: Dokumentasi kegiatan Karang Taruna Desa Sejahtera.
---

<div class="gallery-grid">
  {% for i in (1..12) %}
    <figure class="gallery-item md3-surface-1">
      <img src="https://picsum.photos/seed/kt{{ i }}/600/400" alt="Dokumentasi kegiatan {{ i }}" loading="lazy" decoding="async">
      <figcaption>Kegiatan #{{ i }}</figcaption>
    </figure>
  {% endfor %}
</div>

2️⃣8️⃣ _pages/dokumen.md

---
layout: page
title: Dokumen
permalink: /dokumen/
description: Unduh dokumen resmi, AD/ART, laporan, dan formulir Karang Taruna.
---

<div class="docs-grid">
  <a href="/assets/docs/ad-art.pdf" class="doc-card md3-surface-1" download>
    <span class="material-symbols-rounded doc-icon pdf">picture_as_pdf</span>
    <div>
      <h3>AD/ART Karang Taruna</h3>
      <p>Anggaran Dasar & Anggaran Rumah Tangga</p>
      <small>PDF · 2.4 MB</small>
    </div>
  </a>

  <a href="/assets/docs/laporan-2025.pdf" class="doc-card md3-surface-1" download>
    <span class="material-symbols-rounded doc-icon pdf">picture_as_pdf</span>
    <div>
      <h3>Laporan Tahunan 2025</h3>
      <p>Laporan pertanggungjawaban pengurus</p>
      <small>PDF · 5.1 MB</small>
    </div>
  </a>

  <a href="/assets/docs/formulir-anggota.docx" class="doc-card md3-surface-1" download>
    <span class="material-symbols-rounded doc-icon doc">description</span>
    <div>
      <h3>Formulir Pendaftaran Anggota</h3>
      <p>Formulir keanggotaan baru</p>
      <small>DOCX · 120 KB</small>
    </div>
  </a>

  <a href="/assets/docs/proker-2026.xlsx" class="doc-card md3-surface-1" download>
    <span class="material-symbols-rounded doc-icon xls">table_view</span>
    <div>
      <h3>Program Kerja 2026</h3>
      <p>Rencana kerja tahunan</p>
      <small>XLSX · 340 KB</small>
    </div>
  </a>
</div>

2️⃣9️⃣ _pages/kontak.md

---
layout: page
title: Kontak
permalink: /kontak/
description: Hubungi kami untuk informasi, kerja sama, atau pertanyaan.
---

<div class="contact-grid">
  <div class="contact-info md3-surface-1">
    <h2>Mari Terhubung</h2>
    <p>Kami terbuka untuk diskusi, kerja sama, maupun pertanyaan seputar kegiatan Karang Taruna.</p>

    <ul class="contact-list">
      <li><span class="material-symbols-rounded">location_on</span><div><strong>Alamat</strong><br>Jl. Pemuda No. 17, Desa Sejahtera, Kec. Makmur, Kab. Sejahtera 12345</div></li>
      <li><span class="material-symbols-rounded">mail</span><div><strong>Email</strong><br><a href="mailto:{{ site.email }}">{{ site.email }}</a></div></li>
      <li><span class="material-symbols-rounded">call</span><div><strong>Telepon</strong><br>+62 812-3456-7890</div></li>
      <li><span class="material-symbols-rounded">schedule</span><div><strong>Jam Operasional</strong><br>Senin – Sabtu, 09:00 – 17:00 WIB</div></li>
    </ul>
  </div>

  <form class="contact-form md3-surface-1" action="https://formspree.io/f/your_form_id" method="POST">
    <h2>Kirim Pesan</h2>
    <div class="form-field">
      <label for="nama">Nama Lengkap</label>
      <input type="text" id="nama" name="nama" required>
    </div>
    <div class="form-field">
      <label for="email">Email</label>
      <input type="email" id="email" name="email" required>
    </div>
    <div class="form-field">
      <label for="subjek">Subjek</label>
      <input type="text" id="subjek" name="subjek" required>
    </div>
    <div class="form-field">
      <label for="pesan">Pesan</label>
      <textarea id="pesan" name="pesan" rows="5" required></textarea>
    </div>
    <button type="submit" class="md3-button filled">
      <span class="material-symbols-rounded">send</span> Kirim Pesan
    </button>
  </form>
</div>

<div class="map-container md3-surface-1">
  <iframe
    src="https://www.openstreetmap.org/export/embed.html?bbox=106.8,-6.2,106.9,-6.1&layer=mapnik"
    loading="lazy"
    title="Peta Lokasi Karang Taruna"
    referrerpolicy="no-referrer-when-downgrade">
  </iframe>
</div>

3️⃣0️⃣ _pages/kategori.md

---
layout: archive
title: Kategori
permalink: /kategori/
layout_type: kategori
---

<div class="kategori-grid">
{% assign cats = site.categories | sort %}
{% for cat in cats %}
  {% assign name = cat | first %}
  {% assign posts = cat | last %}
  <a href="{{ '/kategori/' | append: name | slugify | append: '/' | relative_url }}" class="kategori-card md3-surface-1">
    <span class="material-symbols-rounded">folder</span>
    <h3>{{ name }}</h3>
    <span class="count">{{ posts.size }} artikel</span>
  </a>
{% endfor %}
</div>

3️⃣1️⃣ _pages/tags.md

---
layout: archive
title: Tag
permalink: /tags/
layout_type: tags
---

<div class="tags-cloud">
{% assign tags = site.tags | sort %}
{% for tag in tags %}
  {% assign name = tag | first %}
  {% assign posts = tag | last %}
  <a href="{{ '/tags/' | append: name | slugify | append: '/' | relative_url }}" class="md3-chip">
    #{{ name }} <span class="count">({{ posts.size }})</span>
  </a>
{% endfor %}
</div>

3️⃣2️⃣ _pages/arsip.md

---
layout: archive
title: Arsip
permalink: /arsip/
layout_type: arsip
---

{% assign postsByYear = site.posts | group_by_exp: "post", "post.date | date: '%Y'" %}
{% for year in postsByYear %}
  <section class="archive-year">
    <h2 class="year-title"><span class="material-symbols-rounded">calendar_month</span> {{ year.name }}</h2>
    <ul class="archive-list">
    {% for post in year.items %}
      <li class="archive-item">
        <time datetime="{{ post.date | date_to_xmlschema }}">{{ post.date | date: "%d %b" }}</time>
        <a href="{{ post.url | relative_url }}">{{ post.title }}</a>
        {% if post.categories.size > 0 %}
          <span class="md3-chip small">{{ post.categories.first }}</span>
        {% endif %}
      </li>
    {% endfor %}
    </ul>
  </section>
{% endfor %}

3️⃣3️⃣ _pages/search.md

---
layout: page
title: Pencarian
permalink: /search/
sitemap: false
---

<div class="search-page">
  <div class="search-input-wrap big">
    <span class="material-symbols-rounded">search</span>
    <input id="page-search-input" type="search" placeholder="Ketik kata kunci..." autocomplete="off">
  </div>
  <div id="page-search-results" class="search-results"></div>
</div>

3️⃣4️⃣ _pages/pengumuman.md & _pages/agenda.md

---
layout: page
title: Pengumuman
permalink: /pengumuman/
---
<div class="articles-grid">
{% for post in site.categories.pengumuman limit: 10 %}
  {% include article-card.html post=post %}
{% endfor %}
</div>
---
layout: page
title: Agenda
permalink: /agenda/
---
<div class="articles-grid">
{% for post in site.categories.agenda limit: 10 %}
  {% include article-card.html post=post %}
{% endfor %}
</div>

3️⃣5️⃣ berita/index.html (Halaman Berita dengan Pagination)

---
layout: default
title: Berita
permalink: /berita/
pagination:
  enabled: true
---

<section class="section">
  <header class="page-header">
    <h1 class="page-title">Berita & Artikel</h1>
    <p class="page-subtitle">Informasi terbaru seputar kegiatan Karang Taruna.</p>
  </header>

  <div class="filter-bar">
    <div class="md3-tabs">
      <a href="{{ '/kategori/' | relative_url }}" class="md3-tab">Kategori</a>
      <a href="{{ '/tags/' | relative_url }}" class="md3-tab">Tag</a>
      <a href="{{ '/arsip/' | relative_url }}" class="md3-tab">Arsip</a>
    </div>
  </div>

  <div class="articles-grid">
    {% for post in paginator.posts %}
      {% include article-card.html post=post %}
    {% endfor %}
  </div>

  <nav class="pagination" aria-label="Paginasi">
    {% if paginator.previous_page %}
      <a href="{{ paginator.previous_page_path | relative_url }}" class="md3-button outlined">← Sebelumnya</a>
    {% endif %}
    <span class="page-info">Halaman {{ paginator.page }} dari {{ paginator.total_pages }}</span>
    {% if paginator.next_page %}
      <a href="{{ paginator.next_page_path | relative_url }}" class="md3-button outlined">Selanjutnya →</a>
    {% endif %}
  </nav>
</section>

⚠️ Catatan: Karena jekyll-paginate tidak mendukung custom collection, gunakan plugin jekyll-paginate-v2 jika ingin pagination di /berita/. Untuk kompatibilitas GitHub Pages, alternatifnya gunakan infinite scroll di beranda dan tampilkan semua di /berita/ atau buat pagination manual.

Versi sederhana tanpa plugin tambahan — ganti bagian pagination di atas dengan:

<div class="articles-grid">
{% for post in site.posts %}
  {% include article-card.html post=post %}
{% endfor %}
</div>

3️⃣6️⃣ _posts/2026-06-30-selamat-datang.md (Contoh Artikel)

---
layout: post
title: "Selamat Datang di Portal Resmi Karang Taruna"
date: 2026-06-30 08:00:00 +0700
categories: [Pengumuman, Berita]
tags: [karang-taruna, pemuda, desa-sejahtera]
image: /assets/img/posts/welcome.jpg
description: "Portal resmi Karang Taruna Desa Sejahtera kini hadir untuk melayani informasi kegiatan, program kerja, dan berita pemuda."
---

Selamat datang di portal resmi **Karang Taruna Desa Sejahtera**! 🎉

Website ini dibangun sebagai wadah informasi resmi organisasi, mencakup berita, kegiatan, program kerja, hingga dokumen-dokumen penting.

## ✨ Fitur Website

- 📰 Berita dan artikel terbaru
- 📅 Agenda kegiatan
- 📊 Struktur pengurus
- 📁 Download dokumen
- 💬 Diskusi via Giscus

## 📝 Contoh Sintaks Markdown

### Teks & Format

Ini **bold**, ini *italic*, ini `inline code`, dan ini ~~strikethrough~~.

> "Pemuda adalah harapan bangsa." — Kutipan inspiratif

### Daftar

- [x] Mendirikan website
- [x] Menyusun program kerja
- [ ] Melaksanakan kegiatan perdana

### Tabel

| Bidang         | Koordinator | Status  |
|----------------|-------------|---------|
| Kepemudaan     | Dewi        | Aktif   |
| Sosial         | Rudi        | Aktif   |
| Seni & Budaya  | Maya        | Aktif   |

### Code Block

```javascript
// Contoh kode dengan syntax highlighting
function sapa(nama) {
  return `Halo, ${nama}! Selamat datang di Karang Taruna.`;
}
console.log(sapa("Pemuda"));

Gambar

Suasana kegiatan

Video Embed

Checklist

  • Website live
  • Tambah galeri foto
  • Integrasi newsletter

Terima kasih telah mengunjungi portal kami. Mari berkarya bersama! 💚


---

## 3️⃣7️⃣ `assets/css/style.scss`

```scss
---
---
@import "variables";
@import "base";
@import "md3";
@import "layout";
@import "components";
@import "markdown";

3️⃣8️⃣ _sass/_variables.scss

// MD3 Color Tokens — Green/Teal Nature Theme
:root,
[data-theme="light"] {
  --md-primary: #2E7D32;
  --md-on-primary: #FFFFFF;
  --md-primary-container: #C8E6C9;
  --md-on-primary-container: #1B5E20;

  --md-secondary: #00897B;
  --md-on-secondary: #FFFFFF;
  --md-secondary-container: #B2DFDB;
  --md-on-secondary-container: #004D40;

  --md-tertiary: #689F38;
  --md-tertiary-container: #DCEDC8;

  --md-surface: #F8FBF5;
  --md-surface-1: #FFFFFF;
  --md-surface-2: #F1F7EC;
  --md-surface-3: #E8F0E2;
  --md-surface-variant: #DCE5D6;

  --md-on-surface: #1A1C19;
  --md-on-surface-variant: #44483E;
  --md-outline: #74796D;
  --md-outline-variant: #C4C8BA;

  --md-error: #BA1A1A;
  --md-shadow: rgba(0, 0, 0, 0.08);
  --md-scrim: rgba(0, 0, 0, 0.4);

  --md-elevation-1: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
  --md-elevation-2: 0 3px 6px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04);
  --md-elevation-3: 0 8px 20px rgba(0,0,0,.10), 0 4px 8px rgba(0,0,0,.05);

  --md-radius-sm: 8px;
  --md-radius-md: 12px;
  --md-radius-lg: 16px;
  --md-radius-xl: 28px;

  --font-sans: 'Plus Jakarta Sans', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
  --container: 1200px;
  --gutter: 1.5rem;
}

[data-theme="dark"] {
  --md-primary: #81C784;
  --md-on-primary: #0A3810;
  --md-primary-container: #1B5E20;
  --md-on-primary-container: #C8E6C9;

  --md-secondary: #4DB6AC;
  --md-on-secondary: #00332C;
  --md-secondary-container: #004D40;
  --md-on-secondary-container: #B2DFDB;

  --md-surface: #0F1B15;
  --md-surface-1: #15201A;
  --md-surface-2: #1A2820;
  --md-surface-3: #203028;
  --md-surface-variant: #2A3A30;

  --md-on-surface: #E3E3DC;
  --md-on-surface-variant: #C4C8BA;
  --md-outline: #8E9386;
  --md-outline-variant: #44483E;

  --md-shadow: rgba(0, 0, 0, 0.4);
  --md-elevation-1: 0 1px 3px rgba(0,0,0,.3), 0 1px 2px rgba(0,0,0,.2);
  --md-elevation-2: 0 3px 6px rgba(0,0,0,.35), 0 2px 4px rgba(0,0,0,.25);
  --md-elevation-3: 0 8px 20px rgba(0,0,0,.5), 0 4px 8px rgba(0,0,0,.3);
}

3️⃣9️⃣ _sass/_base.scss

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.6;
  color: var(--md-on-surface);
  background: var(--md-surface);
  transition: background .3s, color .3s;
  -webkit-font-smoothing: antialiased;
}

img, video, iframe { max-width: 100%; height: auto; display: block; }
a { color: var(--md-primary); text-decoration: none; transition: color .2s; }
a:hover { color: var(--md-on-primary-container); }

.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--md-primary); color: var(--md-on-primary);
  padding: .75rem 1rem; z-index: 9999; border-radius: 0 0 var(--md-radius-sm) 0;
  &:focus { left: 0; }
}

::selection { background: var(--md-primary); color: var(--md-on-primary); }

:focus-visible {
  outline: 3px solid var(--md-primary);
  outline-offset: 2px;
  border-radius: var(--md-radius-sm);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

4️⃣0️⃣ _sass/_md3.scss

// MD3 Buttons
.md3-button {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .75rem 1.5rem; border-radius: var(--md-radius-xl);
  font-weight: 600; font-size: .95rem; border: none; cursor: pointer;
  transition: all .2s; font-family: inherit; text-decoration: none;
  .material-symbols-rounded { font-size: 1.1rem; }

  &.filled {
    background: var(--md-primary); color: var(--md-on-primary);
    box-shadow: var(--md-elevation-1);
    &:hover { box-shadow: var(--md-elevation-2); filter: brightness(1.05); }
  }
  &.tonal {
    background: var(--md-secondary-container); color: var(--md-on-secondary-container);
    &:hover { filter: brightness(.95); }
  }
  &.outlined {
    background: transparent; color: var(--md-primary);
    border: 1px solid var(--md-outline);
    &:hover { background: var(--md-surface-2); }
  }
  &.text {
    background: transparent; color: var(--md-primary); padding: .5rem 1rem;
    &:hover { background: var(--md-surface-2); }
  }
}

// MD3 Chip
.md3-chip {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .35rem .85rem; border-radius: var(--md-radius-xl);
  background: var(--md-surface-2); color: var(--md-on-surface);
  font-size: .8rem; font-weight: 500; border: 1px solid var(--md-outline-variant);
  transition: all .2s;
  &.small { padding: .2rem .6rem; font-size: .75rem; }
  &:hover { background: var(--md-primary-container); color: var(--md-on-primary-container); border-color: transparent; }
}

// MD3 Surface
.md3-surface-1 { background: var(--md-surface-1); box-shadow: var(--md-elevation-1); border-radius: var(--md-radius-lg); }
.md3-surface-2 { background: var(--md-surface-2); border-radius: var(--md-radius-lg); }

// Icon Button
.icon-btn {
  width: 40px; height: 40px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: none; cursor: pointer;
  color: var(--md-on-surface-variant); transition: background .2s;
  &:hover { background: var(--md-surface-2); }
}

// Tabs
.md3-tabs { display: flex; gap: .5rem; flex-wrap: wrap; }
.md3-tab {
  padding: .6rem 1.2rem; border-radius: var(--md-radius-xl);
  color: var(--md-on-surface-variant); font-weight: 500;
  border: 1px solid transparent; transition: all .2s;
  &.active, &:hover {
    background: var(--md-primary-container);
    color: var(--md-on-primary-container);
  }
}

// Text gradient
.text-gradient {
  background: linear-gradient(135deg, var(--md-primary), var(--md-secondary));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}

.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

4️⃣1️⃣ _sass/_layout.scss

// App Bar
.md3-app-bar {
  position: sticky; top: 0; z-index: 100;
  background: var(--md-surface-1);
  border-bottom: 1px solid var(--md-outline-variant);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}
.app-bar-inner {
  max-width: var(--container); margin: 0 auto;
  display: flex; align-items: center; gap: 1rem;
  padding: .75rem var(--gutter);
}
.brand {
  display: flex; align-items: center; gap: .75rem;
  color: var(--md-on-surface); font-weight: 700;
  img { border-radius: 50%; }
  .brand-text { font-size: 1.05rem; }
}
.primary-nav { flex: 1; display: flex; justify-content: center; }
.nav-list { display: flex; gap: .25rem; list-style: none; margin: 0; padding: 0; }
.nav-link {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .5rem .9rem; border-radius: var(--md-radius-xl);
  color: var(--md-on-surface-variant); font-weight: 500; font-size: .9rem;
  transition: all .2s;
  .material-symbols-rounded { font-size: 1.1rem; }
  &:hover, &.active {
    background: var(--md-primary-container);
    color: var(--md-on-primary-container);
  }
}
.app-bar-actions { display: flex; gap: .25rem; }
.menu-toggle { display: none; }

@media (max-width: 960px) {
  .menu-toggle { display: inline-flex; }
  .primary-nav {
    position: fixed; top: 64px; left: 0; right: 0; bottom: 0;
    background: var(--md-surface-1); padding: 1rem;
    transform: translateX(-100%); transition: transform .3s;
    overflow-y: auto; justify-content: flex-start;
    &.open { transform: translateX(0); }
  }
  .nav-list { flex-direction: column; width: 100%; }
  .nav-link { padding: .9rem 1rem; border-radius: var(--md-radius-md); }
  .brand-text { display: none; }
}

// Main
.md3-main {
  max-width: var(--container); margin: 0 auto;
  padding: 1.5rem var(--gutter) 4rem;
  min-height: calc(100vh - 200px);
}

// Breadcrumb
.breadcrumb {
  margin-bottom: 1.5rem; font-size: .85rem;
  ol { display: flex; flex-wrap: wrap; gap: .5rem; list-style: none; margin: 0; padding: 0; }
  li { display: flex; align-items: center; gap: .5rem; color: var(--md-on-surface-variant);
    &::after { content: '/'; color: var(--md-outline); }
    &:last-child::after { content: ''; }
    &[aria-current] { color: var(--md-primary); font-weight: 600; }
  }
}

// Hero
.hero {
  padding: 4rem var(--gutter); margin-bottom: 3rem;
  background: linear-gradient(135deg, var(--md-primary-container), var(--md-secondary-container));
  border-radius: var(--md-radius-xl);
  position: relative; overflow: hidden;
  &::before {
    content: ''; position: absolute; inset: 0;
    background-image: radial-gradient(circle at 20% 30%, rgba(255,255,255,.3), transparent 50%),
                      radial-gradient(circle at 80% 70%, rgba(46,125,50,.15), transparent 50%);
  }
}
.hero-inner { position: relative; max-width: 720px; }
.hero-badge { margin-bottom: 1rem; }
.hero-title { font-size: clamp(1.8rem, 4vw, 3rem); font-weight: 800; line-height: 1.15; margin: 0 0 1rem; }
.hero-subtitle { font-size: 1.1rem; color: var(--md-on-surface-variant); margin: 0 0 2rem; }
.hero-actions { display: flex; flex-wrap: wrap; gap: .75rem; }

// Section
.section { margin-bottom: 4rem; }
.section-header {
  display: flex; justify-content: space-between; align-items: center;
  margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;
}
.section-title {
  display: flex; align-items: center; gap: .5rem;
  font-size: 1.5rem; font-weight: 700; margin: 0;
  &.center { justify-content: center; margin-bottom: 2rem; }
  .material-symbols-rounded { color: var(--md-primary); }
}

// Articles Grid
.articles-grid {
  display: grid; gap: 1.5rem;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}

// Features
.features-grid {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.feature-card {
  padding: 1.5rem; text-align: center; color: var(--md-on-surface);
  transition: all .25s;
  &:hover { transform: translateY(-4px); box-shadow: var(--md-elevation-2); }
  .feature-icon {
    font-size: 2.5rem; color: var(--md-primary);
    background: var(--md-primary-container);
    width: 64px; height: 64px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    margin-bottom: 1rem;
  }
  h3 { margin: 0 0 .5rem; font-size: 1.05rem; }
  p { margin: 0; font-size: .85rem; color: var(--md-on-surface-variant); }
}

// Footer
.md3-footer {
  background: var(--md-surface-1); border-top: 1px solid var(--md-outline-variant);
  padding: 3rem var(--gutter) 1.5rem; margin-top: 4rem;
}
.footer-grid {
  max-width: var(--container); margin: 0 auto;
  display: grid; gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.footer-col h3, .footer-col h4 { margin-top: 0; }
.footer-title { color: var(--md-primary); }
.footer-about { color: var(--md-on-surface-variant); font-size: .9rem; }
.social-links { display: flex; gap: .5rem; margin-top: 1rem; }
.social-icon {
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--md-surface-2); color: var(--md-on-surface);
  display: inline-flex; align-items: center; justify-content: center;
  transition: all .2s;
  &:hover { background: var(--md-primary); color: var(--md-on-primary); transform: translateY(-2px); }
}
.footer-links, .footer-contact { list-style: none; padding: 0; margin: 0; }
.footer-links li, .footer-contact li { margin-bottom: .5rem; font-size: .9rem; }
.footer-contact li { display: flex; gap: .5rem; align-items: flex-start; }
.footer-bottom {
  max-width: var(--container); margin: 2rem auto 0; padding-top: 1.5rem;
  border-top: 1px solid var(--md-outline-variant);
  text-align: center; font-size: .85rem; color: var(--md-on-surface-variant);
  p { margin: .25rem 0; }
}

// FAB
.fab {
  position: fixed; bottom: 1.5rem; right: 1.5rem;
  width: 56px; height: 56px; border-radius: 50%;
  background: var(--md-primary); color: var(--md-on-primary);
  border: none; cursor: pointer; box-shadow: var(--md-elevation-3);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transform: translateY(20px) scale(.8); pointer-events: none;
  transition: all .3s; z-index: 90;
  &.visible { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
  &:hover { filter: brightness(1.1); }
}

// Reading Progress
.reading-progress {
  position: fixed; top: 0; left: 0; height: 3px;
  background: linear-gradient(90deg, var(--md-primary), var(--md-secondary));
  width: 0; z-index: 1000; transition: width .1s;
}

4️⃣2️⃣ _sass/_components.scss

// Article Card
.article-card {
  overflow: hidden; display: flex; flex-direction: column;
  transition: all .25s;
  &:hover { transform: translateY(-4px); box-shadow: var(--md-elevation-2); }
  .card-media {
    aspect-ratio: 16/10; overflow: hidden; background: var(--md-surface-2);
    img { width: 100%; height: 100%; object-fit: cover; transition: transform .4s; }
  }
  &:hover .card-media img { transform: scale(1.05); }
  .card-placeholder {
    width: 100%; height: 100%;
    display: flex; align-items: center; justify-content: center;
    color: var(--md-outline);
    .material-symbols-rounded { font-size: 3rem; }
  }
  .card-body { padding: 1.25rem; display: flex; flex-direction: column; gap: .75rem; flex: 1; }
  .card-chips { display: flex; gap: .35rem; flex-wrap: wrap; }
  .card-title {
    margin: 0; font-size: 1.1rem; font-weight: 700; line-height: 1.35;
    a { color: var(--md-on-surface);
      &:hover { color: var(--md-primary); }
    }
  }
  .card-excerpt {
    margin: 0; color: var(--md-on-surface-variant); font-size: .9rem;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
  }
  .card-meta {
    margin-top: auto; display: flex; gap: .5rem; align-items: center;
    font-size: .8rem; color: var(--md-on-surface-variant);
  }
}

// Post
.post-header { margin-bottom: 2rem; }
.post-hero {
  width: 100%; aspect-ratio: 21/9; object-fit: cover;
  border-radius: var(--md-radius-lg); margin-bottom: 1.5rem;
}
.post-meta-container { padding: 0 1rem; }
.post-title { font-size: clamp(1.5rem, 3.5vw, 2.25rem); line-height: 1.2; margin: .5rem 0; }
.post-meta {
  display: flex; flex-wrap: wrap; gap: .75rem; align-items: center;
  color: var(--md-on-surface-variant); font-size: .9rem;
  .material-symbols-rounded { font-size: 1rem; }
}
.post-content { padding: 1rem; }
.post-tags {
  padding: 1.5rem; margin-top: 2rem;
  display: flex; flex-wrap: wrap; gap: .5rem; align-items: center;
  border-top: 1px solid var(--md-outline-variant);
}

// Share
.share-bar {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  padding: 1rem; margin: 2rem 0;
  background: var(--md-surface-2); border-radius: var(--md-radius-lg);
}
.share-label { font-weight: 600; margin-right: .5rem; }
.share-btn { color: var(--md-on-surface); }

// Comments
.comments { margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--md-outline-variant); }
.comments-title { display: flex; align-items: center; gap: .5rem; }
.giscus-wrapper { margin-top: 1rem; padding: 1rem; }

// Page
.page-header { margin-bottom: 2rem; }
.page-title { font-size: clamp(1.75rem, 4vw, 2.5rem); margin: 0 0 .5rem; }
.page-subtitle { color: var(--md-on-surface-variant); margin: 0; font-size: 1.05rem; }
.page-content { padding: 1.5rem; }

// Pengurus
.pengurus-grid {
  display: grid; gap: 1.5rem;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.pengurus-card {
  text-align: center; padding: 1.5rem; transition: transform .25s;
  &:hover { transform: translateY(-4px); }
  .pengurus-photo {
    width: 140px; height: 140px; border-radius: 50%; margin: 0 auto 1rem;
    overflow: hidden; background: var(--md-primary-container);
    img { width: 100%; height: 100%; object-fit: cover; }
  }
  h3 { margin: 0 0 .25rem; font-size: 1.05rem; }
  .jabatan { color: var(--md-primary); font-weight: 600; margin: 0 0 .5rem; font-size: .9rem; }
}

// Program
.program-container { display: grid; gap: 1.5rem; }
.program-bidang { padding: 1.5rem; }
.program-header {
  display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;
  .program-icon {
    font-size: 2rem; color: var(--md-on-primary);
    background: var(--md-primary); padding: .5rem; border-radius: 50%;
  }
  h2 { margin: 0; }
}
.program-list { list-style: none; padding: 0; margin: 0; }
.program-item {
  display: flex; gap: 1rem; padding: .75rem 0;
  border-bottom: 1px solid var(--md-outline-variant);
  &:last-child { border-bottom: none; }
  .material-symbols-rounded { color: var(--md-primary); flex-shrink: 0; }
  h4 { margin: 0 0 .25rem; }
  p { margin: 0; font-size: .9rem; color: var(--md-on-surface-variant); }
}

// Gallery
.gallery-grid {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.gallery-item {
  overflow: hidden; border-radius: var(--md-radius-md);
  img { aspect-ratio: 1; object-fit: cover; transition: transform .4s; }
  &:hover img { transform: scale(1.08); }
  figcaption { padding: .75rem; font-size: .85rem; text-align: center; }
}

// Documents
.docs-grid {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.doc-card {
  display: flex; gap: 1rem; padding: 1.25rem;
  color: var(--md-on-surface); transition: all .25s;
  &:hover { transform: translateY(-2px); box-shadow: var(--md-elevation-2); }
  .doc-icon { font-size: 2.5rem; flex-shrink: 0;
    &.pdf { color: #D32F2F; }
    &.doc { color: #1976D2; }
    &.xls { color: #388E3C; }
  }
  h3 { margin: 0 0 .25rem; font-size: 1rem; }
  p { margin: 0 0 .25rem; font-size: .85rem; color: var(--md-on-surface-variant); }
  small { color: var(--md-outline); }
}

// Contact
.contact-grid {
  display: grid; gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  margin-bottom: 2rem;
}
.contact-info, .contact-form { padding: 2rem; }
.contact-list { list-style: none; padding: 0; }
.contact-list li {
  display: flex; gap: 1rem; margin-bottom: 1.25rem;
  .material-symbols-rounded {
    color: var(--md-primary); background: var(--md-primary-container);
    width: 44px; height: 44px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  }
}
.form-field { margin-bottom: 1rem;
  label { display: block; font-weight: 500; margin-bottom: .35rem; font-size: .9rem; }
  input, textarea {
    width: 100%; padding: .75rem 1rem; border-radius: var(--md-radius-md);
    border: 1px solid var(--md-outline-variant); background: var(--md-surface);
    color: var(--md-on-surface); font-family: inherit; font-size: .95rem;
    transition: border-color .2s;
    &:focus { outline: none; border-color: var(--md-primary); box-shadow: 0 0 0 3px var(--md-primary-container); }
  }
}
.map-container {
  padding: .5rem; border-radius: var(--md-radius-lg); overflow: hidden;
  iframe { width: 100%; height: 400px; border: 0; border-radius: var(--md-radius-md); }
}

// Agenda
.agenda-list { display: grid; gap: 1rem; margin-bottom: 2rem; }
.agenda-card {
  display: flex; gap: 1rem; padding: 1rem; align-items: center;
  .agenda-date {
    background: var(--md-primary); color: var(--md-on-primary);
    padding: .75rem 1rem; border-radius: var(--md-radius-md);
    text-align: center; min-width: 70px;
    .day { display: block; font-size: 1.5rem; font-weight: 800; line-height: 1; }
    .month { font-size: .8rem; text-transform: uppercase; }
  }
  h3 { margin: 0 0 .25rem; }
  p { margin: 0; font-size: .85rem; color: var(--md-on-surface-variant);
    display: flex; align-items: center; gap: .25rem;
    .material-symbols-rounded { font-size: 1rem; }
  }
}

// Kategori
.kategori-grid {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
.kategori-card {
  padding: 1.5rem; text-align: center; color: var(--md-on-surface);
  transition: all .25s;
  &:hover { transform: translateY(-3px); background: var(--md-primary-container); }
  .material-symbols-rounded { font-size: 2rem; color: var(--md-primary); margin-bottom: .5rem; }
  h3 { margin: .25rem 0; }
  .count { color: var(--md-on-surface-variant); font-size: .85rem; }
}

.tags-cloud { display: flex; flex-wrap: wrap; gap: .5rem; }
.tags-cloud .count { opacity: .7; font-size: .75rem; }

// Archive
.archive-year { margin-bottom: 2rem; }
.year-title {
  display: flex; align-items: center; gap: .5rem;
  color: var(--md-primary); border-bottom: 2px solid var(--md-primary-container);
  padding-bottom: .5rem;
}
.archive-list { list-style: none; padding: 0; }
.archive-item {
  display: flex; gap: 1rem; align-items: center; padding: .75rem 0;
  border-bottom: 1px solid var(--md-outline-variant);
  time { color: var(--md-on-surface-variant); font-size: .85rem; min-width: 60px; }
  a { flex: 1; color: var(--md-on-surface); font-weight: 500; }
}

// Filter
.filter-bar { margin-bottom: 1.5rem; }

// Pagination
.pagination {
  display: flex; justify-content: center; align-items: center;
  gap: 1rem; margin-top: 2rem; flex-wrap: wrap;
  .page-info { color: var(--md-on-surface-variant); font-size: .9rem; }
}

// Search Modal
.search-modal {
  position: fixed; inset: 0; background: var(--md-scrim);
  z-index: 1000; display: flex; align-items: flex-start; justify-content: center;
  padding: 10vh 1rem;
  &[hidden] { display: none; }
}
.search-dialog {
  width: 100%; max-width: 600px; padding: 1rem;
  box-shadow: var(--md-elevation-3);
}
.search-input-wrap {
  display: flex; align-items: center; gap: .75rem;
  padding: .75rem 1rem; border-radius: var(--md-radius-md);
  background: var(--md-surface-3);
  input {
    flex: 1; border: none; background: transparent; outline: none;
    font-size: 1rem; color: var(--md-on-surface); font-family: inherit;
  }
  kbd {
    font-size: .7rem; padding: .2rem .4rem; border-radius: 4px;
    background: var(--md-surface-1); border: 1px solid var(--md-outline-variant);
    color: var(--md-on-surface-variant);
  }
  &.big { padding: 1rem 1.25rem; font-size: 1.1rem; }
}
.search-results {
  margin-top: 1rem; max-height: 50vh; overflow-y: auto;
  .search-item {
    display: block; padding: .75rem 1rem; border-radius: var(--md-radius-md);
    color: var(--md-on-surface); transition: background .15s;
    &:hover, &.active { background: var(--md-surface-3); }
    h4 { margin: 0 0 .25rem; font-size: .95rem; }
    p { margin: 0; font-size: .8rem; color: var(--md-on-surface-variant); }
  }
  .no-results { padding: 2rem; text-align: center; color: var(--md-on-surface-variant); }
}
.search-footer {
  display: flex; gap: 1rem; padding-top: .75rem; margin-top: .75rem;
  border-top: 1px solid var(--md-outline-variant);
  font-size: .75rem; color: var(--md-on-surface-variant);
  kbd { padding: .1rem .35rem; border-radius: 3px; background: var(--md-surface-3); margin-right: .25rem; }
}

// Skeleton
.skeleton-grid {
  display: grid; gap: 1.5rem;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.skeleton-card { overflow: hidden; }
.skeleton-img { aspect-ratio: 16/10; background: var(--md-surface-2); }
.skeleton-body { padding: 1.25rem; display: flex; flex-direction: column; gap: .6rem; }
.skeleton-line { height: 12px; background: var(--md-surface-2); border-radius: 4px;
  &.short { width: 40%; }
  &.medium { width: 75%; }
}
.skeleton-shimmer {
  background: linear-gradient(90deg, var(--md-surface-2) 25%, var(--md-surface-3) 50%, var(--md-surface-2) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

// 404
.error-page {
  padding: 4rem 2rem; text-align: center;
  .error-icon { font-size: 5rem; color: var(--md-primary); }
  h1 { font-size: 5rem; margin: 0; color: var(--md-primary); font-weight: 800; }
  h2 { margin: .5rem 0; }
  p { color: var(--md-on-surface-variant); max-width: 500px; margin: 1rem auto; }
  .error-actions { display: flex; gap: .75rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem; }
}

// Infinite scroll sentinel
.infinite-sentinel { height: 1px; }
.load-status {
  text-align: center; padding: 2rem; color: var(--md-on-surface-variant);
  display: flex; align-items: center; justify-content: center; gap: .5rem;
}

4️⃣3️⃣ _sass/_markdown.scss

.md3-typography {
  font-size: 1.05rem; line-height: 1.75; color: var(--md-on-surface);

  h1, h2, h3, h4, h5, h6 {
    margin: 2rem 0 1rem; font-weight: 700; line-height: 1.3;
    scroll-margin-top: 80px;
  }
  h1 { font-size: 2rem; }
  h2 { font-size: 1.6rem; border-bottom: 2px solid var(--md-primary-container); padding-bottom: .35rem; }
  h3 { font-size: 1.3rem; color: var(--md-primary); }
  h4 { font-size: 1.1rem; }

  p { margin: 1rem 0; }

  a { text-decoration: underline; text-underline-offset: 3px;
    &:hover { color: var(--md-secondary); }
  }

  strong { font-weight: 700; color: var(--md-on-surface); }
  em { font-style: italic; }
  del { text-decoration: line-through; color: var(--md-on-surface-variant); }

  blockquote {
    margin: 1.5rem 0; padding: 1rem 1.5rem;
    border-left: 4px solid var(--md-primary);
    background: var(--md-surface-2); border-radius: 0 var(--md-radius-md) var(--md-radius-md) 0;
    color: var(--md-on-surface-variant); font-style: italic;
    p { margin: .5rem 0; }
  }

  ul, ol { padding-left: 1.5rem; margin: 1rem 0; }
  li { margin: .4rem 0; }
  li::marker { color: var(--md-primary); }

  // Task list
  ul.task-list { list-style: none; padding-left: 0; }
  ul.task-list li { display: flex; align-items: flex-start; gap: .5rem; }
  ul.task-list input[type="checkbox"] {
    accent-color: var(--md-primary);
    width: 18px; height: 18px; margin-top: .3rem;
  }

  code {
    background: var(--md-surface-2); color: var(--md-primary);
    padding: .15rem .4rem; border-radius: 4px;
    font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace;
    font-size: .9em;
  }

  pre {
    background: var(--md-surface-2); padding: 1.25rem;
    border-radius: var(--md-radius-md); overflow-x: auto;
    border: 1px solid var(--md-outline-variant);
    code { background: transparent; color: var(--md-on-surface); padding: 0; font-size: .88rem; }
  }

  // Rouge syntax highlighting
  .highlight { @extend pre; }
  .hll { background: var(--md-primary-container); }
  .c, .cm, .c1, .cs { color: #6A9955; font-style: italic; } // comment
  .k, .kd, .kn, .kp, .kr, .kt { color: #C586C0; } // keyword
  .s, .sb, .sc, .sd, .s2, .se, .sh, .si, .sx, .sr, .s1, .ss { color: #CE9178; } // string
  .na, .nb, .nc, .nd, .ne, .nf, .ni, .nl, .no, .nt, .nv { color: #4EC9B0; } // name
  .m, .mf, .mh, .mi, .mo { color: #B5CEA8; } // number
  .o, .ow { color: #D4D4D4; } // operator
  .p { color: var(--md-on-surface); }

  table {
    width: 100%; border-collapse: collapse; margin: 1.5rem 0;
    background: var(--md-surface-1); border-radius: var(--md-radius-md);
    overflow: hidden; box-shadow: var(--md-elevation-1);
  }
  th, td { padding: .75rem 1rem; text-align: left; border-bottom: 1px solid var(--md-outline-variant); }
  th { background: var(--md-primary-container); color: var(--md-on-primary-container); font-weight: 600; }
  tr:last-child td { border-bottom: none; }
  tr:hover td { background: var(--md-surface-2); }

  img, video, iframe {
    border-radius: var(--md-radius-md); margin: 1.5rem auto;
    box-shadow: var(--md-elevation-1);
  }
  img { max-width: 100%; height: auto; }

  hr {
    border: none; height: 2px;
    background: linear-gradient(90deg, transparent, var(--md-outline-variant), transparent);
    margin: 2.5rem 0;
  }

  // Details/Summary
  details {
    background: var(--md-surface-2); padding: 1rem 1.25rem;
    border-radius: var(--md-radius-md); margin: 1rem 0;
    summary { cursor: pointer; font-weight: 600; color: var(--md-primary); }
    &[open] summary { margin-bottom: .75rem; }
  }

  mark {
    background: var(--md-primary-container); color: var(--md-on-primary-container);
    padding: .1rem .3rem; border-radius: 3px;
  }

  sup, sub { font-size: .75em; }
}

4️⃣4️⃣ assets/js/main.js

/* =========================================================
   KARANG TARUNA — Main JavaScript
   Features: Theme, Menu, Search, Infinite Scroll, Back to Top,
             Reading Progress, Copy Link, Lazy Load
   ========================================================= */

(() => {
  'use strict';

  // ---------- THEME ----------
  const THEME_KEY = 'kt-theme';
  const themeToggle = document.getElementById('theme-toggle');
  const themeIcon = themeToggle?.querySelector('.theme-icon');

  const applyTheme = (t) => {
    document.documentElement.setAttribute('data-theme', t);
    if (themeIcon) themeIcon.textContent = t === 'dark' ? 'light_mode' : 'dark_mode';
    localStorage.setItem(THEME_KEY, t);
  };

  const savedTheme = localStorage.getItem(THEME_KEY);
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  applyTheme(savedTheme || (prefersDark ? 'dark' : 'light'));

  themeToggle?.addEventListener('click', () => {
    const current = document.documentElement.getAttribute('data-theme');
    applyTheme(current === 'dark' ? 'light' : 'dark');
  });

  // ---------- MOBILE MENU ----------
  const menuToggle = document.querySelector('.menu-toggle');
  const primaryNav = document.getElementById('primary-nav');
  menuToggle?.addEventListener('click', () => {
    const expanded = primaryNav.classList.toggle('open');
    menuToggle.setAttribute('aria-expanded', expanded);
  });
  document.addEventListener('click', (e) => {
    if (!primaryNav?.contains(e.target) && !menuToggle?.contains(e.target)) {
      primaryNav?.classList.remove('open');
      menuToggle?.setAttribute('aria-expanded', 'false');
    }
  });

  // ---------- BACK TO TOP ----------
  const btt = document.getElementById('back-to-top');
  const onScroll = () => {
    if (window.scrollY > 400) btt?.classList.add('visible');
    else btt?.classList.remove('visible');
    updateProgress();
  };
  window.addEventListener('scroll', onScroll, { passive: true });
  btt?.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));

  // ---------- READING PROGRESS ----------
  const progress = document.getElementById('reading-progress');
  const updateProgress = () => {
    if (!progress) return;
    const docH = document.documentElement.scrollHeight - window.innerHeight;
    const pct = docH > 0 ? (window.scrollY / docH) * 100 : 0;
    progress.style.width = pct + '%';
  };

  // ---------- SEARCH ----------
  const searchModal = document.getElementById('search-modal');
  const searchInput = document.getElementById('search-input');
  const searchResults = document.getElementById('search-results');
  const searchToggle = document.getElementById('search-toggle');
  let searchIndex = null;

  const openSearch = () => {
    searchModal?.removeAttribute('hidden');
    setTimeout(() => searchInput?.focus(), 50);
    loadSearchIndex();
  };
  const closeSearch = () => {
    searchModal?.setAttribute('hidden', '');
    if (searchInput) searchInput.value = '';
    if (searchResults) searchResults.innerHTML = '';
  };

  searchToggle?.addEventListener('click', openSearch);
  searchModal?.addEventListener('click', (e) => { if (e.target === searchModal) closeSearch(); });
  document.addEventListener('keydown', (e) => {
    if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); openSearch(); }
    if (e.key === 'Escape') closeSearch();
  });

  const loadSearchIndex = async () => {
    if (searchIndex) return;
    try {
      const res = await fetch('/search.json');
      searchIndex = await res.json();
    } catch (err) {
      console.warn('Search index not found', err);
    }
  };

  searchInput?.addEventListener('input', (e) => {
    const q = e.target.value.trim().toLowerCase();
    if (!searchIndex || q.length < 2) {
      searchResults.innerHTML = '';
      return;
    }
    const results = searchIndex.filter(p =>
      p.title.toLowerCase().includes(q) ||
      (p.content || '').toLowerCase().includes(q) ||
      (p.categories || []).some(c => c.toLowerCase().includes(q)) ||
      (p.tags || []).some(t => t.toLowerCase().includes(q))
    ).slice(0, 10);

    if (results.length === 0) {
      searchResults.innerHTML = '<div class="no-results">Tidak ada hasil untuk <strong>' + escapeHtml(q) + '</strong></div>';
      return;
    }
    searchResults.innerHTML = results.map(p => `
      <a class="search-item" href="${p.url}" role="option">
        <h4>${highlight(p.title, q)}</h4>
        <p>${highlight((p.content || '').slice(0, 120), q)}...</p>
      </a>
    `).join('');
  });

  const escapeHtml = (s) => s.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
  const highlight = (text, q) => {
    if (!q) return escapeHtml(text);
    const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
    return escapeHtml(text).replace(re, '<mark>$1</mark>');
  };

  // ---------- INFINITE SCROLL (Beranda) ----------
  const grid = document.getElementById('articles-grid');
  const sentinel = document.getElementById('infinite-sentinel');
  const loadStatus = document.getElementById('load-status');

  if (grid && sentinel && window.location.pathname === '/') {
    // Load posts from JSON
    const BATCH = 6;
    let allPosts = [];
    let currentPage = 0;
    let loading = false;

    const renderCard = (p) => {
      const cats = (p.categories || []).slice(0, 2).map(c =>
        `<a href="/kategori/${slugify(c)}/" class="md3-chip small">${c}</a>`).join('');
      const excerpt = (p.content || '').replace(/<[^>]+>/g, '').slice(0, 140);
      const words = (p.content || '').replace(/<[^>]+>/g, '').split(/\s+/).length;
      const readTime = Math.max(1, Math.floor(words / 200));
      const date = new Date(p.date).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' });
      const media = p.image
        ? `<img src="${p.image}" alt="${escapeHtml(p.title)}" loading="lazy" decoding="async">`
        : `<div class="card-placeholder"><span class="material-symbols-rounded">article</span></div>`;

      return `
        <article class="article-card md3-surface-1">
          <a href="${p.url}" class="card-media" aria-hidden="true" tabindex="-1">${media}</a>
          <div class="card-body">
            ${cats ? `<div class="card-chips">${cats}</div>` : ''}
            <h3 class="card-title"><a href="${p.url}">${escapeHtml(p.title)}</a></h3>
            <p class="card-excerpt">${escapeHtml(excerpt)}...</p>
            <div class="card-meta"><time>${date}</time><span>·</span><span>${readTime} menit baca</span></div>
          </div>
        </article>`;
    };

    const slugify = (s) => s.toString().toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');

    const loadBatch = () => {
      if (loading || currentPage * BATCH >= allPosts.length) return;
      loading = true;
      loadStatus?.removeAttribute('hidden');

      setTimeout(() => {
        const batch = allPosts.slice(currentPage * BATCH, (currentPage + 1) * BATCH);
        batch.forEach(p => grid.insertAdjacentHTML('beforeend', renderCard(p)));
        currentPage++;
        loading = false;
        loadStatus?.setAttribute('hidden', '');
        if (currentPage * BATCH >= allPosts.length) observer.disconnect();
      }, 300);
    };

    fetch('/search.json').then(r => r.json()).then(posts => {
      allPosts = posts;
      grid.innerHTML = ''; // remove skeleton
      loadBatch();

      const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) loadBatch();
      }, { rootMargin: '200px' });
      observer.observe(sentinel);
    }).catch(() => {
      grid.innerHTML = '<p>Belum ada artikel.</p>';
    });
  }

  // ---------- COPY LINK ----------
  document.getElementById('copy-link')?.addEventListener('click', async (e) => {
    try {
      await navigator.clipboard.writeText(window.location.href);
      const icon = e.currentTarget.querySelector('.material-symbols-rounded');
      const original = icon.textContent;
      icon.textContent = 'check';
      setTimeout(() => icon.textContent = original, 1500);
    } catch {}
  });

  // ---------- LAZY LOAD IMAGES ----------
  if ('loading' in HTMLImageElement.prototype) {
    document.querySelectorAll('img[loading="lazy"]').forEach(img => {
      if (img.dataset.src) { img.src = img.dataset.src; }
    });
  } else {
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => {
        if (e.isIntersecting) {
          const img = e.target;
          if (img.dataset.src) img.src = img.dataset.src;
          io.unobserve(img);
        }
      });
    });
    document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
  }

  // ---------- SMOOTH ANCHOR ----------
  document.querySelectorAll('a[href^="#"]').forEach(a => {
    a.addEventListener('click', (e) => {
      const target = document.querySelector(a.getAttribute('href'));
      if (target) { e.preventDefault(); target.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
    });
  });

})();

4️⃣5️⃣ feed.xml

---
layout: none
---
<?xml version="1.0" encoding="UTF-8"?>
{% feed %}

Jika plugin jekyll-feed aktif, file ini otomatis digenerate. Jika tidak, gunakan template di bawah:

---
layout: none
permalink: /feed.xml
---
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>{{ site.title | xml_escape }}</title>
    <description>{{ site.description | xml_escape }}</description>
    <link>{{ site.url }}{{ site.baseurl }}/</link>
    <atom:link href="{{ '/feed.xml' | absolute_url }}" rel="self" type="application/rss+xml"/>
    <lastBuildDate>{{ site.time | date_to_rfc822 }}</lastBuildDate>
    {% for post in site.posts limit:20 %}
    <item>
      <title>{{ post.title | xml_escape }}</title>
      <description>{{ post.content | xml_escape }}</description>
      <pubDate>{{ post.date | date_to_rfc822 }}</pubDate>
      <link>{{ post.url | absolute_url }}</link>
      <guid isPermaLink="true">{{ post.url | absolute_url }}</guid>
    </item>
    {% endfor %}
  </channel>
</rss>

4️⃣6️⃣ robots.txt

---
layout: none
permalink: /robots.txt
---
User-agent: *
Allow: /

Sitemap: {{ site.url | append: site.baseurl }}/sitemap.xml

4️⃣7️⃣ search.json (Endpoint Pencarian)

---
layout: none
permalink: /search.json
---
[
{% for post in site.posts %}
  {
    "title": {{ post.title | jsonify }},
    "url": {{ post.url | relative_url | jsonify }},
    "date": {{ post.date | date_to_xmlschema | jsonify }},
    "categories": {{ post.categories | jsonify }},
    "tags": {{ post.tags | jsonify }},
    "image": {{ post.image | default: '' | relative_url | jsonify }},
    "content": {{ post.content | strip_html | normalize_whitespace | truncate: 400 | jsonify }}
  }{% unless forloop.last %},{% endunless %}
{% endfor %}
]

🚀 Cara Deploy ke GitHub Pages

1. Buat Repository GitHub

git init
git add .
git commit -m "Initial commit: Website Karang Taruna"
git branch -M main
git remote add origin https://github.com/USERNAME/USERNAME.github.io.git
git push -u origin main

2. Aktifkan GitHub Pages

  • Buka Settings → Pages
  • Source: Deploy from a branch
  • Branch: main / root

3. Konfigurasi Giscus

  1. Kunjungi giscus.app
  2. Install aplikasi Giscus di repository
  3. Salin repo_id dan category_id ke _config.yml

4. Tambahkan Logo & Gambar

  • /assets/img/logo.png (logo organisasi)
  • /assets/img/favicon.png
  • /assets/img/pengurus/*.jpg
  • /assets/img/posts/*.jpg

5. Menulis Artikel Baru

Buat file di _posts/ dengan format:

YYYY-MM-DD-judul-artikel.md

Front matter wajib:

---
layout: post
title: "Judul Artikel"
date: 2026-06-30 10:00:00 +0700
categories: [Kategori1, Kategori2]
tags: [tag1, tag2]
image: /assets/img/posts/cover.jpg
description: "Ringkasan artikel untuk SEO."
---

✅ Fitur yang Sudah Terpenuhi

Fitur Status
Material Design 3 (Green/Teal/Nature)
SEO (OG, Twitter, JSON-LD, Sitemap, RSS)
Markdown lengkap + syntax highlighting
Infinite Scroll di beranda
Dark/Light mode + localStorage
Search instan (Ctrl+K)
Breadcrumb navigation
Reading time + progress bar
Back to Top FAB
Skeleton loading
Giscus comments
Share ke sosial media
Lazy loading gambar
Responsive super mobile-friendly
Halaman 404 menarik
Profil, Struktur, Proker, Galeri, Dokumen, Kontak
Kategori, Tag, Arsip
Peta lokasi + form kontak
Core Web Vitals optimized

💡 Tips Pengembangan

  1. Tambah kategori baru — cukup buat posting dengan categories: [NamaKategori], halaman kategori otomatis terbuat.
  2. Ganti warna tema — edit _sass/_variables.scss.
  3. Ganti komentar — ubah konfigurasi Giscus di _config.yml atau ganti ke Disqus/Utterances.
  4. Form kontak — ganti action di _pages/kontak.md dengan Formspree atau Getform.
  5. Analytics — tambahkan Google Analytics / Plausible di _includes/head.html.

Selamat membangun portal Karang Taruna yang modern, cepat, dan bermanfaat! 🌿💚

Lebih lamaTerbaru

Posting Komentar