Skip to content

Commit 87ca832

Browse files
committed
add blog feed component and related styles; create new blog posts for runtime generics and TypePHP introduction
1 parent de48ffd commit 87ca832

5 files changed

Lines changed: 456 additions & 36 deletions

File tree

docs/.vitepress/data/posts.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"title": "How to Enforce Runtime Generics on Third-Party Collections (Doctrine, Ramsey) with TypePHP",
4+
"url": "/docs/blog/reifying-third-party-collections",
5+
"date": "August 18, 2026",
6+
"readTime": "4 min read",
7+
"badge": "Guide",
8+
"badgeClass": "badge-guide",
9+
"excerpt": "A step-by-step tutorial on making third-party collections (Doctrine ArrayCollection, Ramsey Collection) reified and strictly type-checked at runtime using path whitelisting and TypePHP extensions."
10+
},
11+
{
12+
"title": "The Death of the 'DocBlock Lie': Introducing TypePHP and Runtime Reified Generics in Pure PHP",
13+
"url": "/docs/blog/introducing-typephp",
14+
"date": "August 18, 2026",
15+
"readTime": "5 min read",
16+
"badge": "Announcement",
17+
"badgeClass": "badge-announcement",
18+
"excerpt": "An introduction to TypePHP: how it uses Load-Time Weaving via native StreamWrappers to enforce PHPDoc contracts at runtime, eliminates the \"DocBlock Lie\", and brings true Reified Generics to modern PHP without transpilers or C-extensions."
19+
}
20+
]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<template>
2+
<div class="blog-feed-container">
3+
<div class="blog-feed">
4+
<article v-for="post in paginatedPosts" :key="post.url" class="blog-post-card">
5+
<div class="blog-post-meta">
6+
<span :class="['blog-badge', post.badgeClass || 'badge-guide']">
7+
{{ post.badge }}
8+
</span>
9+
<time>{{ post.date }}</time>
10+
<span class="dot">•</span>
11+
<span>{{ post.readTime }}</span>
12+
</div>
13+
14+
<h2 class="blog-post-title">
15+
<a :href="post.url">{{ post.title }}</a>
16+
</h2>
17+
18+
<p class="blog-post-excerpt">
19+
{{ post.excerpt }}
20+
</p>
21+
22+
<div class="blog-post-footer">
23+
<a class="blog-read-more" :href="post.url">Read Article →</a>
24+
</div>
25+
</article>
26+
</div>
27+
28+
<div v-if="totalPages > 1" class="blog-pagination">
29+
<button class="pagination-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">
30+
← Newer
31+
</button>
32+
33+
<div class="pagination-pages">
34+
<button v-for="page in totalPages" :key="page"
35+
:class="['page-number', { active: currentPage === page }]" @click="goToPage(page)">
36+
{{ page }}
37+
</button>
38+
</div>
39+
40+
<button class="pagination-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">
41+
Older →
42+
</button>
43+
</div>
44+
</div>
45+
</template>
46+
47+
<script setup lang="ts">
48+
import { ref, computed } from 'vue'
49+
import rawPosts from '../../data/posts.json'
50+
51+
export interface BlogPost {
52+
title: string
53+
url: string
54+
date: string
55+
readTime: string
56+
badge: string
57+
badgeClass?: string
58+
excerpt: string
59+
}
60+
61+
const posts: BlogPost[] = rawPosts
62+
const POSTS_PER_PAGE = 5
63+
const currentPage = ref(1)
64+
65+
const totalPages = computed(() => Math.ceil(posts.length / POSTS_PER_PAGE))
66+
67+
const paginatedPosts = computed(() => {
68+
const start = (currentPage.value - 1) * POSTS_PER_PAGE
69+
return posts.slice(start, start + POSTS_PER_PAGE)
70+
})
71+
72+
function goToPage(page: number) {
73+
if (page >= 1 && page <= totalPages.value) {
74+
currentPage.value = page
75+
window.scrollTo({ top: 0, behavior: 'smooth' })
76+
}
77+
}
78+
</script>

docs/.vitepress/theme/custom.css

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@
3333
}
3434
}
3535

36-
/* ================= BLOG FEED STYLES ================= */
36+
.blog-feed-container {
37+
margin-top: 24px;
38+
}
39+
3740
.blog-feed {
3841
display: flex;
3942
flex-direction: column;
4043
gap: 24px;
41-
margin-top: 32px;
4244
}
4345

4446
.blog-post-card {
@@ -73,6 +75,12 @@
7375
letter-spacing: 0.5px;
7476
}
7577

78+
.badge-guide {
79+
background: rgba(59, 130, 246, 0.15);
80+
color: #3b82f6;
81+
border: 1px solid rgba(59, 130, 246, 0.3);
82+
}
83+
7684
.badge-announcement {
7785
background: rgba(16, 185, 129, 0.15);
7886
color: #10b981;
@@ -107,7 +115,7 @@
107115
.blog-post-footer {
108116
display: flex;
109117
align-items: center;
110-
justify-content: space-between;
118+
justify-content: flex-end;
111119
padding-top: 14px;
112120
border-top: 1px solid var(--vp-c-divider);
113121
font-size: 13.5px;
@@ -139,19 +147,19 @@
139147
border-radius: 6px;
140148
font-size: 13.5px;
141149
color: var(--vp-c-text-1);
142-
text-decoration: none;
150+
background: var(--vp-c-bg-soft);
151+
cursor: pointer;
143152
transition: all 0.2s ease;
144153
}
145154

146-
.pagination-btn:hover:not(.disabled) {
155+
.pagination-btn:hover:not(:disabled) {
147156
border-color: var(--vp-c-brand-1);
148157
color: var(--vp-c-brand-1);
149158
}
150159

151-
.pagination-btn.disabled {
160+
.pagination-btn:disabled {
152161
opacity: 0.4;
153162
cursor: not-allowed;
154-
pointer-events: none;
155163
}
156164

157165
.pagination-pages {
@@ -169,10 +177,20 @@
169177
font-size: 14px;
170178
font-weight: 600;
171179
border: 1px solid var(--vp-c-divider);
180+
background: var(--vp-c-bg-soft);
181+
color: var(--vp-c-text-1);
182+
cursor: pointer;
183+
transition: all 0.2s ease;
172184
}
173185

174186
.page-number.active {
175187
background: var(--vp-c-brand-1);
176188
color: #fff;
177189
border-color: var(--vp-c-brand-1);
190+
}
191+
192+
.badge-guide {
193+
background: rgba(59, 130, 246, 0.15);
194+
color: #3b82f6;
195+
border: 1px solid rgba(59, 130, 246, 0.3);
178196
}

docs/blog/index.md

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,12 @@ prev: false
66
next: false
77
---
88

9+
<script setup>
10+
import BlogFeed from '../.vitepress/theme/components/BlogFeed.vue'
11+
</script>
12+
913
# TypePHP Blog & Engineering Insights
1014

1115
Articles, architecture deep-dives, release notes, and tutorials from the TypePHP team.
1216

13-
<div class="blog-feed">
14-
15-
<article class="blog-post-card">
16-
<div class="blog-post-meta">
17-
<span class="blog-badge badge-announcement">Announcement</span>
18-
<time datetime="2026-08-18">August 18, 2026</time>
19-
<span class="dot">•</span>
20-
<span>3 min read</span>
21-
</div>
22-
<h2 class="blog-post-title">
23-
<a href="/docs/blog/introducing-typephp">The Death of the 'DocBlock Lie': Introducing TypePHP and Runtime Reified Generics in Pure PHP</a>
24-
</h2>
25-
<p class="blog-post-excerpt">
26-
An introduction to TypePHP: how it uses Load-Time Weaving via native StreamWrappers to enforce PHPDoc contracts at runtime, eliminates the "DocBlock Lie", and brings true Reified Generics to modern PHP without transpilers or C-extensions.
27-
</p>
28-
<div class="blog-post-footer">
29-
<a class="blog-read-more" href="/docs/blog/introducing-typephp">Read Article →</a>
30-
</div>
31-
</article>
32-
33-
</div>
34-
35-
<div class="blog-pagination">
36-
<span class="pagination-btn disabled">← Newer</span>
37-
<div class="pagination-pages">
38-
<span class="page-number active">1</span>
39-
</div>
40-
<span class="pagination-btn disabled">Older →</span>
41-
</div>
17+
<BlogFeed />

0 commit comments

Comments
 (0)