@eox/pages-theme-eox 1.9.0 → 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +4 -1
- package/README.md +88 -11
- package/cypress/support/mocks/authors.data.js +14 -0
- package/cypress/support/mocks/helpers.js +65 -4
- package/cypress/support/mocks/vitepress.js +8 -1
- package/cypress.config.js +14 -0
- package/package.json +3 -1
- package/src/Layout.vue +69 -1
- package/src/authors.data.js +14 -0
- package/src/components/BlogList.vue +655 -0
- package/src/components/BlogPost.vue +472 -0
- package/src/components/CTASection.vue +18 -4
- package/src/components/FeatureCard/Index.vue +462 -31
- package/src/components/FeatureCard/Poster.vue +25 -3
- package/src/components/FeatureSection.cy.js +16 -0
- package/src/components/FeatureSection.vue +3 -8
- package/src/components/FeaturesGallery.vue +34 -2
- package/src/components/Footer.cy.js +52 -2
- package/src/components/Footer.vue +17 -5
- package/src/components/HeroSection.cy.js +20 -0
- package/src/components/HeroSection.vue +140 -25
- package/src/components/ImageCarousel.vue +11 -2
- package/src/components/ImageElement.vue +16 -4
- package/src/components/ImageGallery.vue +29 -4
- package/src/components/LogoSection.vue +26 -3
- package/src/components/MobileNavDropdown.vue +13 -3
- package/src/components/NavBar.cy.js +15 -0
- package/src/components/NavBar.vue +149 -21
- package/src/components/NavDropdown.vue +12 -2
- package/src/components/OpenPositions.vue +292 -0
- package/src/components/SectionHeader.cy.js +54 -0
- package/src/components/SectionHeader.vue +115 -0
- package/src/components/TagList.vue +311 -0
- package/src/components/TeamPage.vue +347 -0
- package/src/components/TestimonialsSection.cy.js +76 -0
- package/src/components/TestimonialsSection.vue +368 -0
- package/src/components/TextSection.cy.js +115 -0
- package/src/components/TextSection.vue +396 -0
- package/src/helpers.js +182 -37
- package/src/helpers.test.js +181 -0
- package/src/index.js +16 -0
- package/src/pages/author/[author].md +5 -0
- package/src/pages/author/[author].paths.js +60 -0
- package/src/pages/blog.md +5 -0
- package/src/pages/tag/[tag].md +5 -0
- package/src/pages/tag/[tag].paths.js +49 -0
- package/src/pages/tag.md +5 -0
- package/src/pages/team.md +5 -0
- package/src/posts.data.js +134 -0
- package/src/style.css +389 -8
- package/src/tagUtils.js +148 -0
- package/src/team.data.js +1 -1
- package/src/vitepressConfig.mjs +101 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
normalizeTrackingUrl,
|
|
5
|
+
trackPageView,
|
|
6
|
+
trackEvent,
|
|
7
|
+
trackSiteSearch,
|
|
8
|
+
formatPrice,
|
|
9
|
+
t,
|
|
10
|
+
isActive,
|
|
11
|
+
} from "./helpers.js";
|
|
12
|
+
|
|
13
|
+
describe("normalizeTrackingUrl", () => {
|
|
14
|
+
it("strips trailing slashes from path", () => {
|
|
15
|
+
assert.equal(normalizeTrackingUrl("/explore/"), "/explore");
|
|
16
|
+
assert.equal(normalizeTrackingUrl("/explore"), "/explore");
|
|
17
|
+
assert.equal(normalizeTrackingUrl("/story/"), "/story");
|
|
18
|
+
assert.equal(normalizeTrackingUrl("/news_updates/"), "/news_updates");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps single root slash as /", () => {
|
|
22
|
+
assert.equal(normalizeTrackingUrl("/"), "/");
|
|
23
|
+
assert.equal(normalizeTrackingUrl(""), "");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("removes volatile map coordinate parameters (x, y, z) and keeps template/zoom/datetime", () => {
|
|
27
|
+
assert.equal(
|
|
28
|
+
normalizeTrackingUrl("/explore/?x=15.0000&y=48.0000&z=4.0000"),
|
|
29
|
+
"/explore",
|
|
30
|
+
);
|
|
31
|
+
assert.equal(
|
|
32
|
+
normalizeTrackingUrl(
|
|
33
|
+
"/explore/?x=15.0000&y=48.0000&z=4.0000&template=expert&indicator=N11&datetime=2024-08-16&zoom=5",
|
|
34
|
+
),
|
|
35
|
+
"/explore?datetime=2024-08-16&indicator=N11&template=expert&zoom=5",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("preserves semantic content query params and sorts them", () => {
|
|
40
|
+
assert.equal(
|
|
41
|
+
normalizeTrackingUrl("/story/?page=2&id=sea-ice-extent"),
|
|
42
|
+
"/story?id=sea-ice-extent&page=2",
|
|
43
|
+
);
|
|
44
|
+
assert.equal(
|
|
45
|
+
normalizeTrackingUrl("/story/?id=sea-ice-extent&page=2"),
|
|
46
|
+
"/story?id=sea-ice-extent&page=2",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("normalizes full URLs", () => {
|
|
51
|
+
assert.equal(
|
|
52
|
+
normalizeTrackingUrl("https://eodash.eox.at/explore/"),
|
|
53
|
+
"https://eodash.eox.at/explore",
|
|
54
|
+
);
|
|
55
|
+
assert.equal(
|
|
56
|
+
normalizeTrackingUrl(
|
|
57
|
+
"https://eodash.eox.at/explore/?x=12&y=34&indicator=TEST",
|
|
58
|
+
),
|
|
59
|
+
"https://eodash.eox.at/explore?indicator=TEST",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("trackEvent", () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
globalThis._paq = [];
|
|
67
|
+
globalThis.window = globalThis;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("pushes array payload to _paq and normalizes path arguments", () => {
|
|
71
|
+
trackEvent(["Interaction", "Scroll Depth", "/explore/", 50]);
|
|
72
|
+
assert.deepEqual(globalThis._paq, [
|
|
73
|
+
["trackEvent", "Interaction", "Scroll Depth", "/explore", 50],
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("supports positional arguments", () => {
|
|
78
|
+
trackEvent("CTA", "/click-link/", "Button Text");
|
|
79
|
+
assert.deepEqual(globalThis._paq, [
|
|
80
|
+
["trackEvent", "CTA", "/click-link", "Button Text"],
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("trackPageView", () => {
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
globalThis._paq = [];
|
|
88
|
+
globalThis.window = globalThis;
|
|
89
|
+
globalThis.document = { title: "Default Title" };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("normalizes URL and sets document title before tracking page view", () => {
|
|
93
|
+
trackPageView({
|
|
94
|
+
path: "/explore/?x=10&indicator=AIR",
|
|
95
|
+
title: "Explore Indicators",
|
|
96
|
+
});
|
|
97
|
+
assert.deepEqual(globalThis._paq, [
|
|
98
|
+
["setCustomUrl", "/explore?indicator=AIR"],
|
|
99
|
+
["setDocumentTitle", "Explore Indicators"],
|
|
100
|
+
["trackPageView"],
|
|
101
|
+
["enableLinkTracking"],
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("accepts router route object", () => {
|
|
106
|
+
trackPageView({
|
|
107
|
+
route: {
|
|
108
|
+
path: "/story/",
|
|
109
|
+
data: { title: "Climate Story" },
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
assert.deepEqual(globalThis._paq, [
|
|
113
|
+
["setCustomUrl", "/story"],
|
|
114
|
+
["setDocumentTitle", "Climate Story"],
|
|
115
|
+
["trackPageView"],
|
|
116
|
+
["enableLinkTracking"],
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("trackSiteSearch", () => {
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
globalThis._paq = [];
|
|
124
|
+
globalThis.window = globalThis;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("pushes trackSiteSearch event with keyword, category, and result count", () => {
|
|
128
|
+
trackSiteSearch("methane", "indicators", 5);
|
|
129
|
+
assert.deepEqual(globalThis._paq, [
|
|
130
|
+
["trackSiteSearch", "methane", "indicators", 5],
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("existing helpers", () => {
|
|
136
|
+
it("formats prices correctly", () => {
|
|
137
|
+
assert.equal(formatPrice(12000, "en"), "€ 12,000.-");
|
|
138
|
+
assert.equal(formatPrice(49.99, "de"), "€ 49,99");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("translates i18n keys with fallback", () => {
|
|
142
|
+
assert.equal(t("nav.home", { nav: { home: "Home Page" } }), "Home Page");
|
|
143
|
+
assert.equal(
|
|
144
|
+
t("nav.unknown", { nav: { home: "Home Page" } }),
|
|
145
|
+
"nav.unknown",
|
|
146
|
+
);
|
|
147
|
+
assert.equal(t("Imprint", { Imprint: "Impressum" }), "Impressum");
|
|
148
|
+
assert.equal(t("Imprint", {}), "Imprint");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("checks isActive links correctly", () => {
|
|
152
|
+
assert.equal(isActive({ link: "/explore/" }, "/explore"), true);
|
|
153
|
+
assert.equal(isActive({ link: "/explore" }, "/explore/"), true);
|
|
154
|
+
assert.equal(isActive({ link: "/explore" }, "/explore/subpage"), true);
|
|
155
|
+
assert.equal(isActive({ link: "/explore" }, "/explore-other"), false);
|
|
156
|
+
assert.equal(isActive({ link: "/explore" }, "/explore?tab=1#sec"), true);
|
|
157
|
+
assert.equal(isActive({ link: "/" }, "/"), true);
|
|
158
|
+
assert.equal(isActive({ link: "/" }, "/explore"), false);
|
|
159
|
+
|
|
160
|
+
// Parent dropdown with items
|
|
161
|
+
const parentDropdown = {
|
|
162
|
+
text: "Expertise",
|
|
163
|
+
items: [
|
|
164
|
+
{ text: "Data Management", link: "/expertise/data-management" },
|
|
165
|
+
{ text: "Data Visualization", link: "/expertise/data-visualization" },
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
assert.equal(isActive(parentDropdown, "/expertise/data-management"), true);
|
|
169
|
+
assert.equal(isActive(parentDropdown, "/expertise/data-management/"), true);
|
|
170
|
+
assert.equal(isActive(parentDropdown, "/solutions"), false);
|
|
171
|
+
|
|
172
|
+
// Parent with both link and items
|
|
173
|
+
const parentWithLinkAndItems = {
|
|
174
|
+
text: "Products",
|
|
175
|
+
link: "/products",
|
|
176
|
+
items: [{ text: "Product A", link: "/products/a" }],
|
|
177
|
+
};
|
|
178
|
+
assert.equal(isActive(parentWithLinkAndItems, "/products/a"), true);
|
|
179
|
+
assert.equal(isActive(parentWithLinkAndItems, "/products"), true);
|
|
180
|
+
});
|
|
181
|
+
});
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,14 @@ import ImageElement from "./components/ImageElement.vue";
|
|
|
17
17
|
import ImageGallery from "./components/ImageGallery.vue";
|
|
18
18
|
import ImageCarousel from "./components/ImageCarousel.vue";
|
|
19
19
|
import SingleTweet from "./components/SingleTweet.vue";
|
|
20
|
+
import BlogPost from "./components/BlogPost.vue";
|
|
21
|
+
import TagList from "./components/TagList.vue";
|
|
22
|
+
import BlogList from "./components/BlogList.vue";
|
|
23
|
+
import TeamPage from "./components/TeamPage.vue";
|
|
24
|
+
import SectionHeader from "./components/SectionHeader.vue";
|
|
25
|
+
import TestimonialsSection from "./components/TestimonialsSection.vue";
|
|
26
|
+
import TextSection from "./components/TextSection.vue";
|
|
27
|
+
import OpenPositions from "./components/OpenPositions.vue";
|
|
20
28
|
import Layout from "./Layout.vue";
|
|
21
29
|
import "./style.css";
|
|
22
30
|
|
|
@@ -43,6 +51,14 @@ export default {
|
|
|
43
51
|
app.component("GalleryGrid", ImageGallery);
|
|
44
52
|
app.component("ImageCarousel", ImageCarousel);
|
|
45
53
|
app.component("SingleTweet", SingleTweet);
|
|
54
|
+
app.component("BlogPost", BlogPost);
|
|
55
|
+
app.component("TagList", TagList);
|
|
56
|
+
app.component("BlogList", BlogList);
|
|
57
|
+
app.component("TeamPage", TeamPage);
|
|
58
|
+
app.component("SectionHeader", SectionHeader);
|
|
59
|
+
app.component("TestimonialsSection", TestimonialsSection);
|
|
60
|
+
app.component("TextSection", TextSection);
|
|
61
|
+
app.component("OpenPositions", OpenPositions);
|
|
46
62
|
|
|
47
63
|
router.onAfterRouteChanged = () => {
|
|
48
64
|
if (!import.meta.env.SSR) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
paths() {
|
|
6
|
+
const postsDir = path.resolve(process.cwd(), "_posts");
|
|
7
|
+
if (!fs.existsSync(postsDir)) return [];
|
|
8
|
+
|
|
9
|
+
const files = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
|
|
10
|
+
const allAuthors = new Set();
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const filePath = path.join(postsDir, file);
|
|
14
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
15
|
+
|
|
16
|
+
// Extract yaml frontmatter block
|
|
17
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (frontmatterMatch) {
|
|
19
|
+
const yaml = frontmatterMatch[1];
|
|
20
|
+
|
|
21
|
+
// 1. Match inline array or string style author
|
|
22
|
+
const authorMatch = yaml.match(/author:[ \t]+(.*?)\r?\n/);
|
|
23
|
+
if (authorMatch) {
|
|
24
|
+
const val = authorMatch[1].trim();
|
|
25
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
26
|
+
val
|
|
27
|
+
.slice(1, -1)
|
|
28
|
+
.split(",")
|
|
29
|
+
.forEach((a) => {
|
|
30
|
+
const clean = a.trim().replace(/['"]/g, "").toLowerCase();
|
|
31
|
+
if (clean) allAuthors.add(clean);
|
|
32
|
+
});
|
|
33
|
+
} else if (val && !val.startsWith("-")) {
|
|
34
|
+
const clean = val.replace(/['"]/g, "").toLowerCase();
|
|
35
|
+
if (clean) allAuthors.add(clean);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Match block list style authors
|
|
40
|
+
const authorBlockMatch = yaml.match(
|
|
41
|
+
/author:\r?\n((?:\s*-\s*.*\r?\n?)+)/,
|
|
42
|
+
);
|
|
43
|
+
if (authorBlockMatch) {
|
|
44
|
+
const lines = authorBlockMatch[1].split(/\r?\n/);
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
const m = line.match(/^\s*-\s*["']?(.*?)["']?\s*$/);
|
|
47
|
+
if (m && m[1]) {
|
|
48
|
+
const auth = m[1].trim().toLowerCase();
|
|
49
|
+
if (auth) allAuthors.add(auth);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Array.from(allAuthors).map((author) => {
|
|
57
|
+
return { params: { author } };
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
paths() {
|
|
6
|
+
const postsDir = path.resolve(process.cwd(), "_posts");
|
|
7
|
+
if (!fs.existsSync(postsDir)) return [];
|
|
8
|
+
|
|
9
|
+
const files = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
|
|
10
|
+
const allTags = new Set();
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const filePath = path.join(postsDir, file);
|
|
14
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
15
|
+
|
|
16
|
+
// Robust YAML frontmatter parser
|
|
17
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (frontmatterMatch) {
|
|
19
|
+
const yaml = frontmatterMatch[1];
|
|
20
|
+
|
|
21
|
+
// 1. Match list-style tags
|
|
22
|
+
const tagsBlockMatch = yaml.match(/tags:\r?\n((?:\s*-\s*.*\r?\n?)+)/);
|
|
23
|
+
if (tagsBlockMatch) {
|
|
24
|
+
const lines = tagsBlockMatch[1].split(/\r?\n/);
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const m = line.match(/^\s*-\s*["']?(.*?)["']?\s*$/);
|
|
27
|
+
if (m && m[1]) {
|
|
28
|
+
const tag = m[1].trim().toLowerCase();
|
|
29
|
+
if (tag) allTags.add(tag);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Match inline-array style tags
|
|
35
|
+
const inlineMatch = yaml.match(/tags:\s*\[([\s\S]*?)\]/);
|
|
36
|
+
if (inlineMatch) {
|
|
37
|
+
inlineMatch[1].split(",").forEach((t) => {
|
|
38
|
+
const tag = t.trim().replace(/['"]/g, "").toLowerCase();
|
|
39
|
+
if (tag) allTags.add(tag);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Array.from(allTags).map((tag) => {
|
|
46
|
+
return { params: { tag } };
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
package/src/pages/tag.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createContentLoader } from "vitepress";
|
|
2
|
+
|
|
3
|
+
function cleanMarkdown(src) {
|
|
4
|
+
if (!src) return "";
|
|
5
|
+
|
|
6
|
+
// 1. If there's a <!-- more --> tag, truncate there
|
|
7
|
+
const moreIndex = src.indexOf("<!-- more -->");
|
|
8
|
+
let content = moreIndex !== -1 ? src.slice(0, moreIndex) : src;
|
|
9
|
+
|
|
10
|
+
// 2. Remove frontmatter (just in case)
|
|
11
|
+
if (content.startsWith("---")) {
|
|
12
|
+
const nextThree = content.indexOf("---", 3);
|
|
13
|
+
if (nextThree !== -1) {
|
|
14
|
+
content = content.slice(nextThree + 3);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 3. Remove standard markdown and HTML elements
|
|
19
|
+
let text = content
|
|
20
|
+
// Remove custom containers like ::: info or ::: warning ... :::
|
|
21
|
+
.replace(/:::[^\n]*\n/g, "")
|
|
22
|
+
.replace(/:::/g, "")
|
|
23
|
+
// Remove HTML comments
|
|
24
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
25
|
+
// Remove images: 
|
|
26
|
+
.replace(/!\[.*?\]\(.*?\)/g, "")
|
|
27
|
+
// Remove links but keep text: [text](url)
|
|
28
|
+
.replace(/\[(.*?)\]\(.*?\)/g, "$1")
|
|
29
|
+
// Remove blockquotes: > quote
|
|
30
|
+
.replace(/^\s*>\s+/gm, "")
|
|
31
|
+
// Clean up lists: * list item, - list item, 1. list item
|
|
32
|
+
.replace(/^\s*[\*+-]\s+/gm, "")
|
|
33
|
+
.replace(/^\s*\d+\.\s+/gm, "")
|
|
34
|
+
// Remove tables/borders
|
|
35
|
+
.replace(/\|/g, "")
|
|
36
|
+
.replace(/[-]{3,}/g, "")
|
|
37
|
+
// Remove custom components / HTML tags but keep nested content
|
|
38
|
+
.replace(/<[^>]+>/g, "")
|
|
39
|
+
// Remove bold/italic formatting
|
|
40
|
+
.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
|
41
|
+
.replace(/(\*|_)(.*?)\1/g, "$2")
|
|
42
|
+
// Remove inline code
|
|
43
|
+
.replace(/`(.*?)`/g, "$1")
|
|
44
|
+
// Replace multiple newlines or spaces with a single space
|
|
45
|
+
.replace(/\s+/g, " ")
|
|
46
|
+
.trim();
|
|
47
|
+
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getExcerpt(src, maxLength = 200) {
|
|
52
|
+
const text = cleanMarkdown(src);
|
|
53
|
+
if (text.length > maxLength) {
|
|
54
|
+
// Try to truncate at a word boundary within the last 30 characters
|
|
55
|
+
const truncated = text.slice(0, maxLength);
|
|
56
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
57
|
+
if (lastSpace > maxLength - 30) {
|
|
58
|
+
return truncated.slice(0, lastSpace) + " ...";
|
|
59
|
+
}
|
|
60
|
+
return truncated + " ...";
|
|
61
|
+
}
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolvePostUrl(page) {
|
|
66
|
+
if (!page.url) return "";
|
|
67
|
+
const cleanUrls = global.VITEPRESS_CONFIG?.cleanUrls ?? true;
|
|
68
|
+
|
|
69
|
+
const relFile = page.url.replace(/^\//, "").replace(/\.html$/, "") + ".md";
|
|
70
|
+
|
|
71
|
+
// 1. Check VitePress rewrites map first if available
|
|
72
|
+
const rewritten = global.VITEPRESS_CONFIG?.rewrites?.map?.[relFile];
|
|
73
|
+
if (rewritten) {
|
|
74
|
+
return (
|
|
75
|
+
"/" +
|
|
76
|
+
rewritten
|
|
77
|
+
.replace(/(^|\/)index\.md$/, "$1")
|
|
78
|
+
.replace(/\.md$/, cleanUrls ? "" : ".html")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. If it is in _posts/, compute fallback rewrite based on frontmatter date and slug
|
|
83
|
+
if (page.url.startsWith("/_posts/")) {
|
|
84
|
+
const baseName = page.url.replace(/^\/_posts\//, "").replace(/\.html$/, "");
|
|
85
|
+
const slug = baseName
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
88
|
+
.replace(/^-+|-+$/g, "");
|
|
89
|
+
|
|
90
|
+
const dateStr = String(page.frontmatter?.date || "");
|
|
91
|
+
const dateMatch = dateStr.match(/^(\d{4})-(\d{2})/);
|
|
92
|
+
if (dateMatch) {
|
|
93
|
+
const [, year, month] = dateMatch;
|
|
94
|
+
return `/${year}/${month}/${slug}${cleanUrls ? "" : ".html"}`;
|
|
95
|
+
} else {
|
|
96
|
+
return `/blog/${slug}${cleanUrls ? "" : ".html"}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return page.url;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default createContentLoader("_posts/*.md", {
|
|
104
|
+
includeSrc: true,
|
|
105
|
+
transform(rawData) {
|
|
106
|
+
return rawData
|
|
107
|
+
.map((page) => {
|
|
108
|
+
// Normalize single author / authors array
|
|
109
|
+
const author = page.frontmatter.author || "";
|
|
110
|
+
const authors = Array.isArray(author) ? author : [author];
|
|
111
|
+
|
|
112
|
+
const excerpt =
|
|
113
|
+
page.frontmatter.description ||
|
|
114
|
+
page.frontmatter.summary ||
|
|
115
|
+
getExcerpt(page.src);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
title: page.frontmatter.title || "",
|
|
119
|
+
url: resolvePostUrl(page),
|
|
120
|
+
date: page.frontmatter.date || "",
|
|
121
|
+
authors,
|
|
122
|
+
tags: page.frontmatter.tags || [],
|
|
123
|
+
categories: page.frontmatter.categories || [],
|
|
124
|
+
coverImage:
|
|
125
|
+
page.frontmatter.coverImage || page.frontmatter.image || "",
|
|
126
|
+
excerpt,
|
|
127
|
+
};
|
|
128
|
+
})
|
|
129
|
+
.sort((a, b) => {
|
|
130
|
+
// Sort descending by date (newest first)
|
|
131
|
+
return b.date.localeCompare(a.date);
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
});
|