livemark 0.20.0 → 0.22.0
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/build/components/SidebarPosts.js +4 -4
- package/build/helpers/rss.d.ts +21 -0
- package/build/helpers/rss.js +56 -0
- package/build/helpers/rss.unit.d.ts +1 -0
- package/build/helpers/rss.unit.js +123 -0
- package/build/plugins/vite-robots.d.ts +17 -0
- package/build/plugins/vite-robots.js +40 -0
- package/build/plugins/vite-robots.unit.d.ts +1 -0
- package/build/plugins/vite-robots.unit.js +20 -0
- package/build/routeTree.gen.d.ts +16 -3
- package/build/routeTree.gen.js +8 -1
- package/build/routes/$section.rss[.]xml.d.ts +7 -0
- package/build/routes/$section.rss[.]xml.js +56 -0
- package/build/routes/__root.js +12 -1
- package/build/vite.config.js +19 -2
- package/package.json +14 -15
- package/source/components/SidebarPosts.tsx +12 -6
- package/source/helpers/rss.ts +80 -0
- package/source/helpers/rss.unit.ts +134 -0
- package/source/plugins/vite-robots.ts +51 -0
- package/source/plugins/vite-robots.unit.ts +32 -0
- package/source/routeTree.gen.ts +21 -3
- package/source/routes/$section.rss[.]xml.tsx +60 -0
- package/source/routes/__root.tsx +11 -0
- package/source/vite.config.ts +19 -1
package/build/routes/__root.js
CHANGED
|
@@ -16,6 +16,16 @@ import markdownCss from "../styles/markdown.css?url";
|
|
|
16
16
|
export const Route = createRootRoute({
|
|
17
17
|
head: () => {
|
|
18
18
|
const favicon = prefixUrl(config.favicon ?? config.logo ?? defaultFavicon, config.base);
|
|
19
|
+
const feedLinks = config.site
|
|
20
|
+
? (config.sections ?? [])
|
|
21
|
+
.filter(s => s.type === "blog")
|
|
22
|
+
.map(s => ({
|
|
23
|
+
rel: "alternate",
|
|
24
|
+
type: "application/rss+xml",
|
|
25
|
+
title: `${s.siteTitle ?? s.title} RSS Feed`,
|
|
26
|
+
href: prefixUrl(`${s.prefix}rss.xml`, config.base),
|
|
27
|
+
}))
|
|
28
|
+
: [];
|
|
19
29
|
return {
|
|
20
30
|
meta: [
|
|
21
31
|
{ charSet: "utf-8" },
|
|
@@ -38,6 +48,7 @@ export const Route = createRootRoute({
|
|
|
38
48
|
{ rel: "shortcut icon", href: favicon, type: faviconType(favicon) },
|
|
39
49
|
{ rel: "stylesheet", href: generalCss },
|
|
40
50
|
{ rel: "stylesheet", href: markdownCss },
|
|
51
|
+
...feedLinks,
|
|
41
52
|
],
|
|
42
53
|
};
|
|
43
54
|
},
|
|
@@ -69,4 +80,4 @@ function Document(props) {
|
|
|
69
80
|
] })
|
|
70
81
|
] }));
|
|
71
82
|
}
|
|
72
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
83
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiX19yb290LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc291cmNlL3JvdXRlcy9fX3Jvb3QudHN4Il0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQSxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sa0JBQWtCLENBQUE7QUFDekMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sMEJBQTBCLENBQUE7QUFDM0QsT0FBTyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sZ0NBQWdDLENBQUE7QUFDeEUsT0FBTyxFQUNMLGVBQWUsRUFDZixXQUFXLEVBQ1gsTUFBTSxFQUNOLE9BQU8sRUFDUCxRQUFRLEdBQ1QsTUFBTSx3QkFBd0IsQ0FBQTtBQUMvQixPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQTtBQUU3RSxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0seUJBQTBCLENBQUE7QUFDakQsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sdUNBQXdDLENBQUE7QUFDN0UsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLDJCQUE0QixDQUFBO0FBQ3JELE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSx1QkFBd0IsQ0FBQTtBQUNoRCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sdUJBQXVCLENBQUE7QUFDbkQsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBQ3BELE9BQU8sY0FBYyxNQUFNLG9CQUFvQixDQUFBO0FBQy9DLE9BQU8sVUFBVSxNQUFNLDJCQUEyQixDQUFBO0FBQ2xELE9BQU8sV0FBVyxNQUFNLDRCQUE0QixDQUFBO0FBRXBELE1BQU0sQ0FBQyxNQUFNLEtBQUssR0FBRyxlQUFlLENBQUM7SUFDbkMsSUFBSSxFQUFFLEdBQUcsRUFBRTtRQUNULE1BQU0sT0FBTyxHQUFHLFNBQVMsQ0FDdkIsTUFBTSxDQUFDLE9BQU8sSUFBSSxNQUFNLENBQUMsSUFBSSxJQUFJLGNBQWMsRUFDL0MsTUFBTSxDQUFDLElBQUksQ0FDWixDQUFBO1FBQ0QsTUFBTSxTQUFTLEdBQUcsTUFBTSxDQUFDLElBQUk7WUFDM0IsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLFFBQVEsSUFBSSxFQUFFLENBQUM7aUJBQ3BCLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEtBQUssTUFBTSxDQUFDO2lCQUM5QixHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDO2dCQUNULEdBQUcsRUFBRSxXQUFXO2dCQUNoQixJQUFJLEVBQUUscUJBQXFCO2dCQUMzQixLQUFLLEVBQUUsR0FBRyxDQUFDLENBQUMsU0FBUyxJQUFJLENBQUMsQ0FBQyxLQUFLLFdBQVc7Z0JBQzNDLElBQUksRUFBRSxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxTQUFTLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQzthQUNuRCxDQUFDLENBQUM7WUFDUCxDQUFDLENBQUMsRUFBRSxDQUFBO1FBQ04sT0FBTztZQUNMLElBQUksRUFBRTtnQkFDSixFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUU7Z0JBQ3BCLEVBQUUsS0FBSyxFQUFFLE1BQU0sQ0FBQyxLQUFLLEVBQUU7Z0JBQ3ZCLEVBQUUsSUFBSSxFQUFFLGFBQWEsRUFBRSxPQUFPLEVBQUUsTUFBTSxDQUFDLFdBQVcsRUFBRTtnQkFDcEQsRUFBRSxRQUFRLEVBQUUsVUFBVSxFQUFFLE9BQU8sRUFBRSxNQUFNLENBQUMsS0FBSyxFQUFFO2dCQUMvQztvQkFDRSxRQUFRLEVBQUUsZ0JBQWdCO29CQUMxQixPQUFPLEVBQUUsTUFBTSxDQUFDLFdBQVc7aUJBQzVCO2dCQUNELEVBQUUsUUFBUSxFQUFFLGNBQWMsRUFBRSxPQUFPLEVBQUUsTUFBTSxDQUFDLEtBQUssRUFBRTtnQkFDbkQsRUFBRSxRQUFRLEVBQUUsU0FBUyxFQUFFLE9BQU8sRUFBRSxTQUFTLEVBQUU7Z0JBQzNDO29CQUNFLElBQUksRUFBRSxVQUFVO29CQUNoQixPQUFPLEVBQUUscUNBQXFDO2lCQUMvQzthQUNGO1lBQ0QsS0FBSyxFQUFFO2dCQUNMLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxXQUFXLENBQUMsT0FBTyxDQUFDLEVBQUU7Z0JBQzFELEVBQUUsR0FBRyxFQUFFLGVBQWUsRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxXQUFXLENBQUMsT0FBTyxDQUFDLEVBQUU7Z0JBQ25FLEVBQUUsR0FBRyxFQUFFLFlBQVksRUFBRSxJQUFJLEVBQUUsVUFBVSxFQUFFO2dCQUN2QyxFQUFFLEdBQUcsRUFBRSxZQUFZLEVBQUUsSUFBSSxFQUFFLFdBQVcsRUFBRTtnQkFDeEMsR0FBRyxTQUFTO2FBQ2I7U0FDRixDQUFBO0lBQ0gsQ0FBQztJQUNELGlCQUFpQixFQUFFLEdBQUcsRUFBRSxDQUFDLEtBQUMsUUFBUSxLQUFHO0lBQ3JDLGNBQWMsRUFBRSxLQUFLLENBQUMsRUFBRTtRQUN0QixPQUFPLENBQ0wsS0FBQyxRQUFRLGNBQ1AsS0FBQyxvQkFBb0IsT0FBSyxLQUFLLEdBQUksR0FDMUIsQ0FDWixDQUFBO0lBQ0gsQ0FBQztJQUNELFNBQVMsRUFBRSxHQUFHLEVBQUU7UUFDZCxPQUFPLENBQ0wsS0FBQyxRQUFRLGNBQ1AsS0FBQyxNQUFNLEtBQUcsR0FDRCxDQUNaLENBQUE7SUFDSCxDQUFDO0NBQ0YsQ0FBQyxDQUFBO0FBRUYsU0FBUyxRQUFRLENBQUMsS0FBb0M7SUFDcEQsTUFBTSxZQUFZLEdBQUcsUUFBUSxDQUFDLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxXQUFXLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQTtJQUNqRSxNQUFNLFdBQVcsR0FBRyxZQUFZLEVBQUUsVUFBVSxFQUFFLE9BQU8sQ0FBQTtJQUVyRCxPQUFPLENBQ0wsZ0JBQU0sSUFBSSxFQUFDLElBQUk7WUFDYix5QkFDRSxLQUFDLFdBQVcsS0FBRyxHQUNWLEVBQ1A7b0JBQ0UsS0FBQyxNQUFNLElBQUMsV0FBVyxFQUFFLFdBQVcsWUFBRyxLQUFLLENBQUMsUUFBUSxHQUFVLEVBQzNELEtBQUMsT0FBTyxJQUFDLFFBQVEsRUFBQyxZQUFZLEdBQUcsRUFDakMsS0FBQyxnQkFBZ0IsSUFDZixNQUFNLEVBQUUsRUFBRSxjQUFjLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxhQUFhLEVBQUUsRUFDekQsT0FBTyxFQUFFOzRCQUNQO2dDQUNFLElBQUksRUFBRSxnQkFBZ0I7Z0NBQ3RCLE1BQU0sRUFBRSxLQUFDLHVCQUF1QixLQUFHO2dDQUNuQyxXQUFXLEVBQUUsSUFBSTs2QkFDbEI7NEJBQ0Q7Z0NBQ0UsSUFBSSxFQUFFLGlCQUFpQjtnQ0FDdkIsTUFBTSxFQUFFLEtBQUMsMkJBQTJCLEtBQUc7Z0NBQ3ZDLFdBQVcsRUFBRSxLQUFLOzZCQUNuQjt5QkFDRixHQUNELEVBQ0YsS0FBQyxPQUFPLEtBQUc7b0JBQ047WUFDRixDQUNSLENBQUE7QUFDSCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY29uZmlnIH0gZnJvbSBcImxpdmVtYXJrOnZpcnR1YWxcIlxuaW1wb3J0IHsgVGFuU3RhY2tEZXZ0b29scyB9IGZyb20gXCJAdGFuc3RhY2svcmVhY3QtZGV2dG9vbHNcIlxuaW1wb3J0IHsgUmVhY3RRdWVyeURldnRvb2xzUGFuZWwgfSBmcm9tIFwiQHRhbnN0YWNrL3JlYWN0LXF1ZXJ5LWRldnRvb2xzXCJcbmltcG9ydCB7XG4gIGNyZWF0ZVJvb3RSb3V0ZSxcbiAgSGVhZENvbnRlbnQsXG4gIE91dGxldCxcbiAgU2NyaXB0cyxcbiAgdXNlTWF0Y2gsXG59IGZyb20gXCJAdGFuc3RhY2svcmVhY3Qtcm91dGVyXCJcbmltcG9ydCB7IFRhblN0YWNrUm91dGVyRGV2dG9vbHNQYW5lbCB9IGZyb20gXCJAdGFuc3RhY2svcmVhY3Qtcm91dGVyLWRldnRvb2xzXCJcbmltcG9ydCB0eXBlICogYXMgUmVhY3QgZnJvbSBcInJlYWN0XCJcbmltcG9ydCB7IExheW91dCB9IGZyb20gXCIuLi9jb21wb25lbnRzL0xheW91dC50c3hcIlxuaW1wb3J0IHsgRGVmYXVsdENhdGNoQm91bmRhcnkgfSBmcm9tIFwiLi4vY29tcG9uZW50cy9EZWZhdWx0Q2F0Y2hCb3VuZGFyeS50c3hcIlxuaW1wb3J0IHsgTm90Rm91bmQgfSBmcm9tIFwiLi4vY29tcG9uZW50cy9Ob3RGb3VuZC50c3hcIlxuaW1wb3J0IHsgVG9hc3RlciB9IGZyb20gXCIuLi9lbGVtZW50cy9zb25uZXIudHN4XCJcbmltcG9ydCB7IGZhdmljb25UeXBlIH0gZnJvbSBcIi4uL2hlbHBlcnMvZmF2aWNvbi50c1wiXG5pbXBvcnQgeyBwcmVmaXhVcmwgfSBmcm9tIFwiLi4vaGVscGVycy9wcmVmaXgtdXJsLnRzXCJcbmltcG9ydCBkZWZhdWx0RmF2aWNvbiBmcm9tIFwiLi4vYXNzZXRzL2xvZ28uc3ZnXCJcbmltcG9ydCBnZW5lcmFsQ3NzIGZyb20gXCIuLi9zdHlsZXMvZ2VuZXJhbC5jc3M/dXJsXCJcbmltcG9ydCBtYXJrZG93bkNzcyBmcm9tIFwiLi4vc3R5bGVzL21hcmtkb3duLmNzcz91cmxcIlxuXG5leHBvcnQgY29uc3QgUm91dGUgPSBjcmVhdGVSb290Um91dGUoe1xuICBoZWFkOiAoKSA9PiB7XG4gICAgY29uc3QgZmF2aWNvbiA9IHByZWZpeFVybChcbiAgICAgIGNvbmZpZy5mYXZpY29uID8/IGNvbmZpZy5sb2dvID8/IGRlZmF1bHRGYXZpY29uLFxuICAgICAgY29uZmlnLmJhc2UsXG4gICAgKVxuICAgIGNvbnN0IGZlZWRMaW5rcyA9IGNvbmZpZy5zaXRlXG4gICAgICA/IChjb25maWcuc2VjdGlvbnMgPz8gW10pXG4gICAgICAgICAgLmZpbHRlcihzID0+IHMudHlwZSA9PT0gXCJibG9nXCIpXG4gICAgICAgICAgLm1hcChzID0+ICh7XG4gICAgICAgICAgICByZWw6IFwiYWx0ZXJuYXRlXCIsXG4gICAgICAgICAgICB0eXBlOiBcImFwcGxpY2F0aW9uL3Jzcyt4bWxcIixcbiAgICAgICAgICAgIHRpdGxlOiBgJHtzLnNpdGVUaXRsZSA/PyBzLnRpdGxlfSBSU1MgRmVlZGAsXG4gICAgICAgICAgICBocmVmOiBwcmVmaXhVcmwoYCR7cy5wcmVmaXh9cnNzLnhtbGAsIGNvbmZpZy5iYXNlKSxcbiAgICAgICAgICB9KSlcbiAgICAgIDogW11cbiAgICByZXR1cm4ge1xuICAgICAgbWV0YTogW1xuICAgICAgICB7IGNoYXJTZXQ6IFwidXRmLThcIiB9LFxuICAgICAgICB7IHRpdGxlOiBjb25maWcudGl0bGUgfSxcbiAgICAgICAgeyBuYW1lOiBcImRlc2NyaXB0aW9uXCIsIGNvbnRlbnQ6IGNvbmZpZy5kZXNjcmlwdGlvbiB9LFxuICAgICAgICB7IHByb3BlcnR5OiBcIm9nOnRpdGxlXCIsIGNvbnRlbnQ6IGNvbmZpZy50aXRsZSB9LFxuICAgICAgICB7XG4gICAgICAgICAgcHJvcGVydHk6IFwib2c6ZGVzY3JpcHRpb25cIixcbiAgICAgICAgICBjb250ZW50OiBjb25maWcuZGVzY3JpcHRpb24sXG4gICAgICAgIH0sXG4gICAgICAgIHsgcHJvcGVydHk6IFwib2c6c2l0ZV9uYW1lXCIsIGNvbnRlbnQ6IGNvbmZpZy50aXRsZSB9LFxuICAgICAgICB7IHByb3BlcnR5OiBcIm9nOnR5cGVcIiwgY29udGVudDogXCJ3ZWJzaXRlXCIgfSxcbiAgICAgICAge1xuICAgICAgICAgIG5hbWU6IFwidmlld3BvcnRcIixcbiAgICAgICAgICBjb250ZW50OiBcIndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xXCIsXG4gICAgICAgIH0sXG4gICAgICBdLFxuICAgICAgbGlua3M6IFtcbiAgICAgICAgeyByZWw6IFwiaWNvblwiLCBocmVmOiBmYXZpY29uLCB0eXBlOiBmYXZpY29uVHlwZShmYXZpY29uKSB9LFxuICAgICAgICB7IHJlbDogXCJzaG9ydGN1dCBpY29uXCIsIGhyZWY6IGZhdmljb24sIHR5cGU6IGZhdmljb25UeXBlKGZhdmljb24pIH0sXG4gICAgICAgIHsgcmVsOiBcInN0eWxlc2hlZXRcIiwgaHJlZjogZ2VuZXJhbENzcyB9LFxuICAgICAgICB7IHJlbDogXCJzdHlsZXNoZWV0XCIsIGhyZWY6IG1hcmtkb3duQ3NzIH0sXG4gICAgICAgIC4uLmZlZWRMaW5rcyxcbiAgICAgIF0sXG4gICAgfVxuICB9LFxuICBub3RGb3VuZENvbXBvbmVudDogKCkgPT4gPE5vdEZvdW5kIC8+LFxuICBlcnJvckNvbXBvbmVudDogcHJvcHMgPT4ge1xuICAgIHJldHVybiAoXG4gICAgICA8RG9jdW1lbnQ+XG4gICAgICAgIDxEZWZhdWx0Q2F0Y2hCb3VuZGFyeSB7Li4ucHJvcHN9IC8+XG4gICAgICA8L0RvY3VtZW50PlxuICAgIClcbiAgfSxcbiAgY29tcG9uZW50OiAoKSA9PiB7XG4gICAgcmV0dXJuIChcbiAgICAgIDxEb2N1bWVudD5cbiAgICAgICAgPE91dGxldCAvPlxuICAgICAgPC9Eb2N1bWVudD5cbiAgICApXG4gIH0sXG59KVxuXG5mdW5jdGlvbiBEb2N1bWVudChwcm9wczogeyBjaGlsZHJlbjogUmVhY3QuUmVhY3ROb2RlIH0pIHtcbiAgY29uc3QgYXJ0aWNsZVJvdXRlID0gdXNlTWF0Y2goeyBmcm9tOiBcIi8kXCIsIHNob3VsZFRocm93OiBmYWxzZSB9KVxuICBjb25zdCB3aXRoU2lkZWJhciA9IGFydGljbGVSb3V0ZT8ubG9hZGVyRGF0YT8uc2lkZWJhclxuXG4gIHJldHVybiAoXG4gICAgPGh0bWwgbGFuZz1cImVuXCI+XG4gICAgICA8aGVhZD5cbiAgICAgICAgPEhlYWRDb250ZW50IC8+XG4gICAgICA8L2hlYWQ+XG4gICAgICA8Ym9keT5cbiAgICAgICAgPExheW91dCB3aXRoU2lkZWJhcj17d2l0aFNpZGViYXJ9Pntwcm9wcy5jaGlsZHJlbn08L0xheW91dD5cbiAgICAgICAgPFRvYXN0ZXIgcG9zaXRpb249XCJ0b3AtY2VudGVyXCIgLz5cbiAgICAgICAgPFRhblN0YWNrRGV2dG9vbHNcbiAgICAgICAgICBjb25maWc9e3sgaGlkZVVudGlsSG92ZXI6IHRydWUsIHBvc2l0aW9uOiBcImJvdHRvbS1sZWZ0XCIgfX1cbiAgICAgICAgICBwbHVnaW5zPXtbXG4gICAgICAgICAgICB7XG4gICAgICAgICAgICAgIG5hbWU6IFwiVGFuU3RhY2sgUXVlcnlcIixcbiAgICAgICAgICAgICAgcmVuZGVyOiA8UmVhY3RRdWVyeURldnRvb2xzUGFuZWwgLz4sXG4gICAgICAgICAgICAgIGRlZmF1bHRPcGVuOiB0cnVlLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgbmFtZTogXCJUYW5TdGFjayBSb3V0ZXJcIixcbiAgICAgICAgICAgICAgcmVuZGVyOiA8VGFuU3RhY2tSb3V0ZXJEZXZ0b29sc1BhbmVsIC8+LFxuICAgICAgICAgICAgICBkZWZhdWx0T3BlbjogZmFsc2UsXG4gICAgICAgICAgICB9LFxuICAgICAgICAgIF19XG4gICAgICAgIC8+XG4gICAgICAgIDxTY3JpcHRzIC8+XG4gICAgICA8L2JvZHk+XG4gICAgPC9odG1sPlxuICApXG59XG4iXX0=
|
package/build/vite.config.js
CHANGED
|
@@ -7,10 +7,24 @@ import { defineConfig } from "vite";
|
|
|
7
7
|
import svgr from "vite-plugin-svgr";
|
|
8
8
|
import { loadConfig } from "./actions/config/load.js";
|
|
9
9
|
import { livemark } from "./plugins/vite-livemark.js";
|
|
10
|
+
import { robots } from "./plugins/vite-robots.js";
|
|
10
11
|
const config = await loadConfig();
|
|
12
|
+
// Per-blog-section RSS feed paths to prerender. Gated on `config.site`
|
|
13
|
+
// because feed URLs are absolute (matching the sitemap/robots.txt gate)
|
|
14
|
+
// and the route returns 404 without it anyway. Excluded from the sitemap
|
|
15
|
+
// since `.xml` payloads don't belong in a sitemap.
|
|
16
|
+
const blogFeedPages = config.site
|
|
17
|
+
? (config.sections ?? [])
|
|
18
|
+
.filter(s => s.type === "blog")
|
|
19
|
+
.map(s => ({
|
|
20
|
+
path: `${config.base ?? ""}${s.prefix}rss.xml`,
|
|
21
|
+
sitemap: { exclude: true },
|
|
22
|
+
}))
|
|
23
|
+
: [];
|
|
11
24
|
export default defineConfig({
|
|
12
25
|
plugins: [
|
|
13
26
|
livemark({ config }),
|
|
27
|
+
robots({ config }),
|
|
14
28
|
devtools(),
|
|
15
29
|
tailwind(),
|
|
16
30
|
contentCollections({ configPath: "content-collections.ts" }),
|
|
@@ -19,7 +33,10 @@ export default defineConfig({
|
|
|
19
33
|
// Seed path must match what the crawler produces (router-emitted
|
|
20
34
|
// hrefs include the basepath), otherwise prerender records both
|
|
21
35
|
// shapes and the sitemap ends up with duplicate entries.
|
|
22
|
-
pages: [
|
|
36
|
+
pages: [
|
|
37
|
+
{ path: config.base ? `${config.base}/` : "/" },
|
|
38
|
+
...blogFeedPages,
|
|
39
|
+
],
|
|
23
40
|
// Disable route-tree auto-discovery: it produces basepath-naive
|
|
24
41
|
// paths (e.g. `/`) that duplicate the crawl-derived prefixed paths,
|
|
25
42
|
// bloating pages.json and sitemap. Crawling from the seed already
|
|
@@ -41,4 +58,4 @@ export default defineConfig({
|
|
|
41
58
|
svgr(),
|
|
42
59
|
],
|
|
43
60
|
});
|
|
44
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
61
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2Uvdml0ZS5jb25maWcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxrQkFBa0IsTUFBTSwyQkFBMkIsQ0FBQTtBQUMxRCxPQUFPLFFBQVEsTUFBTSxtQkFBbUIsQ0FBQTtBQUN4QyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0seUJBQXlCLENBQUE7QUFDbEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLG1DQUFtQyxDQUFBO0FBQ2pFLE9BQU8sS0FBSyxNQUFNLHNCQUFzQixDQUFBO0FBQ3hDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxNQUFNLENBQUE7QUFDbkMsT0FBTyxJQUFJLE1BQU0sa0JBQWtCLENBQUE7QUFDbkMsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBQ3JELE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQTtBQUNyRCxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sMEJBQTBCLENBQUE7QUFFakQsTUFBTSxNQUFNLEdBQUcsTUFBTSxVQUFVLEVBQUUsQ0FBQTtBQUVqQyx1RUFBdUU7QUFDdkUsd0VBQXdFO0FBQ3hFLHlFQUF5RTtBQUN6RSxtREFBbUQ7QUFDbkQsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFDLElBQUk7SUFDL0IsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLFFBQVEsSUFBSSxFQUFFLENBQUM7U0FDcEIsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksS0FBSyxNQUFNLENBQUM7U0FDOUIsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUNULElBQUksRUFBRSxHQUFHLE1BQU0sQ0FBQyxJQUFJLElBQUksRUFBRSxHQUFHLENBQUMsQ0FBQyxNQUFNLFNBQVM7UUFDOUMsT0FBTyxFQUFFLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRTtLQUMzQixDQUFDLENBQUM7SUFDUCxDQUFDLENBQUMsRUFBRSxDQUFBO0FBRU4sZUFBZSxZQUFZLENBQUM7SUFDMUIsT0FBTyxFQUFFO1FBQ1AsUUFBUSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFDcEIsTUFBTSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFDbEIsUUFBUSxFQUFFO1FBQ1YsUUFBUSxFQUFFO1FBQ1Ysa0JBQWtCLENBQUMsRUFBRSxVQUFVLEVBQUUsd0JBQXdCLEVBQUUsQ0FBQztRQUM1RCxhQUFhLENBQUM7WUFDWixZQUFZLEVBQUUsR0FBRztZQUNqQixpRUFBaUU7WUFDakUsZ0VBQWdFO1lBQ2hFLHlEQUF5RDtZQUN6RCxLQUFLLEVBQUU7Z0JBQ0wsRUFBRSxJQUFJLEVBQUUsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsR0FBRyxNQUFNLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsRUFBRTtnQkFDL0MsR0FBRyxhQUFhO2FBQ2pCO1lBQ0QsZ0VBQWdFO1lBQ2hFLG9FQUFvRTtZQUNwRSxrRUFBa0U7WUFDbEUsNkJBQTZCO1lBQzdCLFNBQVMsRUFBRTtnQkFDVCxPQUFPLEVBQUUsSUFBSTtnQkFDYixVQUFVLEVBQUUsSUFBSTtnQkFDaEIsd0JBQXdCLEVBQUUsS0FBSzthQUNoQztZQUNELE9BQU8sRUFBRSxNQUFNLENBQUMsSUFBSTtnQkFDbEIsQ0FBQyxDQUFDLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsTUFBTSxDQUFDLElBQUksRUFBRTtnQkFDdEMsQ0FBQyxDQUFDLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRTtZQUN0QixNQUFNLEVBQUU7Z0JBQ04sa0JBQWtCLEVBQUUsa0JBQWtCO2dCQUN0QyxRQUFRLEVBQUUsTUFBTSxDQUFDLElBQUk7YUFDdEI7U0FDRixDQUFDO1FBQ0YsS0FBSyxFQUFFO1FBQ1AsSUFBSSxFQUFFO0tBQ1A7Q0FDRixDQUFDLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgY29udGVudENvbGxlY3Rpb25zIGZyb20gXCJAY29udGVudC1jb2xsZWN0aW9ucy92aXRlXCJcbmltcG9ydCB0YWlsd2luZCBmcm9tIFwiQHRhaWx3aW5kY3NzL3ZpdGVcIlxuaW1wb3J0IHsgZGV2dG9vbHMgfSBmcm9tIFwiQHRhbnN0YWNrL2RldnRvb2xzLXZpdGVcIlxuaW1wb3J0IHsgdGFuc3RhY2tTdGFydCB9IGZyb20gXCJAdGFuc3RhY2svcmVhY3Qtc3RhcnQvcGx1Z2luL3ZpdGVcIlxuaW1wb3J0IHJlYWN0IGZyb20gXCJAdml0ZWpzL3BsdWdpbi1yZWFjdFwiXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgc3ZnciBmcm9tIFwidml0ZS1wbHVnaW4tc3ZnclwiXG5pbXBvcnQgeyBsb2FkQ29uZmlnIH0gZnJvbSBcIi4vYWN0aW9ucy9jb25maWcvbG9hZC50c1wiXG5pbXBvcnQgeyBsaXZlbWFyayB9IGZyb20gXCIuL3BsdWdpbnMvdml0ZS1saXZlbWFyay50c1wiXG5pbXBvcnQgeyByb2JvdHMgfSBmcm9tIFwiLi9wbHVnaW5zL3ZpdGUtcm9ib3RzLnRzXCJcblxuY29uc3QgY29uZmlnID0gYXdhaXQgbG9hZENvbmZpZygpXG5cbi8vIFBlci1ibG9nLXNlY3Rpb24gUlNTIGZlZWQgcGF0aHMgdG8gcHJlcmVuZGVyLiBHYXRlZCBvbiBgY29uZmlnLnNpdGVgXG4vLyBiZWNhdXNlIGZlZWQgVVJMcyBhcmUgYWJzb2x1dGUgKG1hdGNoaW5nIHRoZSBzaXRlbWFwL3JvYm90cy50eHQgZ2F0ZSlcbi8vIGFuZCB0aGUgcm91dGUgcmV0dXJucyA0MDQgd2l0aG91dCBpdCBhbnl3YXkuIEV4Y2x1ZGVkIGZyb20gdGhlIHNpdGVtYXBcbi8vIHNpbmNlIGAueG1sYCBwYXlsb2FkcyBkb24ndCBiZWxvbmcgaW4gYSBzaXRlbWFwLlxuY29uc3QgYmxvZ0ZlZWRQYWdlcyA9IGNvbmZpZy5zaXRlXG4gID8gKGNvbmZpZy5zZWN0aW9ucyA/PyBbXSlcbiAgICAgIC5maWx0ZXIocyA9PiBzLnR5cGUgPT09IFwiYmxvZ1wiKVxuICAgICAgLm1hcChzID0+ICh7XG4gICAgICAgIHBhdGg6IGAke2NvbmZpZy5iYXNlID8/IFwiXCJ9JHtzLnByZWZpeH1yc3MueG1sYCxcbiAgICAgICAgc2l0ZW1hcDogeyBleGNsdWRlOiB0cnVlIH0sXG4gICAgICB9KSlcbiAgOiBbXVxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgbGl2ZW1hcmsoeyBjb25maWcgfSksXG4gICAgcm9ib3RzKHsgY29uZmlnIH0pLFxuICAgIGRldnRvb2xzKCksXG4gICAgdGFpbHdpbmQoKSxcbiAgICBjb250ZW50Q29sbGVjdGlvbnMoeyBjb25maWdQYXRoOiBcImNvbnRlbnQtY29sbGVjdGlvbnMudHNcIiB9KSxcbiAgICB0YW5zdGFja1N0YXJ0KHtcbiAgICAgIHNyY0RpcmVjdG9yeTogXCIuXCIsXG4gICAgICAvLyBTZWVkIHBhdGggbXVzdCBtYXRjaCB3aGF0IHRoZSBjcmF3bGVyIHByb2R1Y2VzIChyb3V0ZXItZW1pdHRlZFxuICAgICAgLy8gaHJlZnMgaW5jbHVkZSB0aGUgYmFzZXBhdGgpLCBvdGhlcndpc2UgcHJlcmVuZGVyIHJlY29yZHMgYm90aFxuICAgICAgLy8gc2hhcGVzIGFuZCB0aGUgc2l0ZW1hcCBlbmRzIHVwIHdpdGggZHVwbGljYXRlIGVudHJpZXMuXG4gICAgICBwYWdlczogW1xuICAgICAgICB7IHBhdGg6IGNvbmZpZy5iYXNlID8gYCR7Y29uZmlnLmJhc2V9L2AgOiBcIi9cIiB9LFxuICAgICAgICAuLi5ibG9nRmVlZFBhZ2VzLFxuICAgICAgXSxcbiAgICAgIC8vIERpc2FibGUgcm91dGUtdHJlZSBhdXRvLWRpc2NvdmVyeTogaXQgcHJvZHVjZXMgYmFzZXBhdGgtbmFpdmVcbiAgICAgIC8vIHBhdGhzIChlLmcuIGAvYCkgdGhhdCBkdXBsaWNhdGUgdGhlIGNyYXdsLWRlcml2ZWQgcHJlZml4ZWQgcGF0aHMsXG4gICAgICAvLyBibG9hdGluZyBwYWdlcy5qc29uIGFuZCBzaXRlbWFwLiBDcmF3bGluZyBmcm9tIHRoZSBzZWVkIGFscmVhZHlcbiAgICAgIC8vIHJlYWNoZXMgZXZlcnkgbGlua2VkIHBhZ2UuXG4gICAgICBwcmVyZW5kZXI6IHtcbiAgICAgICAgZW5hYmxlZDogdHJ1ZSxcbiAgICAgICAgY3Jhd2xMaW5rczogdHJ1ZSxcbiAgICAgICAgYXV0b1N0YXRpY1BhdGhzRGlzY292ZXJ5OiBmYWxzZSxcbiAgICAgIH0sXG4gICAgICBzaXRlbWFwOiBjb25maWcuc2l0ZVxuICAgICAgICA/IHsgZW5hYmxlZDogdHJ1ZSwgaG9zdDogY29uZmlnLnNpdGUgfVxuICAgICAgICA6IHsgZW5hYmxlZDogZmFsc2UgfSxcbiAgICAgIHJvdXRlcjoge1xuICAgICAgICBnZW5lcmF0ZWRSb3V0ZVRyZWU6IFwicm91dGVUcmVlLmdlbi50c1wiLFxuICAgICAgICBiYXNlcGF0aDogY29uZmlnLmJhc2UsXG4gICAgICB9LFxuICAgIH0pLFxuICAgIHJlYWN0KCksXG4gICAgc3ZncigpLFxuICBdLFxufSlcbiJdfQ==
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livemark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Livemark description",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Evgeny Karev",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"@shikijs/twoslash": "^4.0.2",
|
|
34
34
|
"@sindresorhus/slugify": "^3.0.0",
|
|
35
35
|
"@tailwindcss/typography": "^0.5.19",
|
|
36
|
-
"@tailwindcss/vite": "4.
|
|
37
|
-
"@tanstack/devtools-vite": "0.
|
|
36
|
+
"@tailwindcss/vite": "4.3.0",
|
|
37
|
+
"@tanstack/devtools-vite": "0.7.0",
|
|
38
38
|
"@tanstack/react-devtools": "0.10.0",
|
|
39
39
|
"@tanstack/react-hotkeys": "^0.9.1",
|
|
40
40
|
"@tanstack/react-query": "5.91.3",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"unified": "^11.0.5",
|
|
88
88
|
"unist-util-visit": "^5.1.0",
|
|
89
89
|
"vaul": "1.1.2",
|
|
90
|
-
"vite": "8.0.
|
|
90
|
+
"vite": "8.0.13",
|
|
91
91
|
"vite-plugin-svgr": "5.2.0",
|
|
92
92
|
"zod": "4.3.6"
|
|
93
93
|
},
|
|
@@ -96,10 +96,8 @@
|
|
|
96
96
|
"@semantic-release/exec": "7.1.0",
|
|
97
97
|
"@types/node": "24.2.0",
|
|
98
98
|
"@typescript/native-preview": "7.0.0-dev.20260316.1",
|
|
99
|
-
"@vitest/coverage-v8": "4.1.
|
|
99
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
100
100
|
"conventional-changelog-conventionalcommits": "9.1.0",
|
|
101
|
-
"husky": "9.1.7",
|
|
102
|
-
"is-ci": "4.1.0",
|
|
103
101
|
"lefthook": "2.1.1",
|
|
104
102
|
"livemark": "link:.",
|
|
105
103
|
"npm-check-updates": "18.0.1",
|
|
@@ -107,8 +105,7 @@
|
|
|
107
105
|
"react-dom": "19.2.4",
|
|
108
106
|
"semantic-release": "24.2.9",
|
|
109
107
|
"tempy": "3.2.0",
|
|
110
|
-
"vite-plus": "0.1.
|
|
111
|
-
"vitest-polly": "2.1.0",
|
|
108
|
+
"vite-plus": "0.1.21",
|
|
112
109
|
"wrangler": "4.85.0"
|
|
113
110
|
},
|
|
114
111
|
"peerDependencies": {
|
|
@@ -117,21 +114,23 @@
|
|
|
117
114
|
},
|
|
118
115
|
"engines": {
|
|
119
116
|
"node": "^24.0.0",
|
|
120
|
-
"pnpm": "^
|
|
117
|
+
"pnpm": "^11.0.0"
|
|
121
118
|
},
|
|
122
119
|
"scripts": {
|
|
123
120
|
"build": "tsgo --project tsconfig.build.json",
|
|
124
|
-
"
|
|
125
|
-
"clean": "rm -rf pnpm-lock.yaml node_modules build",
|
|
121
|
+
"deps": "ncu -u",
|
|
126
122
|
"docs:build": "node source/entrypoints/main.ts build",
|
|
127
123
|
"docs:start": "node source/entrypoints/main.ts start",
|
|
128
124
|
"format": "vp fmt",
|
|
125
|
+
"leaks": "gitleaks git --config=.gitleaks.toml --verbose --redact",
|
|
129
126
|
"lint": "vp fmt --check && vp lint",
|
|
130
127
|
"release": "pnpm publish --access public --ignore-scripts --no-git-checks",
|
|
131
|
-
"
|
|
128
|
+
"scan": "pnpm leaks && pnpm vulns",
|
|
132
129
|
"start": "node source/entrypoints/main.ts",
|
|
133
|
-
"unit": "vp test",
|
|
134
130
|
"test": "pnpm lint && pnpm type && pnpm unit",
|
|
135
|
-
"type": "tsgo --noEmit"
|
|
131
|
+
"type": "tsgo --noEmit",
|
|
132
|
+
"unit": "vp test",
|
|
133
|
+
"vers": "pnpm version --no-git-tag-version",
|
|
134
|
+
"vulns": "semgrep --config=p/typescript --config=p/nodejs --config=p/owasp-top-ten --config=p/secrets --exclude=\"**/fixtures/**\" --metrics=off"
|
|
136
135
|
}
|
|
137
136
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { config } from "livemark:virtual"
|
|
1
2
|
import { Link, useLocation } from "@tanstack/react-router"
|
|
2
3
|
import { Clock, List, Rss, Tag } from "lucide-react"
|
|
3
4
|
import {
|
|
@@ -87,12 +88,17 @@ export function SidebarPosts() {
|
|
|
87
88
|
})}
|
|
88
89
|
</SidebarMenuSub>
|
|
89
90
|
</SidebarMenuItem>
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
{config.site && (
|
|
92
|
+
<SidebarMenuItem>
|
|
93
|
+
<SidebarMenuButton
|
|
94
|
+
className="opacity-75"
|
|
95
|
+
render={<a href={`/${sectionPath}/rss.xml`} />}
|
|
96
|
+
>
|
|
97
|
+
<Rss className="size-4" />
|
|
98
|
+
<span>RSS Feed</span>
|
|
99
|
+
</SidebarMenuButton>
|
|
100
|
+
</SidebarMenuItem>
|
|
101
|
+
)}
|
|
96
102
|
</SidebarMenu>
|
|
97
103
|
</SidebarGroupContent>
|
|
98
104
|
</SidebarGroup>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export interface RssItem {
|
|
2
|
+
title: string
|
|
3
|
+
link: string
|
|
4
|
+
guid: string
|
|
5
|
+
description?: string
|
|
6
|
+
pubDate?: string
|
|
7
|
+
author?: string | string[]
|
|
8
|
+
categories?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RssFeed {
|
|
12
|
+
title: string
|
|
13
|
+
description: string
|
|
14
|
+
link: string
|
|
15
|
+
feedLink: string
|
|
16
|
+
items: RssItem[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Render an RSS 2.0 channel as a serialized XML document. Pure — all I/O,
|
|
20
|
+
* URL composition, and item-count limits live in the caller. `pubDate` is
|
|
21
|
+
* accepted as an ISO string (article frontmatter shape) and converted to
|
|
22
|
+
* RFC 822 here, which is what readers like NetNewsWire and Feedly parse. */
|
|
23
|
+
export function renderRssFeed(feed: RssFeed) {
|
|
24
|
+
const lines: string[] = []
|
|
25
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>')
|
|
26
|
+
lines.push('<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">')
|
|
27
|
+
lines.push(" <channel>")
|
|
28
|
+
lines.push(` <title>${escapeXml(feed.title)}</title>`)
|
|
29
|
+
lines.push(` <link>${escapeXml(feed.link)}</link>`)
|
|
30
|
+
lines.push(` <description>${escapeXml(feed.description)}</description>`)
|
|
31
|
+
lines.push(
|
|
32
|
+
` <atom:link href="${escapeXml(feed.feedLink)}" rel="self" type="application/rss+xml" />`,
|
|
33
|
+
)
|
|
34
|
+
for (const item of feed.items) {
|
|
35
|
+
lines.push(" <item>")
|
|
36
|
+
lines.push(` <title>${escapeXml(item.title)}</title>`)
|
|
37
|
+
lines.push(` <link>${escapeXml(item.link)}</link>`)
|
|
38
|
+
lines.push(` <guid isPermaLink="true">${escapeXml(item.guid)}</guid>`)
|
|
39
|
+
if (item.description) {
|
|
40
|
+
lines.push(
|
|
41
|
+
` <description>${escapeXml(item.description)}</description>`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
if (item.pubDate) {
|
|
45
|
+
lines.push(
|
|
46
|
+
` <pubDate>${escapeXml(toRfc822(item.pubDate))}</pubDate>`,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
if (item.author) {
|
|
50
|
+
const author = Array.isArray(item.author)
|
|
51
|
+
? item.author.join(", ")
|
|
52
|
+
: item.author
|
|
53
|
+
lines.push(` <author>${escapeXml(author)}</author>`)
|
|
54
|
+
}
|
|
55
|
+
if (item.categories) {
|
|
56
|
+
for (const category of item.categories) {
|
|
57
|
+
lines.push(` <category>${escapeXml(category)}</category>`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
lines.push(" </item>")
|
|
61
|
+
}
|
|
62
|
+
lines.push(" </channel>")
|
|
63
|
+
lines.push("</rss>")
|
|
64
|
+
return `${lines.join("\n")}\n`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function escapeXml(value: string) {
|
|
68
|
+
return value
|
|
69
|
+
.replace(/&/g, "&")
|
|
70
|
+
.replace(/</g, "<")
|
|
71
|
+
.replace(/>/g, ">")
|
|
72
|
+
.replace(/"/g, """)
|
|
73
|
+
.replace(/'/g, "'")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toRfc822(iso: string) {
|
|
77
|
+
const date = new Date(iso)
|
|
78
|
+
if (Number.isNaN(date.getTime())) return iso
|
|
79
|
+
return date.toUTCString()
|
|
80
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
2
|
+
import { renderRssFeed } from "./rss.ts"
|
|
3
|
+
|
|
4
|
+
describe("renderRssFeed", () => {
|
|
5
|
+
it("renders a minimal channel with no items", () => {
|
|
6
|
+
const xml = renderRssFeed({
|
|
7
|
+
title: "Blog",
|
|
8
|
+
description: "Latest posts",
|
|
9
|
+
link: "https://example.com/blog/",
|
|
10
|
+
feedLink: "https://example.com/blog/rss.xml",
|
|
11
|
+
items: [],
|
|
12
|
+
})
|
|
13
|
+
expect(xml).toBe(
|
|
14
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
15
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
16
|
+
<channel>
|
|
17
|
+
<title>Blog</title>
|
|
18
|
+
<link>https://example.com/blog/</link>
|
|
19
|
+
<description>Latest posts</description>
|
|
20
|
+
<atom:link href="https://example.com/blog/rss.xml" rel="self" type="application/rss+xml" />
|
|
21
|
+
</channel>
|
|
22
|
+
</rss>
|
|
23
|
+
`,
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("renders an item with all optional fields", () => {
|
|
28
|
+
const xml = renderRssFeed({
|
|
29
|
+
title: "Blog",
|
|
30
|
+
description: "Latest posts",
|
|
31
|
+
link: "https://example.com/blog/",
|
|
32
|
+
feedLink: "https://example.com/blog/rss.xml",
|
|
33
|
+
items: [
|
|
34
|
+
{
|
|
35
|
+
title: "Hello",
|
|
36
|
+
link: "https://example.com/blog/hello/",
|
|
37
|
+
guid: "https://example.com/blog/hello/",
|
|
38
|
+
description: "First post",
|
|
39
|
+
pubDate: "2026-05-16T00:00:00Z",
|
|
40
|
+
author: "Jane",
|
|
41
|
+
categories: ["news"],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
expect(xml).toContain("<title>Hello</title>")
|
|
46
|
+
expect(xml).toContain("<link>https://example.com/blog/hello/</link>")
|
|
47
|
+
expect(xml).toContain(
|
|
48
|
+
'<guid isPermaLink="true">https://example.com/blog/hello/</guid>',
|
|
49
|
+
)
|
|
50
|
+
expect(xml).toContain("<description>First post</description>")
|
|
51
|
+
expect(xml).toContain("<pubDate>Sat, 16 May 2026 00:00:00 GMT</pubDate>")
|
|
52
|
+
expect(xml).toContain("<author>Jane</author>")
|
|
53
|
+
expect(xml).toContain("<category>news</category>")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("joins array authors with a comma", () => {
|
|
57
|
+
const xml = renderRssFeed({
|
|
58
|
+
title: "Blog",
|
|
59
|
+
description: "",
|
|
60
|
+
link: "https://example.com/blog/",
|
|
61
|
+
feedLink: "https://example.com/blog/rss.xml",
|
|
62
|
+
items: [
|
|
63
|
+
{
|
|
64
|
+
title: "x",
|
|
65
|
+
link: "https://example.com/blog/x/",
|
|
66
|
+
guid: "https://example.com/blog/x/",
|
|
67
|
+
author: ["Alice", "Bob"],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
})
|
|
71
|
+
expect(xml).toContain("<author>Alice, Bob</author>")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("emits one <category> per tag", () => {
|
|
75
|
+
const xml = renderRssFeed({
|
|
76
|
+
title: "Blog",
|
|
77
|
+
description: "",
|
|
78
|
+
link: "https://example.com/blog/",
|
|
79
|
+
feedLink: "https://example.com/blog/rss.xml",
|
|
80
|
+
items: [
|
|
81
|
+
{
|
|
82
|
+
title: "x",
|
|
83
|
+
link: "https://example.com/blog/x/",
|
|
84
|
+
guid: "https://example.com/blog/x/",
|
|
85
|
+
categories: ["a", "b", "c"],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
expect(xml).toContain("<category>a</category>")
|
|
90
|
+
expect(xml).toContain("<category>b</category>")
|
|
91
|
+
expect(xml).toContain("<category>c</category>")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("escapes XML metacharacters in text content", () => {
|
|
95
|
+
const xml = renderRssFeed({
|
|
96
|
+
title: 'Title & "quoted" <tag>',
|
|
97
|
+
description: "A & B",
|
|
98
|
+
link: "https://example.com/blog/",
|
|
99
|
+
feedLink: "https://example.com/blog/rss.xml",
|
|
100
|
+
items: [
|
|
101
|
+
{
|
|
102
|
+
title: "1 < 2 & 3 > 0",
|
|
103
|
+
link: "https://example.com/blog/x/",
|
|
104
|
+
guid: "https://example.com/blog/x/",
|
|
105
|
+
description: "It's fine",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
expect(xml).toContain(
|
|
110
|
+
"<title>Title & "quoted" <tag></title>",
|
|
111
|
+
)
|
|
112
|
+
expect(xml).toContain("<description>A & B</description>")
|
|
113
|
+
expect(xml).toContain("<title>1 < 2 & 3 > 0</title>")
|
|
114
|
+
expect(xml).toContain("<description>It's fine</description>")
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it("leaves an unparsable pubDate unchanged", () => {
|
|
118
|
+
const xml = renderRssFeed({
|
|
119
|
+
title: "Blog",
|
|
120
|
+
description: "",
|
|
121
|
+
link: "https://example.com/blog/",
|
|
122
|
+
feedLink: "https://example.com/blog/rss.xml",
|
|
123
|
+
items: [
|
|
124
|
+
{
|
|
125
|
+
title: "x",
|
|
126
|
+
link: "https://example.com/blog/x/",
|
|
127
|
+
guid: "https://example.com/blog/x/",
|
|
128
|
+
pubDate: "not-a-date",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
})
|
|
132
|
+
expect(xml).toContain("<pubDate>not-a-date</pubDate>")
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import type { Plugin } from "vite"
|
|
4
|
+
import type { Config } from "../models/config.ts"
|
|
5
|
+
|
|
6
|
+
export interface RobotsOptions {
|
|
7
|
+
config: Config
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Emit a default `robots.txt` into the client build output, with a
|
|
11
|
+
* `Sitemap:` line pointing at the TanStack-Start-generated `sitemap.xml`
|
|
12
|
+
* when {@link Config.site} is set. Skipped when the consumer ships
|
|
13
|
+
* their own `.livemark/public/robots.txt` so the standard publicDir
|
|
14
|
+
* overlay wins — Vite copies the publicDir before writeBundle, so
|
|
15
|
+
* unconditional emitFile would silently overwrite the override. */
|
|
16
|
+
export function robots(opts: RobotsOptions): Plugin {
|
|
17
|
+
const overrideFile = join(
|
|
18
|
+
opts.config.root,
|
|
19
|
+
".livemark",
|
|
20
|
+
"public",
|
|
21
|
+
"robots.txt",
|
|
22
|
+
)
|
|
23
|
+
return {
|
|
24
|
+
name: "livemark:robots",
|
|
25
|
+
applyToEnvironment(env) {
|
|
26
|
+
return env.name === "client"
|
|
27
|
+
},
|
|
28
|
+
generateBundle() {
|
|
29
|
+
if (existsSync(overrideFile)) return
|
|
30
|
+
this.emitFile({
|
|
31
|
+
type: "asset",
|
|
32
|
+
fileName: "robots.txt",
|
|
33
|
+
source: renderRobots(opts.config),
|
|
34
|
+
})
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Render the body of a permissive default `robots.txt`. A `Sitemap:`
|
|
40
|
+
* line is appended only when `site` is set, because the directive
|
|
41
|
+
* requires a fully-qualified URL; `base` is included so the URL matches
|
|
42
|
+
* where the build (and the `sitemap.xml` inside it) is actually mounted. */
|
|
43
|
+
export function renderRobots(config: Pick<Config, "site" | "base">) {
|
|
44
|
+
const lines = ["User-agent: *", "Allow: /"]
|
|
45
|
+
if (config.site) {
|
|
46
|
+
const host = config.site.replace(/\/+$/, "")
|
|
47
|
+
const base = config.base ?? ""
|
|
48
|
+
lines.push("", `Sitemap: ${host}${base}/sitemap.xml`)
|
|
49
|
+
}
|
|
50
|
+
return `${lines.join("\n")}\n`
|
|
51
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test"
|
|
2
|
+
import { renderRobots } from "./vite-robots.ts"
|
|
3
|
+
|
|
4
|
+
describe("renderRobots", () => {
|
|
5
|
+
it("emits an allow-all body when site is not set", () => {
|
|
6
|
+
expect(renderRobots({})).toBe("User-agent: *\nAllow: /\n")
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("appends a Sitemap line when site is set", () => {
|
|
10
|
+
expect(renderRobots({ site: "https://livemark.dev" })).toBe(
|
|
11
|
+
"User-agent: *\nAllow: /\n\nSitemap: https://livemark.dev/sitemap.xml\n",
|
|
12
|
+
)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("strips a trailing slash from site before composing the URL", () => {
|
|
16
|
+
expect(renderRobots({ site: "https://livemark.dev/" })).toBe(
|
|
17
|
+
"User-agent: *\nAllow: /\n\nSitemap: https://livemark.dev/sitemap.xml\n",
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("includes the base path so the URL matches the build mount", () => {
|
|
22
|
+
expect(
|
|
23
|
+
renderRobots({ site: "https://user.github.io", base: "/repo" }),
|
|
24
|
+
).toBe(
|
|
25
|
+
"User-agent: *\nAllow: /\n\nSitemap: https://user.github.io/repo/sitemap.xml\n",
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("ignores base when site is not set", () => {
|
|
30
|
+
expect(renderRobots({ base: "/repo" })).toBe("User-agent: *\nAllow: /\n")
|
|
31
|
+
})
|
|
32
|
+
})
|
package/source/routeTree.gen.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { Route as rootRouteImport } from './routes/__root'
|
|
12
12
|
import { Route as splatRouteImport } from './routes/$'
|
|
13
|
+
import { Route as SectionRssDotxmlRouteImport } from './routes/$section.rss[.]xml'
|
|
13
14
|
import { Route as IndexRouteImport } from './../.livemark/routes/index'
|
|
14
15
|
|
|
15
16
|
const splatRoute = splatRouteImport.update({
|
|
@@ -17,6 +18,11 @@ const splatRoute = splatRouteImport.update({
|
|
|
17
18
|
path: '/$',
|
|
18
19
|
getParentRoute: () => rootRouteImport,
|
|
19
20
|
} as any)
|
|
21
|
+
const SectionRssDotxmlRoute = SectionRssDotxmlRouteImport.update({
|
|
22
|
+
id: '/$section/rss.xml',
|
|
23
|
+
path: '/$section/rss.xml',
|
|
24
|
+
getParentRoute: () => rootRouteImport,
|
|
25
|
+
} as any)
|
|
20
26
|
const IndexRoute = IndexRouteImport.update({
|
|
21
27
|
id: '/',
|
|
22
28
|
path: '/',
|
|
@@ -26,27 +32,31 @@ const IndexRoute = IndexRouteImport.update({
|
|
|
26
32
|
export interface FileRoutesByFullPath {
|
|
27
33
|
'/': typeof IndexRoute
|
|
28
34
|
'/$': typeof splatRoute
|
|
35
|
+
'/$section/rss.xml': typeof SectionRssDotxmlRoute
|
|
29
36
|
}
|
|
30
37
|
export interface FileRoutesByTo {
|
|
31
38
|
'/': typeof IndexRoute
|
|
32
39
|
'/$': typeof splatRoute
|
|
40
|
+
'/$section/rss.xml': typeof SectionRssDotxmlRoute
|
|
33
41
|
}
|
|
34
42
|
export interface FileRoutesById {
|
|
35
43
|
__root__: typeof rootRouteImport
|
|
36
44
|
'/': typeof IndexRoute
|
|
37
45
|
'/$': typeof splatRoute
|
|
46
|
+
'/$section/rss.xml': typeof SectionRssDotxmlRoute
|
|
38
47
|
}
|
|
39
48
|
export interface FileRouteTypes {
|
|
40
49
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
41
|
-
fullPaths: '/' | '/$'
|
|
50
|
+
fullPaths: '/' | '/$' | '/$section/rss.xml'
|
|
42
51
|
fileRoutesByTo: FileRoutesByTo
|
|
43
|
-
to: '/' | '/$'
|
|
44
|
-
id: '__root__' | '/' | '/$'
|
|
52
|
+
to: '/' | '/$' | '/$section/rss.xml'
|
|
53
|
+
id: '__root__' | '/' | '/$' | '/$section/rss.xml'
|
|
45
54
|
fileRoutesById: FileRoutesById
|
|
46
55
|
}
|
|
47
56
|
export interface RootRouteChildren {
|
|
48
57
|
IndexRoute: typeof IndexRoute
|
|
49
58
|
splatRoute: typeof splatRoute
|
|
59
|
+
SectionRssDotxmlRoute: typeof SectionRssDotxmlRoute
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
declare module '@tanstack/react-router' {
|
|
@@ -58,6 +68,13 @@ declare module '@tanstack/react-router' {
|
|
|
58
68
|
preLoaderRoute: typeof splatRouteImport
|
|
59
69
|
parentRoute: typeof rootRouteImport
|
|
60
70
|
}
|
|
71
|
+
'/$section/rss.xml': {
|
|
72
|
+
id: '/$section/rss.xml'
|
|
73
|
+
path: '/$section/rss.xml'
|
|
74
|
+
fullPath: '/$section/rss.xml'
|
|
75
|
+
preLoaderRoute: typeof SectionRssDotxmlRouteImport
|
|
76
|
+
parentRoute: typeof rootRouteImport
|
|
77
|
+
}
|
|
61
78
|
'/': {
|
|
62
79
|
id: '/'
|
|
63
80
|
path: '/'
|
|
@@ -71,6 +88,7 @@ declare module '@tanstack/react-router' {
|
|
|
71
88
|
const rootRouteChildren: RootRouteChildren = {
|
|
72
89
|
IndexRoute: IndexRoute,
|
|
73
90
|
splatRoute: splatRoute,
|
|
91
|
+
SectionRssDotxmlRoute: SectionRssDotxmlRoute,
|
|
74
92
|
}
|
|
75
93
|
export const routeTree = rootRouteImport
|
|
76
94
|
._addFileChildren(rootRouteChildren)
|