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.
@@ -0,0 +1,60 @@
1
+ import { config } from "livemark:virtual"
2
+ import { createFileRoute } from "@tanstack/react-router"
3
+ import { sectionFlatArticles, sortedArticles } from "../content/article.ts"
4
+ import { renderRssFeed, type RssItem } from "../helpers/rss.ts"
5
+
6
+ const FEED_ITEM_LIMIT = 20
7
+
8
+ export const Route = createFileRoute("/$section/rss.xml")({
9
+ server: {
10
+ handlers: {
11
+ GET: async ({ params }: { params: { section: string } }) => {
12
+ if (!config.site) {
13
+ return new Response("RSS requires `site` in livemark.config.ts", {
14
+ status: 404,
15
+ })
16
+ }
17
+ const prefix = `/${params.section}/`
18
+ const section = config.sections?.find(
19
+ s => s.type === "blog" && s.prefix === prefix,
20
+ )
21
+ if (!section || section.type !== "blog") {
22
+ return new Response("Not found", { status: 404 })
23
+ }
24
+ const host = config.site.replace(/\/+$/, "")
25
+ const base = config.base ?? ""
26
+ const toAbsolute = (path: string) => `${host}${base}${path}`
27
+
28
+ const paths = sectionFlatArticles.get(section.prefix) ?? []
29
+ const items: RssItem[] = []
30
+ for (const path of paths) {
31
+ if (items.length >= FEED_ITEM_LIMIT) break
32
+ const article = sortedArticles.find(a => a.path === path)
33
+ if (!article?.date) continue
34
+ const link = toAbsolute(article.path)
35
+ items.push({
36
+ title: article.title,
37
+ link,
38
+ guid: link,
39
+ description: article.description,
40
+ pubDate: article.date,
41
+ author: article.author,
42
+ categories: article.tags,
43
+ })
44
+ }
45
+
46
+ const xml = renderRssFeed({
47
+ title: section.siteTitle ?? section.title,
48
+ description: section.siteDescription ?? config.description ?? "",
49
+ link: toAbsolute(section.prefix),
50
+ feedLink: toAbsolute(`${section.prefix}rss.xml`),
51
+ items,
52
+ })
53
+
54
+ return new Response(xml, {
55
+ headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
56
+ })
57
+ },
58
+ },
59
+ },
60
+ })
@@ -26,6 +26,16 @@ export const Route = createRootRoute({
26
26
  config.favicon ?? config.logo ?? defaultFavicon,
27
27
  config.base,
28
28
  )
29
+ const feedLinks = config.site
30
+ ? (config.sections ?? [])
31
+ .filter(s => s.type === "blog")
32
+ .map(s => ({
33
+ rel: "alternate",
34
+ type: "application/rss+xml",
35
+ title: `${s.siteTitle ?? s.title} RSS Feed`,
36
+ href: prefixUrl(`${s.prefix}rss.xml`, config.base),
37
+ }))
38
+ : []
29
39
  return {
30
40
  meta: [
31
41
  { charSet: "utf-8" },
@@ -48,6 +58,7 @@ export const Route = createRootRoute({
48
58
  { rel: "shortcut icon", href: favicon, type: faviconType(favicon) },
49
59
  { rel: "stylesheet", href: generalCss },
50
60
  { rel: "stylesheet", href: markdownCss },
61
+ ...feedLinks,
51
62
  ],
52
63
  }
53
64
  },
@@ -7,12 +7,27 @@ import { defineConfig } from "vite"
7
7
  import svgr from "vite-plugin-svgr"
8
8
  import { loadConfig } from "./actions/config/load.ts"
9
9
  import { livemark } from "./plugins/vite-livemark.ts"
10
+ import { robots } from "./plugins/vite-robots.ts"
10
11
 
11
12
  const config = await loadConfig()
12
13
 
14
+ // Per-blog-section RSS feed paths to prerender. Gated on `config.site`
15
+ // because feed URLs are absolute (matching the sitemap/robots.txt gate)
16
+ // and the route returns 404 without it anyway. Excluded from the sitemap
17
+ // since `.xml` payloads don't belong in a sitemap.
18
+ const blogFeedPages = config.site
19
+ ? (config.sections ?? [])
20
+ .filter(s => s.type === "blog")
21
+ .map(s => ({
22
+ path: `${config.base ?? ""}${s.prefix}rss.xml`,
23
+ sitemap: { exclude: true },
24
+ }))
25
+ : []
26
+
13
27
  export default defineConfig({
14
28
  plugins: [
15
29
  livemark({ config }),
30
+ robots({ config }),
16
31
  devtools(),
17
32
  tailwind(),
18
33
  contentCollections({ configPath: "content-collections.ts" }),
@@ -21,7 +36,10 @@ export default defineConfig({
21
36
  // Seed path must match what the crawler produces (router-emitted
22
37
  // hrefs include the basepath), otherwise prerender records both
23
38
  // shapes and the sitemap ends up with duplicate entries.
24
- pages: [{ path: config.base ? `${config.base}/` : "/" }],
39
+ pages: [
40
+ { path: config.base ? `${config.base}/` : "/" },
41
+ ...blogFeedPages,
42
+ ],
25
43
  // Disable route-tree auto-discovery: it produces basepath-naive
26
44
  // paths (e.g. `/`) that duplicate the crawl-derived prefixed paths,
27
45
  // bloating pages.json and sitemap. Crawling from the seed already