@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,221 @@
1
+ // `softure-blog skill install`: the writing skill of `skill/`, rendered with the app's blog config.
2
+ // The templates hold `{{name}}` values and `{{#flag}}…{{/flag}}` / `{{^flag}}…{{/flag}}` sections;
3
+ // `references/rules.md` holds one table row per built-in rule, which gets the effective severity
4
+ // and the catalog's description, or is dropped when the app's gate does not enforce the rule.
5
+ import { readdirSync, readFileSync } from "node:fs";
6
+ import { join, relative } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { listQualityRules } from "../quality/catalog.js";
9
+ import { getBlogOptions, getBlogReservedSlugs, getQualitySettings } from "../server/options.js";
10
+ /** The folder the skill installs into, relative to the app's root. */
11
+ export const DEFAULT_SKILL_DIR = ".claude/skills/blog-write";
12
+ /** How the skill tells the agent to run the command. */
13
+ export const DEFAULT_SKILL_COMMAND = "npx softure-blog";
14
+ /** The line every generated `SKILL.md` holds; install overwrites only a folder whose `SKILL.md` has it. */
15
+ export const SKILL_MARKER = "<!-- Generated by `softure-blog skill install`";
16
+ /** `modules/blog/skill/`, from `src/cli/` and from `dist/cli/` alike. */
17
+ const TEMPLATE_DIR = fileURLToPath(new URL("../../skill/", import.meta.url));
18
+ const RULES_TEMPLATE = "references/rules.md";
19
+ /** The file that holds the app's own sections (`blog({ skill: { sections } })`), written only when there are some. */
20
+ export const APP_SECTIONS_FILE = "references/app.md";
21
+ const LANGUAGE_NAMES = { en: "English", pl: "Polish" };
22
+ /** The skill's files for the app's config. Throws when the gate is off: the skill is built on it. */
23
+ export function renderBlogSkill(config, options = {}) {
24
+ const settings = getQualitySettings(config);
25
+ if (settings === null)
26
+ throw new Error("the quality gate is off (blog({ quality: false })); the writing skill is built on it");
27
+ const values = getSkillValues(config, settings, options);
28
+ const rules = listQualityRules(settings);
29
+ const files = listTemplateFiles().map((path) => {
30
+ const rendered = renderSkillTemplate(readFileSync(join(TEMPLATE_DIR, path), "utf8"), values);
31
+ return { path, text: path === RULES_TEMPLATE ? fillRulesTables(rendered, rules) : rendered };
32
+ });
33
+ const { sections } = getBlogOptions(config).skill;
34
+ if (sections.length === 0)
35
+ return files;
36
+ return [...files, { path: APP_SECTIONS_FILE, text: renderAppSections(sections) }].sort((a, b) => (a.path < b.path ? -1 : 1));
37
+ }
38
+ /** `references/app.md`: each section as `## <title>` with its body verbatim, never run through the template engine. */
39
+ export function renderAppSections(sections) {
40
+ return [
41
+ "# The app's own sections",
42
+ "",
43
+ "Written by the app in its blog config (`blog({ skill: { sections } })`). They add to this skill where they name",
44
+ "the app's own data, blocks and fields; they never switch off a rule of the gate.",
45
+ "",
46
+ ...sections.flatMap((section) => [`## ${section.title}`, "", section.body, ""]),
47
+ ].join("\n");
48
+ }
49
+ /** The values the templates read. */
50
+ export function getSkillValues(config, settings, options = {}) {
51
+ const blogOptions = getBlogOptions(config);
52
+ const { limits, ymyl, voice, paths, plugins } = settings.options;
53
+ const fieldKeys = Object.keys(blogOptions.fields?.shape ?? {});
54
+ const reservedSlugs = getBlogReservedSlugs(config);
55
+ return {
56
+ command: options.command ?? DEFAULT_SKILL_COMMAND,
57
+ language: settings.ruleset.language,
58
+ languageName: LANGUAGE_NAMES[settings.ruleset.language] ?? settings.ruleset.language,
59
+ contentDir: trimTrailingSlashes(blogOptions.contentDir),
60
+ articlesPath: paths.articles,
61
+ termsPath: paths.terms,
62
+ articleWordsMin: String(limits.words.article.min),
63
+ articleWordsMax: String(limits.words.article.max),
64
+ termWordsMin: String(limits.words.term.min),
65
+ termWordsMax: String(limits.words.term.max),
66
+ leadWordsArticle: String(limits.leadWords.article),
67
+ leadWordsTerm: String(limits.leadWords.term),
68
+ answerWords: String(limits.answerWords),
69
+ internalLinksArticle: String(limits.internalLinks.article),
70
+ internalLinksTerm: String(limits.internalLinks.term),
71
+ staleAfterDays: String(limits.staleAfterDays),
72
+ titleChars: String(limits.titleChars),
73
+ descriptionCharsMin: String(limits.descriptionChars.min),
74
+ descriptionCharsMax: String(limits.descriptionChars.max),
75
+ wordsPerDash: String(limits.wordsPerDash),
76
+ wordsPerBold: String(limits.wordsPerBold),
77
+ sentenceWords: String(limits.sentenceWords),
78
+ ymyl: ymyl !== null,
79
+ ownCalculationMark: ymyl?.ownCalculationMark ?? false,
80
+ firstPersonSingular: voice.forbidFirstPersonSingular,
81
+ plugins: plugins.length > 0,
82
+ fields: fieldKeys.length > 0,
83
+ fieldKeys: fieldKeys.map((key) => `\`${key}\``).join(", "),
84
+ reservedSlugs: reservedSlugs.length > 0,
85
+ reservedSlugList: reservedSlugs.map((slug) => `\`${slug}\``).join(", "),
86
+ appRules: "",
87
+ appSections: blogOptions.skill.sections.length > 0,
88
+ appSectionTitles: blogOptions.skill.sections.map((section) => section.title).join(", "),
89
+ };
90
+ }
91
+ /** Without trailing slashes; a loop, not a regular expression, so a long run of slashes stays linear. */
92
+ function trimTrailingSlashes(path) {
93
+ let end = path.length;
94
+ while (end > 1 && path[end - 1] === "/")
95
+ end -= 1;
96
+ return path.slice(0, end);
97
+ }
98
+ const STANDALONE_SECTION_TAG = /^[ \t]*(\{\{[#^/][A-Za-z]+\}\})[ \t]*\r?\n/gm;
99
+ const TAG = /\{\{([#^/]?)([A-Za-z]+)\}\}/g;
100
+ /**
101
+ * Fills `{{name}}` with a value and keeps `{{#flag}}…{{/flag}}` when the value is truthy (a non-empty
102
+ * string or `true`), `{{^flag}}…{{/flag}}` when it is not. A tag alone on its line takes the line with
103
+ * it. Throws on a name without a value and on a section that does not close, naming it.
104
+ */
105
+ export function renderSkillTemplate(template, values) {
106
+ const source = template.replace(STANDALONE_SECTION_TAG, "$1");
107
+ const getValue = (name) => {
108
+ const value = values[name];
109
+ if (value === undefined)
110
+ throw new Error(`skill template: no value for {{${name}}}`);
111
+ return value;
112
+ };
113
+ const stack = [];
114
+ let output = "";
115
+ let last = 0;
116
+ const isKept = () => stack.every((section) => section.keep);
117
+ for (const match of source.matchAll(TAG)) {
118
+ const [tag, kind = "", name = ""] = match;
119
+ if (isKept())
120
+ output += source.slice(last, match.index);
121
+ last = match.index + tag.length;
122
+ if (kind === "#" || kind === "^") {
123
+ const truthy = isTruthy(getValue(name));
124
+ stack.push({ name, inverted: kind === "^", keep: kind === "^" ? !truthy : truthy });
125
+ }
126
+ else if (kind === "/") {
127
+ const open = stack.pop();
128
+ if (open?.name !== name)
129
+ throw new Error(`skill template: {{/${name}}} closes ${open === undefined ? "nothing" : `{{${open.inverted ? "^" : "#"}${open.name}}}`}`);
130
+ }
131
+ else if (isKept()) {
132
+ const value = getValue(name);
133
+ if (typeof value !== "string")
134
+ throw new Error(`skill template: {{${name}}} is a flag; use it as a section`);
135
+ output += value;
136
+ }
137
+ }
138
+ const unclosed = stack.at(-1);
139
+ if (unclosed !== undefined)
140
+ throw new Error(`skill template: {{${unclosed.inverted ? "^" : "#"}${unclosed.name}}} is not closed`);
141
+ return output + source.slice(last);
142
+ }
143
+ function isTruthy(value) {
144
+ return typeof value === "string" ? value.length > 0 : value;
145
+ }
146
+ /** A row of a rules table in the template: `` | `rule-id` | | | guidance | ``. */
147
+ const TEMPLATE_RULE_ROW = /^\| `([a-z0-9-]+)` \| \| \| (.*) \|$/;
148
+ const RULES_TABLE_HEADER = "| Rule | Severity | What the gate looks for | What to write instead |";
149
+ const APP_RULES_SLOT = "## The app's own rules\n\n";
150
+ /** The rule ids the template's tables name. */
151
+ export function findTemplateRuleIds(rulesTemplate) {
152
+ return rulesTemplate.split("\n").flatMap((line) => {
153
+ const match = TEMPLATE_RULE_ROW.exec(line);
154
+ return match?.[1] === undefined ? [] : [match[1]];
155
+ });
156
+ }
157
+ /**
158
+ * Fills the rows of rules the gate enforces with their severity and description, drops the rows of
159
+ * rules it does not, drops a table (and its heading) left without rows, and lists every catalog rule
160
+ * the template has no row for (the app's voice phrases and plugin rules) under the app's own rules.
161
+ */
162
+ export function fillRulesTables(text, rules) {
163
+ const byId = new Map(rules.map((rule) => [rule.id, rule]));
164
+ const filled = text.split("\n").flatMap((line) => {
165
+ const match = TEMPLATE_RULE_ROW.exec(line);
166
+ if (match === null)
167
+ return [line];
168
+ const [, id = "", guidance = ""] = match;
169
+ const rule = byId.get(id);
170
+ return rule === undefined ? [] : [`| \`${id}\` | ${formatSeverity(rule)} | ${escapeCell(rule.description)} | ${guidance} |`];
171
+ });
172
+ const named = new Set(findTemplateRuleIds(text));
173
+ const appRules = rules.filter((rule) => !named.has(rule.id));
174
+ const appTable = appRules.length === 0
175
+ ? "None: the gate enforces only the rules above."
176
+ : [
177
+ "Rules of the app's voice and its plugins; the description is the app's own.",
178
+ "",
179
+ "| Rule | Severity | What the gate looks for |",
180
+ "| --- | --- | --- |",
181
+ ...appRules.map((rule) => `| \`${rule.id}\` | ${formatSeverity(rule)} | ${escapeCell(rule.description)} |`),
182
+ ].join("\n");
183
+ return dropEmptyTables(filled.join("\n")).replace(APP_RULES_SLOT, `${APP_RULES_SLOT}${appTable}`);
184
+ }
185
+ function formatSeverity(rule) {
186
+ return rule.severity === "error" ? "**error**" : "warning";
187
+ }
188
+ function escapeCell(text) {
189
+ return text.replaceAll("|", "\\|").replaceAll("\n", " ");
190
+ }
191
+ /** Removes a `## ` section whose rules table has no rows left. */
192
+ function dropEmptyTables(text) {
193
+ return text
194
+ .split(/\n(?=## )/)
195
+ .filter((section) => {
196
+ if (!section.includes(RULES_TABLE_HEADER))
197
+ return true;
198
+ return section.split("\n").some((line) => line.startsWith("| `"));
199
+ })
200
+ .join("\n");
201
+ }
202
+ /** The template files, relative to `skill/`. */
203
+ export function listTemplateFiles() {
204
+ const files = [];
205
+ const walk = (dir) => {
206
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
207
+ const path = join(dir, entry.name);
208
+ if (entry.isDirectory())
209
+ walk(path);
210
+ else if (entry.name.endsWith(".md"))
211
+ files.push(relative(TEMPLATE_DIR, path).split("\\").join("/"));
212
+ }
213
+ };
214
+ walk(TEMPLATE_DIR);
215
+ return files.sort();
216
+ }
217
+ /** The text of one template file, as shipped. */
218
+ export function readSkillTemplate(path) {
219
+ return readFileSync(join(TEMPLATE_DIR, path), "utf8");
220
+ }
221
+ //# sourceMappingURL=skill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill.js","sourceRoot":"","sources":["../../src/cli/skill.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { type BlogArticleContent, type BlogArticleInput } from "../contract.js";
2
+ import { type BlogFieldsSchema } from "../options.js";
3
+ export interface ParseArticleFileOptions {
4
+ /** Slugs taken by static pages (`blog({ reservedSlugs })`). */
5
+ readonly reservedSlugs?: readonly string[];
6
+ /** The app's own frontmatter fields (`blog({ fields })`). */
7
+ readonly fields?: BlogFieldsSchema;
8
+ }
9
+ export type ArticleFileResult = {
10
+ readonly ok: true;
11
+ readonly article: BlogArticleInput;
12
+ } | {
13
+ readonly ok: false;
14
+ readonly errors: readonly string[];
15
+ };
16
+ /**
17
+ * Reads an article file. `fileName` is a path or a bare name; it must read `<slug>.md`, so the file
18
+ * and the address cannot drift apart.
19
+ */
20
+ export declare function parseArticleFile(text: string, fileName: string, options?: ParseArticleFileOptions): ArticleFileResult;
21
+ /**
22
+ * The hash of what a reader sees, without status, slug, publication date and pillar: it moves
23
+ * `updated_at`. A fixed array, so the order of keys in the file does not change it. Fields added
24
+ * later join only when present, so the hash of an existing text does not change with the format.
25
+ */
26
+ export declare function computeContentHash(content: BlogArticleContent): string;
27
+ //# sourceMappingURL=article-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"article-file.d.ts","sourceRoot":"","sources":["../../src/content/article-file.ts"],"names":[],"mappings":"AAQA,OAAO,EAA6C,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAmB,MAAM,gBAAgB,CAAC;AAC5I,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACtC,+DAA+D;IAC/D,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;CACpC;AAED,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AA6BvJ;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,uBAA4B,GAAG,iBAAiB,CAsDzH;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAetE"}
@@ -0,0 +1,179 @@
1
+ // An article file `<slug>.md` → the input of `publishArticle` (FIRE_TRACKER `src/lib/blog-article-file.ts`).
2
+ // A YAML frontmatter between `---` lines, then the Markdown body. The frontmatter is strict: an
3
+ // unknown key is an error, so a typo in `current_as_of` cannot pass as a missing date. The app's
4
+ // own keys (`blog({ fields })`) are checked by its schema.
5
+ import { createHash } from "node:crypto";
6
+ import { basename } from "node:path";
7
+ import { parse as parseYaml } from "yaml";
8
+ import { z } from "zod";
9
+ import { BLOG_ARTICLE_KINDS, BLOG_ARTICLE_STATUSES } from "../contract.js";
10
+ import { FRONTMATTER_KEYS } from "../options.js";
11
+ const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
12
+ const FENCE = "---";
13
+ const kebab = () => z.string().max(100).regex(KEBAB, "lowercase letters, digits and single hyphens, e.g. index-funds-basics");
14
+ const httpUrl = z.url({ protocol: /^https?$/, error: "must be an http(s) address" });
15
+ const frontmatterSchema = z.strictObject({
16
+ id: kebab(),
17
+ slug: kebab(),
18
+ kind: z.enum(BLOG_ARTICLE_KINDS).default("article"),
19
+ cluster: kebab().optional(),
20
+ /** The main text of its cluster: other texts of the cluster always recommend it. Needs `cluster`. */
21
+ pillar: z.boolean().default(false),
22
+ title: z.string().trim().min(1).max(200),
23
+ description: z.string().trim().min(1).max(320),
24
+ /** The "in short" box above the body: a few sentences an AI assistant can quote. */
25
+ summary: z.string().trim().min(1).max(600).optional(),
26
+ status: z.enum(BLOG_ARTICLE_STATUSES),
27
+ current_as_of: z.iso.date(),
28
+ /** A day (`YYYY-MM-DD`, read as midnight UTC) or a moment with an offset. */
29
+ published_at: z.union([z.iso.date(), z.iso.datetime({ offset: true })]).optional(),
30
+ sources: z.array(z.strictObject({ name: z.string().trim().min(1), url: httpUrl })).default([]),
31
+ faq: z.array(z.strictObject({ question: z.string().trim().min(1), answer: z.string().trim().min(1) })).default([]),
32
+ /** Only for a term: the phrases, as they stand in texts, that link to its definition. */
33
+ forms: z.array(z.string().trim().min(2).max(80)).optional(),
34
+ });
35
+ /**
36
+ * Reads an article file. `fileName` is a path or a bare name; it must read `<slug>.md`, so the file
37
+ * and the address cannot drift apart.
38
+ */
39
+ export function parseArticleFile(text, fileName, options = {}) {
40
+ const parts = splitFrontmatter(text);
41
+ if (parts === null)
42
+ return { ok: false, errors: ["no frontmatter: the file starts with a --- line, the metadata, then a --- line"] };
43
+ let raw;
44
+ try {
45
+ raw = parseYaml(parts.frontmatter);
46
+ }
47
+ catch (error) {
48
+ return { ok: false, errors: [`the frontmatter is not valid YAML: ${error instanceof Error ? error.message : String(error)}`] };
49
+ }
50
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
51
+ return { ok: false, errors: ["the frontmatter must be a YAML mapping of keys to values"] };
52
+ const { core, fields: fieldsInput } = splitKeys(raw, options.fields);
53
+ const errors = [];
54
+ const parsed = frontmatterSchema.safeParse(core);
55
+ if (!parsed.success)
56
+ errors.push(...parsed.error.issues.map((issue) => `${formatPath(issue.path)}: ${issue.message}`));
57
+ const fields = parseFields(fieldsInput, options.fields, errors);
58
+ if (!parsed.success || fields === null)
59
+ return { ok: false, errors };
60
+ const meta = parsed.data;
61
+ const bodyMarkdown = parts.body.trim();
62
+ if (bodyMarkdown === "")
63
+ errors.push("the body under the frontmatter is empty");
64
+ if (meta.pillar && meta.cluster === undefined)
65
+ errors.push("pillar: true needs a cluster; a pillar is the main text of one topic");
66
+ if (options.reservedSlugs?.includes(meta.slug) === true)
67
+ errors.push(`slug ${meta.slug} is taken by a static page of the blog; choose another`);
68
+ if (meta.kind === "term" && (meta.forms === undefined || meta.forms.length === 0))
69
+ errors.push("forms: a term needs at least one form, or no article links to it");
70
+ if (meta.kind === "article" && meta.forms !== undefined)
71
+ errors.push("forms: only for kind: term; an article is not a link target");
72
+ if (basename(fileName) !== `${meta.slug}.md`)
73
+ errors.push(`the file name ${basename(fileName)} differs from the slug plus .md (${meta.slug}.md)`);
74
+ if (errors.length > 0)
75
+ return { ok: false, errors };
76
+ const content = {
77
+ kind: meta.kind,
78
+ cluster: meta.cluster ?? null,
79
+ title: meta.title,
80
+ description: meta.description,
81
+ summary: meta.summary ?? null,
82
+ bodyMarkdown,
83
+ currentAsOf: meta.current_as_of,
84
+ sources: meta.sources,
85
+ faq: meta.faq,
86
+ termForms: meta.forms ?? [],
87
+ fields,
88
+ };
89
+ return {
90
+ ok: true,
91
+ article: {
92
+ id: meta.id,
93
+ slug: meta.slug,
94
+ status: meta.status,
95
+ publishedAt: readPublishedAt(meta.published_at),
96
+ isPillar: meta.pillar,
97
+ ...content,
98
+ contentSha256: computeContentHash(content),
99
+ },
100
+ };
101
+ }
102
+ /**
103
+ * The hash of what a reader sees, without status, slug, publication date and pillar: it moves
104
+ * `updated_at`. A fixed array, so the order of keys in the file does not change it. Fields added
105
+ * later join only when present, so the hash of an existing text does not change with the format.
106
+ */
107
+ export function computeContentHash(content) {
108
+ const canonical = JSON.stringify([
109
+ content.kind,
110
+ content.cluster,
111
+ content.title,
112
+ content.description,
113
+ content.summary,
114
+ content.bodyMarkdown,
115
+ content.currentAsOf,
116
+ content.sources.map((source) => [source.name, source.url]),
117
+ content.faq.map((entry) => [entry.question, entry.answer]),
118
+ ...(content.termForms.length > 0 ? [content.termForms] : []),
119
+ ...(Object.keys(content.fields).length > 0 ? [sortKeys(content.fields)] : []),
120
+ ]);
121
+ return createHash("sha256").update(canonical).digest("hex");
122
+ }
123
+ function splitFrontmatter(text) {
124
+ const lines = text.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").split("\n");
125
+ if (lines[0] !== FENCE)
126
+ return null;
127
+ const end = lines.indexOf(FENCE, 1);
128
+ if (end === -1)
129
+ return null;
130
+ return { frontmatter: lines.slice(1, end).join("\n"), body: lines.slice(end + 1).join("\n") };
131
+ }
132
+ /** The module's keys and unknown keys go to the strict schema (which names the unknown ones); the app's keys to its schema. */
133
+ function splitKeys(raw, fields) {
134
+ const appKeys = new Set(Object.keys(fields?.shape ?? {}).filter((key) => !FRONTMATTER_KEYS.includes(key)));
135
+ const core = {};
136
+ const app = {};
137
+ for (const [key, value] of Object.entries(raw)) {
138
+ if (appKeys.has(key))
139
+ app[key] = value;
140
+ else
141
+ core[key] = value;
142
+ }
143
+ return { core, fields: app };
144
+ }
145
+ function parseFields(input, schema, errors) {
146
+ if (schema === undefined)
147
+ return {};
148
+ const result = schema.safeParse(input);
149
+ if (!result.success) {
150
+ errors.push(...result.error.issues.map((issue) => `${formatPath(issue.path)}: ${issue.message}`));
151
+ return null;
152
+ }
153
+ const data = result.data;
154
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
155
+ errors.push("the app's fields schema must parse to an object");
156
+ return null;
157
+ }
158
+ // Undefined optional fields are not stored: an absent key and a missing one read the same.
159
+ return Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined));
160
+ }
161
+ function readPublishedAt(value) {
162
+ if (value === undefined)
163
+ return null;
164
+ return new Date(value.length === 10 ? `${value}T00:00:00Z` : value);
165
+ }
166
+ function formatPath(path) {
167
+ return path.length === 0 ? "frontmatter" : path.map(String).join(".");
168
+ }
169
+ /** A copy with object keys sorted at every level, so the hash does not depend on key order. */
170
+ function sortKeys(value) {
171
+ if (Array.isArray(value))
172
+ return value.map(sortKeys);
173
+ if (typeof value !== "object" || value === null)
174
+ return value;
175
+ return Object.fromEntries(Object.keys(value)
176
+ .sort()
177
+ .map((key) => [key, sortKeys(value[key])]));
178
+ }
179
+ //# sourceMappingURL=article-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"article-file.js","sourceRoot":"","sources":["../../src/content/article-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ export declare const BLOG_ARTICLE_KINDS: readonly ["article", "term"];
2
+ /** `article`: a text in the blog; `term`: a glossary definition that articles link to. */
3
+ export type BlogArticleKind = (typeof BLOG_ARTICLE_KINDS)[number];
4
+ export declare const BLOG_ARTICLE_STATUSES: readonly ["draft", "published", "withdrawn"];
5
+ /** `withdrawn` keeps the row: its address answers 410 Gone, not 404. */
6
+ export type BlogArticleStatus = (typeof BLOG_ARTICLE_STATUSES)[number];
7
+ export interface BlogSource {
8
+ readonly name: string;
9
+ readonly url: string;
10
+ }
11
+ export interface BlogFaqEntry {
12
+ readonly question: string;
13
+ readonly answer: string;
14
+ }
15
+ /** The app's own frontmatter fields (`blog({ fields })`), as its schema parsed them. */
16
+ export type BlogFields = Readonly<Record<string, unknown>>;
17
+ /** The content a reader sees; the content hash covers exactly these fields. */
18
+ export interface BlogArticleContent {
19
+ readonly kind: BlogArticleKind;
20
+ readonly cluster: string | null;
21
+ readonly title: string;
22
+ readonly description: string;
23
+ /** The "in short" box above the body; `null` for none. */
24
+ readonly summary: string | null;
25
+ readonly bodyMarkdown: string;
26
+ /** `YYYY-MM-DD`: the day the facts were checked. */
27
+ readonly currentAsOf: string;
28
+ readonly sources: readonly BlogSource[];
29
+ readonly faq: readonly BlogFaqEntry[];
30
+ /** Phrases that link to a term's definition; empty for an article. */
31
+ readonly termForms: readonly string[];
32
+ readonly fields: BlogFields;
33
+ }
34
+ /** An article as a file gives it: what `publishArticle` stores. */
35
+ export interface BlogArticleInput extends BlogArticleContent {
36
+ readonly id: string;
37
+ readonly slug: string;
38
+ readonly status: BlogArticleStatus;
39
+ /** From the file; `null`: the moment it is first published. */
40
+ readonly publishedAt: Date | null;
41
+ /** The main text of its cluster; outside the content hash (it changes other pages, not this one). */
42
+ readonly isPillar: boolean;
43
+ readonly contentSha256: string;
44
+ }
45
+ /** A stored article. */
46
+ export interface BlogArticle extends BlogArticleInput {
47
+ /** Set when the content of an already published text changes. */
48
+ readonly updatedAt: Date | null;
49
+ readonly createdAt: Date;
50
+ }
51
+ /** A row before or after a publish. */
52
+ export interface BlogArticleState {
53
+ readonly slug: string;
54
+ readonly status: BlogArticleStatus;
55
+ readonly publishedAt: Date | null;
56
+ readonly updatedAt: Date | null;
57
+ readonly contentSha256: string;
58
+ }
59
+ export type BlogPublishAction = "added" | "changed" | "unchanged";
60
+ /**
61
+ * `blog.slug_taken`: another article has the slug now. `blog.slug_in_history`: another article had
62
+ * it before and the old address redirects there (a 301 would have two targets).
63
+ */
64
+ export type BlogSlugErrorCode = "blog.slug_taken" | "blog.slug_in_history";
65
+ export type BlogPublishResult = {
66
+ readonly ok: true;
67
+ readonly action: BlogPublishAction;
68
+ readonly before: BlogArticleState | null;
69
+ readonly after: BlogArticleState;
70
+ /** The slug that has just entered the slug history. */
71
+ readonly previousSlug: string | null;
72
+ } | {
73
+ readonly ok: false;
74
+ readonly error: BlogSlugErrorCode;
75
+ readonly otherArticleId: string;
76
+ };
77
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ // Types and codes of the blog's content contract: what a file holds, what a row holds, what the
2
+ // store answers. No user-facing copy here: labels come from `messages`.
3
+ export const BLOG_ARTICLE_KINDS = ["article", "term"];
4
+ export const BLOG_ARTICLE_STATUSES = ["draft", "published", "withdrawn"];
5
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,wEAAwE;AAExE,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,SAAS,EAAE,MAAM,CAAU,CAAC;AAI/D,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,CAAU,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { ModuleContext } from "@softure-ai/core";
2
+ import type { Queryable } from "@softure-ai/db";
3
+ import type { BlogArticle, BlogArticleInput, BlogArticleKind, BlogPublishResult } from "../contract.js";
4
+ export type BlogContext = ModuleContext<Queryable>;
5
+ /**
6
+ * Inserts or updates the article by its `id`. Idempotent: the same file again is `unchanged` and
7
+ * writes nothing, so a release can publish every file on every deploy.
8
+ *
9
+ * Dates:
10
+ * - `published_at` is the file's date when given; otherwise the row's; otherwise the moment the
11
+ * text first becomes `published`. A draft gets none.
12
+ * - `updated_at` moves only when the content hash of a text that already has `published_at` changes.
13
+ *
14
+ * Runs in a transaction that locks the row (`FOR UPDATE`), so two publishes of one article never
15
+ * overwrite each other silently; inside an open transaction it becomes a savepoint.
16
+ */
17
+ export declare function publishArticle(ctx: BlogContext, input: BlogArticleInput): Promise<BlogPublishResult>;
18
+ /** The article under its current slug, in any status: a page tells 200 from 410 by it. */
19
+ export declare function findArticleBySlug(ctx: BlogContext, slug: string): Promise<BlogArticle | null>;
20
+ /** The published article under its current slug, or `null` (a draft, a withdrawn text, no text). */
21
+ export declare function getPublishedArticle(ctx: BlogContext, slug: string): Promise<BlogArticle | null>;
22
+ /** The current slug of the article that once had `oldSlug`: the target of a 301. */
23
+ export declare function findSlugRedirect(ctx: BlogContext, oldSlug: string): Promise<string | null>;
24
+ export interface ListArticlesFilter {
25
+ readonly kind?: BlogArticleKind;
26
+ readonly cluster?: string;
27
+ }
28
+ /** Published texts, newest first; ties by slug. Without a filter, both kinds. */
29
+ export declare function listArticles(ctx: BlogContext, filter?: ListArticlesFilter): Promise<BlogArticle[]>;
30
+ //# sourceMappingURL=articles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["../../src/db/articles.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAoB,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG1H,MAAM,MAAM,WAAW,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAgF1G;AAED,0FAA0F;AAC1F,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAGnG;AAED,oGAAoG;AACpG,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAMrG;AAED,oFAAoF;AACpF,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOhG;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,iFAAiF;AACjF,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAS5G"}