Skip to content
Draft
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
207 changes: 207 additions & 0 deletions docs/src/pages/blog/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
import Base from "@/layouts/Base.astro";
import CallToAction from "@/partials/CallToAction.astro";
import config from "@/config/config.json";
import { dateFormat } from "@/lib/utils/dateFormat";

interface WordPressPost {
slug: string;
title: { rendered: string };
modified: string;
excerpt?: { rendered?: string };
yoast_head_json?: {
og_description?: string;
description?: string;
};
}

const blogBaseUrl = config.blog.base_url;
const {
site: { base_url },
} = config;

let posts: WordPressPost[] = [];

try {
const res = await fetch(
`${blogBaseUrl}/wp-json/wp/v2/posts?_embed&per_page=100`,
);
posts = (await res.json()) as WordPressPost[];
} catch (e) {
throw new Error(
`Failed to fetch blog posts from "${blogBaseUrl}/wp-json/wp/v2/posts": ${e instanceof Error ? e.message : String(e)}`,
);
}

const items = posts
.map((post) => {
const yoast = post.yoast_head_json || {};
const rawDescription =
yoast.og_description || yoast.description || post.excerpt?.rendered || "";
const description = rawDescription.replace(/<[^>]+>/g, "").trim();

return {
slug: post.slug,
title: post.title.rendered,
modified: post.modified,
description,
};
})
.sort(
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime(),
);

const [featured, ...rest] = items;

const seo = {
title: "Blog - RocketSim",
description:
"Tips, guides, and deep dives for iOS developers who want to move faster.",
canonical: `${base_url}/blog/`,
};

const structuredData = {
type: "static" as const,
};
---

<Base seo={seo} structuredData={structuredData}>
<section class="ph-spacing">
<div class="container">
<div class="flex flex-col items-center text-center max-w-[680px] mx-auto">
<span
class="inline-flex items-center gap-1.5 rounded-full border border-primary/30 bg-primary/10 px-3.5 py-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-primary"
data-aos="fade-up-sm"
data-aos-delay="0"
>
Developer Resources
</span>
<h1
class="page-heading mt-5 !mb-4 hasEmphasize text-balance"
data-aos="fade-up-sm"
data-aos-delay="50"
>
The RocketSim Blog
</h1>
<p
class="text-lg text-text text-balance leading-relaxed"
data-aos="fade-up-sm"
data-aos-delay="100"
>
Tips, guides, and deep dives for iOS developers who want to move
faster.
</p>
</div>
</div>
</section>

<section class="section pt-12">
<div class="container">
<div class="mx-auto flex max-w-[1080px] flex-col gap-9">
{
featured && (
<a
href={`/blog/${featured.slug}/`}
class="featured-card group relative grid grid-cols-1 items-center gap-8 overflow-hidden rounded-2xl border border-white/8 bg-white/[0.03] p-8 transition-all duration-200 hover:border-white/14 hover:bg-white/[0.05] md:grid-cols-[1fr_auto] md:gap-12 md:p-11"
data-aos="fade-up-sm"
data-aos-delay="0"
>
<span class="featured-accent absolute inset-y-0 left-0 w-[3px] bg-primary opacity-50 transition-opacity duration-200 group-hover:opacity-100" />

<div class="flex flex-col gap-3.5">
<div class="flex items-center gap-2.5">
<span class="inline-flex items-center rounded-full border border-primary/25 bg-primary/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.06em] text-primary">
Latest
</span>
<span class="text-[11px] font-semibold uppercase tracking-[0.06em] text-text-dark/30">
Featured
</span>
</div>

<h2
class="text-2xl font-bold leading-snug tracking-tight text-text-light text-balance md:text-[28px]"
set:html={featured.title}
/>

{featured.description && (
<p class="max-w-[580px] text-[15.5px] leading-[1.7] text-text-dark/50 text-balance">
{featured.description}
</p>
)}

<div class="mt-1 flex items-center gap-4 text-sm text-text-dark/30">
<span>
Last updated {dateFormat(featured.modified, "MMM dd, yyyy")}
</span>
</div>
</div>

<div class="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full border border-white/8 bg-white/5 transition-all duration-200 group-hover:border-primary/40 group-hover:bg-primary/20">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
class="text-text-dark/40 transition-colors duration-200 group-hover:text-primary"
>
<path
d="M3 8h10M9 4l4 4-4 4"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
</a>
)
}

{
rest.length > 0 && (
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3">
{rest.map((post, index) => (
<a
href={`/blog/${post.slug}/`}
class="post-card group relative flex flex-col gap-3 overflow-hidden rounded-[14px] border border-white/7 bg-white/[0.025] px-7 pb-6 pt-6 transition-all duration-200 hover:border-white/12 hover:bg-white/[0.05]"
data-aos="fade-up-sm"
data-aos-delay={50 + index * 25}
>
<span class="post-accent absolute inset-x-0 top-0 h-[2px] bg-primary opacity-0 transition-opacity duration-200 group-hover:opacity-70" />

<span class="text-xs text-text-dark/25">
{dateFormat(post.modified, "MMM dd, yyyy")}
</span>

<h3
class="text-[17px] font-semibold leading-snug tracking-tight text-text-light/90 text-balance transition-colors duration-150 group-hover:text-text-light"
set:html={post.title}
/>

{post.description && (
<p class="line-clamp-2 text-[13.5px] leading-[1.65] text-text-dark/40 text-balance">
{post.description}
</p>
)}
</a>
))}
</div>
)
}
</div>
</div>
</section>

<CallToAction />
</Base>

<style>
.featured-card,
.post-card {
text-decoration: none;
}

.post-card {
min-height: 180px;
}
</style>
Loading