@softure-ai/blog 0.0.0-stage → 0.1.5

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 (418) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +602 -2
  3. package/dist/cli/bin.d.ts +3 -0
  4. package/dist/cli/bin.d.ts.map +1 -0
  5. package/dist/cli/bin.js +5 -0
  6. package/dist/cli/bin.js.map +1 -0
  7. package/dist/cli/command.d.ts +9 -0
  8. package/dist/cli/command.d.ts.map +1 -0
  9. package/dist/cli/command.js +40 -0
  10. package/dist/cli/command.js.map +1 -0
  11. package/dist/cli/index.d.ts +4 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +5 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/cli/run.d.ts +62 -0
  16. package/dist/cli/run.d.ts.map +1 -0
  17. package/dist/cli/run.js +552 -0
  18. package/dist/cli/run.js.map +1 -0
  19. package/dist/cli/skill.d.ts +47 -0
  20. package/dist/cli/skill.d.ts.map +1 -0
  21. package/dist/cli/skill.js +221 -0
  22. package/dist/cli/skill.js.map +1 -0
  23. package/dist/content/article-file.d.ts +27 -0
  24. package/dist/content/article-file.d.ts.map +1 -0
  25. package/dist/content/article-file.js +179 -0
  26. package/dist/content/article-file.js.map +1 -0
  27. package/dist/contract.d.ts +77 -0
  28. package/dist/contract.d.ts.map +1 -0
  29. package/dist/contract.js +5 -0
  30. package/dist/contract.js.map +1 -0
  31. package/dist/db/articles.d.ts +30 -0
  32. package/dist/db/articles.d.ts.map +1 -0
  33. package/dist/db/articles.js +138 -0
  34. package/dist/db/articles.js.map +1 -0
  35. package/dist/db/publish-run.d.ts +50 -0
  36. package/dist/db/publish-run.d.ts.map +1 -0
  37. package/dist/db/publish-run.js +168 -0
  38. package/dist/db/publish-run.js.map +1 -0
  39. package/dist/db/schema.d.ts +403 -0
  40. package/dist/db/schema.d.ts.map +1 -0
  41. package/dist/db/schema.js +33 -0
  42. package/dist/db/schema.js.map +1 -0
  43. package/dist/discovery/dates.d.ts +9 -0
  44. package/dist/discovery/dates.d.ts.map +1 -0
  45. package/dist/discovery/dates.js +19 -0
  46. package/dist/discovery/dates.js.map +1 -0
  47. package/dist/discovery/index.d.ts +8 -0
  48. package/dist/discovery/index.d.ts.map +1 -0
  49. package/dist/discovery/index.js +11 -0
  50. package/dist/discovery/index.js.map +1 -0
  51. package/dist/discovery/indexnow.d.ts +11 -0
  52. package/dist/discovery/indexnow.d.ts.map +1 -0
  53. package/dist/discovery/indexnow.js +23 -0
  54. package/dist/discovery/indexnow.js.map +1 -0
  55. package/dist/discovery/refresh.d.ts +49 -0
  56. package/dist/discovery/refresh.d.ts.map +1 -0
  57. package/dist/discovery/refresh.js +55 -0
  58. package/dist/discovery/refresh.js.map +1 -0
  59. package/dist/discovery/related.d.ts +17 -0
  60. package/dist/discovery/related.d.ts.map +1 -0
  61. package/dist/discovery/related.js +28 -0
  62. package/dist/discovery/related.js.map +1 -0
  63. package/dist/discovery/rss.d.ts +30 -0
  64. package/dist/discovery/rss.d.ts.map +1 -0
  65. package/dist/discovery/rss.js +48 -0
  66. package/dist/discovery/rss.js.map +1 -0
  67. package/dist/discovery/sitemap.d.ts +25 -0
  68. package/dist/discovery/sitemap.d.ts.map +1 -0
  69. package/dist/discovery/sitemap.js +31 -0
  70. package/dist/discovery/sitemap.js.map +1 -0
  71. package/dist/discovery/submit.d.ts +44 -0
  72. package/dist/discovery/submit.d.ts.map +1 -0
  73. package/dist/discovery/submit.js +46 -0
  74. package/dist/discovery/submit.js.map +1 -0
  75. package/dist/index.d.ts +371 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +69 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/messages/en.d.ts +83 -0
  80. package/dist/messages/en.d.ts.map +1 -0
  81. package/dist/messages/en.js +83 -0
  82. package/dist/messages/en.js.map +1 -0
  83. package/dist/messages/index.d.ts +158 -0
  84. package/dist/messages/index.d.ts.map +1 -0
  85. package/dist/messages/index.js +5 -0
  86. package/dist/messages/index.js.map +1 -0
  87. package/dist/messages/pl.d.ts +78 -0
  88. package/dist/messages/pl.d.ts.map +1 -0
  89. package/dist/messages/pl.js +78 -0
  90. package/dist/messages/pl.js.map +1 -0
  91. package/dist/next/context.d.ts +6 -0
  92. package/dist/next/context.d.ts.map +1 -0
  93. package/dist/next/context.js +27 -0
  94. package/dist/next/context.js.map +1 -0
  95. package/dist/next/data.d.ts +11 -0
  96. package/dist/next/data.d.ts.map +1 -0
  97. package/dist/next/data.js +40 -0
  98. package/dist/next/data.js.map +1 -0
  99. package/dist/next/discovery.d.ts +6 -0
  100. package/dist/next/discovery.d.ts.map +1 -0
  101. package/dist/next/discovery.js +49 -0
  102. package/dist/next/discovery.js.map +1 -0
  103. package/dist/next/index.d.ts +9 -0
  104. package/dist/next/index.d.ts.map +1 -0
  105. package/dist/next/index.js +10 -0
  106. package/dist/next/index.js.map +1 -0
  107. package/dist/next/og-fonts.d.ts +6 -0
  108. package/dist/next/og-fonts.d.ts.map +1 -0
  109. package/dist/next/og-fonts.js +8 -0
  110. package/dist/next/og-fonts.js.map +1 -0
  111. package/dist/next/og-image.d.ts +36 -0
  112. package/dist/next/og-image.d.ts.map +1 -0
  113. package/dist/next/og-image.js +77 -0
  114. package/dist/next/og-image.js.map +1 -0
  115. package/dist/next/pages.d.ts +45 -0
  116. package/dist/next/pages.d.ts.map +1 -0
  117. package/dist/next/pages.js +189 -0
  118. package/dist/next/pages.js.map +1 -0
  119. package/dist/next/refresh.d.ts +9 -0
  120. package/dist/next/refresh.d.ts.map +1 -0
  121. package/dist/next/refresh.js +81 -0
  122. package/dist/next/refresh.js.map +1 -0
  123. package/dist/options.d.ts +320 -0
  124. package/dist/options.d.ts.map +1 -0
  125. package/dist/options.js +184 -0
  126. package/dist/options.js.map +1 -0
  127. package/dist/pages/body.d.ts +18 -0
  128. package/dist/pages/body.d.ts.map +1 -0
  129. package/dist/pages/body.js +21 -0
  130. package/dist/pages/body.js.map +1 -0
  131. package/dist/pages/dates.d.ts +21 -0
  132. package/dist/pages/dates.d.ts.map +1 -0
  133. package/dist/pages/dates.js +24 -0
  134. package/dist/pages/dates.js.map +1 -0
  135. package/dist/pages/index.d.ts +7 -0
  136. package/dist/pages/index.d.ts.map +1 -0
  137. package/dist/pages/index.js +9 -0
  138. package/dist/pages/index.js.map +1 -0
  139. package/dist/pages/json-ld.d.ts +33 -0
  140. package/dist/pages/json-ld.d.ts.map +1 -0
  141. package/dist/pages/json-ld.js +95 -0
  142. package/dist/pages/json-ld.js.map +1 -0
  143. package/dist/pages/listing.d.ts +46 -0
  144. package/dist/pages/listing.d.ts.map +1 -0
  145. package/dist/pages/listing.js +57 -0
  146. package/dist/pages/listing.js.map +1 -0
  147. package/dist/pages/paths.d.ts +37 -0
  148. package/dist/pages/paths.d.ts.map +1 -0
  149. package/dist/pages/paths.js +61 -0
  150. package/dist/pages/paths.js.map +1 -0
  151. package/dist/pages/redirects.d.ts +57 -0
  152. package/dist/pages/redirects.d.ts.map +1 -0
  153. package/dist/pages/redirects.js +78 -0
  154. package/dist/pages/redirects.js.map +1 -0
  155. package/dist/proxy/index.d.ts +15 -0
  156. package/dist/proxy/index.d.ts.map +1 -0
  157. package/dist/proxy/index.js +60 -0
  158. package/dist/proxy/index.js.map +1 -0
  159. package/dist/quality/blocks.d.ts +21 -0
  160. package/dist/quality/blocks.d.ts.map +1 -0
  161. package/dist/quality/blocks.js +113 -0
  162. package/dist/quality/blocks.js.map +1 -0
  163. package/dist/quality/catalog.d.ts +10 -0
  164. package/dist/quality/catalog.d.ts.map +1 -0
  165. package/dist/quality/catalog.js +71 -0
  166. package/dist/quality/catalog.js.map +1 -0
  167. package/dist/quality/check-article.d.ts +34 -0
  168. package/dist/quality/check-article.d.ts.map +1 -0
  169. package/dist/quality/check-article.js +74 -0
  170. package/dist/quality/check-article.js.map +1 -0
  171. package/dist/quality/check-files.d.ts +23 -0
  172. package/dist/quality/check-files.d.ts.map +1 -0
  173. package/dist/quality/check-files.js +44 -0
  174. package/dist/quality/check-files.js.map +1 -0
  175. package/dist/quality/external-links.d.ts +19 -0
  176. package/dist/quality/external-links.d.ts.map +1 -0
  177. package/dist/quality/external-links.js +30 -0
  178. package/dist/quality/external-links.js.map +1 -0
  179. package/dist/quality/finding.d.ts +17 -0
  180. package/dist/quality/finding.d.ts.map +1 -0
  181. package/dist/quality/finding.js +15 -0
  182. package/dist/quality/finding.js.map +1 -0
  183. package/dist/quality/gate.d.ts +5 -0
  184. package/dist/quality/gate.d.ts.map +1 -0
  185. package/dist/quality/gate.js +10 -0
  186. package/dist/quality/gate.js.map +1 -0
  187. package/dist/quality/index.d.ts +15 -0
  188. package/dist/quality/index.d.ts.map +1 -0
  189. package/dist/quality/index.js +16 -0
  190. package/dist/quality/index.js.map +1 -0
  191. package/dist/quality/link-targets.d.ts +44 -0
  192. package/dist/quality/link-targets.d.ts.map +1 -0
  193. package/dist/quality/link-targets.js +91 -0
  194. package/dist/quality/link-targets.js.map +1 -0
  195. package/dist/quality/options.d.ts +113 -0
  196. package/dist/quality/options.d.ts.map +1 -0
  197. package/dist/quality/options.js +93 -0
  198. package/dist/quality/options.js.map +1 -0
  199. package/dist/quality/plugin.d.ts +34 -0
  200. package/dist/quality/plugin.d.ts.map +1 -0
  201. package/dist/quality/plugin.js +7 -0
  202. package/dist/quality/plugin.js.map +1 -0
  203. package/dist/quality/rules/blocks.d.ts +5 -0
  204. package/dist/quality/rules/blocks.d.ts.map +1 -0
  205. package/dist/quality/rules/blocks.js +35 -0
  206. package/dist/quality/rules/blocks.js.map +1 -0
  207. package/dist/quality/rules/images.d.ts +4 -0
  208. package/dist/quality/rules/images.d.ts.map +1 -0
  209. package/dist/quality/rules/images.js +33 -0
  210. package/dist/quality/rules/images.js.map +1 -0
  211. package/dist/quality/rules/input.d.ts +11 -0
  212. package/dist/quality/rules/input.d.ts.map +1 -0
  213. package/dist/quality/rules/input.js +2 -0
  214. package/dist/quality/rules/input.js.map +1 -0
  215. package/dist/quality/rules/links.d.ts +17 -0
  216. package/dist/quality/rules/links.d.ts.map +1 -0
  217. package/dist/quality/rules/links.js +46 -0
  218. package/dist/quality/rules/links.js.map +1 -0
  219. package/dist/quality/rules/structure.d.ts +13 -0
  220. package/dist/quality/rules/structure.d.ts.map +1 -0
  221. package/dist/quality/rules/structure.js +139 -0
  222. package/dist/quality/rules/structure.js.map +1 -0
  223. package/dist/quality/rules/style.d.ts +11 -0
  224. package/dist/quality/rules/style.d.ts.map +1 -0
  225. package/dist/quality/rules/style.js +129 -0
  226. package/dist/quality/rules/style.js.map +1 -0
  227. package/dist/quality/rules/ymyl.d.ts +5 -0
  228. package/dist/quality/rules/ymyl.d.ts.map +1 -0
  229. package/dist/quality/rules/ymyl.js +51 -0
  230. package/dist/quality/rules/ymyl.js.map +1 -0
  231. package/dist/quality/rulesets/en/ruleset.d.ts +3 -0
  232. package/dist/quality/rulesets/en/ruleset.d.ts.map +1 -0
  233. package/dist/quality/rulesets/en/ruleset.js +106 -0
  234. package/dist/quality/rulesets/en/ruleset.js.map +1 -0
  235. package/dist/quality/rulesets/index.d.ts +7 -0
  236. package/dist/quality/rulesets/index.d.ts.map +1 -0
  237. package/dist/quality/rulesets/index.js +7 -0
  238. package/dist/quality/rulesets/index.js.map +1 -0
  239. package/dist/quality/rulesets/pl/ruleset.d.ts +3 -0
  240. package/dist/quality/rulesets/pl/ruleset.d.ts.map +1 -0
  241. package/dist/quality/rulesets/pl/ruleset.js +114 -0
  242. package/dist/quality/rulesets/pl/ruleset.js.map +1 -0
  243. package/dist/quality/rulesets/types.d.ts +28 -0
  244. package/dist/quality/rulesets/types.d.ts.map +1 -0
  245. package/dist/quality/rulesets/types.js +2 -0
  246. package/dist/quality/rulesets/types.js.map +1 -0
  247. package/dist/quality/settings.d.ts +26 -0
  248. package/dist/quality/settings.d.ts.map +1 -0
  249. package/dist/quality/settings.js +32 -0
  250. package/dist/quality/settings.js.map +1 -0
  251. package/dist/quality/text.d.ts +43 -0
  252. package/dist/quality/text.d.ts.map +1 -0
  253. package/dist/quality/text.js +85 -0
  254. package/dist/quality/text.js.map +1 -0
  255. package/dist/render/glossary.d.ts +27 -0
  256. package/dist/render/glossary.d.ts.map +1 -0
  257. package/dist/render/glossary.js +71 -0
  258. package/dist/render/glossary.js.map +1 -0
  259. package/dist/render/images.d.ts +42 -0
  260. package/dist/render/images.d.ts.map +1 -0
  261. package/dist/render/images.js +84 -0
  262. package/dist/render/images.js.map +1 -0
  263. package/dist/render/index.d.ts +6 -0
  264. package/dist/render/index.d.ts.map +1 -0
  265. package/dist/render/index.js +8 -0
  266. package/dist/render/index.js.map +1 -0
  267. package/dist/render/reading-time.d.ts +8 -0
  268. package/dist/render/reading-time.d.ts.map +1 -0
  269. package/dist/render/reading-time.js +17 -0
  270. package/dist/render/reading-time.js.map +1 -0
  271. package/dist/render/render-article.d.ts +97 -0
  272. package/dist/render/render-article.d.ts.map +1 -0
  273. package/dist/render/render-article.js +373 -0
  274. package/dist/render/render-article.js.map +1 -0
  275. package/dist/render/slugify-heading.d.ts +2 -0
  276. package/dist/render/slugify-heading.d.ts.map +1 -0
  277. package/dist/render/slugify-heading.js +21 -0
  278. package/dist/render/slugify-heading.js.map +1 -0
  279. package/dist/server/health.d.ts +3 -0
  280. package/dist/server/health.d.ts.map +1 -0
  281. package/dist/server/health.js +11 -0
  282. package/dist/server/health.js.map +1 -0
  283. package/dist/server/index.d.ts +10 -0
  284. package/dist/server/index.d.ts.map +1 -0
  285. package/dist/server/index.js +13 -0
  286. package/dist/server/index.js.map +1 -0
  287. package/dist/server/og-fonts.d.ts +24 -0
  288. package/dist/server/og-fonts.d.ts.map +1 -0
  289. package/dist/server/og-fonts.js +85 -0
  290. package/dist/server/og-fonts.js.map +1 -0
  291. package/dist/server/options.d.ts +17 -0
  292. package/dist/server/options.d.ts.map +1 -0
  293. package/dist/server/options.js +56 -0
  294. package/dist/server/options.js.map +1 -0
  295. package/dist/sitemap.d.ts +11 -0
  296. package/dist/sitemap.d.ts.map +1 -0
  297. package/dist/sitemap.js +35 -0
  298. package/dist/sitemap.js.map +1 -0
  299. package/dist/ui/blog-article.d.ts +49 -0
  300. package/dist/ui/blog-article.d.ts.map +1 -0
  301. package/dist/ui/blog-article.js +45 -0
  302. package/dist/ui/blog-article.js.map +1 -0
  303. package/dist/ui/blog-glossary.d.ts +27 -0
  304. package/dist/ui/blog-glossary.d.ts.map +1 -0
  305. package/dist/ui/blog-glossary.js +20 -0
  306. package/dist/ui/blog-glossary.js.map +1 -0
  307. package/dist/ui/blog-layout.d.ts +31 -0
  308. package/dist/ui/blog-layout.d.ts.map +1 -0
  309. package/dist/ui/blog-layout.js +25 -0
  310. package/dist/ui/blog-layout.js.map +1 -0
  311. package/dist/ui/blog-listing.d.ts +15 -0
  312. package/dist/ui/blog-listing.d.ts.map +1 -0
  313. package/dist/ui/blog-listing.js +29 -0
  314. package/dist/ui/blog-listing.js.map +1 -0
  315. package/dist/ui/blog-method.d.ts +5 -0
  316. package/dist/ui/blog-method.d.ts.map +1 -0
  317. package/dist/ui/blog-method.js +21 -0
  318. package/dist/ui/blog-method.js.map +1 -0
  319. package/dist/ui/index.d.ts +7 -0
  320. package/dist/ui/index.d.ts.map +1 -0
  321. package/dist/ui/index.js +8 -0
  322. package/dist/ui/index.js.map +1 -0
  323. package/dist/ui/page-context.d.ts +15 -0
  324. package/dist/ui/page-context.d.ts.map +1 -0
  325. package/dist/ui/page-context.js +2 -0
  326. package/dist/ui/page-context.js.map +1 -0
  327. package/migrations/0001_create_articles.sql +67 -0
  328. package/migrations/README.md +8 -0
  329. package/module.json +28 -0
  330. package/package.json +98 -4
  331. package/skill/SKILL.md +72 -0
  332. package/skill/references/reviewer.md +39 -0
  333. package/skill/references/rules.md +122 -0
  334. package/skill/references/structure.md +69 -0
  335. package/skill/references/template.md +86 -0
  336. package/src/cli/bin.ts +5 -0
  337. package/src/cli/command.ts +49 -0
  338. package/src/cli/index.ts +20 -0
  339. package/src/cli/run.ts +589 -0
  340. package/src/cli/skill.ts +242 -0
  341. package/src/content/article-file.ts +184 -0
  342. package/src/contract.ts +88 -0
  343. package/src/db/articles.ts +160 -0
  344. package/src/db/publish-run.ts +224 -0
  345. package/src/db/schema.ts +36 -0
  346. package/src/discovery/dates.ts +27 -0
  347. package/src/discovery/index.ts +18 -0
  348. package/src/discovery/indexnow.ts +25 -0
  349. package/src/discovery/refresh.ts +79 -0
  350. package/src/discovery/related.ts +35 -0
  351. package/src/discovery/rss.ts +77 -0
  352. package/src/discovery/sitemap.ts +59 -0
  353. package/src/discovery/submit.ts +69 -0
  354. package/src/index.ts +99 -0
  355. package/src/messages/en.ts +82 -0
  356. package/src/messages/index.ts +7 -0
  357. package/src/messages/pl.ts +77 -0
  358. package/src/next/context.ts +30 -0
  359. package/src/next/data.ts +55 -0
  360. package/src/next/discovery.ts +49 -0
  361. package/src/next/index.ts +25 -0
  362. package/src/next/next-modules.d.ts +15 -0
  363. package/src/next/og-fonts.ts +14 -0
  364. package/src/next/og-image.tsx +108 -0
  365. package/src/next/pages.tsx +251 -0
  366. package/src/next/refresh.ts +85 -0
  367. package/src/options.ts +224 -0
  368. package/src/pages/body.ts +38 -0
  369. package/src/pages/dates.ts +39 -0
  370. package/src/pages/index.ts +37 -0
  371. package/src/pages/json-ld.ts +120 -0
  372. package/src/pages/listing.ts +95 -0
  373. package/src/pages/paths.ts +84 -0
  374. package/src/pages/redirects.ts +120 -0
  375. package/src/proxy/index.ts +66 -0
  376. package/src/quality/blocks.ts +130 -0
  377. package/src/quality/catalog.ts +86 -0
  378. package/src/quality/check-article.ts +112 -0
  379. package/src/quality/check-files.ts +63 -0
  380. package/src/quality/external-links.ts +37 -0
  381. package/src/quality/finding.ts +29 -0
  382. package/src/quality/gate.ts +15 -0
  383. package/src/quality/index.ts +28 -0
  384. package/src/quality/link-targets.ts +107 -0
  385. package/src/quality/options.ts +105 -0
  386. package/src/quality/plugin.ts +43 -0
  387. package/src/quality/rules/blocks.ts +43 -0
  388. package/src/quality/rules/images.ts +34 -0
  389. package/src/quality/rules/input.ts +12 -0
  390. package/src/quality/rules/links.ts +56 -0
  391. package/src/quality/rules/structure.ts +137 -0
  392. package/src/quality/rules/style.ts +137 -0
  393. package/src/quality/rules/ymyl.ts +57 -0
  394. package/src/quality/rulesets/en/ruleset.ts +117 -0
  395. package/src/quality/rulesets/index.ts +9 -0
  396. package/src/quality/rulesets/pl/ruleset.ts +126 -0
  397. package/src/quality/rulesets/types.ts +31 -0
  398. package/src/quality/settings.ts +60 -0
  399. package/src/quality/text.ts +113 -0
  400. package/src/render/glossary.ts +103 -0
  401. package/src/render/images.ts +110 -0
  402. package/src/render/index.ts +29 -0
  403. package/src/render/reading-time.ts +18 -0
  404. package/src/render/render-article.ts +487 -0
  405. package/src/render/slugify-heading.ts +22 -0
  406. package/src/server/health.ts +12 -0
  407. package/src/server/index.ts +28 -0
  408. package/src/server/og-fonts.ts +102 -0
  409. package/src/server/options.ts +62 -0
  410. package/src/sitemap.ts +36 -0
  411. package/src/ui/blog-article.tsx +186 -0
  412. package/src/ui/blog-glossary.tsx +104 -0
  413. package/src/ui/blog-layout.tsx +89 -0
  414. package/src/ui/blog-listing.tsx +95 -0
  415. package/src/ui/blog-method.tsx +34 -0
  416. package/src/ui/index.ts +8 -0
  417. package/src/ui/page-context.ts +17 -0
  418. package/styles.css +498 -0
@@ -0,0 +1,9 @@
1
+ // Page logic without React and without Next: paths, dates, listing order, crumbs, JSON-LD and the
2
+ // 301/410 decisions. The components (`../ui/`) and the Next adapter (`../next/`) render what it returns.
3
+ export { findArticlesLinkingTerm, renderPageBody } from "./body.js";
4
+ export { formatDay, getArticleDates, getDayInZone } from "./dates.js";
5
+ export { getArticleImageUrl, getArticleJsonLd, getGlossaryJsonLd, getTermJsonLd, serializeJsonLd } from "./json-ld.js";
6
+ export { getArticleCrumbs, getClusterAnchor, getClusterLabel, getTermCrumbs, groupByCluster, sortTerms, splitClusterLead, } from "./listing.js";
7
+ export { getArticlePath, getReservedSlugs, getTermPath, getTextPath, matchBlogPath, normalizeRoute, } from "./paths.js";
8
+ export { buildGonePage, createCachedBlogPathDecider, decideBlogPath, } from "./redirects.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pages/index.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,yGAAyG;AACzG,OAAO,EAAE,uBAAuB,EAAE,cAAc,EAA8B,MAAM,WAAW,CAAC;AAChG,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAqB,MAAM,YAAY,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,eAAe,EAAsB,MAAM,cAAc,CAAC;AAC3I,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,cAAc,EACd,SAAS,EACT,gBAAgB,GAIjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,aAAa,EACb,cAAc,GAIf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,aAAa,EACb,2BAA2B,EAC3B,cAAc,GAKf,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { Locale, SiteUrls } from "@softure-ai/core";
2
+ import type { BlogArticle } from "../contract.js";
3
+ import type { Crumb } from "./listing.js";
4
+ import { type BlogRoutes } from "./paths.js";
5
+ export interface JsonLdContext {
6
+ /** The site's origin and canonical rule (`getSiteUrls(config)`: seo's when the app lists it, else `appOrigin`). */
7
+ readonly urls: SiteUrls;
8
+ readonly routes: BlogRoutes;
9
+ readonly locale: Locale;
10
+ readonly timezone: string;
11
+ /** The brand as author and publisher; `null` leaves them out. */
12
+ readonly brand: string | null;
13
+ }
14
+ type JsonLd = Record<string, unknown>;
15
+ /** JSON for a `<script type="application/ld+json">`: no text in it can close the tag. */
16
+ export declare function serializeJsonLd(data: unknown): string;
17
+ /**
18
+ * The URL of an article's OG image (Next serves `opengraph-image` under the page's path). A file, not a
19
+ * page: on the site's origin without the canonical trailing-slash rule.
20
+ */
21
+ export declare function getArticleImageUrl(article: Pick<BlogArticle, "slug">, ctx: Pick<JsonLdContext, "urls" | "routes">): string;
22
+ /**
23
+ * An article: `BlogPosting` (an `Article` type), its `BreadcrumbList`, and `FAQPage` when it has
24
+ * questions, in one `@graph`. `dateModified` equals the visible update day, or the publication day
25
+ * when the content never changed: a mismatch reads as a faked freshness.
26
+ */
27
+ export declare function getArticleJsonLd(article: BlogArticle, crumbs: readonly Crumb[], ctx: JsonLdContext): JsonLd;
28
+ /** A term: `DefinedTerm` in the glossary's `DefinedTermSet`, and its `BreadcrumbList`. */
29
+ export declare function getTermJsonLd(term: BlogArticle, crumbs: readonly Crumb[], ctx: JsonLdContext, glossaryTitle: string): JsonLd;
30
+ /** The glossary index: one `DefinedTermSet` with every term, in the order given. */
31
+ export declare function getGlossaryJsonLd(terms: readonly BlogArticle[], ctx: JsonLdContext, glossaryTitle: string): JsonLd;
32
+ export {};
33
+ //# sourceMappingURL=json-ld.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-ld.d.ts","sourceRoot":"","sources":["../../src/pages/json-ld.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,WAAW,aAAa;IAC5B,mHAAmH;IACnH,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEtC,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAErD;AAeD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,CAAC,GAAG,MAAM,CAE1H;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CA4B3G;AAMD,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,KAAK,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAkB5H;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAWlH"}
@@ -0,0 +1,95 @@
1
+ import { getArticleDates } from "./dates.js";
2
+ import { getArticlePath, getTermPath } from "./paths.js";
3
+ /** JSON for a `<script type="application/ld+json">`: no text in it can close the tag. */
4
+ export function serializeJsonLd(data) {
5
+ return JSON.stringify(data).replace(/</g, "\\u003c");
6
+ }
7
+ /** A crumb's URL: its page's canonical URL, then its fragment (a cluster's anchor on the listing), which the canonical rule drops. */
8
+ function getCrumbUrl(path, urls) {
9
+ const hashAt = path.indexOf("#");
10
+ return hashAt === -1 ? urls.getCanonicalUrl(path) : `${urls.getCanonicalUrl(path.slice(0, hashAt))}${path.slice(hashAt)}`;
11
+ }
12
+ function getBreadcrumbs(crumbs, urls) {
13
+ return {
14
+ "@type": "BreadcrumbList",
15
+ itemListElement: crumbs.map((crumb, index) => ({ "@type": "ListItem", position: index + 1, name: crumb.name, item: getCrumbUrl(crumb.path, urls) })),
16
+ };
17
+ }
18
+ /**
19
+ * The URL of an article's OG image (Next serves `opengraph-image` under the page's path). A file, not a
20
+ * page: on the site's origin without the canonical trailing-slash rule.
21
+ */
22
+ export function getArticleImageUrl(article, ctx) {
23
+ return `${ctx.urls.origin}${getArticlePath(ctx.routes, article.slug)}/opengraph-image`;
24
+ }
25
+ /**
26
+ * An article: `BlogPosting` (an `Article` type), its `BreadcrumbList`, and `FAQPage` when it has
27
+ * questions, in one `@graph`. `dateModified` equals the visible update day, or the publication day
28
+ * when the content never changed: a mismatch reads as a faked freshness.
29
+ */
30
+ export function getArticleJsonLd(article, crumbs, ctx) {
31
+ const dates = getArticleDates(article, ctx.timezone);
32
+ const url = ctx.urls.getCanonicalUrl(getArticlePath(ctx.routes, article.slug));
33
+ const brand = ctx.brand === null ? null : { "@type": "Organization", name: ctx.brand, url: ctx.urls.getCanonicalUrl("/") };
34
+ const graph = [
35
+ {
36
+ "@type": "BlogPosting",
37
+ "@id": `${url}#article`,
38
+ mainEntityOfPage: url,
39
+ url,
40
+ headline: article.title,
41
+ description: article.description,
42
+ inLanguage: ctx.locale,
43
+ datePublished: dates.published,
44
+ dateModified: dates.updated ?? dates.published,
45
+ ...(brand === null ? {} : { author: brand, publisher: brand }),
46
+ image: getArticleImageUrl(article, ctx),
47
+ ...(article.sources.length > 0 ? { citation: article.sources.map((source) => ({ "@type": "CreativeWork", name: source.name, url: source.url })) } : {}),
48
+ },
49
+ getBreadcrumbs(crumbs, ctx.urls),
50
+ ];
51
+ if (article.faq.length > 0) {
52
+ graph.push({
53
+ "@type": "FAQPage",
54
+ mainEntity: article.faq.map((entry) => ({ "@type": "Question", name: entry.question, acceptedAnswer: { "@type": "Answer", text: entry.answer } })),
55
+ });
56
+ }
57
+ return { "@context": "https://schema.org", "@graph": graph };
58
+ }
59
+ function getTermSetReference(ctx, glossaryTitle) {
60
+ return { "@type": "DefinedTermSet", "@id": `${ctx.urls.getCanonicalUrl(ctx.routes.glossary)}#glossary`, name: glossaryTitle };
61
+ }
62
+ /** A term: `DefinedTerm` in the glossary's `DefinedTermSet`, and its `BreadcrumbList`. */
63
+ export function getTermJsonLd(term, crumbs, ctx, glossaryTitle) {
64
+ const url = ctx.urls.getCanonicalUrl(getTermPath(ctx.routes, term.slug));
65
+ return {
66
+ "@context": "https://schema.org",
67
+ "@graph": [
68
+ {
69
+ "@type": "DefinedTerm",
70
+ "@id": `${url}#term`,
71
+ url,
72
+ name: term.title,
73
+ description: term.description,
74
+ inLanguage: ctx.locale,
75
+ ...(term.termForms.length > 0 ? { alternateName: term.termForms } : {}),
76
+ inDefinedTermSet: getTermSetReference(ctx, glossaryTitle),
77
+ },
78
+ getBreadcrumbs(crumbs, ctx.urls),
79
+ ],
80
+ };
81
+ }
82
+ /** The glossary index: one `DefinedTermSet` with every term, in the order given. */
83
+ export function getGlossaryJsonLd(terms, ctx, glossaryTitle) {
84
+ return {
85
+ "@context": "https://schema.org",
86
+ ...getTermSetReference(ctx, glossaryTitle),
87
+ url: ctx.urls.getCanonicalUrl(ctx.routes.glossary),
88
+ inLanguage: ctx.locale,
89
+ hasDefinedTerm: terms.map((term) => {
90
+ const url = ctx.urls.getCanonicalUrl(getTermPath(ctx.routes, term.slug));
91
+ return { "@type": "DefinedTerm", "@id": `${url}#term`, name: term.title, description: term.description, url };
92
+ }),
93
+ };
94
+ }
95
+ //# sourceMappingURL=json-ld.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-ld.js","sourceRoot":"","sources":["../../src/pages/json-ld.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { Locale } from "@softure-ai/core";
2
+ import type { BlogArticle } from "../contract.js";
3
+ import type { LocalizedText } from "../options.js";
4
+ import { type BlogRoutes } from "./paths.js";
5
+ export interface ClusterGroup<T> {
6
+ /** `null`: the texts without a cluster. */
7
+ readonly cluster: string | null;
8
+ readonly label: string;
9
+ /** The cluster's pillar, shown before the other cards; `null` without one. */
10
+ readonly lead: T | null;
11
+ /** The other texts, in the order they came in (newest first from `listArticles`). */
12
+ readonly rest: readonly T[];
13
+ }
14
+ /** The display name of a cluster: the app's label in the locale, else the key with spaces and a capital. */
15
+ export declare function getClusterLabel(cluster: string, labels: Readonly<Record<string, LocalizedText>>, locale: Locale): string;
16
+ /** The anchor of a cluster's section on the listing: the target of the article's middle crumb. */
17
+ export declare function getClusterAnchor(cluster: string): string;
18
+ /** The pillar leads its group; the rest keep their order. Without a pillar there is no lead. */
19
+ export declare function splitClusterLead<T extends Pick<BlogArticle, "isPillar">>(articles: readonly T[]): {
20
+ lead: T | null;
21
+ rest: T[];
22
+ };
23
+ /**
24
+ * Articles in groups by cluster. A group's place comes from its newest text (a fresh topic on top);
25
+ * texts without a cluster always come last, under `otherLabel`.
26
+ */
27
+ export declare function groupByCluster<T extends Pick<BlogArticle, "cluster" | "isPillar">>(articles: readonly T[], getLabel: (cluster: string) => string, otherLabel: string): ClusterGroup<T>[];
28
+ /** Terms in the locale's alphabetical order (a letter with a diacritic right after its base letter, not after Z). */
29
+ export declare function sortTerms<T extends Pick<BlogArticle, "title">>(terms: readonly T[], locale: Locale): T[];
30
+ export interface Crumb {
31
+ readonly name: string;
32
+ /** The path on the site; the last crumb (the page itself) has one too, as JSON-LD needs it. */
33
+ readonly path: string;
34
+ }
35
+ export interface CrumbLabels {
36
+ /** The listing's name. */
37
+ readonly blog: string;
38
+ /** The glossary's name. */
39
+ readonly glossary: string;
40
+ readonly cluster: (cluster: string) => string;
41
+ }
42
+ /** Blog › cluster › title; without a cluster, Blog › title. */
43
+ export declare function getArticleCrumbs(article: Pick<BlogArticle, "slug" | "title" | "cluster">, routes: BlogRoutes, labels: CrumbLabels): Crumb[];
44
+ /** Blog › glossary › term. */
45
+ export declare function getTermCrumbs(term: Pick<BlogArticle, "slug" | "title">, routes: BlogRoutes, labels: CrumbLabels): Crumb[];
46
+ //# sourceMappingURL=listing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listing.d.ts","sourceRoot":"","sources":["../../src/pages/listing.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1E,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IACxB,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;CAC7B;AAED,4GAA4G;AAC5G,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKxH;AAED,kGAAkG;AAClG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,GAAG;IAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,EAAE,CAAA;CAAE,CAG/H;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,UAAU,CAAC,EAChF,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EACrC,UAAU,EAAE,MAAM,GACjB,YAAY,CAAC,CAAC,CAAC,EAAE,CAYnB;AAED,qHAAqH;AACrH,wBAAgB,SAAS,CAAC,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,CAExG;AAED,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,0BAA0B;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2BAA2B;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/C;AAED,+DAA+D;AAC/D,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,EAAE,CAO3I;AAED,8BAA8B;AAC9B,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,EAAE,CAMzH"}
@@ -0,0 +1,57 @@
1
+ import { getArticlePath, getTermPath } from "./paths.js";
2
+ /** The display name of a cluster: the app's label in the locale, else the key with spaces and a capital. */
3
+ export function getClusterLabel(cluster, labels, locale) {
4
+ const label = labels[cluster];
5
+ if (label !== undefined)
6
+ return label[locale] ?? label.en;
7
+ const words = cluster.replace(/-/g, " ");
8
+ return words.charAt(0).toUpperCase() + words.slice(1);
9
+ }
10
+ /** The anchor of a cluster's section on the listing: the target of the article's middle crumb. */
11
+ export function getClusterAnchor(cluster) {
12
+ return `cluster-${cluster}`;
13
+ }
14
+ /** The pillar leads its group; the rest keep their order. Without a pillar there is no lead. */
15
+ export function splitClusterLead(articles) {
16
+ const lead = articles.find((article) => article.isPillar) ?? null;
17
+ return { lead, rest: articles.filter((article) => article !== lead) };
18
+ }
19
+ /**
20
+ * Articles in groups by cluster. A group's place comes from its newest text (a fresh topic on top);
21
+ * texts without a cluster always come last, under `otherLabel`.
22
+ */
23
+ export function groupByCluster(articles, getLabel, otherLabel) {
24
+ const groups = new Map();
25
+ for (const article of articles) {
26
+ const list = groups.get(article.cluster) ?? [];
27
+ list.push(article);
28
+ groups.set(article.cluster, list);
29
+ }
30
+ const named = [...groups.entries()]
31
+ .filter((entry) => entry[0] !== null)
32
+ .map(([cluster, list]) => ({ cluster, label: getLabel(cluster), ...splitClusterLead(list) }));
33
+ const rest = groups.get(null);
34
+ return rest === undefined ? named : [...named, { cluster: null, label: otherLabel, ...splitClusterLead(rest) }];
35
+ }
36
+ /** Terms in the locale's alphabetical order (a letter with a diacritic right after its base letter, not after Z). */
37
+ export function sortTerms(terms, locale) {
38
+ return [...terms].sort((a, b) => a.title.localeCompare(b.title, locale));
39
+ }
40
+ /** Blog › cluster › title; without a cluster, Blog › title. */
41
+ export function getArticleCrumbs(article, routes, labels) {
42
+ const crumbs = [{ name: labels.blog, path: routes.index }];
43
+ if (article.cluster !== null) {
44
+ crumbs.push({ name: labels.cluster(article.cluster), path: `${routes.index}#${getClusterAnchor(article.cluster)}` });
45
+ }
46
+ crumbs.push({ name: article.title, path: getArticlePath(routes, article.slug) });
47
+ return crumbs;
48
+ }
49
+ /** Blog › glossary › term. */
50
+ export function getTermCrumbs(term, routes, labels) {
51
+ return [
52
+ { name: labels.blog, path: routes.index },
53
+ { name: labels.glossary, path: routes.glossary },
54
+ { name: term.title, path: getTermPath(routes, term.slug) },
55
+ ];
56
+ }
57
+ //# sourceMappingURL=listing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listing.js","sourceRoot":"","sources":["../../src/pages/listing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ export interface BlogRoutes {
2
+ /** The listing; an article lives at `<index>/<slug>`. */
3
+ readonly index: string;
4
+ /** The glossary index; a term lives at `<glossary>/<slug>`. */
5
+ readonly glossary: string;
6
+ /** The "how our texts are made" page (only with `blog({ methodPage: true })`). */
7
+ readonly method: string;
8
+ /** The RSS feed of the published texts. */
9
+ readonly rss: string;
10
+ }
11
+ export type BlogPathKind = "article" | "term";
12
+ export interface BlogPathMatch {
13
+ readonly kind: BlogPathKind;
14
+ readonly slug: string;
15
+ }
16
+ /** A route without its trailing slashes (the root stays `/`). A loop, not `/\/+$/`: linear on any input. */
17
+ export declare function normalizeRoute(path: string): string;
18
+ export declare function getArticlePath(routes: BlogRoutes, slug: string): string;
19
+ export declare function getTermPath(routes: BlogRoutes, slug: string): string;
20
+ /** The path of a text by its kind. */
21
+ export declare function getTextPath(routes: BlogRoutes, kind: BlogPathKind, slug: string): string;
22
+ /**
23
+ * Slugs an article cannot take: a static page one segment under the listing (the glossary index,
24
+ * the method page when the app mounts it) wins over the article route, so such an article would be
25
+ * unreachable. Merged with the app's own `reservedSlugs`, without repeats.
26
+ */
27
+ export declare function getReservedSlugs(routes: BlogRoutes, options: {
28
+ readonly methodPage: boolean;
29
+ readonly reservedSlugs: readonly string[];
30
+ }): string[];
31
+ /**
32
+ * The text a request path names: exactly `<index>/<slug>` or `<glossary>/<slug>` with a slug the
33
+ * parser could have accepted, and not a reserved slug. Anything else (the listing, a deeper path such
34
+ * as an OG image, upper case, a static page) is `null`, so it never costs a database query.
35
+ */
36
+ export declare function matchBlogPath(pathname: string, routes: BlogRoutes, reservedSlugs: readonly string[]): BlogPathMatch | null;
37
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/pages/paths.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,UAAU;IACzB,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,MAAM,CAAC;AAE9C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAMD,4GAA4G;AAC5G,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAInD;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,sCAAsC;AACtC,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAExF;AAUD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG,MAAM,EAAE,CAInJ;AAMD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAM1H"}
@@ -0,0 +1,61 @@
1
+ // Where the blog's pages live: the module's routes (`blog({ routes })`), the paths of articles and
2
+ // terms under them, the slugs the static pages take, and the reverse: which text a request path names.
3
+ /** A slug as the file parser accepts it: kebab-case, at most 100 characters. */
4
+ const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
5
+ const MAX_SLUG_LENGTH = 100;
6
+ /** A route without its trailing slashes (the root stays `/`). A loop, not `/\/+$/`: linear on any input. */
7
+ export function normalizeRoute(path) {
8
+ let end = path.length;
9
+ while (end > 1 && path[end - 1] === "/")
10
+ end -= 1;
11
+ return path.slice(0, end);
12
+ }
13
+ function joinPath(base, segment) {
14
+ return base === "/" ? `/${segment}` : `${base}/${segment}`;
15
+ }
16
+ export function getArticlePath(routes, slug) {
17
+ return joinPath(routes.index, slug);
18
+ }
19
+ export function getTermPath(routes, slug) {
20
+ return joinPath(routes.glossary, slug);
21
+ }
22
+ /** The path of a text by its kind. */
23
+ export function getTextPath(routes, kind, slug) {
24
+ return kind === "term" ? getTermPath(routes, slug) : getArticlePath(routes, slug);
25
+ }
26
+ /** The one segment `path` adds under `base`, or `null` when it is not exactly one segment deeper. */
27
+ function getChildSegment(base, path) {
28
+ const prefix = base === "/" ? "/" : `${base}/`;
29
+ if (!path.startsWith(prefix))
30
+ return null;
31
+ const rest = path.slice(prefix.length);
32
+ return rest === "" || rest.includes("/") ? null : rest;
33
+ }
34
+ /**
35
+ * Slugs an article cannot take: a static page one segment under the listing (the glossary index,
36
+ * the method page when the app mounts it) wins over the article route, so such an article would be
37
+ * unreachable. Merged with the app's own `reservedSlugs`, without repeats.
38
+ */
39
+ export function getReservedSlugs(routes, options) {
40
+ const pages = options.methodPage ? [routes.glossary, routes.method] : [routes.glossary];
41
+ const derived = pages.map((path) => getChildSegment(routes.index, path)).filter((segment) => segment !== null);
42
+ return [...new Set([...options.reservedSlugs, ...derived])];
43
+ }
44
+ function isSlug(segment) {
45
+ return segment.length <= MAX_SLUG_LENGTH && SLUG.test(segment);
46
+ }
47
+ /**
48
+ * The text a request path names: exactly `<index>/<slug>` or `<glossary>/<slug>` with a slug the
49
+ * parser could have accepted, and not a reserved slug. Anything else (the listing, a deeper path such
50
+ * as an OG image, upper case, a static page) is `null`, so it never costs a database query.
51
+ */
52
+ export function matchBlogPath(pathname, routes, reservedSlugs) {
53
+ const termSlug = getChildSegment(routes.glossary, pathname);
54
+ if (termSlug !== null && isSlug(termSlug))
55
+ return { kind: "term", slug: termSlug };
56
+ const articleSlug = getChildSegment(routes.index, pathname);
57
+ if (articleSlug !== null && isSlug(articleSlug) && !reservedSlugs.includes(articleSlug))
58
+ return { kind: "article", slug: articleSlug };
59
+ return null;
60
+ }
61
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/pages/paths.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,uGAAuG;AAoBvG,gFAAgF;AAChF,MAAM,IAAI,GAAG,4BAA4B,CAAC;AAC1C,MAAM,eAAe,GAAG,GAAG,CAAC;AAE5B,4GAA4G;AAC5G,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC;IACtB,OAAO,GAAG,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,GAAG;QAAE,GAAG,IAAI,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,OAAe;IAC7C,OAAO,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAkB,EAAE,IAAY;IAC7D,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAkB,EAAE,IAAY;IAC1D,OAAO,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AACzC,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,WAAW,CAAC,MAAkB,EAAE,IAAkB,EAAE,IAAY;IAC9E,OAAO,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACpF,CAAC;AAED,qGAAqG;AACrG,SAAS,eAAe,CAAC,IAAY,EAAE,IAAY;IACjD,MAAM,MAAM,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;IAC/C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAkB,EAAE,OAAoF;IACvI,MAAM,KAAK,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxF,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,EAAqB,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC;IAClI,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,aAAa,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,MAAM,CAAC,OAAe;IAC7B,OAAO,OAAO,CAAC,MAAM,IAAI,eAAe,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACjE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,MAAkB,EAAE,aAAgC;IAClG,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC5D,IAAI,QAAQ,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACnF,MAAM,WAAW,GAAG,eAAe,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC5D,IAAI,WAAW,KAAK,IAAI,IAAI,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IACvI,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,57 @@
1
+ import type { BlogArticleKind, BlogArticleStatus } from "../contract.js";
2
+ import { type BlogPathMatch, type BlogRoutes } from "./paths.js";
3
+ export type BlogPathDecision = {
4
+ readonly kind: "pass";
5
+ } | {
6
+ readonly kind: "redirect";
7
+ readonly path: string;
8
+ } | {
9
+ readonly kind: "gone";
10
+ };
11
+ export interface BlogPathLookup {
12
+ /** The row under its current slug, in any status; `null` when there is none. */
13
+ readonly findText: (slug: string) => Promise<{
14
+ readonly status: BlogArticleStatus;
15
+ readonly kind: BlogArticleKind;
16
+ } | null>;
17
+ /** The current slug of the text that once had `oldSlug`: the target of a 301. */
18
+ readonly findRedirect: (oldSlug: string) => Promise<string | null>;
19
+ }
20
+ /**
21
+ * - A current row wins over the history: a slug back in use is the address of a text again.
22
+ * - A withdrawn text under the path of its own kind → 410.
23
+ * - A published text under the other kind's path (a term at the article path) → 301 to its own path.
24
+ * - An old slug → 301 to the current path of its text (which may answer 410 itself).
25
+ * - Everything else (unknown, draft) passes on: the page answers 404.
26
+ */
27
+ export declare function decideBlogPath(match: BlogPathMatch, routes: BlogRoutes, lookup: BlogPathLookup): Promise<BlogPathDecision>;
28
+ export interface CachedDeciderOptions {
29
+ /** How long a decision holds; an urgent fix through the CLI shows up at the latest after this. Default 60 s. */
30
+ readonly ttlMs?: number;
31
+ /** The most decisions kept; paths a scanner makes up cannot grow the memory. Default 500. */
32
+ readonly maxEntries?: number;
33
+ readonly now?: () => number;
34
+ /** Where a failed lookup is reported; `console.error` by default. */
35
+ readonly onError?: (message: string) => void;
36
+ }
37
+ /**
38
+ * Decisions with a memory: the proxy runs on every request and the pages are cached (ISR), so
39
+ * without one every cached view would cost a query. A failed lookup is not remembered and passes the
40
+ * request on: the page answers by itself, and a database outage never turns the blog into a 500.
41
+ */
42
+ export declare function createCachedBlogPathDecider(routes: BlogRoutes, lookup: BlogPathLookup, { ttlMs, maxEntries, now, onError }?: CachedDeciderOptions): (match: BlogPathMatch) => Promise<BlogPathDecision>;
43
+ export interface GonePageCopy {
44
+ readonly title: string;
45
+ readonly heading: string;
46
+ readonly body: string;
47
+ readonly link: string;
48
+ }
49
+ /**
50
+ * The body of a 410: a short page with the way on. No React (the proxy renders no components), no
51
+ * script, no external resource; `noindex`. Every value is escaped: copy comes from the app's overrides.
52
+ */
53
+ export declare function buildGonePage(copy: GonePageCopy, options: {
54
+ readonly lang: string;
55
+ readonly indexPath: string;
56
+ }): string;
57
+ //# sourceMappingURL=redirects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirects.d.ts","sourceRoot":"","sources":["../../src/pages/redirects.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAe,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9B,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC5H,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACpE;AAID;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAUhI;AAED,MAAM,WAAW,oBAAoB;IACnC,gHAAgH;IAChH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,6FAA6F;IAC7F,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,cAAc,EACtB,EAAE,KAAc,EAAE,UAAgB,EAAE,GAAc,EAAE,OAA6C,EAAE,GAAE,oBAAyB,GAC7H,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAqBrD;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAMD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAiBxH"}
@@ -0,0 +1,78 @@
1
+ import { getTextPath } from "./paths.js";
2
+ const PASS = { kind: "pass" };
3
+ /**
4
+ * - A current row wins over the history: a slug back in use is the address of a text again.
5
+ * - A withdrawn text under the path of its own kind → 410.
6
+ * - A published text under the other kind's path (a term at the article path) → 301 to its own path.
7
+ * - An old slug → 301 to the current path of its text (which may answer 410 itself).
8
+ * - Everything else (unknown, draft) passes on: the page answers 404.
9
+ */
10
+ export async function decideBlogPath(match, routes, lookup) {
11
+ const text = await lookup.findText(match.slug);
12
+ if (text !== null) {
13
+ if (text.kind === match.kind)
14
+ return text.status === "withdrawn" ? { kind: "gone" } : PASS;
15
+ return text.status === "published" ? { kind: "redirect", path: getTextPath(routes, text.kind, match.slug) } : PASS;
16
+ }
17
+ const target = await lookup.findRedirect(match.slug);
18
+ if (target === null || target === match.slug)
19
+ return PASS;
20
+ const current = await lookup.findText(target);
21
+ return current === null ? PASS : { kind: "redirect", path: getTextPath(routes, current.kind, target) };
22
+ }
23
+ /**
24
+ * Decisions with a memory: the proxy runs on every request and the pages are cached (ISR), so
25
+ * without one every cached view would cost a query. A failed lookup is not remembered and passes the
26
+ * request on: the page answers by itself, and a database outage never turns the blog into a 500.
27
+ */
28
+ export function createCachedBlogPathDecider(routes, lookup, { ttlMs = 60_000, maxEntries = 500, now = Date.now, onError = (message) => console.error(message) } = {}) {
29
+ const cache = new Map();
30
+ return async (match) => {
31
+ const key = `${match.kind}:${match.slug}`;
32
+ const cached = cache.get(key);
33
+ if (cached !== undefined && cached.expiresAt > now())
34
+ return cached.decision;
35
+ let decision;
36
+ try {
37
+ decision = await decideBlogPath(match, routes, lookup);
38
+ }
39
+ catch (error) {
40
+ onError(`@softure-ai/blog proxy: lookup of the ${match.kind} "${match.slug}" failed: ${error instanceof Error ? error.message : String(error)}`);
41
+ return PASS;
42
+ }
43
+ cache.delete(key);
44
+ if (cache.size >= maxEntries) {
45
+ const oldest = cache.keys().next().value;
46
+ if (oldest !== undefined)
47
+ cache.delete(oldest);
48
+ }
49
+ cache.set(key, { decision, expiresAt: now() + ttlMs });
50
+ return decision;
51
+ };
52
+ }
53
+ function escapeHtml(text) {
54
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
55
+ }
56
+ /**
57
+ * The body of a 410: a short page with the way on. No React (the proxy renders no components), no
58
+ * script, no external resource; `noindex`. Every value is escaped: copy comes from the app's overrides.
59
+ */
60
+ export function buildGonePage(copy, options) {
61
+ return [
62
+ "<!doctype html>",
63
+ `<html lang="${escapeHtml(options.lang)}">`,
64
+ "<head>",
65
+ '<meta charset="utf-8">',
66
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
67
+ '<meta name="robots" content="noindex">',
68
+ `<title>${escapeHtml(copy.title)}</title>`,
69
+ "</head>",
70
+ '<body style="font-family:system-ui,sans-serif;max-width:40rem;margin:4rem auto;padding:0 1rem;line-height:1.5">',
71
+ `<h1>${escapeHtml(copy.heading)}</h1>`,
72
+ `<p>${escapeHtml(copy.body)} <a href="${escapeHtml(options.indexPath)}">${escapeHtml(copy.link)}</a></p>`,
73
+ "</body>",
74
+ "</html>",
75
+ "",
76
+ ].join("\n");
77
+ }
78
+ //# sourceMappingURL=redirects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirects.js","sourceRoot":"","sources":["../../src/pages/redirects.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { type SoftureConfig } from "@softure-ai/core";
2
+ import { type BlogContext } from "../db/articles.js";
3
+ import { type CachedDeciderOptions } from "../pages/redirects.js";
4
+ export type BlogRedirects = (request: Request) => Promise<Response | null>;
5
+ export interface BlogRedirectsOptions extends CachedDeciderOptions {
6
+ /** The store's context; the shared database handle of `config.database` by default (tests pass PGlite). */
7
+ readonly getContext?: () => Promise<BlogContext>;
8
+ }
9
+ /**
10
+ * Answers a request for a text's address: 301 to the current path (the query kept) for an old slug
11
+ * or a text under the other kind's path, 410 with a short page for a withdrawn text, `null` for
12
+ * everything else (the next piece or the page answers). Only GET and HEAD are considered.
13
+ */
14
+ export declare function createBlogRedirects(config: SoftureConfig, options?: BlogRedirectsOptions): BlogRedirects;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEnE,OAAO,EAAuC,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE1F,OAAO,EAAmE,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAGnI,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAE3E,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,2GAA2G;IAC3G,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAC;CAClD;AAUD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAyB,GAAG,aAAa,CA4B5G"}
@@ -0,0 +1,60 @@
1
+ // The proxy piece of @softure-ai/blog: 301 from an old slug, 410 for a withdrawn text, before the page
2
+ // (docs/02-module-standard.md §8, the ID-3 pattern). It uses only Web `Request` and `Response`, and it
3
+ // is async (it may query the database), so the app awaits it first in its `proxy.ts`:
4
+ //
5
+ // const blogRedirects = createBlogRedirects(softureConfig);
6
+ // export async function proxy(request: NextRequest) {
7
+ // return (await blogRedirects(request)) ?? guard(request) ?? NextResponse.next();
8
+ // }
9
+ //
10
+ // Next 16 runs `proxy.ts` on Node.js, so the database handle is the process-wide one.
11
+ import { systemClock } from "@softure-ai/core";
12
+ import { getSharedDatabase } from "@softure-ai/db";
13
+ import { findArticleBySlug, findSlugRedirect } from "../db/articles.js";
14
+ import { matchBlogPath } from "../pages/paths.js";
15
+ import { buildGonePage, createCachedBlogPathDecider } from "../pages/redirects.js";
16
+ import { getBlogMessages, getBlogReservedSlugs, getBlogRoutes } from "../server/options.js";
17
+ function createDefaultContext(config) {
18
+ return async () => {
19
+ if (config.database === null)
20
+ throw new Error("@softure-ai/blog: softure.config.ts has no database; the blog needs one");
21
+ const { db } = await getSharedDatabase(config.database.url);
22
+ return { db, clock: systemClock, config };
23
+ };
24
+ }
25
+ /**
26
+ * Answers a request for a text's address: 301 to the current path (the query kept) for an old slug
27
+ * or a text under the other kind's path, 410 with a short page for a withdrawn text, `null` for
28
+ * everything else (the next piece or the page answers). Only GET and HEAD are considered.
29
+ */
30
+ export function createBlogRedirects(config, options = {}) {
31
+ const routes = getBlogRoutes(config);
32
+ const reservedSlugs = getBlogReservedSlugs(config);
33
+ const getContext = options.getContext ?? createDefaultContext(config);
34
+ const lookup = {
35
+ findText: async (slug) => findArticleBySlug(await getContext(), slug),
36
+ findRedirect: async (oldSlug) => findSlugRedirect(await getContext(), oldSlug),
37
+ };
38
+ const decide = createCachedBlogPathDecider(routes, lookup, options);
39
+ const gonePage = buildGonePage(getBlogMessages(config).gone, { lang: config.locale, indexPath: routes.index });
40
+ return async (request) => {
41
+ if (request.method !== "GET" && request.method !== "HEAD")
42
+ return null;
43
+ const url = new URL(request.url);
44
+ const match = matchBlogPath(url.pathname, routes, reservedSlugs);
45
+ if (match === null)
46
+ return null;
47
+ const decision = await decide(match);
48
+ if (decision.kind === "redirect") {
49
+ const target = new URL(decision.path, url);
50
+ target.search = url.search;
51
+ // 301, not 308: what the roadmap asks for and what every crawler reads as a move.
52
+ return Response.redirect(target, 301);
53
+ }
54
+ if (decision.kind === "gone") {
55
+ return new Response(request.method === "HEAD" ? null : gonePage, { status: 410, headers: { "content-type": "text/html; charset=utf-8" } });
56
+ }
57
+ return null;
58
+ };
59
+ }
60
+ //# sourceMappingURL=index.js.map