@jant/core 0.3.40 → 0.3.43

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 (1191) hide show
  1. package/README.md +21 -34
  2. package/bin/commands/assets/upload.js +247 -0
  3. package/bin/commands/collections.js +268 -0
  4. package/bin/commands/import-site.js +1169 -343
  5. package/bin/commands/media.js +302 -0
  6. package/bin/commands/migrate-text-attachments.js +183 -0
  7. package/bin/commands/migrate.js +263 -2
  8. package/bin/commands/posts.js +262 -0
  9. package/bin/commands/search-reindex.js +175 -0
  10. package/bin/commands/search.js +53 -0
  11. package/bin/commands/settings.js +93 -0
  12. package/bin/commands/site/export.js +34 -33
  13. package/bin/commands/site/{localize-media.js → pull-media.js} +17 -17
  14. package/bin/lib/d1-query.js +15 -3
  15. package/bin/lib/http-api.js +223 -0
  16. package/bin/lib/hugo-markdown.js +102 -0
  17. package/bin/lib/media-upload.js +206 -0
  18. package/bin/lib/migration-runner.js +64 -0
  19. package/bin/lib/runtime-target.js +7 -2
  20. package/bin/lib/site-media-parser.js +71 -16
  21. package/bin/lib/site-pull-media.js +695 -0
  22. package/bin/lib/site-snapshot.js +0 -1
  23. package/dist/app-Ctl0T0zO.js +5 -0
  24. package/dist/{app-CAtsuLLh.js → app-GbfwoeDJ.js} +24357 -23139
  25. package/dist/client/.vite/manifest.json +3485 -0
  26. package/dist/client/_assets/chunks/02611a045a7fe83a12014e3debc9f731-BqEDtwXC.woff2 +0 -0
  27. package/dist/client/_assets/chunks/033466ef683afe931f7f520cfb42d928-BluLHEh9.woff2 +0 -0
  28. package/dist/client/_assets/chunks/04b8bde59cff68eeee74fb1914eb09c7-D5j2CLTt.woff2 +0 -0
  29. package/dist/client/_assets/chunks/057a6a98bda7fe57105ddaa99ec82015-DY_AoijT.woff2 +0 -0
  30. package/dist/client/_assets/chunks/05ac821472e235943ed1435e4bb8ecad-CUm8GiH6.woff2 +0 -0
  31. package/dist/client/_assets/chunks/05da12edb9d52210581dc6ec4541031f-BXAOx2XP.woff2 +0 -0
  32. package/dist/client/_assets/chunks/074c1f8483d5a4d8c45c8c5f3e4cbb32-CDqdMQm8.woff2 +0 -0
  33. package/dist/client/_assets/chunks/0968e4861204b51f62a2f8e9f15dd5e0-D5tLgF_8.woff2 +0 -0
  34. package/dist/client/_assets/chunks/0acd1fe2b2ea1ad1bfee7ae1fa139d27-BKK1m412.woff2 +0 -0
  35. package/dist/client/_assets/chunks/0c2ab6b295e55f356f8020d4e7747522-CkCQjQw3.woff2 +0 -0
  36. package/dist/client/_assets/chunks/0c5f9492af03a4fa42c784de94649de1-CQlJqksa.woff2 +0 -0
  37. package/dist/client/_assets/chunks/0d5dec931dc885f07fe5cd5af8bed675-C74x-U_U.woff2 +0 -0
  38. package/dist/client/_assets/chunks/0d9a936885a4c39077438effd3779cbd-Bw0fYF4S.woff2 +0 -0
  39. package/dist/client/_assets/chunks/0e645da524f7cfc0e8c3c03fb2b08428-BVeI_5Th.woff2 +0 -0
  40. package/dist/client/_assets/chunks/0f5e1a18987dbc84ca05188c129e1936-B6_lZ6fE.woff2 +0 -0
  41. package/dist/client/_assets/chunks/1076f0f6f66d28d7a2f16427faad4413-ubiXnD1P.woff2 +0 -0
  42. package/dist/client/_assets/chunks/112743a4ab5fdd1498dfdf2b11336380-B5FzQxTK.woff2 +0 -0
  43. package/dist/client/_assets/chunks/1211f03d3ff5759a702631445793f60e-DDcSiyCt.woff2 +0 -0
  44. package/dist/client/_assets/chunks/12ef1ba76bd20b004b865266a1aa70b3-BwmaD87S.woff2 +0 -0
  45. package/dist/client/_assets/chunks/135e83b403475c5dc9e49b43501a5b84-C5mE_KZS.woff2 +0 -0
  46. package/dist/client/_assets/chunks/1380210a722ac9aeef54005ad7b015c9-CjA-MP82.woff2 +0 -0
  47. package/dist/client/_assets/chunks/13b2c53b30e6a3e4de7132dbc18dbdfc-B9WOB7kv.woff2 +0 -0
  48. package/dist/client/_assets/chunks/147b24c2f08d03bbed30887d4cb3311c-IeeFCJ13.woff2 +0 -0
  49. package/dist/client/_assets/chunks/152395634a207579552f8cb54db88599-C2y2JyNZ.woff2 +0 -0
  50. package/dist/client/_assets/chunks/15d95680dc31cc6ce20e0a5464106dc4-CpD8dqRm.woff2 +0 -0
  51. package/dist/client/_assets/chunks/16663e567f81f4725a1522f37e18f71f-BNoxS9pQ.woff2 +0 -0
  52. package/dist/client/_assets/chunks/1733f27476507ca68b68a803bc533cc2-DXL-14Ug.woff2 +0 -0
  53. package/dist/client/_assets/chunks/17c7f5d0a45e92ede0e5dec3a2c77efa-CNj3jQMl.woff2 +0 -0
  54. package/dist/client/_assets/chunks/1824321da801e6257b902f3d1d09054c-D4dgdyWB.woff2 +0 -0
  55. package/dist/client/_assets/chunks/188c2db794f3dd7a45889ddbc81da9bb-C9NbUBBO.woff2 +0 -0
  56. package/dist/client/_assets/chunks/1a08931435f885e707edb85978ea3bb6-BOiP2u7G.woff2 +0 -0
  57. package/dist/client/_assets/chunks/1a3a92c7c060a71a6c35819b9ebcdbb9-DFfonQfz.woff2 +0 -0
  58. package/dist/client/_assets/chunks/1be602ad456b0d75902d352116cb35fd-Bc-XQtQh.woff2 +0 -0
  59. package/dist/client/_assets/chunks/1cf737900dd49c2e88f1b3221a82a602-CfTmnns3.woff2 +0 -0
  60. package/dist/client/_assets/chunks/1fba7ec0e412e911bf31841de5a8a4e4-18ELMbFF.woff2 +0 -0
  61. package/dist/client/_assets/chunks/233d3a685ee18276b319363474599d47-BaFdJ0-G.woff2 +0 -0
  62. package/dist/client/_assets/chunks/2612c60f9dc5459ac42763591240a1d6-CTD69cq1.woff2 +0 -0
  63. package/dist/client/_assets/chunks/284b53bbefb06924cf236d24c8ed5641-CPDiuOv6.woff2 +0 -0
  64. package/dist/client/_assets/chunks/28a97af9ab9d38983d20f39ff09840e1-mFqcT3Jw.woff2 +0 -0
  65. package/dist/client/_assets/chunks/2a56eaf19d1d38a6b57e2a388f733676-CU52qBlE.woff2 +0 -0
  66. package/dist/client/_assets/chunks/2ab8cf1f23a5ac7939a7876054e711a7-B4T3dK7A.woff2 +0 -0
  67. package/dist/client/_assets/chunks/2abfbab82b6a7c04426afc054d2464da-D5PIIHde.woff2 +0 -0
  68. package/dist/client/_assets/chunks/2bfaadaf3479c72286248e6de0be0ec9-DEpU6O74.woff2 +0 -0
  69. package/dist/client/_assets/chunks/2c8c55e4cec85ce77e95cac9d330f5cf-BCTA96EZ.woff2 +0 -0
  70. package/dist/client/_assets/chunks/2e6f4bb71ef6b38765d51acc5a79f638-Mbs6PECD.woff2 +0 -0
  71. package/dist/client/_assets/chunks/2ed9981d2e8983365bd051159b976b6d-D8gg6NWM.woff2 +0 -0
  72. package/dist/client/_assets/chunks/2f4c633e923ba30c6ba376367379fc91-CIJCFztv.woff2 +0 -0
  73. package/dist/client/_assets/chunks/31194d303a67561926a544ed0e072aee-w3-eGYAi.woff2 +0 -0
  74. package/dist/client/_assets/chunks/31a197213ae61d7043c81013f52d10d6-DXZrrkJP.woff2 +0 -0
  75. package/dist/client/_assets/chunks/349965eee0a9b6c984a319ab96a4ece9-DByAVmXw.woff2 +0 -0
  76. package/dist/client/_assets/chunks/34e6750e00c3a911ef87dc66b336594d-DNbWesOu.woff2 +0 -0
  77. package/dist/client/_assets/chunks/3623d96698132c61e861f984ce11cc23-jqV9ErzX.woff2 +0 -0
  78. package/dist/client/_assets/chunks/36cda3eae13370b934bba4429fab9078-BPDcUlw9.woff2 +0 -0
  79. package/dist/client/_assets/chunks/36f3df4730cfca4fc77fe52b52e6a126-BwK10NC0.woff2 +0 -0
  80. package/dist/client/_assets/chunks/37887deb7a9c466cf2af6ee7ff372ea6-BrAerJB1.woff2 +0 -0
  81. package/dist/client/_assets/chunks/37dbd564820cce2f7b3331e442da0df3-Cql2kzIi.woff2 +0 -0
  82. package/dist/client/_assets/chunks/37dc8505b20b37a2477a9121b16d44a7-CoXw6XWV.woff2 +0 -0
  83. package/dist/client/_assets/chunks/39211b02f24c69cbd1b54f6d41b01933-4Xu7vvnq.woff2 +0 -0
  84. package/dist/client/_assets/chunks/3aa13f1843d7c8fa16109b6395a9a29d-BQFZUc3B.woff2 +0 -0
  85. package/dist/client/_assets/chunks/3aba89c4d4281553078de4622b498cbb-8QS1zDYk.woff2 +0 -0
  86. package/dist/client/_assets/chunks/3ac0a07878cacdc98bb889f40f5970d3-C-BaM6-y.woff2 +0 -0
  87. package/dist/client/_assets/chunks/3d88558097559d775231194d43745ba7-CR2aqGC7.woff2 +0 -0
  88. package/dist/client/_assets/chunks/3dc3e8c35524cad2d1236f3393104ccf-9sqBsF7B.woff2 +0 -0
  89. package/dist/client/_assets/chunks/3e24063b19abd2b051af986a6870046d-TlweM3SW.woff2 +0 -0
  90. package/dist/client/_assets/chunks/3e2c06bdd9dcab29aeaeb0cfb7fa608a-BaqU-dIt.woff2 +0 -0
  91. package/dist/client/_assets/chunks/424123f9371ba357087363263eeafcda-C8SVfEJi.woff2 +0 -0
  92. package/dist/client/_assets/chunks/43b3bbf43055c3e65db9cdf2c5b18c1c-CbHx6-Ki.woff2 +0 -0
  93. package/dist/client/_assets/chunks/4410f876d27c9821839474b6b1059fd7-ClYz91aY.woff2 +0 -0
  94. package/dist/client/_assets/chunks/44704471b60c18b3ecec5300a88f1102-DDmmy6-m.woff2 +0 -0
  95. package/dist/client/_assets/chunks/45140742dfa8686b58db2899ce89f89a-D5IMMzDM.woff2 +0 -0
  96. package/dist/client/_assets/chunks/46604090efbb99c2db1800928af0a239-JX2fpYX_.woff2 +0 -0
  97. package/dist/client/_assets/chunks/46998df85d31e629c0142f0556f5e7c5-BIkNG9Wh.woff2 +0 -0
  98. package/dist/client/_assets/chunks/469d2754e315e77270c12fa0ab28026c-D-y-vsfA.woff2 +0 -0
  99. package/dist/client/_assets/chunks/479329a39a3eb6c0047e6b0981919855-vZKCRx42.woff2 +0 -0
  100. package/dist/client/_assets/chunks/491bbba374093fff045f55803a22bfef-DSQXiDCd.woff2 +0 -0
  101. package/dist/client/_assets/chunks/49902f1c41b281f8eb3771abdd4dccb8-DxVxNg2W.woff2 +0 -0
  102. package/dist/client/_assets/chunks/4c0760284569b87d8f2290c324953973-Bqd-TKHH.woff2 +0 -0
  103. package/dist/client/_assets/chunks/4cba92c942694bdc479bc54101bb1aa4-BrfsrgrC.woff2 +0 -0
  104. package/dist/client/_assets/chunks/4d38e56738156625329d93bd5c8cc74a-DONkMP0y.woff2 +0 -0
  105. package/dist/client/_assets/chunks/4db4f31a16965baa42cb7dad667a8f04-Dt1Cy4gr.woff2 +0 -0
  106. package/dist/client/_assets/chunks/4e7d263c30bb48604069a3b4404d4eeb-DNPCkvKD.woff2 +0 -0
  107. package/dist/client/_assets/chunks/5279c7e465d9871edf51fd02e691eee7-CKhyhvVf.woff2 +0 -0
  108. package/dist/client/_assets/chunks/527ff336279465617aafbafe1a830632-CC3lv4pm.woff2 +0 -0
  109. package/dist/client/_assets/chunks/54bfdb6f21f9d6191e5100580239e14f-PGlqYvgl.woff2 +0 -0
  110. package/dist/client/_assets/chunks/56b40518ea0608e62826bd44ee27f5f9-BD9F9Oss.woff2 +0 -0
  111. package/dist/client/_assets/chunks/56be5e32fd01668fe5ba814d94905839-CSyFA-9l.woff2 +0 -0
  112. package/dist/client/_assets/chunks/572b9e28f1e6d89b765a16d809db84a1-BIYMjzEa.woff2 +0 -0
  113. package/dist/client/_assets/chunks/57e9a86651366c1ba299e47aa7e358c2-BHuZXB4W.woff2 +0 -0
  114. package/dist/client/_assets/chunks/57f89adeae55aa7fe2add3fc1801ce03-Cy3PNrfx.woff2 +0 -0
  115. package/dist/client/_assets/chunks/5921cef36750ff6092d94b83a802823b-hF-uymTh.woff2 +0 -0
  116. package/dist/client/_assets/chunks/59e95d8b5332dcdae894a19946ab767d-ClRfsjWZ.woff2 +0 -0
  117. package/dist/client/_assets/chunks/5acc7f736217259db79a42bf44241c48-FYQUuFOM.woff2 +0 -0
  118. package/dist/client/_assets/chunks/5c05f8b75c9d7f25b3c04ca711cf43b3-D-YWPkSq.woff2 +0 -0
  119. package/dist/client/_assets/chunks/5df9ce98c75f8c50fb4fff7f01a23925-DgoCENyW.woff2 +0 -0
  120. package/dist/client/_assets/chunks/5e0124c7265f1b4cf0fc797b94efbebc-BTBNHsSB.woff2 +0 -0
  121. package/dist/client/_assets/chunks/5ec349ff62b9ee5fe0c6dae867704dc4-B4dezhrz.woff2 +0 -0
  122. package/dist/client/_assets/chunks/60020a830d809c26970cf8e48ba1eeda-B6FDTSik.woff2 +0 -0
  123. package/dist/client/_assets/chunks/601f812e8b1eafecd5ba585a6a6d7962-gIVBlUU9.woff2 +0 -0
  124. package/dist/client/_assets/chunks/6025ddaf99d86cc8bfd781006b19fbd6-CS-ze337.woff2 +0 -0
  125. package/dist/client/_assets/chunks/60bcfa3ea7446eb42394aab02d28be40-C-PQfCu3.woff2 +0 -0
  126. package/dist/client/_assets/chunks/60f6e28b3f0400b0e8fe32c3c7116102-Dag9L2qU.woff2 +0 -0
  127. package/dist/client/_assets/chunks/615a7e0d63f257109d599b2a1977de68-D6Y93yoG.woff2 +0 -0
  128. package/dist/client/_assets/chunks/615b38e0b5bb3ea68426f44f47706e6f-B5yzIIer.woff2 +0 -0
  129. package/dist/client/_assets/chunks/61a2c80d0c924d5a6187c02e8d1d1642-BY3MmHbZ.woff2 +0 -0
  130. package/dist/client/_assets/chunks/62fc94f8d4c5a750b7f25e7387539910-3ql2_WDK.woff2 +0 -0
  131. package/dist/client/_assets/chunks/639c7c6b0b0c27c738702741cfa4b8c0-BCD3OGri.woff2 +0 -0
  132. package/dist/client/_assets/chunks/63fafcb069520613d0ea35ad3e6b1e42-DP0Rrj4o.woff2 +0 -0
  133. package/dist/client/_assets/chunks/64a404d675f1d726f0891b7a80794595-DU3L0I9s.woff2 +0 -0
  134. package/dist/client/_assets/chunks/65a60d87c64228d258a123cbe85f5f31-B4kbf-79.woff2 +0 -0
  135. package/dist/client/_assets/chunks/65e9c4585d71bf48a5c62f367010da16-CvzgzReN.woff2 +0 -0
  136. package/dist/client/_assets/chunks/68a3a4bf337f8a0722be76676e20b850-Dnaw8EPD.woff2 +0 -0
  137. package/dist/client/_assets/chunks/6968e889e18891d912809fe484c2e745-Bj1m5e0s.woff2 +0 -0
  138. package/dist/client/_assets/chunks/6abdd163d2c4b85698db8aa1ce149361-B5FVGJuo.woff2 +0 -0
  139. package/dist/client/_assets/chunks/6b75213eb0be40ce84241eb2bb438a2e-CqHmF-NR.woff2 +0 -0
  140. package/dist/client/_assets/chunks/70714891ad3fbfc3d5f10a8669dacc5a-BWk5nLFN.woff2 +0 -0
  141. package/dist/client/_assets/chunks/7158022889c6c177062ac85036e7af10-B3D4K22n.woff2 +0 -0
  142. package/dist/client/_assets/chunks/73b794d885b88f7befb7ea8ab1780a15-D0ZehnEI.woff2 +0 -0
  143. package/dist/client/_assets/chunks/743f290baf027d4626a86e22f3d44600-DDTDQdRC.woff2 +0 -0
  144. package/dist/client/_assets/chunks/7520ca6ca8c60eb1e62d50e71c8d30f1-CVRaGu2G.woff2 +0 -0
  145. package/dist/client/_assets/chunks/757019c01c5e2c2b0f54293cea3b5636-BTovUnoH.woff2 +0 -0
  146. package/dist/client/_assets/chunks/759a647b77791b1e98f99bc0ab5317a7-CrK6FIFz.woff2 +0 -0
  147. package/dist/client/_assets/chunks/761d84afca8d7f34eebefe538adba827-DRbiVuwX.woff2 +0 -0
  148. package/dist/client/_assets/chunks/76e07530323418ec723c5d7634c9bcca-DGtVJTTH.woff2 +0 -0
  149. package/dist/client/_assets/chunks/76e51630143b95b6322ef93ad330da7a-D-kV82C_.woff2 +0 -0
  150. package/dist/client/_assets/chunks/76f117a3baacda304a71965a17266a13-DFfvsFUm.woff2 +0 -0
  151. package/dist/client/_assets/chunks/773819b7b9b8fd404a929867c0fd677e-Bu1xCMZb.woff2 +0 -0
  152. package/dist/client/_assets/chunks/77747f17625a259fb405b7bbdd84ac4b-o_vNfh-6.woff2 +0 -0
  153. package/dist/client/_assets/chunks/7817dd16805145d8538ad57590f69f5a-DguSUP3O.woff2 +0 -0
  154. package/dist/client/_assets/chunks/788498548ddfb710d6ef3c7bff79fa97-3ffoQfRd.woff2 +0 -0
  155. package/dist/client/_assets/chunks/78ea6c40923ce95367e3517dd1e5a849-DvLDRhX9.woff2 +0 -0
  156. package/dist/client/_assets/chunks/79aa7c8c842c4a27cf57c0a3a1ffca5a-sDoCeoKI.woff2 +0 -0
  157. package/dist/client/_assets/chunks/7b59f0ec7792b18458dc5a361e37884c-DWvxkKmT.woff2 +0 -0
  158. package/dist/client/_assets/chunks/7c3945788a689a69356c1a622d69d48b-1JjIpss2.woff2 +0 -0
  159. package/dist/client/_assets/chunks/7cc72fd2c9105560422b6a67c6162945-Co3AF5_G.woff2 +0 -0
  160. package/dist/client/_assets/chunks/7d0ea0690e432462f4d05a23d720ec19-Dx7DK4C3.woff2 +0 -0
  161. package/dist/client/_assets/chunks/7dffa5c1bec57e0903fd62357401ff1a-DuQHmiiN.woff2 +0 -0
  162. package/dist/client/_assets/chunks/7e38ca789a9e76ac91d9256374e154f0-BtETgGRe.woff2 +0 -0
  163. package/dist/client/_assets/chunks/7f9a6c9286b68de9c72b0024f7beeb40-JpXdo4FC.woff2 +0 -0
  164. package/dist/client/_assets/chunks/815a0b797465190f60d8b1a04656c42e-Spzo94nU.woff2 +0 -0
  165. package/dist/client/_assets/chunks/823d1bb11097331238d103c7f72138dc-CGN6m2VF.woff2 +0 -0
  166. package/dist/client/_assets/chunks/82f6ccc063960eed1cdfd1d61ada8862-CsmAqx2a.woff2 +0 -0
  167. package/dist/client/_assets/chunks/835b6505bb9eea9678925a1fa885353d-C6mSBwUc.woff2 +0 -0
  168. package/dist/client/_assets/chunks/845b4b67564d62cf5cad242f7ac08ea5-DnvzrloJ.woff2 +0 -0
  169. package/dist/client/_assets/chunks/84c8e7bc0931008ed91f44ed12dfea94-C2Hvhh-b.woff2 +0 -0
  170. package/dist/client/_assets/chunks/8726558cf297cbda831cc19514897205-gSFV1vW0.woff2 +0 -0
  171. package/dist/client/_assets/chunks/8855472f3c02f6c7ebb3216617ebe4af-C1JsWVqq.woff2 +0 -0
  172. package/dist/client/_assets/chunks/887a11290ba78b1e66c6d2f67043005e-L_A1f5Fz.woff2 +0 -0
  173. package/dist/client/_assets/chunks/888ba53510213d5d1f6a7deb60e569b6-OP8E-ELN.woff2 +0 -0
  174. package/dist/client/_assets/chunks/88db1d042074fb6e66821ffc10941930-kZ6K3e62.woff2 +0 -0
  175. package/dist/client/_assets/chunks/892aa49b2529c89bb4076d4aa51fe30f-Bw0_Km2A.woff2 +0 -0
  176. package/dist/client/_assets/chunks/8b5c9ef81159f31d2ab35f45ca4373e0-Djw95sKC.woff2 +0 -0
  177. package/dist/client/_assets/chunks/8b91c7c2ed390f1278b9befa3aa87233-B2C3iFbQ.woff2 +0 -0
  178. package/dist/client/_assets/chunks/8dc18c0cebe6aa4bf4c45dbb831048ab-bUlANLit.woff2 +0 -0
  179. package/dist/client/_assets/chunks/8e231d707f0c4f8e3cde90a6b52a79aa-CvwbWlul.woff2 +0 -0
  180. package/dist/client/_assets/chunks/8fb45117a62d92dce44a80f0b729ead5-Djr4-o5-.woff2 +0 -0
  181. package/dist/client/_assets/chunks/9064c12fd72c7aba235d8c3881755b91-BnguxT-p.woff2 +0 -0
  182. package/dist/client/_assets/chunks/90b7848e9b1623b77bdcf155e90b839c-CFvPRnoM.woff2 +0 -0
  183. package/dist/client/_assets/chunks/911b9e53e9814de2998c60bf3115f560-DmPmj635.woff2 +0 -0
  184. package/dist/client/_assets/chunks/919a879bd2d580d8491a31a449390689-BY2w-8_Y.woff2 +0 -0
  185. package/dist/client/_assets/chunks/91da6cb174bebfb96976e2f1316be2fe-DztAJb6W.woff2 +0 -0
  186. package/dist/client/_assets/chunks/92fdc376bce277874e75db666f175b44-DJUnHEIN.woff2 +0 -0
  187. package/dist/client/_assets/chunks/930a23430f0eb64480d7fe5f82834e21-2VrDPT7E.woff2 +0 -0
  188. package/dist/client/_assets/chunks/934dfdbed2bb2c4b6129199d699a34fa-CtpHsjRD.woff2 +0 -0
  189. package/dist/client/_assets/chunks/93bed0e5f2dd5a21cf73304fcfbc149d-DJiaETX7.woff2 +0 -0
  190. package/dist/client/_assets/chunks/93f3ea6918533d96d9c252378b9a4af0-D-FJaGi-.woff2 +0 -0
  191. package/dist/client/_assets/chunks/95a0951d2a2722ff773a1a45e8c474d6-DuZg-BpP.woff2 +0 -0
  192. package/dist/client/_assets/chunks/95ff41191c76ef893ac70a1043e95e44-C08QDbJO.woff2 +0 -0
  193. package/dist/client/_assets/chunks/96c75862c8cec51a7c05ff025fea86cd-DBnRwCuW.woff2 +0 -0
  194. package/dist/client/_assets/chunks/97d7a17234f2b5030ae9697ae00aded2-CkUSaB-p.woff2 +0 -0
  195. package/dist/client/_assets/chunks/982cd1765ca6bbb3e6547e47834d5148-DkIybx1r.woff2 +0 -0
  196. package/dist/client/_assets/chunks/985d4d41afa0934a4eb2de35473fb9a2-CF7rgJMw.woff2 +0 -0
  197. package/dist/client/_assets/chunks/995c4fda62d25c3b79dd5987737df6ae-CY5ri8HW.woff2 +0 -0
  198. package/dist/client/_assets/chunks/9b0c3caac458c7bc9c9133e40405ddea-DhPrYOcz.woff2 +0 -0
  199. package/dist/client/_assets/chunks/9b21e8244f5930c48ad5073f83777b6c-CuE9-r-r.woff2 +0 -0
  200. package/dist/client/_assets/chunks/9bb38fd05201de666b88b82d56a386f7-nMl6lB8X.woff2 +0 -0
  201. package/dist/client/_assets/chunks/9d0afa53dc2f92ba42024f55f11f6779-Bozpx8bL.woff2 +0 -0
  202. package/dist/client/_assets/chunks/9e13a47242926f1cd7d68c08d9ef8889-DwKQjc8M.woff2 +0 -0
  203. package/dist/client/_assets/chunks/9eff2ab2a9d2ced47ab761bf6fdb11ec-D86-Zm1H.woff2 +0 -0
  204. package/dist/client/_assets/chunks/9f1963fe8d4d6878d717d872a8f3fdb5-DxjITUFq.woff2 +0 -0
  205. package/dist/client/_assets/chunks/9f3bdab4025dc7c5ba1a5871e62e8918-BqxDEFLJ.woff2 +0 -0
  206. package/dist/client/_assets/chunks/9fce02d1a09f464c36c9fcfb14a354c5-F2tMAB_j.woff2 +0 -0
  207. package/dist/client/_assets/chunks/a00e94d4f04df6aa659db9c4954c7efe-CRBKCj_3.woff2 +0 -0
  208. package/dist/client/_assets/chunks/a0a6755c7e3f9e4cf03387027dd9f16c-BAyRqF2w.woff2 +0 -0
  209. package/dist/client/_assets/chunks/a0d00fe4816c95a8c7dffd445ef00b03-CxcTiRLJ.woff2 +0 -0
  210. package/dist/client/_assets/chunks/a0f199077fa1a33bc2a1e01c64de7fe6-IdgVJmZD.woff2 +0 -0
  211. package/dist/client/_assets/chunks/a2b0597837e382aca19919c4b74e58c8-7iVOBjzI.woff2 +0 -0
  212. package/dist/client/_assets/chunks/a373e85c96aa0dc303092429fb837e51-Dvmw8mbX.woff2 +0 -0
  213. package/dist/client/_assets/chunks/a379ada89abd750c587ded29f77e731c-CgPBsEsv.woff2 +0 -0
  214. package/dist/client/_assets/chunks/a4475c442c6b259354c9eda62e64e7df-TZ0c8vK-.woff2 +0 -0
  215. package/dist/client/_assets/chunks/a48be56ce5d3a99dc8f8331398004412-BR_ixH1W.woff2 +0 -0
  216. package/dist/client/_assets/chunks/a4a39b5f048671aa37c2b2a9581ab08a-EDfDYsUd.woff2 +0 -0
  217. package/dist/client/_assets/chunks/a534449d67561ac9b06489f64e57ad98-C355TCrD.woff2 +0 -0
  218. package/dist/client/_assets/chunks/a6e044424780a57610833cc856c15bf5-x1b8FE4H.woff2 +0 -0
  219. package/dist/client/_assets/chunks/a7c35e42a659347e490c3cb7983b2e7f-BWpA0-oz.woff2 +0 -0
  220. package/dist/client/_assets/chunks/a7c58070d68dc1724e9d4e781afb78db-ql27pT-6.woff2 +0 -0
  221. package/dist/client/_assets/chunks/a806759e72987951d6d08b7cbdf36a1b-TvLDwB_u.woff2 +0 -0
  222. package/dist/client/_assets/chunks/a88481ec8bc7be11cb66e46781a79bb9-Ck-r_sGx.woff2 +0 -0
  223. package/dist/client/_assets/chunks/a919a8c6fec6d7e8bb21177965f2e9d7-CC5uWzwd.woff2 +0 -0
  224. package/dist/client/_assets/chunks/a9a016d4a93409f65278327e8a8bb38d-dylrNwF8.woff2 +0 -0
  225. package/dist/client/_assets/chunks/aa662092cf249707123ca4d575e2764b-CSxNsTUL.woff2 +0 -0
  226. package/dist/client/_assets/chunks/aae1eda193285ab817a2d1b408440326-CjyjBoRm.woff2 +0 -0
  227. package/dist/client/_assets/chunks/ab92d41062a7644faa45f50bf384454a-Cq0X8a9H.woff2 +0 -0
  228. package/dist/client/_assets/chunks/accc43762ad05edfbff66ba1380d192a-B5jIykaZ.woff2 +0 -0
  229. package/dist/client/_assets/chunks/ad47bcd6d4663026d648c132d969318d-BYdIbDge.woff2 +0 -0
  230. package/dist/client/_assets/chunks/ad67113f88b59582991cb0c5d33ea19f-DOH1vcIZ.woff2 +0 -0
  231. package/dist/client/_assets/chunks/adb6e613fb99c6dd660b727101f554a8-DJDw_ZZK.woff2 +0 -0
  232. package/dist/client/_assets/chunks/adc4a98a89870ef984ee8f4b57c8a3a5-D30vw26R.woff2 +0 -0
  233. package/dist/client/_assets/chunks/af57a2e8c72f9ba0efb1af771d32c124-D5Zwq9pS.woff2 +0 -0
  234. package/dist/client/_assets/chunks/b0ae5f374e43dac992a4a5d334cebc0b-Ccoi4dPt.woff2 +0 -0
  235. package/dist/client/_assets/chunks/b0cb664cb2e1371efda8943c0b7dcd1c-YzGFnelF.woff2 +0 -0
  236. package/dist/client/_assets/chunks/b0d1cdced482352cf0d3ae58638aacb9-IunWfc6R.woff2 +0 -0
  237. package/dist/client/_assets/chunks/b12379ab782468d725519cd07a7d15bc-IjbydKs2.woff2 +0 -0
  238. package/dist/client/_assets/chunks/b320f5d185b2cff933ac549c184031c5-IYnPrIz4.woff2 +0 -0
  239. package/dist/client/_assets/chunks/b326a8a9c33b554db570da94f60bc380-B6dzaCQg.woff2 +0 -0
  240. package/dist/client/_assets/chunks/b55486f8a459c838fe329d4e79a8c211-DW8q7oGV.woff2 +0 -0
  241. package/dist/client/_assets/chunks/b6117ff2993b11bb1fdc7ea3588a010c-JoMYL5W_.woff2 +0 -0
  242. package/dist/client/_assets/chunks/b61982951bd51b724143c30dfaaa9fe9-D3KhBKl6.woff2 +0 -0
  243. package/dist/client/_assets/chunks/b91234c10fd8b8c8abc88e03afe66a1f-BABNh2la.woff2 +0 -0
  244. package/dist/client/_assets/chunks/b9317198f118a1dfd8ddf2b82ec028f3-CiMecxeH.woff2 +0 -0
  245. package/dist/client/_assets/chunks/ba825ae79b1a6f7e0cce5215fcb5c96f-B6EWU_D5.woff2 +0 -0
  246. package/dist/client/_assets/chunks/ba9c59d7dfa4494db1bb764ada81467d-D0yPcC1m.woff2 +0 -0
  247. package/dist/client/_assets/chunks/bab547459d514f46206e340c4bb2dc88-YnBqlGhw.woff2 +0 -0
  248. package/dist/client/_assets/chunks/bb0b15492b8cdbbec57c0bcfa0aa9241-BjIFbEsN.woff2 +0 -0
  249. package/dist/client/_assets/chunks/bc6183ac08f0fac78c46f80c10cf7c92-B81nbRb3.woff2 +0 -0
  250. package/dist/client/_assets/chunks/bc8ccea5abf6598cf3cfa97eb59804bb-B_7IbluG.woff2 +0 -0
  251. package/dist/client/_assets/chunks/bc95a792cbca6639214c9b0da13392ff-CHQe0E-D.woff2 +0 -0
  252. package/dist/client/_assets/chunks/bcc39eda837bb7a7a3d37c8c60fffb81-CGd1Zk6S.woff2 +0 -0
  253. package/dist/client/_assets/chunks/bcfa62f35731856246c146d3a6932bf3-DMDq6w4v.woff2 +0 -0
  254. package/dist/client/_assets/chunks/bd73264d7f98776708d5d6f3c9b78fcc-d9QWWMym.woff2 +0 -0
  255. package/dist/client/_assets/chunks/bed610b217d500f5975cfc9fe6157570-B5t7s1bw.woff2 +0 -0
  256. package/dist/client/_assets/chunks/bedc74b423b7293b6ad0bdecc61c42cc-4Nnh7peP.woff2 +0 -0
  257. package/dist/client/_assets/chunks/bf8901f8f11d4f433ea17dabc9370ea6-B8dkfvGn.woff2 +0 -0
  258. package/dist/client/_assets/chunks/bfb00d4a4c48661bd0be99f300a0faae-OxsWZ7l-.woff2 +0 -0
  259. package/dist/client/_assets/chunks/c0098958e20db68cab90097b5e62516f-VpjQo3vV.woff2 +0 -0
  260. package/dist/client/_assets/chunks/c063897793f593eb26d6ff7b7baaba18-BWeJJ1sH.woff2 +0 -0
  261. package/dist/client/_assets/chunks/c1b0df29ae41d764904df84e9ac83d1e-D1N934vs.woff2 +0 -0
  262. package/dist/client/_assets/chunks/c4143bb9f2fe77b6ccf20088a8904650-nHfYM24e.woff2 +0 -0
  263. package/dist/client/_assets/chunks/c42c67070cfe99cf823df92d81c7fa6e-B_a405Ez.woff2 +0 -0
  264. package/dist/client/_assets/chunks/c4bfaa5e50798246e3770718b7a7c84a-B-r6Y_o0.woff2 +0 -0
  265. package/dist/client/_assets/chunks/c4d749e45ecd5a5aed5a0bb3ebfd355d-CkqE1fRi.woff2 +0 -0
  266. package/dist/client/_assets/chunks/c6c2971ad1f3221f6cf84028aa0f477e-D7BZGIcY.woff2 +0 -0
  267. package/dist/client/_assets/chunks/c7ff3f6bbdcd5f604b7343602ab904df-Bq7UjVrZ.woff2 +0 -0
  268. package/dist/client/_assets/chunks/c84598999133455503042e06f4ab79cb-DIXndJX3.woff2 +0 -0
  269. package/dist/client/_assets/chunks/c945c62368357d05a53206620460fb30-82gFEf6n.woff2 +0 -0
  270. package/dist/client/_assets/chunks/c96d83978add28b356c22c4c84916733-BL4VaPIE.woff2 +0 -0
  271. package/dist/client/_assets/chunks/c97f41eef722121d86f55d553c056a39-CjejgQO2.woff2 +0 -0
  272. package/dist/client/_assets/chunks/ca62704509932d3232d62918de97af3f-CKWF-3mX.woff2 +0 -0
  273. package/dist/client/_assets/chunks/ca9a533988d7019597a60d4e17127e0c-ClZ6ygZM.woff2 +0 -0
  274. package/dist/client/_assets/chunks/cb7ccd6494256f7a2977b7c2b0225592-Cxp8XObm.woff2 +0 -0
  275. package/dist/client/_assets/chunks/ce022e18a1377ac509443c3c3790b431-DpH0t6Tn.woff2 +0 -0
  276. package/dist/client/_assets/chunks/ce41d70ce6a069a498525c9e15c45cf2-DGtgv-_b.woff2 +0 -0
  277. package/dist/client/_assets/chunks/cf2b28f90f47276f7e2688a65e88a101-LVbNkXHj.woff2 +0 -0
  278. package/dist/client/_assets/chunks/cf9cffe56636322f62b40d61130fbc5e-DNCBMqAo.woff2 +0 -0
  279. package/dist/client/_assets/chunks/d1f064825fa5784b5c930652bd831cce-BU6qwjp8.woff2 +0 -0
  280. package/dist/client/_assets/chunks/d20d8944bc0b85f5b2aae4b24f343516-oY9l-NmC.woff2 +0 -0
  281. package/dist/client/_assets/chunks/d2faabcedd19f016e7b21bce073c0ec8-Dv6eFCf5.woff2 +0 -0
  282. package/dist/client/_assets/chunks/d320171e57480510f87dbbc7d5264b0a-CcCfjaFX.woff2 +0 -0
  283. package/dist/client/_assets/chunks/d36644d6502527e1fff205d0c7eca434-kVBj8zy_.woff2 +0 -0
  284. package/dist/client/_assets/chunks/d3a72a99d365dddfbca8d017a8011368-Bgdmhl3M.woff2 +0 -0
  285. package/dist/client/_assets/chunks/d40ab99c7a38026f411c8f112f742b48-D7Nm7fg6.woff2 +0 -0
  286. package/dist/client/_assets/chunks/d42aafa0f246ad3b4c16fe96d3a3a432-CRxTqZXU.woff2 +0 -0
  287. package/dist/client/_assets/chunks/d4aaf23c13ae808a4bed617afd13aa07-d_T1SKe9.woff2 +0 -0
  288. package/dist/client/_assets/chunks/d509ee5c8241bc7c3de2039d75564fa5-BpNFD9JE.woff2 +0 -0
  289. package/dist/client/_assets/chunks/d675d717cd329bfd4c0524f76ae1579c-Bd5_ctoQ.woff2 +0 -0
  290. package/dist/client/_assets/chunks/d689b1861d7e4377dd72ad3013482612-jPsVOU3F.woff2 +0 -0
  291. package/dist/client/_assets/chunks/d6bb686bddfbe8f38a36a68e609a8667-C_QduS-X.woff2 +0 -0
  292. package/dist/client/_assets/chunks/d9020ff69a83b2a6ee1f42ae480f7db0-DLBUPMCe.woff2 +0 -0
  293. package/dist/client/_assets/chunks/d9047070d72a816b3dba9d40c2d85e69-LTQ42vEp.woff2 +0 -0
  294. package/dist/client/_assets/chunks/da04549f3f4ed28076b01b8cd710d313-CDXOeduw.woff2 +0 -0
  295. package/dist/client/_assets/chunks/da7af303f8c645f9a9dbae0e6e32dd35-DCkS2jqi.woff2 +0 -0
  296. package/dist/client/_assets/chunks/da90a9012ab2d98f759e3fa0820ef502-D0D9d1zg.woff2 +0 -0
  297. package/dist/client/_assets/chunks/dc6e234ded795e91f76d6647f628fbf0-IkooXcRh.woff2 +0 -0
  298. package/dist/client/_assets/chunks/dc8f6256445e68199540be9ade33529f-umb5tWbZ.woff2 +0 -0
  299. package/dist/client/_assets/chunks/dd0cfdfdac0866e66d587e2b5a9e9961-bl4ozE0c.woff2 +0 -0
  300. package/dist/client/_assets/chunks/de304c8a02e45ded4f8dcc479d167198-ClRu89-4.woff2 +0 -0
  301. package/dist/client/_assets/chunks/df10d94bea357a43313e20da5d84bd30-CmAgh2oA.woff2 +0 -0
  302. package/dist/client/_assets/chunks/df9568257eb29b156449fdd4bec5ec76-BMz6aLRr.woff2 +0 -0
  303. package/dist/client/_assets/chunks/e067cd0ed76c90cd0a93c9339253f20b-6RxCQ4jy.woff2 +0 -0
  304. package/dist/client/_assets/chunks/e08b07772e7bed3cec2832d43f7fd339-CQCkeKsr.woff2 +0 -0
  305. package/dist/client/_assets/chunks/e12150d5a39b30be8f567968c7a527b0-DjdoWzeQ.woff2 +0 -0
  306. package/dist/client/_assets/chunks/e34b2b141e472dc776c86fdf8eea23b0-Cjk7H-Ev.woff2 +0 -0
  307. package/dist/client/_assets/chunks/e444fd88f1390636e603d0d681538ac8-Bx7CPOZh.woff2 +0 -0
  308. package/dist/client/_assets/chunks/e53fcb2381eee345db4f6f973dd95a3e-DmkkCUWz.woff2 +0 -0
  309. package/dist/client/_assets/chunks/e5bd313ef81f687d398aacb11cec3069-CwBEbcji.woff2 +0 -0
  310. package/dist/client/_assets/chunks/e79898628283edc27180fc39d0d769c1-By2pplQz.woff2 +0 -0
  311. package/dist/client/_assets/chunks/e8c15be62faf978d208925e79ea6a10d-Ct5M9lTP.woff2 +0 -0
  312. package/dist/client/_assets/chunks/e8c364c16daa04835bf32d293d2598db-3v4Tut_d.woff2 +0 -0
  313. package/dist/client/_assets/chunks/e91c4d941ed9f5c2f3e27f205a3a225e-BwOJ2Lst.woff2 +0 -0
  314. package/dist/client/_assets/chunks/e9c566be7a5d38a9085225f7372bc82b-BCtnCVML.woff2 +0 -0
  315. package/dist/client/_assets/chunks/e9ebc567a711eeb29019ddae3e0ce7fe-CnNhV49O.woff2 +0 -0
  316. package/dist/client/_assets/chunks/eaf332445a40942928e5f5750c1c3116-qZ5LSRrj.woff2 +0 -0
  317. package/dist/client/_assets/chunks/eb5afb3d952b8593782caec6026514b6-BXugZF2T.woff2 +0 -0
  318. package/dist/client/_assets/chunks/ec86e23683052da5cfdc3b77641bd15a-BeLcbcRc.woff2 +0 -0
  319. package/dist/client/_assets/chunks/ecb8875a56c7b038b35432fda41ae128-BVqgNf1o.woff2 +0 -0
  320. package/dist/client/_assets/chunks/eda1b0cb6d1719dd9bedcf3216a9e8de-BJTfLL5z.woff2 +0 -0
  321. package/dist/client/_assets/chunks/edd6a4f608d04fc0351d7688cfc321e4-d3V4U40c.woff2 +0 -0
  322. package/dist/client/_assets/chunks/ee6fce20b420a480714607c66d7f97e5-DEtC-G5G.woff2 +0 -0
  323. package/dist/client/_assets/chunks/eee3836d6ac17ebb2c450bbcbc9db121-C5a2TZjQ.woff2 +0 -0
  324. package/dist/client/_assets/chunks/et-book-bold-line-figures-BFJr2_zv.woff +0 -0
  325. package/dist/client/_assets/chunks/et-book-display-italic-old-style-figures-CoeRJAe8.woff +0 -0
  326. package/dist/client/_assets/chunks/et-book-roman-line-figures-CaA40oOf.woff +0 -0
  327. package/dist/client/_assets/chunks/f1661731474b78bdf81114daf10b254f-KnMKX_U3.woff2 +0 -0
  328. package/dist/client/_assets/chunks/f2c58dee206ba9355046fc23d05491f7-Cm4tCWav.woff2 +0 -0
  329. package/dist/client/_assets/chunks/f50ac27ea4358d67fdda403c2bb52467-WCfYHde1.woff2 +0 -0
  330. package/dist/client/_assets/chunks/f55334112f8e8be82d65db29887a663f-vDq1zH8k.woff2 +0 -0
  331. package/dist/client/_assets/chunks/f5738255e92d8dd34a46d1bcdf4c4074-tpghTzL1.woff2 +0 -0
  332. package/dist/client/_assets/chunks/f740e93f3c0277ecc616594103bca683-C4BgW_ko.woff2 +0 -0
  333. package/dist/client/_assets/chunks/f7633b5af033d76ff2fb3c3c266d77c5-BCkrZxwO.woff2 +0 -0
  334. package/dist/client/_assets/chunks/f7d8468cba2335a83ee414ea68291bab-CVnotkZU.woff2 +0 -0
  335. package/dist/client/_assets/chunks/f91cd722855f4269256eae1187df64ec-DJlAx1A_.woff2 +0 -0
  336. package/dist/client/_assets/chunks/f9695c6c4df2bf6bc03045ff79d4f01f-DDaYGYHT.woff2 +0 -0
  337. package/dist/client/_assets/chunks/fa6e58ce4b52695e7ae19bbea6336ec8-B30jEpze.woff2 +0 -0
  338. package/dist/client/_assets/chunks/fada6eaa68ff8816afe43d2a36c5423e-DlGAWNPb.woff2 +0 -0
  339. package/dist/client/_assets/chunks/fbcc4bf5367218951172bdee6f77d7a6-Dax2VSCd.woff2 +0 -0
  340. package/dist/client/_assets/chunks/fc895f5ce66b656f4a933097bf2a8775-DjjysNdf.woff2 +0 -0
  341. package/dist/client/_assets/chunks/fc9ae5b600fb711f2d67e93ce768cba4-D1lx7LBz.woff2 +0 -0
  342. package/dist/client/_assets/chunks/fca6720fd14c467d29a90f18ef3859b9-B3btrwq3.woff2 +0 -0
  343. package/dist/client/_assets/chunks/fd1bb507bcbf04856eeb7f7fd47ea579-NR1nGJTt.woff2 +0 -0
  344. package/dist/client/_assets/chunks/fd1bceb55d3e0183ac2454b8532fec7d-D3t-v6nn.woff2 +0 -0
  345. package/dist/client/_assets/chunks/fd2069e6e8588c70b4f11364093b81f2-BoXhYVgw.woff2 +0 -0
  346. package/dist/client/_assets/chunks/{url-CG0eolsk.js → url-pLre2DM_.js} +1 -1
  347. package/dist/client/_assets/client-C_kImWZj.css +2 -0
  348. package/dist/client/_assets/client-D95FNDg5.js +272 -0
  349. package/dist/client/_assets/client-auth-CXILhW1b.js +4547 -0
  350. package/dist/client/_assets/client-cjk-jp-DZwrTzQC.css +1 -0
  351. package/dist/client/_assets/client-cjk-kr-_3ZNI2ZP.css +1 -0
  352. package/dist/env-CgaH9Mut.js +252 -0
  353. package/dist/github-api-BkRWnqMx.js +176 -0
  354. package/dist/github-app-WeadXMb8.js +275 -0
  355. package/dist/github-sync-7y_nTXx1.js +4723 -0
  356. package/dist/index.js +88 -2
  357. package/dist/node.js +71 -30
  358. package/dist/url-umUptr5z.js +334 -0
  359. package/package.json +16 -12
  360. package/src/__tests__/bin/content-cli.test.ts +179 -0
  361. package/src/__tests__/bin/media-cli.test.ts +192 -0
  362. package/src/__tests__/export-hugo-build.test.ts +178 -0
  363. package/src/__tests__/export-import-roundtrip.test.ts +265 -0
  364. package/src/__tests__/export-service.test.ts +763 -510
  365. package/src/__tests__/helpers/app.ts +15 -4
  366. package/src/__tests__/helpers/export-fixtures.ts +108 -0
  367. package/src/__tests__/import-site-command.test.ts +358 -350
  368. package/src/__tests__/site-pull-media.test.ts +256 -0
  369. package/src/app.tsx +68 -13
  370. package/src/client/__tests__/collection-form-bridge.test.ts +87 -7
  371. package/src/client/__tests__/collection-page-actions.test.ts +4 -4
  372. package/src/client/__tests__/collection-picker-order.test.ts +53 -0
  373. package/src/client/__tests__/collection-sort-menu.test.ts +2 -2
  374. package/src/client/__tests__/compose-bridge.test.ts +305 -13
  375. package/src/client/__tests__/compose-launch.test.ts +120 -0
  376. package/src/client/__tests__/compose-shortcuts.test.ts +141 -0
  377. package/src/client/__tests__/feed-video-player.test.ts +44 -0
  378. package/src/client/__tests__/site-header-nav.test.ts +250 -0
  379. package/src/client/__tests__/sortable-list.test.ts +6 -2
  380. package/src/client/__tests__/thread-context.test.ts +117 -0
  381. package/src/client/collection-form-bridge.ts +109 -4
  382. package/src/client/collection-page-actions.ts +3 -1
  383. package/src/client/collection-picker-order.ts +104 -0
  384. package/src/client/components/__tests__/{jant-collection-sidebar.test.ts → jant-collection-directory.test.ts} +91 -10
  385. package/src/client/components/__tests__/jant-collection-form.test.ts +40 -18
  386. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1285 -66
  387. package/src/client/components/__tests__/jant-compose-editor.test.ts +254 -19
  388. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +127 -1
  389. package/src/client/components/__tests__/jant-media-lightbox.test.ts +237 -0
  390. package/src/client/components/__tests__/jant-nav-manager.test.ts +326 -0
  391. package/src/client/components/__tests__/jant-post-menu.test.ts +372 -2
  392. package/src/client/components/__tests__/jant-settings-avatar.test.ts +61 -5
  393. package/src/client/components/__tests__/jant-settings-general.test.ts +29 -28
  394. package/src/client/components/__tests__/jant-text-preview.test.ts +206 -0
  395. package/src/client/components/collection-manager-types.ts +18 -1
  396. package/src/client/components/compose-types.ts +48 -0
  397. package/src/client/components/jant-collection-directory.ts +1448 -0
  398. package/src/client/components/jant-collection-form.ts +65 -17
  399. package/src/client/components/jant-command-palette.ts +544 -0
  400. package/src/client/components/jant-compose-dialog.ts +2490 -375
  401. package/src/client/components/jant-compose-editor.ts +420 -75
  402. package/src/client/components/jant-compose-fullscreen.ts +59 -8
  403. package/src/client/components/jant-media-lightbox.ts +373 -18
  404. package/src/client/components/jant-nav-manager.ts +624 -286
  405. package/src/client/components/jant-post-menu.ts +451 -126
  406. package/src/client/components/jant-repo-picker-types.ts +39 -0
  407. package/src/client/components/jant-repo-picker.ts +799 -0
  408. package/src/client/components/jant-settings-avatar.ts +16 -2
  409. package/src/client/components/jant-settings-general.ts +137 -35
  410. package/src/client/components/jant-text-preview.ts +239 -55
  411. package/src/client/components/nav-manager-types.ts +25 -9
  412. package/src/client/components/settings-types.ts +13 -2
  413. package/src/client/compose-bridge.ts +428 -68
  414. package/src/client/compose-launch.ts +41 -4
  415. package/src/client/compose-shortcuts.ts +77 -5
  416. package/src/client/feed-video-player.ts +374 -0
  417. package/src/client/image-processor.ts +53 -127
  418. package/src/client/media-lightbox-events.ts +1 -0
  419. package/src/client/media-scroll-hint.ts +66 -0
  420. package/src/client/multipart-upload.ts +4 -1
  421. package/src/client/palette-shortcuts.ts +35 -0
  422. package/src/client/search-rank.ts +74 -0
  423. package/src/client/settings-bridge.ts +7 -0
  424. package/src/client/site-header-nav.d.ts +1 -0
  425. package/src/client/site-header-nav.js +139 -51
  426. package/src/client/sortable-list.ts +2 -0
  427. package/src/client/thread-context.ts +54 -11
  428. package/src/client/tiptap/__tests__/block-insertion.test.ts +65 -0
  429. package/src/client/tiptap/__tests__/footnotes.test.ts +692 -0
  430. package/src/client/tiptap/__tests__/insert-paragraph-around.test.ts +228 -0
  431. package/src/client/tiptap/__tests__/link-toolbar.test.ts +120 -4
  432. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +48 -0
  433. package/src/client/tiptap/block-insertion.ts +36 -0
  434. package/src/client/tiptap/bubble-menu.ts +34 -4
  435. package/src/client/tiptap/create-editor.ts +49 -30
  436. package/src/client/tiptap/embed-dialog.ts +301 -0
  437. package/src/client/tiptap/embed-node.ts +373 -0
  438. package/src/client/tiptap/embed-paste.ts +91 -0
  439. package/src/client/tiptap/extensions.ts +80 -28
  440. package/src/client/tiptap/footnotes.ts +718 -0
  441. package/src/client/tiptap/html-block-node.ts +222 -0
  442. package/src/client/tiptap/image-node.ts +49 -6
  443. package/src/client/tiptap/inline-image-upload.ts +200 -40
  444. package/src/client/tiptap/insert-paragraph-around.ts +79 -0
  445. package/src/client/tiptap/link-input-rules.ts +50 -16
  446. package/src/client/tiptap/link-toolbar.ts +156 -133
  447. package/src/client/tiptap/markdown-clipboard.ts +120 -0
  448. package/src/client/tiptap/more-break.ts +36 -3
  449. package/src/client/tiptap/paste-media.ts +6 -0
  450. package/src/client/tiptap/slash-commands.ts +167 -14
  451. package/src/client/tiptap/tab-indent.ts +184 -0
  452. package/src/client/tiptap/toolbar-mode.ts +15 -2
  453. package/src/client/tiptap/wrapping-input-rules.ts +102 -0
  454. package/src/client/types/sortablejs.d.ts +9 -1
  455. package/src/client/upload-session.ts +2 -1
  456. package/src/client/video-processor.ts +177 -20
  457. package/src/client-auth.ts +9 -1
  458. package/src/client-site.ts +15 -0
  459. package/src/client.ts +2 -0
  460. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  461. package/src/db/__tests__/migration-rehearsal.test.ts +5 -1
  462. package/src/db/__tests__/migrations.test.ts +25 -2
  463. package/src/db/backfills/0001_strip_collection_path_c_prefix.sql +11 -0
  464. package/src/db/backfills/0002_strip_collection_path_c_prefix_fix.sql +12 -0
  465. package/src/db/backfills/0003_clear_system_nav_default_labels.sql +15 -0
  466. package/src/db/dialect.ts +40 -0
  467. package/src/db/migrations/0005_busy_the_phantom.sql +39 -0
  468. package/src/db/migrations/0006_adorable_magdalene.sql +3 -0
  469. package/src/db/migrations/0007_unusual_warstar.sql +1 -0
  470. package/src/db/migrations/0008_nasty_lockheed.sql +1 -0
  471. package/src/db/migrations/0009_clear_fixer.sql +31 -0
  472. package/src/db/migrations/0010_futuristic_preak.sql +31 -0
  473. package/src/db/migrations/0011_bizarre_smasher.sql +30 -0
  474. package/src/db/migrations/0012_furry_thena.sql +52 -0
  475. package/src/db/migrations/0013_mixed_lightspeed.sql +39 -0
  476. package/src/db/migrations/0014_high_the_santerians.sql +41 -0
  477. package/src/db/migrations/0015_skinny_shinobi_shaw.sql +1 -0
  478. package/src/db/migrations/0016_remarkable_nicolaos.sql +17 -0
  479. package/src/db/migrations/0017_powerful_moonstone.sql +14 -0
  480. package/src/db/migrations/0018_yummy_franklin_richards.sql +6 -0
  481. package/src/db/migrations/meta/0005_snapshot.json +1904 -0
  482. package/src/db/migrations/meta/0006_snapshot.json +1925 -0
  483. package/src/db/migrations/meta/0007_snapshot.json +1933 -0
  484. package/src/db/migrations/meta/0008_snapshot.json +1940 -0
  485. package/src/db/migrations/meta/0009_snapshot.json +1952 -0
  486. package/src/db/migrations/meta/0010_snapshot.json +1952 -0
  487. package/src/db/migrations/meta/0011_snapshot.json +1948 -0
  488. package/src/db/migrations/meta/0012_snapshot.json +1955 -0
  489. package/src/db/migrations/meta/0013_snapshot.json +1977 -0
  490. package/src/db/migrations/meta/0014_snapshot.json +1988 -0
  491. package/src/db/migrations/meta/0015_snapshot.json +1995 -0
  492. package/src/db/migrations/meta/0016_snapshot.json +2104 -0
  493. package/src/db/migrations/meta/0017_snapshot.json +2188 -0
  494. package/src/db/migrations/meta/0018_snapshot.json +2225 -0
  495. package/src/db/migrations/meta/_journal.json +98 -0
  496. package/src/db/migrations/pg/0003_motionless_norrin_radd.sql +21 -0
  497. package/src/db/migrations/pg/0004_nervous_captain_midlands.sql +3 -0
  498. package/src/db/migrations/pg/0005_romantic_mesmero.sql +1 -0
  499. package/src/db/migrations/pg/0006_perpetual_bruce_banner.sql +1 -0
  500. package/src/db/migrations/pg/0007_nav_item_placement.sql +1 -0
  501. package/src/db/migrations/pg/0008_yielding_frightful_four.sql +3 -0
  502. package/src/db/migrations/pg/0009_outstanding_ogun.sql +1 -0
  503. package/src/db/migrations/pg/0010_overjoyed_gertrude_yorkes.sql +28 -0
  504. package/src/db/migrations/pg/0011_fixed_hulk.sql +19 -0
  505. package/src/db/migrations/pg/0012_cute_shockwave.sql +2 -0
  506. package/src/db/migrations/pg/0013_bizarre_obadiah_stane.sql +1 -0
  507. package/src/db/migrations/pg/0014_tearful_grim_reaper.sql +16 -0
  508. package/src/db/migrations/pg/0015_daffy_mikhail_rasputin.sql +14 -0
  509. package/src/db/migrations/pg/0016_familiar_lionheart.sql +6 -0
  510. package/src/db/migrations/pg/meta/0003_snapshot.json +2482 -0
  511. package/src/db/migrations/pg/meta/0004_snapshot.json +2500 -0
  512. package/src/db/migrations/pg/meta/0005_snapshot.json +2507 -0
  513. package/src/db/migrations/pg/meta/0006_snapshot.json +2513 -0
  514. package/src/db/migrations/pg/meta/0008_snapshot.json +2524 -0
  515. package/src/db/migrations/pg/meta/0009_snapshot.json +2520 -0
  516. package/src/db/migrations/pg/meta/0010_snapshot.json +2526 -0
  517. package/src/db/migrations/pg/meta/0011_snapshot.json +2563 -0
  518. package/src/db/migrations/pg/meta/0012_snapshot.json +2573 -0
  519. package/src/db/migrations/pg/meta/0013_snapshot.json +2579 -0
  520. package/src/db/migrations/pg/meta/0014_snapshot.json +2702 -0
  521. package/src/db/migrations/pg/meta/0015_snapshot.json +2803 -0
  522. package/src/db/migrations/pg/meta/0016_snapshot.json +2840 -0
  523. package/src/db/migrations/pg/meta/_journal.json +98 -0
  524. package/src/db/pg/__tests__/node.test.ts +38 -0
  525. package/src/db/pg/node.ts +149 -0
  526. package/src/db/pg/schema.ts +148 -12
  527. package/src/db/rehearsal-fixtures/demo-current.sql +47 -3
  528. package/src/db/schema.ts +162 -12
  529. package/src/i18n/Trans.tsx +3 -3
  530. package/src/i18n/__tests__/context.test.tsx +9 -6
  531. package/src/i18n/__tests__/detect.test.ts +65 -48
  532. package/src/i18n/__tests__/fallback.test.ts +40 -0
  533. package/src/i18n/__tests__/message-placeholders.test.ts +43 -0
  534. package/src/i18n/__tests__/middleware.test.ts +83 -0
  535. package/src/i18n/context.tsx +9 -44
  536. package/src/i18n/detect.ts +95 -28
  537. package/src/i18n/i18n.ts +41 -17
  538. package/src/i18n/index.ts +7 -5
  539. package/src/i18n/locales/{en.po → public/en.po} +284 -1151
  540. package/src/i18n/locales/public/en.ts +1 -0
  541. package/src/i18n/locales/public/zh-Hans.po +2327 -0
  542. package/src/i18n/locales/public/zh-Hans.ts +1 -0
  543. package/src/i18n/locales/public/zh-Hant.po +2327 -0
  544. package/src/i18n/locales/public/zh-Hant.ts +1 -0
  545. package/src/i18n/locales/settings/en.po +1622 -0
  546. package/src/i18n/locales/settings/en.ts +1 -0
  547. package/src/i18n/locales/settings/zh-Hans.po +1622 -0
  548. package/src/i18n/locales/settings/zh-Hans.ts +1 -0
  549. package/src/i18n/locales/settings/zh-Hant.po +1622 -0
  550. package/src/i18n/locales/settings/zh-Hant.ts +1 -0
  551. package/src/i18n/middleware.ts +37 -24
  552. package/src/index.ts +4 -6
  553. package/src/lib/__tests__/collection-groups.test.ts +17 -0
  554. package/src/lib/__tests__/constants.test.ts +2 -24
  555. package/src/lib/__tests__/csp-builder.test.ts +68 -0
  556. package/src/lib/__tests__/display-text.test.ts +27 -0
  557. package/src/lib/__tests__/embed-providers.test.ts +104 -0
  558. package/src/lib/__tests__/embed-render.test.ts +60 -0
  559. package/src/lib/__tests__/feed.test.ts +125 -63
  560. package/src/lib/__tests__/hosted-signin.test.ts +41 -2
  561. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +88 -0
  562. package/src/lib/__tests__/markdown-to-tiptap.test.ts +59 -0
  563. package/src/lib/__tests__/markdown.test.ts +47 -5
  564. package/src/lib/__tests__/navigation.test.ts +132 -0
  565. package/src/lib/__tests__/post-display.test.ts +3 -2
  566. package/src/lib/__tests__/post-meta.test.ts +3 -0
  567. package/src/lib/__tests__/rate-limit-d1.test.ts +82 -0
  568. package/src/lib/__tests__/rate-limit-memory.test.ts +69 -0
  569. package/src/lib/__tests__/resolve-config.test.ts +30 -25
  570. package/src/lib/__tests__/schemas.test.ts +20 -3
  571. package/src/lib/__tests__/summary.test.ts +540 -1
  572. package/src/lib/__tests__/theme.test.ts +41 -2
  573. package/src/lib/__tests__/timeline.test.ts +33 -27
  574. package/src/lib/__tests__/tiptap-render.test.ts +224 -0
  575. package/src/lib/__tests__/tiptap-to-markdown.test.ts +51 -0
  576. package/src/lib/__tests__/video-playback.test.ts +48 -0
  577. package/src/lib/__tests__/view.test.ts +181 -8
  578. package/src/lib/__tests__/worker-response-cache.test.ts +311 -0
  579. package/src/lib/__tests__/youtube.test.ts +99 -0
  580. package/src/lib/api-media.ts +55 -0
  581. package/src/lib/api-posts.ts +99 -0
  582. package/src/lib/api-search.ts +47 -0
  583. package/src/lib/api-settings.ts +122 -0
  584. package/src/lib/asset-path.ts +20 -2
  585. package/src/lib/collection-groups.ts +2 -1
  586. package/src/lib/collection-paths.ts +40 -0
  587. package/src/lib/constants.ts +3 -24
  588. package/src/lib/csp-builder.ts +99 -0
  589. package/src/lib/decorative-quote-mark.ts +11 -0
  590. package/src/lib/display-text.ts +56 -0
  591. package/src/lib/embed-providers.ts +289 -0
  592. package/src/lib/embed-render.ts +151 -0
  593. package/src/lib/env.ts +70 -1
  594. package/src/lib/excerpt.ts +11 -2
  595. package/src/lib/feed.ts +154 -95
  596. package/src/lib/footnotes.ts +146 -0
  597. package/src/lib/github-api.ts +423 -0
  598. package/src/lib/github-app-state.ts +135 -0
  599. package/src/lib/github-app.ts +487 -0
  600. package/src/lib/github-sync-queue-handler.ts +69 -0
  601. package/src/lib/github-sync-site-config.ts +57 -0
  602. package/src/lib/github-sync-status.tsx +87 -0
  603. package/src/lib/github-sync-trigger.ts +199 -0
  604. package/src/lib/github-sync-worker.ts +72 -0
  605. package/src/lib/hosted-signin.ts +9 -3
  606. package/src/lib/hugo-markdown.ts +255 -0
  607. package/src/lib/job-queue-cf.ts +18 -0
  608. package/src/lib/job-queue-db.ts +149 -0
  609. package/src/lib/job-queue.ts +35 -0
  610. package/src/lib/markdown-manager.ts +864 -0
  611. package/src/lib/markdown-to-tiptap.ts +4 -323
  612. package/src/lib/markdown.ts +16 -24
  613. package/src/lib/media-helpers.ts +1 -0
  614. package/src/lib/navigation.ts +50 -19
  615. package/src/lib/post-display.ts +10 -22
  616. package/src/lib/post-meta.ts +20 -2
  617. package/src/lib/public-storage.ts +6 -1
  618. package/src/lib/rate-limit-d1.ts +99 -0
  619. package/src/lib/rate-limit-memory.ts +105 -0
  620. package/src/lib/rate-limit.ts +63 -0
  621. package/src/lib/render.tsx +14 -2
  622. package/src/lib/resolve-config.ts +32 -11
  623. package/src/lib/rich-image.ts +89 -0
  624. package/src/lib/schemas.ts +180 -13
  625. package/src/lib/summary.ts +162 -20
  626. package/src/lib/theme.ts +119 -0
  627. package/src/lib/timeline.ts +63 -42
  628. package/src/lib/tiptap-render.ts +265 -126
  629. package/src/lib/tiptap-to-markdown.ts +6 -329
  630. package/src/lib/upload.ts +97 -6
  631. package/src/lib/url.ts +34 -0
  632. package/src/lib/version.ts +18 -0
  633. package/src/lib/video-playback.ts +73 -0
  634. package/src/lib/view.ts +168 -94
  635. package/src/lib/webhook-signature.ts +65 -0
  636. package/src/lib/worker-response-cache.ts +220 -0
  637. package/src/lib/youtube.ts +119 -0
  638. package/src/middleware/__tests__/auth.test.ts +44 -4
  639. package/src/middleware/__tests__/onboarding.test.ts +3 -3
  640. package/src/middleware/__tests__/rate-limit.test.ts +113 -0
  641. package/src/middleware/__tests__/secure-headers.test.ts +28 -1
  642. package/src/middleware/__tests__/session.test.ts +85 -0
  643. package/src/middleware/auth.ts +62 -25
  644. package/src/middleware/config.ts +7 -10
  645. package/src/middleware/cors.ts +49 -0
  646. package/src/middleware/rate-limit.ts +54 -0
  647. package/src/middleware/secure-headers.ts +45 -55
  648. package/src/middleware/session.ts +36 -0
  649. package/src/node/__tests__/cli-migrate.test.ts +111 -0
  650. package/src/node/__tests__/cli-runtime-target.test.ts +1 -1
  651. package/src/node/__tests__/cli-site-token-env.test.ts +2 -2
  652. package/src/preset.css +316 -12
  653. package/src/routes/__tests__/compose.test.ts +4 -2
  654. package/src/routes/api/__tests__/collections.test.ts +81 -27
  655. package/src/routes/api/__tests__/mcp.test.ts +389 -0
  656. package/src/routes/api/__tests__/nav-items.test.ts +163 -2
  657. package/src/routes/api/__tests__/posts.test.ts +190 -0
  658. package/src/routes/api/__tests__/search.test.ts +48 -0
  659. package/src/routes/api/__tests__/settings.test.ts +99 -3
  660. package/src/routes/api/__tests__/upload-multipart.test.ts +21 -6
  661. package/src/routes/api/__tests__/upload.test.ts +132 -0
  662. package/src/routes/api/__tests__/uploads.test.ts +5 -3
  663. package/src/routes/api/collections.ts +67 -26
  664. package/src/routes/api/custom-urls.ts +1 -0
  665. package/src/routes/api/export.ts +8 -4
  666. package/src/routes/api/github-sync.tsx +380 -0
  667. package/src/routes/api/internal/__tests__/sites.test.ts +45 -2
  668. package/src/routes/api/internal/search-reindex.ts +40 -0
  669. package/src/routes/api/internal/sites.ts +2 -0
  670. package/src/routes/api/internal/text-attachments.ts +60 -0
  671. package/src/routes/api/internal/uploads.ts +5 -1
  672. package/src/routes/api/mcp.ts +30 -0
  673. package/src/routes/api/nav-items.ts +30 -11
  674. package/src/routes/api/palette.ts +17 -0
  675. package/src/routes/api/posts.ts +37 -106
  676. package/src/routes/api/public/__tests__/posts.test.ts +372 -0
  677. package/src/routes/api/public/posts.ts +302 -0
  678. package/src/routes/api/search.ts +13 -46
  679. package/src/routes/api/settings.ts +44 -58
  680. package/src/routes/api/upload-multipart.ts +15 -5
  681. package/src/routes/api/upload.ts +62 -49
  682. package/src/routes/api/uploads.ts +1 -0
  683. package/src/routes/auth/__tests__/hosted-sso.test.ts +33 -0
  684. package/src/routes/auth/__tests__/setup.test.ts +29 -4
  685. package/src/routes/auth/dev.ts +1 -1
  686. package/src/routes/auth/hosted-sso-expired-page.tsx +89 -0
  687. package/src/routes/auth/hosted-sso.ts +42 -1
  688. package/src/routes/auth/reset.tsx +46 -32
  689. package/src/routes/auth/setup.tsx +53 -43
  690. package/src/routes/auth/signin.tsx +57 -29
  691. package/src/routes/compose.tsx +114 -1
  692. package/src/routes/dash/__tests__/font-theme.test.ts +6 -27
  693. package/src/routes/dash/__tests__/settings-avatar.test.ts +2 -3
  694. package/src/routes/dash/custom-urls.tsx +250 -112
  695. package/src/routes/dash/settings.tsx +1182 -75
  696. package/src/routes/feed/__tests__/{rss.test.ts → feed.test.ts} +62 -123
  697. package/src/routes/feed/__tests__/sitemap.test.ts +320 -4
  698. package/src/routes/feed/feed.ts +386 -0
  699. package/src/routes/feed/manifest.ts +67 -0
  700. package/src/routes/feed/sitemap.ts +211 -27
  701. package/src/routes/pages/__tests__/collection-routing.test.ts +176 -0
  702. package/src/routes/pages/__tests__/collections.test.ts +2 -3
  703. package/src/routes/pages/__tests__/featured.test.ts +2 -3
  704. package/src/routes/pages/__tests__/page-canonical.test.ts +101 -0
  705. package/src/routes/pages/archive.tsx +523 -127
  706. package/src/routes/pages/collection.tsx +197 -43
  707. package/src/routes/pages/collections.tsx +5 -1
  708. package/src/routes/pages/home.tsx +26 -17
  709. package/src/routes/pages/latest.tsx +3 -3
  710. package/src/routes/pages/page.tsx +255 -3
  711. package/src/routes/pages/partials.tsx +8 -14
  712. package/src/routes/pages/search.tsx +17 -1
  713. package/src/routes/pages/theme-sample.tsx +1 -2
  714. package/src/runtime/__tests__/node.test.ts +1 -1
  715. package/src/runtime/cloudflare.ts +4 -0
  716. package/src/runtime/node.ts +16 -0
  717. package/src/runtime/readiness.ts +1 -1
  718. package/src/runtime/site.ts +9 -1
  719. package/src/services/__tests__/collection.test.ts +137 -54
  720. package/src/services/__tests__/github-app-installations.test.ts +181 -0
  721. package/src/services/__tests__/github-sync-classify.test.ts +189 -0
  722. package/src/services/__tests__/github-sync-push.test.ts +159 -0
  723. package/src/services/__tests__/media.test.ts +444 -7
  724. package/src/services/__tests__/navigation.test.ts +271 -14
  725. package/src/services/__tests__/post-timeline.test.ts +31 -21
  726. package/src/services/__tests__/post.test.ts +288 -9
  727. package/src/services/__tests__/search.test.ts +44 -0
  728. package/src/services/__tests__/settings.test.ts +43 -21
  729. package/src/services/auth.ts +2 -2
  730. package/src/services/bootstrap.ts +10 -7
  731. package/src/services/collection.ts +591 -193
  732. package/src/services/custom-url.ts +34 -7
  733. package/src/services/export-theme/assets/client-site.css +2 -0
  734. package/src/services/export-theme/assets/client-site.js +145 -0
  735. package/src/services/export-theme/layouts/_default/alias.html +49 -0
  736. package/src/services/export-theme/layouts/_default/baseof.html +21 -0
  737. package/src/services/export-theme/layouts/_default/list.html +104 -0
  738. package/src/services/export-theme/layouts/_default/rss.xml +160 -0
  739. package/src/services/export-theme/layouts/_default/single.html +15 -0
  740. package/src/services/export-theme/layouts/archive/list.html +31 -0
  741. package/src/services/export-theme/layouts/collection/single.html +19 -0
  742. package/src/services/export-theme/layouts/collections/list.html +111 -0
  743. package/src/services/export-theme/layouts/featured/list.html +28 -0
  744. package/src/services/export-theme/layouts/index.html +41 -0
  745. package/src/services/export-theme/layouts/partials/feed-post-content.xml +107 -0
  746. package/src/services/export-theme/layouts/partials/footer.html +32 -0
  747. package/src/services/export-theme/layouts/partials/head.html +52 -0
  748. package/src/services/export-theme/layouts/partials/header.html +115 -0
  749. package/src/services/export-theme/layouts/partials/media-gallery.html +246 -0
  750. package/src/services/export-theme/layouts/partials/pagination.html +55 -0
  751. package/src/services/export-theme/layouts/partials/post-card.html +127 -0
  752. package/src/services/export-theme/layouts/partials/reply.html +91 -0
  753. package/src/services/export-theme/layouts/partials/thread-preview.html +82 -0
  754. package/src/services/export-theme/layouts/post/list.html +129 -0
  755. package/src/services/export-theme/styles/main.css +1981 -0
  756. package/src/services/export-theme/theme.toml +12 -0
  757. package/src/services/export.ts +1372 -2412
  758. package/src/services/github-app-installations.ts +302 -0
  759. package/src/services/github-sync.ts +769 -0
  760. package/src/services/index.ts +15 -0
  761. package/src/services/mcp.ts +1324 -0
  762. package/src/services/media.ts +336 -25
  763. package/src/services/navigation.ts +115 -23
  764. package/src/services/path.ts +129 -28
  765. package/src/services/post.ts +858 -197
  766. package/src/services/search.ts +3 -0
  767. package/src/services/settings.ts +29 -15
  768. package/src/services/site-admin.ts +26 -6
  769. package/src/services/upload-session.ts +69 -34
  770. package/src/style-cjk-jp.css +3 -0
  771. package/src/style-cjk-kr.css +3 -0
  772. package/src/styles/components.css +160 -59
  773. package/src/styles/fonts/et-book/et-book-bold-line-figures.woff +0 -0
  774. package/src/styles/fonts/et-book/et-book-display-italic-old-style-figures.woff +0 -0
  775. package/src/styles/fonts/et-book/et-book-roman-line-figures.woff +0 -0
  776. package/src/styles/fonts/et-book.css +31 -0
  777. package/src/styles/fonts/latin.css +1 -0
  778. package/src/styles/fonts/noto-serif-jp/400/057a6a98bda7fe57105ddaa99ec82015.woff2 +0 -0
  779. package/src/styles/fonts/noto-serif-jp/400/0acd1fe2b2ea1ad1bfee7ae1fa139d27.woff2 +0 -0
  780. package/src/styles/fonts/noto-serif-jp/400/0c2ab6b295e55f356f8020d4e7747522.woff2 +0 -0
  781. package/src/styles/fonts/noto-serif-jp/400/0c5f9492af03a4fa42c784de94649de1.woff2 +0 -0
  782. package/src/styles/fonts/noto-serif-jp/400/0d9a936885a4c39077438effd3779cbd.woff2 +0 -0
  783. package/src/styles/fonts/noto-serif-jp/400/0e645da524f7cfc0e8c3c03fb2b08428.woff2 +0 -0
  784. package/src/styles/fonts/noto-serif-jp/400/135e83b403475c5dc9e49b43501a5b84.woff2 +0 -0
  785. package/src/styles/fonts/noto-serif-jp/400/13b2c53b30e6a3e4de7132dbc18dbdfc.woff2 +0 -0
  786. package/src/styles/fonts/noto-serif-jp/400/15d95680dc31cc6ce20e0a5464106dc4.woff2 +0 -0
  787. package/src/styles/fonts/noto-serif-jp/400/1733f27476507ca68b68a803bc533cc2.woff2 +0 -0
  788. package/src/styles/fonts/noto-serif-jp/400/188c2db794f3dd7a45889ddbc81da9bb.woff2 +0 -0
  789. package/src/styles/fonts/noto-serif-jp/400/1cf737900dd49c2e88f1b3221a82a602.woff2 +0 -0
  790. package/src/styles/fonts/noto-serif-jp/400/233d3a685ee18276b319363474599d47.woff2 +0 -0
  791. package/src/styles/fonts/noto-serif-jp/400/284b53bbefb06924cf236d24c8ed5641.woff2 +0 -0
  792. package/src/styles/fonts/noto-serif-jp/400/2a56eaf19d1d38a6b57e2a388f733676.woff2 +0 -0
  793. package/src/styles/fonts/noto-serif-jp/400/2abfbab82b6a7c04426afc054d2464da.woff2 +0 -0
  794. package/src/styles/fonts/noto-serif-jp/400/2e6f4bb71ef6b38765d51acc5a79f638.woff2 +0 -0
  795. package/src/styles/fonts/noto-serif-jp/400/31194d303a67561926a544ed0e072aee.woff2 +0 -0
  796. package/src/styles/fonts/noto-serif-jp/400/3623d96698132c61e861f984ce11cc23.woff2 +0 -0
  797. package/src/styles/fonts/noto-serif-jp/400/39211b02f24c69cbd1b54f6d41b01933.woff2 +0 -0
  798. package/src/styles/fonts/noto-serif-jp/400/3e24063b19abd2b051af986a6870046d.woff2 +0 -0
  799. package/src/styles/fonts/noto-serif-jp/400/4410f876d27c9821839474b6b1059fd7.woff2 +0 -0
  800. package/src/styles/fonts/noto-serif-jp/400/479329a39a3eb6c0047e6b0981919855.woff2 +0 -0
  801. package/src/styles/fonts/noto-serif-jp/400/527ff336279465617aafbafe1a830632.woff2 +0 -0
  802. package/src/styles/fonts/noto-serif-jp/400/56b40518ea0608e62826bd44ee27f5f9.woff2 +0 -0
  803. package/src/styles/fonts/noto-serif-jp/400/5acc7f736217259db79a42bf44241c48.woff2 +0 -0
  804. package/src/styles/fonts/noto-serif-jp/400/5c05f8b75c9d7f25b3c04ca711cf43b3.woff2 +0 -0
  805. package/src/styles/fonts/noto-serif-jp/400/5e0124c7265f1b4cf0fc797b94efbebc.woff2 +0 -0
  806. package/src/styles/fonts/noto-serif-jp/400/615a7e0d63f257109d599b2a1977de68.woff2 +0 -0
  807. package/src/styles/fonts/noto-serif-jp/400/61a2c80d0c924d5a6187c02e8d1d1642.woff2 +0 -0
  808. package/src/styles/fonts/noto-serif-jp/400/62fc94f8d4c5a750b7f25e7387539910.woff2 +0 -0
  809. package/src/styles/fonts/noto-serif-jp/400/639c7c6b0b0c27c738702741cfa4b8c0.woff2 +0 -0
  810. package/src/styles/fonts/noto-serif-jp/400/68a3a4bf337f8a0722be76676e20b850.woff2 +0 -0
  811. package/src/styles/fonts/noto-serif-jp/400/6abdd163d2c4b85698db8aa1ce149361.woff2 +0 -0
  812. package/src/styles/fonts/noto-serif-jp/400/73b794d885b88f7befb7ea8ab1780a15.woff2 +0 -0
  813. package/src/styles/fonts/noto-serif-jp/400/743f290baf027d4626a86e22f3d44600.woff2 +0 -0
  814. package/src/styles/fonts/noto-serif-jp/400/757019c01c5e2c2b0f54293cea3b5636.woff2 +0 -0
  815. package/src/styles/fonts/noto-serif-jp/400/76f117a3baacda304a71965a17266a13.woff2 +0 -0
  816. package/src/styles/fonts/noto-serif-jp/400/77747f17625a259fb405b7bbdd84ac4b.woff2 +0 -0
  817. package/src/styles/fonts/noto-serif-jp/400/788498548ddfb710d6ef3c7bff79fa97.woff2 +0 -0
  818. package/src/styles/fonts/noto-serif-jp/400/78ea6c40923ce95367e3517dd1e5a849.woff2 +0 -0
  819. package/src/styles/fonts/noto-serif-jp/400/7f9a6c9286b68de9c72b0024f7beeb40.woff2 +0 -0
  820. package/src/styles/fonts/noto-serif-jp/400/823d1bb11097331238d103c7f72138dc.woff2 +0 -0
  821. package/src/styles/fonts/noto-serif-jp/400/888ba53510213d5d1f6a7deb60e569b6.woff2 +0 -0
  822. package/src/styles/fonts/noto-serif-jp/400/892aa49b2529c89bb4076d4aa51fe30f.woff2 +0 -0
  823. package/src/styles/fonts/noto-serif-jp/400/95ff41191c76ef893ac70a1043e95e44.woff2 +0 -0
  824. package/src/styles/fonts/noto-serif-jp/400/96c75862c8cec51a7c05ff025fea86cd.woff2 +0 -0
  825. package/src/styles/fonts/noto-serif-jp/400/9b21e8244f5930c48ad5073f83777b6c.woff2 +0 -0
  826. package/src/styles/fonts/noto-serif-jp/400/9fce02d1a09f464c36c9fcfb14a354c5.woff2 +0 -0
  827. package/src/styles/fonts/noto-serif-jp/400/a0a6755c7e3f9e4cf03387027dd9f16c.woff2 +0 -0
  828. package/src/styles/fonts/noto-serif-jp/400/a4475c442c6b259354c9eda62e64e7df.woff2 +0 -0
  829. package/src/styles/fonts/noto-serif-jp/400/a4a39b5f048671aa37c2b2a9581ab08a.woff2 +0 -0
  830. package/src/styles/fonts/noto-serif-jp/400/a534449d67561ac9b06489f64e57ad98.woff2 +0 -0
  831. package/src/styles/fonts/noto-serif-jp/400/a919a8c6fec6d7e8bb21177965f2e9d7.woff2 +0 -0
  832. package/src/styles/fonts/noto-serif-jp/400/ad67113f88b59582991cb0c5d33ea19f.woff2 +0 -0
  833. package/src/styles/fonts/noto-serif-jp/400/adb6e613fb99c6dd660b727101f554a8.woff2 +0 -0
  834. package/src/styles/fonts/noto-serif-jp/400/adc4a98a89870ef984ee8f4b57c8a3a5.woff2 +0 -0
  835. package/src/styles/fonts/noto-serif-jp/400/b12379ab782468d725519cd07a7d15bc.woff2 +0 -0
  836. package/src/styles/fonts/noto-serif-jp/400/b55486f8a459c838fe329d4e79a8c211.woff2 +0 -0
  837. package/src/styles/fonts/noto-serif-jp/400/bb0b15492b8cdbbec57c0bcfa0aa9241.woff2 +0 -0
  838. package/src/styles/fonts/noto-serif-jp/400/bedc74b423b7293b6ad0bdecc61c42cc.woff2 +0 -0
  839. package/src/styles/fonts/noto-serif-jp/400/bfb00d4a4c48661bd0be99f300a0faae.woff2 +0 -0
  840. package/src/styles/fonts/noto-serif-jp/400/c1b0df29ae41d764904df84e9ac83d1e.woff2 +0 -0
  841. package/src/styles/fonts/noto-serif-jp/400/c42c67070cfe99cf823df92d81c7fa6e.woff2 +0 -0
  842. package/src/styles/fonts/noto-serif-jp/400/c4d749e45ecd5a5aed5a0bb3ebfd355d.woff2 +0 -0
  843. package/src/styles/fonts/noto-serif-jp/400/c96d83978add28b356c22c4c84916733.woff2 +0 -0
  844. package/src/styles/fonts/noto-serif-jp/400/cb7ccd6494256f7a2977b7c2b0225592.woff2 +0 -0
  845. package/src/styles/fonts/noto-serif-jp/400/ce41d70ce6a069a498525c9e15c45cf2.woff2 +0 -0
  846. package/src/styles/fonts/noto-serif-jp/400/d509ee5c8241bc7c3de2039d75564fa5.woff2 +0 -0
  847. package/src/styles/fonts/noto-serif-jp/400/d675d717cd329bfd4c0524f76ae1579c.woff2 +0 -0
  848. package/src/styles/fonts/noto-serif-jp/400/dc8f6256445e68199540be9ade33529f.woff2 +0 -0
  849. package/src/styles/fonts/noto-serif-jp/400/e8c15be62faf978d208925e79ea6a10d.woff2 +0 -0
  850. package/src/styles/fonts/noto-serif-jp/400/e91c4d941ed9f5c2f3e27f205a3a225e.woff2 +0 -0
  851. package/src/styles/fonts/noto-serif-jp/400/e9c566be7a5d38a9085225f7372bc82b.woff2 +0 -0
  852. package/src/styles/fonts/noto-serif-jp/400/e9ebc567a711eeb29019ddae3e0ce7fe.woff2 +0 -0
  853. package/src/styles/fonts/noto-serif-jp/400/ecb8875a56c7b038b35432fda41ae128.woff2 +0 -0
  854. package/src/styles/fonts/noto-serif-jp/400/f1661731474b78bdf81114daf10b254f.woff2 +0 -0
  855. package/src/styles/fonts/noto-serif-jp/400/f2c58dee206ba9355046fc23d05491f7.woff2 +0 -0
  856. package/src/styles/fonts/noto-serif-jp/400/f50ac27ea4358d67fdda403c2bb52467.woff2 +0 -0
  857. package/src/styles/fonts/noto-serif-jp/400/f55334112f8e8be82d65db29887a663f.woff2 +0 -0
  858. package/src/styles/fonts/noto-serif-jp/400/f740e93f3c0277ecc616594103bca683.woff2 +0 -0
  859. package/src/styles/fonts/noto-serif-jp/400/f7633b5af033d76ff2fb3c3c266d77c5.woff2 +0 -0
  860. package/src/styles/fonts/noto-serif-jp/400/f7d8468cba2335a83ee414ea68291bab.woff2 +0 -0
  861. package/src/styles/fonts/noto-serif-jp/400/fc9ae5b600fb711f2d67e93ce768cba4.woff2 +0 -0
  862. package/src/styles/fonts/noto-serif-jp/400/fd1bceb55d3e0183ac2454b8532fec7d.woff2 +0 -0
  863. package/src/styles/fonts/noto-serif-jp/700/02611a045a7fe83a12014e3debc9f731.woff2 +0 -0
  864. package/src/styles/fonts/noto-serif-jp/700/0f5e1a18987dbc84ca05188c129e1936.woff2 +0 -0
  865. package/src/styles/fonts/noto-serif-jp/700/112743a4ab5fdd1498dfdf2b11336380.woff2 +0 -0
  866. package/src/styles/fonts/noto-serif-jp/700/1211f03d3ff5759a702631445793f60e.woff2 +0 -0
  867. package/src/styles/fonts/noto-serif-jp/700/147b24c2f08d03bbed30887d4cb3311c.woff2 +0 -0
  868. package/src/styles/fonts/noto-serif-jp/700/16663e567f81f4725a1522f37e18f71f.woff2 +0 -0
  869. package/src/styles/fonts/noto-serif-jp/700/1824321da801e6257b902f3d1d09054c.woff2 +0 -0
  870. package/src/styles/fonts/noto-serif-jp/700/1a3a92c7c060a71a6c35819b9ebcdbb9.woff2 +0 -0
  871. package/src/styles/fonts/noto-serif-jp/700/1fba7ec0e412e911bf31841de5a8a4e4.woff2 +0 -0
  872. package/src/styles/fonts/noto-serif-jp/700/28a97af9ab9d38983d20f39ff09840e1.woff2 +0 -0
  873. package/src/styles/fonts/noto-serif-jp/700/2bfaadaf3479c72286248e6de0be0ec9.woff2 +0 -0
  874. package/src/styles/fonts/noto-serif-jp/700/2c8c55e4cec85ce77e95cac9d330f5cf.woff2 +0 -0
  875. package/src/styles/fonts/noto-serif-jp/700/2ed9981d2e8983365bd051159b976b6d.woff2 +0 -0
  876. package/src/styles/fonts/noto-serif-jp/700/2f4c633e923ba30c6ba376367379fc91.woff2 +0 -0
  877. package/src/styles/fonts/noto-serif-jp/700/349965eee0a9b6c984a319ab96a4ece9.woff2 +0 -0
  878. package/src/styles/fonts/noto-serif-jp/700/36cda3eae13370b934bba4429fab9078.woff2 +0 -0
  879. package/src/styles/fonts/noto-serif-jp/700/3aa13f1843d7c8fa16109b6395a9a29d.woff2 +0 -0
  880. package/src/styles/fonts/noto-serif-jp/700/3e2c06bdd9dcab29aeaeb0cfb7fa608a.woff2 +0 -0
  881. package/src/styles/fonts/noto-serif-jp/700/424123f9371ba357087363263eeafcda.woff2 +0 -0
  882. package/src/styles/fonts/noto-serif-jp/700/43b3bbf43055c3e65db9cdf2c5b18c1c.woff2 +0 -0
  883. package/src/styles/fonts/noto-serif-jp/700/44704471b60c18b3ecec5300a88f1102.woff2 +0 -0
  884. package/src/styles/fonts/noto-serif-jp/700/46604090efbb99c2db1800928af0a239.woff2 +0 -0
  885. package/src/styles/fonts/noto-serif-jp/700/46998df85d31e629c0142f0556f5e7c5.woff2 +0 -0
  886. package/src/styles/fonts/noto-serif-jp/700/49902f1c41b281f8eb3771abdd4dccb8.woff2 +0 -0
  887. package/src/styles/fonts/noto-serif-jp/700/4db4f31a16965baa42cb7dad667a8f04.woff2 +0 -0
  888. package/src/styles/fonts/noto-serif-jp/700/572b9e28f1e6d89b765a16d809db84a1.woff2 +0 -0
  889. package/src/styles/fonts/noto-serif-jp/700/5921cef36750ff6092d94b83a802823b.woff2 +0 -0
  890. package/src/styles/fonts/noto-serif-jp/700/59e95d8b5332dcdae894a19946ab767d.woff2 +0 -0
  891. package/src/styles/fonts/noto-serif-jp/700/5ec349ff62b9ee5fe0c6dae867704dc4.woff2 +0 -0
  892. package/src/styles/fonts/noto-serif-jp/700/601f812e8b1eafecd5ba585a6a6d7962.woff2 +0 -0
  893. package/src/styles/fonts/noto-serif-jp/700/6025ddaf99d86cc8bfd781006b19fbd6.woff2 +0 -0
  894. package/src/styles/fonts/noto-serif-jp/700/60f6e28b3f0400b0e8fe32c3c7116102.woff2 +0 -0
  895. package/src/styles/fonts/noto-serif-jp/700/64a404d675f1d726f0891b7a80794595.woff2 +0 -0
  896. package/src/styles/fonts/noto-serif-jp/700/7158022889c6c177062ac85036e7af10.woff2 +0 -0
  897. package/src/styles/fonts/noto-serif-jp/700/7e38ca789a9e76ac91d9256374e154f0.woff2 +0 -0
  898. package/src/styles/fonts/noto-serif-jp/700/815a0b797465190f60d8b1a04656c42e.woff2 +0 -0
  899. package/src/styles/fonts/noto-serif-jp/700/8726558cf297cbda831cc19514897205.woff2 +0 -0
  900. package/src/styles/fonts/noto-serif-jp/700/8855472f3c02f6c7ebb3216617ebe4af.woff2 +0 -0
  901. package/src/styles/fonts/noto-serif-jp/700/8e231d707f0c4f8e3cde90a6b52a79aa.woff2 +0 -0
  902. package/src/styles/fonts/noto-serif-jp/700/9064c12fd72c7aba235d8c3881755b91.woff2 +0 -0
  903. package/src/styles/fonts/noto-serif-jp/700/911b9e53e9814de2998c60bf3115f560.woff2 +0 -0
  904. package/src/styles/fonts/noto-serif-jp/700/92fdc376bce277874e75db666f175b44.woff2 +0 -0
  905. package/src/styles/fonts/noto-serif-jp/700/930a23430f0eb64480d7fe5f82834e21.woff2 +0 -0
  906. package/src/styles/fonts/noto-serif-jp/700/93bed0e5f2dd5a21cf73304fcfbc149d.woff2 +0 -0
  907. package/src/styles/fonts/noto-serif-jp/700/93f3ea6918533d96d9c252378b9a4af0.woff2 +0 -0
  908. package/src/styles/fonts/noto-serif-jp/700/95a0951d2a2722ff773a1a45e8c474d6.woff2 +0 -0
  909. package/src/styles/fonts/noto-serif-jp/700/982cd1765ca6bbb3e6547e47834d5148.woff2 +0 -0
  910. package/src/styles/fonts/noto-serif-jp/700/9b0c3caac458c7bc9c9133e40405ddea.woff2 +0 -0
  911. package/src/styles/fonts/noto-serif-jp/700/9f3bdab4025dc7c5ba1a5871e62e8918.woff2 +0 -0
  912. package/src/styles/fonts/noto-serif-jp/700/a00e94d4f04df6aa659db9c4954c7efe.woff2 +0 -0
  913. package/src/styles/fonts/noto-serif-jp/700/a2b0597837e382aca19919c4b74e58c8.woff2 +0 -0
  914. package/src/styles/fonts/noto-serif-jp/700/a373e85c96aa0dc303092429fb837e51.woff2 +0 -0
  915. package/src/styles/fonts/noto-serif-jp/700/a6e044424780a57610833cc856c15bf5.woff2 +0 -0
  916. package/src/styles/fonts/noto-serif-jp/700/a7c58070d68dc1724e9d4e781afb78db.woff2 +0 -0
  917. package/src/styles/fonts/noto-serif-jp/700/accc43762ad05edfbff66ba1380d192a.woff2 +0 -0
  918. package/src/styles/fonts/noto-serif-jp/700/ad47bcd6d4663026d648c132d969318d.woff2 +0 -0
  919. package/src/styles/fonts/noto-serif-jp/700/af57a2e8c72f9ba0efb1af771d32c124.woff2 +0 -0
  920. package/src/styles/fonts/noto-serif-jp/700/b0ae5f374e43dac992a4a5d334cebc0b.woff2 +0 -0
  921. package/src/styles/fonts/noto-serif-jp/700/b91234c10fd8b8c8abc88e03afe66a1f.woff2 +0 -0
  922. package/src/styles/fonts/noto-serif-jp/700/b9317198f118a1dfd8ddf2b82ec028f3.woff2 +0 -0
  923. package/src/styles/fonts/noto-serif-jp/700/ba825ae79b1a6f7e0cce5215fcb5c96f.woff2 +0 -0
  924. package/src/styles/fonts/noto-serif-jp/700/bc6183ac08f0fac78c46f80c10cf7c92.woff2 +0 -0
  925. package/src/styles/fonts/noto-serif-jp/700/bc8ccea5abf6598cf3cfa97eb59804bb.woff2 +0 -0
  926. package/src/styles/fonts/noto-serif-jp/700/bc95a792cbca6639214c9b0da13392ff.woff2 +0 -0
  927. package/src/styles/fonts/noto-serif-jp/700/bcfa62f35731856246c146d3a6932bf3.woff2 +0 -0
  928. package/src/styles/fonts/noto-serif-jp/700/c063897793f593eb26d6ff7b7baaba18.woff2 +0 -0
  929. package/src/styles/fonts/noto-serif-jp/700/c6c2971ad1f3221f6cf84028aa0f477e.woff2 +0 -0
  930. package/src/styles/fonts/noto-serif-jp/700/c97f41eef722121d86f55d553c056a39.woff2 +0 -0
  931. package/src/styles/fonts/noto-serif-jp/700/ca62704509932d3232d62918de97af3f.woff2 +0 -0
  932. package/src/styles/fonts/noto-serif-jp/700/d1f064825fa5784b5c930652bd831cce.woff2 +0 -0
  933. package/src/styles/fonts/noto-serif-jp/700/d2faabcedd19f016e7b21bce073c0ec8.woff2 +0 -0
  934. package/src/styles/fonts/noto-serif-jp/700/d320171e57480510f87dbbc7d5264b0a.woff2 +0 -0
  935. package/src/styles/fonts/noto-serif-jp/700/d3a72a99d365dddfbca8d017a8011368.woff2 +0 -0
  936. package/src/styles/fonts/noto-serif-jp/700/d42aafa0f246ad3b4c16fe96d3a3a432.woff2 +0 -0
  937. package/src/styles/fonts/noto-serif-jp/700/d4aaf23c13ae808a4bed617afd13aa07.woff2 +0 -0
  938. package/src/styles/fonts/noto-serif-jp/700/d6bb686bddfbe8f38a36a68e609a8667.woff2 +0 -0
  939. package/src/styles/fonts/noto-serif-jp/700/d9020ff69a83b2a6ee1f42ae480f7db0.woff2 +0 -0
  940. package/src/styles/fonts/noto-serif-jp/700/da90a9012ab2d98f759e3fa0820ef502.woff2 +0 -0
  941. package/src/styles/fonts/noto-serif-jp/700/df10d94bea357a43313e20da5d84bd30.woff2 +0 -0
  942. package/src/styles/fonts/noto-serif-jp/700/e34b2b141e472dc776c86fdf8eea23b0.woff2 +0 -0
  943. package/src/styles/fonts/noto-serif-jp/700/e444fd88f1390636e603d0d681538ac8.woff2 +0 -0
  944. package/src/styles/fonts/noto-serif-jp/700/e79898628283edc27180fc39d0d769c1.woff2 +0 -0
  945. package/src/styles/fonts/noto-serif-jp/700/eaf332445a40942928e5f5750c1c3116.woff2 +0 -0
  946. package/src/styles/fonts/noto-serif-jp/700/f91cd722855f4269256eae1187df64ec.woff2 +0 -0
  947. package/src/styles/fonts/noto-serif-jp/700/fc895f5ce66b656f4a933097bf2a8775.woff2 +0 -0
  948. package/src/styles/fonts/noto-serif-jp/700/fca6720fd14c467d29a90f18ef3859b9.woff2 +0 -0
  949. package/src/styles/fonts/noto-serif-jp/700/fd1bb507bcbf04856eeb7f7fd47ea579.woff2 +0 -0
  950. package/src/styles/fonts/noto-serif-jp/noto-serif-jp.css +3349 -0
  951. package/src/styles/fonts/noto-serif-kr/400/033466ef683afe931f7f520cfb42d928.woff2 +0 -0
  952. package/src/styles/fonts/noto-serif-kr/400/05da12edb9d52210581dc6ec4541031f.woff2 +0 -0
  953. package/src/styles/fonts/noto-serif-kr/400/0968e4861204b51f62a2f8e9f15dd5e0.woff2 +0 -0
  954. package/src/styles/fonts/noto-serif-kr/400/0d5dec931dc885f07fe5cd5af8bed675.woff2 +0 -0
  955. package/src/styles/fonts/noto-serif-kr/400/1076f0f6f66d28d7a2f16427faad4413.woff2 +0 -0
  956. package/src/styles/fonts/noto-serif-kr/400/12ef1ba76bd20b004b865266a1aa70b3.woff2 +0 -0
  957. package/src/styles/fonts/noto-serif-kr/400/152395634a207579552f8cb54db88599.woff2 +0 -0
  958. package/src/styles/fonts/noto-serif-kr/400/1a08931435f885e707edb85978ea3bb6.woff2 +0 -0
  959. package/src/styles/fonts/noto-serif-kr/400/31a197213ae61d7043c81013f52d10d6.woff2 +0 -0
  960. package/src/styles/fonts/noto-serif-kr/400/37dbd564820cce2f7b3331e442da0df3.woff2 +0 -0
  961. package/src/styles/fonts/noto-serif-kr/400/37dc8505b20b37a2477a9121b16d44a7.woff2 +0 -0
  962. package/src/styles/fonts/noto-serif-kr/400/3ac0a07878cacdc98bb889f40f5970d3.woff2 +0 -0
  963. package/src/styles/fonts/noto-serif-kr/400/491bbba374093fff045f55803a22bfef.woff2 +0 -0
  964. package/src/styles/fonts/noto-serif-kr/400/4d38e56738156625329d93bd5c8cc74a.woff2 +0 -0
  965. package/src/styles/fonts/noto-serif-kr/400/57e9a86651366c1ba299e47aa7e358c2.woff2 +0 -0
  966. package/src/styles/fonts/noto-serif-kr/400/57f89adeae55aa7fe2add3fc1801ce03.woff2 +0 -0
  967. package/src/styles/fonts/noto-serif-kr/400/5df9ce98c75f8c50fb4fff7f01a23925.woff2 +0 -0
  968. package/src/styles/fonts/noto-serif-kr/400/60020a830d809c26970cf8e48ba1eeda.woff2 +0 -0
  969. package/src/styles/fonts/noto-serif-kr/400/60bcfa3ea7446eb42394aab02d28be40.woff2 +0 -0
  970. package/src/styles/fonts/noto-serif-kr/400/63fafcb069520613d0ea35ad3e6b1e42.woff2 +0 -0
  971. package/src/styles/fonts/noto-serif-kr/400/65a60d87c64228d258a123cbe85f5f31.woff2 +0 -0
  972. package/src/styles/fonts/noto-serif-kr/400/6968e889e18891d912809fe484c2e745.woff2 +0 -0
  973. package/src/styles/fonts/noto-serif-kr/400/70714891ad3fbfc3d5f10a8669dacc5a.woff2 +0 -0
  974. package/src/styles/fonts/noto-serif-kr/400/761d84afca8d7f34eebefe538adba827.woff2 +0 -0
  975. package/src/styles/fonts/noto-serif-kr/400/76e07530323418ec723c5d7634c9bcca.woff2 +0 -0
  976. package/src/styles/fonts/noto-serif-kr/400/773819b7b9b8fd404a929867c0fd677e.woff2 +0 -0
  977. package/src/styles/fonts/noto-serif-kr/400/7817dd16805145d8538ad57590f69f5a.woff2 +0 -0
  978. package/src/styles/fonts/noto-serif-kr/400/7b59f0ec7792b18458dc5a361e37884c.woff2 +0 -0
  979. package/src/styles/fonts/noto-serif-kr/400/7c3945788a689a69356c1a622d69d48b.woff2 +0 -0
  980. package/src/styles/fonts/noto-serif-kr/400/835b6505bb9eea9678925a1fa885353d.woff2 +0 -0
  981. package/src/styles/fonts/noto-serif-kr/400/8b5c9ef81159f31d2ab35f45ca4373e0.woff2 +0 -0
  982. package/src/styles/fonts/noto-serif-kr/400/8b91c7c2ed390f1278b9befa3aa87233.woff2 +0 -0
  983. package/src/styles/fonts/noto-serif-kr/400/8fb45117a62d92dce44a80f0b729ead5.woff2 +0 -0
  984. package/src/styles/fonts/noto-serif-kr/400/90b7848e9b1623b77bdcf155e90b839c.woff2 +0 -0
  985. package/src/styles/fonts/noto-serif-kr/400/985d4d41afa0934a4eb2de35473fb9a2.woff2 +0 -0
  986. package/src/styles/fonts/noto-serif-kr/400/9bb38fd05201de666b88b82d56a386f7.woff2 +0 -0
  987. package/src/styles/fonts/noto-serif-kr/400/9eff2ab2a9d2ced47ab761bf6fdb11ec.woff2 +0 -0
  988. package/src/styles/fonts/noto-serif-kr/400/9f1963fe8d4d6878d717d872a8f3fdb5.woff2 +0 -0
  989. package/src/styles/fonts/noto-serif-kr/400/a0d00fe4816c95a8c7dffd445ef00b03.woff2 +0 -0
  990. package/src/styles/fonts/noto-serif-kr/400/a0f199077fa1a33bc2a1e01c64de7fe6.woff2 +0 -0
  991. package/src/styles/fonts/noto-serif-kr/400/a7c35e42a659347e490c3cb7983b2e7f.woff2 +0 -0
  992. package/src/styles/fonts/noto-serif-kr/400/aa662092cf249707123ca4d575e2764b.woff2 +0 -0
  993. package/src/styles/fonts/noto-serif-kr/400/aae1eda193285ab817a2d1b408440326.woff2 +0 -0
  994. package/src/styles/fonts/noto-serif-kr/400/b0cb664cb2e1371efda8943c0b7dcd1c.woff2 +0 -0
  995. package/src/styles/fonts/noto-serif-kr/400/b0d1cdced482352cf0d3ae58638aacb9.woff2 +0 -0
  996. package/src/styles/fonts/noto-serif-kr/400/b320f5d185b2cff933ac549c184031c5.woff2 +0 -0
  997. package/src/styles/fonts/noto-serif-kr/400/ba9c59d7dfa4494db1bb764ada81467d.woff2 +0 -0
  998. package/src/styles/fonts/noto-serif-kr/400/bab547459d514f46206e340c4bb2dc88.woff2 +0 -0
  999. package/src/styles/fonts/noto-serif-kr/400/bcc39eda837bb7a7a3d37c8c60fffb81.woff2 +0 -0
  1000. package/src/styles/fonts/noto-serif-kr/400/bd73264d7f98776708d5d6f3c9b78fcc.woff2 +0 -0
  1001. package/src/styles/fonts/noto-serif-kr/400/bed610b217d500f5975cfc9fe6157570.woff2 +0 -0
  1002. package/src/styles/fonts/noto-serif-kr/400/bf8901f8f11d4f433ea17dabc9370ea6.woff2 +0 -0
  1003. package/src/styles/fonts/noto-serif-kr/400/c4143bb9f2fe77b6ccf20088a8904650.woff2 +0 -0
  1004. package/src/styles/fonts/noto-serif-kr/400/c7ff3f6bbdcd5f604b7343602ab904df.woff2 +0 -0
  1005. package/src/styles/fonts/noto-serif-kr/400/c945c62368357d05a53206620460fb30.woff2 +0 -0
  1006. package/src/styles/fonts/noto-serif-kr/400/ce022e18a1377ac509443c3c3790b431.woff2 +0 -0
  1007. package/src/styles/fonts/noto-serif-kr/400/cf9cffe56636322f62b40d61130fbc5e.woff2 +0 -0
  1008. package/src/styles/fonts/noto-serif-kr/400/d20d8944bc0b85f5b2aae4b24f343516.woff2 +0 -0
  1009. package/src/styles/fonts/noto-serif-kr/400/d36644d6502527e1fff205d0c7eca434.woff2 +0 -0
  1010. package/src/styles/fonts/noto-serif-kr/400/d40ab99c7a38026f411c8f112f742b48.woff2 +0 -0
  1011. package/src/styles/fonts/noto-serif-kr/400/dc6e234ded795e91f76d6647f628fbf0.woff2 +0 -0
  1012. package/src/styles/fonts/noto-serif-kr/400/dd0cfdfdac0866e66d587e2b5a9e9961.woff2 +0 -0
  1013. package/src/styles/fonts/noto-serif-kr/400/de304c8a02e45ded4f8dcc479d167198.woff2 +0 -0
  1014. package/src/styles/fonts/noto-serif-kr/400/e8c364c16daa04835bf32d293d2598db.woff2 +0 -0
  1015. package/src/styles/fonts/noto-serif-kr/400/ec86e23683052da5cfdc3b77641bd15a.woff2 +0 -0
  1016. package/src/styles/fonts/noto-serif-kr/400/eda1b0cb6d1719dd9bedcf3216a9e8de.woff2 +0 -0
  1017. package/src/styles/fonts/noto-serif-kr/400/ee6fce20b420a480714607c66d7f97e5.woff2 +0 -0
  1018. package/src/styles/fonts/noto-serif-kr/400/eee3836d6ac17ebb2c450bbcbc9db121.woff2 +0 -0
  1019. package/src/styles/fonts/noto-serif-kr/400/f5738255e92d8dd34a46d1bcdf4c4074.woff2 +0 -0
  1020. package/src/styles/fonts/noto-serif-kr/400/fa6e58ce4b52695e7ae19bbea6336ec8.woff2 +0 -0
  1021. package/src/styles/fonts/noto-serif-kr/400/fd2069e6e8588c70b4f11364093b81f2.woff2 +0 -0
  1022. package/src/styles/fonts/noto-serif-kr/700/04b8bde59cff68eeee74fb1914eb09c7.woff2 +0 -0
  1023. package/src/styles/fonts/noto-serif-kr/700/05ac821472e235943ed1435e4bb8ecad.woff2 +0 -0
  1024. package/src/styles/fonts/noto-serif-kr/700/074c1f8483d5a4d8c45c8c5f3e4cbb32.woff2 +0 -0
  1025. package/src/styles/fonts/noto-serif-kr/700/1380210a722ac9aeef54005ad7b015c9.woff2 +0 -0
  1026. package/src/styles/fonts/noto-serif-kr/700/17c7f5d0a45e92ede0e5dec3a2c77efa.woff2 +0 -0
  1027. package/src/styles/fonts/noto-serif-kr/700/1be602ad456b0d75902d352116cb35fd.woff2 +0 -0
  1028. package/src/styles/fonts/noto-serif-kr/700/2612c60f9dc5459ac42763591240a1d6.woff2 +0 -0
  1029. package/src/styles/fonts/noto-serif-kr/700/2ab8cf1f23a5ac7939a7876054e711a7.woff2 +0 -0
  1030. package/src/styles/fonts/noto-serif-kr/700/34e6750e00c3a911ef87dc66b336594d.woff2 +0 -0
  1031. package/src/styles/fonts/noto-serif-kr/700/36f3df4730cfca4fc77fe52b52e6a126.woff2 +0 -0
  1032. package/src/styles/fonts/noto-serif-kr/700/37887deb7a9c466cf2af6ee7ff372ea6.woff2 +0 -0
  1033. package/src/styles/fonts/noto-serif-kr/700/3aba89c4d4281553078de4622b498cbb.woff2 +0 -0
  1034. package/src/styles/fonts/noto-serif-kr/700/3d88558097559d775231194d43745ba7.woff2 +0 -0
  1035. package/src/styles/fonts/noto-serif-kr/700/3dc3e8c35524cad2d1236f3393104ccf.woff2 +0 -0
  1036. package/src/styles/fonts/noto-serif-kr/700/45140742dfa8686b58db2899ce89f89a.woff2 +0 -0
  1037. package/src/styles/fonts/noto-serif-kr/700/469d2754e315e77270c12fa0ab28026c.woff2 +0 -0
  1038. package/src/styles/fonts/noto-serif-kr/700/4c0760284569b87d8f2290c324953973.woff2 +0 -0
  1039. package/src/styles/fonts/noto-serif-kr/700/4cba92c942694bdc479bc54101bb1aa4.woff2 +0 -0
  1040. package/src/styles/fonts/noto-serif-kr/700/4e7d263c30bb48604069a3b4404d4eeb.woff2 +0 -0
  1041. package/src/styles/fonts/noto-serif-kr/700/5279c7e465d9871edf51fd02e691eee7.woff2 +0 -0
  1042. package/src/styles/fonts/noto-serif-kr/700/54bfdb6f21f9d6191e5100580239e14f.woff2 +0 -0
  1043. package/src/styles/fonts/noto-serif-kr/700/56be5e32fd01668fe5ba814d94905839.woff2 +0 -0
  1044. package/src/styles/fonts/noto-serif-kr/700/615b38e0b5bb3ea68426f44f47706e6f.woff2 +0 -0
  1045. package/src/styles/fonts/noto-serif-kr/700/65e9c4585d71bf48a5c62f367010da16.woff2 +0 -0
  1046. package/src/styles/fonts/noto-serif-kr/700/6b75213eb0be40ce84241eb2bb438a2e.woff2 +0 -0
  1047. package/src/styles/fonts/noto-serif-kr/700/7520ca6ca8c60eb1e62d50e71c8d30f1.woff2 +0 -0
  1048. package/src/styles/fonts/noto-serif-kr/700/759a647b77791b1e98f99bc0ab5317a7.woff2 +0 -0
  1049. package/src/styles/fonts/noto-serif-kr/700/76e51630143b95b6322ef93ad330da7a.woff2 +0 -0
  1050. package/src/styles/fonts/noto-serif-kr/700/79aa7c8c842c4a27cf57c0a3a1ffca5a.woff2 +0 -0
  1051. package/src/styles/fonts/noto-serif-kr/700/7cc72fd2c9105560422b6a67c6162945.woff2 +0 -0
  1052. package/src/styles/fonts/noto-serif-kr/700/7d0ea0690e432462f4d05a23d720ec19.woff2 +0 -0
  1053. package/src/styles/fonts/noto-serif-kr/700/7dffa5c1bec57e0903fd62357401ff1a.woff2 +0 -0
  1054. package/src/styles/fonts/noto-serif-kr/700/82f6ccc063960eed1cdfd1d61ada8862.woff2 +0 -0
  1055. package/src/styles/fonts/noto-serif-kr/700/845b4b67564d62cf5cad242f7ac08ea5.woff2 +0 -0
  1056. package/src/styles/fonts/noto-serif-kr/700/84c8e7bc0931008ed91f44ed12dfea94.woff2 +0 -0
  1057. package/src/styles/fonts/noto-serif-kr/700/887a11290ba78b1e66c6d2f67043005e.woff2 +0 -0
  1058. package/src/styles/fonts/noto-serif-kr/700/88db1d042074fb6e66821ffc10941930.woff2 +0 -0
  1059. package/src/styles/fonts/noto-serif-kr/700/8dc18c0cebe6aa4bf4c45dbb831048ab.woff2 +0 -0
  1060. package/src/styles/fonts/noto-serif-kr/700/919a879bd2d580d8491a31a449390689.woff2 +0 -0
  1061. package/src/styles/fonts/noto-serif-kr/700/91da6cb174bebfb96976e2f1316be2fe.woff2 +0 -0
  1062. package/src/styles/fonts/noto-serif-kr/700/934dfdbed2bb2c4b6129199d699a34fa.woff2 +0 -0
  1063. package/src/styles/fonts/noto-serif-kr/700/97d7a17234f2b5030ae9697ae00aded2.woff2 +0 -0
  1064. package/src/styles/fonts/noto-serif-kr/700/995c4fda62d25c3b79dd5987737df6ae.woff2 +0 -0
  1065. package/src/styles/fonts/noto-serif-kr/700/9d0afa53dc2f92ba42024f55f11f6779.woff2 +0 -0
  1066. package/src/styles/fonts/noto-serif-kr/700/9e13a47242926f1cd7d68c08d9ef8889.woff2 +0 -0
  1067. package/src/styles/fonts/noto-serif-kr/700/a379ada89abd750c587ded29f77e731c.woff2 +0 -0
  1068. package/src/styles/fonts/noto-serif-kr/700/a48be56ce5d3a99dc8f8331398004412.woff2 +0 -0
  1069. package/src/styles/fonts/noto-serif-kr/700/a806759e72987951d6d08b7cbdf36a1b.woff2 +0 -0
  1070. package/src/styles/fonts/noto-serif-kr/700/a88481ec8bc7be11cb66e46781a79bb9.woff2 +0 -0
  1071. package/src/styles/fonts/noto-serif-kr/700/a9a016d4a93409f65278327e8a8bb38d.woff2 +0 -0
  1072. package/src/styles/fonts/noto-serif-kr/700/ab92d41062a7644faa45f50bf384454a.woff2 +0 -0
  1073. package/src/styles/fonts/noto-serif-kr/700/b326a8a9c33b554db570da94f60bc380.woff2 +0 -0
  1074. package/src/styles/fonts/noto-serif-kr/700/b6117ff2993b11bb1fdc7ea3588a010c.woff2 +0 -0
  1075. package/src/styles/fonts/noto-serif-kr/700/b61982951bd51b724143c30dfaaa9fe9.woff2 +0 -0
  1076. package/src/styles/fonts/noto-serif-kr/700/c0098958e20db68cab90097b5e62516f.woff2 +0 -0
  1077. package/src/styles/fonts/noto-serif-kr/700/c4bfaa5e50798246e3770718b7a7c84a.woff2 +0 -0
  1078. package/src/styles/fonts/noto-serif-kr/700/c84598999133455503042e06f4ab79cb.woff2 +0 -0
  1079. package/src/styles/fonts/noto-serif-kr/700/ca9a533988d7019597a60d4e17127e0c.woff2 +0 -0
  1080. package/src/styles/fonts/noto-serif-kr/700/cf2b28f90f47276f7e2688a65e88a101.woff2 +0 -0
  1081. package/src/styles/fonts/noto-serif-kr/700/d689b1861d7e4377dd72ad3013482612.woff2 +0 -0
  1082. package/src/styles/fonts/noto-serif-kr/700/d9047070d72a816b3dba9d40c2d85e69.woff2 +0 -0
  1083. package/src/styles/fonts/noto-serif-kr/700/da04549f3f4ed28076b01b8cd710d313.woff2 +0 -0
  1084. package/src/styles/fonts/noto-serif-kr/700/da7af303f8c645f9a9dbae0e6e32dd35.woff2 +0 -0
  1085. package/src/styles/fonts/noto-serif-kr/700/df9568257eb29b156449fdd4bec5ec76.woff2 +0 -0
  1086. package/src/styles/fonts/noto-serif-kr/700/e067cd0ed76c90cd0a93c9339253f20b.woff2 +0 -0
  1087. package/src/styles/fonts/noto-serif-kr/700/e08b07772e7bed3cec2832d43f7fd339.woff2 +0 -0
  1088. package/src/styles/fonts/noto-serif-kr/700/e12150d5a39b30be8f567968c7a527b0.woff2 +0 -0
  1089. package/src/styles/fonts/noto-serif-kr/700/e53fcb2381eee345db4f6f973dd95a3e.woff2 +0 -0
  1090. package/src/styles/fonts/noto-serif-kr/700/e5bd313ef81f687d398aacb11cec3069.woff2 +0 -0
  1091. package/src/styles/fonts/noto-serif-kr/700/eb5afb3d952b8593782caec6026514b6.woff2 +0 -0
  1092. package/src/styles/fonts/noto-serif-kr/700/edd6a4f608d04fc0351d7688cfc321e4.woff2 +0 -0
  1093. package/src/styles/fonts/noto-serif-kr/700/f9695c6c4df2bf6bc03045ff79d4f01f.woff2 +0 -0
  1094. package/src/styles/fonts/noto-serif-kr/700/fada6eaa68ff8816afe43d2a36c5423e.woff2 +0 -0
  1095. package/src/styles/fonts/noto-serif-kr/700/fbcc4bf5367218951172bdee6f77d7a6.woff2 +0 -0
  1096. package/src/styles/fonts/noto-serif-kr/noto-serif-kr.css +2601 -0
  1097. package/src/styles/site-media.css +823 -0
  1098. package/src/styles/tokens.css +292 -21
  1099. package/src/styles/ui.css +4188 -1857
  1100. package/src/types/app-context.ts +20 -0
  1101. package/src/types/bindings.ts +3 -1
  1102. package/src/types/config.ts +147 -9
  1103. package/src/types/constants.ts +46 -11
  1104. package/src/types/entities.ts +24 -8
  1105. package/src/types/operations.ts +80 -1
  1106. package/src/types/props.ts +10 -9
  1107. package/src/types/raw-assets.d.ts +39 -0
  1108. package/src/types/views.ts +28 -7
  1109. package/src/ui/__tests__/color-themes.test.ts +7 -40
  1110. package/src/ui/__tests__/font-themes.test.ts +30 -11
  1111. package/src/ui/color-themes.ts +311 -270
  1112. package/src/ui/compose/ComposeDialog.tsx +712 -394
  1113. package/src/ui/compose/ComposePrompt.tsx +15 -10
  1114. package/src/ui/dash/ActionButtons.tsx +27 -18
  1115. package/src/ui/dash/DangerZone.tsx +15 -10
  1116. package/src/ui/dash/FormatBadge.tsx +21 -8
  1117. package/src/ui/dash/StatusBadge.tsx +33 -22
  1118. package/src/ui/dash/appearance/AdvancedContent.tsx +44 -30
  1119. package/src/ui/dash/appearance/CodeInjectionContent.tsx +164 -0
  1120. package/src/ui/dash/appearance/ColorThemeContent.tsx +127 -160
  1121. package/src/ui/dash/appearance/FontThemeContent.tsx +20 -15
  1122. package/src/ui/dash/appearance/NavigationContent.tsx +368 -225
  1123. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +54 -0
  1124. package/src/ui/dash/settings/AccountContent.tsx +34 -23
  1125. package/src/ui/dash/settings/AccountMenuContent.tsx +226 -137
  1126. package/src/ui/dash/settings/ApiTokensContent.tsx +122 -79
  1127. package/src/ui/dash/settings/AvatarContent.tsx +73 -45
  1128. package/src/ui/dash/settings/DeleteAccountContent.tsx +106 -66
  1129. package/src/ui/dash/settings/GeneralContent.tsx +274 -157
  1130. package/src/ui/dash/settings/GitHubSyncContent.tsx +558 -0
  1131. package/src/ui/dash/settings/SessionsContent.tsx +79 -38
  1132. package/src/ui/dash/settings/SettingsRootContent.tsx +209 -109
  1133. package/src/ui/dash/settings/__tests__/AccountMenuContent.test.tsx +67 -0
  1134. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +75 -0
  1135. package/src/ui/feed/CuratedThreadPreview.tsx +24 -14
  1136. package/src/ui/feed/LinkCard.tsx +116 -84
  1137. package/src/ui/feed/LinkPreview.tsx +75 -0
  1138. package/src/ui/feed/NoteCard.tsx +60 -16
  1139. package/src/ui/feed/PostStatusBadges.tsx +15 -0
  1140. package/src/ui/feed/QuoteCard.tsx +10 -3
  1141. package/src/ui/feed/ThreadPreview.tsx +62 -36
  1142. package/src/ui/feed/TimelineFeed.tsx +2 -1
  1143. package/src/ui/feed/__tests__/thread-preview.test.ts +220 -11
  1144. package/src/ui/feed/__tests__/timeline-cards.test.ts +186 -5
  1145. package/src/ui/feed/thread-preview-state.ts +23 -10
  1146. package/src/ui/font-themes.ts +57 -97
  1147. package/src/ui/layouts/BaseLayout.tsx +230 -136
  1148. package/src/ui/layouts/SiteLayout.tsx +431 -143
  1149. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +76 -6
  1150. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +76 -0
  1151. package/src/ui/pages/ArchivePage.tsx +320 -207
  1152. package/src/ui/pages/BrandPage.tsx +499 -341
  1153. package/src/ui/pages/CollectionEditorPage.tsx +50 -30
  1154. package/src/ui/pages/CollectionPage.tsx +151 -78
  1155. package/src/ui/pages/CollectionsPage.tsx +27 -18
  1156. package/src/ui/pages/ComposePage.tsx +15 -10
  1157. package/src/ui/pages/FeaturedPage.tsx +16 -11
  1158. package/src/ui/pages/HomePage.tsx +15 -10
  1159. package/src/ui/pages/PostPage.tsx +5 -1
  1160. package/src/ui/pages/SearchPage.tsx +93 -43
  1161. package/src/ui/pages/ThemeSamplePage.tsx +758 -520
  1162. package/src/ui/pages/__tests__/ArchivePage.test.tsx +72 -0
  1163. package/src/ui/shared/CollectionDirectory.tsx +215 -34
  1164. package/src/ui/shared/CollectionsManager.tsx +155 -75
  1165. package/src/ui/shared/DecorativeQuoteMark.tsx +12 -9
  1166. package/src/ui/shared/MediaGallery.tsx +350 -256
  1167. package/src/ui/shared/Pagination.tsx +36 -25
  1168. package/src/ui/shared/PostFooter.tsx +139 -96
  1169. package/src/ui/shared/__tests__/media-gallery.test.ts +27 -0
  1170. package/src/ui/shared/__tests__/navigation-labels.test.ts +79 -28
  1171. package/src/ui/shared/__tests__/post-footer.test.ts +42 -17
  1172. package/src/ui/shared/collection-management-labels.ts +109 -33
  1173. package/src/ui/shared/navigation-labels.ts +78 -8
  1174. package/bin/lib/site-localize-media.js +0 -427
  1175. package/dist/client/_assets/client-auth.js +0 -3260
  1176. package/dist/client/_assets/client.css +0 -2
  1177. package/dist/client/_assets/client.js +0 -380
  1178. package/src/__tests__/site-localize-media.test.ts +0 -150
  1179. package/src/client/components/jant-collection-sidebar.ts +0 -816
  1180. package/src/i18n/locales/en.ts +0 -1
  1181. package/src/i18n/locales/zh-Hans.po +0 -3197
  1182. package/src/i18n/locales/zh-Hans.ts +0 -1
  1183. package/src/i18n/locales/zh-Hant.po +0 -3197
  1184. package/src/i18n/locales/zh-Hant.ts +0 -1
  1185. package/src/routes/feed/rss.ts +0 -216
  1186. package/src/types/lingui-react-macro.d.ts +0 -34
  1187. /package/dist/client/_assets/chunks/{heic-to-XcUDQvtx.js → heic-to-DUUaO23q.js} +0 -0
  1188. /package/dist/client/_assets/chunks/{module-ChVQstFd.js → module-DcsAZQZ_.js} +0 -0
  1189. /package/dist/client/_assets/chunks/{native-CR5HLOyf.js → native-DpcrFAPh.js} +0 -0
  1190. /package/dist/client/_assets/{client-cjk.css → client-cjk-B7Z0snDu.css} +0 -0
  1191. /package/dist/client/_assets/{client-cjk-tc.css → client-cjk-tc-BesJYrb2.css} +0 -0
@@ -9,6 +9,7 @@
9
9
  import {
10
10
  eq,
11
11
  and,
12
+ or,
12
13
  type SQL,
13
14
  type SQLWrapper,
14
15
  isNull,
@@ -18,6 +19,7 @@ import {
18
19
  isNotNull,
19
20
  asc,
20
21
  lte,
22
+ gt,
21
23
  } from "drizzle-orm";
22
24
  import {
23
25
  type Database,
@@ -31,9 +33,10 @@ import {
31
33
  } from "../db/schema-bundle.js";
32
34
  import { createEntityId } from "../lib/ids.js";
33
35
  import { now } from "../lib/time.js";
34
- import { renderTiptapJson } from "../lib/tiptap-render.js";
36
+ import { renderTiptapJson, trimTiptapBody } from "../lib/tiptap-render.js";
35
37
  import { extractSummary, extractBodyText } from "../lib/summary.js";
36
38
  import { markdownToTiptapJson } from "../lib/markdown-to-tiptap.js";
39
+ import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
37
40
  import { generatePostSlug } from "../lib/slug.js";
38
41
  import { getSlugValidationIssue } from "../lib/slug-format.js";
39
42
  import { normalizePath, slugify } from "../lib/url.js";
@@ -64,6 +67,12 @@ import {
64
67
  NotFoundError,
65
68
  } from "../lib/errors.js";
66
69
  import { createPathService, type PathService } from "./path.js";
70
+ import {
71
+ extractYouTubeVideoId,
72
+ getYouTubeThumbnailUrls,
73
+ } from "../lib/youtube.js";
74
+ import { getPreviewStorageKey } from "../lib/upload.js";
75
+ import { generateRandomId } from "../lib/nanoid.js";
67
76
 
68
77
  /** Dependencies for operations that coordinate with other services */
69
78
  export interface PostDeleteDeps {
@@ -139,8 +148,29 @@ export interface CollectionFeedEntry {
139
148
  collectedAt: number;
140
149
  }
141
150
 
151
+ export interface PostBodyContent {
152
+ id: string;
153
+ type: "post";
154
+ format: Format;
155
+ contentFormat: "markdown";
156
+ content: string;
157
+ chars: number;
158
+ }
159
+
160
+ /** Minimal projection used by the sitemap renderer. */
161
+ export interface SitemapPostEntry {
162
+ id: string;
163
+ /** Canonical slug from `path_registry` */
164
+ slug: string;
165
+ /** Primary alias, if the post has one; used in preference to `slug` for URLs */
166
+ alias: string | null;
167
+ updatedAt: number;
168
+ featuredAt: number | null;
169
+ }
170
+
142
171
  export interface PostService {
143
172
  getById(id: string): Promise<Post | null>;
173
+ getBodyContent(id: string): Promise<PostBodyContent | null>;
144
174
  getBySlug(slug: string): Promise<Post | null>;
145
175
  suggestSlug(input: {
146
176
  title?: string;
@@ -149,6 +179,35 @@ export interface PostService {
149
179
  }): Promise<string>;
150
180
  checkSlugAvailability(slug: string, excludePostId?: string): Promise<boolean>;
151
181
  list(filters?: PostFilters): Promise<Post[]>;
182
+ /**
183
+ * List minimal fields needed to render sitemap entries, paginated by `id`
184
+ * (ascending). Excludes replies, private posts, deleted posts, and drafts.
185
+ *
186
+ * Uses keyset pagination on the primary key so old sitemap shards are cheap
187
+ * to serve and stable across shard boundaries: a newly created post always
188
+ * gets a larger TypeID than any previously-committed post, so it lands in
189
+ * the last shard and never rewrites older ones.
190
+ *
191
+ * @param options.afterId Exclusive lower bound on `id`. Omit for the first
192
+ * shard.
193
+ * @param options.limit Maximum rows to return.
194
+ */
195
+ listForSitemap(options: {
196
+ afterId?: string;
197
+ limit: number;
198
+ }): Promise<SitemapPostEntry[]>;
199
+ /** Count posts that qualify for the sitemap (same filters as `listForSitemap`) */
200
+ countForSitemap(): Promise<number>;
201
+ /**
202
+ * Return the id at the given 0-based offset in the sitemap ordering.
203
+ * Used to compute keyset cursors for sharded sitemap endpoints.
204
+ *
205
+ * Returns `null` when the offset is beyond the available rows.
206
+ *
207
+ * Walks the primary-key index with `ORDER BY id ASC LIMIT 1 OFFSET ?` —
208
+ * SQLite/D1 scan only the index for this, not the row data.
209
+ */
210
+ getSitemapIdAt(offset: number): Promise<string | null>;
152
211
  /** Count posts matching filters (ignores cursor, offset, limit) */
153
212
  count(filters?: PostFilters): Promise<number>;
154
213
  /** Count posts matching filters up to a fixed limit (ignores cursor, offset, limit) */
@@ -164,6 +223,24 @@ export interface PostService {
164
223
  deps: PostAttachmentDeps,
165
224
  summaryConfig?: SummaryConfig,
166
225
  ): Promise<Post>;
226
+ /**
227
+ * Atomically create a thread of posts. The first item is the root; each
228
+ * subsequent item is automatically chained as a reply to the previous one.
229
+ * On failure, all already-created posts are rolled back.
230
+ *
231
+ * @param items - Ordered list of (data, attachments) pairs; at least 2 required
232
+ * @param deps - Media/storage dependencies
233
+ * @param summaryConfig - Optional summary extraction config
234
+ * @returns Ordered list of created posts; posts[0] is the root
235
+ */
236
+ createThreadWithAttachments(
237
+ items: Array<{
238
+ data: CreatePost;
239
+ attachments: PostAttachmentInput[] | undefined;
240
+ }>,
241
+ deps: PostAttachmentDeps,
242
+ summaryConfig?: SummaryConfig,
243
+ ): Promise<Post[]>;
167
244
  update(
168
245
  id: string,
169
246
  data: UpdatePost,
@@ -184,6 +261,11 @@ export interface PostService {
184
261
  * @param deps - Media service and optional storage driver for file cleanup
185
262
  */
186
263
  delete(id: string, deps?: PostDeleteDeps): Promise<boolean>;
264
+ /**
265
+ * Delete a thread draft and release its slug paths so they can be reused.
266
+ * Used when replacing a saved thread draft with a new version.
267
+ */
268
+ deleteThreadDraft(id: string, deps?: PostDeleteDeps): Promise<boolean>;
187
269
  getThread(rootId: string): Promise<Post[]>;
188
270
  updateThreadStatusAndVisibility(
189
271
  rootId: string,
@@ -251,6 +333,28 @@ export interface PostService {
251
333
  getDistinctYears(filters?: PostFilters): Promise<number[]>;
252
334
  /** For each thread ID, return the ID of the last published, non-deleted post */
253
335
  getLastPostIdsByThread(threadIds: string[]): Promise<Map<string, string>>;
336
+ /**
337
+ * Rebuild `post.body_text` for a batch of non-deleted posts, cursor-paginated
338
+ * by post id. For each row, recomputes the plain-text extraction via
339
+ * `extractBodyText(body)` and writes it back only when it differs from the
340
+ * stored value. FTS indexes (SQLite trigger / Postgres generated column)
341
+ * refresh automatically on the UPDATE.
342
+ *
343
+ * Idempotent: re-running after a no-op pass returns `updated: 0`.
344
+ *
345
+ * @param options.limit Batch size (1..500, default 50)
346
+ * @param options.cursor Exclusive lower bound on post id; pass the previous
347
+ * response's `nextCursor` to continue
348
+ * @returns processed/updated/skipped counts, the next cursor, and a `done`
349
+ * flag the caller uses to terminate the loop
350
+ */
351
+ reindexBodyText(options?: { limit?: number; cursor?: string }): Promise<{
352
+ processed: number;
353
+ updated: number;
354
+ skipped: number;
355
+ nextCursor: string | null;
356
+ done: boolean;
357
+ }>;
254
358
  }
255
359
 
256
360
  const SLUG_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
@@ -259,24 +363,8 @@ function isValidSlug(value: string): boolean {
259
363
  return SLUG_RE.test(value);
260
364
  }
261
365
 
262
- /** Check if an error (or any of its causes) is a SQLite UNIQUE constraint violation */
263
- function isUniqueConstraintError(err: unknown): boolean {
264
- let current: unknown = err;
265
- while (current) {
266
- const msg = String(current);
267
- if (
268
- msg.includes("UNIQUE constraint") ||
269
- msg.includes("SQLITE_CONSTRAINT")
270
- ) {
271
- return true;
272
- }
273
- current =
274
- current instanceof Error && current.cause !== current
275
- ? current.cause
276
- : undefined;
277
- }
278
- return false;
279
- }
366
+ // Re-export shared constraint detection see db/dialect.ts
367
+ import { isUniqueConstraintError } from "../db/dialect.js";
280
368
 
281
369
  function hasNonEmptyText(value: string | null | undefined): boolean {
282
370
  return typeof value === "string" && value.trim().length > 0;
@@ -349,8 +437,10 @@ function ensurePostRating(
349
437
  function assertPostFormatShape(data: {
350
438
  format: Format;
351
439
  title?: string | null;
440
+ body?: unknown;
352
441
  url?: string | null;
353
442
  quoteText?: string | null;
443
+ hasAttachments?: boolean;
354
444
  }): void {
355
445
  const hasTitle = hasNonEmptyText(data.title);
356
446
  const hasUrl = hasNonEmptyText(data.url);
@@ -363,6 +453,18 @@ function assertPostFormatShape(data: {
363
453
  if (hasQuoteText) {
364
454
  throw new ValidationError("Notes can't include quoted text.");
365
455
  }
456
+ // hasAttachments === undefined means unknown (e.g. update without
457
+ // attachment changes) — skip the empty-content check in that case.
458
+ if (
459
+ !hasTitle &&
460
+ !data.body &&
461
+ data.hasAttachments !== undefined &&
462
+ !data.hasAttachments
463
+ ) {
464
+ throw new ValidationError(
465
+ "Notes need a title, body, or at least one attachment.",
466
+ );
467
+ }
366
468
  return;
367
469
  }
368
470
 
@@ -497,6 +599,87 @@ export function createPostService(
497
599
  return [...new Set(collectionIds)];
498
600
  }
499
601
 
602
+ /**
603
+ * Fetch a link preview thumbnail and store it.
604
+ * Silently returns without updating if the URL is not a recognized provider
605
+ * or if the fetch fails.
606
+ */
607
+ async function resolveAndStorePreview(
608
+ postId: string,
609
+ format: string,
610
+ url: string | null | undefined,
611
+ storage: StorageDriver | null | undefined,
612
+ ): Promise<void> {
613
+ if (!storage || format !== "link" || !url) return;
614
+
615
+ const videoId = extractYouTubeVideoId(url);
616
+ if (!videoId) return;
617
+
618
+ const thumbnailUrls = getYouTubeThumbnailUrls(videoId);
619
+ let imageBytes: Uint8Array | null = null;
620
+ let contentType = "image/jpeg";
621
+
622
+ for (const thumbUrl of thumbnailUrls) {
623
+ try {
624
+ const response = await fetch(thumbUrl);
625
+ if (response.ok) {
626
+ const ct = response.headers.get("content-type");
627
+ // YouTube returns a grey placeholder (120×90) when maxresdefault
628
+ // is not available. Detect it by checking content-length.
629
+ const cl = response.headers.get("content-length");
630
+ if (cl && parseInt(cl, 10) < 5000) {
631
+ // Too small — likely a placeholder, try next quality
632
+ continue;
633
+ }
634
+ imageBytes = new Uint8Array(await response.arrayBuffer());
635
+ if (ct) contentType = ct;
636
+ break;
637
+ }
638
+ } catch {
639
+ // Network failure — try next URL
640
+ }
641
+ }
642
+
643
+ if (!imageBytes) return;
644
+
645
+ const ext = contentType.includes("webp")
646
+ ? "webp"
647
+ : contentType.includes("png")
648
+ ? "png"
649
+ : "jpg";
650
+ const suffix = generateRandomId(8);
651
+ const storageKey = getPreviewStorageKey(siteId, postId, suffix, ext);
652
+
653
+ try {
654
+ await storage.put(storageKey, imageBytes, { contentType });
655
+ await db
656
+ .update(posts)
657
+ .set({
658
+ previewImageKey: storageKey,
659
+ previewKind: "video",
660
+ previewProvider: "youtube",
661
+ })
662
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, postId)));
663
+ } catch {
664
+ // Storage or DB failure — preview is best-effort
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Delete a preview image from storage if it exists.
670
+ */
671
+ async function deletePreviewImage(
672
+ previewImageKey: string | null | undefined,
673
+ storage: StorageDriver | null | undefined,
674
+ ): Promise<void> {
675
+ if (!storage || !previewImageKey) return;
676
+ try {
677
+ await storage.delete(previewImageKey);
678
+ } catch {
679
+ // Best-effort cleanup
680
+ }
681
+ }
682
+
500
683
  function buildCollectionMembershipCondition(
501
684
  collectionIds: readonly string[],
502
685
  ): SQL<unknown> {
@@ -631,6 +814,25 @@ export function createPostService(
631
814
  return conditions;
632
815
  }
633
816
 
817
+ async function getLastLivePostIdInThread(
818
+ threadId: string,
819
+ ): Promise<string | null> {
820
+ const rows = await db
821
+ .select({ id: posts.id })
822
+ .from(posts)
823
+ .where(
824
+ and(
825
+ eq(posts.siteId, siteId),
826
+ eq(posts.threadId, threadId),
827
+ isNull(posts.deletedAt),
828
+ ),
829
+ )
830
+ .orderBy(desc(posts.createdAt), desc(posts.id))
831
+ .limit(1);
832
+
833
+ return rows[0]?.id ?? null;
834
+ }
835
+
634
836
  function getCursorSortTimestamp(row: typeof posts.$inferSelect): number {
635
837
  return row.status === "draft" ? row.updatedAt : (row.lastActivityAt ?? -1);
636
838
  }
@@ -790,6 +992,9 @@ export function createPostService(
790
992
  quoteText: row.quoteText,
791
993
  summary: row.summary,
792
994
  rating: ensurePostRating(row.rating, Error),
995
+ previewImageKey: row.previewImageKey,
996
+ previewKind: row.previewKind,
997
+ previewProvider: row.previewProvider,
793
998
  replyToId: row.replyToId,
794
999
  threadId: row.threadId,
795
1000
  deletedAt: row.deletedAt,
@@ -981,6 +1186,24 @@ export function createPostService(
981
1186
  return rows.map((row) => row.collectionId);
982
1187
  }
983
1188
 
1189
+ async function getPostCollectionTimestamps(
1190
+ postId: string,
1191
+ ): Promise<Map<string, number>> {
1192
+ const rows = await db
1193
+ .select({
1194
+ collectionId: postCollections.collectionId,
1195
+ createdAt: postCollections.createdAt,
1196
+ })
1197
+ .from(postCollections)
1198
+ .where(
1199
+ and(
1200
+ eq(postCollections.siteId, siteId),
1201
+ eq(postCollections.postId, postId),
1202
+ ),
1203
+ );
1204
+ return new Map(rows.map((r) => [r.collectionId, r.createdAt]));
1205
+ }
1206
+
984
1207
  function buildRollbackUpdate(
985
1208
  post: Post,
986
1209
  collectionIds: string[],
@@ -1018,6 +1241,20 @@ export function createPostService(
1018
1241
  return hydratePost(result[0]);
1019
1242
  },
1020
1243
 
1244
+ async getBodyContent(id) {
1245
+ const post = await this.getById(id);
1246
+ if (!post) return null;
1247
+
1248
+ return {
1249
+ id: post.id,
1250
+ type: "post",
1251
+ format: post.format,
1252
+ contentFormat: "markdown",
1253
+ content: post.body ? tiptapJsonToMarkdown(post.body) : "",
1254
+ chars: post.bodyText?.length ?? 0,
1255
+ };
1256
+ },
1257
+
1021
1258
  async getBySlug(slug) {
1022
1259
  const resolved = await resolvedPaths.resolve(slug);
1023
1260
  if (!resolved || resolved.kind !== "slug" || !resolved.postId) {
@@ -1073,6 +1310,14 @@ export function createPostService(
1073
1310
  ELSE 1
1074
1311
  END`;
1075
1312
 
1313
+ // NULL-sort order differs between dialects: SQLite puts NULLs last for
1314
+ // DESC, Postgres puts them first. Wrap nullable sort keys in COALESCE
1315
+ // so pinned/featured posts land on top under both engines. Mirrors the
1316
+ // expressions used by `buildListCursorCondition` above.
1317
+ const pinnedSortExpr = sql<number>`coalesce(${posts.pinnedAt}, -1)`;
1318
+ const featuredSortExpr = sql<number>`coalesce(${posts.featuredAt}, -1)`;
1319
+ const sortTimestampSortExpr = sql<number>`coalesce(${sortTimestamp}, -1)`;
1320
+
1076
1321
  const baseQuery = db
1077
1322
  .select()
1078
1323
  .from(posts)
@@ -1082,29 +1327,31 @@ export function createPostService(
1082
1327
  let query =
1083
1328
  filters.featured || filters.sortOrder === undefined
1084
1329
  ? baseQuery.orderBy(
1085
- desc(posts.pinnedAt),
1086
- filters.featured ? desc(posts.featuredAt) : desc(sortTimestamp),
1330
+ desc(pinnedSortExpr),
1331
+ filters.featured
1332
+ ? desc(featuredSortExpr)
1333
+ : desc(sortTimestampSortExpr),
1087
1334
  desc(posts.id),
1088
1335
  )
1089
1336
  : filters.sortOrder === "oldest"
1090
1337
  ? baseQuery.orderBy(
1091
- desc(posts.pinnedAt),
1092
- asc(sortTimestamp),
1338
+ desc(pinnedSortExpr),
1339
+ asc(sortTimestampSortExpr),
1093
1340
  asc(posts.id),
1094
1341
  )
1095
1342
  : filters.sortOrder === "rating_desc"
1096
1343
  ? baseQuery.orderBy(
1097
- desc(posts.pinnedAt),
1344
+ desc(pinnedSortExpr),
1098
1345
  desc(ratingPresence),
1099
1346
  desc(posts.rating),
1100
- desc(sortTimestamp),
1347
+ desc(sortTimestampSortExpr),
1101
1348
  desc(posts.id),
1102
1349
  )
1103
1350
  : baseQuery.orderBy(
1104
- desc(posts.pinnedAt),
1351
+ desc(pinnedSortExpr),
1105
1352
  desc(ratingPresence),
1106
1353
  asc(posts.rating),
1107
- desc(sortTimestamp),
1354
+ desc(sortTimestampSortExpr),
1108
1355
  desc(posts.id),
1109
1356
  );
1110
1357
 
@@ -1116,11 +1363,88 @@ export function createPostService(
1116
1363
  return hydratePosts(rows);
1117
1364
  },
1118
1365
 
1366
+ async listForSitemap({ afterId, limit }) {
1367
+ // Share the filter conditions with `list()` so visibility/reply/deleted
1368
+ // semantics stay consistent if they ever change.
1369
+ const conditions = buildFilterConditions({
1370
+ status: "published",
1371
+ excludePrivate: true,
1372
+ excludeReplies: true,
1373
+ });
1374
+ if (afterId !== undefined) {
1375
+ conditions.push(sql`${posts.id} > ${afterId}`);
1376
+ }
1377
+
1378
+ const rows = await db
1379
+ .select({
1380
+ id: posts.id,
1381
+ updatedAt: posts.updatedAt,
1382
+ featuredAt: posts.featuredAt,
1383
+ })
1384
+ .from(posts)
1385
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
1386
+ .orderBy(asc(posts.id))
1387
+ .limit(limit);
1388
+
1389
+ if (rows.length === 0) return [];
1390
+
1391
+ const ids = rows.map((row) => row.id);
1392
+ const [slugMap, aliasesMap] = await Promise.all([
1393
+ resolvedPaths.getPostSlugMap(ids),
1394
+ resolvedPaths.getPostAliases(ids),
1395
+ ]);
1396
+
1397
+ return rows
1398
+ .map((row): SitemapPostEntry | null => {
1399
+ const slug = slugMap.get(row.id);
1400
+ if (!slug) return null;
1401
+ const alias = aliasesMap.get(row.id)?.[0] ?? null;
1402
+ return {
1403
+ id: row.id,
1404
+ slug,
1405
+ alias,
1406
+ updatedAt: row.updatedAt,
1407
+ featuredAt: row.featuredAt,
1408
+ };
1409
+ })
1410
+ .filter((entry): entry is SitemapPostEntry => entry !== null);
1411
+ },
1412
+
1413
+ async countForSitemap() {
1414
+ const conditions = buildFilterConditions({
1415
+ status: "published",
1416
+ excludePrivate: true,
1417
+ excludeReplies: true,
1418
+ });
1419
+ const result = await db
1420
+ .select({ count: sql<number>`CAST(count(*) AS INTEGER)`.as("count") })
1421
+ .from(posts)
1422
+ .where(conditions.length > 0 ? and(...conditions) : undefined);
1423
+ return result[0]?.count ?? 0;
1424
+ },
1425
+
1426
+ async getSitemapIdAt(offset) {
1427
+ if (offset < 0) return null;
1428
+ const conditions = buildFilterConditions({
1429
+ status: "published",
1430
+ excludePrivate: true,
1431
+ excludeReplies: true,
1432
+ });
1433
+ const rows = await db
1434
+ .select({ id: posts.id })
1435
+ .from(posts)
1436
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
1437
+ .orderBy(asc(posts.id))
1438
+ .limit(1)
1439
+ .offset(offset);
1440
+ return rows[0]?.id ?? null;
1441
+ },
1442
+
1119
1443
  async count(filters = {}) {
1120
1444
  const conditions = buildFilterConditions(filters);
1121
1445
 
1122
1446
  const result = await db
1123
- .select({ count: sql<number>`count(*)`.as("count") })
1447
+ .select({ count: sql<number>`CAST(count(*) AS INTEGER)`.as("count") })
1124
1448
  .from(posts)
1125
1449
  .where(conditions.length > 0 ? and(...conditions) : undefined);
1126
1450
 
@@ -1153,7 +1477,7 @@ export function createPostService(
1153
1477
  return db
1154
1478
  .select({
1155
1479
  yearMonth: publishedYearMonthExpr.as("year_month"),
1156
- count: sql<number>`count(*)`.as("count"),
1480
+ count: sql<number>`CAST(count(*) AS INTEGER)`.as("count"),
1157
1481
  })
1158
1482
  .from(posts)
1159
1483
  .where(conditions.length > 0 ? and(...conditions) : undefined)
@@ -1173,22 +1497,33 @@ export function createPostService(
1173
1497
  : undefined;
1174
1498
  const rating = ensurePostRating(data.rating);
1175
1499
 
1500
+ const rawBody = data.bodyMarkdown
1501
+ ? markdownToTiptapJson(data.bodyMarkdown)
1502
+ : (data.body ?? null);
1503
+ const body = rawBody ? trimTiptapBody(rawBody) : null;
1504
+ const title = data.title?.trim() || null;
1505
+ const quoteText = data.quoteText?.trim() || null;
1506
+ const url = data.url?.trim() || null;
1507
+
1176
1508
  assertPostFormatShape({
1177
1509
  format,
1178
- title: data.title,
1179
- url: data.url,
1180
- quoteText: data.quoteText,
1510
+ title,
1511
+ body,
1512
+ url,
1513
+ quoteText,
1514
+ hasAttachments: data.attachments
1515
+ ? data.attachments.length > 0
1516
+ : undefined,
1181
1517
  });
1182
1518
 
1183
- const body = data.bodyMarkdown
1184
- ? markdownToTiptapJson(data.bodyMarkdown)
1185
- : (data.body ?? null);
1186
1519
  const bodyHtml = body ? renderTiptapJson(body) : null;
1187
- const bodyText = body ? extractBodyText(body) : null;
1520
+ const bodyText = body
1521
+ ? extractBodyText(body, { includeLinkHrefs: true })
1522
+ : null;
1188
1523
 
1189
1524
  // Generate summary for titled notes with body content
1190
1525
  let summary: string | null = null;
1191
- if (format === "note" && data.title && body && summaryConfig) {
1526
+ if (format === "note" && title && body && summaryConfig) {
1192
1527
  summary = extractSummary(
1193
1528
  body,
1194
1529
  summaryConfig.maxParagraphs,
@@ -1201,18 +1536,45 @@ export function createPostService(
1201
1536
  let status: Status = requestedStatus ?? "published";
1202
1537
  let visibility: Visibility | null = requestedVisibility ?? "public";
1203
1538
 
1539
+ // Collapse the two-input (`flag` + `flagAt`) pair the DTO exposes
1540
+ // into the single timestamp form the DB column stores. Explicit
1541
+ // `*At` wins over the boolean shorthand when both are set.
1542
+ const resolvedPinnedAt =
1543
+ data.pinnedAt !== undefined
1544
+ ? data.pinnedAt
1545
+ : data.pinned !== undefined
1546
+ ? data.pinned
1547
+ ? timestamp
1548
+ : null
1549
+ : null;
1550
+ const resolvedFeaturedAt =
1551
+ data.featuredAt !== undefined
1552
+ ? data.featuredAt
1553
+ : data.featured !== undefined
1554
+ ? data.featured
1555
+ ? timestamp
1556
+ : null
1557
+ : null;
1558
+
1204
1559
  if (data.replyToId) {
1205
1560
  const parent = await this.getById(data.replyToId);
1206
1561
  if (!parent) {
1207
1562
  throw new NotFoundError("Parent post");
1208
1563
  }
1209
1564
 
1210
- if (data.pinned) {
1565
+ if (resolvedPinnedAt !== null) {
1211
1566
  throw new ConflictError(
1212
1567
  "Cannot pin a thread reply. Pin the root post instead.",
1213
1568
  );
1214
1569
  }
1215
1570
 
1571
+ const lastLivePostId = await getLastLivePostIdInThread(parent.threadId);
1572
+ if (lastLivePostId && lastLivePostId !== parent.id) {
1573
+ throw new ConflictError(
1574
+ "This post is no longer the end of the thread. Reply to the latest post instead.",
1575
+ );
1576
+ }
1577
+
1216
1578
  threadId = parent.threadId;
1217
1579
 
1218
1580
  // Replies inherit visibility from the root at read time.
@@ -1236,6 +1598,14 @@ export function createPostService(
1236
1598
  let slug: string;
1237
1599
  let aliasPath: string | null = null;
1238
1600
 
1601
+ // Quote `title` stores source attribution (e.g. author name), not a real
1602
+ // title — deriving the slug from it would produce URLs like `/basho`
1603
+ // that read as "a page about Basho" and collide across every quote from
1604
+ // the same source. Fall through to random IDs instead; users who want a
1605
+ // readable slug can still set one explicitly.
1606
+ const titleForSlug =
1607
+ format === "quote" ? undefined : (title ?? undefined);
1608
+
1239
1609
  if (data.path) {
1240
1610
  const normalized = normalizePath(data.path);
1241
1611
  if (isValidSlug(normalized)) {
@@ -1250,7 +1620,7 @@ export function createPostService(
1250
1620
  const slugified = slugify(normalized);
1251
1621
  slug = await generatePostSlug({
1252
1622
  slug: slugified || undefined,
1253
- title: data.title,
1623
+ title: titleForSlug,
1254
1624
  idLength: config.slugIdLength,
1255
1625
  isAvailable: isSlugAvailable,
1256
1626
  });
@@ -1263,13 +1633,61 @@ export function createPostService(
1263
1633
  } else {
1264
1634
  slug = await generatePostSlug({
1265
1635
  slug: data.slug,
1266
- title: data.title,
1636
+ title: titleForSlug,
1267
1637
  idLength: config.slugIdLength,
1268
1638
  isAvailable: isSlugAvailable,
1269
1639
  });
1270
1640
  }
1271
1641
 
1272
- const collectionIds = [...new Set(data.collectionIds ?? [])];
1642
+ // When structured `collectionEntries` are provided (Hugo import path),
1643
+ // they win over the bare `collectionIds` slug list and carry
1644
+ // `createdAt` / `position` / `pinnedAt` per row. Otherwise fall back to
1645
+ // the simple list and derive those fields from sensible defaults.
1646
+ const hasCollectionEntries =
1647
+ data.collectionEntries !== undefined &&
1648
+ data.collectionEntries.length > 0;
1649
+ const collectionInsertRows: {
1650
+ siteId: string;
1651
+ postId: string;
1652
+ collectionId: string;
1653
+ createdAt: number;
1654
+ position: number;
1655
+ pinnedAt: number | null;
1656
+ }[] = hasCollectionEntries
1657
+ ? (() => {
1658
+ const seen = new Set<string>();
1659
+ const rows: {
1660
+ siteId: string;
1661
+ postId: string;
1662
+ collectionId: string;
1663
+ createdAt: number;
1664
+ position: number;
1665
+ pinnedAt: number | null;
1666
+ }[] = [];
1667
+ let fallbackPosition = 0;
1668
+ for (const entry of data.collectionEntries ?? []) {
1669
+ if (seen.has(entry.collectionId)) continue;
1670
+ seen.add(entry.collectionId);
1671
+ rows.push({
1672
+ siteId,
1673
+ postId: id,
1674
+ collectionId: entry.collectionId,
1675
+ createdAt: entry.createdAt ?? timestamp,
1676
+ position: entry.position ?? fallbackPosition,
1677
+ pinnedAt: entry.pinnedAt ?? null,
1678
+ });
1679
+ fallbackPosition++;
1680
+ }
1681
+ return rows;
1682
+ })()
1683
+ : [...new Set(data.collectionIds ?? [])].map((collectionId, index) => ({
1684
+ siteId,
1685
+ postId: id,
1686
+ collectionId,
1687
+ createdAt: timestamp,
1688
+ position: index,
1689
+ pinnedAt: null,
1690
+ }));
1273
1691
 
1274
1692
  try {
1275
1693
  if (usesBatchWrites) {
@@ -1282,14 +1700,14 @@ export function createPostService(
1282
1700
  format,
1283
1701
  status,
1284
1702
  visibility,
1285
- pinnedAt: data.pinned ? timestamp : null,
1286
- featuredAt: data.featured ? timestamp : null,
1287
- title: data.title ?? null,
1288
- url: data.url ?? null,
1289
- body: body ?? null,
1703
+ pinnedAt: resolvedPinnedAt,
1704
+ featuredAt: resolvedFeaturedAt,
1705
+ title,
1706
+ url,
1707
+ body,
1290
1708
  bodyHtml,
1291
1709
  bodyText,
1292
- quoteText: data.quoteText ?? null,
1710
+ quoteText,
1293
1711
  summary,
1294
1712
  rating,
1295
1713
  replyToId: data.replyToId ?? null,
@@ -1333,16 +1751,9 @@ export function createPostService(
1333
1751
  );
1334
1752
  }
1335
1753
 
1336
- if (collectionIds.length > 0) {
1754
+ if (collectionInsertRows.length > 0) {
1337
1755
  writeQueries.push(
1338
- db.insert(postCollections).values(
1339
- collectionIds.map((collectionId) => ({
1340
- siteId,
1341
- postId: id,
1342
- collectionId,
1343
- createdAt: timestamp,
1344
- })),
1345
- ),
1756
+ db.insert(postCollections).values(collectionInsertRows),
1346
1757
  );
1347
1758
  }
1348
1759
 
@@ -1360,14 +1771,14 @@ export function createPostService(
1360
1771
  format,
1361
1772
  status,
1362
1773
  visibility,
1363
- pinnedAt: data.pinned ? timestamp : null,
1364
- featuredAt: data.featured ? timestamp : null,
1365
- title: data.title ?? null,
1366
- url: data.url ?? null,
1367
- body: body ?? null,
1774
+ pinnedAt: resolvedPinnedAt,
1775
+ featuredAt: resolvedFeaturedAt,
1776
+ title,
1777
+ url,
1778
+ body,
1368
1779
  bodyHtml,
1369
1780
  bodyText,
1370
- quoteText: data.quoteText ?? null,
1781
+ quoteText,
1371
1782
  summary,
1372
1783
  rating,
1373
1784
  replyToId: data.replyToId ?? null,
@@ -1406,15 +1817,8 @@ export function createPostService(
1406
1817
  });
1407
1818
  }
1408
1819
 
1409
- if (collectionIds.length > 0) {
1410
- await tx.insert(postCollections).values(
1411
- collectionIds.map((collectionId) => ({
1412
- siteId,
1413
- postId: id,
1414
- collectionId,
1415
- createdAt: timestamp,
1416
- })),
1417
- );
1820
+ if (collectionInsertRows.length > 0) {
1821
+ await tx.insert(postCollections).values(collectionInsertRows);
1418
1822
  }
1419
1823
  });
1420
1824
  }
@@ -1434,7 +1838,7 @@ export function createPostService(
1434
1838
  }
1435
1839
 
1436
1840
  // Bump thread root's lastActivityAt when creating a published reply
1437
- if (data.replyToId && status === "published") {
1841
+ if (data.replyToId && status === "published" && !data.quietReply) {
1438
1842
  await recalculateThreadLastActivity(threadId);
1439
1843
  }
1440
1844
 
@@ -1447,13 +1851,25 @@ export function createPostService(
1447
1851
  await createAttachmentMediaIds(attachmentInputs, deps);
1448
1852
 
1449
1853
  try {
1450
- const post = await this.create(data, summaryConfig);
1854
+ const post = await this.create(
1855
+ { ...data, attachments: attachmentInputs },
1856
+ summaryConfig,
1857
+ );
1451
1858
 
1452
1859
  try {
1453
1860
  if (orderedMediaIds.length > 0) {
1454
1861
  await deps.media.attachToPost(post.id, orderedMediaIds);
1455
1862
  }
1456
1863
  await applyAttachmentAltUpdates(attachmentInputs, deps);
1864
+
1865
+ // Best-effort: fetch and store link preview thumbnail
1866
+ await resolveAndStorePreview(
1867
+ post.id,
1868
+ post.format,
1869
+ post.url,
1870
+ deps.storage,
1871
+ );
1872
+
1457
1873
  return post;
1458
1874
  } catch (error) {
1459
1875
  await deps.media.attachToPost(post.id, []).catch(() => undefined);
@@ -1470,6 +1886,47 @@ export function createPostService(
1470
1886
  }
1471
1887
  },
1472
1888
 
1889
+ async createThreadWithAttachments(items, deps, summaryConfig) {
1890
+ if (items.length < 2) {
1891
+ throw new ValidationError("A thread requires at least 2 posts.");
1892
+ }
1893
+
1894
+ const created: Post[] = [];
1895
+
1896
+ for (let i = 0; i < items.length; i++) {
1897
+ const item = items[i];
1898
+ if (!item) continue;
1899
+ const { data, attachments } = item;
1900
+ const prevPost = created[i - 1];
1901
+ const postData: CreatePost = {
1902
+ ...data,
1903
+ // Chain each post as a reply to the previous one (server-side chaining)
1904
+ replyToId: i === 0 ? data.replyToId : prevPost?.id,
1905
+ };
1906
+
1907
+ try {
1908
+ const post = await this.createWithAttachments(
1909
+ postData,
1910
+ attachments,
1911
+ deps,
1912
+ summaryConfig,
1913
+ );
1914
+ created.push(post);
1915
+ } catch (error) {
1916
+ // Rollback: delete all already-created posts in reverse order
1917
+ for (const p of [...created].reverse()) {
1918
+ await this.delete(p.id, {
1919
+ media: deps.media,
1920
+ storage: deps.storage,
1921
+ }).catch(() => undefined);
1922
+ }
1923
+ throw error;
1924
+ }
1925
+ }
1926
+
1927
+ return created;
1928
+ },
1929
+
1473
1930
  async update(id, data, summaryConfig) {
1474
1931
  const existing = await this.getById(id);
1475
1932
  if (!existing) return null;
@@ -1479,9 +1936,12 @@ export function createPostService(
1479
1936
  data.format !== undefined
1480
1937
  ? ensurePostFormat(data.format)
1481
1938
  : existing.format;
1482
- const nextUrl = data.url !== undefined ? data.url : existing.url;
1939
+ const nextUrl =
1940
+ data.url !== undefined ? data.url?.trim() || null : existing.url;
1483
1941
  const nextQuoteText =
1484
- data.quoteText !== undefined ? data.quoteText : existing.quoteText;
1942
+ data.quoteText !== undefined
1943
+ ? data.quoteText?.trim() || null
1944
+ : existing.quoteText;
1485
1945
  const nextStatus =
1486
1946
  data.status !== undefined
1487
1947
  ? ensurePostStatus(data.status)
@@ -1493,11 +1953,26 @@ export function createPostService(
1493
1953
  const nextRating =
1494
1954
  data.rating !== undefined ? ensurePostRating(data.rating) : undefined;
1495
1955
 
1956
+ const nextTitle =
1957
+ data.title !== undefined ? data.title?.trim() || null : existing.title;
1958
+
1496
1959
  assertPostFormatShape({
1497
1960
  format: nextFormat,
1498
- title: data.title !== undefined ? data.title : existing.title,
1961
+ title: nextTitle,
1962
+ body:
1963
+ data.body !== undefined || data.bodyMarkdown !== undefined
1964
+ ? data.bodyMarkdown
1965
+ ? markdownToTiptapJson(data.bodyMarkdown)
1966
+ : (data.body ?? null)
1967
+ : existing.body,
1499
1968
  url: nextUrl,
1500
1969
  quoteText: nextQuoteText,
1970
+ // During update we can't cheaply resolve final attachment state;
1971
+ // existing posts already passed creation validation, so we only
1972
+ // guard against clearing all text fields here.
1973
+ hasAttachments: data.attachments
1974
+ ? data.attachments.length > 0
1975
+ : undefined,
1501
1976
  });
1502
1977
  assertDraftPublishedAt(nextStatus, data.publishedAt);
1503
1978
 
@@ -1520,32 +1995,40 @@ export function createPostService(
1520
1995
  }
1521
1996
 
1522
1997
  if (data.format !== undefined) updates.format = nextFormat;
1523
- if (data.title !== undefined) updates.title = data.title;
1524
- if (data.url !== undefined) updates.url = data.url;
1525
- if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
1998
+ if (data.title !== undefined) updates.title = nextTitle;
1999
+ if (data.url !== undefined) updates.url = nextUrl;
2000
+ if (data.quoteText !== undefined) updates.quoteText = nextQuoteText;
1526
2001
  if (data.rating !== undefined) updates.rating = nextRating;
1527
- if (data.pinned !== undefined)
1528
- updates.pinnedAt = data.pinned ? now() : null;
1529
- if (data.featured !== undefined)
1530
- updates.featuredAt = data.featured ? now() : null;
2002
+ // Prefer explicit timestamp when provided; fall back to boolean shorthand.
2003
+ if (data.pinnedAt !== undefined) {
2004
+ updates.pinnedAt = data.pinnedAt;
2005
+ } else if (data.pinned !== undefined) {
2006
+ updates.pinnedAt = data.pinned ? timestamp : null;
2007
+ }
2008
+ if (data.featuredAt !== undefined) {
2009
+ updates.featuredAt = data.featuredAt;
2010
+ } else if (data.featured !== undefined) {
2011
+ updates.featuredAt = data.featured ? timestamp : null;
2012
+ }
1531
2013
 
1532
2014
  if (data.body !== undefined || data.bodyMarkdown !== undefined) {
1533
- const normalizedBody = data.bodyMarkdown
2015
+ const rawBody = data.bodyMarkdown
1534
2016
  ? markdownToTiptapJson(data.bodyMarkdown)
1535
2017
  : (data.body ?? null);
2018
+ const normalizedBody = rawBody ? trimTiptapBody(rawBody) : null;
1536
2019
  updates.body = normalizedBody;
1537
2020
  updates.bodyHtml = normalizedBody
1538
2021
  ? renderTiptapJson(normalizedBody)
1539
2022
  : null;
1540
2023
  updates.bodyText = normalizedBody
1541
- ? extractBodyText(normalizedBody)
2024
+ ? extractBodyText(normalizedBody, { includeLinkHrefs: true })
1542
2025
  : null;
1543
2026
  }
1544
2027
 
1545
2028
  // Recompute summary when body, title, or format change
1546
2029
  if (summaryConfig) {
1547
2030
  const format = nextFormat;
1548
- const title = data.title !== undefined ? data.title : existing.title;
2031
+ const title = nextTitle;
1549
2032
  const body =
1550
2033
  data.bodyMarkdown !== undefined
1551
2034
  ? data.bodyMarkdown
@@ -1572,7 +2055,10 @@ export function createPostService(
1572
2055
  "Cannot change visibility of a thread reply. Update the root post instead.",
1573
2056
  );
1574
2057
  }
1575
- if (data.pinned !== undefined) {
2058
+ if (
2059
+ (data.pinnedAt !== undefined && data.pinnedAt !== null) ||
2060
+ data.pinned === true
2061
+ ) {
1576
2062
  throw new ConflictError(
1577
2063
  "Cannot pin a thread reply. Pin the root post instead.",
1578
2064
  );
@@ -1631,9 +2117,9 @@ export function createPostService(
1631
2117
  }
1632
2118
 
1633
2119
  // Complex case: cascade + update + collection sync atomically
1634
- const existingCollectionIds = needsCollectionSync
1635
- ? await getCollectionIdsForPost(id)
1636
- : [];
2120
+ const existingCollectionTimestamps = needsCollectionSync
2121
+ ? await getPostCollectionTimestamps(id)
2122
+ : new Map<string, number>();
1637
2123
  let updateResult: (typeof posts.$inferSelect)[] | undefined;
1638
2124
 
1639
2125
  if (usesBatchWrites) {
@@ -1688,38 +2174,30 @@ export function createPostService(
1688
2174
  );
1689
2175
 
1690
2176
  if (needsCollectionSync) {
1691
- const existingIds = new Set(existingCollectionIds);
1692
- const nextIds = new Set(nextCollectionIds);
1693
- const removedIds = existingCollectionIds.filter(
1694
- (cid) => !nextIds.has(cid),
1695
- );
1696
- const addedIds = nextCollectionIds.filter(
1697
- (cid) => !existingIds.has(cid),
1698
- );
1699
-
1700
- if (removedIds.length > 0) {
1701
- writeQueries.push(
1702
- db
1703
- .delete(postCollections)
1704
- .where(
1705
- and(
1706
- eq(postCollections.siteId, siteId),
1707
- eq(postCollections.postId, id),
1708
- inArray(postCollections.collectionId, removedIds),
1709
- ),
2177
+ // Delete all and re-insert to preserve user-specified ordering
2178
+ writeQueries.push(
2179
+ db
2180
+ .delete(postCollections)
2181
+ .where(
2182
+ and(
2183
+ eq(postCollections.siteId, siteId),
2184
+ eq(postCollections.postId, id),
1710
2185
  ),
1711
- );
1712
- }
2186
+ ),
2187
+ );
1713
2188
 
1714
- if (addedIds.length > 0) {
2189
+ if (nextCollectionIds.length > 0) {
1715
2190
  const collectionTimestamp = now();
1716
2191
  writeQueries.push(
1717
2192
  db.insert(postCollections).values(
1718
- addedIds.map((collectionId) => ({
2193
+ nextCollectionIds.map((collectionId, index) => ({
1719
2194
  siteId,
1720
2195
  postId: id,
1721
2196
  collectionId,
1722
- createdAt: collectionTimestamp,
2197
+ createdAt:
2198
+ existingCollectionTimestamps.get(collectionId) ??
2199
+ collectionTimestamp,
2200
+ position: index,
1723
2201
  })),
1724
2202
  ),
1725
2203
  );
@@ -1779,35 +2257,27 @@ export function createPostService(
1779
2257
  .returning();
1780
2258
 
1781
2259
  if (needsCollectionSync) {
1782
- const existingIds = new Set(existingCollectionIds);
1783
- const nextIds = new Set(nextCollectionIds);
1784
- const removedIds = existingCollectionIds.filter(
1785
- (cid) => !nextIds.has(cid),
1786
- );
1787
- const addedIds = nextCollectionIds.filter(
1788
- (cid) => !existingIds.has(cid),
1789
- );
1790
-
1791
- if (removedIds.length > 0) {
1792
- await tx
1793
- .delete(postCollections)
1794
- .where(
1795
- and(
1796
- eq(postCollections.siteId, siteId),
1797
- eq(postCollections.postId, id),
1798
- inArray(postCollections.collectionId, removedIds),
1799
- ),
1800
- );
1801
- }
2260
+ // Delete all and re-insert to preserve user-specified ordering
2261
+ await tx
2262
+ .delete(postCollections)
2263
+ .where(
2264
+ and(
2265
+ eq(postCollections.siteId, siteId),
2266
+ eq(postCollections.postId, id),
2267
+ ),
2268
+ );
1802
2269
 
1803
- if (addedIds.length > 0) {
2270
+ if (nextCollectionIds.length > 0) {
1804
2271
  const collectionTimestamp = now();
1805
2272
  await tx.insert(postCollections).values(
1806
- addedIds.map((collectionId) => ({
2273
+ nextCollectionIds.map((collectionId, index) => ({
1807
2274
  siteId,
1808
2275
  postId: id,
1809
2276
  collectionId,
1810
- createdAt: collectionTimestamp,
2277
+ createdAt:
2278
+ existingCollectionTimestamps.get(collectionId) ??
2279
+ collectionTimestamp,
2280
+ position: index,
1811
2281
  })),
1812
2282
  );
1813
2283
  }
@@ -1824,7 +2294,32 @@ export function createPostService(
1824
2294
 
1825
2295
  async updateWithAttachments(id, data, attachments, deps, summaryConfig) {
1826
2296
  if (attachments === undefined) {
1827
- return this.update(id, data, summaryConfig);
2297
+ const existing = await this.getById(id);
2298
+ const post = await this.update(id, data, summaryConfig);
2299
+ if (post && existing) {
2300
+ const urlChanged =
2301
+ data.url !== undefined && data.url !== existing.url;
2302
+ const formatChanged =
2303
+ data.format !== undefined && data.format !== existing.format;
2304
+ if (urlChanged || formatChanged) {
2305
+ await deletePreviewImage(existing.previewImageKey, deps.storage);
2306
+ await db
2307
+ .update(posts)
2308
+ .set({
2309
+ previewImageKey: null,
2310
+ previewKind: null,
2311
+ previewProvider: null,
2312
+ })
2313
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, post.id)));
2314
+ await resolveAndStorePreview(
2315
+ post.id,
2316
+ post.format,
2317
+ post.url,
2318
+ deps.storage,
2319
+ );
2320
+ }
2321
+ }
2322
+ return post;
1828
2323
  }
1829
2324
 
1830
2325
  const existingPost = await this.getById(id);
@@ -1862,17 +2357,41 @@ export function createPostService(
1862
2357
  await applyAttachmentAltUpdates(attachments, deps);
1863
2358
 
1864
2359
  const nextAttachmentIds = new Set(orderedMediaIds);
1865
- const removedTextAttachmentIds = existingAttachments
1866
- .filter(
1867
- (attachment) =>
1868
- attachment.mimeType === "text/x-tiptap+json" &&
1869
- !nextAttachmentIds.has(attachment.id),
1870
- )
2360
+ const removedAttachmentIds = existingAttachments
2361
+ .filter((attachment) => !nextAttachmentIds.has(attachment.id))
1871
2362
  .map((attachment) => attachment.id);
1872
2363
  await deps.media
1873
- .deleteByIds(removedTextAttachmentIds, deps.storage)
2364
+ .deleteByIds(removedAttachmentIds, deps.storage)
1874
2365
  .catch(() => undefined);
1875
2366
 
2367
+ // Best-effort: update link preview when URL changes
2368
+ const urlChanged =
2369
+ data.url !== undefined && data.url !== existingPost.url;
2370
+ const formatChanged =
2371
+ data.format !== undefined && data.format !== existingPost.format;
2372
+ if (urlChanged || formatChanged) {
2373
+ // Clean up old preview image
2374
+ await deletePreviewImage(existingPost.previewImageKey, deps.storage);
2375
+
2376
+ // Clear preview fields first (may be re-set by resolveAndStorePreview)
2377
+ await db
2378
+ .update(posts)
2379
+ .set({
2380
+ previewImageKey: null,
2381
+ previewKind: null,
2382
+ previewProvider: null,
2383
+ })
2384
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, post.id)));
2385
+
2386
+ // Fetch new preview if applicable
2387
+ await resolveAndStorePreview(
2388
+ post.id,
2389
+ post.format,
2390
+ post.url,
2391
+ deps.storage,
2392
+ );
2393
+ }
2394
+
1876
2395
  return post;
1877
2396
  } catch (error) {
1878
2397
  if (replacedAttachments) {
@@ -1900,17 +2419,16 @@ export function createPostService(
1900
2419
  const existing = await this.getById(id);
1901
2420
  if (!existing) return false;
1902
2421
 
1903
- // Clean up media for all affected posts
1904
- if (deps?.media) {
1905
- let postIds: string[];
1906
- if (!isThreadReply(existing)) {
1907
- const thread = await this.getThread(id);
1908
- postIds = thread.map((p) => p.id);
1909
- } else {
1910
- postIds = [id];
1911
- }
2422
+ const isRoot = !isThreadReply(existing);
2423
+ const affectedPosts: Post[] = isRoot
2424
+ ? await this.getThread(id)
2425
+ : [existing];
1912
2426
 
1913
- const mediaMap = await deps.media.getByPostIds(postIds);
2427
+ // Clean up media and preview images
2428
+ if (deps?.media) {
2429
+ const mediaMap = await deps.media.getByPostIds(
2430
+ affectedPosts.map((p) => p.id),
2431
+ );
1914
2432
  const allMedia = [...mediaMap.values()].flat();
1915
2433
  if (allMedia.length > 0) {
1916
2434
  await deps.media.deleteByIds(
@@ -1918,18 +2436,20 @@ export function createPostService(
1918
2436
  deps.storage,
1919
2437
  );
1920
2438
  }
2439
+
2440
+ for (const p of affectedPosts) {
2441
+ await deletePreviewImage(p.previewImageKey, deps.storage);
2442
+ }
1921
2443
  }
1922
2444
 
1923
2445
  const timestamp = now();
1924
2446
 
1925
- // If this is a thread root, soft delete all posts in the thread
1926
- if (!isThreadReply(existing)) {
2447
+ if (isRoot) {
1927
2448
  await db
1928
2449
  .update(posts)
1929
2450
  .set({ deletedAt: timestamp, updatedAt: timestamp })
1930
2451
  .where(and(eq(posts.siteId, siteId), eq(posts.threadId, id)));
1931
2452
  } else {
1932
- // Soft-delete the single reply
1933
2453
  await db
1934
2454
  .update(posts)
1935
2455
  .set({ deletedAt: timestamp, updatedAt: timestamp })
@@ -1940,6 +2460,23 @@ export function createPostService(
1940
2460
  return true;
1941
2461
  },
1942
2462
 
2463
+ async deleteThreadDraft(id, deps) {
2464
+ const deleted = await this.delete(id, deps);
2465
+ if (!deleted) return false;
2466
+
2467
+ // Release path_registry entries for all posts in the thread so slugs
2468
+ // can be reused by the replacement thread.
2469
+ const threadRows = await db
2470
+ .select({ id: posts.id })
2471
+ .from(posts)
2472
+ .where(and(eq(posts.siteId, siteId), eq(posts.threadId, id)));
2473
+ for (const row of threadRows) {
2474
+ await resolvedPaths.deleteByPostId(row.id);
2475
+ }
2476
+
2477
+ return true;
2478
+ },
2479
+
1943
2480
  async getThread(rootId) {
1944
2481
  const rows = await db
1945
2482
  .select()
@@ -2029,7 +2566,7 @@ export function createPostService(
2029
2566
  const rows = await db
2030
2567
  .select({
2031
2568
  threadId: posts.threadId,
2032
- count: sql<number>`count(*)`.as("count"),
2569
+ count: sql<number>`CAST(count(*) AS INTEGER)`.as("count"),
2033
2570
  })
2034
2571
  .from(posts)
2035
2572
  .where(
@@ -2058,10 +2595,10 @@ export function createPostService(
2058
2595
  id: posts.id,
2059
2596
  threadId: posts.threadId,
2060
2597
  createdAt: posts.createdAt,
2061
- previewRank: sql<number>`ROW_NUMBER() OVER (
2598
+ previewRank: sql<number>`CAST(ROW_NUMBER() OVER (
2062
2599
  PARTITION BY ${posts.threadId}
2063
2600
  ORDER BY ${posts.createdAt}, ${posts.id}
2064
- )`.as("preview_rank"),
2601
+ ) AS INTEGER)`.as("preview_rank"),
2065
2602
  })
2066
2603
  .from(posts)
2067
2604
  .where(
@@ -2115,14 +2652,17 @@ export function createPostService(
2115
2652
  .select({
2116
2653
  id: posts.id,
2117
2654
  threadId: posts.threadId,
2118
- replyToId: posts.replyToId,
2119
- replyRank: sql<number>`ROW_NUMBER() OVER (
2655
+ firstReplyRank: sql<number>`CAST(ROW_NUMBER() OVER (
2656
+ PARTITION BY ${posts.threadId}
2657
+ ORDER BY ${posts.createdAt}, ${posts.id}
2658
+ ) AS INTEGER)`.as("first_reply_rank"),
2659
+ latestReplyRank: sql<number>`CAST(ROW_NUMBER() OVER (
2120
2660
  PARTITION BY ${posts.threadId}
2121
2661
  ORDER BY ${posts.createdAt} DESC, ${posts.id} DESC
2122
- )`.as("reply_rank"),
2123
- totalReplyCount: sql<number>`COUNT(*) OVER (
2662
+ ) AS INTEGER)`.as("latest_reply_rank"),
2663
+ totalReplyCount: sql<number>`CAST(COUNT(*) OVER (
2124
2664
  PARTITION BY ${posts.threadId}
2125
- )`.as("total_reply_count"),
2665
+ ) AS INTEGER)`.as("total_reply_count"),
2126
2666
  })
2127
2667
  .from(posts)
2128
2668
  .where(
@@ -2136,41 +2676,72 @@ export function createPostService(
2136
2676
  )
2137
2677
  .as("ranked_replies");
2138
2678
 
2139
- const latestReplyRows = await db
2679
+ const contextRows = await db
2140
2680
  .select({
2141
2681
  threadId: rankedReplies.threadId,
2142
- latestReplyId: rankedReplies.id,
2143
- latestReplyToId: rankedReplies.replyToId,
2682
+ id: rankedReplies.id,
2683
+ firstReplyRank: rankedReplies.firstReplyRank,
2684
+ latestReplyRank: rankedReplies.latestReplyRank,
2144
2685
  totalReplyCount: rankedReplies.totalReplyCount,
2145
2686
  })
2146
2687
  .from(rankedReplies)
2147
- .where(eq(rankedReplies.replyRank, 1));
2688
+ .where(
2689
+ or(
2690
+ eq(rankedReplies.firstReplyRank, 1),
2691
+ lte(rankedReplies.latestReplyRank, 2),
2692
+ ),
2693
+ );
2148
2694
 
2149
- const relatedPostIds = latestReplyRows.flatMap((row) => {
2150
- const ids = [row.latestReplyId];
2695
+ const hydratedPosts = await hydratePostsById(
2696
+ contextRows.map((row) => row.id),
2697
+ );
2151
2698
 
2152
- if (row.latestReplyToId && row.latestReplyToId !== row.threadId) {
2153
- ids.push(row.latestReplyToId);
2699
+ const contextByThreadId = new Map<
2700
+ string,
2701
+ {
2702
+ secondReplyId: string | null;
2703
+ penultimateReplyId: string | null;
2704
+ latestReplyId: string | null;
2705
+ totalReplyCount: number;
2154
2706
  }
2707
+ >();
2708
+ for (const row of contextRows) {
2709
+ const existing = contextByThreadId.get(row.threadId) ?? {
2710
+ secondReplyId: null,
2711
+ penultimateReplyId: null,
2712
+ latestReplyId: null,
2713
+ totalReplyCount: row.totalReplyCount,
2714
+ };
2155
2715
 
2156
- return ids;
2157
- });
2158
- const hydratedPosts = await hydratePostsById(relatedPostIds);
2716
+ if (row.firstReplyRank === 1) {
2717
+ existing.secondReplyId = row.id;
2718
+ }
2719
+ if (row.latestReplyRank === 2) {
2720
+ existing.penultimateReplyId = row.id;
2721
+ }
2722
+ if (row.latestReplyRank === 1) {
2723
+ existing.latestReplyId = row.id;
2724
+ }
2725
+
2726
+ contextByThreadId.set(row.threadId, existing);
2727
+ }
2159
2728
 
2160
2729
  const result = new Map<string, ThreadTimelineContext>();
2161
- for (const row of latestReplyRows) {
2162
- const latestReply = hydratedPosts.get(row.latestReplyId);
2163
- if (!latestReply) continue;
2730
+ for (const [threadId, context] of contextByThreadId) {
2731
+ if (!context.latestReplyId) continue;
2164
2732
 
2165
- const parentReply =
2166
- row.latestReplyToId && row.latestReplyToId !== row.threadId
2167
- ? (hydratedPosts.get(row.latestReplyToId) ?? null)
2168
- : null;
2733
+ const latestReply = hydratedPosts.get(context.latestReplyId);
2734
+ if (!latestReply) continue;
2169
2735
 
2170
- result.set(row.threadId, {
2736
+ result.set(threadId, {
2737
+ secondReply: context.secondReplyId
2738
+ ? (hydratedPosts.get(context.secondReplyId) ?? null)
2739
+ : null,
2740
+ penultimateReply: context.penultimateReplyId
2741
+ ? (hydratedPosts.get(context.penultimateReplyId) ?? null)
2742
+ : null,
2171
2743
  latestReply,
2172
- parentReply,
2173
- totalReplyCount: row.totalReplyCount,
2744
+ totalReplyCount: context.totalReplyCount,
2174
2745
  });
2175
2746
  }
2176
2747
 
@@ -2185,7 +2756,10 @@ export function createPostService(
2185
2756
 
2186
2757
  const rows = await db
2187
2758
  .select({
2188
- count: sql<number>`count(distinct ${posts.threadId})`.as("count"),
2759
+ count:
2760
+ sql<number>`CAST(count(distinct ${posts.threadId}) AS INTEGER)`.as(
2761
+ "count",
2762
+ ),
2189
2763
  })
2190
2764
  .from(posts)
2191
2765
  .where(and(...conditions));
@@ -2241,7 +2815,10 @@ export function createPostService(
2241
2815
 
2242
2816
  const rows = await db
2243
2817
  .select({
2244
- count: sql<number>`count(distinct ${posts.threadId})`.as("count"),
2818
+ count:
2819
+ sql<number>`CAST(count(distinct ${posts.threadId}) AS INTEGER)`.as(
2820
+ "count",
2821
+ ),
2245
2822
  })
2246
2823
  .from(posts)
2247
2824
  .innerJoin(
@@ -2272,10 +2849,10 @@ export function createPostService(
2272
2849
  buildCollectionMembershipCondition(collectionIds),
2273
2850
  ];
2274
2851
  const sortOrder = options.sortOrder ?? "newest";
2275
- const collectedAt =
2852
+ const publishedAt =
2276
2853
  sortOrder === "oldest"
2277
- ? sql<number>`MIN(${postCollections.createdAt})`.as("collected_at")
2278
- : sql<number>`MAX(${postCollections.createdAt})`.as("collected_at");
2854
+ ? sql<number>`MIN(${posts.publishedAt})`.as("published_at")
2855
+ : sql<number>`MAX(${posts.publishedAt})`.as("published_at");
2279
2856
  const ratingPresence = sql<number>`MAX(
2280
2857
  CASE
2281
2858
  WHEN ${posts.rating} IS NULL THEN 0
@@ -2286,10 +2863,16 @@ export function createPostService(
2286
2863
  "rating_value",
2287
2864
  );
2288
2865
 
2866
+ const collectionPinnedAt =
2867
+ sql<number>`MAX(coalesce(${postCollections.pinnedAt}, -1))`.as(
2868
+ "collection_pinned_at",
2869
+ );
2870
+
2289
2871
  const baseQuery = db
2290
2872
  .select({
2291
2873
  threadId: posts.threadId,
2292
- collectedAt,
2874
+ publishedAt,
2875
+ collectionPinnedAt,
2293
2876
  ratingPresence,
2294
2877
  ratingValue,
2295
2878
  })
@@ -2306,15 +2889,24 @@ export function createPostService(
2306
2889
 
2307
2890
  let query =
2308
2891
  sortOrder === "oldest"
2309
- ? baseQuery.orderBy(asc(collectedAt), asc(posts.threadId))
2892
+ ? baseQuery.orderBy(
2893
+ desc(collectionPinnedAt),
2894
+ asc(publishedAt),
2895
+ asc(posts.threadId),
2896
+ )
2310
2897
  : sortOrder === "rating_desc"
2311
2898
  ? baseQuery.orderBy(
2899
+ desc(collectionPinnedAt),
2312
2900
  desc(ratingPresence),
2313
2901
  desc(ratingValue),
2314
- desc(collectedAt),
2902
+ desc(publishedAt),
2315
2903
  desc(posts.threadId),
2316
2904
  )
2317
- : baseQuery.orderBy(desc(collectedAt), desc(posts.threadId));
2905
+ : baseQuery.orderBy(
2906
+ desc(collectionPinnedAt),
2907
+ desc(publishedAt),
2908
+ desc(posts.threadId),
2909
+ );
2318
2910
 
2319
2911
  if (options.limit !== undefined) {
2320
2912
  query = query.limit(options.limit) as typeof query;
@@ -2339,14 +2931,23 @@ export function createPostService(
2339
2931
  ...buildThreadRootPageConditions(options),
2340
2932
  buildCollectionMembershipCondition(collectionIds),
2341
2933
  ];
2934
+ const publishedAt = sql<number>`MAX(${posts.publishedAt})`.as(
2935
+ "published_at",
2936
+ );
2342
2937
  const collectedAt = sql<number>`MAX(${postCollections.createdAt})`.as(
2343
2938
  "collected_at",
2344
2939
  );
2940
+ const collectionPinnedAt =
2941
+ sql<number>`MAX(coalesce(${postCollections.pinnedAt}, -1))`.as(
2942
+ "collection_pinned_at",
2943
+ );
2345
2944
 
2346
2945
  let query = db
2347
2946
  .select({
2348
2947
  threadId: posts.threadId,
2948
+ publishedAt,
2349
2949
  collectedAt,
2950
+ collectionPinnedAt,
2350
2951
  })
2351
2952
  .from(posts)
2352
2953
  .innerJoin(
@@ -2358,7 +2959,11 @@ export function createPostService(
2358
2959
  )
2359
2960
  .where(and(...conditions))
2360
2961
  .groupBy(posts.threadId)
2361
- .orderBy(desc(collectedAt), desc(posts.threadId));
2962
+ .orderBy(
2963
+ desc(collectionPinnedAt),
2964
+ desc(publishedAt),
2965
+ desc(posts.threadId),
2966
+ );
2362
2967
 
2363
2968
  if (options.limit !== undefined) {
2364
2969
  query = query.limit(options.limit) as typeof query;
@@ -2504,5 +3109,61 @@ export function createPostService(
2504
3109
 
2505
3110
  return rows.map((r) => parseInt(r.year, 10));
2506
3111
  },
3112
+
3113
+ async reindexBodyText(options = {}) {
3114
+ const requested = options.limit ?? 50;
3115
+ const limit = Math.min(Math.max(Math.trunc(requested), 1), 500);
3116
+ const cursor = options.cursor;
3117
+
3118
+ const whereConditions = [
3119
+ eq(posts.siteId, siteId),
3120
+ isNull(posts.deletedAt),
3121
+ ];
3122
+ if (cursor) whereConditions.push(gt(posts.id, cursor));
3123
+
3124
+ // Fetch one extra row to detect end-of-data without a separate COUNT.
3125
+ const rows = await db
3126
+ .select({
3127
+ id: posts.id,
3128
+ body: posts.body,
3129
+ bodyText: posts.bodyText,
3130
+ })
3131
+ .from(posts)
3132
+ .where(and(...whereConditions))
3133
+ .orderBy(asc(posts.id))
3134
+ .limit(limit + 1);
3135
+
3136
+ const hasMore = rows.length > limit;
3137
+ const batch = hasMore ? rows.slice(0, limit) : rows;
3138
+
3139
+ let updated = 0;
3140
+ let skipped = 0;
3141
+
3142
+ for (const row of batch) {
3143
+ const nextBodyText = row.body
3144
+ ? extractBodyText(row.body, { includeLinkHrefs: true })
3145
+ : null;
3146
+ if (nextBodyText === row.bodyText) {
3147
+ skipped++;
3148
+ continue;
3149
+ }
3150
+ await db
3151
+ .update(posts)
3152
+ .set({ bodyText: nextBodyText })
3153
+ .where(and(eq(posts.siteId, siteId), eq(posts.id, row.id)));
3154
+ updated++;
3155
+ }
3156
+
3157
+ const lastRow = batch.at(-1);
3158
+ const lastId = lastRow ? lastRow.id : null;
3159
+
3160
+ return {
3161
+ processed: batch.length,
3162
+ updated,
3163
+ skipped,
3164
+ nextCursor: hasMore ? lastId : null,
3165
+ done: !hasMore,
3166
+ };
3167
+ },
2507
3168
  };
2508
3169
  }