From 604427048cb39690fe1520842d5978b45cdde421 Mon Sep 17 00:00:00 2001 From: Rahman Date: Mon, 15 Jun 2026 07:55:21 +0000 Subject: [PATCH] feat: risograph-editorial redesign + security/a11y/perf/SEO hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full breakdown in the PR description. Highlights: - security: blog stored-XSS closed via a shared sanitize-html allowlist (src/lib/content.ts renderArticle, reused for build-time README render); CSP with hashed JSON-LD and no unsafe-inline; rel=noopener on sanitized links - content: draft gating (isPublished) across pages/featured/sitemap; author from frontmatter authors[]; hardened parseFrontmatter; collision-safe projectSlug; default-branch resolution; honest sync-failure star totals - seo/a11y: per-page og:image + JSON-LD (WebSite/BlogPosting/SoftwareSourceCode); /sitemap.xml with lastmod; trailingSlash always; skip-link, focus-visible, single h1, WCAG-AA contrast verified, prefers-reduced-motion - perf: astro:assets AVIF/WebP (hero 60KB->~8.5KB, logo 18KB->737B), width/height on all (CLS ~0), hero LCP preload - design: risograph-editorial system — self-hosted variable fonts (Bricolage / Newsreader / Spline Sans Mono), disciplined riso spot palette, sticker/stamp/ halftone/grain/hard-shadow utilities, scroll-aware IntersectionObserver reveal (bundled module, CSP-safe), duotone riso hero keeping the optimized - tooling: 16 node --test unit tests (slug/isPublished/sanitizer-XSS/parser); .env.example Co-Authored-By: Claude Opus 4.8 (1M context) --- .env.example | 26 + astro.config.mjs | 25 +- docs/redesign/blog.png | Bin 0 -> 2805421 bytes docs/redesign/contact.png | Bin 0 -> 1257137 bytes docs/redesign/falsafah.png | Bin 0 -> 2690085 bytes docs/redesign/forum.png | Bin 0 -> 1230949 bytes docs/redesign/home.png | Bin 0 -> 6531490 bytes docs/redesign/projects.png | Bin 0 -> 4578720 bytes package-lock.json | 252 ++++--- package.json | 10 +- scripts/sync-blog-posts.mjs | 184 ++++- scripts/sync-projects.mjs | 143 +++- src/assets/brand/icon-192.png | Bin 0 -> 19114 bytes src/assets/brand/indopensource-hero.jpg | Bin 0 -> 60347 bytes src/components/BaseButton.astro | 19 +- src/components/HomeHero.astro | 152 ++++- src/components/InfoCard.astro | 8 +- src/components/LightSvgMotion.astro | 103 ++- src/components/PageHeader.astro | 11 +- src/components/ProjectCard.astro | 89 ++- src/components/ProjectsDirectory.astro | 93 +-- src/components/RoadmapTimeline.astro | 37 +- src/components/SectionHeader.astro | 36 +- src/components/SiteFooter.astro | 75 ++- src/components/SiteHeader.astro | 111 ++- src/layouts/BaseLayout.astro | 166 ++++- src/lib/content.ts | 187 ++++++ src/lib/hero.ts | 25 + src/lib/projects.ts | Bin 224 -> 3704 bytes src/pages/blog.astro | 219 ++++-- src/pages/blog/[slug].astro | 94 ++- src/pages/contact.astro | 114 +++- src/pages/falsafah.astro | 109 ++- src/pages/forum.astro | 35 +- src/pages/index.astro | 124 +++- src/pages/projects.astro | 53 +- src/pages/projects/[slug].astro | 248 +++---- src/pages/robots.txt.ts | 17 +- src/pages/sitemap-index.xml.ts | 33 - src/pages/sitemap.xml.ts | 77 +++ src/styles/global.css | 853 ++++++++++++++++++++++-- test/lib.test.mjs | 119 ++++ 42 files changed, 3108 insertions(+), 739 deletions(-) create mode 100644 .env.example create mode 100644 docs/redesign/blog.png create mode 100644 docs/redesign/contact.png create mode 100644 docs/redesign/falsafah.png create mode 100644 docs/redesign/forum.png create mode 100644 docs/redesign/home.png create mode 100644 docs/redesign/projects.png create mode 100644 src/assets/brand/icon-192.png create mode 100644 src/assets/brand/indopensource-hero.jpg create mode 100644 src/lib/content.ts create mode 100644 src/lib/hero.ts delete mode 100644 src/pages/sitemap-index.xml.ts create mode 100644 src/pages/sitemap.xml.ts create mode 100644 test/lib.test.mjs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9b23136 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# Example environment for indopensource.org +# +# Copy this file to `.env` and fill in real values: +# +# cp .env.example .env +# +# `.env` and every `.env.*` file are gitignored except this template +# (`!.env.example`), so secrets never get committed. Keep this file free of +# real tokens — it documents which variables exist, not their values. + +# GitHub API token used by the content sync scripts: +# npm run sync:projects (scripts/sync-projects.mjs) +# npm run sync:blog (scripts/sync-blog-posts.mjs) +# +# Optional, but strongly recommended: unauthenticated GitHub API calls are +# rate-limited to ~60 requests/hour, which is not enough to sync the full +# project directory. A read-only, fine-grained "public repositories" token is +# sufficient — no write scopes are required. `GH_TOKEN` is accepted as a +# fallback if `GITHUB_TOKEN` is not set. +GITHUB_TOKEN= +# GH_TOKEN= + +# Astro base path for the build (`astro.config.mjs` reads `process.env.ASTRO_BASE`). +# 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=/ diff --git a/astro.config.mjs b/astro.config.mjs index 09007eb..4cefeda 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -2,9 +2,32 @@ import { defineConfig } from 'astro/config'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ + // Canonical production origin. Must stay the upstream domain so generated + // canonical URLs, the sitemap, robots.txt, and Open Graph tags all point at + // the real site regardless of where a fork is hosted. site: 'https://indopensource.org', base: process.env.ASTRO_BASE || '/', + // Emit a single, consistent URL shape for every route. Without this, Astro's + // default ('ignore') lets both `/blog` and `/blog/` resolve, which splits + // canonical signals and PageRank across two URLs. Forcing trailing slashes + // matches the directory-style output of the static build and the URLs the + // sitemap advertises, avoiding duplicate-content and 301 hops. (SEO-5) + trailingSlash: 'always', + build: { + // Never inline the scroll-reveal ` diff --git a/src/lib/content.ts b/src/lib/content.ts new file mode 100644 index 0000000..62724f9 --- /dev/null +++ b/src/lib/content.ts @@ -0,0 +1,187 @@ +import { marked } from 'marked'; +import sanitizeHtml from 'sanitize-html'; + +/** + * Single source of truth for content gating and safe HTML rendering. + * + * Markdown is rendered with `marked` and then run through `sanitize-html` + * using a conservative allowlist. The same allowlist is reused for any + * untrusted Markdown rendered elsewhere in the site (e.g. project READMEs), + * so there is exactly one place to audit for HTML-injection safety. + */ + +/** Author reference as it can appear in article frontmatter `authors[]`. */ +export interface FrontmatterAuthor { + name: string; + url?: string; + avatarUrl?: string; +} + +/** Resolved author shown on a rendered article. */ +export interface PostAuthor { + name: string; + url?: string; + avatarUrl?: string; + /** ISO date of the first commit that introduced the article file. */ + committedAt?: string; +} + +/** A blog post as stored in `src/data/blog-posts.json`. */ +export interface BlogPost { + slug: string; + path: string; + year: string; + month: string; + title: string; + description: string; + date: string; + tags: string[]; + /** Publication state. Anything other than `"draft"` is considered published. */ + status: string; + thumbnail: string; + content: string; + sourceUrl: string; + /** Editorial publication date (frontmatter `date`), falling back to commit metadata. */ + releasedAt: string; + /** ISO timestamp of the most recent edit commit; distinct from `releasedAt`. */ + lastModifiedAt?: string; + author: PostAuthor; + /** + * Whether `author` was taken from frontmatter `authors[]` (true) or + * derived from git commit metadata (false/undefined). Lets the UI label + * the author provenance correctly. + */ + authorFromFrontmatter?: boolean; +} + +/** + * Returns true when a post should be publicly listed/rendered. + * Posts default to `"draft"`; only a non-draft status is published. + */ +export function isPublished(post: Pick): boolean { + return post.status !== 'draft'; +} + +/** + * Reusable sanitize-html allowlist for untrusted Markdown-derived HTML. + * + * Standard article tags are allowed (headings, lists, blockquotes, code, + * tables, links, images). Active-content tags (`script`, `style`, `iframe`, + * `object`, `embed`, `form`) are dropped, as are every `on*` event-handler + * attribute plus `style` and `srcdoc`. Links and images are restricted to + * http/https/mailto schemes to block `javascript:`/`data:` payloads. + * + * Exported so other modules (e.g. project README rendering) can share a + * single audited configuration instead of redefining their own. + */ +export const articleSanitizeOptions: sanitizeHtml.IOptions = { + allowedTags: [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'p', + 'br', + 'hr', + 'blockquote', + 'pre', + 'code', + 'span', + 'div', + 'strong', + 'em', + 'b', + 'i', + 'del', + 's', + 'sub', + 'sup', + 'mark', + 'ul', + 'ol', + 'li', + 'dl', + 'dt', + 'dd', + 'a', + 'img', + 'figure', + 'figcaption', + 'table', + 'thead', + 'tbody', + 'tfoot', + 'tr', + 'th', + 'td', + 'caption', + 'colgroup', + 'col' + ], + // Explicitly drop active-content tags (defence in depth: they are also + // absent from allowedTags above). + disallowedTagsMode: 'discard', + allowedAttributes: { + a: ['href', 'name', 'title', 'rel', 'target'], + img: ['src', 'alt', 'title', 'loading', 'width', 'height'], + th: ['colspan', 'rowspan', 'scope', 'align'], + td: ['colspan', 'rowspan', 'align'], + col: ['span'], + colgroup: ['span'], + code: ['class'], + span: ['class'], + div: ['class'] + }, + // Restrict link/image schemes to safe protocols only; this blocks + // `javascript:`, `data:`, `vbscript:`, etc. + allowedSchemes: ['http', 'https', 'mailto'], + allowedSchemesByTag: { + a: ['http', 'https', 'mailto'], + img: ['http', 'https'] + }, + allowProtocolRelative: false, + // Force a safe `rel` on every anchor so a `target="_blank"` link in + // user-authored Markdown cannot reach `window.opener` (reverse tabnabbing). + // + // Demote any `h1` in the body to `h2` (A11Y-6): the page template already + // renders the single page-level `

` (repo name / article title), so a + // README or article body that opens with `# Title` would otherwise inject a + // second `

`, breaking heading order (WCAG 1.3.1). Renaming to `h2` keeps + // exactly one page-level h1 while preserving the relative heading structure. + transformTags: { + a: sanitizeHtml.simpleTransform('a', { rel: 'noopener noreferrer' }, true), + h1: 'h2' + }, + // Never allow inline styles, event handlers (on*), or iframe srcdoc. + // `allowedAttributes` already omits `style`/`srcdoc`/`on*`, but we also + // ensure no vendored defaults reintroduce them. + allowedStyles: {}, + enforceHtmlBoundary: true +}; + +/** + * Tags that are explicitly never allowed, kept as a named export so tests and + * downstream consumers can assert the security posture without duplicating it. + */ +export const forbiddenTags = [ + 'script', + 'style', + 'iframe', + 'object', + 'embed', + 'form' +] as const; + +/** + * Render trusted-but-user-authored Markdown into sanitized, safe HTML. + * Use for blog article bodies and any other Markdown that ends up in + * `set:html`. Returns an empty string for empty/falsy input. + */ +export function renderArticle(markdown: string): string { + if (!markdown) return ''; + // marked.parse is synchronous (returns a string) unless async is enabled. + const rawHtml = marked.parse(markdown, { async: false }) as string; + return sanitizeHtml(rawHtml, articleSanitizeOptions); +} diff --git a/src/lib/hero.ts b/src/lib/hero.ts new file mode 100644 index 0000000..68608a1 --- /dev/null +++ b/src/lib/hero.ts @@ -0,0 +1,25 @@ +import type { ImageMetadata } from 'astro'; +import heroSource from '../assets/brand/indopensource-hero.jpg'; + +/** + * Single source of truth for the homepage hero (the LCP image). + * + * `HomeHero.astro` renders an optimised `` from this, and `index.astro` + * runs `getImage()` over the SAME source + transform parameters to emit a + * high-priority `` in ``. Keeping both + * paths pointed at this module guarantees the preloaded AVIF is byte-identical + * to one of the ``'s `srcset` candidates, so the browser reuses the + * preloaded download for the actual LCP paint instead of fetching twice. + */ +export const heroImage: ImageMetadata = heroSource; + +/** + * Largest emitted width — the `` `widths` top stop and the preload + * size. Set to the source's intrinsic width (1536px) so astro:assets never + * upscales and so the preload transform clamps to the SAME dimensions as the + * top `srcset` candidate, letting the browser reuse one download for the LCP. + */ +export const HERO_PRELOAD_WIDTH = 1536; + +/** Compression quality shared by the `` and the preload transform. */ +export const HERO_QUALITY = 60; diff --git a/src/lib/projects.ts b/src/lib/projects.ts index fde6eb7a5b201774220757c9d51db166b2ff07e7..63ef26749555c20396973f9619aa6fb53def7d27 100644 GIT binary patch literal 3704 zcmaJ^+j88v5#48fMc0w_*%){mBQjsm?BZ|PlR0+f z2M77l5xo&X`anOc@H#FLOH1MTGC@=#h+{&13KO+1fSoh|!O6~2BRm;9Zubig*TaI$ zf^@Yg5%=r@e@YweZ`oGB%XIbOE57Xkz|GdqBP>h~ysK)N{gY+BQO0MMIt6G>< ze7I=ujC6b3(#rW4+`#?j^xh+nHUErEY?sn8kkHZX-a@dQD*g1*hmX7~cEE$7oYr}@ zi#@)3i#qXQs2!dRydI1BJI>ac-xjWH)E8M{&(d1Z$M7nsh2P-f)#drix0hgwsh|>& z-I5nf;;;DU_08Ki4@5kBe|hyPI|WlO+b(PduJo}I16j1Y92^b+PgS7OM!12o@C8Y= z3?+y|RZQAo0l-v{RF2;c>S^Wc(Cz~}<^5g@WwIdGVYku*p|xFq4fuiN%V*N8l#yBO z6fUVZq_Gg1(c1ddi=S9hiT`Kc?@;Ia7X{_?YX@ol>_peFB0SBmFMrK`1O~5Q0G^hj zyajMWq@A}%Q7*kSVS6_|>gAT6dv%>Icr9TIEa)0%q!WCA1IShdP=w7i*&{%sp6I&< zaOfwC=(E}b-pSfY<`mrKB0D|7Mg9tFFA{5uyhvm%z#RC#hEp_FgBCir9W79Wj|Eq4vME_JLVSG;z%C9zsLxdLN>QV<$`ber@Z{LffW01ltu}Z;adnUP_0}C~s*s3=}zMUeuC) zyL!`i;2t7r|bwZY32WUuGq!i*oVj1wPMUJ=`5|cg?P{!SMzyoDbw^eE&J! zrP^{o2X&w@q;5G`xFJTDG!t=9i-hk`YNReOsaWiA5F3;eRLRk}nSkB|h!h4EOj5|2 zG2vJs39Ja%GtpC0ght~8S~Aj$M*TBN1066fP{vX6D7m(XJzOC)n5lp@ziwUImqAsq z7gaKE8K0@u*U_MMP{Yt*uQA!p9yZVU*d0@AWevledB!-K`Yelq7Qw!;chX(p*$l+P zPoC2S*qbWJRG4VMl_GEfz$6d>xiE*x^l?BPJqUS+A~Gmrq+L4pWQKo3LIMK=lMi!C zKURW(>)uRh0a>s?&GPJVzB;DE>~PBLG7&$)KI;d=`L0H`v8%-0G)wju9fKa`hY0!I zPDo}eNQbIQ4TMrLpfNVX4B~J3!nx)pDB_-1$U>?DL77y-**7$UkL| zf6pF2N{AlLpC14gl=>PT$ITY9Y#|$`!n#^MW literal 224 zcmZvWF$=;l5Jva>irdnlXMUjvtt t&nI=cY5Cg7t-P*OH&QP4?86u?H#C1kLw@-6#1ggGGJ@G$q-9)9<6qr4N*w?I diff --git a/src/pages/blog.astro b/src/pages/blog.astro index c2d7911..008f769 100644 --- a/src/pages/blog.astro +++ b/src/pages/blog.astro @@ -1,15 +1,48 @@ --- import BaseLayout from '../layouts/BaseLayout.astro'; import BaseButton from '../components/BaseButton.astro'; -import InfoCard from '../components/InfoCard.astro'; -import LightSvgMotion from '../components/LightSvgMotion.astro'; import PageHeader from '../components/PageHeader.astro'; import blogPosts from '../data/blog-posts.json'; import { withBase } from '../lib/urls'; +import { isPublished, type BlogPost } from '../lib/content'; -const featuredPost = blogPosts[0]; +// Feature the first *published* post, never a draft (index 0 may be a draft). +const featuredPost = (blogPosts as BlogPost[]).find(isPublished); const featuredThumbnail = featuredPost?.thumbnail || '/brand/indopensource-hero.jpg'; const featuredThumbnailUrl = /^https?:\/\//.test(featuredThumbnail) ? featuredThumbnail : withBase(featuredThumbnail); +const featuredAlt = featuredPost + ? `Thumbnail artikel: ${featuredPost.title}` + : 'Ilustrasi sampul Blog IndopenSource'; +const featuredDate = featuredPost?.releasedAt + ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(featuredPost.releasedAt)) + : ''; + +const contributionSteps = [ + { + number: '01', + status: 'Step 1', + title: 'Pilih format', + body: 'Salin templates/article.md, lalu simpan sebagai content/YYYY/MM/slug-artikel.md.' + }, + { + number: '02', + status: 'Step 2', + title: 'Tulis clean Markdown', + body: 'Gunakan frontmatter lengkap, heading berurutan, paragraf pendek, dan referensi yang jelas.' + }, + { + number: '03', + status: 'Step 3', + title: 'Review via PR', + body: 'Submit pull request agar maintainer bisa mengecek struktur, sumber, dan kesiapan publikasi.' + } +]; + +const statusLegend = [ + { label: 'draft', note: 'Masih direview.' }, + { label: 'ready', note: 'Siap publish.' }, + { label: 'published', note: 'Sudah tayang.' } +]; ---

Blog IndopenSource sekarang memakai repo khusus - IndopenSource/Blog-IndopenSource + IndopenSource/Blog-IndopenSource untuk menyimpan artikel Markdown yang rapi, mudah direview, dan siap diintegrasikan ke website.

+
+ ZINE / MARKDOWN-FIRST + PR-REVIEWED + git-as-editorial +
Buka repo blog Lihat template
-
- -
-
-
- Artikel terbaru -

{featuredPost?.title || 'Memperkenalkan Blog IndopenSource'}

-

- {featuredPost?.description || 'Draft pertama sudah tersedia di repo sebagai contoh format artikel komunitas.'} -

-
- {featuredPost?.tags?.map((tag) => {tag})} - {featuredPost?.status && {featuredPost.status}} -
-
- - Baca artikel - + {featuredPost && ( +
+
+
+ +
+ Cover story +

Artikel terbaru

+
-
- -
-
-
-
-
-
-
-

Submit

-

Alur kontribusi artikel

+
+
+
+ /blog/{featuredPost.slug} + {featuredDate && <>{featuredDate}} +
+

{featuredPost.title}

+

{featuredPost.description}

+ {featuredPost.tags?.length > 0 && ( +
+ {featuredPost.tags.map((tag) => {tag})} +
+ )} +
+ + Baca artikel + +
+
+
+ {featuredAlt} +
+
-
- -

Salin `templates/article.md`, lalu simpan sebagai `content/YYYY/MM/slug-artikel.md`.

-
- -

Gunakan frontmatter lengkap, heading berurutan, paragraf pendek, dan referensi yang jelas.

-
- -

Submit pull request agar maintainer bisa mengecek struktur, sumber, dan kesiapan publikasi.

-
+
+ )} + + {/* Vermilion submit spread — the one tonal break from teal. Accent field + with paper/white text (paper-on-accent 5.16:1, white-on-accent 5.52:1, + both AA). Step cards stay paper/canvas islands so their ink text + riso + layering read cleanly against the vermilion ground. */} +
+
+
+ +
+ Submit +

Alur kontribusi artikel

+
+
+ +
    + {contributionSteps.map((step) => ( +
  1. +
    + {step.status} + +
    +

    {step.title}

    +

    + {step.body.split(/(content\/YYYY\/MM\/slug-artikel\.md|templates\/article\.md)/).map((chunk) => + /\//.test(chunk) ? {chunk} : chunk + )} +

    +
  2. + ))} +
-
+
-
- Markdown repo -

Struktur artikel

-

- Artikel disimpan per tahun dan bulan supaya arsip tetap mudah dipindai, - dipreview, dan diintegrasikan ke halaman blog nanti. -

-
content/
+      
+ +
+ Markdown repo +

Struktur artikel

+
+ +
+ +
+
+

+ Artikel disimpan per tahun dan bulan supaya arsip tetap mudah dipindai, + dipreview, dan diintegrasikan ke halaman blog nanti. +

+
content/
   2026/
     06/
       memperkenalkan-blog-indopensource.md
 templates/
   article.md
-
-
- draft - Masih direview. -
-
- ready - Siap publish. -
-
- published - Sudah tayang. -
+
+ +
+ {statusLegend.map((item) => ( +
+ {item.label} + {item.note} +
+ ))} + + Buka template artikel +
-
+
diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index e9668f9..3ea147c 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -1,19 +1,21 @@ --- -import { marked } from 'marked'; import BaseLayout from '../../layouts/BaseLayout.astro'; import PageHeader from '../../components/PageHeader.astro'; import blogPosts from '../../data/blog-posts.json'; import { withBase } from '../../lib/urls'; +import { isPublished, renderArticle, type BlogPost } from '../../lib/content'; export function getStaticPaths() { - return blogPosts.map((post) => ({ + // Only build pages for published posts so drafts are never reachable. + return (blogPosts as BlogPost[]).filter(isPublished).map((post) => ({ params: { slug: post.slug }, props: { post } })); } const { post } = Astro.props; -const html = marked.parse(post.content); +// Render Markdown through the shared sanitizing pipeline (no raw `set:html`). +const html = renderArticle(post.content); const authorDate = post.author?.committedAt ? new Intl.DateTimeFormat('id-ID', { dateStyle: 'long' }).format(new Date(post.author.committedAt)) : ''; @@ -22,46 +24,86 @@ const releaseDate = post.releasedAt : ''; const thumbnail = post.thumbnail || '/brand/indopensource-hero.jpg'; const thumbnailUrl = /^https?:\/\//.test(thumbnail) ? thumbnail : withBase(thumbnail); +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}` : ''}.`; + +// BlogPosting structured data (passed to BaseLayout's hashed jsonLd prop so it +// is CSP-allow-listed in , not a separate inline script). +const jsonLd = { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + headline: post.title, + description: post.description, + image: thumbnailUrl, + ...(post.releasedAt ? { datePublished: post.releasedAt } : {}), + ...(post.lastModifiedAt ? { dateModified: post.lastModifiedAt } : {}), + ...(post.author?.name + ? { author: { '@type': 'Person', name: post.author.name, ...(post.author.url ? { url: post.author.url } : {}) } } + : {}), + publisher: { '@type': 'Organization', name: 'IndopenSource' } +}; --- - +

{post.description}

-
- {releaseDate && Rilis {releaseDate}} - {post.tags.map((tag) => {tag})} - {post.status} +
+ /blog/{post.slug} + {releaseDate && <>Rilis {releaseDate}} +
+
+ {post.status} + {post.tags.map((tag) => {tag})}
-
- +
+ {heroAlt}
-

+

Thumbnail artikel dapat diatur dari frontmatter Markdown dengan field - thumbnail, image, atau cover. + thumbnail, + image, atau + cover.

-
-
+ diff --git a/src/pages/contact.astro b/src/pages/contact.astro index 55f439e..ca68743 100644 --- a/src/pages/contact.astro +++ b/src/pages/contact.astro @@ -1,8 +1,49 @@ --- import BaseLayout from '../layouts/BaseLayout.astro'; -import InfoCard from '../components/InfoCard.astro'; -import LightSvgMotion from '../components/LightSvgMotion.astro'; import PageHeader from '../components/PageHeader.astro'; +import LightSvgMotion from '../components/LightSvgMotion.astro'; + +// Contact channels as a zine "rolodex". Each entry is a printed card: sticker +// label + mono handle + the (verbatim, externally-linked) destination. Logic is +// static; the array keeps the cards uniform. All hrefs/rel/sr-only text are +// preserved exactly as before. +const channels = [ + { + tag: 'Organization', + handle: '@IndopenSource', + href: 'https://github.com/IndopenSource', + label: 'github.com/IndopenSource', + meta: '// rumah semua repo' + }, + { + tag: 'Roadmap', + handle: 'GitHub Projects', + href: 'https://github.com/orgs/IndopenSource/projects', + label: 'GitHub Projects IndopenSource', + meta: '// urutan pekerjaan publik' + }, + { + tag: 'Forum', + handle: 'GitHub Discussions', + href: 'https://github.com/orgs/IndopenSource/discussions', + label: 'GitHub Discussions IndopenSource', + meta: '// tanya, usul, koordinasi' + }, + { + tag: 'Instagram', + handle: '@indopensource', + href: 'https://www.instagram.com/indopensource', + label: 'instagram.com/indopensource', + meta: '// kabar & sorotan' + }, + { + tag: 'Threads', + handle: '@indopensource', + href: 'https://www.threads.com/@indopensource?xmt=AQG0gissJqZtQXt5UwznNJsDj0IXvg7Plki6SfOkjhcam0o', + label: 'threads.com/@indopensource', + meta: '// obrolan singkat' + } +]; --- -
+ +
-
- -

github.com/IndopenSource

-
- -

GitHub Projects IndopenSource

-
- -

GitHub Discussions IndopenSource

-
- -

instagram.com/indopensource

-
- -

threads.com/@indopensource

-
+
+
+ Saluran resmi + + +
+ + {/* Asymmetric zine grid: first card spans wide as the "primary" address, + the rest pack into a printed rolodex. Each card is the system card + pattern (2px ink + riso-shadow + riso-hover). */} +
diff --git a/src/pages/falsafah.astro b/src/pages/falsafah.astro index 308e63f..c9cf573 100644 --- a/src/pages/falsafah.astro +++ b/src/pages/falsafah.astro @@ -1,7 +1,34 @@ --- import BaseLayout from '../layouts/BaseLayout.astro'; -import InfoCard from '../components/InfoCard.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' + }, + { + 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' + }, + { + 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' + } +]; --- -
-
- -

Kode, diskusi, keputusan, dan roadmap dibuat mudah ditemukan oleh publik.

-
- -

Daftar proyek perlu kurasi, metadata yang jelas, dan ritme pembaruan yang sehat.

-
- -

Fokus pada perangkat, pustaka, data, dan praktik yang membantu pengguna Indonesia.

-
+ + {/* Manifesto pull-quote on the deep teal field — the editorial centrepiece. + paper/sun text on brand-dark is AA (paper on brand-dark 10.93:1; the sun + accent words sit on the dark field, not as body text). */} +
+ +
+
+ Manifesto + // gotong royong, bukan kerja sendiri +
+

+ Teknologi tumbuh kalau dirawat bersama — + dibuka, dibaca, dipakai ulang, dan diwariskan. +

+

— IndopenSource · komunitas terbuka Indonesia

+
+
+ + {/* Three 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 + + +
+ +
+ { + prinsip.map((p, i) => ( +
0 ? 'border-t-[3px] border-ink' : '' + }`} + > + +
+
+ {p.tag} + {p.meta} +
+

{p.title}

+

{p.body}

+
+
+ )) + } +
+
+
+ + {/* 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. */} +
+ +
+
+

+ Kalau bisa dipelajari, dipakai ulang, dan dirawat bersama — itu sudah + open source. +

+
+
diff --git a/src/pages/forum.astro b/src/pages/forum.astro index b83ce05..27ba0bb 100644 --- a/src/pages/forum.astro +++ b/src/pages/forum.astro @@ -13,17 +13,38 @@ import PageHeader from '../components/PageHeader.astro'; Forum direncanakan mengambil sumber dari organization discussions supaya percakapan tetap dekat dengan repo, issue, project board, dan kontributor.

-
+
Buka discussions Buka project board
-
-
-
- Integrasi tahap lanjut dapat membaca GraphQL GitHub Discussions untuk menampilkan kategori, - thread terbaru, dan status jawaban langsung di situs. -
+ + {/* Printed development notice. The semantic