Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/sync-blog-posts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ for (const path of articleFiles) {
date: editorialDate,
tags: data.tags || [],
status: data.status || 'draft',
lang: data.lang === 'en' ? 'en' : 'id',
translationKey: data.translationKey || '',
thumbnail: resolveThumbnail(data.thumbnail || data.image || data.cover || '', path, defaultBranch),
content,
sourceUrl: file.html_url,
Expand Down
154 changes: 154 additions & 0 deletions src/components/BlogArticlePage.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import PageHeader from './PageHeader.astro';
import { withBase, safeHref } from '../lib/urls';
import { articleHref, articleLanguage, isPublished, renderArticle, type BlogPost } from '../lib/content';

interface Props {
post: BlogPost;
translations: BlogPost[];
}

const { post, translations } = Astro.props;
const language = articleLanguage(post);
const english = language === 'en';
const locale = english ? 'en-US' : 'id-ID';
const published = isPublished(post);
const articlePath = articleHref(post).replace(/\/$/, '');
const html = renderArticle(post.content);
const authors = post.authors?.length ? post.authors : post.author ? [post.author] : [];
const authorDate = post.author?.committedAt
? new Intl.DateTimeFormat(locale, { dateStyle: 'long' }).format(new Date(post.author.committedAt))
: '';
const modifiedDate = post.lastModifiedAt
? new Intl.DateTimeFormat(locale, { 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(locale, { dateStyle: 'long', timeZone: 'UTC' }).format(new Date(post.releasedAt))
: '';
const thumbnail = post.thumbnail || '/brand/indopensource-hero.jpg';
const thumbnailUrl = /^https?:\/\//.test(thumbnail) ? thumbnail : withBase(thumbnail);
const heroAlt = english ? `Article thumbnail: ${post.title}` : `Thumbnail artikel: ${post.title}`;
const authorByline = post.authorFromFrontmatter
? english
? `Named in the article frontmatter${authorDate ? ` (first commit ${authorDate})` : ''}.`
: `Disebutkan di frontmatter artikel${authorDate ? ` (commit pertama ${authorDate})` : ''}.`
: english
? `Contributors are derived from the Markdown source history${authorDate ? published ? `. Published ${authorDate}` : `. First commit ${authorDate}` : ''}${modifiedDate ? ` · updated ${modifiedDate}` : ''}.`
: `Kontributor diambil dari riwayat commit sumber Markdown${authorDate ? published ? `. Terbit ${authorDate}` : `. Commit pertama ${authorDate}` : ''}${modifiedDate ? ` · diperbarui ${modifiedDate}` : ''}.`;
const alternates = [post, ...translations].map((translation) => ({
lang: articleLanguage(translation),
href: articleHref(translation)
}));

const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.description,
image: thumbnailUrl,
inLanguage: locale,
...(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) } : {}) })) }
: {}),
publisher: { '@type': 'Organization', name: 'IndopenSource' }
};
---

<BaseLayout
title={seoTitle}
description={post.description}
image={thumbnailUrl}
type="article"
robots={published ? undefined : 'noindex, nofollow, noarchive'}
publishedAt={published ? post.releasedAt : undefined}
modifiedAt={post.lastModifiedAt}
language={language}
alternates={alternates}
jsonLd={jsonLd}
>
<PageHeader eyebrow="Blog" title={post.title} centered>
{!published && (
<div class="mb-6 border-2 border-ink bg-sun p-4 font-body text-ink riso-shadow-sm" role="status">
<strong class="font-display">{english ? 'Draft preview.' : 'Preview draf.'}</strong>
{' '}
{english
? 'This article is under review, unpublished, and excluded from search indexing.'
: 'Artikel ini masih ditinjau, belum diterbitkan, dan tidak diindeks mesin pencari.'}
</div>
)}
<p>{post.description}</p>
<div class="meta-row mt-5 flex flex-wrap items-center justify-center gap-x-3 gap-y-2 font-semibold text-ink uppercase tracking-[0.05em]">
<span class="text-brand-dark">{articlePath}</span>
{releaseDate && <><span aria-hidden="true">·</span><span>{published ? english ? 'Published' : 'Rilis' : english ? 'Draft date' : 'Tanggal draf'} {releaseDate}</span></>}
{authors.length > 0 && <><span aria-hidden="true">·</span><span class="text-brand-dark">{english ? 'by' : 'oleh'} {authors.map((author) => author.name).join(' & ')}</span></>}
</div>
<div class="mt-5 flex flex-wrap items-center justify-center gap-2">
<span class="sticker">{post.status}</span>
{post.tags.map((tag) => <span class="stamp">{tag}</span>)}
{translations.map((translation) => (
<a class="sticker riso-hover no-underline" href={articleHref(translation)} hreflang={articleLanguage(translation)}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor ASTRO_BASE for article links

The raw anchors that render articleHref() bypass withBase/normalizeHref. When the site is built with ASTRO_BASE=/indopensource.org (as the CI build does), this link still points at /en/blog/... or /blog/... instead of /indopensource.org/..., so language switching leaves the configured base and 404s on base-path previews; the same pattern appears in the new English article-card title/image links. Please wrap rendered internal articleHref() hrefs with withBase while keeping sitemap/canonical uses base-free.

Useful? React with 👍 / 👎.

{articleLanguage(translation) === 'en' ? 'Read in English' : 'Baca dalam Bahasa Indonesia'}
</a>
))}
</div>
<figure class="riso-hover riso-shadow mt-8 overflow-hidden border-2 border-ink bg-ink">
<img class="aspect-[16/9] w-full object-cover" src={thumbnailUrl} alt={heroAlt} width="1280" height="720" loading="eager" fetchpriority="high" decoding="async" />
</figure>
</PageHeader>

<article class="mx-auto w-[min(760px,calc(100%-32px))] pt-12 pb-16">
<div class="reveal reveal-1 article-body" set:html={html} />

<footer class="reveal reveal-2 mt-14 border-2 border-ink bg-canvas riso-shadow p-6 md:p-7">
<div class="mb-5 flex items-center gap-3">
<span class="sticker">{english ? 'Authors' : 'Penulis'}</span>
<span class="rule grow"></span>
</div>
<div class="grid gap-4 sm:grid-cols-2">
{authors.map((author) => {
const authorUrl = safeHref(author.url);
return (
<div class="flex items-center gap-4">
{author.avatarUrl && (
<img class="size-14 shrink-0 border-2 border-ink bg-paper riso-shadow-sm" src={author.avatarUrl} alt="" width="56" height="56" loading="lazy" decoding="async" />
)}
<div class="min-w-0">
{authorUrl
? <a class="display-sm text-brand underline decoration-2 underline-offset-2 hover:text-accent" href={authorUrl}>{author.name}</a>
: <span class="display-sm text-brand">{author.name}</span>}
</div>
</div>
);
})}
</div>
<p class="mt-4 font-body text-sm leading-6 text-muted">{authorByline}</p>
{latestCommitUrl && post.latestCommitSha && (
<a class="meta-row mt-2 inline-block font-semibold text-brand underline decoration-2 underline-offset-4 hover:text-accent" href={latestCommitUrl} target="_blank" rel="noopener noreferrer">
{english ? 'Latest commit' : 'Commit terbaru'} {post.latestCommitSha.slice(0, 7)} ↗
</a>
)}
<hr class="rule my-6" />
<div class="flex flex-wrap gap-3">
<a
class="riso-hover riso-shadow-sm inline-flex min-h-11 items-center justify-center border-2 border-ink bg-paper px-4 font-mono text-xs font-bold uppercase tracking-[0.04em] text-ink hover:bg-sun"
href={post.sourceUrl}
>
{english ? 'View Markdown source' : 'Lihat sumber Markdown'}
</a>
<a
class="riso-hover riso-shadow-sm inline-flex min-h-11 items-center justify-center border-2 border-ink bg-paper px-4 font-mono text-xs font-bold uppercase tracking-[0.04em] text-ink hover:bg-sun"
href="https://github.com/IndopenSource/Blog-IndopenSource"
>
{english ? 'Blog repository' : 'Repo blog'}
</a>
</div>
</footer>
</article>
</BaseLayout>
45 changes: 34 additions & 11 deletions src/components/SiteFooter.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
---
import { withBase } from '../lib/urls';

interface Props {
language?: 'id' | 'en';
}

const { language = 'id' } = Astro.props;
const english = language === 'en';

const links: { label: string; href: string; external?: boolean }[] = [
{ label: 'Proyek', href: '/projects/' },
{ label: 'Belajar', href: '/belajar/' },
Expand All @@ -25,6 +32,21 @@ const links: { label: string; href: string; external?: boolean }[] = [
{ label: 'GitHub', href: 'https://github.com/IndopenSource', external: true },
{ label: 'Discussions', href: 'https://github.com/orgs/IndopenSource/discussions', external: true },
];
const englishLabels: Record<string, string> = {
Proyek: 'Projects',
Belajar: 'Learn',
Tentang: 'About',
Falsafah: 'Philosophy',
Komunitas: 'Community',
'Top User': 'Top Users',
'Cara Berkontribusi': 'Contribute',
'Kode Etik': 'Code of Conduct',
Donasi: 'Donate',
Program: 'Programs',
'Tata Kelola': 'Governance',
Event: 'Events',
Kontak: 'Contact'
};

const year = new Date().getFullYear();
---
Expand All @@ -46,8 +68,9 @@ const year = new Date().getFullYear();
<span class="text-sun">Indo</span>penSource
</p>
<p class="mt-4 max-w-xl font-body text-lg leading-7 text-paper/90">
Ruang kerja terbuka untuk ekosistem open source Indonesia, dibikin
bareng, dirawat bareng.
{english
? 'An open workspace for Indonesia’s open source ecosystem, built and maintained together.'
: 'Ruang kerja terbuka untuk ekosistem open source Indonesia, dibikin bareng, dirawat bareng.'}
</p>
</div>
<span class="stamp border-paper bg-sun text-ink">Open source · Indonesia</span>
Expand All @@ -64,27 +87,27 @@ const year = new Date().getFullYear();
<dd class="text-paper/90">github.com/IndopenSource</dd>
</div>
<div class="flex gap-2">
<dt class="text-sun">lisensi</dt>
<dd class="text-paper/90">open source · komunitas</dd>
<dt class="text-sun">{english ? 'license' : 'lisensi'}</dt>
<dd class="text-paper/90">open source · {english ? 'community' : 'komunitas'}</dd>
</div>
<div class="flex gap-2">
<dt class="text-sun">terbit</dt>
<dt class="text-sun">{english ? 'published' : 'terbit'}</dt>
<dd class="text-paper/90">indopensource.org · {year}</dd>
</div>
</dl>

<nav aria-label="Tautan komunitas" class="flex flex-col items-start gap-3">
<span class="meta-row uppercase tracking-[0.12em] text-paper/60">// ruang organisasi</span>
<nav aria-label={english ? 'Community links' : 'Tautan komunitas'} class="flex flex-col items-start gap-3">
<span class="meta-row uppercase tracking-[0.12em] text-paper/60">// {english ? 'organization spaces' : 'ruang organisasi'}</span>
<div class="flex flex-wrap gap-3">
{
links.map(({ label, href, external }) => (
<a
class="sticker riso-hover no-underline"
href={external ? href : withBase(href)}
href={external ? href : withBase(english && label === 'Blog' ? '/en/blog/' : href)}
rel={external ? 'noopener noreferrer' : undefined}
>
{label}
{external && <><span aria-hidden="true">↗</span><span class="sr-only"> (situs eksternal)</span></>}
{english ? englishLabels[label] || label : label}
{external && <><span aria-hidden="true">↗</span><span class="sr-only"> ({english ? 'external site' : 'situs eksternal'})</span></>}
</a>
))
}
Expand All @@ -95,7 +118,7 @@ const year = new Date().getFullYear();
{/* Footer baseline: thin printed credit row in mono */}
<hr class="mt-10 rule" aria-hidden="true" />
<p class="meta-row mt-6 text-paper/60">
Dibuat dengan Joko UI · dibikin di Indonesia
{english ? 'Built with Joko UI · made in Indonesia' : 'Dibuat dengan Joko UI · dibikin di Indonesia'}
</p>
</div>
</footer>
31 changes: 19 additions & 12 deletions src/components/SiteHeader.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,28 @@ import { Picture } from 'astro:assets';
import { withBase } from '../lib/urls';
import brandIcon from '../assets/brand/icon-192.png';

interface Props {
language?: 'id' | 'en';
}

const { language = 'id' } = Astro.props;
const english = language === 'en';

const navItems = [
['Home', '/', 'ph-house'],
['Proyek', '/projects/', 'ph-grid-four'],
['Blog', '/blog/', 'ph-article'],
['Belajar', '/belajar/', 'ph-books'],
[english ? 'Projects' : 'Proyek', '/projects/', 'ph-grid-four'],
['Blog', english ? '/en/blog/' : '/blog/', 'ph-article'],
[english ? 'Learn' : 'Belajar', '/belajar/', 'ph-books'],
['FAQ', '/faq/', 'ph-question'],
['Donasi', '/donasi/', 'ph-heart']
[english ? 'Donate' : 'Donasi', '/donasi/', 'ph-heart']
];

const mobileNavItems = [
['Home', '/', 'ph-house'],
['Proyek', '/projects/', 'ph-grid-four'],
['Blog', '/blog/', 'ph-article'],
['Belajar', '/belajar/', 'ph-books'],
['Top User', '/users/', 'ph-ranking']
[english ? 'Projects' : 'Proyek', '/projects/', 'ph-grid-four'],
['Blog', english ? '/en/blog/' : '/blog/', 'ph-article'],
[english ? 'Learn' : 'Belajar', '/belajar/', 'ph-books'],
[english ? 'Top Users' : 'Top User', '/users/', 'ph-ranking']
];

const currentPath = Astro.url.pathname;
Expand All @@ -34,7 +41,7 @@ const currentPathWithoutBase = currentPath.replace(new RegExp(`^${baseUrl.replac
<header class="sticky top-0 z-20 border-b border-line bg-paper/90 backdrop-blur-xl">
<nav
class="mx-auto flex min-h-16 w-[min(1120px,calc(100%-32px))] items-center justify-between gap-6 py-3"
aria-label="Navigasi utama"
aria-label={english ? 'Main navigation' : 'Navigasi utama'}
>
{/* Wordmark = stamped logo tile + editorial display name + mono kicker */}
<a
Expand All @@ -54,7 +61,7 @@ const currentPathWithoutBase = currentPath.replace(new RegExp(`^${baseUrl.replac
/>
<span class="flex flex-col leading-none">
<span class="font-display text-xl font-extrabold tracking-tight text-ink"><span class="text-brand">Indo</span>penSource</span>
<span class="meta-row mt-0.5 text-muted max-sm:hidden">Komunitas Open Source Indonesia</span>
<span class="meta-row mt-0.5 text-muted max-sm:hidden">{english ? 'Indonesia Open Source Community' : 'Komunitas Open Source Indonesia'}</span>
</span>
</a>

Expand Down Expand Up @@ -95,7 +102,7 @@ const currentPathWithoutBase = currentPath.replace(new RegExp(`^${baseUrl.replac
href="https://github.com/IndopenSource"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub IndopenSource (situs eksternal)"
aria-label={english ? 'IndopenSource GitHub (external site)' : 'GitHub IndopenSource (situs eksternal)'}
>
<i class="ph-fill ph-github-logo text-xl" aria-hidden="true"></i>
</a>
Expand All @@ -109,7 +116,7 @@ const currentPathWithoutBase = currentPath.replace(new RegExp(`^${baseUrl.replac
<nav
class="fixed inset-x-3 bottom-3 z-30 hidden overflow-hidden rounded-2xl border border-line bg-paper/95 shadow-[0_16px_40px_rgba(16,24,40,0.18)] backdrop-blur-xl max-md:grid max-md:grid-cols-5"
style="padding-bottom: env(safe-area-inset-bottom, 0px)"
aria-label="Navigasi mobile"
aria-label={english ? 'Mobile navigation' : 'Navigasi mobile'}
>
{
mobileNavItems.map(([label, href, icon]) => {
Expand Down
Loading