@geekity/cms 0.3.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.
Files changed (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +870 -0
  3. package/admin/layouts/_flash.njk +5 -0
  4. package/admin/layouts/base.njk +27 -0
  5. package/admin/layouts/dashboard.njk +78 -0
  6. package/admin/layouts/document-conflict.njk +49 -0
  7. package/admin/layouts/document-editor.njk +167 -0
  8. package/admin/layouts/document-list.njk +92 -0
  9. package/admin/layouts/federation.njk +145 -0
  10. package/admin/layouts/login.njk +23 -0
  11. package/admin/layouts/placeholder.njk +10 -0
  12. package/admin/layouts/settings.njk +116 -0
  13. package/admin/layouts/setup.njk +27 -0
  14. package/admin/layouts/shell.njk +37 -0
  15. package/admin/layouts/users.njk +91 -0
  16. package/admin/static/admin.css +726 -0
  17. package/admin/static/editor.js +30 -0
  18. package/dist/admin/assets.d.ts +26 -0
  19. package/dist/admin/assets.d.ts.map +1 -0
  20. package/dist/admin/assets.js +38 -0
  21. package/dist/admin/assets.js.map +1 -0
  22. package/dist/admin/credentials.d.ts +28 -0
  23. package/dist/admin/credentials.d.ts.map +1 -0
  24. package/dist/admin/credentials.js +39 -0
  25. package/dist/admin/credentials.js.map +1 -0
  26. package/dist/admin/documents.d.ts +134 -0
  27. package/dist/admin/documents.d.ts.map +1 -0
  28. package/dist/admin/documents.js +613 -0
  29. package/dist/admin/documents.js.map +1 -0
  30. package/dist/admin/federation.d.ts +194 -0
  31. package/dist/admin/federation.d.ts.map +1 -0
  32. package/dist/admin/federation.js +299 -0
  33. package/dist/admin/federation.js.map +1 -0
  34. package/dist/admin/flash.d.ts +24 -0
  35. package/dist/admin/flash.d.ts.map +1 -0
  36. package/dist/admin/flash.js +31 -0
  37. package/dist/admin/flash.js.map +1 -0
  38. package/dist/admin/index.d.ts +23 -0
  39. package/dist/admin/index.d.ts.map +1 -0
  40. package/dist/admin/index.js +15 -0
  41. package/dist/admin/index.js.map +1 -0
  42. package/dist/admin/passwords.d.ts +49 -0
  43. package/dist/admin/passwords.d.ts.map +1 -0
  44. package/dist/admin/passwords.js +122 -0
  45. package/dist/admin/passwords.js.map +1 -0
  46. package/dist/admin/preview.d.ts +18 -0
  47. package/dist/admin/preview.d.ts.map +1 -0
  48. package/dist/admin/preview.js +83 -0
  49. package/dist/admin/preview.js.map +1 -0
  50. package/dist/admin/routes.d.ts +54 -0
  51. package/dist/admin/routes.d.ts.map +1 -0
  52. package/dist/admin/routes.js +383 -0
  53. package/dist/admin/routes.js.map +1 -0
  54. package/dist/admin/session.d.ts +44 -0
  55. package/dist/admin/session.d.ts.map +1 -0
  56. package/dist/admin/session.js +69 -0
  57. package/dist/admin/session.js.map +1 -0
  58. package/dist/admin/settings.d.ts +194 -0
  59. package/dist/admin/settings.d.ts.map +1 -0
  60. package/dist/admin/settings.js +470 -0
  61. package/dist/admin/settings.js.map +1 -0
  62. package/dist/admin/store.d.ts +407 -0
  63. package/dist/admin/store.d.ts.map +1 -0
  64. package/dist/admin/store.js +704 -0
  65. package/dist/admin/store.js.map +1 -0
  66. package/dist/admin/templates.d.ts +36 -0
  67. package/dist/admin/templates.d.ts.map +1 -0
  68. package/dist/admin/templates.js +43 -0
  69. package/dist/admin/templates.js.map +1 -0
  70. package/dist/admin/uploads.d.ts +96 -0
  71. package/dist/admin/uploads.d.ts.map +1 -0
  72. package/dist/admin/uploads.js +179 -0
  73. package/dist/admin/uploads.js.map +1 -0
  74. package/dist/admin/users.d.ts +95 -0
  75. package/dist/admin/users.d.ts.map +1 -0
  76. package/dist/admin/users.js +233 -0
  77. package/dist/admin/users.js.map +1 -0
  78. package/dist/cli.d.ts +85 -0
  79. package/dist/cli.d.ts.map +1 -0
  80. package/dist/cli.js +424 -0
  81. package/dist/cli.js.map +1 -0
  82. package/dist/config.d.ts +172 -0
  83. package/dist/config.d.ts.map +1 -0
  84. package/dist/config.js +184 -0
  85. package/dist/config.js.map +1 -0
  86. package/dist/content/document.d.ts +66 -0
  87. package/dist/content/document.d.ts.map +1 -0
  88. package/dist/content/document.js +13 -0
  89. package/dist/content/document.js.map +1 -0
  90. package/dist/content/index.d.ts +17 -0
  91. package/dist/content/index.d.ts.map +1 -0
  92. package/dist/content/index.js +10 -0
  93. package/dist/content/index.js.map +1 -0
  94. package/dist/content/markdown.d.ts +3 -0
  95. package/dist/content/markdown.d.ts.map +1 -0
  96. package/dist/content/markdown.js +39 -0
  97. package/dist/content/markdown.js.map +1 -0
  98. package/dist/content/media.d.ts +65 -0
  99. package/dist/content/media.d.ts.map +1 -0
  100. package/dist/content/media.js +143 -0
  101. package/dist/content/media.js.map +1 -0
  102. package/dist/content/parser.d.ts +24 -0
  103. package/dist/content/parser.d.ts.map +1 -0
  104. package/dist/content/parser.js +165 -0
  105. package/dist/content/parser.js.map +1 -0
  106. package/dist/content/save.d.ts +60 -0
  107. package/dist/content/save.d.ts.map +1 -0
  108. package/dist/content/save.js +103 -0
  109. package/dist/content/save.js.map +1 -0
  110. package/dist/content/slug.d.ts +25 -0
  111. package/dist/content/slug.d.ts.map +1 -0
  112. package/dist/content/slug.js +75 -0
  113. package/dist/content/slug.js.map +1 -0
  114. package/dist/content/store.d.ts +133 -0
  115. package/dist/content/store.d.ts.map +1 -0
  116. package/dist/content/store.js +411 -0
  117. package/dist/content/store.js.map +1 -0
  118. package/dist/content/sync.d.ts +146 -0
  119. package/dist/content/sync.d.ts.map +1 -0
  120. package/dist/content/sync.js +421 -0
  121. package/dist/content/sync.js.map +1 -0
  122. package/dist/content/writer.d.ts +37 -0
  123. package/dist/content/writer.d.ts.map +1 -0
  124. package/dist/content/writer.js +97 -0
  125. package/dist/content/writer.js.map +1 -0
  126. package/dist/env.d.ts +48 -0
  127. package/dist/env.d.ts.map +1 -0
  128. package/dist/env.js +2 -0
  129. package/dist/env.js.map +1 -0
  130. package/dist/federation/actor.d.ts +55 -0
  131. package/dist/federation/actor.d.ts.map +1 -0
  132. package/dist/federation/actor.js +101 -0
  133. package/dist/federation/actor.js.map +1 -0
  134. package/dist/federation/article.d.ts +83 -0
  135. package/dist/federation/article.d.ts.map +1 -0
  136. package/dist/federation/article.js +169 -0
  137. package/dist/federation/article.js.map +1 -0
  138. package/dist/federation/delivery.d.ts +90 -0
  139. package/dist/federation/delivery.d.ts.map +1 -0
  140. package/dist/federation/delivery.js +289 -0
  141. package/dist/federation/delivery.js.map +1 -0
  142. package/dist/federation/federation.d.ts +75 -0
  143. package/dist/federation/federation.d.ts.map +1 -0
  144. package/dist/federation/federation.js +190 -0
  145. package/dist/federation/federation.js.map +1 -0
  146. package/dist/federation/followers.d.ts +43 -0
  147. package/dist/federation/followers.d.ts.map +1 -0
  148. package/dist/federation/followers.js +69 -0
  149. package/dist/federation/followers.js.map +1 -0
  150. package/dist/federation/inbox.d.ts +68 -0
  151. package/dist/federation/inbox.d.ts.map +1 -0
  152. package/dist/federation/inbox.js +171 -0
  153. package/dist/federation/inbox.js.map +1 -0
  154. package/dist/federation/index.d.ts +14 -0
  155. package/dist/federation/index.d.ts.map +1 -0
  156. package/dist/federation/index.js +10 -0
  157. package/dist/federation/index.js.map +1 -0
  158. package/dist/federation/keys.d.ts +27 -0
  159. package/dist/federation/keys.d.ts.map +1 -0
  160. package/dist/federation/keys.js +60 -0
  161. package/dist/federation/keys.js.map +1 -0
  162. package/dist/federation/mount.d.ts +23 -0
  163. package/dist/federation/mount.d.ts.map +1 -0
  164. package/dist/federation/mount.js +71 -0
  165. package/dist/federation/mount.js.map +1 -0
  166. package/dist/federation/paths.d.ts +85 -0
  167. package/dist/federation/paths.d.ts.map +1 -0
  168. package/dist/federation/paths.js +97 -0
  169. package/dist/federation/paths.js.map +1 -0
  170. package/dist/index.d.ts +106 -0
  171. package/dist/index.d.ts.map +1 -0
  172. package/dist/index.js +148 -0
  173. package/dist/index.js.map +1 -0
  174. package/dist/init.d.ts +71 -0
  175. package/dist/init.d.ts.map +1 -0
  176. package/dist/init.js +125 -0
  177. package/dist/init.js.map +1 -0
  178. package/dist/web/assets.d.ts +65 -0
  179. package/dist/web/assets.d.ts.map +1 -0
  180. package/dist/web/assets.js +152 -0
  181. package/dist/web/assets.js.map +1 -0
  182. package/dist/web/context.d.ts +121 -0
  183. package/dist/web/context.d.ts.map +1 -0
  184. package/dist/web/context.js +112 -0
  185. package/dist/web/context.js.map +1 -0
  186. package/dist/web/documents.d.ts +28 -0
  187. package/dist/web/documents.d.ts.map +1 -0
  188. package/dist/web/documents.js +39 -0
  189. package/dist/web/documents.js.map +1 -0
  190. package/dist/web/feeds.d.ts +122 -0
  191. package/dist/web/feeds.d.ts.map +1 -0
  192. package/dist/web/feeds.js +229 -0
  193. package/dist/web/feeds.js.map +1 -0
  194. package/dist/web/index.d.ts +17 -0
  195. package/dist/web/index.d.ts.map +1 -0
  196. package/dist/web/index.js +10 -0
  197. package/dist/web/index.js.map +1 -0
  198. package/dist/web/negotiate.d.ts +219 -0
  199. package/dist/web/negotiate.d.ts.map +1 -0
  200. package/dist/web/negotiate.js +409 -0
  201. package/dist/web/negotiate.js.map +1 -0
  202. package/dist/web/pagination.d.ts +48 -0
  203. package/dist/web/pagination.d.ts.map +1 -0
  204. package/dist/web/pagination.js +30 -0
  205. package/dist/web/pagination.js.map +1 -0
  206. package/dist/web/render.d.ts +72 -0
  207. package/dist/web/render.d.ts.map +1 -0
  208. package/dist/web/render.js +73 -0
  209. package/dist/web/render.js.map +1 -0
  210. package/dist/web/routes.d.ts +31 -0
  211. package/dist/web/routes.d.ts.map +1 -0
  212. package/dist/web/routes.js +366 -0
  213. package/dist/web/routes.js.map +1 -0
  214. package/dist/web/templates.d.ts +34 -0
  215. package/dist/web/templates.d.ts.map +1 -0
  216. package/dist/web/templates.js +106 -0
  217. package/dist/web/templates.js.map +1 -0
  218. package/package.json +68 -0
  219. package/templates/site/content/_data/site.json +8 -0
  220. package/templates/site/content/pages/about.md +11 -0
  221. package/templates/site/content/pages/pages.json +3 -0
  222. package/templates/site/content/posts/2026-01-01-hello-world.md +34 -0
  223. package/templates/site/content/posts/posts.json +4 -0
  224. package/templates/site/geekity.config.ts +11 -0
  225. package/templates/site/gitignore +3 -0
  226. package/templates/site/server.ts +20 -0
  227. package/templates/site/tsconfig.json +18 -0
  228. package/themes/default/README.md +148 -0
  229. package/themes/default/layouts/404.njk +9 -0
  230. package/themes/default/layouts/base.njk +59 -0
  231. package/themes/default/layouts/home.njk +9 -0
  232. package/themes/default/layouts/page.njk +13 -0
  233. package/themes/default/layouts/post.njk +30 -0
  234. package/themes/default/layouts/tag.njk +19 -0
  235. package/themes/default/partials/pagination.njk +14 -0
  236. package/themes/default/partials/post-list.njk +24 -0
  237. package/themes/default/partials/tags.njk +12 -0
  238. package/themes/default/static/style.css +327 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Where one listing page sits in the sequence.
3
+ *
4
+ * `pageNumber` is zero-based, as it is in Eleventy, so a layout ported from an
5
+ * Eleventy build does the same arithmetic. `href` holds the URLs a pager needs;
6
+ * `next` and `previous` are `null` at the ends rather than absent, which is
7
+ * what Eleventy does and what `{% if pagination.href.next %}` expects.
8
+ */
9
+ export interface Pagination {
10
+ /** Zero-based index of this page. */
11
+ pageNumber: number;
12
+ /** How many pages the listing has. At least 1, even when it is empty. */
13
+ totalPages: number;
14
+ /** How many documents the whole listing holds. */
15
+ total: number;
16
+ /** How many documents fit on a page. */
17
+ size: number;
18
+ /** Where the pager links. */
19
+ href: {
20
+ first: string;
21
+ last: string;
22
+ next: string | null;
23
+ previous: string | null;
24
+ };
25
+ /** Every page's URL, in order, so a template can render a numbered pager. */
26
+ pages: string[];
27
+ }
28
+ /** What {@link paginate} needs to place a page. */
29
+ export interface PaginateOptions {
30
+ /** How many documents the listing holds in total. */
31
+ total: number;
32
+ /** How many fit on a page. Values below 1 are treated as 1. */
33
+ size: number;
34
+ /** Zero-based index of the page being rendered. */
35
+ pageNumber: number;
36
+ /** The URL of a page, by zero-based index. */
37
+ hrefForPage: (index: number) => string;
38
+ }
39
+ /** How many documents to skip to reach a page. */
40
+ export declare function offsetForPage(pageNumber: number, size: number): number;
41
+ /**
42
+ * The page count and the pager URLs for a listing.
43
+ *
44
+ * An empty listing still has one page, so the home page of a site with no
45
+ * posts renders normally instead of 404ing.
46
+ */
47
+ export declare function paginate(options: PaginateOptions): Pagination;
48
+ //# sourceMappingURL=pagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/web/pagination.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;KACzB,CAAC;IACF,6EAA6E;IAC7E,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,mDAAmD;AACnD,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC;AAED,kDAAkD;AAClD,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,UAAU,CAmB7D"}
@@ -0,0 +1,30 @@
1
+ /** How many documents to skip to reach a page. */
2
+ export function offsetForPage(pageNumber, size) {
3
+ return pageNumber * Math.max(1, size);
4
+ }
5
+ /**
6
+ * The page count and the pager URLs for a listing.
7
+ *
8
+ * An empty listing still has one page, so the home page of a site with no
9
+ * posts renders normally instead of 404ing.
10
+ */
11
+ export function paginate(options) {
12
+ const size = Math.max(1, options.size);
13
+ const totalPages = Math.max(1, Math.ceil(options.total / size));
14
+ const pageNumber = options.pageNumber;
15
+ const pages = Array.from({ length: totalPages }, (_, index) => options.hrefForPage(index));
16
+ return {
17
+ pageNumber,
18
+ totalPages,
19
+ total: options.total,
20
+ size,
21
+ href: {
22
+ first: options.hrefForPage(0),
23
+ last: options.hrefForPage(totalPages - 1),
24
+ next: pageNumber + 1 < totalPages ? options.hrefForPage(pageNumber + 1) : null,
25
+ previous: pageNumber > 0 ? options.hrefForPage(pageNumber - 1) : null,
26
+ },
27
+ pages,
28
+ };
29
+ }
30
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../src/web/pagination.ts"],"names":[],"mappings":"AAwCA,kDAAkD;AAClD,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,IAAY;IAC5D,OAAO,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACxC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAwB;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC;IAChE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAE3F,OAAO;QACL,UAAU;QACV,UAAU;QACV,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,IAAI;QACJ,IAAI,EAAE;YACJ,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;YAC7B,IAAI,EAAE,OAAO,CAAC,WAAW,CAAC,UAAU,GAAG,CAAC,CAAC;YACzC,IAAI,EAAE,UAAU,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;YAC9E,QAAQ,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;SACtE;QACD,KAAK;KACN,CAAC;AACJ,CAAC"}
@@ -0,0 +1,72 @@
1
+ import type { Environment } from 'nunjucks';
2
+ import type { ResolvedConfig } from '../config.ts';
3
+ import type { Document } from '../content/document.ts';
4
+ import type { SiteData, SiteSettingsSource } from './context.ts';
5
+ import type { Pagination } from './pagination.ts';
6
+ /** Templates the default theme ships and the public routes ask for by name. */
7
+ export declare const TEMPLATES: {
8
+ readonly home: 'layouts/home.njk';
9
+ readonly post: 'layouts/post.njk';
10
+ readonly page: 'layouts/page.njk';
11
+ readonly tag: 'layouts/tag.njk';
12
+ readonly notFound: 'layouts/404.njk';
13
+ };
14
+ /** A listing of documents, ready to render. */
15
+ export interface Listing {
16
+ /** Heading for the page, and the basis of its `<title>`. */
17
+ title: string;
18
+ /** The listing's own URL, for `page.url`. */
19
+ url: string;
20
+ /** The documents on this page of the listing, newest first. */
21
+ documents: Document[];
22
+ /** Where this page sits in the sequence. */
23
+ pagination: Pagination;
24
+ /** The tag, on a tag archive. Absent on the home page. */
25
+ tag?: string | undefined;
26
+ /** Which template to use. Defaults to the home layout. */
27
+ template?: string | undefined;
28
+ }
29
+ /**
30
+ * The HTML side of the public site: everything that turns a document or a
31
+ * listing into a page of the theme.
32
+ *
33
+ * It is deliberately separate from the routes. Content negotiation picks a
34
+ * representation and then asks the renderer for the HTML one; feeds ask for
35
+ * nothing here at all. Nothing in this interface touches a request.
36
+ */
37
+ export interface Renderer {
38
+ /** Site-wide data, re-read when `content/_data/site.json` changes. */
39
+ site(): SiteData;
40
+ /** How many documents a listing page holds, per the site data. */
41
+ pageSize(): number;
42
+ /** One document through its type's layout. */
43
+ renderDocument(document: Document): string;
44
+ /** A listing through the home or tag layout. */
45
+ renderListing(listing: Listing): string;
46
+ /** The 404 page, for a path that resolved to nothing. */
47
+ renderNotFound(url: string): string;
48
+ /** Any template by name, with the site data already in the context. */
49
+ render(template: string, context?: Record<string, unknown>): string;
50
+ /** The Nunjucks environment, for a site that wants to add its own filters. */
51
+ readonly environment: Environment;
52
+ }
53
+ /** How to build a {@link Renderer}. */
54
+ export interface CreateRendererOptions {
55
+ /** Config after defaults, for the theme directory, base URL and content directory. */
56
+ config: ResolvedConfig;
57
+ /**
58
+ * The admin's stored settings, which win over `content/_data/site.json` for
59
+ * the values the settings screen manages. Absent for a renderer built
60
+ * without an admin store, which then reads the file alone.
61
+ */
62
+ settings?: SiteSettingsSource | undefined;
63
+ }
64
+ /**
65
+ * Build the renderer for one site.
66
+ *
67
+ * Templates are cached unless the content watcher is on, which is the same
68
+ * switch as "this is a development server": with `watch: true` a template edit
69
+ * shows up on the next request, exactly as a content edit does.
70
+ */
71
+ export declare function createRenderer(options: CreateRendererOptions): Renderer;
72
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/web/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,KAAK,EAAmB,QAAQ,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlD,+EAA+E;AAC/E,eAAO,MAAM,SAAS;aACpB,IAAI,EAAE,kBAAkB;aACxB,IAAI,EAAE,kBAAkB;aACxB,IAAI,EAAE,kBAAkB;aACxB,GAAG,EAAE,iBAAiB;aACtB,QAAQ,EAAE,iBAAiB;CACnB,CAAC;AAEX,+CAA+C;AAC/C,MAAM,WAAW,OAAO;IACtB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,+DAA+D;IAC/D,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,4CAA4C;IAC5C,UAAU,EAAE,UAAU,CAAC;IACvB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,sEAAsE;IACtE,IAAI,IAAI,QAAQ,CAAC;IACjB,kEAAkE;IAClE,QAAQ,IAAI,MAAM,CAAC;IACnB,8CAA8C;IAC9C,cAAc,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC3C,gDAAgD;IAChD,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IACxC,yDAAyD;IACzD,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,uEAAuE;IACvE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;IACpE,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;CACnC;AAED,uCAAuC;AACvC,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,MAAM,EAAE,cAAc,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,QAAQ,CA8DvE"}
@@ -0,0 +1,73 @@
1
+ import { createSiteDataSource, documentContext, postsPerPage } from './context.js';
2
+ import { activityStreamsId } from './documents.js';
3
+ import { createTemplateEnvironment } from './templates.js';
4
+ /** Templates the default theme ships and the public routes ask for by name. */
5
+ export const TEMPLATES = {
6
+ home: 'layouts/home.njk',
7
+ post: 'layouts/post.njk',
8
+ page: 'layouts/page.njk',
9
+ tag: 'layouts/tag.njk',
10
+ notFound: 'layouts/404.njk',
11
+ };
12
+ /**
13
+ * Build the renderer for one site.
14
+ *
15
+ * Templates are cached unless the content watcher is on, which is the same
16
+ * switch as "this is a development server": with `watch: true` a template edit
17
+ * shows up on the next request, exactly as a content edit does.
18
+ */
19
+ export function createRenderer(options) {
20
+ const { config } = options;
21
+ const environment = createTemplateEnvironment({
22
+ themeDir: config.themeDir,
23
+ baseUrl: config.baseUrl,
24
+ noCache: config.watch,
25
+ });
26
+ const siteData = createSiteDataSource(config, { settings: options.settings });
27
+ function render(template, context = {}) {
28
+ const site = siteData.read();
29
+ return environment.render(template, { site, ...context });
30
+ }
31
+ return {
32
+ environment,
33
+ site() {
34
+ return siteData.read();
35
+ },
36
+ pageSize() {
37
+ return postsPerPage(siteData.read());
38
+ },
39
+ renderDocument(document) {
40
+ const template = document.type === 'post' ? TEMPLATES.post : TEMPLATES.page;
41
+ // `activityStreams` is the object id the base layout advertises. It is
42
+ // added here rather than in `documentContext` because it needs the
43
+ // site's base URL, which a document on its own does not carry.
44
+ const objectId = activityStreamsId(document, config.baseUrl);
45
+ return render(template, {
46
+ ...documentContext(document),
47
+ ...(objectId === undefined ? {} : { activityStreams: objectId }),
48
+ });
49
+ },
50
+ renderListing(listing) {
51
+ const items = listing.documents.map(documentContext);
52
+ return render(listing.template ?? TEMPLATES.home, {
53
+ title: listing.title,
54
+ url: listing.url,
55
+ page: { url: listing.url },
56
+ // `posts` is the friendly name a theme loops over; `pagination.items`
57
+ // is the same array under the name Eleventy gives it.
58
+ posts: items,
59
+ pagination: { ...listing.pagination, items },
60
+ ...(listing.tag === undefined ? {} : { tag: listing.tag }),
61
+ });
62
+ },
63
+ renderNotFound(url) {
64
+ return render(TEMPLATES.notFound, {
65
+ title: 'Not found',
66
+ url,
67
+ page: { url },
68
+ });
69
+ },
70
+ render,
71
+ };
72
+ }
73
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/web/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { Context, Hono } from 'hono';
2
+ import type { GeekityEnv } from '../env.ts';
3
+ import type { FeedFormat } from './feeds.ts';
4
+ /** Where a paginated listing's later pages live, under any listing root. */
5
+ export declare const PAGE_SEGMENT = "page";
6
+ /** Root of the tag archives. */
7
+ export declare const TAG_SEGMENT = "tags";
8
+ /**
9
+ * Register the public site on a Hono app.
10
+ *
11
+ * The listings and the theme assets are routes; a document is resolved in the
12
+ * not-found handler instead. That ordering is deliberate: admin and federation
13
+ * routes registered after this one still win, and a permalink can never shadow
14
+ * them, because the document lookup only runs once nothing else has matched.
15
+ */
16
+ export declare function mountPublicSite(app: Hono<GeekityEnv>): void;
17
+ /** The URL of a page of the home listing, by zero-based index. */
18
+ export declare function homeHref(index: number): string;
19
+ /** The URL of a page of a tag archive, by zero-based index. */
20
+ export declare function tagHref(tag: string, index: number): string;
21
+ /** The URL of a feed: the whole archive's, or one tag's. */
22
+ export declare function feedHref(tag: string | undefined, format: FeedFormat): string;
23
+ /** The theme's 404 page. */
24
+ export declare function notFound(c: Context<GeekityEnv>): Response;
25
+ /**
26
+ * The requested path, percent-decoded, because permalinks are stored decoded.
27
+ * A path whose escapes are malformed is used as it arrived, which resolves to
28
+ * nothing and so 404s.
29
+ */
30
+ export declare function requestPath(c: Context<GeekityEnv>): string;
31
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/web/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAgB5C,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,YAAY,CAAC;AAkBzD,4EAA4E;AAC5E,eAAO,MAAM,YAAY,SAAS,CAAC;AAEnC,gCAAgC;AAChC,eAAO,MAAM,WAAW,SAAS,CAAC;AAElC;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,CAiC3D;AAqVD,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,+DAA+D;AAC/D,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,4DAA4D;AAC5D,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAE5E;AAED,4BAA4B;AAC5B,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,QAAQ,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,MAAM,CAO1D"}
@@ -0,0 +1,366 @@
1
+ import { serializeDocument } from '../content/writer.js';
2
+ import { assetNotModified, assetResponse, findThemeAsset, findUpload, matchesEtag, themeAssetNotModified, themeAssetResponse, themeSearchPath, THEME_ASSET_PREFIX, UPLOAD_ASSET_MAX_AGE, UPLOAD_ASSET_PREFIX, } from './assets.js';
3
+ import { isPublicDocument, publicDocumentAt } from './documents.js';
4
+ import { feedResponse, feedSize, FEED_FILES } from './feeds.js';
5
+ import { DOCUMENT_REPRESENTATIONS, documentJson, LISTING_REPRESENTATIONS, lastModifiedOf, latestModified, notAcceptableResponse, representationEtag, representationResponse, selectRepresentation, splitRepresentationExtension, } from './negotiate.js';
6
+ import { offsetForPage, paginate } from './pagination.js';
7
+ import { TEMPLATES } from './render.js';
8
+ /** Where a paginated listing's later pages live, under any listing root. */
9
+ export const PAGE_SEGMENT = 'page';
10
+ /** Root of the tag archives. */
11
+ export const TAG_SEGMENT = 'tags';
12
+ /**
13
+ * Register the public site on a Hono app.
14
+ *
15
+ * The listings and the theme assets are routes; a document is resolved in the
16
+ * not-found handler instead. That ordering is deliberate: admin and federation
17
+ * routes registered after this one still win, and a permalink can never shadow
18
+ * them, because the document lookup only runs once nothing else has matched.
19
+ */
20
+ export function mountPublicSite(app) {
21
+ app.get(`${THEME_ASSET_PREFIX}*`, themeAsset);
22
+ app.get(`${UPLOAD_ASSET_PREFIX}*`, upload);
23
+ app.get('/', (c) => listing(c, { tag: undefined, pageNumber: 0 }));
24
+ app.get(`/${PAGE_SEGMENT}/:page{[0-9]+}/`, (c) => {
25
+ const requested = Number(c.req.param('page'));
26
+ // Page one is the home page; it does not get a second URL.
27
+ if (requested <= 1)
28
+ return c.redirect('/', 301);
29
+ return listing(c, { tag: undefined, pageNumber: requested - 1 });
30
+ });
31
+ // Feeds are routes rather than representations, so a reader's careless
32
+ // `Accept` header cannot land it on the HTML (doc-3). They are registered
33
+ // before the tag archive so the shape of the URL, not the negotiator,
34
+ // decides what comes back.
35
+ app.get(`/${FEED_FILES.atom}`, (c) => feed(c, 'atom', undefined));
36
+ app.get(`/${FEED_FILES.json}`, (c) => feed(c, 'json', undefined));
37
+ app.get(`/${TAG_SEGMENT}/:tag/${FEED_FILES.atom}`, (c) => feed(c, 'atom', c.req.param('tag')));
38
+ app.get(`/${TAG_SEGMENT}/:tag/${FEED_FILES.json}`, (c) => feed(c, 'json', c.req.param('tag')));
39
+ app.get(`/${TAG_SEGMENT}/:tag/`, (c) => listing(c, { tag: c.req.param('tag'), pageNumber: 0 }));
40
+ app.get(`/${TAG_SEGMENT}/:tag/${PAGE_SEGMENT}/:page{[0-9]+}/`, (c) => {
41
+ const tag = c.req.param('tag');
42
+ const requested = Number(c.req.param('page'));
43
+ if (requested <= 1)
44
+ return c.redirect(tagHref(tag, 0), 301);
45
+ return listing(c, { tag, pageNumber: requested - 1 });
46
+ });
47
+ app.notFound(resolveDocument);
48
+ }
49
+ /**
50
+ * The last stop for a request: the document at this URL, the same document in
51
+ * the representation a `.md` or `.json` suffix asked for, the canonical URL it
52
+ * should have asked for, or the theme's 404.
53
+ *
54
+ * The suffix is stripped and the *same* {@link publicDocumentAt} lookup runs
55
+ * again, so no representation can resolve to a document the others cannot see.
56
+ */
57
+ function resolveDocument(c) {
58
+ const { store } = c.var;
59
+ const pathname = requestPath(c);
60
+ const document = publicDocumentAt(store, pathname);
61
+ if (document !== undefined) {
62
+ return negotiateDocument(c, document, selectFromAccept(c, DOCUMENT_REPRESENTATIONS));
63
+ }
64
+ const extension = splitRepresentationExtension(pathname);
65
+ if (extension !== undefined) {
66
+ for (const candidate of extension.paths) {
67
+ const found = publicDocumentAt(store, candidate);
68
+ if (found !== undefined)
69
+ return negotiateDocument(c, found, extension.representation);
70
+ }
71
+ // `/index.json`, `/page/2/index.json`, `/tags/x/index.json`: the same
72
+ // escape hatch over a listing, for the representations a listing has.
73
+ if (LISTING_REPRESENTATIONS.includes(extension.representation)) {
74
+ for (const candidate of extension.paths) {
75
+ const request = parseListingPath(candidate);
76
+ if (request !== undefined)
77
+ return listing(c, request, extension.representation);
78
+ }
79
+ }
80
+ }
81
+ const canonical = canonicalPath(c, pathname);
82
+ if (canonical !== undefined)
83
+ return c.redirect(canonical, 301);
84
+ return notFound(c);
85
+ }
86
+ /** The representation an `Accept` header asked for, or `undefined` for a 406. */
87
+ function selectFromAccept(c, available) {
88
+ return selectRepresentation(c.req.header('accept'), available);
89
+ }
90
+ /** One document in one representation, or the 406 an impossible `Accept` earns. */
91
+ function negotiateDocument(c, document, representation) {
92
+ if (representation === undefined) {
93
+ return notAcceptableResponse(encodePath(document.permalink), DOCUMENT_REPRESENTATIONS);
94
+ }
95
+ const body = representation === 'markdown'
96
+ ? serializeDocument(document)
97
+ : representation === 'json'
98
+ ? documentJson(document, { baseUrl: c.var.config.baseUrl })
99
+ : c.var.renderer.renderDocument(document);
100
+ // The theme can change without the document changing, and only the document
101
+ // is hashed. While the watcher is on — a development server, where a template
102
+ // edit lands mid-process — the HTML gets no validator rather than a stale one.
103
+ const validated = representation !== 'html' || !c.var.config.watch;
104
+ return representationResponse({
105
+ body,
106
+ representation,
107
+ href: encodePath(document.permalink),
108
+ available: DOCUMENT_REPRESENTATIONS,
109
+ ...(validated
110
+ ? {
111
+ etag: representationEtag(representation, document.hash),
112
+ lastModified: lastModifiedOf(document),
113
+ }
114
+ : {}),
115
+ conditional: conditionalHeaders(c),
116
+ });
117
+ }
118
+ /** The conditional request headers, as the negotiator wants them. */
119
+ function conditionalHeaders(c) {
120
+ return {
121
+ ifNoneMatch: c.req.header('if-none-match'),
122
+ ifModifiedSince: c.req.header('if-modified-since'),
123
+ };
124
+ }
125
+ /**
126
+ * The canonical URL for a path that arrived without its trailing slash, or
127
+ * `undefined` when adding one would not resolve either.
128
+ *
129
+ * Redirecting only to somewhere real means a genuinely missing URL costs one
130
+ * 404 rather than a redirect and then a 404. The target is the canonical URL
131
+ * itself rather than the path with a slash bolted on, so `/page/1` lands on
132
+ * `/` in one hop instead of two.
133
+ */
134
+ function canonicalPath(c, pathname) {
135
+ if (pathname === '' || pathname.endsWith('/'))
136
+ return undefined;
137
+ const target = canonicalTarget(c, `${pathname}/`);
138
+ if (target === undefined)
139
+ return undefined;
140
+ return `${target}${new URL(c.req.url).search}`;
141
+ }
142
+ /** Where a path with a trailing slash canonically lives, if anywhere. */
143
+ function canonicalTarget(c, pathname) {
144
+ const { store, renderer } = c.var;
145
+ const document = store.getByPermalink(pathname);
146
+ if (document !== undefined) {
147
+ return isPublicDocument(document) ? encodePath(pathname) : undefined;
148
+ }
149
+ const listing = parseListingPath(pathname);
150
+ if (listing === undefined)
151
+ return undefined;
152
+ // The home listing exists even with nothing on it; a tag archive does not.
153
+ const total = listing.tag === undefined ? store.counts().posts : store.countByTag(listing.tag);
154
+ if (listing.tag !== undefined && total === 0)
155
+ return undefined;
156
+ const totalPages = Math.max(1, Math.ceil(total / renderer.pageSize()));
157
+ if (listing.pageNumber >= totalPages)
158
+ return undefined;
159
+ return listing.tag === undefined
160
+ ? homeHref(listing.pageNumber)
161
+ : tagHref(listing.tag, listing.pageNumber);
162
+ }
163
+ /** `/`, `/page/N/`, `/tags/x/` and `/tags/x/page/N/`, as a listing to check. */
164
+ function parseListingPath(pathname) {
165
+ const segments = pathname.split('/').filter((segment) => segment !== '');
166
+ if (segments.length === 0)
167
+ return { tag: undefined, pageNumber: 0 };
168
+ if (segments[0] === PAGE_SEGMENT && segments.length === 2) {
169
+ const page = pageIndex(segments[1]);
170
+ return page === undefined ? undefined : { tag: undefined, pageNumber: page };
171
+ }
172
+ if (segments[0] !== TAG_SEGMENT || segments[1] === undefined)
173
+ return undefined;
174
+ const tag = segments[1];
175
+ if (segments.length === 2)
176
+ return { tag, pageNumber: 0 };
177
+ if (segments.length === 4 && segments[2] === PAGE_SEGMENT) {
178
+ const page = pageIndex(segments[3]);
179
+ return page === undefined ? undefined : { tag, pageNumber: page };
180
+ }
181
+ return undefined;
182
+ }
183
+ /**
184
+ * A `/page/N/` segment as a zero-based index. `page/1` parses to the first
185
+ * page, which lives at the listing root, so canonicalisation collapses it.
186
+ */
187
+ function pageIndex(segment) {
188
+ if (segment === undefined || !/^[0-9]+$/.test(segment))
189
+ return undefined;
190
+ const requested = Number(segment);
191
+ return requested < 1 ? undefined : requested - 1;
192
+ }
193
+ /** One page of a listing, in whichever representation the request settled on. */
194
+ function listing(c, request, representation = selectFromAccept(c, LISTING_REPRESENTATIONS)) {
195
+ const { store, renderer } = c.var;
196
+ const { tag } = request;
197
+ const size = renderer.pageSize();
198
+ // A tag archive only exists while something carries the tag; the home
199
+ // listing exists even with nothing on it.
200
+ const total = tag === undefined ? store.counts().posts : store.countByTag(tag);
201
+ if (tag !== undefined && total === 0)
202
+ return notFound(c);
203
+ const href = tag === undefined ? homeHref(request.pageNumber) : tagHref(tag, request.pageNumber);
204
+ const pagination = paginate({
205
+ total,
206
+ size,
207
+ pageNumber: request.pageNumber,
208
+ hrefForPage: (index) => (tag === undefined ? homeHref(index) : tagHref(tag, index)),
209
+ });
210
+ if (request.pageNumber >= pagination.totalPages)
211
+ return notFound(c);
212
+ if (representation === undefined)
213
+ return notAcceptableResponse(href, LISTING_REPRESENTATIONS);
214
+ const paging = { limit: size, offset: offsetForPage(request.pageNumber, size) };
215
+ const documents = tag === undefined ? store.listPosts(paging) : store.listByTag(tag, paging);
216
+ const full = wantsFullDocuments(c);
217
+ const body = representation === 'json'
218
+ ? documents.map((document) => documentJson(document, { baseUrl: c.var.config.baseUrl, body: full }))
219
+ : renderer.renderListing({
220
+ title: tag ?? renderer.site().title,
221
+ url: href,
222
+ documents,
223
+ pagination,
224
+ ...(tag === undefined ? {} : { tag, template: TEMPLATES.tag }),
225
+ });
226
+ const validated = representation !== 'html' || !c.var.config.watch;
227
+ return representationResponse({
228
+ body,
229
+ representation,
230
+ href,
231
+ available: LISTING_REPRESENTATIONS,
232
+ ...(validated
233
+ ? {
234
+ etag: representationEtag(representation, listingFingerprint(href, pagination, documents, full)),
235
+ lastModified: latestModified(documents),
236
+ }
237
+ : {}),
238
+ conditional: conditionalHeaders(c),
239
+ });
240
+ }
241
+ /**
242
+ * Whether the request asked for whole documents rather than summaries.
243
+ *
244
+ * Anything but an explicit denial counts, so `?full`, `?full=1` and `?full=yes`
245
+ * all mean the same thing to someone typing a URL by hand.
246
+ */
247
+ function wantsFullDocuments(c) {
248
+ const value = c.req.query('full');
249
+ return value !== undefined && value !== '0' && value.toLowerCase() !== 'false';
250
+ }
251
+ /**
252
+ * What a listing page is made of, as one string to hash.
253
+ *
254
+ * The documents on the page are not the whole story: the same two posts on a
255
+ * differently sized archive are a different page, and a summary is not the
256
+ * body, so the pagination and the `full` flag are in the fingerprint too.
257
+ */
258
+ function listingFingerprint(href, pagination, documents, full) {
259
+ return [
260
+ href,
261
+ `${String(pagination.pageNumber)}/${String(pagination.totalPages)}/${String(pagination.total)}`,
262
+ full ? 'full' : 'summary',
263
+ ...documents.map((document) => document.hash),
264
+ ].join('\n');
265
+ }
266
+ /**
267
+ * The site's feed, or one tag's, in one format.
268
+ *
269
+ * A tag with nothing published under it 404s exactly as its archive does: a
270
+ * feed reader should be told the URL is wrong rather than handed an empty feed
271
+ * it will poll forever.
272
+ */
273
+ function feed(c, format, tag) {
274
+ const { store, renderer, config } = c.var;
275
+ const site = renderer.site();
276
+ if (tag !== undefined && store.countByTag(tag) === 0)
277
+ return notFound(c);
278
+ const documents = tag === undefined
279
+ ? store.listPosts({ limit: feedSize(site) })
280
+ : store.listByTag(tag, { limit: feedSize(site) });
281
+ const href = tag === undefined ? homeHref(0) : tagHref(tag, 0);
282
+ const source = {
283
+ site,
284
+ documents,
285
+ title: tag === undefined ? site.title : `${site.title}: ${tag}`,
286
+ href,
287
+ feedHref: feedHref(tag, format),
288
+ baseUrl: config.baseUrl,
289
+ };
290
+ return feedResponse({ format, source, conditional: conditionalHeaders(c) });
291
+ }
292
+ /**
293
+ * A file from the theme's `static/` directory, the site's copy first.
294
+ *
295
+ * Assets are cacheable and validated, so a browser that already has one pays a
296
+ * conditional request rather than a download.
297
+ */
298
+ function themeAsset(c) {
299
+ const relative = requestPath(c).slice(THEME_ASSET_PREFIX.length);
300
+ const asset = findThemeAsset(relative, themeSearchPath(c.var.config.themeDir));
301
+ if (asset === undefined)
302
+ return notFound(c);
303
+ if (matchesEtag(c.req.header('if-none-match'), asset.etag)) {
304
+ return themeAssetNotModified(asset);
305
+ }
306
+ return themeAssetResponse(asset);
307
+ }
308
+ /**
309
+ * A file from `content/uploads/`, at the URL Eleventy's passthrough copy puts
310
+ * it at.
311
+ *
312
+ * Uploads are not documents — the index never sees them — so they are served
313
+ * as files, with the same validators and the same traversal check the theme's
314
+ * assets get. The cache lifetime is longer than a theme asset's because an
315
+ * upload's URL names one set of bytes: the upload endpoint never overwrites,
316
+ * it suffixes.
317
+ */
318
+ function upload(c) {
319
+ const relative = requestPath(c).slice(UPLOAD_ASSET_PREFIX.length);
320
+ const asset = findUpload(relative, c.var.config.contentDir);
321
+ if (asset === undefined)
322
+ return notFound(c);
323
+ const options = { maxAge: UPLOAD_ASSET_MAX_AGE };
324
+ return matchesEtag(c.req.header('if-none-match'), asset.etag)
325
+ ? assetNotModified(asset, options)
326
+ : assetResponse(asset, options);
327
+ }
328
+ /** The URL of a page of the home listing, by zero-based index. */
329
+ export function homeHref(index) {
330
+ return index === 0 ? '/' : `/${PAGE_SEGMENT}/${String(index + 1)}/`;
331
+ }
332
+ /** The URL of a page of a tag archive, by zero-based index. */
333
+ export function tagHref(tag, index) {
334
+ const root = `/${TAG_SEGMENT}/${encodeURIComponent(tag)}/`;
335
+ return index === 0 ? root : `${root}${PAGE_SEGMENT}/${String(index + 1)}/`;
336
+ }
337
+ /** The URL of a feed: the whole archive's, or one tag's. */
338
+ export function feedHref(tag, format) {
339
+ return tag === undefined ? `/${FEED_FILES[format]}` : `${tagHref(tag, 0)}${FEED_FILES[format]}`;
340
+ }
341
+ /** The theme's 404 page. */
342
+ export function notFound(c) {
343
+ return c.html(c.var.renderer.renderNotFound(requestPath(c)), 404);
344
+ }
345
+ /**
346
+ * The requested path, percent-decoded, because permalinks are stored decoded.
347
+ * A path whose escapes are malformed is used as it arrived, which resolves to
348
+ * nothing and so 404s.
349
+ */
350
+ export function requestPath(c) {
351
+ const { pathname } = new URL(c.req.url);
352
+ try {
353
+ return decodeURIComponent(pathname);
354
+ }
355
+ catch {
356
+ return pathname;
357
+ }
358
+ }
359
+ /** A decoded path back into one safe to put in a `Location` header. */
360
+ function encodePath(pathname) {
361
+ return pathname
362
+ .split('/')
363
+ .map((segment) => encodeURIComponent(segment))
364
+ .join('/');
365
+ }
366
+ //# sourceMappingURL=routes.js.map