Skip to content

Commit 2e165dc

Browse files
committed
Add blog metadata: Author and Preview
1 parent 66401ec commit 2e165dc

12 files changed

Lines changed: 192 additions & 34 deletions

File tree

.vitepress/config.mts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineConfig } from 'vitepress'
1+
import { defineConfig, HeadConfig } from 'vitepress'
22
import { RssPlugin } from 'vitepress-plugin-rss'
33

44
const baseUrl = 'https://php-testo.github.io'
@@ -148,4 +148,26 @@ export default defineConfig({
148148
provider: 'local',
149149
},
150150
},
151+
152+
transformHead({ pageData }) {
153+
const head: HeadConfig[] = []
154+
155+
if (pageData.frontmatter.image) {
156+
head.push(['meta', { property: 'og:image', content: baseUrl + pageData.frontmatter.image }])
157+
head.push(['meta', { name: 'twitter:image', content: baseUrl + pageData.frontmatter.image }])
158+
head.push(['meta', { name: 'twitter:card', content: 'summary_large_image' }])
159+
}
160+
161+
if (pageData.frontmatter.description) {
162+
head.push(['meta', { property: 'og:description', content: pageData.frontmatter.description }])
163+
head.push(['meta', { name: 'twitter:description', content: pageData.frontmatter.description }])
164+
}
165+
166+
if (pageData.frontmatter.title) {
167+
head.push(['meta', { property: 'og:title', content: pageData.frontmatter.title }])
168+
head.push(['meta', { name: 'twitter:title', content: pageData.frontmatter.title }])
169+
}
170+
171+
return head
172+
},
151173
})
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
import { useData } from 'vitepress'
3+
4+
const { frontmatter } = useData()
5+
6+
function formatDate(date: string | Date): string {
7+
const d = new Date(date)
8+
return d.toISOString().slice(0, 10)
9+
}
10+
</script>
11+
12+
<template>
13+
<div class="blog-post-header" v-if="frontmatter.image || frontmatter.date || frontmatter.author">
14+
<img
15+
v-if="frontmatter.image"
16+
:src="frontmatter.image"
17+
:alt="frontmatter.title"
18+
class="post-hero-image"
19+
/>
20+
<div class="post-meta" v-if="frontmatter.date || frontmatter.author">
21+
<span v-if="frontmatter.date" class="post-date">{{ formatDate(frontmatter.date) }}</span>
22+
<span v-if="frontmatter.author" class="post-author">{{ frontmatter.author }}</span>
23+
</div>
24+
</div>
25+
</template>
26+
27+
<style scoped>
28+
.blog-post-header {
29+
margin-bottom: 1.5rem;
30+
}
31+
32+
.post-hero-image {
33+
width: 100%;
34+
height: auto;
35+
border-radius: 12px;
36+
margin-bottom: 1rem;
37+
}
38+
39+
.post-meta {
40+
display: flex;
41+
gap: 1rem;
42+
color: var(--vp-c-text-2);
43+
font-size: 0.9rem;
44+
}
45+
</style>

.vitepress/theme/BlogPosts.vue

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,40 +12,111 @@ const filteredPosts = computed(() => {
1212
</script>
1313

1414
<template>
15-
<ul class="blog-posts">
16-
<li v-for="post in filteredPosts" :key="post.url">
17-
<div class="post-header">
18-
<a :href="post.url">{{ post.title }}</a>
15+
<div class="blog-posts">
16+
<article v-for="post in filteredPosts" :key="post.url" class="post-card">
17+
<a :href="post.url" class="post-image-link">
18+
<img v-if="post.image" :src="post.image" :alt="post.title" class="post-image" />
19+
</a>
20+
<div class="post-content">
21+
<div class="post-title">
22+
<a :href="post.url">{{ post.title }}</a>
23+
</div>
24+
<p class="post-description">{{ post.description }}</p>
25+
<div class="post-meta">
26+
<span class="post-date">{{ post.date }}</span>
27+
<span v-if="post.author" class="post-author">{{ post.author }}</span>
28+
</div>
1929
</div>
20-
<p class="post-description">{{ post.description }}</p>
21-
<div class="timestamp">{{ post.date }}</div>
22-
</li>
23-
</ul>
30+
</article>
31+
</div>
2432
</template>
2533

2634
<style scoped>
2735
.blog-posts {
28-
list-style: none;
29-
padding: 0;
36+
display: flex;
37+
flex-direction: column;
38+
gap: 2rem;
3039
}
3140
32-
.blog-posts .post-header a {
33-
text-decoration: none !important;
41+
.post-card {
42+
display: flex;
43+
align-items: center;
44+
gap: 1.5rem;
45+
padding-bottom: 2rem;
46+
border-bottom: 1px solid var(--vp-c-divider);
3447
}
3548
36-
.blog-posts li {
37-
margin: 1.5em 0;
49+
.post-card:last-child {
50+
border-bottom: none;
3851
}
3952
40-
.blog-posts .timestamp {
41-
margin-right: 0.5em;
42-
color: var(--vp-c-text-2);
43-
font-size: 0.8em;
53+
.post-image-link {
54+
flex-shrink: 0;
55+
display: block;
56+
width: 200px;
57+
aspect-ratio: 16 / 10;
58+
overflow: hidden;
59+
border-radius: 8px;
60+
background: var(--vp-c-bg-soft);
4461
}
4562
46-
.post-description {
47-
margin: 0.25em 0 0 0;
63+
.post-image {
64+
width: 100%;
65+
height: 100%;
66+
object-fit: cover;
67+
transition: transform 0.2s ease;
68+
}
69+
70+
.post-image-link:hover .post-image {
71+
transform: scale(1.05);
72+
}
73+
74+
.post-content {
75+
display: flex;
76+
flex-direction: column;
77+
gap: 0.5rem;
78+
min-width: 0;
79+
}
80+
81+
.post-title {
82+
margin: 0;
83+
font-size: 1.25rem;
84+
font-weight: 600;
85+
line-height: 1.3;
86+
}
87+
88+
.post-title a {
89+
text-decoration: none;
4890
color: var(--vp-c-text-1);
49-
font-size: 0.9em;
91+
}
92+
93+
.post-title a:hover {
94+
color: var(--vp-c-brand-1);
95+
}
96+
97+
.post-description {
98+
margin: 0;
99+
color: var(--vp-c-text-2);
100+
font-size: 0.95rem;
101+
line-height: 1.5;
102+
}
103+
104+
.post-meta {
105+
display: flex;
106+
gap: 1rem;
107+
color: var(--vp-c-text-3);
108+
font-size: 0.85rem;
109+
margin-top: auto;
110+
}
111+
112+
@media (max-width: 640px) {
113+
.post-card {
114+
flex-direction: column;
115+
}
116+
117+
.post-image-link {
118+
width: 100%;
119+
aspect-ratio: 16 / 9;
120+
}
50121
}
51122
</style>

.vitepress/theme/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
import { h } from 'vue'
22
import type { Theme } from 'vitepress'
3+
import { useRoute } from 'vitepress'
34
import DefaultTheme from 'vitepress/theme'
45
import BlogSponsor from './BlogSponsor.vue'
56
import GitHubStars from './GitHubStars.vue'
67
import CodeTabs from './CodeTabs.vue'
78
import JetBrainsPluginButton from './JetBrainsPluginButton.vue'
89
import BlogPosts from './BlogPosts.vue'
10+
import BlogPostHeader from './BlogPostHeader.vue'
911
import './style.css'
1012

13+
function isBlogPost() {
14+
const route = useRoute()
15+
const path = route.path
16+
return (path.startsWith('/blog/') || path.startsWith('/ru/blog/')) &&
17+
path !== '/blog/' && path !== '/ru/blog/'
18+
}
19+
1120
export default {
1221
extends: DefaultTheme,
1322
Layout: () => {
1423
return h(DefaultTheme.Layout, null, {
24+
'doc-before': () => isBlogPost() ? h(BlogPostHeader) : null,
1525
'doc-after': () => h(BlogSponsor),
1626
'nav-bar-content-after': () => h(GitHubStars),
1727
})

.vitepress/theme/posts.data.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export interface Post {
55
url: string
66
date: string
77
description: string
8+
image?: string
9+
author?: string
810
}
911

1012
declare const data: Post[]
@@ -19,6 +21,8 @@ export default createContentLoader(['blog/*.md', 'ru/blog/*.md'], {
1921
url: page.url,
2022
date: formatDate(page.frontmatter.date),
2123
description: page.frontmatter.description,
24+
image: page.frontmatter.image,
25+
author: page.frontmatter.author,
2226
}))
2327
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
2428
},

CLAUDE.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,20 @@ ru/ # Russian locale (same structure)
4848
1. Create both `blog/post.md` (EN) and `ru/blog/post.md` (RU)
4949
2. Add link to `blog/index.md` and `ru/blog/index.md`
5050

51-
**Required frontmatter for RSS:**
51+
**Required frontmatter:**
5252
```yaml
5353
---
5454
title: "Post Title"
5555
date: 2025-01-01
56-
description: "Short description for RSS feed"
56+
description: "Short description for RSS and sharing"
57+
image: /blog/post-name/preview.jpg
58+
author: Author Name
5759
---
5860
```
5961

60-
All three fields (`title`, `date`, `description`) are required for proper RSS generation.
62+
- `title`, `date`, `description` — required for RSS
63+
- `image` — used for preview in blog list, og:image for social sharing, and displayed in post header
64+
- `author` — displayed in blog list and post header
6165

6266
## VitePress Commands
6367

blog/assert-and-expect.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
title: "Assert and Expect"
33
date: 2026-01-01
44
description: "Exploring assertion API design in Testo: why we split Assert and Expect facades, and how pipe assertions make tests cleaner."
5+
image: /blog/assert-and-expect/img-1.jpg
6+
author: Aleksei Gagarin
57
---
68

79
# Testo. Assert and Expect
810

9-
![Testo Assert and Expect](/blog/assert-and-expect/img-1.jpg)
10-
1111
Let's talk about the pitfalls of reinventing the wheel that I've already stumbled upon while building a new testing framework [Testo](https://github.com/php-testo/testo).
1212

1313
PHPUnit provides multiple ways to write the same assertions in tests:

blog/filters.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
title: "Filters"
33
date: 2025-11-07
44
description: "Multi-layered filtering system in Testo: from Test Suite level down to individual test methods."
5+
image: /blog/filters/img-1.jpg
6+
author: Aleksei Gagarin
57
---
68

79
# Testo. Filters
810

9-
![Filters](/blog/filters/img-1.jpg)
10-
1111
Filters are needed to narrow down the set of tests to run. In other words, it's the ability to select tests before running them.
1212

1313
Adding filters is one of the important milestones on the road to version 1.0.0

blog/khinkali.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
title: "Khinkali"
33
date: 2025-10-30
44
description: "The story behind the khinkali icon for Testo IDE plugin."
5+
author: Aleksei Gagarin
56
---
67

78
# Khinkali

ru/blog/assert-and-expect.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
title: "Assert и Expect"
33
date: 2026-01-01
44
description: "Исследуем дизайн API утверждений в Testo: почему мы разделили фасады Assert и Expect, и как пайп-ассерты делают тесты чище."
5+
image: /blog/assert-and-expect/img-1.jpg
6+
author: Алексей Гагарин
57
---
68

79
# Testo. Assert и Expect
810

9-
![Testo Assert и Expect](/blog/assert-and-expect/img-1.jpg)
10-
1111
Поговорим про грабли велосипедостроения, с которыми я уже познакомился при написании нового фреймворка тестирования [Testo](https://github.com/php-testo/testo).
1212

1313
PHPUnit предоставляет множество вариантов одних и тех же проверок (утверждений) в тестах:

0 commit comments

Comments
 (0)