From bb5e3a92400ddc541db82a9d061b32399641fafb Mon Sep 17 00:00:00 2001 From: wauputr4 <103489788+wauputr4@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:58:05 +0700 Subject: [PATCH 1/9] feat: refresh community website content --- public/brand/tokengratis-icon.png | Bin 0 -> 435 bytes src/components/DonationPage.astro | 254 +++++++++++++++++++++++++ src/components/HomeHero.astro | 5 +- src/components/ProjectsDirectory.astro | 4 +- src/components/SiteFooter.astro | 12 +- src/components/SiteHeader.astro | 44 +++-- src/data/communities.json | 1 + src/data/roadmap.ts | 37 ++++ src/layouts/BaseLayout.astro | 3 +- src/pages/contact.astro | 6 +- src/pages/donasi.astro | 172 +---------------- src/pages/falsafah.astro | 64 +++++-- src/pages/faq.astro | 20 +- src/pages/forum.astro | 2 +- src/pages/index.astro | 112 +++++++++-- src/pages/kode-etik.astro | 19 +- src/pages/komunitas.astro | 103 +++++----- src/pages/legal.astro | 34 +--- src/pages/press.astro | 148 ++++---------- src/pages/projects.astro | 66 ++++++- src/pages/roadmap.astro | 38 ++++ src/pages/sitemap.xml.ts | 4 + src/pages/sponsors.astro | 164 +--------------- src/pages/tentang.astro | 8 +- src/pages/users.astro | 120 ++++++++++++ 25 files changed, 825 insertions(+), 615 deletions(-) create mode 100644 public/brand/tokengratis-icon.png create mode 100644 src/components/DonationPage.astro create mode 100644 src/data/communities.json create mode 100644 src/data/roadmap.ts create mode 100644 src/pages/roadmap.astro create mode 100644 src/pages/users.astro diff --git a/public/brand/tokengratis-icon.png b/public/brand/tokengratis-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bfebf7c716770060140a4f85df379b12ed9b26e6 GIT binary patch literal 435 zcmV;k0ZjghP)fa9Fbmcy~QbQ6Q1gv^D3bAZe_1_23~&jaNEP}DOFnD3#OfgDYe3M&7z^78&i z#>Ned12psu|9|}O;{TDO2S%U+I=egnuUo$w?0`p)ZvS7tVhI?}m^F<)4mfi30N8SL zeCyWb5fTN0qYjW5jRKj`MEGA)Uits~chCM`y?TCNvZ0`a%>TXnc7V%*ox8VzOTo^b zF8Vsa$inLXmaQB9KYo0h{w1Lx6*0qrJ3wFnTExgLV+K$!Xh0jKg5uK61Lpwg#sTY? z2uetQ;E|LRfpyJ7=-oBrmyixZ?;nPM5(Pbcj$t_rfck2n?vkLm%vox7Siok9%bW$p dIXu{*005+?K#Q_r;~xM3002ovPDHLkV1hP-ytV)U literal 0 HcmV?d00001 diff --git a/src/components/DonationPage.astro b/src/components/DonationPage.astro new file mode 100644 index 0000000..31962f2 --- /dev/null +++ b/src/components/DonationPage.astro @@ -0,0 +1,254 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import BaseButton from './BaseButton.astro'; +import PageHeader from './PageHeader.astro'; +import { withBase } from '../lib/urls'; + +const tiers = [ + { + name: 'Utama', + min: 'Rp 5.000.000/tahun', + benefits: [ + 'Logo di homepage & footer website', + 'Profil organisasi di halaman sponsor', + 'Kolaborasi satu program atau event', + 'Laporan penggunaan dan dampak berkala' + ], + highlight: true + }, + { + name: 'Partner', + min: 'Rp 2.500.000/tahun', + benefits: [ + 'Logo di halaman sponsor & footer', + 'Apresiasi di kanal komunitas', + 'Laporan penggunaan dana berkala' + ], + highlight: false + }, + { + name: 'Pendukung', + min: 'Rp 1.000.000/tahun', + benefits: [ + 'Logo di halaman sponsor', + 'Apresiasi publik', + 'Akses laporan terbuka' + ], + highlight: false + }, + { + name: 'Sahabat', + min: 'Rp 250.000/tahun', + benefits: [ + 'Nama di halaman sponsor', + 'Apresiasi publik', + 'Akses laporan terbuka' + ], + highlight: false + } +]; + +const currentSponsors = [ + { name: 'Tokengratis.id', since: '2026', url: 'https://tokengratis.id/' } +]; + +const openSawerIcon = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='64'%20height='64'%20viewBox='0%200%2040%2040'%3e%3ctitle%3eOpenSawer%3c/title%3e%3ccircle%20cx='20'%20cy='20'%20r='20'%20fill='%230f694e'/%3e%3ccircle%20cx='20'%20cy='11.5'%20r='3.2'%20fill='%23e8b83f'/%3e%3cpath%20d='M10.5%2019.5c1.4%207.2%205%2010.8%209.5%2010.8s8.1-3.6%209.5-10.8M12%2019.5h16'%20fill='none'%20stroke='%23fff'%20stroke-width='3.2'%20stroke-linecap='round'/%3e%3c/svg%3e"; +--- + + + +

IndopenSource dijalankan oleh komunitas sukarela. Dukungan individu, sponsorship, CSR, dan kolaborasi membantu kami merawat infrastruktur, mengadakan event, dan mendukung proyek-proyek potensial.

+
+ +
+
+
+ Donasi per proyek +

Dukung proyek yang Anda gunakan.

+

OpenSawer akan dihubungkan ke halaman setiap proyek yang terdaftar di direktori IndopenSource. Setelah integrasinya siap, siapa pun dapat memilih proyek lalu berdonasi langsung melalui kanal OpenSawer proyek tersebut.

+

Fitur ini masih dalam pengembangan dan belum tersedia di seluruh halaman proyek.

+
+ + +

Buka OpenSawer

+

Lihat kanal donasi terbuka yang sedang disiapkan untuk komunitas dan proyek.

+ sawer.indopensource.org ↗ +
+
+
+ +
+
+ Dampak bersama +

Setiap kontribusi membantu menjaga open source Indonesia tetap hidup dan berkembang.

+
+
+ +
+
+
+
+ Program CSR +

Kami menerima dukungan CSR.

+

+ Dana CSR dapat diarahkan untuk infrastruktur komunitas, dokumentasi dan materi belajar, + event, serta dukungan bagi proyek open source Indonesia yang membutuhkan ruang tumbuh. + Program dan indikator hasil disepakati bersama perusahaan sebelum pelaksanaan. +

+
+ Diskusikan program CSR +
+
+ +
+

Dasar hukum dan cakupannya

+
+

+ Pasal 74 UU No. 40 Tahun 2007 + mewajibkan Perseroan yang menjalankan usaha di bidang dan/atau berkaitan dengan sumber daya alam melaksanakan Tanggung Jawab Sosial dan Lingkungan. Anggarannya diperhitungkan sebagai biaya Perseroan dengan memperhatikan kepatutan dan kewajaran. +

+

+ PP No. 47 Tahun 2012 + mengatur pelaksanaannya melalui rencana kerja tahunan dan pemuatan hasil pelaksanaan dalam laporan tahunan Perseroan. +

+

+ Pasal 15 huruf b UU No. 25 Tahun 2007 + juga mewajibkan setiap penanam modal melaksanakan tanggung jawab sosial perusahaan. Penerapan kewajiban tetap mengikuti status, bidang usaha, dan aturan sektoral perusahaan masing-masing. +

+
+

Informasi umum · perusahaan tetap perlu menilai kewajibannya bersama penasihat hukum.

+
+
+
+
+ +
+
+
+
+ Transparansi terbuka +

Dana masuk, penggunaan, dan dampaknya dapat dilihat.

+

+ Penerimaan dan penggunaan dana CSR direncanakan dipublikasikan melalui OpenSawer. Laporan program, + rincian penyaluran, bukti kegiatan, dan pembaruan dampak akan dibuka setelah kanal pelaporan siap. +

+
+ +
+ + + +
+

OpenSawer

+

Kanal transparansi yang sedang disiapkan

+
+
+

Lihat aplikasi open source yang akan digunakan untuk membuka catatan dana dan laporan program.

+ Buka OpenSawer → +
+
+
+
+ +
+
+
+ Pendukung kami +

Organisasi yang mendukung.

+

Terima kasih kepada organisasi yang telah mendukung misi IndopenSource.

+
+
+ {currentSponsors.map((s) => ( + +
+
+ +
+
+

{s.name}

+

Mendukung sejak {s.since}

+
+
+
+ ))} +
+
+
+ +
+ +
+ +
+
+
+ Dukungan organisasi +

Pilih tingkat kemitraan.

+

Semua paket memberikan akses ke laporan dampak dan apresiasi publik.

+
+
+ {tiers.map((tier) => ( +
+ {tier.highlight && Populer} +

{tier.name}

+

{tier.min}

+
    + {tier.benefits.map((b) => ( +
  • + + {b} +
  • + ))} +
+
+ ))} +
+
+
+ +
+
+
+ Tertarik bermitra? +

Mari diskusikan kemitraan Anda.

+

Kami terbuka untuk paket kustom, sponsorship event, atau bentuk kolaborasi lain yang sejalan dengan misi open source.

+
+ Hubungi kami + Media Kit +
+
+
+
+
diff --git a/src/components/HomeHero.astro b/src/components/HomeHero.astro index 5a96446..0e1d05c 100644 --- a/src/components/HomeHero.astro +++ b/src/components/HomeHero.astro @@ -19,13 +19,12 @@ import { heroImage, HERO_PRELOAD_WIDTH, HERO_QUALITY } from '../lib/hero';

- Rumah terbuka untuk merawat daftar proyek, mendokumentasikan falsafah, - menulis pembelajaran, dan menghubungkan kontributor open source Indonesia. + Rumah terbuka untuk merawat daftar proyek, menulis pembelajaran, + dan menghubungkan kontributor open source Indonesia.

Lihat proyek - Roadmap GitHub
diff --git a/src/components/ProjectsDirectory.astro b/src/components/ProjectsDirectory.astro index 665bf52..9fff852 100644 --- a/src/components/ProjectsDirectory.astro +++ b/src/components/ProjectsDirectory.astro @@ -10,6 +10,7 @@ interface Props { const { projects } = Astro.props; const languages = [...new Set(projects.map((project) => project.language).filter((language): language is string => Boolean(language)))].sort(); const topics = [...new Set(projects.flatMap((project) => project.topics || []))].sort().slice(0, 80); +const repoName = (fullName: string) => fullName.split('/').at(-1) || fullName; const formatDate = (value?: string) => value ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'medium' }).format(new Date(value)) : '-'; @@ -110,7 +111,8 @@ const formatDate = (value?: string) => value ? : } - {project.fullName} + {repoName(project.fullName)} + {project.owner && oleh {project.owner}} {project.description} {!project.syncFailed && ( diff --git a/src/components/SiteFooter.astro b/src/components/SiteFooter.astro index 17f1dbd..5b14a7c 100644 --- a/src/components/SiteFooter.astro +++ b/src/components/SiteFooter.astro @@ -2,8 +2,14 @@ import { withBase } from '../lib/urls'; const links: { label: string; href: string; external?: boolean }[] = [ + { label: 'Proyek', href: '/projects/' }, + { label: 'Belajar', href: '/belajar/' }, { label: 'Tentang', href: '/tentang/' }, + { label: 'Falsafah', href: '/falsafah/' }, { label: 'Komunitas', href: '/komunitas/' }, + { label: 'Top User', href: '/users/' }, + { label: 'Blog', href: '/blog/' }, + { label: 'Forum', href: '/forum/' }, { label: 'Cara Berkontribusi', href: '/cara-berkontribusi/' }, { label: 'Kode Etik', href: '/kode-etik/' }, { label: 'Donasi', href: '/donasi/' }, @@ -11,13 +17,13 @@ const links: { label: string; href: string; external?: boolean }[] = [ { label: 'Program', href: '/program/' }, { label: 'Tata Kelola', href: '/tata-kelola/' }, { label: 'Event', href: '/events/' }, - { label: 'Sponsors', href: '/sponsors/' }, + { label: 'Kontak', href: '/contact/' }, { label: 'Press', href: '/press/' }, { label: 'Legal', href: '/legal/' }, { label: 'Security', href: '/security/' }, + { label: 'Roadmap', href: '/roadmap/' }, { label: 'GitHub', href: 'https://github.com/IndopenSource', external: true }, { label: 'Discussions', href: 'https://github.com/orgs/IndopenSource/discussions', external: true }, - { label: 'Roadmap', href: 'https://github.com/orgs/IndopenSource/projects', external: true } ]; const year = new Date().getFullYear(); @@ -44,7 +50,7 @@ const year = new Date().getFullYear(); bareng, dirawat bareng.

- Open Source ID + Open source · Indonesia {/* Thick rule between mission and the colophon grid */} diff --git a/src/components/SiteHeader.astro b/src/components/SiteHeader.astro index 372f2f1..9524fc7 100644 --- a/src/components/SiteHeader.astro +++ b/src/components/SiteHeader.astro @@ -4,24 +4,20 @@ import { withBase } from '../lib/urls'; import brandIcon from '../assets/brand/icon-192.png'; const navItems = [ - ['Home', '/'], - ['Falsafah', '/falsafah/'], - ['Proyek', '/projects/'], - ['Belajar', '/belajar/'], - ['Kontribusi', '/cara-berkontribusi/'], - ['Blog', '/blog/'], - ['Forum', '/forum/'], - ['FAQ', '/faq/'], - ['Donasi', '/donasi/'], - ['Kontak', '/contact/'] + ['Home', '/', 'ph-house'], + ['Proyek', '/projects/', 'ph-grid-four'], + ['Blog', '/blog/', 'ph-article'], + ['Belajar', '/belajar/', 'ph-books'], + ['FAQ', '/faq/', 'ph-question'], + ['Donasi', '/donasi/', 'ph-heart'] ]; const mobileNavItems = [ ['Home', '/', 'ph-house'], ['Proyek', '/projects/', 'ph-grid-four'], - ['Belajar', '/belajar/', 'ph-books'], ['Blog', '/blog/', 'ph-article'], - ['Forum', '/forum/', 'ph-chats-circle'] + ['Belajar', '/belajar/', 'ph-books'], + ['FAQ', '/faq/', 'ph-question'] ]; const currentPath = Astro.url.pathname; @@ -65,26 +61,44 @@ const currentPathWithoutBase = currentPath.replace(new RegExp(`^${baseUrl.replac {/* Desktop nav — mono uppercase labels, riso-underline reveal, sticker active state */}
{ - navItems.map(([label, href]) => { + navItems.map(([label, href, icon]) => { const isCurrent = currentPathWithoutBase === href; + const isDonation = href === '/donasi/'; return isCurrent ? ( + {label} ) : ( + {label} ); }) } + + +
diff --git a/src/data/communities.json b/src/data/communities.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/communities.json @@ -0,0 +1 @@ +[] diff --git a/src/data/roadmap.ts b/src/data/roadmap.ts new file mode 100644 index 0000000..f2e0551 --- /dev/null +++ b/src/data/roadmap.ts @@ -0,0 +1,37 @@ +export const roadmapPeriods = [ + { + period: 'Jul–Sep 2026', + title: 'Rapikan fondasi data', + items: [ + 'Jaga sinkronisasi direktori proyek', + 'Publikasikan roadmap dan kanal transparansi', + 'Rapikan jalur kontribusi komunitas', + 'Uji OpenSawer bersama proyek terpilih', + 'Rekrut penggerak komunitas', + 'Adakan event perdana' + ] + }, + { + period: 'Okt–Des 2026', + title: 'Buka dukungan komunitas', + items: [ + 'Kurasi kebutuhan proyek potensial', + 'Mulai laporan sponsor dan CSR terbuka', + 'Perluas artikel dan materi belajar' + ] + }, + { + period: 'Jan–Mar 2027', + title: 'Hubungkan proyek dan organisasi', + items: [ + 'Adakan konferensi perangkat terbuka', + 'Buka kolaborasi program dengan komunitas', + 'Dokumentasikan hasil program' + ] + }, + { + period: 'Apr–Jun 2027', + title: 'Ukur dan tentukan langkah berikutnya', + items: ['Terbitkan laporan satu tahun', 'Evaluasi program yang berdampak', 'Susun roadmap periode selanjutnya'] + } +]; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 31dc3e4..69dbdb9 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -82,11 +82,12 @@ for (const byte of new Uint8Array(jsonLdDigest)) { jsonLdBinary += String.fromCharCode(byte); } const jsonLdHash = btoa(jsonLdBinary); +const styleSrc = import.meta.env.DEV ? "style-src 'self' 'unsafe-inline'" : "style-src 'self'"; const csp = [ "default-src 'self'", "base-uri 'self'", `script-src 'self' 'sha256-${jsonLdHash}'`, - "style-src 'self'", + styleSrc, // First-party + GitHub avatar/raw image hosts only — no third-party beacons. "img-src 'self' data: https://raw.githubusercontent.com https://avatars.githubusercontent.com", "font-src 'self' data:", diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 4c50a3a..6f71501 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -17,9 +17,9 @@ const channels = [ }, { tag: 'Roadmap', - handle: 'GitHub Projects', - href: 'https://github.com/orgs/IndopenSource/projects', - label: 'GitHub Projects IndopenSource', + handle: 'Roadmap 2026–2027', + href: '/roadmap/', + label: 'Roadmap IndopenSource', meta: '// urutan pekerjaan publik' }, { diff --git a/src/pages/donasi.astro b/src/pages/donasi.astro index 9c3b6be..b1ab0de 100644 --- a/src/pages/donasi.astro +++ b/src/pages/donasi.astro @@ -1,173 +1,5 @@ --- -import BaseLayout from '../layouts/BaseLayout.astro'; -import BaseButton from '../components/BaseButton.astro'; -import PageHeader from '../components/PageHeader.astro'; -import InfoCard from '../components/InfoCard.astro'; -import { withBase } from '../lib/urls'; - -const highlights = [ - { - title: 'Open Source', - desc: 'Sawer App adalah perangkat donasi yang source code-nya terbuka — bisa diaudit, difork, dan dipasang mandiri oleh proyek mana pun.' - }, - { - title: 'Untuk Komunitas', - desc: 'Dirancang khusus untuk kebutuhan ekosistem open source Indonesia. Ringan, transparan, tanpa iklan.' - }, - { - title: 'Bisa Dipasang Mandiri', - desc: 'Setiap proyek yang listing di IndopenSource nantinya bisa menggunakan Sawer App sebagai kanal donasi resmi proyek mereka.' - } -]; +import DonationPage from '../components/DonationPage.astro'; --- - - -

- IndopenSource adalah inisiatif komunitas yang dijalankan secara sukarela. - Ada dua cara untuk mendukung — donasi dari individu lewat Sawer App, - atau sponsor dari organisasi yang sejalan dengan misi open source Indonesia. -

-
- - {/* PREVIEW — banner mencolok, semua fitur masih preview */} -
-
-
-
- Preview -

Halaman dukungan masih tahap awal.

-

- Baik kanal donasi lewat Sawer App maupun paket sponsor - organisasi masih dalam persiapan. Yang bisa kamu lihat sekarang adalah - preview awal — pantau terus untuk info selanjutnya. -

-
- - Kunjungi Sawer App → - -
-
-
- - {/* DUA JALUR — donasi individu ⬄ sponsor organisasi */} -
-
-
- Dua jalur dukungan - // donasi & sponsor -

- Individu sawer, organisasi sponsor. -

-

- Baik kamu kontributor individu yang ingin menyawer proyek favorit, - maupun organisasi yang ingin berpartner secara resmi — ada jalurnya. - Dua bentuk dukungan ini menjaga IndopenSource tetap independen dan terbuka. -

-
- -
-
- -

Donasi individu

-

- Lewat Sawer App, kamu bisa mendukung proyek open source - secara langsung. Sawer App adalah platform donasi terbuka — source code-nya - publik, bisa dipasang mandiri, dan dirancang untuk ekosistem Indonesia. -

-
- - Coba Sawer App - -
-
- -
- -

Sponsor organisasi

-

- Organisasi bisa menjadi sponsor resmi IndopenSource — dukungan finansial - untuk infrastruktur, event komunitas, dan program pengembangan. - Tersedia beberapa tingkat sponsorship dengan benefit masing-masing. -

-
- - Lihat paket sponsor - -
-
-
-
-
- - {/* TENTANG SAWER — app open source untuk proyek yang listing */} -
-
-
- Sawer App - // platform donasi terbuka -

- Sawer dulu, baru saweran. -

-

- Sawer App adalah aplikasi donasi yang source code-nya terbuka untuk umum. - Bukan sekadar platform — ini adalah perangkat yang bisa dipakai ulang - oleh proyek open source Indonesia untuk menerima dukungan dari komunitas. -

-
- -
- {highlights.map((h) => ( - -

{h.desc}

-
- ))} -
- -
-
-

- Sawer App — open source, MIT license. - Setiap proyek yang listing di IndopenSource nantinya bisa menggunakan Sawer - sebagai kanal donasi resmi langsung dari halaman proyek mereka. -

-

- Repositori sedang disiapkan · pantau GitHub IndopenSource -

-
- - Daftarkan proyekmu - -
-
-
- - {/* CLOSING — coming soon dengan tiga aksi */} -
-
- Coming soon -

- Siap-siap. Sawer & Sponsor segera hadir. -

-

- Halaman ini masih preview — baik donasi individu lewat Sawer App - maupun paket sponsor organisasi sedang disiapkan. - Pantau GitHub dan website untuk info selanjutnya. -

-
- - Coba Sawer App → - - - Info sponsor - - - Ikuti blog - -
-
-
-
+ diff --git a/src/pages/falsafah.astro b/src/pages/falsafah.astro index af91978..fbb869e 100644 --- a/src/pages/falsafah.astro +++ b/src/pages/falsafah.astro @@ -3,32 +3,43 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import PageHeader from '../components/PageHeader.astro'; import LightSvgMotion from '../components/LightSvgMotion.astro'; -// Three pillars, numbered editorial-style. The page logic is purely -// presentational (static content) so the restyle is the whole job; an array -// keeps the numbered rhythm consistent and easy to read. const prinsip = [ { no: '01', - tag: 'Terbuka', - title: 'Kode, jejak, dan keputusan dibuka.', - body: 'Kode, diskusi, keputusan, dan roadmap dibuat mudah ditemukan oleh publik — bukan rahasia tim, tapi catatan bersama yang bisa dibaca siapa saja.', - meta: '// transparansi default' + tag: 'Kebebasan', + title: 'Boleh dipelajari, diubah, dan dibagikan.', + body: 'Akses ke kode sumber bukan tujuan akhir. Orang perlu punya ruang nyata untuk memahami cara kerja perangkat lunak, menyesuaikannya dengan kebutuhan, dan meneruskan perbaikan kepada orang lain.', + meta: '// pengguna juga pelaku' }, { no: '02', - tag: 'Terawat', - title: 'Daftar yang dijaga, bukan ditumpuk.', - body: 'Daftar proyek perlu kurasi, metadata yang jelas, dan ritme pembaruan yang sehat. Yang dirawat tetap berguna; yang ditinggalkan jadi beban.', - meta: '// kurasi > akumulasi' + tag: 'Terbuka', + title: 'Kerja terlihat, agar orang bisa ikut.', + body: 'Kode, diskusi, keputusan, dokumentasi, dan roadmap sebaiknya meninggalkan jejak publik. Keterbukaan membuat masalah lebih mudah ditemukan, pengetahuan tidak berhenti pada satu orang, dan kontribusi dapat diperiksa bersama.', + meta: '// catatan adalah infrastruktur' }, { no: '03', - tag: 'Bermanfaat', - title: 'Dipakai orang Indonesia, hari ini.', - body: 'Fokus pada perangkat, pustaka, data, dan praktik yang membantu pengguna Indonesia secara nyata — bukan demo yang mengkilap lalu terlupakan.', - meta: '// manfaat lokal nyata' + tag: 'Terawat', + title: 'Rilis bukan garis akhir.', + body: 'Proyek yang berguna memerlukan kurasi, dokumentasi, review, keamanan, dan regenerasi pengelola. Merawat pekerjaan lama sama berharganya dengan memulai repository baru.', + meta: '// keberlanjutan adalah kerja' + }, + { + no: '04', + tag: 'Berakar', + title: 'Terbuka untuk kebutuhan Indonesia.', + body: 'Open source memberi ruang untuk membangun kemampuan sendiri, memakai ulang pengetahuan, dan menyesuaikan teknologi dengan bahasa serta kebutuhan lokal. Kemandirian tidak berarti bekerja sendirian; ia tumbuh lewat gotong royong lintas proyek dan komunitas.', + meta: '// lokal, bukan terisolasi' } ]; + +const references = [ + { title: 'History of the Open Source Initiative', note: 'sejarah istilah dan gerakan open source', href: 'https://opensource.org/about/history-of-the-open-source-initiative' }, + { title: 'GNU Manifesto', note: 'kebebasan mempelajari, mengubah, dan membagikan perangkat lunak', href: 'https://web.cs.ucdavis.edu/~rogaway/classes/188/materials/gnu-manifesto.pdf' }, + { title: 'The Cathedral and the Bazaar', note: 'pengembangan terbuka, umpan balik, dan kolaborasi publik', href: 'https://firstmonday.org/ojs/index.php/fm/article/download/578/499?inline=1' }, + { title: 'Indonesia, Go Open Source', note: 'jejak gerakan dan kemandirian teknologi di Indonesia', href: 'https://id.wikipedia.org/wiki/Indonesia,_Go_Open_Source' } +]; --- - {/* Three numbered principles — editorial grid, asymmetric: oversized index + + {/* Numbered principles — editorial grid, asymmetric: oversized index + sticker tag on the left rail, the statement and body on the right. Thick ink rule separates each entry. */}
- Tiga prinsip + Empat prinsip - +
@@ -97,6 +108,23 @@ const prinsip = [
+
+
+ Bacaan dasar +

Falsafah yang bisa ditelusuri.

+

Prinsip di atas dirumuskan untuk konteks IndopenSource dari sejarah dan praktik gerakan open source. Ini bukan kutipan atau definisi hukum lisensi, melainkan pegangan kerja komunitas.

+
+ {references.map((reference) => ( + +

{reference.title}

+

{reference.note}

+ Baca sumber ↗ +
+ ))} +
+
+
+ {/* Closing stamp — a halftone poster band with a rubber-stamp sign-off, so the manifesto ends on a printed, community-made note. ink/brand on canvas all AA. */} diff --git a/src/pages/faq.astro b/src/pages/faq.astro index d30d979..087a573 100644 --- a/src/pages/faq.astro +++ b/src/pages/faq.astro @@ -17,28 +17,28 @@ const faqs = [ { category: 'Proyek', items: [ - { q: 'Bagaimana cara mendaftarkan proyek saya?', a: 'Buka issue di repository awesome-indonesia dengan template yang tersedia. Tim kurasi akan mereview dan membantu menambahkan proyek Anda ke direktori.' }, - { q: 'Apa syarat proyek bisa masuk direktori?', a: 'Proyek harus open source (lisensi OSI-approved), dibuat atau dipelihara oleh orang Indonesia, dan memiliki dokumentasi dasar (README, cara install). Tidak harus besar atau populer.' }, - { q: 'Proyek saya tidak aktif, apakah bisa dihapus?', a: 'Kami memiliki kategori "revival" untuk proyek potensial yang perlu dihidupkan kembali. Jika proyek benar-benar tidak relevan, kami bisa memindahkannya ke arsip.' }, - { q: 'Bolehkah proyek komersial masuk?', a: 'Boleh, selama ada komponen open source yang jelas dan lisensinya transparan. Kami juga menerima proyek dengan model open-core.' } + { q: 'Bagaimana cara mendaftarkan proyek saya?', a: 'Tambahkan nama repository ke repos.json di repository awesome-indonesia, jalankan validasi, lalu kirim pull request dengan ringkasan proyek.' }, + { q: 'Apa syarat proyek bisa masuk direktori?', a: 'Repository harus publik, relevan dengan ekosistem Indonesia, aktif atau fresh pada 2026, dan bukan duplikasi entri yang sudah ada.' }, + { q: 'Di mana resource dan proyek lama ditempatkan?', a: 'Resource non-app dan materi belajar masuk ke Awesome Indonesia Legacy. Proyek lama atau kandidat yang potensial dihidupkan kembali masuk ke Awesome Indonesia Revival.' }, + { q: 'Apakah metadata README harus diedit manual?', a: 'Tidak. Contributor cukup mengubah repos.json. Metadata README akan dibuat dari GitHub API melalui workflow setelah perubahan digabungkan.' } ] }, { category: 'Kontribusi', items: [ { q: 'Saya pemula, bisa mulai dari mana?', a: 'Cari issue berlabel "good first issue" di repository yang menarik. Atau mulai dari kontribusi non-kode: perbaiki dokumentasi, laporkan bug, atau bantu terjemahan.' }, - { q: 'Bagaimana cara menjadi maintainer?', a: 'Mulai dengan kontribusi konsisten di satu proyek. Setelah beberapa PR di-merge dan Anda memahami kodebase, maintainer existing bisa mengundang Anda menjadi co-maintainer.' }, - { q: 'PR saya tidak di-review, harus bagaimana?', a: 'Bisa mention maintainer di PR atau tanya di Discussions. Kadang review tertunda karena maintainer sibuk. Bersabar dan sopan selalu membantu.' }, - { q: 'Apakah ada mentoring untuk kontributor baru?', a: 'Ya! Kami memiliki program mentoring melalui Discussions. Anda bisa meminta guidance dari maintainer berpengalaman atau kontributor senior.' } + { q: 'Bagaimana cara membantu repository lain?', a: 'Baca panduan kontribusi repository tersebut, pilih issue yang sesuai, lalu kirim perubahan kecil yang mudah ditinjau.' }, + { q: 'PR saya belum ditinjau, harus bagaimana?', a: 'Tambahkan konteks yang kurang di PR lalu tanyakan dengan sopan kepada maintainer. Setiap repository memiliki ritme review berbeda.' }, + { q: 'Apakah ada program mentoring?', a: 'Belum ada program mentoring resmi. Pertanyaan dan usulan bantuan dapat dibuka melalui GitHub Discussions.' } ] }, { category: 'Teknis', items: [ - { q: 'Lisensi apa yang direkomendasikan?', a: 'Untuk proyek software: MIT, Apache 2.0, atau GPL. Untuk dokumentasi: CC-BY-SA. Pilih yang sesuai dengan tujuan dan komunitas target Anda.' }, + { q: 'Lisensi apa yang sebaiknya dipilih?', a: 'Pilih lisensi yang sesuai dengan tujuan proyek dan pahami kewajibannya. Direktori menampilkan lisensi dari metadata repository, tetapi tidak memberikan nasihat hukum.' }, { q: 'Bagaimana cara menangani security vulnerability?', a: 'Laporkan secara privat ke maintainer atau via halaman Security kami. Jangan buka issue publik untuk vulnerability sebelum ada patch.' }, - { q: 'Apakah ada CI/CD yang bisa dipakai?', a: 'GitHub Actions tersedia gratis untuk repository publik. Kami juga menyediakan template workflow untuk testing, linting, dan deployment.' }, - { q: 'Bagaimana cara hosting dokumentasi?', a: 'GitHub Pages, Netlify, atau Vercel menyediakan hosting gratis untuk static site. Astro (yang kami pakai) bisa di-deploy ke mana saja.' } + { q: 'Apakah IndopenSource menyediakan template CI/CD?', a: 'Belum ada template CI/CD resmi. Lihat workflow repository IndopenSource sebagai referensi, lalu sesuaikan dengan kebutuhan proyek.' }, + { q: 'Bagaimana website ini dibuat?', a: 'Website ini menggunakan Astro. Kode dan workflow deployment tersedia di repository indopensource.org.' } ] } ]; diff --git a/src/pages/forum.astro b/src/pages/forum.astro index 27ba0bb..6c2f78c 100644 --- a/src/pages/forum.astro +++ b/src/pages/forum.astro @@ -15,7 +15,7 @@ import PageHeader from '../components/PageHeader.astro';

Buka discussions - Buka project board + Lihat roadmap
diff --git a/src/pages/index.astro b/src/pages/index.astro index 6fa4bbb..470ff9d 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,15 +4,35 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import HomeHero from '../components/HomeHero.astro'; import InfoCard from '../components/InfoCard.astro'; import ProjectCard from '../components/ProjectCard.astro'; -import RoadmapTimeline from '../components/RoadmapTimeline.astro'; import SectionHeader from '../components/SectionHeader.astro'; import BaseButton from '../components/BaseButton.astro'; import LightSvgMotion from '../components/LightSvgMotion.astro'; -import projects from '../data/projects.json'; +import projectsData from '../data/projects.json'; +import { roadmapPeriods } from '../data/roadmap'; import { heroImage, HERO_PRELOAD_WIDTH, HERO_QUALITY } from '../lib/hero'; +import type { Project } from '../lib/projects'; +import { withBase } from '../lib/urls'; +const projects = projectsData as Project[]; const totalProjects = projects.length; const topProjects = projects.slice(0, 5); +const roadmapPreview = roadmapPeriods.slice(0, 2); +const usersByOwner = new Map(); +for (const project of projects.filter((item) => !item.syncFailed && item.owner)) { + const owner = project.owner as string; + const user = usersByOwner.get(owner) || { + owner, + avatarUrl: project.ownerAvatarUrl || `https://avatars.githubusercontent.com/${encodeURIComponent(owner)}?size=80`, + repositories: 0, + stars: 0 + }; + user.repositories += 1; + user.stars += project.stars; + usersByOwner.set(owner, user); +} +const topUsers = [...usersByOwner.values()] + .sort((a, b) => b.stars - a.stars || b.repositories - a.repositories || a.owner.localeCompare(b.owner)) + .slice(0, 5); // Pre-generate the AVIF the hero will paint as its LCP candidate so we // can the exact same /_astro/* file from . Same src, @@ -69,11 +89,13 @@ const heroPreload = await getImage({ // kenapa kami ada
- Open source bukan cuma kode di GitHub — ini soal merawat catatan, - membuka pintu kontribusi, dan menulis ulang cerita teknologi Indonesia - secara terbuka. + Open source Indonesia itu soal merawat catatan, membuka pintu kontribusi, + kolaborasi dan menulis ulang cerita teknologi Indonesia secara terbuka.
-

— IndopenSource · komunitas terbuka

+
+

— IndopenSource · komunitas terbuka

+ Baca falsafah +
@@ -101,7 +123,7 @@ const heroPreload = await getImage({ Semua orang boleh ikut menulis catatannya
- Mulai kontribusi + Mulai kontribusi

@@ -119,13 +141,26 @@ const heroPreload = await getImage({

- +
- Gabung diskusi + Lihat selengkapnya
- +
+ {roadmapPreview.map((item, index) => ( +
+
+ {item.period} + +
+

{item.title}

+
    + {item.items.map((entry) =>
  • {entry}
  • )} +
+
+ ))} +
@@ -149,7 +184,9 @@ const heroPreload = await getImage({
- + + Pilihan proyek dari katalog awesome-indonesia. +
Buka semua @@ -169,6 +206,59 @@ const heroPreload = await getImage({
+
+
+
+
+ + Peringkat berdasarkan akumulasi stars pada repository yang tercatat di direktori. + +
+
Lihat semua
+
+ +
+
+ +
+ +
+
diff --git a/src/pages/kode-etik.astro b/src/pages/kode-etik.astro index f0f1d3b..49b8483 100644 --- a/src/pages/kode-etik.astro +++ b/src/pages/kode-etik.astro @@ -20,11 +20,6 @@ const unacceptable = [ 'Perilaku lain yang secara wajar dapat dianggap tidak pantas dalam lingkungan profesional' ]; -const enforcement = [ - { level: 'Koreksi', desc: 'Peringatan tertulis dari maintainer. Permintaan untuk menghentikan perilaku.', action: 'Peringatan verbal/written' }, - { level: 'Peringatan', desc: 'Pelanggaran serius atau berulang. Dilarang berpartisipasi di ruang komunitas untuk periode tertentu.', action: 'Suspend sementara' }, - { level: 'Larangan', desc: 'Pelanggaran berat atau berkelanjutan. Dilarang permanen dari semua ruang komunitas.', action: 'Ban permanen' } -]; --- @@ -85,15 +80,9 @@ const enforcement = [

Tindakan untuk pelanggaran.

Maintainer komunitas berhak mengambil tindakan yang sesuai untuk menegakkan kode etik ini.

-
- {enforcement.map((e, i) => ( -
- Level {i + 1} -

{e.level}

-

{e.desc}

-

{e.action}

-
- ))} +
+

Maintainer dapat memoderasi issue, Discussions, pull request, dan ruang komunitas lain agar kolaborasi tetap sehat dan berguna. Tindakan disesuaikan dengan konteks pelanggaran.

+ Baca dokumen sumber →
@@ -103,7 +92,7 @@ const enforcement = [
Laporan

Melaporkan pelanggaran.

-

Jika Anda mengalami atau menyaksikan pelanggaran kode etik, silakan laporkan kepada tim maintainer. Semua laporan akan ditangani secara rahasia dan serius.

+

Jika Anda mengalami atau menyaksikan pelanggaran kode etik, laporkan melalui kanal privat atau kepada maintainer IndopenSource yang Anda percaya.

Laporkan via Email Diskusi publik diff --git a/src/pages/komunitas.astro b/src/pages/komunitas.astro index e92d00f..dc4ce88 100644 --- a/src/pages/komunitas.astro +++ b/src/pages/komunitas.astro @@ -2,77 +2,72 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import BaseButton from '../components/BaseButton.astro'; import PageHeader from '../components/PageHeader.astro'; -import LightSvgMotion from '../components/LightSvgMotion.astro'; +import communitiesData from '../data/communities.json'; -const paths = [ - { no: '01', tag: 'Temukan', title: 'Kurasi satu proyek.', body: 'Tambahkan proyek Indonesia yang layak dipakai, atau rapikan informasi proyek yang sudah ada.', href: '/projects/', action: 'Buka direktori' }, - { no: '02', tag: 'Tulis', title: 'Bagikan yang dipelajari.', body: 'Tulis catatan teknis, pengalaman merawat proyek, atau panduan praktis untuk pembaca berikutnya.', href: '/blog/', action: 'Baca dan kirim artikel' }, - { no: '03', tag: 'Diskusikan', title: 'Bawa pertanyaan ke ruang terbuka.', body: 'Mulai dari ide kecil, kendala kontribusi, atau usulan kolaborasi lintas komunitas.', href: 'https://github.com/orgs/IndopenSource/discussions', action: 'Buka Discussions' }, - { no: '04', tag: 'Rawat', title: 'Bantu pekerjaan yang sering tak terlihat.', body: 'Dokumentasi, terjemahan, pengujian, desain, dan review sama pentingnya dengan menulis kode.', href: 'https://github.com/IndopenSource', action: 'Lihat repository' } -]; +interface Community { + name: string; + description: string; + focus?: string; + location?: string; + url: string; +} -const spaces = [ - { label: 'Proyek', note: 'direktori dan metadata', href: '/projects/' }, - { label: 'Blog', note: 'catatan dan pembelajaran', href: '/blog/' }, - { label: 'Discussions', note: 'tanya, usul, koordinasi', href: 'https://github.com/orgs/IndopenSource/discussions' }, - { label: 'Roadmap', note: 'pekerjaan yang sedang dibuka', href: 'https://github.com/orgs/IndopenSource/projects' } -]; +const communities = communitiesData as Community[]; --- - -

IndopenSource adalah ruang kerja terbuka. Pilih kontribusi yang sesuai dengan waktu, pengalaman, dan rasa ingin tahu Anda—lalu tinggalkan jejak yang bisa diteruskan orang lain.

+ +

Tempat menemukan komunitas, kelompok pengguna, dan ruang belajar terbuka dari berbagai daerah dan bidang. Daftarnya sedang kami siapkan bersama.

-
- -
- Mulai di sini -

Kontribusi kecil yang terbuka lebih berguna daripada ide besar yang tidak pernah dibagikan.

-
- Mulai diskusi - Cari proyek -
-
-
- -
+
-
-
Empat jalur

Pilih pintu yang paling dekat.

- tidak perlu menunggu undangan -
-
- {paths.map((path) => ( - - ))} -
+ {communities.length === 0 ? ( +
+ Segera hadir +

Belum ada komunitas yang ditampilkan.

+

Kami sengaja memulai dari daftar kosong agar setiap entri punya sumber, tautan aktif, dan diajukan secara terbuka—bukan hasil asumsi.

+
+ ) : ( + + )}
-
-
+
+
- Ritme sederhana -

Baca, bicara, lalu buat perubahan.

+ Tambahkan komunitas +

Buka pull request di repository website.

+

Pengurus atau anggota komunitas boleh mengusulkan entri. Sertakan informasi yang bisa diverifikasi dan pastikan Anda memang berwenang membagikan profil tersebut.

    -
  1. 01 — Pilih satu hal. Temukan proyek, tulisan, atau percakapan yang benar-benar ingin Anda bantu.
  2. -
  3. 02 — Tinggalkan konteks. Ajukan pertanyaan atau usulan secara publik agar orang lain dapat menambahkan pengetahuan.
  4. -
  5. 03 — Kirim kontribusi kecil. Mulai dari koreksi, tautan, contoh penggunaan, atau pull request yang mudah ditinjau.
  6. +
  7. 01 — Fork repository. Buka IndopenSource/indopensource.org lalu buat branch baru.
  8. +
  9. 02 — Tambahkan data. Edit src/data/communities.json dengan nama, deskripsi, wilayah atau fokus, dan tautan resmi.
  10. +
  11. 03 — Verifikasi lokal. Jalankan npm test dan npm run build.
  12. +
  13. 04 — Buka PR ke main. Jelaskan hubungan Anda dengan komunitas serta sumber yang dapat dipakai reviewer untuk memeriksa entri.
diff --git a/src/pages/legal.astro b/src/pages/legal.astro index 617b979..e2ba45c 100644 --- a/src/pages/legal.astro +++ b/src/pages/legal.astro @@ -5,56 +5,34 @@ import PageHeader from '../components/PageHeader.astro'; import { withBase } from '../lib/urls'; const policies = [ - { - icon: 'ph-copyright', - title: 'Lisensi Konten', - desc: 'Konten di website IndopenSource (artikel, dokumentasi, kurasi) dilisensikan di bawah Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0). Anda bebas membagikan dan mengadaptasi konten dengan syarat atribusi dan lisensi yang sama.', - link: 'https://creativecommons.org/licenses/by-sa/4.0/', - isExternal: true - }, { icon: 'ph-code', title: 'Lisensi Kode', - desc: 'Kode sumber website ini dan repository komunitas dilisensikan di bawah MIT License. Proyek yang terkurasi di direktori memiliki lisensi masing-masing yang tercantum di metadata proyek.', + desc: 'Kode sumber website ini dilisensikan di bawah MIT License. Setiap proyek dalam direktori memiliki lisensinya sendiri seperti tercantum pada metadata repository asal.', link: 'https://github.com/IndopenSource/indopensource.org/blob/main/LICENSE', isExternal: true }, { icon: 'ph-shield-warning', title: 'Pelaporan Kerentanan', - desc: 'Jika Anda menemukan kerentanan keamanan di website atau proyek komunitas, laporkan secara bertanggung jawab melalui halaman Security atau email ke security@indopensource.org. Jangan buka issue publik.', + desc: 'Jangan membuka issue publik untuk laporan sensitif. Ikuti halaman Security atau kirim detail melalui kanal privat yang tersedia.', link: '/security/', isExternal: false }, - { - icon: 'ph-gavel', - title: 'DMCA & Hak Cipta', - desc: 'Kami menghormati hak kekayaan intelektual. Jika Anda yakin konten di platform kami melanggar hak cipta, kirimkan notifikasi DMCA ke legal@indopensource.org dengan informasi lengkap.', - link: 'mailto:legal@indopensource.org', - isExternal: true - }, - { - icon: 'ph-trademark', - title: 'Merek Dagang', - desc: 'Nama "IndopenSource", logo, dan identitas visual adalah merek komunitas. Penggunaan harus mendapat izin tertulis. Proyek yang terkurasi tetap memegang merek mereka sendiri.', - link: 'mailto:admin@indopensource.org', - isExternal: true - }, { icon: 'ph-user-circle', title: 'Privasi & Data', - desc: 'Kami tidak mengumpulkan data pribadi beyond yang diperlukan untuk operasional (GitHub username untuk kontribusi). Tidak ada tracking analytics pihak ketiga. Cookie hanya untuk fungsionalitas dasar.', - link: '#', - isExternal: false + desc: 'Website publik ini dapat dibaca tanpa akun atau formulir data pribadi. Saat mengikuti tautan ke GitHub atau layanan lain, kebijakan layanan tersebut berlaku.', + link: 'https://github.com/IndopenSource/indopensource.org', + isExternal: true } ]; const disclaimers = [ - 'IndopenSource adalah inisiatif komunitas, bukan entitas hukum formal.', 'Konten yang dikurasi adalah tanggung jawab masing-masing maintainer proyek.', 'Kami tidak memberikan nasihat hukum atau lisensi profesional.', 'Tautan ke proyek eksternal tidak merupakan endorsement.', - 'Komunitas berhak menolak atau menghapus konten yang melanggar kode etik.' + 'Entri direktori dapat diperbarui atau dihapus jika tidak lagi memenuhi aturan kontribusi.' ]; --- diff --git a/src/pages/press.astro b/src/pages/press.astro index febd715..598c4a9 100644 --- a/src/pages/press.astro +++ b/src/pages/press.astro @@ -1,89 +1,46 @@ --- import BaseLayout from '../layouts/BaseLayout.astro'; -import BaseButton from '../components/BaseButton.astro'; import PageHeader from '../components/PageHeader.astro'; import { withBase } from '../lib/urls'; import projectsData from '../data/projects.json'; -import legacyData from '../data/legacy.json'; -import revivalData from '../data/revival.json'; - -const allProjects = [...(projectsData as any[]), ...(legacyData as any[]), ...(revivalData as any[])]; -const totalProjects = allProjects.length; -const totalStars = allProjects.reduce((sum, p) => sum + (p.stars ?? 0), 0); -const totalForks = allProjects.reduce((sum, p) => sum + (p.forks ?? 0), 0); - -const brandAssets = [ - { name: 'Logo Utama (SVG)', desc: 'Format vektor untuk print & digital', icon: 'ph-file-svg', link: '/brand/indopensource-logo.svg', available: false }, - { name: 'Logo Utama (PNG)', desc: 'Resolusi tinggi, background transparan', icon: 'ph-file-png', link: '/brand/indopensource-logo.png', available: true }, - { name: 'Icon Only', desc: 'Mark tanpa teks untuk avatar & favicon', icon: 'ph-image', link: '/brand/indopensource-icon.svg', available: false }, - { name: 'Brand Guidelines', desc: 'Panduan penggunaan logo & warna', icon: 'ph-book-open', link: '/brand/brand-guidelines.pdf', available: false } -]; - -const colorPalette = [ - { name: 'Brand Green', hex: '#4d7c0f', tailwind: 'bg-[#4d7c0f]', usage: 'Primary brand color' }, - { name: 'Brand Dark', hex: '#365314', tailwind: 'bg-[#365314]', usage: 'Dark variant' }, - { name: 'Sun', hex: '#ecfccb', tailwind: 'bg-[#ecfccb]', usage: 'Highlight & accent' }, - { name: 'Ink', hex: '#101828', tailwind: 'bg-[#101828]', usage: 'Text & dark backgrounds' }, - { name: 'Paper', hex: '#ffffff', tailwind: 'bg-[#ffffff]', usage: 'Light backgrounds' }, - { name: 'Canvas', hex: '#f8fafc', tailwind: 'bg-[#f8fafc]', usage: 'Subtle backgrounds' } -]; - -const pressCoverage = [ - { title: 'IndopenSource: Menghidupkan Kembali Proyek Open Source Indonesia', source: 'Tech in Asia', date: '2025', link: '#' }, - { title: 'Kolaborasi Komunitas OSS Indonesia Melalui IndopenSource', source: 'Kompas Tech', date: '2025', link: '#' }, - { title: 'Direktori Proyek Open Source Buatan Anak Bangsa', source: 'IDN Times', date: '2025', link: '#' } -]; +const projects = (projectsData as any[]).filter((project) => !project.syncFailed); +const totalStars = projects.reduce((sum, project) => sum + (project.stars ?? 0), 0); +const totalForks = projects.reduce((sum, project) => sum + (project.forks ?? 0), 0); const keyFacts = [ - { label: 'Didirikan', value: 'Maret 2025' }, - { label: 'Proyek terkurasi', value: `${totalProjects}+` }, - { label: 'Total bintang GitHub', value: totalStars.toLocaleString('id-ID') }, - { label: 'Fork komunitas', value: totalForks.toLocaleString('id-ID') }, - { label: 'Kontributor aktif', value: '100+' }, - { label: 'Website', value: 'indopensource.org' } + { label: 'Proyek tersinkron', value: projects.length.toLocaleString('id-ID') }, + { label: 'Akumulasi stars', value: totalStars.toLocaleString('id-ID') }, + { label: 'Akumulasi forks', value: totalForks.toLocaleString('id-ID') } ]; - const contact = [ - { type: 'Umum', email: 'admin@indopensource.org', desc: 'Pertanyaan umum & kemitraan' }, - { type: 'Media', email: 'press@indopensource.org', desc: 'Wawancara, liputan, press release' }, - { type: 'Sponsor', email: 'sponsor@indopensource.org', desc: 'Sponsorship & dukungan finansial' } + { type: 'Umum', email: 'admin@indopensource.org', desc: 'Pertanyaan umum dan kemitraan' }, + { type: 'Media', email: 'press@indopensource.org', desc: 'Wawancara dan kebutuhan media' }, + { type: 'Sponsor', email: 'sponsor@indopensource.org', desc: 'Sponsorship dan dukungan finansial' } ]; --- - - -

Halaman ini menyediakan brand assets, fakta kunci, dan informasi kontak untuk jurnalis, blogger, dan organisasi yang ingin meliput atau bermitra dengan IndopenSource.

+ + +

Gunakan ringkasan, data direktori, dan logo yang tersedia di halaman ini. Statistik mengikuti snapshot data proyek terbaru.

- Media kit -

Semua yang Anda butuhkan untuk memberitakan IndopenSource.

+ Tentang IndopenSource +

Ruang terbuka untuk menemukan, mempelajari, dan merawat proyek open source Indonesia.

-
-
- Brand assets -

Logo & identitas visual.

-

Gunakan asset ini sesuai brand guidelines. Untuk penggunaan khusus, hubungi tim media.

+
+
+ Logo tersedia +

Logo IndopenSource.

+

Saat ini hanya logo PNG berikut yang disediakan sebagai aset resmi situs.

+ Download PNG →
-
- {brandAssets.map((asset) => ( -
-
- -
-

{asset.name}

-

{asset.desc}

- {asset.available ? ( - Download → - ) : ( - Segera hadir - )} -
- ))} +
+ Logo IndopenSource
@@ -91,33 +48,13 @@ const contact = [
- Color palette -

Warna brand IndopenSource.

-
-
- {colorPalette.map((color) => ( -
-
-
-

{color.name}

-

{color.hex}

-

{color.usage}

-
-
- ))} -
-
-
- -
-
-
- Fakta kunci -

Angka & informasi penting.

+ Snapshot direktori +

Angka yang dapat diperiksa.

+

Hanya menghitung repo utama yang berhasil disinkronkan; koleksi Legacy dan Revival tidak digabungkan.

-
+
{keyFacts.map((fact) => ( -
+

{fact.label}

{fact.value}

@@ -126,36 +63,15 @@ const contact = [
-
-
-
- Liputan media -

IndopenSource di berita.

-
-
- {pressCoverage.map((item) => ( - - ))} -
-
-
-
-
- Kontak media -

Hubungi tim kami.

-
+
Kontak

Hubungi kami.

- {contact.map((c) => ( + {contact.map((item) => ( ))}
diff --git a/src/pages/projects.astro b/src/pages/projects.astro index 2ce12db..e1b4e20 100644 --- a/src/pages/projects.astro +++ b/src/pages/projects.astro @@ -3,6 +3,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import PageHeader from '../components/PageHeader.astro'; import ProjectsDirectory from '../components/ProjectsDirectory.astro'; import type { Project } from '../lib/projects'; +import { withBase } from '../lib/urls'; import projectsData from '../data/projects.json'; import legacyData from '../data/legacy.json'; import revivalData from '../data/revival.json'; @@ -27,6 +28,25 @@ const latestUpdate = refreshedProjects const latestUpdateLabel = latestUpdate ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'medium' }).format(new Date(latestUpdate)) : ''; +const ownerAvatarUrl = (owner: string) => `https://avatars.githubusercontent.com/${encodeURIComponent(owner)}?size=80`; +const usersByOwner = new Map(); +for (const project of refreshedProjects) { + if (!project.owner) continue; + const user = usersByOwner.get(project.owner) || { + owner: project.owner, + avatarUrl: project.ownerAvatarUrl || ownerAvatarUrl(project.owner), + repositories: 0, + stars: 0, + forks: 0 + }; + user.repositories += 1; + user.stars += project.stars; + user.forks += project.forks; + usersByOwner.set(project.owner, user); +} +const topUsers = [...usersByOwner.values()] + .sort((a, b) => b.stars - a.stars || b.repositories - a.repositories || a.owner.localeCompare(b.owner)) + .slice(0, 5); ---
+ +
+
+
+
+ Top user +

Lima penggerak teratas.

+
+ + Lihat semua + +
+ +
+
{legacyProjects.length > 0 && ( @@ -74,6 +122,7 @@ const latestUpdateLabel = latestUpdate

Resource dan proyek non-app open source Indonesia yang masih relevan sebagai referensi pembelajaran.

+ Buka repository Legacy
{legacyProjects.map((repo) => ( @@ -84,7 +133,13 @@ const latestUpdateLabel = latestUpdate rel="noopener noreferrer" >
-

{repo.name}

+
+ +
+

{repo.name}

+ oleh {repo.owner} +
+

{repo.description || 'Tidak ada deskripsi'}

@@ -109,6 +164,7 @@ const latestUpdateLabel = latestUpdate

Proyek open source Indonesia yang butuh kontribusi untuk aktif kembali.

+ Buka repository Revival
{revivalProjects.map((repo) => ( @@ -119,7 +175,13 @@ const latestUpdateLabel = latestUpdate rel="noopener noreferrer" >
-

{repo.name}

+
+ +
+

{repo.name}

+ oleh {repo.owner} +
+

{repo.description || 'Tidak ada deskripsi'}

diff --git a/src/pages/roadmap.astro b/src/pages/roadmap.astro new file mode 100644 index 0000000..18c3545 --- /dev/null +++ b/src/pages/roadmap.astro @@ -0,0 +1,38 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import BaseButton from '../components/BaseButton.astro'; +import PageHeader from '../components/PageHeader.astro'; +import { roadmapPeriods } from '../data/roadmap'; +--- + + + +

Rencana kerja publik untuk menjaga direktori, pengetahuan, dukungan proyek, dan transparansi komunitas. Urutan dapat berubah mengikuti kebutuhan dan kapasitas kontributor.

+
+ +
+
+
    + {roadmapPeriods.map((item, index) => ( +
  1. +
    + {item.period} + +
    +

    {item.title}

    +
      + {item.items.map((entry) =>
    • {entry}
    • )} +
    +
  2. + ))} +
+
+
+

Roadmap ini milik komunitas.

+

Usulkan perubahan, tawarkan bantuan, atau ceritakan kebutuhan proyekmu melalui kanal komunitas.

+
+
Buka diskusi
+
+
+
+
diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts index 730929c..40d42cf 100644 --- a/src/pages/sitemap.xml.ts +++ b/src/pages/sitemap.xml.ts @@ -37,6 +37,10 @@ const staticPages: SitemapEntry[] = [ { path: '/projects/' }, { path: '/blog/' }, { path: '/forum/' }, + { path: '/roadmap/' }, + { path: '/tentang/' }, + { path: '/komunitas/' }, + { path: '/donasi/' }, { path: '/contact/' } ]; diff --git a/src/pages/sponsors.astro b/src/pages/sponsors.astro index a55a42e..b73f762 100644 --- a/src/pages/sponsors.astro +++ b/src/pages/sponsors.astro @@ -1,165 +1,3 @@ --- -import BaseLayout from '../layouts/BaseLayout.astro'; -import BaseButton from '../components/BaseButton.astro'; -import PageHeader from '../components/PageHeader.astro'; -import { withBase } from '../lib/urls'; - -const tiers = [ - { - name: 'Platinum', - min: 'Rp 50.000.000+/tahun', - benefits: [ - 'Logo di homepage & footer website', - 'Sebutan di semua press release', - 'Booth di event komunitas', - 'Akses prioritas ke talent pool', - 'Co-branding di materi marketing' - ], - highlight: true - }, - { - name: 'Gold', - min: 'Rp 20.000.000/tahun', - benefits: [ - 'Logo di halaman sponsors', - 'Sebutan di newsletter bulanan', - 'Tiket VIP event komunitas', - 'Akses ke laporan tahunan' - ], - highlight: false - }, - { - name: 'Silver', - min: 'Rp 5.000.000/tahun', - benefits: [ - 'Logo di halaman sponsors', - 'Sebutan di media sosial', - 'Tiket event komunitas' - ], - highlight: false - }, - { - name: 'Supporter', - min: 'Rp 500.000/tahun', - benefits: [ - 'Nama di halaman sponsors', - 'Stiker eksklusif', - 'Badge "Supporter" di GitHub' - ], - highlight: false - } -]; - -const currentSponsors = [ - { name: 'Tokengratis.id', tier: 'Platinum', since: '2025', logo: '/brand/tokengratis.svg' }, - { name: 'IDCloudHost', tier: 'Gold', since: '2025', logo: '/brand/idcloudhost.svg' }, - { name: 'Komunitas OSS Indonesia', tier: 'Silver', since: '2025', logo: '/brand/komunitas.svg' } -]; - -const impact = [ - { label: 'Proyek terkurasi', value: '69+' }, - { label: 'Total bintang GitHub', value: '5.361' }, - { label: 'Kontributor aktif', value: '100+' }, - { label: 'Artikel & tutorial', value: '50+' } -]; +return Astro.redirect('/donasi/', 301); --- - - - -

IndopenSource dijalankan oleh komunitas sukarela. Sponsorship membantu kami merawat infrastruktur, mengadakan event, dan mendukung proyek-proyek potensial.

-
- -
-
- Dampak bersama -

Setiap kontribusi membantu menjaga open source Indonesia tetap hidup dan berkembang.

-
-
- -
-
-
- Dampak 2025 -

Apa yang sudah kita capai.

-
-
- {impact.map((item) => ( -
-

{item.value}

-

{item.label}

-
- ))} -
-
-
- -
-
-
- Pendukung kami -

Organisasi yang mendukung.

-

Terima kasih kepada organisasi yang telah mendukung misi IndopenSource.

-
-
- {currentSponsors.map((s) => ( -
-
-
- {s.logo ? ( - {s.name - ) : ( - - )} -
-
-

{s.name}

-

{s.tier} · Sejak {s.since}

-
-
-
- ))} -
-
-
- -
-
-
- Paket sponsorship -

Pilih tingkat dukungan Anda.

-

Semua paket memberikan akses ke laporan dampak dan apresiasi publik.

-
-
- {tiers.map((tier) => ( -
- {tier.highlight && Populer} -

{tier.name}

-

{tier.min}

-
    - {tier.benefits.map((b) => ( -
  • - - {b} -
  • - ))} -
-
- ))} -
-
-
- -
-
-
- Tertarik bermitra? -

Mari diskusikan kemitraan Anda.

-

Kami terbuka untuk paket kustom, sponsorship event, atau bentuk kolaborasi lain yang sejalan dengan misi open source.

-
- Hubungi Tim Sponsor - Media Kit -
-
-
-
-
diff --git a/src/pages/tentang.astro b/src/pages/tentang.astro index 031045b..3fa8346 100644 --- a/src/pages/tentang.astro +++ b/src/pages/tentang.astro @@ -17,5 +17,11 @@ const focus = [
Arah kerja

Open source menjadi lebih kuat ketika jejak kerjanya mudah ditemukan dan diteruskan.

-
01 / peta proyek02 / pengetahuan terbuka03 / kerja komunitas
{focus.map(([title, body], index) =>
0{index + 1}

{title}

{body}

)}
Masuk ke komunitas
+
+
+
01 / peta proyek02 / pengetahuan terbuka03 / kerja komunitas
+
{focus.map(([title, body], index) =>
0{index + 1}

{title}

{body}

)}
+
Lihat direktori komunitasLihat roadmapHubungi kami
+
+
diff --git a/src/pages/users.astro b/src/pages/users.astro new file mode 100644 index 0000000..3c702af --- /dev/null +++ b/src/pages/users.astro @@ -0,0 +1,120 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import PageHeader from '../components/PageHeader.astro'; +import type { Project } from '../lib/projects'; +import projectsData from '../data/projects.json'; + +interface UserRanking { + owner: string; + avatarUrl: string; + repositories: number; + stars: number; + forks: number; +} + +const projects = (projectsData as Project[]).filter((project) => !project.syncFailed && project.owner); +const usersByOwner = new Map(); + +for (const project of projects) { + const owner = project.owner as string; + const current = usersByOwner.get(owner) || { + owner, + avatarUrl: project.ownerAvatarUrl || `https://avatars.githubusercontent.com/${encodeURIComponent(owner)}?size=160`, + repositories: 0, + stars: 0, + forks: 0 + }; + + current.repositories += 1; + current.stars += project.stars; + current.forks += project.forks; + usersByOwner.set(owner, current); +} + +const users = [...usersByOwner.values()].sort((a, b) => + b.stars - a.stars || b.repositories - a.repositories || a.owner.localeCompare(b.owner) +); +const topUsers = users.slice(0, 3); +const totalStars = users.reduce((sum, user) => sum + user.stars, 0); +const formatNumber = (value: number) => value.toLocaleString('id-ID'); +--- + + + +

+ Peringkat pemilik dan organisasi berdasarkan akumulasi repo yang tercatat di direktori IndopenSource. + Angka ini bukan peringkat seluruh aktivitas GitHub. +

+
+
+ User & organisasi + {formatNumber(users.length)} +
+
+ Repo terhitung + {formatNumber(projects.length)} +
+
+ Akumulasi stars + {formatNumber(totalStars)} +
+
+
+ +
+
+
+ Podium +

Tiga teratas.

+
+ + +
+ + + + + + + + + + + + {users.map((user, index) => ( + + + + + + + + ))} + +
#User / organisasiRepoStarsForks
{index + 1} + + + {user.owner} + + {formatNumber(user.repositories)}{formatNumber(user.stars)}{formatNumber(user.forks)}
+
+

Dihitung dari snapshot data direktori terbaru · repo yang gagal disinkronkan tidak disertakan.

+
+
+
From 3a21d16d827dd23f97408ddeaada9c3722633013 Mon Sep 17 00:00:00 2001 From: wauputr4 <103489788+wauputr4@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:18:26 +0700 Subject: [PATCH 2/9] docs: align release and contribution guides --- .github/PULL_REQUEST_TEMPLATE.md | 7 ++++ CONTRIBUTING.md | 25 +++++++++++-- README.md | 64 +++++++++++++++++--------------- docs/release-checklist.md | 30 +++++++++++++++ 4 files changed, 93 insertions(+), 33 deletions(-) create mode 100644 docs/release-checklist.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 85ffb02..841b490 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,7 +4,14 @@ ## Validation +- [ ] `npm test` - [ ] `npm run build` +- [ ] Cloudflare branch preview checked on desktop and mobile + +## Content checks + +- [ ] Claims, organizations, sponsors, and metrics have a verifiable source +- [ ] New internal links work with `ASTRO_BASE` ## Notes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f701b9..38d325b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,13 +6,13 @@ Thanks for helping improve IndopenSource.org. ```bash npm install -npm run sync:projects npm run dev ``` Before opening a pull request: ```bash +npm test npm run build ``` @@ -35,13 +35,32 @@ Use `GITHUB_TOKEN` or `GH_TOKEN` to avoid low GitHub API rate limits. `main` commit to production. - Keep changes focused. - Prefer Astro components in `src/components` over large page-only markup. -- Keep internal links compatible with GitHub Pages base paths by using `withBase()`. -- Run `npm run build` and include the result in the PR description. +- Use `withBase()` for internal links that must remain valid when `ASTRO_BASE` + is set by CI or a preview environment. +- Run `npm test` and `npm run build`, then include the results in the PR description. - For content or roadmap changes, link the relevant issue or discussion. +## Community Directory + +The Community page reads `src/data/communities.json`. To propose one community, +add one object containing: + +- `name` +- `description` +- `url` pointing to an official channel +- optional `focus` +- optional `location` + +Keep one community per pull request and explain your relationship to it or add a +source that reviewers can verify. Do not add inferred or inactive communities. + ## Production Release The `release` branch is reserved for Cloudflare production deployments. Only IndopenSource organization members may update it, through a pull request from `main` that passes CI. Merge production promotion pull requests with a merge commit so `main` remains in the `release` history. + +Cloudflare creates a branch preview for contribution pull requests. Review that +preview before promoting `main` to `release`; use +[`docs/release-checklist.md`](docs/release-checklist.md) for the production pass. diff --git a/README.md b/README.md index ddba074..ffc2177 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,40 @@ # IndopenSource.org -[![Deploy to GitHub Pages](https://github.com/IndopenSource/indopensource.org/actions/workflows/deploy-pages.yml/badge.svg)](https://github.com/IndopenSource/indopensource.org/actions/workflows/deploy-pages.yml) +[![CI](https://github.com/IndopenSource/indopensource.org/actions/workflows/ci.yml/badge.svg)](https://github.com/IndopenSource/indopensource.org/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -Website roadmap untuk `https://indopensource.org`. +Website komunitas dan direktori open source Indonesia di `https://indopensource.org`. ## Tech Stack - Astro - Tailwind CSS - TypeScript -- GitHub Pages - GitHub Actions +- Cloudflare Workers Static Assets ## Pages -- Home: ringkasan roadmap dan arah website. +- Home: pintu masuk ke direktori, roadmap, pembelajaran, dan kontribusi. - Falsafah: prinsip komunitas dan kurasi open source Indonesia. -- Projects: direktori dari `IndopenSource/awesome-indonesia`. -- Blog: placeholder untuk repo khusus `IndopenSource/Blog-IndopenSource`. -- Forum: rencana integrasi `https://github.com/orgs/IndopenSource/discussions`. -- Contact: kanal GitHub organization, projects, dan discussions. +- Projects: direktori proyek dari `IndopenSource/awesome-indonesia`. +- Top User: peringkat pemilik repository berdasarkan akumulasi stars. +- Komunitas: direktori komunitas yang diusulkan melalui pull request. +- Roadmap: rencana kerja publik 2026–2027. +- Belajar dan Blog: materi serta artikel yang disinkronkan dari repository sumber. +- Donasi: informasi OpenSawer, sponsorship, CSR, dan dukungan non-dana. +- Forum dan Kontak: GitHub Discussions serta kanal resmi IndopenSource. ## Development ```bash npm install -npm run sync:projects npm run dev ``` +Sinkronisasi data bersifat opsional untuk pengembangan UI. File hasil sinkronisasi +yang diperlukan sudah tersedia di `src/data/`. + ## Project Structure ```txt @@ -45,38 +50,36 @@ src/ ## Checks ```bash +npm test npm run check npm run build ``` -## MVP Pre-release - -MVP awal berfokus pada homepage sebagai pintu masuk roadmap IndopenSource: - -- Home menjelaskan posisi `indopensource.org`. -- Projects sudah punya data awal dari `awesome-indonesia`. -- Falsafah, Blog, Forum, dan Contact tersedia sebagai halaman roadmap. -- Deployment memakai GitHub Pages bawaan repo lewat GitHub Actions. - -Rilis pre-release bisa dibuat dari tag `v0.1.0-mvp` setelah workflow Pages hijau. -URL produksi setelah custom domain aktif adalah `https://indopensource.org/`. - ## Deployment -Deployment saat ini masih memakai GitHub Pages melalui workflow -`.github/workflows/deploy-pages.yml`. Cloudflare production berikutnya akan -dibangun dari branch `release`. +Cloudflare terhubung langsung ke repository dan membuat preview untuk pull +request atau branch non-production. Production hanya dibangun dari branch +`release`. - Build command: `npm run build` +- Deploy command: `npx wrangler deploy` - Output directory: `dist` -- Source: GitHub Actions +- Konfigurasi Worker: `wrangler.jsonc` +- Production branch: `release` - Custom domain: `indopensource.org` -- Pages base path: `/` -Semua kontribusi tetap masuk ke `main`. Rilis produksi dipromosikan oleh anggota -organisasi melalui pull request dari `main` ke `release`; jangan membuka pull +Semua kontribusi masuk ke `main`. Anggota organisasi mempromosikan versi yang +sudah diuji melalui pull request dari `main` ke `release`; jangan membuka pull request kontribusi langsung ke `release`. +Workflow GitHub Pages lama masih berada di `.github/workflows/deploy-pages.yml` +dan bukan sumber deployment production Cloudflare. Putuskan penghapusan atau +perannya sebagai fallback sebelum release publik agar tidak ada dua jalur deploy +yang dianggap aktif. + +Lihat [checklist release](docs/release-checklist.md) sebelum mempromosikan +`main` ke `release`. + ## Project Sync `npm run sync:projects` membaca `repos.json` dari `IndopenSource/awesome-indonesia`, @@ -103,8 +106,9 @@ Workflow `.github/workflows/sync-content.yml` memperbarui data secara otomatis. - `sync-content` Jika data berubah, workflow membuat commit `Sync content data`. Setelah commit -masuk `main`, workflow yang sama akan upload artifact dan deploy ulang GitHub -Pages agar perubahan data langsung tayang. +masuk `main`, perubahan mengikuti validasi dan alur promosi `main` ke `release`. +Workflow ini masih memuat langkah deployment GitHub Pages lama; jangan menganggap +hasil tersebut sebagai production Cloudflare. ## Contributing diff --git a/docs/release-checklist.md b/docs/release-checklist.md new file mode 100644 index 0000000..92459a1 --- /dev/null +++ b/docs/release-checklist.md @@ -0,0 +1,30 @@ +# Release checklist + +Production is promoted from `main` to `release` through a pull request opened by +an IndopenSource organization member. + +## Before promotion + +- [ ] Contribution pull requests are merged into `main` and CI is green. +- [ ] Cloudflare branch preview has been checked on desktop and mobile. +- [ ] `npm test` and `npm run build` pass from the intended release commit. +- [ ] Navigation, project detail pages, `/donasi/`, `/komunitas/`, `/roadmap/`, + `/sitemap.xml`, and the `/sponsors/` redirect have been checked. +- [ ] `admin@`, `press@`, `sponsor@`, and `legal@indopensource.org` can receive + mail, or links to inactive addresses have been removed. +- [ ] DNS, custom domain, TLS, and the Cloudflare production branch are correct. +- [ ] The role of the legacy GitHub Pages workflows has been decided. + +## Promote + +1. Open a pull request from `main` to `release`. +2. Require green CI and review the diff for unexpected generated data. +3. Merge with a merge commit so `main` remains in the `release` history. +4. Wait for the Cloudflare production deployment to pass. + +## After deployment + +- [ ] Smoke-test `https://indopensource.org/` and the primary navigation. +- [ ] Confirm canonical URLs, robots, sitemap, favicon, and social previews. +- [ ] Send a test message to each published email address. +- [ ] Record the production commit or tag as the rollback point. From ea9f7e1d9540d0986e66420ec1afa60174d0d667 Mon Sep 17 00:00:00 2001 From: wauputr4 <103489788+wauputr4@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:23:35 +0700 Subject: [PATCH 3/9] fix: address release review findings --- src/components/DonationPage.astro | 4 ++-- src/lib/projects.ts | Bin 3861 -> 4851 bytes src/pages/contact.astro | 7 ++++--- src/pages/index.astro | 19 ++---------------- src/pages/legal.astro | 2 +- src/pages/projects.astro | 21 ++------------------ src/pages/sitemap.xml.ts | 11 ++++++++++ src/pages/sponsors.astro | 2 +- src/pages/users.astro | 32 ++---------------------------- test/lib.test.mjs | 18 ++++++++++++++++- 10 files changed, 42 insertions(+), 74 deletions(-) diff --git a/src/components/DonationPage.astro b/src/components/DonationPage.astro index 31962f2..752b9db 100644 --- a/src/components/DonationPage.astro +++ b/src/components/DonationPage.astro @@ -49,7 +49,7 @@ const tiers = [ ]; const currentSponsors = [ - { name: 'Tokengratis.id', since: '2026', url: 'https://tokengratis.id/' } + { name: 'Tokengratis.id', since: '2026', url: 'https://tokengratis.id/', logo: withBase('/brand/tokengratis-icon.png') } ]; const openSawerIcon = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='64'%20height='64'%20viewBox='0%200%2040%2040'%3e%3ctitle%3eOpenSawer%3c/title%3e%3ccircle%20cx='20'%20cy='20'%20r='20'%20fill='%230f694e'/%3e%3ccircle%20cx='20'%20cy='11.5'%20r='3.2'%20fill='%23e8b83f'/%3e%3cpath%20d='M10.5%2019.5c1.4%207.2%205%2010.8%209.5%2010.8s8.1-3.6%209.5-10.8M12%2019.5h16'%20fill='none'%20stroke='%23fff'%20stroke-width='3.2'%20stroke-linecap='round'/%3e%3c/svg%3e"; @@ -162,7 +162,7 @@ const openSawerIcon = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000
- +

{s.name}

diff --git a/src/lib/projects.ts b/src/lib/projects.ts index 43c52ec10a66885771a8cf8c22a32ba502a74cec..77e5937761e358ef10943e021a99b5b8b1055a1a 100644 GIT binary patch delta 973 zcmY*YO>5gg5Iq#y;7cz(mno!Fa-DT@>A|UELLg0{r6FJ_7=u^JNZwlB6}v0DPGa;U zO8-ZpKdpbDvnwf)Zrb^H^WK}$@7Mp{{&{sU!=Jg*7KF5@6CNW>4eA>%S3;(6-vdxv ziFyRaYKojvfN!|vdaAQ_P-Cu)uu2OwBap>9LjCLev%_-V>ka#TI8RfJDYpn36OCqx zw~%Y~17m9-+QBDbuZrlMftVXS64Ia~II+QMK4dBtl0kpis|hECjIB@-G;zJIkiI#p zCTMU+aIH%}Vm1#)ZDF&ymli8&EEFWzz!)TM;VaKSR!Rr$`m-QB?UD3EX$Y#WdI+fG zQAg-O0Ym}~2UeP$j4!#!Nbkb~ys6umcQpjUvO*TWkWBC*2BYll(D+S>CNX0wa%5KM39%&W@^3ri4k`=KKXFGXee|K=eP+QIx#w=$2U2@ za&J$3Upe3yuy#MZAHzv2M<6AwmZFV158JBnu$g_ba5 zj8)!mcvheZ!Z|baKLr6FKor7wapoCE%uj|R;Pn?78+FOMTt^L+@X9!rshDTzeB~O0 PsJ4@!!@n;Np1=4H2SrAe delta 12 TcmeyYI#q6i2*>76+>Go1A(RAA diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 6f71501..c65b5e0 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -2,6 +2,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import PageHeader from '../components/PageHeader.astro'; import LightSvgMotion from '../components/LightSvgMotion.astro'; +import { withBase } from '../lib/urls'; // Contact channels as a zine "rolodex". Each entry is a printed card: sticker // label + mono handle + the (verbatim, externally-linked) destination. Logic is @@ -76,8 +77,8 @@ const channels = [ class={`riso-hover riso-shadow group flex min-h-44 flex-col justify-between border-2 border-ink bg-paper p-5 ${ i === 0 ? 'lg:col-span-2 lg:bg-brand lg:text-paper' : '' }`} - href={c.href} - rel="noopener noreferrer" + href={c.href.startsWith('http') ? c.href : withBase(c.href)} + rel={c.href.startsWith('http') ? 'noopener noreferrer' : undefined} >
{c.tag} @@ -102,7 +103,7 @@ const channels = [ }`} > {c.label} - (situs eksternal) + {c.href.startsWith('http') && (situs eksternal)}
diff --git a/src/pages/index.astro b/src/pages/index.astro index 470ff9d..cd47e24 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -10,29 +10,14 @@ import LightSvgMotion from '../components/LightSvgMotion.astro'; import projectsData from '../data/projects.json'; import { roadmapPeriods } from '../data/roadmap'; import { heroImage, HERO_PRELOAD_WIDTH, HERO_QUALITY } from '../lib/hero'; -import type { Project } from '../lib/projects'; +import { rankProjectOwners, type Project } from '../lib/projects'; import { withBase } from '../lib/urls'; const projects = projectsData as Project[]; const totalProjects = projects.length; const topProjects = projects.slice(0, 5); const roadmapPreview = roadmapPeriods.slice(0, 2); -const usersByOwner = new Map(); -for (const project of projects.filter((item) => !item.syncFailed && item.owner)) { - const owner = project.owner as string; - const user = usersByOwner.get(owner) || { - owner, - avatarUrl: project.ownerAvatarUrl || `https://avatars.githubusercontent.com/${encodeURIComponent(owner)}?size=80`, - repositories: 0, - stars: 0 - }; - user.repositories += 1; - user.stars += project.stars; - usersByOwner.set(owner, user); -} -const topUsers = [...usersByOwner.values()] - .sort((a, b) => b.stars - a.stars || b.repositories - a.repositories || a.owner.localeCompare(b.owner)) - .slice(0, 5); +const topUsers = rankProjectOwners(projects).slice(0, 5); // Pre-generate the AVIF the hero will paint as its LCP candidate so we // can the exact same /_astro/* file from . Same src, diff --git a/src/pages/legal.astro b/src/pages/legal.astro index e2ba45c..643722a 100644 --- a/src/pages/legal.astro +++ b/src/pages/legal.astro @@ -67,7 +67,7 @@ const disclaimers = [

{p.title}

{p.desc}

- Selengkapnya → + Selengkapnya →
diff --git a/src/pages/projects.astro b/src/pages/projects.astro index e1b4e20..296da95 100644 --- a/src/pages/projects.astro +++ b/src/pages/projects.astro @@ -2,7 +2,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import PageHeader from '../components/PageHeader.astro'; import ProjectsDirectory from '../components/ProjectsDirectory.astro'; -import type { Project } from '../lib/projects'; +import { rankProjectOwners, type Project } from '../lib/projects'; import { withBase } from '../lib/urls'; import projectsData from '../data/projects.json'; import legacyData from '../data/legacy.json'; @@ -29,24 +29,7 @@ const latestUpdateLabel = latestUpdate ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'medium' }).format(new Date(latestUpdate)) : ''; const ownerAvatarUrl = (owner: string) => `https://avatars.githubusercontent.com/${encodeURIComponent(owner)}?size=80`; -const usersByOwner = new Map(); -for (const project of refreshedProjects) { - if (!project.owner) continue; - const user = usersByOwner.get(project.owner) || { - owner: project.owner, - avatarUrl: project.ownerAvatarUrl || ownerAvatarUrl(project.owner), - repositories: 0, - stars: 0, - forks: 0 - }; - user.repositories += 1; - user.stars += project.stars; - user.forks += project.forks; - usersByOwner.set(project.owner, user); -} -const topUsers = [...usersByOwner.values()] - .sort((a, b) => b.stars - a.stars || b.repositories - a.repositories || a.owner.localeCompare(b.owner)) - .slice(0, 5); +const topUsers = rankProjectOwners(refreshedProjects).slice(0, 5); --- !project.syncFailed && project.owner); -const usersByOwner = new Map(); - -for (const project of projects) { - const owner = project.owner as string; - const current = usersByOwner.get(owner) || { - owner, - avatarUrl: project.ownerAvatarUrl || `https://avatars.githubusercontent.com/${encodeURIComponent(owner)}?size=160`, - repositories: 0, - stars: 0, - forks: 0 - }; - - current.repositories += 1; - current.stars += project.stars; - current.forks += project.forks; - usersByOwner.set(owner, current); -} - -const users = [...usersByOwner.values()].sort((a, b) => - b.stars - a.stars || b.repositories - a.repositories || a.owner.localeCompare(b.owner) -); +const users = rankProjectOwners(projects); const topUsers = users.slice(0, 3); const totalStars = users.reduce((sum, user) => sum + user.stars, 0); const formatNumber = (value: number) => value.toLocaleString('id-ID'); diff --git a/test/lib.test.mjs b/test/lib.test.mjs index d570eba..e24024a 100644 --- a/test/lib.test.mjs +++ b/test/lib.test.mjs @@ -12,7 +12,7 @@ import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; -import { projectSlug } from '../src/lib/projects.ts'; +import { projectSlug, rankProjectOwners } from '../src/lib/projects.ts'; import { normalizeHref, safeHref } from '../src/lib/urls.ts'; import { isPublished, renderArticle, forbiddenTags } from '../src/lib/content.ts'; @@ -50,6 +50,22 @@ describe('projectSlug (slug)', () => { }); }); +describe('rankProjectOwners (project ranking)', () => { + it('groups case-insensitive owners and ignores failed sync records', () => { + const users = rankProjectOwners([ + { owner: 'Example', stars: 2, forks: 1, fullName: 'Example/a', description: '', url: '' }, + { owner: 'example', stars: 3, forks: 2, fullName: 'example/b', description: '', url: '' }, + { owner: 'Other', stars: 99, forks: 99, fullName: 'Other/c', description: '', url: '', syncFailed: true } + ]); + + assert.equal(users.length, 1); + assert.deepEqual( + { owner: users[0].owner, repositories: users[0].repositories, stars: users[0].stars, forks: users[0].forks }, + { owner: 'Example', repositories: 2, stars: 5, forks: 3 } + ); + }); +}); + describe('normalizeHref (urls)', () => { it('leaves absolute http(s) URLs untouched', () => { assert.equal(normalizeHref('https://example.com/x'), 'https://example.com/x'); From 532725c87fbe6da259b70981995675d3e800a4b7 Mon Sep 17 00:00:00 2001 From: wauputr4 <103489788+wauputr4@users.noreply.github.com> Date: Mon, 20 Jul 2026 19:29:49 +0700 Subject: [PATCH 4/9] feat: show blog commit contributors --- README.md | 5 ++-- scripts/sync-blog-posts.mjs | 16 ++++++++++++ src/components/SiteHeader.astro | 2 +- src/data/blog-posts.json | 16 ++++++++++++ src/lib/content.ts | 2 ++ src/pages/blog/[slug].astro | 43 +++++++++++++++++++-------------- 6 files changed, 63 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ffc2177..ba07dc9 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,9 @@ Gunakan `GITHUB_TOKEN` atau `GH_TOKEN` untuk rate limit yang lebih lega. ## Blog Sync `npm run sync:blog` membaca artikel Markdown dari -`IndopenSource/Blog-IndopenSource`, mengambil metadata commit penulis/rilis, -dan menulis hasilnya ke `src/data/blog-posts.json`. +`IndopenSource/Blog-IndopenSource`, mengambil tanggal commit pertama, commit +terbaru, dan seluruh kontributor unik setiap artikel, lalu menulis hasilnya ke +`src/data/blog-posts.json`. ## Auto Sync diff --git a/scripts/sync-blog-posts.mjs b/scripts/sync-blog-posts.mjs index 661eaac..83bcb99 100644 --- a/scripts/sync-blog-posts.mjs +++ b/scripts/sync-blog-posts.mjs @@ -161,6 +161,20 @@ async function getCommitMeta(path) { const firstCommit = commits.at(-1); const latestCommit = commits.at(0); const author = firstCommit?.author; + const authors = []; + const seenAuthors = new Set(); + + for (const item of commits.toReversed()) { + const name = item.author?.login || item.commit?.author?.name; + if (!name || seenAuthors.has(name.toLowerCase())) continue; + seenAuthors.add(name.toLowerCase()); + authors.push({ + name, + avatarUrl: item.author?.avatar_url || '', + url: item.author?.html_url || '', + committedAt: item.commit?.author?.date || '' + }); + } return { author: { @@ -169,6 +183,7 @@ async function getCommitMeta(path) { url: author?.html_url || firstCommit?.html_url || `https://github.com/${BLOG_REPO}`, committedAt: firstCommit?.commit?.author?.date || '' }, + authors, // The latest commit timestamp is the date of the most recent EDIT, not a // publication/release event (CC-9). Name it honestly as "last modified" so // the rendered label can stop calling an edit timestamp a release date. @@ -265,6 +280,7 @@ for (const path of articleFiles) { releasedAt: editorialDate || commitMeta.author.committedAt || commitMeta.lastModifiedAt, lastModifiedAt: commitMeta.lastModifiedAt, author: resolvedAuthor.author, + authors: commitMeta.authors.length ? commitMeta.authors : [resolvedAuthor.author], authorFromFrontmatter: resolvedAuthor.fromFrontmatter }); } diff --git a/src/components/SiteHeader.astro b/src/components/SiteHeader.astro index 9524fc7..2653ce3 100644 --- a/src/components/SiteHeader.astro +++ b/src/components/SiteHeader.astro @@ -17,7 +17,7 @@ const mobileNavItems = [ ['Proyek', '/projects/', 'ph-grid-four'], ['Blog', '/blog/', 'ph-article'], ['Belajar', '/belajar/', 'ph-books'], - ['FAQ', '/faq/', 'ph-question'] + ['Top User', '/users/', 'ph-ranking'] ]; const currentPath = Astro.url.pathname; diff --git a/src/data/blog-posts.json b/src/data/blog-posts.json index b39a31f..0b10189 100644 --- a/src/data/blog-posts.json +++ b/src/data/blog-posts.json @@ -25,6 +25,14 @@ "url": "https://github.com/wauputr4", "committedAt": "2026-07-16T11:02:36Z" }, + "authors": [ + { + "name": "wauputr4", + "avatarUrl": "https://avatars.githubusercontent.com/u/103489788?v=4", + "url": "https://github.com/wauputr4", + "committedAt": "2026-07-16T11:02:36Z" + } + ], "authorFromFrontmatter": false }, { @@ -51,6 +59,14 @@ "url": "https://github.com/wauputr4", "committedAt": "2026-06-13T17:37:55Z" }, + "authors": [ + { + "name": "wauputr4", + "avatarUrl": "https://avatars.githubusercontent.com/u/103489788?v=4", + "url": "https://github.com/wauputr4", + "committedAt": "2026-06-13T17:37:55Z" + } + ], "authorFromFrontmatter": false } ] diff --git a/src/lib/content.ts b/src/lib/content.ts index 62724f9..b027aa5 100644 --- a/src/lib/content.ts +++ b/src/lib/content.ts @@ -46,6 +46,8 @@ export interface BlogPost { /** ISO timestamp of the most recent edit commit; distinct from `releasedAt`. */ lastModifiedAt?: string; author: PostAuthor; + /** Unique contributors derived from the Markdown file's commit history. */ + authors?: PostAuthor[]; /** * Whether `author` was taken from frontmatter `authors[]` (true) or * derived from git commit metadata (false/undefined). Lets the UI label diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index e44748a..0b49b53 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -16,11 +16,13 @@ export function getStaticPaths() { const { post } = Astro.props; // Render Markdown through the shared sanitizing pipeline (no raw `set:html`). const html = renderArticle(post.content); -// Author link comes from frontmatter/git — allowlist the scheme before href. -const authorUrl = safeHref(post.author?.url); +const authors = post.authors?.length ? post.authors : post.author ? [post.author] : []; const authorDate = post.author?.committedAt ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(post.author.committedAt)) : ''; +const modifiedDate = post.lastModifiedAt + ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(post.lastModifiedAt)) + : ''; const releaseDate = post.releasedAt ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(post.releasedAt)) : ''; @@ -30,7 +32,7 @@ const heroAlt = `Thumbnail artikel: ${post.title}`; // Provenance-aware author byline: frontmatter `authors[]` vs git commit metadata. const authorByline = post.authorFromFrontmatter ? `Disebutkan di frontmatter artikel${authorDate ? ` (commit pertama ${authorDate})` : ''}.` - : `Diambil otomatis dari commit pertama file artikel${authorDate ? ` pada ${authorDate}` : ''}.`; + : `Kontributor diambil dari riwayat commit sumber Markdown${authorDate ? `. Terbit ${authorDate}` : ''}${modifiedDate ? ` · diperbarui ${modifiedDate}` : ''}.`; // BlogPosting structured data (passed to BaseLayout's hashed jsonLd prop so it // is CSP-allow-listed in , not a separate inline script). @@ -42,8 +44,8 @@ const jsonLd = { image: thumbnailUrl, ...(post.releasedAt ? { datePublished: post.releasedAt } : {}), ...(post.lastModifiedAt ? { dateModified: post.lastModifiedAt } : {}), - ...(post.author?.name - ? { author: { '@type': 'Person', name: post.author.name, ...(authorUrl ? { url: authorUrl } : {}) } } + ...(authors.length + ? { author: authors.map((author) => ({ '@type': 'Person', name: author.name, ...(safeHref(author.url) ? { url: safeHref(author.url) } : {}) })) } : {}), publisher: { '@type': 'Organization', name: 'IndopenSource' } }; @@ -55,7 +57,7 @@ const jsonLd = {
/blog/{post.slug} {releaseDate && <>Rilis {releaseDate}} - {post.author?.name && <>oleh {post.author.name}} + {authors.length > 0 && <>oleh {authors.map((author) => author.name).join(' & ')}}
{post.status} @@ -74,19 +76,24 @@ const jsonLd = { Penulis
-
- { - post.author?.avatarUrl && ( - {`${post.author.name} - ) - } -
- {authorUrl - ? {post.author?.name} - : {post.author?.name}} -

{authorByline}

-
+
+ {authors.map((author) => { + const authorUrl = safeHref(author.url); + return ( +
+ {author.avatarUrl && ( + {`${author.name} + )} +
+ {authorUrl + ? {author.name} + : {author.name}} +
+
+ ); + })}
+

{authorByline}


Date: Mon, 20 Jul 2026 19:33:18 +0700 Subject: [PATCH 5/9] feat: link latest article commit --- README.md | 3 ++- scripts/sync-blog-posts.mjs | 4 ++++ src/data/blog-posts.json | 4 ++++ src/lib/content.ts | 2 ++ src/pages/blog/[slug].astro | 6 ++++++ 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba07dc9..c54e021 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,8 @@ Gunakan `GITHUB_TOKEN` atau `GH_TOKEN` untuk rate limit yang lebih lega. `npm run sync:blog` membaca artikel Markdown dari `IndopenSource/Blog-IndopenSource`, mengambil tanggal commit pertama, commit terbaru, dan seluruh kontributor unik setiap artikel, lalu menulis hasilnya ke -`src/data/blog-posts.json`. +`src/data/blog-posts.json`. Halaman artikel menautkan commit terbaru tersebut +agar riwayat sumber dapat diperiksa langsung. ## Auto Sync diff --git a/scripts/sync-blog-posts.mjs b/scripts/sync-blog-posts.mjs index 83bcb99..b45637b 100644 --- a/scripts/sync-blog-posts.mjs +++ b/scripts/sync-blog-posts.mjs @@ -184,6 +184,8 @@ async function getCommitMeta(path) { committedAt: firstCommit?.commit?.author?.date || '' }, authors, + latestCommitSha: latestCommit?.sha || '', + latestCommitUrl: latestCommit?.html_url || '', // The latest commit timestamp is the date of the most recent EDIT, not a // publication/release event (CC-9). Name it honestly as "last modified" so // the rendered label can stop calling an edit timestamp a release date. @@ -279,6 +281,8 @@ for (const path of articleFiles) { // edit timestamp separately for provenance (CC-9). releasedAt: editorialDate || commitMeta.author.committedAt || commitMeta.lastModifiedAt, lastModifiedAt: commitMeta.lastModifiedAt, + latestCommitSha: commitMeta.latestCommitSha, + latestCommitUrl: commitMeta.latestCommitUrl, author: resolvedAuthor.author, authors: commitMeta.authors.length ? commitMeta.authors : [resolvedAuthor.author], authorFromFrontmatter: resolvedAuthor.fromFrontmatter diff --git a/src/data/blog-posts.json b/src/data/blog-posts.json index 0b10189..82752bc 100644 --- a/src/data/blog-posts.json +++ b/src/data/blog-posts.json @@ -19,6 +19,8 @@ "sourceUrl": "https://github.com/IndopenSource/Blog-IndopenSource/blob/main/content/2026/07/bagaimana-open-source-mampu-bertahan.md", "releasedAt": "2026-07-16", "lastModifiedAt": "2026-07-16T11:08:44Z", + "latestCommitSha": "95d3147d541c05b1ab6f683ccddd6b84d816aabd", + "latestCommitUrl": "https://github.com/IndopenSource/Blog-IndopenSource/commit/95d3147d541c05b1ab6f683ccddd6b84d816aabd", "author": { "name": "wauputr4", "avatarUrl": "https://avatars.githubusercontent.com/u/103489788?v=4", @@ -53,6 +55,8 @@ "sourceUrl": "https://github.com/IndopenSource/Blog-IndopenSource/blob/main/content/2026/06/memperkenalkan-blog-indopensource.md", "releasedAt": "2026-06-14", "lastModifiedAt": "2026-07-16T11:02:50Z", + "latestCommitSha": "a411deee259a2d8b9a8b1150d17f5dd5e2d6aab1", + "latestCommitUrl": "https://github.com/IndopenSource/Blog-IndopenSource/commit/a411deee259a2d8b9a8b1150d17f5dd5e2d6aab1", "author": { "name": "wauputr4", "avatarUrl": "https://avatars.githubusercontent.com/u/103489788?v=4", diff --git a/src/lib/content.ts b/src/lib/content.ts index b027aa5..afbaece 100644 --- a/src/lib/content.ts +++ b/src/lib/content.ts @@ -45,6 +45,8 @@ export interface BlogPost { releasedAt: string; /** ISO timestamp of the most recent edit commit; distinct from `releasedAt`. */ lastModifiedAt?: string; + latestCommitSha?: string; + latestCommitUrl?: string; author: PostAuthor; /** Unique contributors derived from the Markdown file's commit history. */ authors?: PostAuthor[]; diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index 0b49b53..666d6f2 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -23,6 +23,7 @@ const authorDate = post.author?.committedAt const modifiedDate = post.lastModifiedAt ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(post.lastModifiedAt)) : ''; +const latestCommitUrl = safeHref(post.latestCommitUrl); const releaseDate = post.releasedAt ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(post.releasedAt)) : ''; @@ -94,6 +95,11 @@ const jsonLd = { })}

{authorByline}

+ {latestCommitUrl && post.latestCommitSha && ( +
+ Commit terbaru {post.latestCommitSha.slice(0, 7)} ↗ + + )}
-
-
-
Direktori
-
Awesome ID
-
-
-
Lisensi
-
Open / Publik
-
-
+
+ Lisensi + Open / Publik ↗ +
From a7e0944cd30633ecfed18987c6867505f2ffab36 Mon Sep 17 00:00:00 2001 From: wauputr4 <103489788+wauputr4@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:04:36 +0700 Subject: [PATCH 7/9] feat: improve SEO analytics and AI discovery --- .env.example | 8 +++ CONTRIBUTING.md | 9 ++++ README.md | 11 +++++ docs/release-checklist.md | 4 ++ src/layouts/BaseLayout.astro | 86 +++++++++++++++++++++++++++++---- src/lib/learning.ts | 5 ++ src/pages/belajar/[slug].astro | 2 +- src/pages/blog/[slug].astro | 5 +- src/pages/projects/[slug].astro | 8 ++- src/pages/robots.txt.ts | 11 ++++- src/pages/sitemap.xml.ts | 2 +- 11 files changed, 135 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 9b23136..ad9d65d 100644 --- a/.env.example +++ b/.env.example @@ -24,3 +24,11 @@ GITHUB_TOKEN= # Production and the deploy workflow use `/` (the site is served from the apex # domain). Override only when previewing under a sub-path, e.g. `/my-fork/`. ASTRO_BASE=/ + +# Public analytics identifiers injected at build time. These values are visible +# in the generated HTML/browser and are identifiers, not secrets. +PUBLIC_CLOUDFLARE_WEB_ANALYTICS_TOKEN= +PUBLIC_GOOGLE_ANALYTICS_ID= + +# Optional Google Search Console HTML-tag verification value. +PUBLIC_GOOGLE_SITE_VERIFICATION= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38d325b..125625e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,6 +54,15 @@ add one object containing: Keep one community per pull request and explain your relationship to it or add a source that reviewers can verify. Do not add inferred or inactive communities. +## Content Quality + +- Lead with a direct summary before background detail. +- Cite primary sources for factual claims and include the relevant date. +- Use descriptive headings and stable internal links to related pages. +- Identify authors and keep publication and update dates traceable. +- Do not add speculative facts, generated statistics, or special AI-search + markup that is not supported by search-engine documentation. + ## Production Release The `release` branch is reserved for Cloudflare production deployments. Only diff --git a/README.md b/README.md index c54e021..4586125 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,17 @@ masuk `main`, perubahan mengikuti validasi dan alur promosi `main` ke `release`. Workflow ini masih memuat langkah deployment GitHub Pages lama; jangan menganggap hasil tersebut sebagai production Cloudflare. +## Analytics & Search Verification + +Analytics aktif saat build Cloudflare menerima environment variable berikut: + +- `PUBLIC_CLOUDFLARE_WEB_ANALYTICS_TOKEN` +- `PUBLIC_GOOGLE_ANALYTICS_ID` (format `G-XXXXXXXXXX`) +- `PUBLIC_GOOGLE_SITE_VERIFICATION` (opsional) + +Nilainya merupakan identifier publik dan tidak perlu dimasukkan ke Git. Isi di +pengaturan build Cloudflare untuk production dan preview sesuai kebutuhan. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 92459a1..3bd76ba 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -13,6 +13,8 @@ an IndopenSource organization member. - [ ] `admin@`, `press@`, `sponsor@`, and `legal@indopensource.org` can receive mail, or links to inactive addresses have been removed. - [ ] DNS, custom domain, TLS, and the Cloudflare production branch are correct. +- [ ] Cloudflare build variables for the Web Analytics token and Google + Analytics Measurement ID are configured. - [ ] The role of the legacy GitHub Pages workflows has been decided. ## Promote @@ -26,5 +28,7 @@ an IndopenSource organization member. - [ ] Smoke-test `https://indopensource.org/` and the primary navigation. - [ ] Confirm canonical URLs, robots, sitemap, favicon, and social previews. +- [ ] Confirm Cloudflare Web Analytics and Google Analytics receive a test page + view without CSP errors. - [ ] Send a test message to each published email address. - [ ] Record the production commit or tag as the rollback point. diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 69dbdb9..745fb61 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -10,6 +10,9 @@ interface Props { image?: string; /** Open Graph `og:type` (e.g. `website`, `article`). */ type?: string; + /** ISO timestamps used by article Open Graph metadata. */ + publishedAt?: string; + modifiedAt?: string; /** * Structured data injected as a JSON-LD `` inside synced data (repo description, post // title) can't terminate the inline block. Escaped before hashing, so the CSP @@ -86,12 +125,12 @@ const styleSrc = import.meta.env.DEV ? "style-src 'self' 'unsafe-inline'" : "sty const csp = [ "default-src 'self'", "base-uri 'self'", - `script-src 'self' 'sha256-${jsonLdHash}'`, + `script-src 'self' 'sha256-${jsonLdHash}'${analyticsEnabled ? ' https://static.cloudflareinsights.com https://www.googletagmanager.com' : ''}`, styleSrc, // First-party + GitHub avatar/raw image hosts only — no third-party beacons. - "img-src 'self' data: https://raw.githubusercontent.com https://avatars.githubusercontent.com", + `img-src 'self' data: https://raw.githubusercontent.com https://avatars.githubusercontent.com${googleAnalyticsId ? ' https://*.google-analytics.com https://*.googletagmanager.com' : ''}`, "font-src 'self' data:", - "connect-src 'self'", + `connect-src 'self'${cloudflareBeaconToken ? ' https://cloudflareinsights.com' : ''}${googleAnalyticsId ? ' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com' : ''}`, "frame-src https://drive.google.com", "form-action 'self'", "frame-ancestors 'none'", @@ -110,6 +149,7 @@ const csp = [ + {googleSiteVerification && } @@ -128,12 +168,15 @@ const csp = [ + + {type === 'article' && publishedAt && } + {type === 'article' && modifiedAt && } @@ -203,6 +246,29 @@ const csp = [ observer.observe(el); } } + + const cloudflareBeaconToken = import.meta.env.PUBLIC_CLOUDFLARE_WEB_ANALYTICS_TOKEN; + if (cloudflareBeaconToken) { + const beacon = document.createElement('script'); + beacon.defer = true; + beacon.src = 'https://static.cloudflareinsights.com/beacon.min.js'; + beacon.dataset.cfBeacon = JSON.stringify({ token: cloudflareBeaconToken }); + document.head.append(beacon); + } + + const googleAnalyticsId = import.meta.env.PUBLIC_GOOGLE_ANALYTICS_ID; + if (googleAnalyticsId) { + const tag = document.createElement('script'); + tag.async = true; + tag.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(googleAnalyticsId)}`; + document.head.append(tag); + + const analyticsWindow = window as typeof window & { dataLayer?: unknown[][] }; + const dataLayer = analyticsWindow.dataLayer ||= []; + const gtag = (...args: unknown[]) => dataLayer.push(args); + gtag('js', new Date()); + gtag('config', googleAnalyticsId); + } diff --git a/src/lib/learning.ts b/src/lib/learning.ts index 5774571..e79c615 100644 --- a/src/lib/learning.ts +++ b/src/lib/learning.ts @@ -9,6 +9,7 @@ import smartDigitalEnergyOg from '../assets/og/learning/smart-digital-energy.jpg export interface LearningResource { slug: string; title: string; + seoTitle?: string; year: number; topic: string; description: string; @@ -38,6 +39,7 @@ export const learningResources: LearningResource[] = [ { slug: 'ai-yang-proaktif', title: 'AI yang Proaktif: Memahami Agentic AI dengan Contoh Sederhana', + seoTitle: 'Agentic AI Proaktif dengan Contoh Sederhana', year: 2025, topic: 'Agentic AI', description: 'Pengenalan agentic AI melalui contoh yang mudah diikuti.', @@ -47,6 +49,7 @@ export const learningResources: LearningResource[] = [ { slug: 'menulis-jurnal-dibantu-ai', title: 'Menulis Jurnal Dibantu AI: Data-First Anti-Halusinasi', + seoTitle: 'Menulis Jurnal dengan AI Tanpa Halusinasi', year: 2025, topic: 'Literasi AI', description: 'Panduan menggunakan AI secara berbasis data untuk penulisan jurnal.', @@ -56,6 +59,7 @@ export const learningResources: LearningResource[] = [ { slug: 'roadmap-ai-gotong-royong', title: 'Roadmap AI Gotong Royong bagi Bangsa Indonesia', + seoTitle: 'Roadmap AI Gotong Royong Indonesia', year: 2025, topic: 'Transformasi Digital', description: 'Pandangan tentang arah pengembangan AI yang kolaboratif di Indonesia.', @@ -65,6 +69,7 @@ export const learningResources: LearningResource[] = [ { slug: 'smart-digital-energy', title: 'Smart Digital Energy: Inovasi Teknologi Open Source untuk Pencapaian Target Produksi Migas', + seoTitle: 'Smart Digital Energy dengan Open Source', year: 2025, topic: 'Energi', description: 'Contoh pemanfaatan teknologi open source untuk sektor energi.', diff --git a/src/pages/belajar/[slug].astro b/src/pages/belajar/[slug].astro index 6485125..9d8ef6c 100644 --- a/src/pages/belajar/[slug].astro +++ b/src/pages/belajar/[slug].astro @@ -19,7 +19,7 @@ const jsonLd = { }; --- - +
Ruang Belajar
diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index 666d6f2..340353c 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -24,6 +24,9 @@ const modifiedDate = post.lastModifiedAt ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(post.lastModifiedAt)) : ''; const latestCommitUrl = safeHref(post.latestCommitUrl); +const seoTitle = post.slug === 'bagaimana-open-source-mampu-bertahan' + ? 'Mengapa Open Source Bertahan Puluhan Tahun - IndopenSource' + : `${post.title} - IndopenSource`; const releaseDate = post.releasedAt ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(post.releasedAt)) : ''; @@ -52,7 +55,7 @@ const jsonLd = { }; --- - +

{post.description}

diff --git a/src/pages/projects/[slug].astro b/src/pages/projects/[slug].astro index 55d2bcd..2a050e7 100644 --- a/src/pages/projects/[slug].astro +++ b/src/pages/projects/[slug].astro @@ -22,7 +22,10 @@ const repoName = project.fullName.split('/').at(-1) || project.fullName; // no longer fetches GitHub or parses Markdown. const readmeHtml = project.readmeHtml || ''; // Snippet-clamped description for /OG, falling back to the raw one (SEO-6). -const metaDescription = project.metaDescription || project.description; +const sourceDescription = project.metaDescription || project.description; +const metaDescription = sourceDescription === 'Proyek dari daftar awesome-indonesia.' + ? `${repoName} adalah proyek open source Indonesia yang terdaftar di direktori IndopenSource.` + : sourceDescription; const topics = project.topics ?? []; // SoftwareSourceCode structured data so search engines understand each project @@ -48,10 +51,11 @@ const jsonLd = { ---
diff --git a/src/pages/robots.txt.ts b/src/pages/robots.txt.ts index 5be2f64..be84ffb 100644 --- a/src/pages/robots.txt.ts +++ b/src/pages/robots.txt.ts @@ -10,7 +10,16 @@ export const GET: APIRoute = ({ site }) => { const origin = site ?? new URL('https://indopensource.org'); const sitemapUrl = new URL('/sitemap.xml', origin).toString(); - return new Response(`User-agent: * + return new Response(`User-agent: OAI-SearchBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: GPTBot +Allow: / + +User-agent: * Allow: / Sitemap: ${sitemapUrl} diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts index 798b2e4..1368570 100644 --- a/src/pages/sitemap.xml.ts +++ b/src/pages/sitemap.xml.ts @@ -59,7 +59,7 @@ const blogPages: SitemapEntry[] = blogPosts .filter((post) => isPublished(post)) .map((post) => ({ path: `/blog/${post.slug}/`, - lastmod: toLastmod(post.releasedAt) ?? toLastmod(post.date) + lastmod: toLastmod(post.lastModifiedAt) ?? toLastmod(post.releasedAt) ?? toLastmod(post.date) })); const projectPages: SitemapEntry[] = projects.map((project) => ({ From b50bb6a71d9dfc6eed921735f11b15171bc07d51 Mon Sep 17 00:00:00 2001 From: wauputr4 <103489788+wauputr4@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:07:34 +0700 Subject: [PATCH 8/9] feat: add draft previews and release polish --- README.md | 4 +++ public/_redirects | 1 + public/brand/apple-touch-icon.png | Bin 16602 -> 16370 bytes public/brand/favicon-96.png | Bin 0 -> 5562 bytes public/brand/favicon.ico | Bin 8662 -> 15086 bytes scripts/sync-blog-posts.mjs | 5 +++- src/layouts/BaseLayout.astro | 10 ++++--- src/pages/blog.astro | 4 ++- .../blog/{[slug].astro => [...slug].astro} | 25 +++++++++++------- src/pages/falsafah.astro | 16 +++++++++++ src/pages/komunitas.astro | 3 ++- 11 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 public/_redirects create mode 100644 public/brand/favicon-96.png rename src/pages/blog/{[slug].astro => [...slug].astro} (81%) diff --git a/README.md b/README.md index 4586125..4956d18 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ terbaru, dan seluruh kontributor unik setiap artikel, lalu menulis hasilnya ke `src/data/blog-posts.json`. Halaman artikel menautkan commit terbaru tersebut agar riwayat sumber dapat diperiksa langsung. +Artikel berstatus `draft` tetap dibangun pada URL `/blog/preview/{slug}/`, +tetapi tidak ditampilkan di indeks blog, tidak masuk sitemap, dan memakai +`noindex, nofollow, noarchive` sampai statusnya diterbitkan. + ## Auto Sync Workflow `.github/workflows/sync-content.yml` memperbarui data secara otomatis. diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..54b9661 --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/sponsors/ /donasi/ 301 diff --git a/public/brand/apple-touch-icon.png b/public/brand/apple-touch-icon.png index e36db39aab0fdd5449363919fa9006a00a2b9ced..b50d69ad56c3d5f6d01073429db945ad829c484c 100644 GIT binary patch literal 16370 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 literal 16602 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 literal 0 HcmV?d00001 diff --git a/public/brand/favicon.ico b/public/brand/favicon.ico index 815d0945d93891e94b42d5f54396a812d02a162a..29eb739e54d907f3ea381e383d3162386985d916 100644 GIT binary patch literal 15086 zcmeHOeQ;b=6@Mil{^L+lKtOED6cLeu0Y)4Yf{qSoMNwJ@M6nEo2GXWqNjBNfx3r=& zI0FibU;$~RX?K(Tc>7*R1g1fU@=+ATO4DXP-p8h;-yhI6Y0|{sdH3D7Z@0TmHpzBi zCYiZ&-rjfbJ->7Ax#ymD?u{T!6RL!(t`b12g>{z*!q){ssIIntPZxwV>ZXIz``Xh5 z;bAoR3fiEHFb{?5OZ|mzQBiP{yxaZBue9E<)L6!o^<@A|2$p}zCJa6s5ZIg z+@@69wQeo?m|u(S=lD1csPUaGYUGinsn8X5y=yP`>ypD3bMpfGsTh8arI32mSeE9y zB^~wVdHjw?tlBKGcX5(h2zj~=)QvZY2D>?#rbpm&6#K*pws(;HhfL! ziGFuFx`^<@o{Hj!4OI|-zCLaI8q;x^%Q96M42Sxn@@F*5@zuXz^GM zlcQ~JE{3p1{(4XEv*#?@y6#H18VQTKbjFCRIN5WO^OyswL!nKQb0oIoAAA!#6e$?~KXMCEZVmyZpnd4OmKeHd`hUv0E zx7`20_XqqZ;LphmIX-gh(Rsgk|EcqtpMt*i#4xjejGe`A_)m&eCVT&7@q@#r$sVl2 z@ndZUocg!-JIG7V9RD3X&s+f8%lA>HPMG~*o+^)@^9RktZ~Fh7Y^IDRHijQQYTlha zkK6cd87*CqO;*DGF~?T?L-t_Vk;T)e#tz+UtU9|f6RK%ZqZ>@!TJLyU?8_e_eh4q> z{o7aTwynO7&s{z}73;S^w-!$9OHyOH?aunGFZ>`MKeH3WbniB%!#6Z*vH9@*oAPnz z#;m;mv3PO&vGtr761Bu3tjpa_o3U|X@51vg$aa`8+V#A@Wn-wHl=yjNU)Mz|RB7hJ z*_S^zHx#PE{4e$E@U-qox|eTWxUVIS&147Le-R?4Tx_rRa`Lq^*t z<@B3hM;Os|0R8r%WW2icFW6I;2m7*Hk3WR{{8z+FG?e;)rn~~)dQk81m5X28zhSzl z%g;eq(6@3Fw$a&;YM~IFH8f{gD*VGG$yzs| zn9J3EJwD)7V?F5m1jc(8m|rDcz&{u;l<03%`LpA}&ilC2@$;bn2;^XPkM)N=c1N=s zc^z|0d(vQIrtMF_pdl7J?n<|>f)7;)gR^+`*xrUrcmdiUhTZIMNVh!>T^z>m<8C$j z8rdMNZL^OqL+{E9Wd9t;M5tSBJ0Dmj*b?@33ztT5BYP&ku<3*J8<>uGv{)~H$MBf- z`|)l!_4n$DkNld1b;^Ai#^U3Wp0diihcWjP!Ccr^%)D5QIoN3WKArpp-@`b~*SGy% zmxmqL^5(>DW65FaCo8`qW|S`!@}G>~-VeHFV!mHtdgC&1{Dr?A^4Pj{)R~y``af`o#&V0JDK%fRlW@C>m7!l>+nR~ zn){S4AwK5s_#EYavwVZHapiVp#$;j~ZLEj1&dVVK@|YYGryN@$JyA|G);E+cQIjto zjbA0+uP_pqAm8*xz6^w$VyK_$H znaAFE))mB{owLTur;_bNRlXj2-YNERyrA?gXxo-!PT2bGfsg-kB{7vVwuo5KZ@l<@ zVA{!YIHX^ah2!ROAuh-&J)+uqbvfgoinij&$Z4Y1aW}q`_n5Y1?{QoXQx*+#^xac| zXFNRafBWq-f_=(8z`rlI9+>W7OX=YD7jMR4^R)4deX9C|M)0j;D7Rxh?4>opY)DCo z*^X~~+_7^V(f3-guj5wcTa_*!&d&I|PZFhdEsP9*tnKBKq|Yg=Q)m1|Y=Elh4$#cZM7>!OZ6|W{SaMjsu6@jI!yHe;3R6{okq0V;;Rb6ne_XBAe=?CUl3*x z#XvPgF+hN*T}1dH`Yl#5+I=RYsznbVM?&xWYZx64FnV&m(mkzel$%hd?rBY}Pg;cK z7R0(Q;oD*x@;J!bD4^{o~?~xS8dI^2Qr-Wul80Zf(8R%vljHU_p+}cp&-0HIzflozY9? z*%IClz)zGNYlsxL)0w3y5BokvUU?tMf}G$X$_YBsu4=85T@DC9RLXn$nqmX2-vbKWt^y;F%l&H^0#@jQX`XRd$8xi@UPe@Ujb zx-r%E0JnR>2A;c`)%Kq(Nv{7U$xc3q<@hGXA3k>|*oQf=WTde%_aEYn<2C1bHl9Dk z+-#(KC=R|{4+Dr##_WImnzF#J%Ci^XE)nE?6YI&5gE!@2;{uC0&seWUqezm~WaI_*cWPYv4WWKFBSj@u&A_e+LTW zmbXI(y|^3pJm|4y>5$KjGhW^azz*?i#n0Ol!;^>GO2 znmcF>!ak2!d(i;Kx`U5f4)y7YTJV4`p>>>_@3A}wv2L(_j`_37FU#Rs*cLhDCE%a) zXO8tqIGjGni3_|3k(0ZpaJ=#ShW;8%3=Z3N@-lJI{T5qCShIb~g7NSw)d%O!?y=1S z`3w2MQsYdd9{l}9F<5%B^8WP`;c>FQ=>zgGR?)qqUuSU;ek( zDbyKQ<^Cu=fm8*RnxRjl5IsquGzS$*b5J`wy+LV>Zlp9QjiDliabN~g7$`%55$7M} zDW;=TQ(*rI`F}E;vDIU5#eFK-Dhb?Qpl4+_VxHaDe{t55SO__beskj-AV59~?FMk~ z_C9>e(H*QKEn3_Cpk}=1H{vDl_E3M?YkY?E5%zufWHUB6?oG#B*!K}LNQg;G_s9Je z)?bUS@#yhqs6WNC6h|SJh_mtJlKjp7@O`-Bdf2B*e%kBFC%2>{&4~B#EFtzQ&7a9{ zep?J8c6y9>LvM6{I6iJw=jFR!D!XW)?}z43nLnZKcI`StFuG2K@XGZDJGvcnvM z@z*?M-qToN$X8%L+lg@uFV4+5zH+EXmo5}P=sLHYKIOM%IaBpaK`2(bs|0}xy*qH0 RAoR~b?zTn{h6AkM{{efE60QIM literal 8662 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 diff --git a/scripts/sync-blog-posts.mjs b/scripts/sync-blog-posts.mjs index b45637b..64ff617 100644 --- a/scripts/sync-blog-posts.mjs +++ b/scripts/sync-blog-posts.mjs @@ -261,6 +261,9 @@ for (const path of articleFiles) { const commitMeta = await getCommitMeta(path); const editorialDate = (data.date || '').trim(); const resolvedAuthor = resolveAuthor(commitMeta.author, data.authors); + const resolvedAuthors = resolvedAuthor.fromFrontmatter + ? [resolvedAuthor.author] + : commitMeta.authors.length ? commitMeta.authors : [resolvedAuthor.author]; posts.push({ slug: slugFromPath(path), @@ -284,7 +287,7 @@ for (const path of articleFiles) { latestCommitSha: commitMeta.latestCommitSha, latestCommitUrl: commitMeta.latestCommitUrl, author: resolvedAuthor.author, - authors: commitMeta.authors.length ? commitMeta.authors : [resolvedAuthor.author], + authors: resolvedAuthors, authorFromFrontmatter: resolvedAuthor.fromFrontmatter }); } diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 745fb61..229e20b 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -10,6 +10,8 @@ interface Props { image?: string; /** Open Graph `og:type` (e.g. `website`, `article`). */ type?: string; + /** Search-engine indexing policy for this page. */ + robots?: string; /** ISO timestamps used by article Open Graph metadata. */ publishedAt?: string; modifiedAt?: string; @@ -33,6 +35,7 @@ const { description = 'Roadmap website IndopenSource untuk proyek open source Indonesia.', image = '/brand/indopensource-hero.jpg', type = 'website', + robots = 'index, follow', publishedAt, modifiedAt, jsonLd, @@ -47,7 +50,6 @@ const imageUrl = new URL(image, SITE_ORIGIN).toString(); const cloudflareBeaconToken = import.meta.env.PUBLIC_CLOUDFLARE_WEB_ANALYTICS_TOKEN; const googleAnalyticsId = import.meta.env.PUBLIC_GOOGLE_ANALYTICS_ID; const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION; -const analyticsEnabled = Boolean(cloudflareBeaconToken || googleAnalyticsId); const siteUrl = new URL('/', SITE_ORIGIN).toString(); const organizationId = `${siteUrl}#organization`; @@ -125,7 +127,7 @@ const styleSrc = import.meta.env.DEV ? "style-src 'self' 'unsafe-inline'" : "sty const csp = [ "default-src 'self'", "base-uri 'self'", - `script-src 'self' 'sha256-${jsonLdHash}'${analyticsEnabled ? ' https://static.cloudflareinsights.com https://www.googletagmanager.com' : ''}`, + `script-src 'self' 'sha256-${jsonLdHash}'${cloudflareBeaconToken ? ' https://static.cloudflareinsights.com' : ''}${googleAnalyticsId ? ' https://www.googletagmanager.com' : ''}`, styleSrc, // First-party + GitHub avatar/raw image hosts only — no third-party beacons. `img-src 'self' data: https://raw.githubusercontent.com https://avatars.githubusercontent.com${googleAnalyticsId ? ' https://*.google-analytics.com https://*.googletagmanager.com' : ''}`, @@ -148,7 +150,7 @@ const csp = [ - + {googleSiteVerification && } @@ -165,7 +167,7 @@ const csp = [ ) } - + diff --git a/src/pages/blog.astro b/src/pages/blog.astro index 38da69d..a0087f7 100644 --- a/src/pages/blog.astro +++ b/src/pages/blog.astro @@ -185,7 +185,9 @@ const statusLegend = [

Artikel disimpan per tahun dan bulan supaya arsip tetap mudah dipindai, - dipreview, dan diintegrasikan ke halaman blog nanti. + dipreview, dan disinkronkan ke website. Draf dapat dibuka melalui + /blog/preview/slug-artikel/ + tanpa ditampilkan di daftar blog atau sitemap.

content/
   2026/
diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[...slug].astro
similarity index 81%
rename from src/pages/blog/[slug].astro
rename to src/pages/blog/[...slug].astro
index 340353c..8717ff2 100644
--- a/src/pages/blog/[slug].astro
+++ b/src/pages/blog/[...slug].astro
@@ -6,14 +6,16 @@ import { withBase, safeHref } from '../../lib/urls';
 import { isPublished, renderArticle, type BlogPost } from '../../lib/content';
 
 export function getStaticPaths() {
-  // Only build pages for published posts so drafts are never reachable.
-  return (blogPosts as BlogPost[]).filter(isPublished).map((post) => ({
-    params: { slug: post.slug },
+  // Drafts use a distinct preview route and stay out of listings and the sitemap.
+  return (blogPosts as BlogPost[]).map((post) => ({
+    params: { slug: isPublished(post) ? post.slug : `preview/${post.slug}` },
     props: { post }
   }));
 }
 
 const { post } = Astro.props;
+const published = isPublished(post);
+const articlePath = published ? `/blog/${post.slug}` : `/blog/preview/${post.slug}`;
 // Render Markdown through the shared sanitizing pipeline (no raw `set:html`).
 const html = renderArticle(post.content);
 const authors = post.authors?.length ? post.authors : post.author ? [post.author] : [];
@@ -36,7 +38,7 @@ const heroAlt = `Thumbnail artikel: ${post.title}`;
 // Provenance-aware author byline: frontmatter `authors[]` vs git commit metadata.
 const authorByline = post.authorFromFrontmatter
   ? `Disebutkan di frontmatter artikel${authorDate ? ` (commit pertama ${authorDate})` : ''}.`
-  : `Kontributor diambil dari riwayat commit sumber Markdown${authorDate ? `. Terbit ${authorDate}` : ''}${modifiedDate ? ` · diperbarui ${modifiedDate}` : ''}.`;
+  : `Kontributor diambil dari riwayat commit sumber Markdown${authorDate ? published ? `. Terbit ${authorDate}` : `. Commit pertama ${authorDate}` : ''}${modifiedDate ? ` · diperbarui ${modifiedDate}` : ''}.`;
 
 // BlogPosting structured data (passed to BaseLayout's hashed jsonLd prop so it
 // is CSP-allow-listed in , not a separate inline script).
@@ -46,7 +48,7 @@ const jsonLd = {
   headline: post.title,
   description: post.description,
   image: thumbnailUrl,
-  ...(post.releasedAt ? { datePublished: post.releasedAt } : {}),
+  ...(published && post.releasedAt ? { datePublished: post.releasedAt } : {}),
   ...(post.lastModifiedAt ? { dateModified: post.lastModifiedAt } : {}),
   ...(authors.length
     ? { author: authors.map((author) => ({ '@type': 'Person', name: author.name, ...(safeHref(author.url) ? { url: safeHref(author.url) } : {}) })) }
@@ -55,12 +57,17 @@ const jsonLd = {
 };
 ---
 
-
+
   
+    {!published && (
+      
+ Preview draf. Artikel ini masih ditinjau, belum diterbitkan, dan tidak diindeks mesin pencari. +
+ )}

{post.description}

- /blog/{post.slug} - {releaseDate && <>Rilis {releaseDate}} + {articlePath} + {releaseDate && <>{published ? 'Rilis' : 'Tanggal draf'} {releaseDate}} {authors.length > 0 && <>oleh {authors.map((author) => author.name).join(' & ')}}
@@ -86,7 +93,7 @@ const jsonLd = { return (
{author.avatarUrl && ( - {`${author.name} + )}
{authorUrl diff --git a/src/pages/falsafah.astro b/src/pages/falsafah.astro index fbb869e..7f69b0b 100644 --- a/src/pages/falsafah.astro +++ b/src/pages/falsafah.astro @@ -125,6 +125,22 @@ const references = [
+
+
+
+ +
+ Segera hadir +

Buku Manifesto Perangkat Terbuka.

+

Sebuah buku yang sedang disiapkan untuk merangkum gagasan, praktik, dan arah gerakan perangkat terbuka dalam konteks Indonesia.

+

COMING SOON · belum ada tanggal terbit

+
+
+
+
+ {/* Closing stamp — a halftone poster band with a rubber-stamp sign-off, so the manifesto ends on a printed, community-made note. ink/brand on canvas all AA. */} diff --git a/src/pages/komunitas.astro b/src/pages/komunitas.astro index dc4ce88..b7fbb38 100644 --- a/src/pages/komunitas.astro +++ b/src/pages/komunitas.astro @@ -3,6 +3,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import BaseButton from '../components/BaseButton.astro'; import PageHeader from '../components/PageHeader.astro'; import communitiesData from '../data/communities.json'; +import { safeHref } from '../lib/urls'; interface Community { name: string; @@ -34,7 +35,7 @@ const communities = communitiesData as Community[]; ) : (