Skip to content

Commit f66dbdc

Browse files
committed
#3414 webpage: add blog overview page
Signed-off-by: Patrizio Bekerle <[email protected]>
1 parent c3c599e commit f66dbdc

File tree

6 files changed

+146
-24
lines changed

6 files changed

+146
-24
lines changed

webpage/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ basement_dist
1414
.shared
1515
src/.vuepress/.cache
1616
src/.vuepress/.temp
17+
src/.vuepress/blog-data.json
1718
test-results
1819
playwright-report/
1920

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Blog Data Generator
5+
*
6+
* This script generates a JSON file containing blog post metadata from markdown files.
7+
* It runs automatically before:
8+
* - Development server starts (npm run dev)
9+
* - Production build (npm run build)
10+
*
11+
* The generated file (src/.vuepress/blog-data.json) is:
12+
* - Used by the BlogIndex.vue component to display blog posts
13+
* - Ignored by git (listed in .gitignore)
14+
* - Regenerated on each build/dev run
15+
*/
16+
17+
import fs from "fs";
18+
import { globSync } from "glob";
19+
import markdownIt from "markdown-it";
20+
import meta from "markdown-it-meta";
21+
import { format, parse } from "date-fns";
22+
23+
// Load all blog MD files
24+
const generateBlogData = () => {
25+
const paths = globSync("src/blog/**/*.md");
26+
27+
const files = paths
28+
.map((path) => {
29+
// Skip the README file (Overview page)
30+
if (path.endsWith("README.md")) {
31+
return null;
32+
}
33+
34+
// Read file content
35+
const fileContent = fs.readFileSync(path, "utf8");
36+
37+
// Instantiate MarkdownIt
38+
const md = new markdownIt();
39+
// Add markdown-it-meta
40+
md.use(meta);
41+
md.render(fileContent);
42+
43+
// Extract title from first H1 heading
44+
const h1Match = fileContent.match(/^#\s+(.+)$/m);
45+
const title = md.meta.title || (h1Match ? h1Match[1] : "Untitled");
46+
47+
// Extract description from frontmatter or after title
48+
const description = md.meta.description || "";
49+
50+
// Extract date from filename (format: YYYY-MM-DD)
51+
const dateMatch = path.match(/(\d{4}-\d{2}-\d{2})/);
52+
const dateFromFilename = dateMatch ? dateMatch[1] : "";
53+
54+
// Use frontmatter date if available, otherwise use filename date
55+
const rawDate = md.meta.date || md.meta.order || dateFromFilename;
56+
57+
// Convert path to URL path
58+
const urlPath = path.replace("src", "").replace(".md", ".html");
59+
60+
// Format date
61+
let formattedDate = dateFromFilename;
62+
if (rawDate) {
63+
try {
64+
// Try parsing with different formats
65+
let parsedDate;
66+
if (typeof rawDate === "string" && rawDate.includes("T")) {
67+
parsedDate = parse(rawDate, "yyyy-MM-dd'T'HH", new Date());
68+
} else if (dateFromFilename) {
69+
parsedDate = parse(dateFromFilename, "yyyy-MM-dd", new Date());
70+
}
71+
if (parsedDate && !isNaN(parsedDate.getTime())) {
72+
formattedDate = format(parsedDate, "yyyy-MM-dd");
73+
}
74+
} catch (e) {
75+
// Keep date from filename if parsing fails
76+
}
77+
}
78+
79+
return {
80+
path: urlPath,
81+
title: title,
82+
description: description,
83+
date: formattedDate,
84+
rawDate: dateFromFilename || rawDate,
85+
};
86+
})
87+
.filter((item) => item !== null);
88+
89+
// Sort by date (newest first)
90+
files.sort((a, b) => {
91+
const dateA = a.rawDate || "";
92+
const dateB = b.rawDate || "";
93+
return String(dateB).localeCompare(String(dateA));
94+
});
95+
96+
// Write to JSON file
97+
const outputPath = "src/.vuepress/blog-data.json";
98+
fs.writeFileSync(outputPath, JSON.stringify(files, null, 2));
99+
100+
console.log(
101+
`✓ Generated blog data with ${files.length} entries at ${outputPath}`,
102+
);
103+
};
104+
105+
generateBlogData();

webpage/scripts/run-build.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ echo "📦 Copying assets..."
1717
cp -R ../screenshots src/.vuepress/public 2>/dev/null || echo "⚠️ Screenshots not found, skipping"
1818
cp ../CHANGELOG.md src/changelog.md 2>/dev/null || echo "⚠️ CHANGELOG.md not found, skipping"
1919

20+
echo ""
21+
echo "📝 Generating blog index data..."
22+
node scripts/generate-blog-data.js
23+
24+
echo ""
2025
echo "🏗️ Building with VuePress 2..."
2126
echo ""
2227

webpage/scripts/run-dev.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ echo "📦 Copying assets..."
1010
cp -R ../screenshots src/.vuepress/public 2>/dev/null || echo "⚠️ Screenshots not found, skipping"
1111
cp ../CHANGELOG.md src/changelog.md 2>/dev/null || echo "⚠️ CHANGELOG.md not found, skipping"
1212

13+
echo ""
14+
echo "📝 Generating blog index data..."
15+
node scripts/generate-blog-data.js
16+
17+
echo ""
1318
echo "🚀 Starting VuePress dev server..."
1419
echo ""
1520

webpage/src/.vuepress/components/BlogIndex.vue

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
<template>
22
<div>
33
<v-card
4-
v-for="page in files"
4+
v-for="page in blogPages"
55
:key="page.path"
66
class="mx-auto"
77
variant="outlined"
88
>
99
<v-list-item>
1010
<template v-slot:default>
1111
<div class="overline mb-4">
12-
{{ formatDate(page.frontmatter.date) }}
12+
{{ page.date }}
1313
</div>
1414
<v-list-item-title class="text-h5 mb-1">
1515
<a :href="page.path">{{ page.title }}</a>
1616
</v-list-item-title>
1717
<v-list-item-subtitle>
18-
{{ page.frontmatter.description }}
18+
{{ page.description }}
1919
</v-list-item-subtitle>
2020
</template>
2121
</v-list-item>
@@ -24,30 +24,15 @@
2424
</template>
2525

2626
<script setup>
27-
import { computed } from "vue";
28-
import { usePageData, useSiteData } from "vuepress/client";
29-
import { format, parse } from "date-fns";
27+
import { ref, onMounted } from "vue";
28+
import blogData from "../blog-data.json";
3029
31-
const siteData = useSiteData();
30+
const blogPages = ref([]);
3231
33-
const files = computed(() => {
34-
const pages = siteData.value.pages;
35-
if (!pages || !Array.isArray(pages)) {
36-
return [];
37-
}
38-
return [...pages]
39-
.reverse()
40-
.filter((p) => p.path.indexOf("/blog/") >= 0 && p.title !== "Overview");
32+
onMounted(() => {
33+
// Load blog data from generated JSON file
34+
blogPages.value = blogData;
4135
});
42-
43-
const formatDate = (dateString) => {
44-
try {
45-
const date = parse(dateString, "yyyy-MM-dd'T'HH", new Date());
46-
return format(date, "yyyy-MM-dd");
47-
} catch (e) {
48-
return dateString;
49-
}
50-
};
5136
</script>
5237

5338
<style scoped>

webpage/tests/blog.spec.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@ test("blog overview page", async ({ page, config }) => {
77
await expect(
88
page.getByRole("heading", { level: 1, name: "Blog" }),
99
).toBeVisible();
10+
11+
// Check that blog entries are displayed
12+
// There should be multiple blog post cards
13+
const blogCards = page.locator(".v-card");
14+
await expect(blogCards).not.toHaveCount(0);
15+
16+
// Check that the first blog entry has a title link
17+
const firstCard = blogCards.first();
18+
const firstLink = firstCard.locator("a");
19+
await expect(firstLink).toBeVisible();
20+
await expect(firstLink).toHaveAttribute("href", /\/blog\/.+\.html/);
21+
22+
// Check that blog entries have dates
23+
const dateElement = firstCard.locator(".overline");
24+
await expect(dateElement).toBeVisible();
25+
// Date should be in YYYY-MM-DD format
26+
await expect(dateElement).toHaveText(/\d{4}-\d{2}-\d{2}/);
27+
28+
// Verify we have at least a few blog entries (should have 80 based on generation)
29+
const cardCount = await blogCards.count();
30+
expect(cardCount).toBeGreaterThan(5);
1031
});
1132

1233
test("blog entry - AI support", async ({ page, config }) => {

0 commit comments

Comments
 (0)