@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
@@ -1,9 +1,22 @@
1
1
  /**
2
2
  * Export Service
3
3
  *
4
- * Generates a ready-to-use Zola static site as a ZIP archive.
5
- * Threads are merged into single pages with reply marker comments.
6
- * Media URLs point to the original site (not exported).
4
+ * Generates a ready-to-use Hugo static site as a ZIP archive.
5
+ *
6
+ * Content layout:
7
+ * - Each thread root is a Hugo branch bundle
8
+ * (`content/{root-slug}/_index.md`).
9
+ * - Each reply is a nested leaf bundle
10
+ * (`content/{root-slug}/{reply-slug}/index.md`) with
11
+ * `build: { render: never, list: local }` so only the parent thread
12
+ * page renders it while it still appears in `.Pages`.
13
+ * - `/{reply-slug}/` URLs redirect to the parent thread via Hugo's
14
+ * `aliases:` mechanism and a custom `_default/alias.html` that injects
15
+ * the reply anchor at runtime.
16
+ * - Media is emitted next to each bundle as Hugo page resources.
17
+ *
18
+ * Real Hugo templates and CSS are scaffolded as placeholders here and
19
+ * filled in by Commit 5.
7
20
  */
8
21
 
9
22
  import type { PostService } from "./post.js";
@@ -11,41 +24,87 @@ import type { PathService } from "./path.js";
11
24
  import type { CollectionService } from "./collection.js";
12
25
  import type { MediaService } from "./media.js";
13
26
  import {
14
- buildJantLogoSvgMarkup,
15
27
  getDefaultJantAppleTouchIconBytes,
16
28
  getDefaultJantFaviconIcoBytes,
17
- HOME_BRANDING_LINK_LABEL,
18
- HOME_BRANDING_PREFIX,
19
- JANT_REPO_URL,
20
29
  } from "../lib/jant-branding.js";
21
30
  import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
31
+ import { extractBodyText } from "../lib/summary.js";
22
32
  import { getMediaUrl, getPublicUrlForProvider } from "../lib/image.js";
23
- import { FEATURED_SPARKLE_PATH } from "../lib/featured-icons.js";
24
- import { escapeHtml } from "../lib/html.js";
25
33
  import { render as renderMarkdown } from "../lib/markdown.js";
26
- import { toISOString } from "../lib/time.js";
34
+ import { formatRelativeAge, toISOString } from "../lib/time.js";
35
+ import {
36
+ formatFrontMatter,
37
+ type HugoCollectionRef,
38
+ type HugoFrontMatter,
39
+ type JantMedia,
40
+ } from "../lib/hugo-markdown.js";
41
+ // Shared design tokens — single source of truth for colors, typography,
42
+ // and layout variables. Consumed verbatim by both the main site (via
43
+ // Tailwind) and the Hugo export (written to static/tokens.css). Using
44
+ // ?raw inlines the file contents as a string at build time so the
45
+ // Worker bundle ships without any filesystem access.
46
+ import TOKENS_CSS from "../styles/tokens.css?raw";
47
+
48
+ // Placeholder Hugo theme files — real templates and styles land in Commit 5.
49
+ // We import them as Vite `?raw` strings so the Worker bundle has no runtime
50
+ // filesystem dependency.
51
+ import THEME_TOML from "./export-theme/theme.toml?raw";
52
+ import THEME_STYLE_MAIN_CSS from "./export-theme/styles/main.css?raw";
53
+ // Static-site client bundle — Lit-based lightbox, feed video autoplay, audio
54
+ // waveform, and gallery scroll hints. Built by `vite.config.site.ts` into
55
+ // `export-theme/assets/client-site.{js,css}` and shipped under the theme's
56
+ // reserved `_jant/` static directory so the theme stays version-aligned with
57
+ // `@jant/core`. The assets folder is regenerated on every build, so the
58
+ // checked-in files are source-of-truth for the most recent release.
59
+ import CLIENT_SITE_JS from "./export-theme/assets/client-site.js?raw";
60
+ import CLIENT_SITE_CSS from "./export-theme/assets/client-site.css?raw";
61
+ import LAYOUT_BASEOF from "./export-theme/layouts/_default/baseof.html?raw";
62
+ import LAYOUT_SINGLE from "./export-theme/layouts/_default/single.html?raw";
63
+ import LAYOUT_LIST from "./export-theme/layouts/_default/list.html?raw";
64
+ import LAYOUT_ALIAS from "./export-theme/layouts/_default/alias.html?raw";
65
+ import LAYOUT_INDEX from "./export-theme/layouts/index.html?raw";
66
+ import LAYOUT_POST_LIST from "./export-theme/layouts/post/list.html?raw";
67
+ import LAYOUT_FEATURED_LIST from "./export-theme/layouts/featured/list.html?raw";
68
+ import LAYOUT_ARCHIVE_LIST from "./export-theme/layouts/archive/list.html?raw";
69
+ import LAYOUT_COLLECTIONS_LIST from "./export-theme/layouts/collections/list.html?raw";
70
+ import LAYOUT_COLLECTION_SINGLE from "./export-theme/layouts/collection/single.html?raw";
71
+ import PARTIAL_HEAD from "./export-theme/layouts/partials/head.html?raw";
72
+ import PARTIAL_HEADER from "./export-theme/layouts/partials/header.html?raw";
73
+ import PARTIAL_FOOTER from "./export-theme/layouts/partials/footer.html?raw";
74
+ import PARTIAL_PAGINATION from "./export-theme/layouts/partials/pagination.html?raw";
75
+ import PARTIAL_POST_CARD from "./export-theme/layouts/partials/post-card.html?raw";
76
+ import PARTIAL_MEDIA_GALLERY from "./export-theme/layouts/partials/media-gallery.html?raw";
77
+ import PARTIAL_REPLY from "./export-theme/layouts/partials/reply.html?raw";
78
+ import PARTIAL_THREAD_PREVIEW from "./export-theme/layouts/partials/thread-preview.html?raw";
79
+ import LAYOUT_RSS from "./export-theme/layouts/_default/rss.xml?raw";
80
+ import PARTIAL_FEED_POST_CONTENT from "./export-theme/layouts/partials/feed-post-content.xml?raw";
81
+
27
82
  import type { StorageDriver } from "../lib/storage.js";
28
83
  import { base64ToUint8Array } from "../lib/favicon.js";
29
- import type {
30
- Post,
31
- Collection,
32
- Media,
33
- NavItem,
34
- TextAttachmentContent,
35
- } from "../types.js";
84
+ import type { Post, Collection, Media, NavItem } from "../types.js";
85
+
86
+ /** A file entry in the exported Hugo site. */
87
+ export interface ExportFile {
88
+ path: string;
89
+ content: string | Uint8Array;
90
+ }
36
91
 
37
92
  export interface ExportService {
38
- generateZolaSite(): Promise<Uint8Array>;
93
+ /** Generate a flat list of files for a complete Hugo site. */
94
+ generateHugoFiles(): Promise<ExportFile[]>;
95
+ /** Generate a ZIP archive of the Hugo site. */
96
+ generateHugoSite(): Promise<Uint8Array>;
39
97
  }
40
98
 
41
- interface SiteConfig {
99
+ export interface SiteConfig {
42
100
  siteName: string;
43
101
  siteUrl: string;
44
102
  siteDescription: string;
45
103
  siteLanguage: string;
46
104
  showJantBrandingOnHome: boolean;
47
105
  homeDefaultView: string;
48
- headerNavMaxVisible: number;
106
+ /** "latest" or "featured" — drives the default RSS nav link in the exported site. */
107
+ mainRssFeed: string;
49
108
  siteFooter: string;
50
109
  showHeaderAvatar: boolean;
51
110
  siteAvatarUrl: string;
@@ -66,31 +125,58 @@ interface SiteConfig {
66
125
  sitePathPrefix?: string;
67
126
  navItems: Pick<
68
127
  NavItem,
69
- "type" | "systemKey" | "label" | "url" | "position"
128
+ "type" | "systemKey" | "label" | "url" | "position" | "placement"
70
129
  >[];
71
- }
72
-
73
- interface AttachmentExportMeta {
74
- kind: Media["mediaKind"];
75
- src?: string;
76
- poster?: string | null;
77
- mimeType: string;
78
- originalName: string;
79
- size: number;
80
- width: number | null;
81
- height: number | null;
82
- alt: string | null;
83
- position: string;
84
- blurhash: string | null;
85
- waveform: string | null;
86
- summary: string | null;
87
- chars: number | null;
88
- contentFormat?: TextAttachmentContent["contentFormat"];
89
- content?: string;
130
+ /** Items per page for Hugo pagination — kept in sync with the main site's PAGE_SIZE. */
131
+ pageSize: number;
132
+ /** Items per archive page — kept in sync with the main site's ARCHIVE_PAGE_SIZE. */
133
+ archivePageSize: number;
134
+ /** Max items per Atom feed — kept in sync with the main site's rssFeedLimit. */
135
+ rssFeedLimit: number;
90
136
  }
91
137
 
92
138
  type IconExportMode = "default" | "custom";
93
139
 
140
+ type ExportedCollectionDirectoryItem =
141
+ | {
142
+ type: "collection";
143
+ sequence: string;
144
+ slug: string;
145
+ title: string;
146
+ /** Rendered HTML of the collection description, or null if empty. */
147
+ descriptionHtml?: string | null;
148
+ entryCount?: number;
149
+ recentActivityLabel?: string | null;
150
+ recentActivityIso?: string | null;
151
+ }
152
+ | {
153
+ type: "divider";
154
+ label: string | null;
155
+ }
156
+ | {
157
+ type: "link";
158
+ sequence: string;
159
+ label: string;
160
+ url: string;
161
+ /** Rendered HTML of the link description, or null if empty. */
162
+ descriptionHtml?: string | null;
163
+ };
164
+
165
+ interface ExportCollectionDirectorySourceItem {
166
+ type: "collection" | "divider" | "link";
167
+ label?: string | null;
168
+ url?: string | null;
169
+ description?: string | null;
170
+ collection?: {
171
+ id: string;
172
+ slug: string;
173
+ title: string;
174
+ description?: string | null;
175
+ postCount?: number;
176
+ recentActivityAt?: number;
177
+ };
178
+ }
179
+
94
180
  interface SiteIconAssets {
95
181
  faviconBytes: Uint8Array;
96
182
  faviconMode: IconExportMode;
@@ -98,6 +184,41 @@ interface SiteIconAssets {
98
184
  appleTouchMode: IconExportMode;
99
185
  }
100
186
 
187
+ interface ExportedCollectionMetrics {
188
+ postCount: number;
189
+ recentActivityAt: number;
190
+ }
191
+
192
+ /**
193
+ * A single `collections:` front-matter entry, already resolved to its
194
+ * Hugo-visible slug. Assembled from
195
+ * `collectionService.getCollectionEntriesByPostIds` + `collectionSlugMap`.
196
+ */
197
+ interface ExportedCollectionEntry {
198
+ slug: string;
199
+ /**
200
+ * Denormalized collection title. Kept alongside the slug so the exported
201
+ * front matter can render a tag label without templates having to resolve
202
+ * another page. Optional because legacy call sites may not supply it.
203
+ */
204
+ title?: string;
205
+ /** Unix seconds. */
206
+ collectedAt: number;
207
+ position: number;
208
+ /** Unix seconds, or null when not pinned in this collection. */
209
+ pinnedAt: number | null;
210
+ }
211
+
212
+ function buildDefaultAppleTouchAsset(): Pick<
213
+ SiteIconAssets,
214
+ "appleTouchBytes" | "appleTouchMode"
215
+ > {
216
+ return {
217
+ appleTouchBytes: getDefaultJantAppleTouchIconBytes(),
218
+ appleTouchMode: "default",
219
+ };
220
+ }
221
+
101
222
  export function createExportService(
102
223
  services: {
103
224
  posts: PostService;
@@ -109,15 +230,22 @@ export function createExportService(
109
230
  deps: { storage?: StorageDriver | null } = {},
110
231
  ): ExportService {
111
232
  return {
112
- async generateZolaSite() {
233
+ async generateHugoFiles() {
234
+ const collectionDirectoryDataPromise =
235
+ typeof services.collections.listDirectoryData === "function"
236
+ ? services.collections.listDirectoryData()
237
+ : Promise.resolve(null);
238
+
113
239
  // 1. Query all data
114
- const [allPosts, allCollections] = await Promise.all([
115
- services.posts.list({
116
- excludeReplies: false,
117
- limit: 10000,
118
- }),
119
- services.collections.list(),
120
- ]);
240
+ const [allPosts, allCollections, collectionDirectoryData] =
241
+ await Promise.all([
242
+ services.posts.list({
243
+ excludeReplies: false,
244
+ limit: 10000,
245
+ }),
246
+ services.collections.list(),
247
+ collectionDirectoryDataPromise,
248
+ ]);
121
249
 
122
250
  const allPostIds = allPosts.map((p) => p.id);
123
251
  const roots = allPosts.filter((p) => p.replyToId === null);
@@ -126,23 +254,45 @@ export function createExportService(
126
254
 
127
255
  const [
128
256
  collectionsByPost,
257
+ collectionEntriesByPost,
129
258
  rawMediaByPost,
130
259
  slugMap,
131
260
  aliasMap,
132
261
  collectionSlugMap,
133
262
  ] = await Promise.all([
134
263
  services.collections.getCollectionsByPostIds(allPostIds),
264
+ services.collections.getCollectionEntriesByPostIds(allPostIds),
135
265
  services.media.getByPostIds(allPostIds),
136
266
  services.paths.getPostSlugMap(allPostIds),
137
267
  services.paths.getPostAliases(rootPostIds),
138
268
  services.paths.getCollectionSlugMap(allCollections.map((c) => c.id)),
139
269
  ]);
140
- const textAttachmentContents = await buildTextAttachmentContentMap(
141
- rawMediaByPost,
142
- services.media,
143
- deps.storage,
144
- );
270
+ // Denormalized title lookup so front-matter collection refs can
271
+ // include a title label without templates having to resolve another
272
+ // page. Source of truth is still `slug` on round-trip; `title` is
273
+ // refreshed from DB on every export.
274
+ const collectionTitleMap = new Map<string, string>();
275
+ for (const collection of allCollections) {
276
+ collectionTitleMap.set(collection.id, collection.title);
277
+ }
278
+
145
279
  const iconAssets = await buildSiteIconAssets(siteConfig, deps.storage);
280
+ const collectionMetrics = buildExportedCollectionMetrics(
281
+ allCollections,
282
+ allPosts,
283
+ collectionsByPost,
284
+ );
285
+ const exportedCollectionDirectoryItems =
286
+ buildExportedCollectionDirectoryItems(
287
+ collectionDirectoryData?.items ??
288
+ allCollections.map((collection) => ({
289
+ id: collection.id,
290
+ type: "collection" as const,
291
+ collection,
292
+ })),
293
+ collectionSlugMap,
294
+ collectionMetrics,
295
+ );
146
296
 
147
297
  // 2. Group replies by threadId
148
298
  const repliesByThread = new Map<string, Post[]>();
@@ -156,90 +306,236 @@ export function createExportService(
156
306
  list.sort((a, b) => a.createdAt - b.createdAt);
157
307
  }
158
308
 
159
- // 3. Build ZIP file structure
160
- const { zipSync } = await import("fflate");
161
- const files: Record<string, Uint8Array> = {};
309
+ // 3. Build file list
310
+ const exportFiles: ExportFile[] = [];
162
311
 
163
- // Generate post files
312
+ // Generate thread bundles (root _index.md + per-reply index.md).
164
313
  for (const root of roots) {
165
314
  const slug = slugMap.get(root.id) ?? root.slug;
166
315
  const threadReplies = repliesByThread.get(root.id) ?? [];
167
- const postCollections = collectionsByPost.get(root.id) ?? [];
168
316
  const rootAliases = [...(aliasMap.get(root.id) ?? [])];
169
- const zolaAliases = [...rootAliases];
170
- const rootMedia = rawMediaByPost.get(root.id) ?? [];
171
-
172
- // Reply URLs must resolve back to the merged thread page in Zola, but
173
- // they are not root aliases when round-tripping into Jant.
174
- for (const reply of threadReplies) {
175
- const replySlug = slugMap.get(reply.id) ?? reply.slug;
176
- zolaAliases.push(`/${replySlug}`);
177
- }
178
317
 
179
- const markdown = buildPostMarkdown(
318
+ const rootCollectionEntries = buildExportedCollectionEntriesForPost(
319
+ root.id,
320
+ collectionEntriesByPost,
321
+ collectionSlugMap,
322
+ collectionTitleMap,
323
+ );
324
+
325
+ const bundleFiles = await buildThreadBundle(
180
326
  root,
181
327
  threadReplies,
182
- postCollections,
183
- { rootAliases, zolaAliases },
328
+ slug,
329
+ rootAliases,
330
+ rootCollectionEntries,
184
331
  slugMap,
332
+ collectionEntriesByPost,
185
333
  collectionSlugMap,
186
- rootMedia,
334
+ collectionTitleMap,
187
335
  rawMediaByPost,
188
336
  siteConfig,
189
- textAttachmentContents,
337
+ deps.storage ?? null,
190
338
  );
191
-
192
- files[`content/${slug}/index.md`] = new TextEncoder().encode(markdown);
339
+ exportFiles.push(...bundleFiles);
193
340
  }
194
341
 
342
+ // Collection landing pages (`content/{slug}/_index.md`).
195
343
  for (const collection of allCollections) {
196
344
  const slug = collectionSlugMap.get(collection.id) ?? collection.slug;
197
- const section = buildCollectionSection(collection);
198
- files[`content/c/${slug}/_index.md`] = new TextEncoder().encode(
199
- section,
200
- );
345
+ const entryCount = collectionMetrics.get(collection.id)?.postCount ?? 0;
346
+ exportFiles.push({
347
+ path: `content/${slug}/_index.md`,
348
+ content: await buildCollectionSection(collection, slug, entryCount),
349
+ });
201
350
  }
202
351
 
203
- // Generate scaffold
204
- files["config.toml"] = new TextEncoder().encode(
205
- buildConfigToml(siteConfig, iconAssets),
206
- );
207
- files["content/_index.md"] = new TextEncoder().encode(buildRootSection());
208
- files["content/archive/_index.md"] = new TextEncoder().encode(
209
- buildArchiveSection(),
210
- );
211
- files["templates/base.html"] = new TextEncoder().encode(TEMPLATE_BASE);
212
- files["templates/archive.html"] = new TextEncoder().encode(
213
- TEMPLATE_ARCHIVE,
214
- );
215
- files["templates/index.html"] = new TextEncoder().encode(TEMPLATE_INDEX);
216
- files["templates/page.html"] = new TextEncoder().encode(TEMPLATE_PAGE);
217
- files["templates/section.html"] = new TextEncoder().encode(
218
- TEMPLATE_SECTION,
219
- );
220
- files["templates/taxonomy_list.html"] = new TextEncoder().encode(
221
- TEMPLATE_TAXONOMY_LIST,
222
- );
223
- files["templates/taxonomy_single.html"] = new TextEncoder().encode(
224
- TEMPLATE_TAXONOMY_SINGLE,
225
- );
226
- files["templates/atom.xml"] = new TextEncoder().encode(TEMPLATE_ATOM);
227
- files["templates/macros.html"] = new TextEncoder().encode(
228
- TEMPLATE_MACROS,
229
- );
230
- files["static/style.css"] = new TextEncoder().encode(STYLE_CSS);
231
- files["static/theme.css"] = new TextEncoder().encode(
232
- siteConfig.themeCss ?? "",
233
- );
234
- files["static/custom.css"] = new TextEncoder().encode(
235
- siteConfig.customCss ?? "",
236
- );
237
- files["static/favicon.ico"] = iconAssets.faviconBytes;
238
- files["static/apple-touch-icon.png"] = iconAssets.appleTouchBytes;
239
- files["README.md"] = new TextEncoder().encode(
240
- buildReadme(siteConfig.siteName),
241
- );
352
+ // Section + home scaffolding.
353
+ exportFiles.push({
354
+ path: "hugo.toml",
355
+ content: buildHugoToml(siteConfig),
356
+ });
357
+ exportFiles.push({
358
+ path: "content/_index.md",
359
+ content: await buildHomeSection(siteConfig),
360
+ });
361
+ exportFiles.push({
362
+ path: "content/collections/_index.md",
363
+ content: await buildCollectionsSection(),
364
+ });
365
+ exportFiles.push({
366
+ path: "content/archive/_index.md",
367
+ content: await buildArchiveSection(),
368
+ });
369
+
370
+ const usedSlugs = new Set<string>();
371
+ for (const s of slugMap.values()) usedSlugs.add(s);
372
+ for (const s of collectionSlugMap.values()) usedSlugs.add(s);
373
+ if (!usedSlugs.has("featured")) {
374
+ exportFiles.push({
375
+ path: "content/featured/_index.md",
376
+ content: await buildFeaturedSection(),
377
+ });
378
+ }
379
+
380
+ // Single data file consumed by templates via `hugo.Data.jant`. The
381
+ // collection directory lives on the same object as `directory` so
382
+ // everything Jant owns round-trips in one place.
383
+ exportFiles.push({
384
+ path: "data/jant.toml",
385
+ content: buildJantDataToml(
386
+ siteConfig,
387
+ iconAssets,
388
+ exportedCollectionDirectoryItems,
389
+ ),
390
+ });
391
+
392
+ // Theme scaffolding (real templates + styles land in Commit 5).
393
+ exportFiles.push({
394
+ path: "themes/jant/theme.toml",
395
+ content: THEME_TOML,
396
+ });
397
+ exportFiles.push({
398
+ path: "themes/jant/layouts/_default/baseof.html",
399
+ content: LAYOUT_BASEOF,
400
+ });
401
+ exportFiles.push({
402
+ path: "themes/jant/layouts/_default/single.html",
403
+ content: LAYOUT_SINGLE,
404
+ });
405
+ exportFiles.push({
406
+ path: "themes/jant/layouts/_default/list.html",
407
+ content: LAYOUT_LIST,
408
+ });
409
+ exportFiles.push({
410
+ path: "themes/jant/layouts/_default/alias.html",
411
+ content: LAYOUT_ALIAS,
412
+ });
413
+ exportFiles.push({
414
+ path: "themes/jant/layouts/index.html",
415
+ content: LAYOUT_INDEX,
416
+ });
417
+ exportFiles.push({
418
+ path: "themes/jant/layouts/post/list.html",
419
+ content: LAYOUT_POST_LIST,
420
+ });
421
+ exportFiles.push({
422
+ path: "themes/jant/layouts/featured/list.html",
423
+ content: LAYOUT_FEATURED_LIST,
424
+ });
425
+ exportFiles.push({
426
+ path: "themes/jant/layouts/archive/list.html",
427
+ content: LAYOUT_ARCHIVE_LIST,
428
+ });
429
+ exportFiles.push({
430
+ path: "themes/jant/layouts/collections/list.html",
431
+ content: LAYOUT_COLLECTIONS_LIST,
432
+ });
433
+ exportFiles.push({
434
+ path: "themes/jant/layouts/collection/single.html",
435
+ content: LAYOUT_COLLECTION_SINGLE,
436
+ });
437
+ exportFiles.push({
438
+ path: "themes/jant/layouts/partials/head.html",
439
+ content: PARTIAL_HEAD,
440
+ });
441
+ exportFiles.push({
442
+ path: "themes/jant/layouts/partials/header.html",
443
+ content: PARTIAL_HEADER,
444
+ });
445
+ exportFiles.push({
446
+ path: "themes/jant/layouts/partials/footer.html",
447
+ content: PARTIAL_FOOTER,
448
+ });
449
+ exportFiles.push({
450
+ path: "themes/jant/layouts/partials/pagination.html",
451
+ content: PARTIAL_PAGINATION,
452
+ });
453
+ exportFiles.push({
454
+ path: "themes/jant/layouts/partials/post-card.html",
455
+ content: PARTIAL_POST_CARD,
456
+ });
457
+ exportFiles.push({
458
+ path: "themes/jant/layouts/partials/media-gallery.html",
459
+ content: PARTIAL_MEDIA_GALLERY,
460
+ });
461
+ exportFiles.push({
462
+ path: "themes/jant/layouts/partials/reply.html",
463
+ content: PARTIAL_REPLY,
464
+ });
465
+ exportFiles.push({
466
+ path: "themes/jant/layouts/partials/thread-preview.html",
467
+ content: PARTIAL_THREAD_PREVIEW,
468
+ });
469
+ exportFiles.push({
470
+ path: "themes/jant/layouts/_default/rss.xml",
471
+ content: LAYOUT_RSS,
472
+ });
473
+ exportFiles.push({
474
+ path: "themes/jant/layouts/partials/feed-post-content.xml",
475
+ content: PARTIAL_FEED_POST_CONTENT,
476
+ });
477
+
478
+ // Static assets. Load order in the template's <head> is
479
+ // tokens → main → theme → custom (wired up by the Commit 5 partial).
480
+ exportFiles.push({
481
+ path: "themes/jant/static/tokens.css",
482
+ content: TOKENS_CSS,
483
+ });
484
+ exportFiles.push({
485
+ path: "themes/jant/static/main.css",
486
+ content: THEME_STYLE_MAIN_CSS,
487
+ });
488
+ exportFiles.push({
489
+ path: "themes/jant/static/theme.css",
490
+ content: siteConfig.themeCss ?? "",
491
+ });
492
+ exportFiles.push({
493
+ path: "themes/jant/static/custom.css",
494
+ content: siteConfig.customCss ?? "",
495
+ });
496
+ // Client-side interactions: media lightbox, feed video autoplay,
497
+ // audio waveform, gallery scroll hints. Reserved namespace keeps these
498
+ // from colliding with user-authored static files.
499
+ exportFiles.push({
500
+ path: "themes/jant/static/_jant/client-site.js",
501
+ content: CLIENT_SITE_JS,
502
+ });
503
+ exportFiles.push({
504
+ path: "themes/jant/static/_jant/client-site.css",
505
+ content: CLIENT_SITE_CSS,
506
+ });
507
+ exportFiles.push({
508
+ path: "themes/jant/static/favicon.ico",
509
+ content: iconAssets.faviconBytes,
510
+ });
511
+ exportFiles.push({
512
+ path: "themes/jant/static/apple-touch-icon.png",
513
+ content: iconAssets.appleTouchBytes,
514
+ });
515
+
516
+ exportFiles.push({
517
+ path: "README.md",
518
+ content: buildReadme(siteConfig.siteName),
519
+ });
520
+ exportFiles.push({
521
+ path: ".gitignore",
522
+ content: buildGitignore(),
523
+ });
524
+
525
+ return exportFiles;
526
+ },
242
527
 
528
+ async generateHugoSite() {
529
+ const exportFiles = await this.generateHugoFiles();
530
+ const { zipSync } = await import("fflate");
531
+ const encoder = new TextEncoder();
532
+ const files: Record<string, Uint8Array> = {};
533
+ for (const file of exportFiles) {
534
+ files[file.path] =
535
+ typeof file.content === "string"
536
+ ? encoder.encode(file.content)
537
+ : file.content;
538
+ }
243
539
  return zipSync(files);
244
540
  },
245
541
  };
@@ -272,25 +568,38 @@ async function buildSiteIconAssets(
272
568
  return {
273
569
  faviconBytes,
274
570
  faviconMode,
275
- appleTouchBytes: getDefaultJantAppleTouchIconBytes(),
276
- appleTouchMode: "default",
571
+ ...buildDefaultAppleTouchAsset(),
277
572
  };
278
573
  }
279
574
 
280
575
  if (!storage) {
281
- throw new Error(
282
- "Custom apple-touch icon is configured but no storage driver is available for export",
576
+ return {
577
+ faviconBytes,
578
+ faviconMode,
579
+ ...buildDefaultAppleTouchAsset(),
580
+ };
581
+ }
582
+
583
+ let appleTouchBytes: Uint8Array | null;
584
+ try {
585
+ appleTouchBytes = await readStorageObjectBytes(
586
+ storage,
587
+ config.appleTouchIconStorageKey,
283
588
  );
589
+ } catch {
590
+ return {
591
+ faviconBytes,
592
+ faviconMode,
593
+ ...buildDefaultAppleTouchAsset(),
594
+ };
284
595
  }
285
596
 
286
- const appleTouchBytes = await readStorageObjectBytes(
287
- storage,
288
- config.appleTouchIconStorageKey,
289
- );
290
597
  if (!appleTouchBytes) {
291
- throw new Error(
292
- `Custom apple-touch icon "${config.appleTouchIconStorageKey}" is unavailable for export`,
293
- );
598
+ return {
599
+ faviconBytes,
600
+ faviconMode,
601
+ ...buildDefaultAppleTouchAsset(),
602
+ };
294
603
  }
295
604
 
296
605
  return {
@@ -302,563 +611,1039 @@ async function buildSiteIconAssets(
302
611
  }
303
612
 
304
613
  // ---------------------------------------------------------------------------
305
- // Markdown generation
614
+ // Thread bundle generation
306
615
  // ---------------------------------------------------------------------------
307
616
 
308
- /** Escape a string for use inside a TOML double-quoted value */
309
- function escapeToml(value: string): string {
310
- return value
311
- .replace(/\\/g, "\\\\")
312
- .replace(/"/g, '\\"')
313
- .replace(/\r/g, "\\r")
314
- .replace(/\n/g, "\\n");
617
+ function buildExportedCollectionEntriesForPost(
618
+ postId: string,
619
+ collectionEntriesByPost: Map<
620
+ string,
621
+ {
622
+ collectionId: string;
623
+ createdAt: number;
624
+ position: number;
625
+ pinnedAt: number | null;
626
+ }[]
627
+ >,
628
+ collectionSlugMap: Map<string, string>,
629
+ collectionTitleMap: Map<string, string>,
630
+ ): ExportedCollectionEntry[] {
631
+ const entries = collectionEntriesByPost.get(postId) ?? [];
632
+ const resolved: ExportedCollectionEntry[] = [];
633
+ for (const entry of entries) {
634
+ const slug = collectionSlugMap.get(entry.collectionId);
635
+ if (!slug) continue;
636
+ const title = collectionTitleMap.get(entry.collectionId);
637
+ resolved.push({
638
+ slug,
639
+ title,
640
+ collectedAt: entry.createdAt,
641
+ position: entry.position,
642
+ pinnedAt: entry.pinnedAt,
643
+ });
644
+ }
645
+ return resolved;
646
+ }
647
+
648
+ function collectionEntriesToRefs(
649
+ entries: readonly ExportedCollectionEntry[],
650
+ ): HugoCollectionRef[] {
651
+ return entries.map((entry) => ({
652
+ slug: entry.slug,
653
+ title: entry.title,
654
+ collected_at: toISOString(entry.collectedAt),
655
+ position: entry.position,
656
+ pinned_at: entry.pinnedAt !== null ? toISOString(entry.pinnedAt) : null,
657
+ }));
658
+ }
659
+
660
+ interface MediaEmission {
661
+ /** Flat `media:` front-matter entry. */
662
+ entry: JantMedia;
663
+ /**
664
+ * Site-relative path under `static/` where the primary bytes should
665
+ * land, or null when the media links to a remote public URL and no
666
+ * bytes need to be emitted.
667
+ */
668
+ inlinePath: string | null;
669
+ /**
670
+ * Site-relative path under `static/` where the poster bytes should
671
+ * land, or null when there's no poster or the poster is linked via
672
+ * a public URL.
673
+ */
674
+ inlinePosterPath: string | null;
675
+ }
676
+
677
+ /**
678
+ * Build a flat `media:` entry for a Media record plus a decision about
679
+ * whether the primary bytes (and poster) should be bundled into the
680
+ * export's `static/media/` directory.
681
+ *
682
+ * When the media's provider has a reachable public URL (R2/S3/local
683
+ * proxy configured with a `*_public_url`), `src` points at that absolute
684
+ * URL and no bytes are emitted — the exported site stays small and the
685
+ * media keeps being served from wherever it already lives. Without a
686
+ * public URL the bytes are written to `static/media/{id}.ext` and `src`
687
+ * is the site-relative path.
688
+ */
689
+ function buildMediaEmission(
690
+ media: Media,
691
+ siteConfig: SiteConfig,
692
+ ): MediaEmission {
693
+ const publicUrl = getPublicUrlForProvider(
694
+ media.provider,
695
+ siteConfig.r2PublicUrl,
696
+ siteConfig.s3PublicUrl,
697
+ siteConfig.localPublicUrl,
698
+ );
699
+ const hasPublic = Boolean(publicUrl);
700
+
701
+ const ext = extOfFilename(media.filename);
702
+ const localName = `${media.id}${ext}`;
703
+ const localPath = `/media/${localName}`;
704
+ const src = hasPublic ? getMediaUrl(media.storageKey, publicUrl) : localPath;
705
+
706
+ const entry: JantMedia = {
707
+ id: media.id,
708
+ kind: media.mediaKind,
709
+ src,
710
+ position: parsePositionForSort(media.position),
711
+ };
712
+ if (media.alt !== null && media.alt !== "") entry.alt = media.alt;
713
+ if (media.width !== null) entry.width = media.width;
714
+ if (media.height !== null) entry.height = media.height;
715
+ if (media.blurhash !== null && media.blurhash !== "")
716
+ entry.blurhash = media.blurhash;
717
+ if (media.originalName) entry.original_name = media.originalName;
718
+ if (media.mimeType) entry.mime_type = media.mimeType;
719
+ if (typeof media.size === "number") entry.size = media.size;
720
+ if (media.waveform) entry.waveform = media.waveform;
721
+ if (media.summary) entry.summary = media.summary;
722
+ if (typeof media.chars === "number") entry.chars = media.chars;
723
+ if (media.durationSeconds !== null && media.durationSeconds !== undefined) {
724
+ entry.duration_seconds = media.durationSeconds;
725
+ }
726
+ entry.provider = media.provider;
727
+ entry.storage_key = media.storageKey;
728
+
729
+ let inlinePosterPath: string | null = null;
730
+ if (media.posterKey) {
731
+ const posterExt = extOfStorageKey(media.posterKey);
732
+ const posterLocalName = `${media.id}-poster.${posterExt}`;
733
+ entry.poster = hasPublic
734
+ ? getMediaUrl(media.posterKey, publicUrl)
735
+ : `/media/${posterLocalName}`;
736
+ entry.poster_key = media.posterKey;
737
+ if (!hasPublic) {
738
+ inlinePosterPath = `static/media/${posterLocalName}`;
739
+ }
740
+ }
741
+
742
+ return {
743
+ entry,
744
+ inlinePath: hasPublic ? null : `static/media/${localName}`,
745
+ inlinePosterPath,
746
+ };
315
747
  }
316
748
 
317
- /** Escape a string for use in YAML (wrap in quotes if needed) */
318
- function yamlString(value: string): string {
319
- // If value contains characters that need quoting in YAML
320
- if (
321
- /[:#{}[\],&*?|>!%@`"'\n\\]/.test(value) ||
322
- value.startsWith(" ") ||
323
- value.endsWith(" ") ||
324
- value === "" ||
325
- value === "true" ||
326
- value === "false" ||
327
- value === "null"
328
- ) {
329
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
749
+ function parsePositionForSort(position: string): number {
750
+ // Fractional indexing keys sort lexicographically, but downstream
751
+ // consumers (and the UI) expect a numeric fallback. Keep a stable
752
+ // ordering by hashing the string into an integer.
753
+ let hash = 0;
754
+ for (let i = 0; i < position.length; i++) {
755
+ hash = (hash * 31 + position.charCodeAt(i)) | 0;
330
756
  }
331
- return value;
757
+ return hash;
758
+ }
759
+
760
+ function extOfFilename(filename: string): string {
761
+ const dot = filename.lastIndexOf(".");
762
+ return dot >= 0 ? filename.slice(dot) : "";
763
+ }
764
+
765
+ function extOfStorageKey(key: string): string {
766
+ const dot = key.lastIndexOf(".");
767
+ return dot >= 0 ? key.slice(dot + 1) : "webp";
332
768
  }
333
769
 
334
- function buildPostMarkdown(
770
+ /**
771
+ * Build a complete set of ExportFile entries for a single thread bundle:
772
+ * the root `_index.md`, one `index.md` per reply, and resource blobs for
773
+ * attached media when the storage driver can fetch them.
774
+ */
775
+ async function buildThreadBundle(
335
776
  root: Post,
336
777
  threadReplies: Post[],
337
- postCollections: Collection[],
338
- aliasData: {
339
- rootAliases: string[];
340
- zolaAliases: string[];
341
- },
778
+ rootSlug: string,
779
+ rootAliases: string[],
780
+ rootCollectionEntries: ExportedCollectionEntry[],
342
781
  slugMap: Map<string, string>,
782
+ collectionEntriesByPost: Map<
783
+ string,
784
+ {
785
+ collectionId: string;
786
+ createdAt: number;
787
+ position: number;
788
+ pinnedAt: number | null;
789
+ }[]
790
+ >,
343
791
  collectionSlugMap: Map<string, string>,
344
- rootMedia: Media[],
792
+ collectionTitleMap: Map<string, string>,
345
793
  mediaByPost: Map<string, Media[]>,
346
794
  siteConfig: SiteConfig,
347
- textAttachmentContents: Map<string, TextAttachmentContent>,
348
- ): string {
349
- const parts: string[] = [];
350
-
351
- // Front matter (YAML)
352
- parts.push("---");
353
- if (root.title && root.format !== "quote") {
354
- parts.push(`title: ${yamlString(root.title)}`);
355
- }
356
- const date = root.publishedAt ?? root.createdAt;
357
- if (date) {
358
- parts.push(`date: ${toISOString(date)}`);
359
- }
360
- if (root.updatedAt && root.updatedAt !== root.publishedAt) {
361
- parts.push(`updated: ${toISOString(root.updatedAt)}`);
362
- }
363
- if (root.status === "draft" || root.visibility === "private") {
364
- parts.push("draft: true");
795
+ storage: StorageDriver | null,
796
+ ): Promise<ExportFile[]> {
797
+ const files: ExportFile[] = [];
798
+
799
+ // Root aliases = historical root slugs + every reply slug (so
800
+ // /{reply-slug}/ gets a Hugo alias page that redirects/anchors to
801
+ // the thread root).
802
+ const aliases = [...rootAliases];
803
+ for (const reply of threadReplies) {
804
+ const replySlug = slugMap.get(reply.id) ?? reply.slug;
805
+ aliases.push(`/${replySlug}/`);
365
806
  }
366
807
 
367
- const slug = slugMap.get(root.id) ?? root.slug;
368
- parts.push(`slug: ${yamlString(slug)}`);
808
+ // Root front matter.
809
+ const rootMedia = mediaByPost.get(root.id) ?? [];
810
+ const rootEmissions = rootMedia.map((m) => buildMediaEmission(m, siteConfig));
811
+ const rootMediaList = rootEmissions.map((e) => e.entry);
812
+ const rootFrontMatter: HugoFrontMatter = {
813
+ id: root.id,
814
+ title: root.format !== "quote" ? (root.title ?? undefined) : undefined,
815
+ date:
816
+ root.publishedAt !== null
817
+ ? toISOString(root.publishedAt)
818
+ : toISOString(root.createdAt),
819
+ updated:
820
+ root.updatedAt && root.updatedAt !== root.publishedAt
821
+ ? toISOString(root.updatedAt)
822
+ : undefined,
823
+ // `updated` only reflects edits to the root post itself; when a reply
824
+ // lands, it does NOT bump. For RSS we want the thread's last activity
825
+ // (max of all published reply timestamps) so readers re-surface a
826
+ // thread when a new reply appears. Kept alongside `updated` so the
827
+ // importer round-trips cleanly.
828
+ last_activity_at:
829
+ root.lastActivityAt !== null && root.lastActivityAt !== root.publishedAt
830
+ ? toISOString(root.lastActivityAt)
831
+ : undefined,
832
+ slug: rootSlug,
833
+ type: "post",
834
+ draft:
835
+ root.status === "draft" || root.visibility === "private"
836
+ ? true
837
+ : undefined,
838
+ aliases: aliases.length > 0 ? aliases : undefined,
839
+ format: root.format,
840
+ status: root.status,
841
+ visibility: root.visibility,
842
+ summary_text: getArchiveSummaryText(root) ?? undefined,
843
+ link_url: root.format === "link" && root.url ? root.url : undefined,
844
+ source_name: root.format === "quote" && root.title ? root.title : undefined,
845
+ source_url: root.format === "quote" && root.url ? root.url : undefined,
846
+ quote_text: root.quoteText ?? undefined,
847
+ rating: root.rating ?? undefined,
848
+ featured_at:
849
+ root.featuredAt !== null ? toISOString(root.featuredAt) : undefined,
850
+ pinned_at: root.pinnedAt !== null ? toISOString(root.pinnedAt) : undefined,
851
+ root_aliases: rootAliases.length > 0 ? rootAliases : undefined,
852
+ collections:
853
+ rootCollectionEntries.length > 0
854
+ ? collectionEntriesToRefs(rootCollectionEntries)
855
+ : undefined,
856
+ media: rootMediaList.length > 0 ? rootMediaList : undefined,
857
+ };
369
858
 
370
- if (aliasData.zolaAliases.length > 0) {
371
- parts.push("aliases:");
372
- for (const a of aliasData.zolaAliases) {
373
- parts.push(` - ${yamlString(a)}`);
859
+ const rootBody = root.body ? tiptapJsonToMarkdown(root.body) : "";
860
+ files.push({
861
+ path: `content/${rootSlug}/_index.md`,
862
+ content: `${await formatFrontMatter(rootFrontMatter)}\n${rootBody}${rootBody.endsWith("\n") ? "" : "\n"}`,
863
+ });
864
+
865
+ // Emit media bytes under static/media/ for any media without a
866
+ // reachable public URL. Media whose provider has a configured public
867
+ // URL keeps `src` pointing at that absolute URL and skips inlining —
868
+ // this avoids re-downloading every attachment when the site is going
869
+ // to keep serving media from the existing CDN/proxy anyway.
870
+ for (const { emission, media } of rootEmissions.map((e, i) => ({
871
+ emission: e,
872
+ media: rootMedia[i] as Media,
873
+ }))) {
874
+ if (emission.inlinePath) {
875
+ const file = await readMediaResourceFile(
876
+ storage,
877
+ media.storageKey,
878
+ emission.inlinePath,
879
+ );
880
+ if (file) files.push(file);
374
881
  }
375
- }
376
-
377
- // Taxonomies
378
- if (postCollections.length > 0) {
379
- parts.push("taxonomies:");
380
- parts.push(" c:");
381
- for (const c of postCollections) {
382
- const colSlug = collectionSlugMap.get(c.id) ?? c.slug;
383
- parts.push(` - ${yamlString(colSlug)}`);
882
+ if (emission.inlinePosterPath && media.posterKey) {
883
+ const posterFile = await readMediaResourceFile(
884
+ storage,
885
+ media.posterKey,
886
+ emission.inlinePosterPath,
887
+ );
888
+ if (posterFile) files.push(posterFile);
384
889
  }
385
890
  }
386
891
 
387
- // Extra metadata
388
- parts.push("extra:");
389
- parts.push(` format: ${root.format}`);
390
- parts.push(` status: ${root.status}`);
391
- parts.push(` visibility: ${root.visibility}`);
392
- const summaryText = getArchiveSummaryText(root);
393
- if (summaryText) {
394
- parts.push(` summary_text: ${yamlString(summaryText)}`);
395
- }
396
- if (root.format === "link" && root.url) {
397
- parts.push(` link_url: ${yamlString(root.url)}`);
398
- }
399
- if (root.format === "quote" && root.title) {
400
- parts.push(` source_name: ${yamlString(root.title)}`);
401
- }
402
- if (root.format === "quote" && root.url) {
403
- parts.push(` source_url: ${yamlString(root.url)}`);
404
- }
405
- if (root.quoteText) {
406
- parts.push(` quote_text: ${yamlString(root.quoteText)}`);
407
- }
408
- if (root.rating !== null) {
409
- parts.push(` rating: ${root.rating}`);
410
- }
411
- if (root.pinnedAt !== null) {
412
- parts.push(" pinned: true");
413
- }
414
- if (root.featuredAt !== null) {
415
- parts.push(" featured: true");
416
- }
417
- if (aliasData.rootAliases.length > 0) {
418
- parts.push(" jant:");
419
- parts.push(" root_aliases:");
420
- for (const alias of aliasData.rootAliases) {
421
- parts.push(` - ${yamlString(alias)}`);
422
- }
423
- }
892
+ // Replies as nested leaf bundles.
893
+ for (const reply of threadReplies) {
894
+ const replySlug = slugMap.get(reply.id) ?? reply.slug;
895
+ const replyMedia = mediaByPost.get(reply.id) ?? [];
896
+ const replyEmissions = replyMedia.map((m) =>
897
+ buildMediaEmission(m, siteConfig),
898
+ );
899
+ const replyMediaList = replyEmissions.map((e) => e.entry);
900
+ const replyCollectionEntries = buildExportedCollectionEntriesForPost(
901
+ reply.id,
902
+ collectionEntriesByPost,
903
+ collectionSlugMap,
904
+ collectionTitleMap,
905
+ );
424
906
 
425
- parts.push("---");
426
- parts.push("");
907
+ const replyFrontMatter: HugoFrontMatter = {
908
+ id: reply.id,
909
+ title: reply.format !== "quote" ? (reply.title ?? undefined) : undefined,
910
+ date:
911
+ reply.publishedAt !== null
912
+ ? toISOString(reply.publishedAt)
913
+ : toISOString(reply.createdAt),
914
+ updated:
915
+ reply.updatedAt && reply.updatedAt !== reply.publishedAt
916
+ ? toISOString(reply.updatedAt)
917
+ : undefined,
918
+ slug: replySlug,
919
+ type: "post",
920
+ draft:
921
+ reply.status === "draft" || reply.visibility === "private"
922
+ ? true
923
+ : undefined,
924
+ build: { render: "never", list: "local" },
925
+ format: reply.format,
926
+ status: reply.status,
927
+ visibility: reply.visibility,
928
+ summary_text: getArchiveSummaryText(reply) ?? undefined,
929
+ link_url: reply.format === "link" && reply.url ? reply.url : undefined,
930
+ source_name:
931
+ reply.format === "quote" && reply.title ? reply.title : undefined,
932
+ source_url: reply.format === "quote" && reply.url ? reply.url : undefined,
933
+ quote_text: reply.quoteText ?? undefined,
934
+ rating: reply.rating ?? undefined,
935
+ featured_at:
936
+ reply.featuredAt !== null ? toISOString(reply.featuredAt) : undefined,
937
+ pinned_at:
938
+ reply.pinnedAt !== null ? toISOString(reply.pinnedAt) : undefined,
939
+ collections:
940
+ replyCollectionEntries.length > 0
941
+ ? collectionEntriesToRefs(replyCollectionEntries)
942
+ : undefined,
943
+ media: replyMediaList.length > 0 ? replyMediaList : undefined,
944
+ };
427
945
 
428
- // Root body
429
- const rootBlocks = [
430
- root.body ? tiptapJsonToMarkdown(root.body) : "",
431
- buildAttachmentBlock(rootMedia, siteConfig, textAttachmentContents),
432
- ].filter(Boolean);
433
- if (rootBlocks.length > 0) {
434
- parts.push(rootBlocks.join("\n\n"));
946
+ const replyBody = reply.body ? tiptapJsonToMarkdown(reply.body) : "";
947
+ files.push({
948
+ path: `content/${rootSlug}/${replySlug}/index.md`,
949
+ content: `${await formatFrontMatter(replyFrontMatter)}\n${replyBody}${replyBody.endsWith("\n") ? "" : "\n"}`,
950
+ });
951
+
952
+ for (const { emission, media } of replyEmissions.map((e, i) => ({
953
+ emission: e,
954
+ media: replyMedia[i] as Media,
955
+ }))) {
956
+ if (emission.inlinePath) {
957
+ const file = await readMediaResourceFile(
958
+ storage,
959
+ media.storageKey,
960
+ emission.inlinePath,
961
+ );
962
+ if (file) files.push(file);
963
+ }
964
+ if (emission.inlinePosterPath && media.posterKey) {
965
+ const posterFile = await readMediaResourceFile(
966
+ storage,
967
+ media.posterKey,
968
+ emission.inlinePosterPath,
969
+ );
970
+ if (posterFile) files.push(posterFile);
971
+ }
972
+ }
435
973
  }
436
974
 
437
- // Thread replies
438
- for (const reply of threadReplies) {
439
- parts.push("");
975
+ return files;
976
+ }
440
977
 
441
- // Reply marker comment
442
- const replySlug = slugMap.get(reply.id) ?? reply.slug;
443
- const esc = escapeCommentAttribute;
444
- let marker = `<!-- jant:reply date="${reply.publishedAt ? toISOString(reply.publishedAt) : ""}" slug="${esc(replySlug)}" format="${reply.format}" status="${reply.status}" visibility="${reply.visibility}"`;
978
+ /**
979
+ * Read a media record's bytes from storage and return an ExportFile so
980
+ * they can be bundled next to the post as a Hugo page resource. Returns
981
+ * null when storage is unavailable or the object cannot be read, in
982
+ * which case the front matter entry still points at the resource name
983
+ * and the CLI's pull-media step (or a later sync) can fill it in.
984
+ */
985
+ async function readMediaResourceFile(
986
+ storage: StorageDriver | null,
987
+ storageKey: string,
988
+ bundlePath: string,
989
+ ): Promise<ExportFile | null> {
990
+ if (!storage) return null;
991
+ try {
992
+ const bytes = await readStorageObjectBytes(storage, storageKey);
993
+ if (!bytes) return null;
994
+ return { path: bundlePath, content: bytes };
995
+ } catch {
996
+ return null;
997
+ }
998
+ }
445
999
 
446
- if (reply.format === "link" && reply.url) {
447
- marker += ` url="${esc(reply.url)}"`;
448
- }
449
- if (reply.format === "quote" && reply.quoteText) {
450
- marker += ` quote_text="${encodeURIComponent(reply.quoteText)}"`;
451
- }
452
- if (reply.format === "quote" && reply.title) {
453
- marker += ` source_name="${esc(reply.title)}"`;
454
- }
455
- if (reply.format === "quote" && reply.url) {
456
- marker += ` source_url="${esc(reply.url)}"`;
457
- }
458
- if (reply.rating !== null) {
459
- marker += ` rating="${reply.rating}"`;
460
- }
461
- if (reply.title && reply.format !== "quote") {
462
- marker += ` title="${esc(reply.title)}"`;
463
- }
464
- marker += " -->";
1000
+ // ---------------------------------------------------------------------------
1001
+ // Section + landing pages
1002
+ // ---------------------------------------------------------------------------
465
1003
 
466
- parts.push(marker);
467
- parts.push("");
1004
+ async function buildHomeSection(siteConfig: SiteConfig): Promise<string> {
1005
+ const frontMatter: HugoFrontMatter = {
1006
+ title: siteConfig.siteName,
1007
+ type: "home",
1008
+ };
1009
+ return `${await formatFrontMatter(frontMatter)}\n`;
1010
+ }
468
1011
 
469
- const replyBlocks = [
470
- reply.body ? tiptapJsonToMarkdown(reply.body) : "",
471
- buildAttachmentBlock(
472
- mediaByPost.get(reply.id) ?? [],
473
- siteConfig,
474
- textAttachmentContents,
475
- ),
476
- ].filter(Boolean);
477
- if (replyBlocks.length > 0) {
478
- parts.push(replyBlocks.join("\n\n"));
479
- }
480
- }
1012
+ async function buildCollectionsSection(): Promise<string> {
1013
+ const frontMatter: HugoFrontMatter = {
1014
+ title: "Collections",
1015
+ type: "collections",
1016
+ };
1017
+ return `${await formatFrontMatter(frontMatter)}\n`;
1018
+ }
481
1019
 
482
- return parts.join("\n");
1020
+ async function buildArchiveSection(): Promise<string> {
1021
+ const frontMatter: HugoFrontMatter = {
1022
+ title: "Archive",
1023
+ type: "archive",
1024
+ // Opt into Atom output at /archive/index.xml.
1025
+ outputs: ["html", "rss"],
1026
+ };
1027
+ return `${await formatFrontMatter(frontMatter)}\n`;
483
1028
  }
484
1029
 
485
- function buildCollectionSection(collection: Collection): string {
486
- const parts: string[] = ["+++"];
487
- parts.push(`title = "${escapeToml(collection.title)}"`);
488
- parts.push("render = false");
489
- if (collection.description) {
490
- parts.push(`description = "${escapeToml(collection.description)}"`);
491
- }
492
- parts.push("[extra]");
493
- parts.push(`sort_order = "${escapeToml(collection.sortOrder)}"`);
494
- parts.push("jant_collection = true");
495
- parts.push("+++");
496
- parts.push("");
497
- return parts.join("\n");
1030
+ async function buildFeaturedSection(): Promise<string> {
1031
+ const frontMatter: HugoFrontMatter = {
1032
+ title: "Featured",
1033
+ type: "featured",
1034
+ // Opt into Atom output at /featured/index.xml.
1035
+ outputs: ["html", "rss"],
1036
+ };
1037
+ return `${await formatFrontMatter(frontMatter)}\n`;
1038
+ }
1039
+
1040
+ async function buildCollectionSection(
1041
+ collection: Collection,
1042
+ slug: string,
1043
+ entryCount: number,
1044
+ ): Promise<string> {
1045
+ const frontMatter: HugoFrontMatter = {
1046
+ title: collection.title,
1047
+ slug,
1048
+ type: "collection",
1049
+ summary_text: collection.description ?? undefined,
1050
+ sort_order: collection.sortOrder,
1051
+ entry_count: entryCount,
1052
+ // Opt into Atom output at /{slug}/index.xml.
1053
+ outputs: ["html", "rss"],
1054
+ };
1055
+ return `${await formatFrontMatter(frontMatter)}\n`;
498
1056
  }
499
1057
 
1058
+ // ---------------------------------------------------------------------------
1059
+ // Summary extraction (kept from the previous exporter)
1060
+ // ---------------------------------------------------------------------------
1061
+
500
1062
  function normalizeArchiveText(text: string | null | undefined): string {
501
1063
  return (text ?? "").replace(/\s+/g, " ").trim();
502
1064
  }
503
1065
 
504
1066
  function getArchiveSummaryText(post: Post): string | null {
1067
+ // `summary_text` is a plain-text projection of the post's primary content,
1068
+ // used for `<meta name="description">`, `og:description`, and archive/card
1069
+ // fallbacks when the rendered body is empty. The candidate list differs
1070
+ // per format so the description reflects the right "primary content":
1071
+ //
1072
+ // - Quote: body (commentary) → quoteText. Quotes have no title, so we
1073
+ // fall back to the quote itself to guarantee a meaningful description.
1074
+ // - Link: body only. Links have a title + domain; the URL is already
1075
+ // serialized as `link_url` and rendered as a domain badge, so using
1076
+ // it as `summary_text` would duplicate that information.
1077
+ // - Note: body only. If the body is empty, there's nothing to describe.
1078
+ //
1079
+ // Note: we re-derive the body text from `post.body` (TipTap JSON) rather
1080
+ // than reusing `post.bodyText`, because `bodyText` is written with
1081
+ // `includeLinkHrefs: true` for FTS search indexing — that pollutes the
1082
+ // stored text with trailing link URLs. Here we need clean prose.
1083
+ const cleanBodyText = post.body ? extractBodyText(post.body) : null;
505
1084
  const candidates =
506
1085
  post.format === "quote"
507
- ? [post.summary, post.quoteText, post.bodyText, post.url]
508
- : [post.summary, post.bodyText, post.quoteText, post.url];
1086
+ ? [post.summary, cleanBodyText, post.quoteText]
1087
+ : [post.summary, cleanBodyText];
509
1088
 
510
1089
  for (const candidate of candidates) {
511
1090
  const normalized = normalizeArchiveText(candidate);
512
1091
  if (normalized) return normalized;
513
1092
  }
514
-
515
1093
  return null;
516
1094
  }
517
1095
 
518
- function buildAttachmentBlock(
519
- mediaList: Media[],
520
- siteConfig: SiteConfig,
521
- textAttachmentContents: Map<string, TextAttachmentContent>,
522
- ): string {
523
- if (mediaList.length === 0) return "";
1096
+ // ---------------------------------------------------------------------------
1097
+ // Collection metrics + directory items (kept from the previous exporter)
1098
+ // ---------------------------------------------------------------------------
524
1099
 
525
- const figures = mediaList
526
- .map((media) =>
527
- buildAttachmentFigure(media, siteConfig, textAttachmentContents),
528
- )
529
- .join("\n");
1100
+ function formatCollectionActivityLabel(
1101
+ timestamp: number | undefined,
1102
+ ): string | null {
1103
+ if (typeof timestamp !== "number") {
1104
+ return null;
1105
+ }
530
1106
 
531
- return `<div data-jant-node="attachments">\n${figures}\n</div>`;
1107
+ return formatRelativeAge(timestamp);
532
1108
  }
533
1109
 
534
- function buildAttachmentFigure(
535
- media: Media,
536
- siteConfig: SiteConfig,
537
- textAttachmentContents: Map<string, TextAttachmentContent>,
538
- ): string {
539
- const meta = buildAttachmentMeta(
540
- media,
541
- siteConfig,
542
- textAttachmentContents.get(media.id),
543
- );
544
- const metaJson = safeJsonForHtml(meta);
545
- const name = escapeHtml(meta.originalName);
546
- const caption =
547
- media.summary && media.summary !== media.originalName
548
- ? `<figcaption>${escapeHtml(media.summary)}</figcaption>`
549
- : "";
550
-
551
- if (
552
- meta.kind === "text" &&
553
- meta.contentFormat === "markdown" &&
554
- typeof meta.content === "string"
555
- ) {
556
- const summaryLabel = escapeHtml(
557
- media.summary?.trim() || meta.originalName || "Text attachment",
558
- );
559
- return `<figure data-jant-node="attachment" data-jant-kind="text">
560
- <script type="application/json" data-jant-meta>${metaJson}</script>
561
- <details>
562
- <summary>${summaryLabel}</summary>
563
- <div class="prose jant-attachment-text-preview">${renderMarkdown(meta.content)}</div>
564
- </details>
565
- </figure>`;
1110
+ function formatCollectionActivityIso(
1111
+ timestamp: number | undefined,
1112
+ ): string | null {
1113
+ if (typeof timestamp !== "number") {
1114
+ return null;
566
1115
  }
567
1116
 
568
- const src = meta.src;
569
- if (!src) {
570
- throw new Error(`Attachment ${media.id} is missing an export URL`);
571
- }
1117
+ return toISOString(timestamp);
1118
+ }
572
1119
 
573
- if (meta.kind === "image") {
574
- const alt = media.alt ? ` alt="${escapeHtml(media.alt)}"` : ' alt=""';
575
- return `<figure data-jant-node="attachment" data-jant-kind="image">
576
- <script type="application/json" data-jant-meta>${metaJson}</script>
577
- <img src="${escapeHtml(src)}"${alt}>
578
- ${caption}
579
- </figure>`;
1120
+ /**
1121
+ * Group-aware sequence labels for the exported collection directory.
1122
+ *
1123
+ * Mirrors the main site's `computeSequenceLabels` in
1124
+ * `ui/shared/CollectionDirectory.tsx` so Hugo exports render the same numeric
1125
+ * indices (e.g. "00" "01" under the first divider, "10" "11" under the
1126
+ * second). Dividers themselves receive an empty string — their slot is
1127
+ * reserved so the returned array is index-aligned with the source list.
1128
+ */
1129
+ function computeCollectionDirectorySequenceLabels(
1130
+ items: readonly ExportCollectionDirectorySourceItem[],
1131
+ ): string[] {
1132
+ const isContentItem = (item: ExportCollectionDirectorySourceItem) =>
1133
+ (item.type === "collection" && item.collection) ||
1134
+ (item.type === "link" && item.label && item.url);
1135
+
1136
+ const groupSizes: number[] = [];
1137
+ let seenDivider = false;
1138
+ let ungroupedCount = 0;
1139
+ for (const item of items) {
1140
+ if (item.type === "divider") {
1141
+ seenDivider = true;
1142
+ groupSizes.push(0);
1143
+ } else if (isContentItem(item)) {
1144
+ if (seenDivider) {
1145
+ const lastGroupIndex = groupSizes.length - 1;
1146
+ const lastGroupSize = groupSizes[lastGroupIndex];
1147
+ if (lastGroupSize !== undefined) {
1148
+ groupSizes[lastGroupIndex] = lastGroupSize + 1;
1149
+ }
1150
+ } else {
1151
+ ungroupedCount += 1;
1152
+ }
1153
+ }
580
1154
  }
581
1155
 
582
- if (meta.kind === "video") {
583
- const posterAttr = meta.poster
584
- ? ` poster="${escapeHtml(meta.poster)}"`
585
- : "";
586
- return `<figure data-jant-node="attachment" data-jant-kind="video">
587
- <script type="application/json" data-jant-meta>${metaJson}</script>
588
- <video controls preload="metadata"${posterAttr}>
589
- <source src="${escapeHtml(src)}" type="${escapeHtml(meta.mimeType)}">
590
- </video>
591
- ${caption}
592
- </figure>`;
593
- }
1156
+ const hasGroups = groupSizes.length > 0;
1157
+ const maxGroupIndex = Math.max(0, groupSizes.length - 1);
1158
+ const groupWidth = hasGroups
1159
+ ? Math.max(1, maxGroupIndex.toString(36).length)
1160
+ : 0;
1161
+ const ungroupedItemWidth = Math.max(
1162
+ 2,
1163
+ String(Math.max(0, ungroupedCount - 1)).length,
1164
+ );
594
1165
 
595
- if (meta.kind === "audio") {
596
- return `<figure data-jant-node="attachment" data-jant-kind="audio">
597
- <script type="application/json" data-jant-meta>${metaJson}</script>
598
- <audio controls preload="metadata" src="${escapeHtml(src)}"></audio>
599
- ${caption}
600
- </figure>`;
1166
+ const labels: string[] = [];
1167
+ let groupIndex = -1;
1168
+ let itemIndex = 0;
1169
+
1170
+ for (const item of items) {
1171
+ if (item.type === "divider") {
1172
+ groupIndex += 1;
1173
+ itemIndex = 0;
1174
+ labels.push("");
1175
+ } else if (isContentItem(item)) {
1176
+ if (hasGroups) {
1177
+ const g = Math.max(0, groupIndex)
1178
+ .toString(36)
1179
+ .padStart(groupWidth, "0");
1180
+ const i = itemIndex.toString(36);
1181
+ labels.push(g + i);
1182
+ } else {
1183
+ labels.push(String(itemIndex).padStart(ungroupedItemWidth, "0"));
1184
+ }
1185
+ itemIndex += 1;
1186
+ } else {
1187
+ labels.push("");
1188
+ }
601
1189
  }
602
1190
 
603
- const description = buildAttachmentTextDescription(media);
604
- const figcaption = description
605
- ? `<figcaption>${escapeHtml(description)}</figcaption>`
606
- : "";
607
- return `<figure data-jant-node="attachment" data-jant-kind="${escapeHtml(meta.kind)}">
608
- <script type="application/json" data-jant-meta>${metaJson}</script>
609
- <a href="${escapeHtml(src)}">${name}</a>
610
- ${figcaption}
611
- </figure>`;
1191
+ return labels;
612
1192
  }
613
1193
 
614
- function buildAttachmentTextDescription(media: Media): string {
615
- if (media.mediaKind === "text") {
616
- const summary = media.summary?.trim();
617
- if (summary) return summary;
618
- if (media.chars) return `${media.chars} chars`;
619
- }
1194
+ function buildExportedCollectionDirectoryItems(
1195
+ items: readonly ExportCollectionDirectorySourceItem[],
1196
+ collectionSlugMap: Map<string, string>,
1197
+ collectionMetrics: Map<string, ExportedCollectionMetrics>,
1198
+ ): ExportedCollectionDirectoryItem[] {
1199
+ const sequenceLabels = computeCollectionDirectorySequenceLabels(items);
1200
+ const exportedItems: ExportedCollectionDirectoryItem[] = [];
1201
+
1202
+ items.forEach((item, index) => {
1203
+ if (item.type === "divider") {
1204
+ exportedItems.push({
1205
+ type: "divider",
1206
+ label: item.label ?? null,
1207
+ });
1208
+ return;
1209
+ }
620
1210
 
621
- if (media.summary?.trim()) {
622
- return media.summary.trim();
623
- }
1211
+ if (item.type === "link") {
1212
+ if (!item.label || !item.url) {
1213
+ return;
1214
+ }
624
1215
 
625
- return media.mimeType;
626
- }
1216
+ const description = item.description?.trim();
1217
+ exportedItems.push({
1218
+ type: "link",
1219
+ sequence: sequenceLabels[index] ?? "",
1220
+ label: item.label,
1221
+ url: item.url,
1222
+ descriptionHtml: description ? renderMarkdown(description) : null,
1223
+ });
1224
+ return;
1225
+ }
627
1226
 
628
- function buildAttachmentMeta(
629
- media: Media,
630
- siteConfig: SiteConfig,
631
- textAttachmentContent?: TextAttachmentContent,
632
- ): AttachmentExportMeta {
633
- if (media.mimeType === "text/x-tiptap+json") {
634
- if (!textAttachmentContent) {
635
- throw new Error(
636
- `Text attachment ${media.id} content is unavailable for export`,
637
- );
1227
+ const collection = item.collection;
1228
+ if (!collection?.id) {
1229
+ return;
638
1230
  }
639
1231
 
640
- return {
641
- kind: media.mediaKind,
642
- mimeType: media.mimeType,
643
- originalName: media.originalName,
644
- size: media.size,
645
- width: media.width,
646
- height: media.height,
647
- alt: media.alt,
648
- position: media.position,
649
- blurhash: media.blurhash,
650
- waveform: media.waveform,
651
- summary: media.summary,
652
- chars: media.chars,
653
- contentFormat: textAttachmentContent.contentFormat,
654
- content: textAttachmentContent.content,
655
- };
1232
+ const slug = collectionSlugMap.get(collection.id) ?? collection.slug;
1233
+ if (!slug) {
1234
+ return;
1235
+ }
1236
+ const metrics = collectionMetrics.get(collection.id);
1237
+ const activityTimestamp =
1238
+ metrics?.recentActivityAt ?? collection.recentActivityAt;
1239
+
1240
+ const collectionDescription = collection.description?.trim();
1241
+ exportedItems.push({
1242
+ type: "collection",
1243
+ sequence: sequenceLabels[index] ?? "",
1244
+ slug,
1245
+ title: collection.title || slug,
1246
+ descriptionHtml: collectionDescription
1247
+ ? renderMarkdown(collectionDescription)
1248
+ : null,
1249
+ entryCount:
1250
+ metrics?.postCount ??
1251
+ (typeof collection.postCount === "number"
1252
+ ? collection.postCount
1253
+ : undefined),
1254
+ recentActivityLabel: formatCollectionActivityLabel(activityTimestamp),
1255
+ recentActivityIso: formatCollectionActivityIso(activityTimestamp),
1256
+ });
1257
+ });
1258
+
1259
+ return exportedItems;
1260
+ }
1261
+
1262
+ function buildExportedCollectionMetrics(
1263
+ collections: readonly Collection[],
1264
+ posts: readonly Post[],
1265
+ collectionsByPost: ReadonlyMap<string, readonly Collection[]>,
1266
+ ): Map<string, ExportedCollectionMetrics> {
1267
+ const metrics = new Map<string, ExportedCollectionMetrics>();
1268
+
1269
+ for (const collection of collections) {
1270
+ metrics.set(collection.id, {
1271
+ postCount: 0,
1272
+ recentActivityAt: collection.updatedAt,
1273
+ });
656
1274
  }
657
1275
 
658
- const publicUrl = getPublicUrlForProvider(
659
- media.provider,
660
- siteConfig.r2PublicUrl,
661
- siteConfig.s3PublicUrl,
662
- siteConfig.localPublicUrl,
663
- );
664
-
665
- return {
666
- kind: media.mediaKind,
667
- src: getMediaUrl(media.storageKey, publicUrl, siteConfig.sitePathPrefix),
668
- poster: media.posterKey
669
- ? getMediaUrl(media.posterKey, publicUrl, siteConfig.sitePathPrefix)
670
- : null,
671
- mimeType: media.mimeType,
672
- originalName: media.originalName,
673
- size: media.size,
674
- width: media.width,
675
- height: media.height,
676
- alt: media.alt,
677
- position: media.position,
678
- blurhash: media.blurhash,
679
- waveform: media.waveform,
680
- summary: media.summary,
681
- chars: media.chars,
682
- };
683
- }
684
-
685
- async function buildTextAttachmentContentMap(
686
- mediaByPost: Map<string, Media[]>,
687
- mediaService: Pick<MediaService, "getTextAttachmentContent">,
688
- storage?: StorageDriver | null,
689
- ): Promise<Map<string, TextAttachmentContent>> {
690
- const textAttachments = [...mediaByPost.values()]
691
- .flat()
692
- .filter((media) => media.mimeType === "text/x-tiptap+json");
1276
+ for (const post of posts) {
1277
+ if (post.deletedAt !== null) {
1278
+ continue;
1279
+ }
1280
+ // Drafts and private posts are excluded — they won't reach Hugo.
1281
+ if (post.status === "draft" || post.visibility === "private") {
1282
+ continue;
1283
+ }
1284
+ // Replies roll up into their thread root for directory metrics.
1285
+ if (post.replyToId !== null) {
1286
+ continue;
1287
+ }
693
1288
 
694
- if (textAttachments.length === 0) {
695
- return new Map();
696
- }
1289
+ const activityAt =
1290
+ post.lastActivityAt ??
1291
+ post.publishedAt ??
1292
+ post.updatedAt ??
1293
+ post.createdAt;
1294
+ const postCollections = collectionsByPost.get(post.id) ?? [];
1295
+
1296
+ for (const collection of postCollections) {
1297
+ const current = metrics.get(collection.id);
1298
+ if (!current) {
1299
+ continue;
1300
+ }
697
1301
 
698
- const contents = await Promise.all(
699
- textAttachments.map(async (media) => {
700
- const content = await mediaService.getTextAttachmentContent(
701
- media.id,
702
- storage,
703
- );
704
- if (!content) {
705
- throw new Error(
706
- `Text attachment ${media.id} content is unavailable for export`,
1302
+ if (current.postCount === 0) {
1303
+ current.recentActivityAt = activityAt;
1304
+ } else {
1305
+ current.recentActivityAt = Math.max(
1306
+ current.recentActivityAt,
1307
+ activityAt,
707
1308
  );
708
1309
  }
709
- return [media.id, content] as const;
710
- }),
711
- );
1310
+ current.postCount += 1;
1311
+ }
1312
+ }
712
1313
 
713
- return new Map(contents);
1314
+ return metrics;
714
1315
  }
715
1316
 
716
- function escapeCommentAttribute(value: string): string {
717
- return value
718
- .replace(/\\/g, "\\\\")
719
- .replace(/"/g, '\\"')
720
- .replace(/\n/g, "\\n");
721
- }
1317
+ // ---------------------------------------------------------------------------
1318
+ // Nav item resolution
1319
+ // ---------------------------------------------------------------------------
722
1320
 
723
- function safeJsonForHtml(value: unknown): string {
724
- return JSON.stringify(value)
725
- .replace(/</g, "\\u003c")
726
- .replace(/>/g, "\\u003e")
727
- .replace(/&/g, "\\u0026");
1321
+ /**
1322
+ * System nav items on the main site store an empty `label` in the DB and
1323
+ * resolve their display text at render time through i18n
1324
+ * (`getNavItemDisplayLabel`). The Hugo export has no i18n runtime, so fall
1325
+ * back to these English defaults when serializing to `data/jant.toml`.
1326
+ * Users can still override by setting a custom label on the nav item.
1327
+ */
1328
+ const SYSTEM_NAV_FALLBACK_LABELS: Record<string, string> = {
1329
+ latest: "Latest",
1330
+ featured: "Featured",
1331
+ collections: "Collections",
1332
+ archive: "Archive",
1333
+ rss: "RSS",
1334
+ settings: "Settings",
1335
+ };
1336
+
1337
+ function resolveNavItemLabel(item: SiteConfig["navItems"][number]): string {
1338
+ if (item.label) return item.label;
1339
+ if (item.systemKey) {
1340
+ const fallback = SYSTEM_NAV_FALLBACK_LABELS[item.systemKey];
1341
+ if (fallback) return fallback;
1342
+ }
1343
+ return item.label;
728
1344
  }
729
1345
 
730
- function buildConfigToml(
731
- config: SiteConfig,
732
- iconAssets: SiteIconAssets,
1346
+ /**
1347
+ * Resolve a nav item's final href for the Hugo export.
1348
+ *
1349
+ * Mirrors the runtime logic in `lib/view.ts:toNavItemView`. System URLs
1350
+ * stored in the DB ("/latest", "/featured") are not real routes — they get
1351
+ * rewritten to "/" when they match `homeDefaultView`, otherwise they
1352
+ * resolve to the dedicated path.
1353
+ *
1354
+ * The "rss" system nav item points at whichever Atom feed the site has
1355
+ * configured as its main feed: `mainRssFeed === "featured"` → the featured
1356
+ * section feed at `/featured/index.xml`, otherwise the home feed at
1357
+ * `/index.xml` (which mirrors the homepage's "latest" timeline).
1358
+ */
1359
+ function resolveNavItemUrl(
1360
+ item: SiteConfig["navItems"][number],
1361
+ homeDefaultView: string,
1362
+ mainRssFeed: string,
733
1363
  ): string {
734
- const footerHtml = config.siteFooter ? renderMarkdown(config.siteFooter) : "";
735
- const parts = [
736
- `base_url = "${escapeToml(config.siteUrl || "https://example.com")}"`,
737
- `title = "${escapeToml(config.siteName)}"`,
738
- `description = "${escapeToml(config.siteDescription)}"`,
739
- `default_language = "${escapeToml(config.siteLanguage)}"`,
740
- "generate_feeds = true",
741
- "compile_sass = false",
1364
+ if (item.systemKey === "latest") {
1365
+ return homeDefaultView === "latest" ? "/" : "/latest/";
1366
+ }
1367
+ if (item.systemKey === "featured") {
1368
+ return homeDefaultView === "featured" ? "/" : "/featured/";
1369
+ }
1370
+ if (item.systemKey === "collections") return "/collections/";
1371
+ if (item.systemKey === "archive") return "/archive/";
1372
+ if (item.systemKey === "rss") {
1373
+ return mainRssFeed === "featured" ? "/featured/index.xml" : "/index.xml";
1374
+ }
1375
+ return item.url;
1376
+ }
1377
+
1378
+ // ---------------------------------------------------------------------------
1379
+ // hugo.toml + data TOMLs
1380
+ // ---------------------------------------------------------------------------
1381
+
1382
+ /** Escape a string for use inside a TOML double-quoted value. */
1383
+ function escapeTomlString(value: string): string {
1384
+ return value
1385
+ .replace(/\\/g, "\\\\")
1386
+ .replace(/"/g, '\\"')
1387
+ .replace(/\r/g, "\\r")
1388
+ .replace(/\n/g, "\\n");
1389
+ }
1390
+
1391
+ function buildHugoToml(config: SiteConfig): string {
1392
+ const baseUrl = (config.siteUrl || "https://example.com").replace(/\/+$/, "");
1393
+ // Hugo requires language codes to be all lowercase (it rejects the BCP-47
1394
+ // casing `zh-Hant` / `zh-Hans` with "must be all lower case and no spaces").
1395
+ const language = config.siteLanguage.toLowerCase();
1396
+ const parts: string[] = [
1397
+ `baseURL = "${escapeTomlString(baseUrl)}/"`,
1398
+ `title = "${escapeTomlString(config.siteName)}"`,
1399
+ `languageCode = "${escapeTomlString(language)}"`,
1400
+ `defaultContentLanguage = "${escapeTomlString(language)}"`,
1401
+ 'theme = "jant"',
1402
+ `paginate = ${config.pageSize}`,
1403
+ "enableRobotsTXT = true",
1404
+ // Disable Hugo's built-in taxonomies — jant has no tags or categories
1405
+ // and the default empty /tags/ and /categories/ pages are noise. This
1406
+ // must stay at the root (before any `[table]` header) so TOML doesn't
1407
+ // nest it under the previous table.
1408
+ "disableKinds = ['taxonomy', 'term']",
1409
+ "",
1410
+ "[permalinks]",
1411
+ ' post = "/:slug/"',
1412
+ "",
1413
+ "[markup]",
1414
+ " [markup.goldmark]",
1415
+ " [markup.goldmark.renderer]",
1416
+ " unsafe = true",
742
1417
  "",
743
- 'feed_filenames = ["atom.xml"]',
1418
+ // Emit an Atom 2005 feed at the site root (/index.xml). Per-section
1419
+ // feeds (featured, archive, each collection) are opted in via each
1420
+ // section's front matter `outputs: ["html", "rss"]` — enabling RSS on
1421
+ // `section` globally here would also create a feed at every root post's
1422
+ // URL, since root posts are themselves branch bundles / sections.
1423
+ // Override the built-in RSS output format to emit Atom instead of
1424
+ // RSS 2.0 so the wire format mirrors the main site's `lib/feed.ts`.
1425
+ "[outputs]",
1426
+ ' home = ["html", "rss"]',
1427
+ // Hugo's default for sections is ["html", "rss"] — without overriding
1428
+ // it here every root post (which is a section) would get its own
1429
+ // /{slug}/index.xml. Turn sections off by default and re-enable RSS
1430
+ // on just featured, archive, and each collection via per-section
1431
+ // front matter `outputs: ["html", "rss"]`.
1432
+ ' section = ["html"]',
744
1433
  "",
745
- "[extra.jant_export]",
1434
+ "[outputFormats]",
1435
+ " [outputFormats.RSS]",
1436
+ ' mediaType = "application/atom+xml"',
1437
+ ' baseName = "index"',
1438
+ // Use text/template (not html/template) so Hugo doesn't HTML-escape
1439
+ // the XML prologue, CDATA markers, or tag literals. Every dynamic
1440
+ // value inside the template passes through `transform.XMLEscape`.
1441
+ " isPlainText = true",
1442
+ ' rel = "alternate"',
1443
+ "",
1444
+ "[mediaTypes]",
1445
+ ' [mediaTypes."application/atom+xml"]',
1446
+ ' suffixes = ["xml"]',
1447
+ "",
1448
+ "[params]",
1449
+ ` description = "${escapeTomlString(config.siteDescription)}"`,
1450
+ ` home_default_view = "${escapeTomlString(config.homeDefaultView)}"`,
1451
+ ` main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
1452
+ ` show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
1453
+ ` show_header_avatar = ${config.showHeaderAvatar}`,
1454
+ ` noindex = ${config.noindex}`,
1455
+ ` theme_id = "${escapeTomlString(config.themeId)}"`,
1456
+ ` default_theme_id = "${escapeTomlString(config.defaultThemeId)}"`,
1457
+ ` font_theme_id = "${escapeTomlString(config.fontThemeId)}"`,
1458
+ ` theme_mode = "${escapeTomlString(config.themeMode)}"`,
1459
+ ` page_size = ${config.pageSize}`,
1460
+ ` archive_page_size = ${config.archivePageSize}`,
1461
+ ` rss_feed_limit = ${config.rssFeedLimit}`,
1462
+ ];
1463
+ if (config.siteAvatarUrl) {
1464
+ parts.push(
1465
+ ` site_avatar_url = "${escapeTomlString(config.siteAvatarUrl)}"`,
1466
+ );
1467
+ }
1468
+ if (config.faviconVersion) {
1469
+ parts.push(
1470
+ ` favicon_version = "${escapeTomlString(config.faviconVersion)}"`,
1471
+ );
1472
+ }
1473
+
1474
+ return `${parts.join("\n")}\n`;
1475
+ }
1476
+
1477
+ function buildJantDataToml(
1478
+ config: SiteConfig,
1479
+ iconAssets: SiteIconAssets,
1480
+ directoryItems: readonly ExportedCollectionDirectoryItem[],
1481
+ ): string {
1482
+ const footerHtml = config.siteFooter ? renderMarkdown(config.siteFooter) : "";
1483
+ const parts: string[] = [
746
1484
  'format = "jant-site"',
747
1485
  "version = 1",
748
- `generated_at = "${escapeToml(toISOString(Math.floor(Date.now() / 1000)))}"`,
749
- "",
750
- "[extra.jant]",
751
- `home_default_view = "${escapeToml(config.homeDefaultView)}"`,
752
- `header_nav_max_visible = ${config.headerNavMaxVisible}`,
1486
+ `generated_at = "${escapeTomlString(toISOString(Math.floor(Date.now() / 1000)))}"`,
1487
+ `site_name = "${escapeTomlString(config.siteName)}"`,
1488
+ `site_description = "${escapeTomlString(config.siteDescription)}"`,
1489
+ `site_language = "${escapeTomlString(config.siteLanguage)}"`,
1490
+ `home_default_view = "${escapeTomlString(config.homeDefaultView)}"`,
1491
+ `main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
753
1492
  `show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
754
1493
  `show_header_avatar = ${config.showHeaderAvatar}`,
755
1494
  `noindex = ${config.noindex}`,
756
1495
  `site_avatar_mode = "${config.siteAvatarUrl ? "custom" : "none"}"`,
757
1496
  `favicon_mode = "${iconAssets.faviconMode}"`,
758
1497
  `apple_touch_mode = "${iconAssets.appleTouchMode}"`,
759
- "nav_exported = true",
760
- `theme_id = "${escapeToml(config.themeId || config.defaultThemeId)}"`,
761
- `default_theme_id = "${escapeToml(config.defaultThemeId)}"`,
762
- `font_theme_id = "${escapeToml(config.fontThemeId)}"`,
763
- `theme_mode = "${escapeToml(config.themeMode)}"`,
1498
+ `theme_id = "${escapeTomlString(config.themeId)}"`,
1499
+ `default_theme_id = "${escapeTomlString(config.defaultThemeId)}"`,
1500
+ `font_theme_id = "${escapeTomlString(config.fontThemeId)}"`,
1501
+ `theme_mode = "${escapeTomlString(config.themeMode)}"`,
1502
+ `page_size = ${config.pageSize}`,
1503
+ `archive_page_size = ${config.archivePageSize}`,
1504
+ `rss_feed_limit = ${config.rssFeedLimit}`,
1505
+ 'favicon_path = "/favicon.ico"',
1506
+ 'apple_touch_icon_path = "/apple-touch-icon.png"',
764
1507
  ];
765
-
766
1508
  if (config.siteAvatarUrl) {
767
- parts.push(`site_avatar_url = "${escapeToml(config.siteAvatarUrl)}"`);
1509
+ parts.push(`site_avatar_url = "${escapeTomlString(config.siteAvatarUrl)}"`);
768
1510
  }
769
1511
  if (config.faviconVersion) {
770
- parts.push(`favicon_version = "${escapeToml(config.faviconVersion)}"`);
1512
+ parts.push(
1513
+ `favicon_version = "${escapeTomlString(config.faviconVersion)}"`,
1514
+ );
771
1515
  }
772
1516
  if (footerHtml) {
773
- parts.push(`site_footer_html = "${escapeToml(footerHtml)}"`);
1517
+ parts.push(`site_footer_html = "${escapeTomlString(footerHtml)}"`);
774
1518
  }
775
1519
  if (config.siteFooter) {
776
- parts.push(`site_footer_markdown = "${escapeToml(config.siteFooter)}"`);
1520
+ parts.push(
1521
+ `site_footer_markdown = "${escapeTomlString(config.siteFooter)}"`,
1522
+ );
777
1523
  }
778
1524
 
779
1525
  for (const item of config.navItems) {
1526
+ // `settings` is authenticated-only and has no corresponding page in the
1527
+ // static Hugo site — drop it at export time so it never shows up in nav.
1528
+ if (item.systemKey === "settings") continue;
780
1529
  parts.push("");
781
- parts.push("[[extra.jant.nav]]");
782
- parts.push(`type = "${escapeToml(item.type)}"`);
783
- parts.push(`label = "${escapeToml(item.label)}"`);
784
- parts.push(`url = "${escapeToml(item.url)}"`);
785
- if (item.systemKey) {
786
- parts.push(`system_key = "${escapeToml(item.systemKey)}"`);
787
- }
1530
+ parts.push("[[nav]]");
1531
+ parts.push(`type = "${escapeTomlString(item.type)}"`);
1532
+ parts.push(`label = "${escapeTomlString(resolveNavItemLabel(item))}"`);
1533
+ parts.push(
1534
+ `url = "${escapeTomlString(resolveNavItemUrl(item, config.homeDefaultView, config.mainRssFeed))}"`,
1535
+ );
1536
+ parts.push(`system_key = "${escapeTomlString(item.systemKey ?? "")}"`);
1537
+ parts.push(`placement = "${escapeTomlString(item.placement ?? "header")}"`);
788
1538
  }
789
1539
 
790
- parts.push("");
791
- parts.push("[[taxonomies]]");
792
- parts.push('name = "c"');
793
- parts.push("feed = true");
794
- parts.push("");
795
- parts.push("[markdown]");
796
- parts.push("highlight_code = true");
797
- parts.push('highlight_theme = "css"');
1540
+ for (const item of directoryItems) {
1541
+ parts.push("");
1542
+ parts.push("[[directory]]");
1543
+ parts.push(`type = "${escapeTomlString(item.type)}"`);
1544
+ if (item.type === "collection") {
1545
+ parts.push(`sequence = "${escapeTomlString(item.sequence)}"`);
1546
+ parts.push(`slug = "${escapeTomlString(item.slug)}"`);
1547
+ parts.push(`title = "${escapeTomlString(item.title)}"`);
1548
+ if (item.descriptionHtml) {
1549
+ parts.push(
1550
+ `description_html = "${escapeTomlString(item.descriptionHtml)}"`,
1551
+ );
1552
+ }
1553
+ if (typeof item.entryCount === "number") {
1554
+ parts.push(`entry_count = ${item.entryCount}`);
1555
+ }
1556
+ if (item.recentActivityLabel) {
1557
+ parts.push(
1558
+ `recent_activity_label = "${escapeTomlString(item.recentActivityLabel)}"`,
1559
+ );
1560
+ }
1561
+ if (item.recentActivityIso) {
1562
+ parts.push(
1563
+ `recent_activity_iso = "${escapeTomlString(item.recentActivityIso)}"`,
1564
+ );
1565
+ }
1566
+ } else if (item.type === "divider") {
1567
+ if (item.label !== null) {
1568
+ parts.push(`label = "${escapeTomlString(item.label)}"`);
1569
+ }
1570
+ } else {
1571
+ parts.push(`sequence = "${escapeTomlString(item.sequence)}"`);
1572
+ parts.push(`label = "${escapeTomlString(item.label)}"`);
1573
+ parts.push(`url = "${escapeTomlString(item.url)}"`);
1574
+ if (item.descriptionHtml) {
1575
+ parts.push(
1576
+ `description_html = "${escapeTomlString(item.descriptionHtml)}"`,
1577
+ );
1578
+ }
1579
+ }
1580
+ }
798
1581
 
799
- return `${parts.join("\n")}
800
- `;
1582
+ return `${parts.join("\n")}\n`;
801
1583
  }
802
1584
 
803
- function buildRootSection(): string {
804
- return `+++
805
- sort_by = "date"
806
- paginate_by = 20
807
- +++
808
- `;
809
- }
1585
+ // ---------------------------------------------------------------------------
1586
+ // README + .gitignore
1587
+ // ---------------------------------------------------------------------------
1588
+
1589
+ function buildGitignore(): string {
1590
+ return `# Hugo build output
1591
+ public/
1592
+ resources/
1593
+ .hugo_build.lock
1594
+
1595
+ # OS
1596
+ .DS_Store
1597
+ Thumbs.db
810
1598
 
811
- function buildArchiveSection(): string {
812
- return `+++
813
- title = "Archive"
814
- template = "archive.html"
815
- +++
1599
+ # Editors
1600
+ .vscode/
1601
+ .idea/
1602
+ *.swp
816
1603
  `;
817
1604
  }
818
1605
 
819
1606
  function buildReadme(siteName: string): string {
820
- return `# ${siteName} — Zola Export
1607
+ return `# ${siteName} — Hugo Export
821
1608
 
822
- This is a static site exported from [Jant](https://github.com/jant-me/jant), ready to build with [Zola](https://www.getzola.org/).
1609
+ This is a static site exported from [Jant](https://github.com/jant-me/jant), ready to build with [Hugo](https://gohugo.io/).
823
1610
 
824
- ## Install Zola
1611
+ ## Install Hugo
1612
+
1613
+ This export targets Hugo **extended 0.160.1+**.
825
1614
 
826
1615
  **macOS (Homebrew):**
827
1616
 
828
1617
  \`\`\`sh
829
- brew install zola
1618
+ brew install hugo
830
1619
  \`\`\`
831
1620
 
832
1621
  **Windows (Scoop):**
833
1622
 
834
1623
  \`\`\`sh
835
- scoop install zola
1624
+ scoop install hugo-extended
836
1625
  \`\`\`
837
1626
 
838
- **Linux (Snap):**
839
-
840
- \`\`\`sh
841
- snap install zola --edge
842
- \`\`\`
1627
+ **Linux:**
843
1628
 
844
- Or download a binary from <https://github.com/getzola/zola/releases>.
1629
+ Download the extended build from <https://github.com/gohugoio/hugo/releases>.
845
1630
 
846
- See the [Zola installation docs](https://www.getzola.org/documentation/getting-started/installation/) for more options.
1631
+ See the [Hugo installation docs](https://gohugo.io/installation/) for more options.
847
1632
 
848
1633
  ## Quick start
849
1634
 
850
1635
  Preview locally:
851
1636
 
852
1637
  \`\`\`sh
853
- zola serve
1638
+ hugo serve
854
1639
  \`\`\`
855
1640
 
856
- Then open <http://127.0.0.1:1111> in your browser.
1641
+ Then open <http://localhost:1313> in your browser.
857
1642
 
858
1643
  Build the site for deployment:
859
1644
 
860
1645
  \`\`\`sh
861
- zola build
1646
+ hugo --minify
862
1647
  \`\`\`
863
1648
 
864
1649
  The output goes to the \`public/\` directory. Upload it to any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, etc.).
@@ -866,1886 +1651,61 @@ The output goes to the \`public/\` directory. Upload it to any static host (Netl
866
1651
  ## Project structure
867
1652
 
868
1653
  \`\`\`
869
- config.toml — Site configuration (title, URL, language)
1654
+ hugo.toml — Site configuration (baseURL, title, theme, params)
870
1655
  content/
871
- _index.md Root section (homepage settings)
872
- {slug}/index.md Individual posts (threads are merged into one page)
873
- c/{slug}/_index.md Collection display metadata for taxonomy pages and round-trip import
874
- templates/ Tera templates (Zola's template engine)
875
- static/
876
- style.css Base exported stylesheet
877
- theme.css — Resolved Jant theme variables
878
- custom.css Exported custom CSS overrides
879
- favicon.ico — Exported site favicon (custom or default fallback)
880
- apple-touch-icon.png Exported Apple touch icon (custom or default fallback)
1656
+ _index.md Home section
1657
+ archive/_index.md Archive section
1658
+ collections/_index.md Collections directory section
1659
+ featured/_index.md Featured section
1660
+ {slug}/
1661
+ _index.md Thread root (branch bundle)
1662
+ {reply-slug}/
1663
+ index.md Reply (leaf bundle, not rendered as its own URL)
1664
+ data/
1665
+ jant.toml Nav items, branding, display preferences, ordered collections directory
1666
+ themes/jant/ — Bundled Hugo theme (overrideable via layouts/ at the site root)
1667
+ static/ — Copy files here to add them to the published site
881
1668
  \`\`\`
882
1669
 
883
1670
  ## Customizing
884
1671
 
885
- - **Site settings** — edit \`config.toml\` to change the title, URL, or language.
886
- - **Jant metadata** — \`config.toml\` stores \`[extra.jant_export]\` and \`[extra.jant]\` for round-trip import.
887
- - **Styles** — edit \`static/style.css\`. The theme supports light and dark modes via \`prefers-color-scheme\`.
888
- - **Templates** — edit files in \`templates/\`. Zola uses the [Tera](https://keats.github.io/tera/) template engine.
889
- - **Debugging** — export to a directory with \`jant site export --directory ./my-site\`, then run \`cd my-site && zola serve\`.
890
- - **Collections** — posts are tagged with collections via the \`c\` taxonomy. Browse them at \`/c/\`.
891
-
892
- ## Notes
893
-
894
- - The raw export API only writes content files. The CLI localizes media by default unless you pass \`--no-localize-media\`.
895
- - Thread replies are merged into the root post as a single page. Reply metadata is preserved in HTML comments (\`<!-- jant:reply ... -->\`).
896
- - Attachments are preserved as Jant HTML blocks (\`data-jant-node="attachments"\`). Text attachments embed canonical Markdown in the block metadata, while the rendered preview is display-only and ignored by \`jant site import\`.
897
- - Posts with \`draft: true\` in front matter are only built when you pass the \`--drafts\` flag to \`zola build\` or \`zola serve\`.
898
- `;
899
- }
900
-
901
- // ---------------------------------------------------------------------------
902
- // Zola theme templates
903
- // ---------------------------------------------------------------------------
904
-
905
- const DECORATIVE_QUOTE_MARK_SVG = `<span class="decorative-quote-mark feed-quote-mark" aria-hidden="true">
906
- <svg viewBox="0 0 96 96" role="presentation" focusable="false">
907
- <path fill="currentColor" d="M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z" />
908
- <path fill="currentColor" d="M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z" />
909
- </svg>
910
- </span>`;
911
-
912
- const TEMPLATE_BASE = `<!DOCTYPE html>
913
- <html lang="{{ config.default_language }}" data-theme-mode="{{ config.extra.jant.theme_mode | default(value='auto') }}">
914
- <head>
915
- <meta charset="utf-8">
916
- <meta name="viewport" content="width=device-width, initial-scale=1">
917
- <title>{% block title %}{{ config.title }}{% endblock %}</title>
918
- {% if config.description %}
919
- <meta name="description" content="{{ config.description }}">
920
- {% endif %}
921
- {% if config.extra.jant.noindex %}
922
- <meta name="robots" content="noindex, nofollow">
923
- {% endif %}
924
- {% set favicon_href = get_url(path='favicon.ico') %}
925
- {% set apple_touch_href = get_url(path='apple-touch-icon.png') %}
926
- {% if config.extra.jant.favicon_version %}
927
- <link rel="icon" href="{{ favicon_href }}?v={{ config.extra.jant.favicon_version }}" sizes="16x16 32x32">
928
- <link rel="apple-touch-icon" href="{{ apple_touch_href }}?v={{ config.extra.jant.favicon_version }}">
929
- {% else %}
930
- <link rel="icon" href="{{ favicon_href }}" sizes="16x16 32x32">
931
- <link rel="apple-touch-icon" href="{{ apple_touch_href }}">
932
- {% endif %}
933
- <link rel="stylesheet" href="{{ get_url(path='style.css') }}">
934
- <link rel="stylesheet" href="{{ get_url(path='theme.css') }}">
935
- <link rel="stylesheet" href="{{ get_url(path='custom.css') }}">
936
- <link rel="alternate" type="application/atom+xml" title="{{ config.title }}" href="{{ get_url(path='atom.xml') }}">
937
- </head>
938
- <body>
939
- <div class="site-page">
940
- <header class="site-header">
941
- <div class="site-header-inner">
942
- <div class="{% block header_top_class %}site-header-top site-header-top-bordered{% endblock %}">
943
- <a href="{{ config.base_url }}" class="site-logo">
944
- {% if config.extra.jant.show_header_avatar and config.extra.jant.site_avatar_url %}
945
- <img src="{{ config.extra.jant.site_avatar_url }}" class="site-logo-avatar" alt="">
946
- {% endif %}
947
- <span>{{ config.title }}</span>
948
- </a>
949
- <div class="site-header-right">
950
- <nav class="site-header-nav" aria-label="Primary">
951
- {% if config.extra.jant.nav and config.extra.jant.nav | length > 0 %}
952
- {% for item in config.extra.jant.nav %}
953
- {% if item.system_key == "settings" %}
954
- {% elif item.system_key == "collections" %}
955
- <a href="{{ get_url(path='c') }}" class="site-header-link">{{ item.label }}</a>
956
- {% elif item.system_key == "rss" %}
957
- <a href="{{ get_url(path='atom.xml') }}" class="site-header-link">{{ item.label }}</a>
958
- {% elif item.system_key == "archive" %}
959
- <a href="{{ get_url(path='archive') }}" class="site-header-link">{{ item.label }}</a>
960
- {% else %}
961
- <a href="{{ item.url }}" class="site-header-link">{{ item.label }}</a>
962
- {% endif %}
963
- {% endfor %}
964
- {% else %}
965
- <a href="{{ config.base_url }}/c/" class="site-header-link">Collections</a>
966
- <a href="{{ get_url(path='atom.xml') }}" class="site-header-link">RSS</a>
967
- {% endif %}
968
- </nav>
969
- </div>
970
- </div>
971
- </div>
972
- </header>
973
-
974
- <main class="site-main">
975
- <div class="site-container">
976
- <div class="{% block site_content_class %}site-content{% endblock %}">
977
- {% block content %}{% endblock %}
978
- </div>
979
- </div>
980
- </main>
981
-
982
- {% if config.extra.jant.site_footer_html %}
983
- <footer class="site-footer" data-footer>
984
- <div class="site-container">
985
- <div class="prose">{{ config.extra.jant.site_footer_html | safe }}</div>
986
- </div>
987
- </footer>
988
- {% endif %}
989
- </div>
990
- </body>
991
- </html>
992
- `;
993
-
994
- const TEMPLATE_INDEX = `{% extends "base.html" %}
995
- {% import "macros.html" as macros %}
996
-
997
- {% block title %}{{ config.title }}{% endblock %}
998
- {% block header_top_class %}site-header-top site-header-top-bordered site-header-top-home{% endblock %}
999
- {% block site_content_class %}site-content site-content-home{% endblock %}
1000
-
1001
- {% block content %}
1002
- <div data-page="home">
1003
- {% if paginator.current_index > 1 %}
1004
- <p class="page-context-label">Page {{ paginator.current_index }}</p>
1005
- {% endif %}
1006
- <div data-feed>
1007
- <div id="timeline-feed">
1008
- <div id="timeline-items">
1009
- {% for page in paginator.pages %}
1010
- {% if page.extra.visibility | default(value="public") != "latest_hidden" %}
1011
- <div class="feed-item" data-timeline-item data-timeline-item-content>
1012
- {% if not loop.first %}<hr class="feed-divider">{% endif %}
1013
- {{ macros::post_card(page=page) }}
1014
- </div>
1015
- {% endif %}
1016
- {% endfor %}
1017
- </div>
1018
- </div>
1019
- </div>
1020
-
1021
- {% if paginator.previous or paginator.next %}
1022
- <nav class="pagination" aria-label="Pagination">
1023
- <div class="pagination-side">
1024
- {% if paginator.previous %}
1025
- <a href="{{ paginator.previous }}" class="pagination-link">&larr; Previous</a>
1026
- {% endif %}
1027
- </div>
1028
- <div class="pagination-center">
1029
- {% if paginator.number_pagers > 1 %}
1030
- <span class="pagination-label">Page {{ paginator.current_index }} of {{ paginator.number_pagers }}</span>
1031
- {% endif %}
1032
- </div>
1033
- <div class="pagination-side pagination-side-end">
1034
- {% if paginator.next %}
1035
- <a href="{{ paginator.next }}" class="pagination-link">Next &rarr;</a>
1036
- {% endif %}
1037
- </div>
1038
- </nav>
1039
- {% endif %}
1040
-
1041
- {% if config.extra.jant.show_jant_branding_on_home %}
1042
- <footer class="home-branding-credit">
1043
- ${HOME_BRANDING_PREFIX}
1044
- <a href="${JANT_REPO_URL}" target="_blank" rel="noopener noreferrer">
1045
- ${buildJantLogoSvgMarkup("positive")}
1046
- <span>${HOME_BRANDING_LINK_LABEL}</span>
1047
- </a>
1048
- </footer>
1049
- {% endif %}
1050
- </div>
1051
- {% endblock %}
1052
- `;
1053
-
1054
- const TEMPLATE_PAGE = `{% extends "base.html" %}
1055
- {% import "macros.html" as macros %}
1672
+ - **Site settings** — edit \`hugo.toml\` to change the baseURL, title, or pagination.
1673
+ - **Jant metadata** — \`data/jant.toml\` drives nav and the collections directory, and is preserved across round-trip import.
1674
+ - **Styles** — edit \`themes/jant/static/main.css\`, or drop a \`static/main.css\` at the site root to override.
1675
+ - **Templates** — add files under \`layouts/\` at the site root to override the bundled theme.
1676
+ - **Debugging** — from a Jant site project, run \`npx jant site export --directory ./my-site\`, then \`cd my-site && hugo serve\`.
1056
1677
 
1057
- {% block title %}{% if page.title %}{{ page.title }} &mdash; {% endif %}{{ config.title }}{% endblock %}
1678
+ ## Fetching media locally
1058
1679
 
1059
- {% block content %}
1060
- <div data-page="post">
1061
- {{ macros::post_card(page=page, detail=true) }}
1062
- </div>
1063
- {% endblock %}
1064
- `;
1065
-
1066
- const TEMPLATE_SECTION = `{% extends "base.html" %}
1067
- {% import "macros.html" as macros %}
1068
-
1069
- {% block title %}{{ section.title }} &mdash; {{ config.title }}{% endblock %}
1070
-
1071
- {% block content %}
1072
- <div class="section-shell">
1073
- <header class="section-header">
1074
- <h1 class="section-title">{{ section.title }}</h1>
1075
- {% if section.description %}
1076
- <p class="section-description">{{ section.description }}</p>
1077
- {% endif %}
1078
- </header>
1079
-
1080
- <div data-feed>
1081
- <div id="timeline-feed">
1082
- <div id="timeline-items">
1083
- {% for page in section.pages %}
1084
- {% if page.extra.visibility | default(value="public") != "latest_hidden" %}
1085
- <div class="feed-item" data-timeline-item data-timeline-item-content>
1086
- {% if not loop.first %}<hr class="feed-divider">{% endif %}
1087
- {{ macros::post_card(page=page) }}
1088
- </div>
1089
- {% endif %}
1090
- {% endfor %}
1091
- </div>
1092
- </div>
1093
- </div>
1094
- </div>
1095
- {% endblock %}
1096
- `;
1680
+ When the source site has a storage provider configured (R2/S3/local proxy), images and attachments in this export link to the provider URL instead of being bundled. That keeps the repo small but means the files aren't on disk — fine if Hugo can reach the internet, not fine if you want a fully self-contained archive.
1097
1681
 
1098
- const TEMPLATE_ARCHIVE = `{% extends "base.html" %}
1099
-
1100
- {% block title %}Archive &mdash; {{ config.title }}{% endblock %}
1101
-
1102
- {% block content %}
1103
- {% set root = get_section(path="_index.md") %}
1104
- <div class="section-shell archive-shell">
1105
- <header class="section-header">
1106
- <h1 class="section-title">Archive</h1>
1107
- <p class="section-description">Every exported post in one chronological list.</p>
1108
- </header>
1109
-
1110
- <div class="archive-list">
1111
- {% for year, year_pages in root.pages | group_by(attribute="year") %}
1112
- {% for month, month_pages in year_pages | group_by(attribute="month") %}
1113
- <section class="archive-month-group">
1114
- <header class="archive-month-heading">
1115
- {{ month_pages[0].date | date(format="%B %Y") }}
1116
- </header>
1117
- {% for page in month_pages %}
1118
- {% if page.extra.visibility | default(value="public") == "public" %}
1119
- {% set archive_title = page.title | default(value="") %}
1120
- {% if archive_title == "" %}
1121
- {% set archive_title = page.extra.summary_text | default(value="") %}
1122
- {% endif %}
1123
- {% if archive_title == "" %}
1124
- {% set archive_title = page.summary | default(value=page.content) | striptags | trim %}
1125
- {% endif %}
1126
- <article class="archive-entry">
1127
- <time class="archive-entry-date" datetime="{{ page.date }}">
1128
- {{ page.date | date(format="%b %e, %Y") }}
1129
- </time>
1130
- <div class="archive-entry-main">
1131
- <a href="{{ page.permalink }}" class="archive-entry-title">
1132
- {{ archive_title | default(value="Untitled") | truncate(length=92) }}
1133
- </a>
1134
- <div class="archive-entry-meta">
1135
- <span class="archive-entry-format">{{ page.extra.format | default(value="note") }}</span>
1136
- {% set collections = page.taxonomies.c | default(value=[]) %}
1137
- {% for col in collections %}
1138
- {% set col_meta = get_section(path='c/' ~ col ~ '/_index.md') %}
1139
- <a href="{{ get_taxonomy_url(kind='c', name=col) }}" class="archive-entry-tag">{{ col_meta.title | default(value=col) }}</a>
1140
- {% endfor %}
1141
- </div>
1142
- </div>
1143
- </article>
1144
- {% endif %}
1145
- {% endfor %}
1146
- </section>
1147
- {% endfor %}
1148
- {% endfor %}
1149
- </div>
1150
- </div>
1151
- {% endblock %}
1152
- `;
1682
+ To download every referenced media file into \`static/media/\` and rewrite the references to local paths, run this from the root of the export:
1153
1683
 
1154
- const TEMPLATE_TAXONOMY_LIST = `{% extends "base.html" %}
1155
-
1156
- {% block title %}Collections &mdash; {{ config.title }}{% endblock %}
1157
-
1158
- {% block content %}
1159
- <div class="section-shell">
1160
- <header class="section-header">
1161
- <h1 class="section-title">Collections</h1>
1162
- <p class="section-description">Browse exported posts by collection.</p>
1163
- </header>
1164
- <ol class="collection-list">
1165
- {% for term in terms %}
1166
- {% set term_meta = get_section(path='c/' ~ term.name ~ '/_index.md') %}
1167
- {% set latest_page = term.pages | first %}
1168
- <li class="collection-list-item">
1169
- <a href="{{ term.permalink }}" class="collection-list-link">
1170
- <span class="collection-list-sequence" aria-hidden="true"></span>
1171
- <span class="collection-list-content">
1172
- <span class="collection-list-title">{{ term_meta.title | default(value=term.name) }}</span>
1173
- <span class="collection-list-meta">
1174
- <span>{{ term.pages | length }} entries</span>
1175
- {% if latest_page %}
1176
- <span>Updated {{ latest_page.updated | default(value=latest_page.date) | date(format="%Y-%m-%d") }}</span>
1177
- {% endif %}
1178
- </span>
1179
- </span>
1180
- </a>
1181
- </li>
1182
- {% endfor %}
1183
- </ol>
1184
- </div>
1185
- {% endblock %}
1186
- `;
1187
-
1188
- const TEMPLATE_TAXONOMY_SINGLE = `{% extends "base.html" %}
1189
- {% import "macros.html" as macros %}
1190
-
1191
- {% block title %}{% set term_meta = get_section(path='c/' ~ term.name ~ '/_index.md') %}{{ term_meta.title | default(value=term.name) }} &mdash; {{ config.title }}{% endblock %}
1192
-
1193
- {% block content %}
1194
- {% set term_meta = get_section(path='c/' ~ term.name ~ '/_index.md') %}
1195
- <div class="section-shell">
1196
- <header class="section-header">
1197
- <h1 class="section-title">{{ term_meta.title | default(value=term.name) }}</h1>
1198
- {% if term_meta.description %}
1199
- <p class="section-description">{{ term_meta.description }}</p>
1200
- {% endif %}
1201
- </header>
1202
- <div data-feed>
1203
- <div id="timeline-feed">
1204
- <div id="timeline-items">
1205
- {% for page in term.pages %}
1206
- {% if page.extra.visibility | default(value="public") != "latest_hidden" %}
1207
- <div class="feed-item" data-timeline-item data-timeline-item-content>
1208
- {% if not loop.first %}<hr class="feed-divider">{% endif %}
1209
- {{ macros::post_card(page=page) }}
1210
- </div>
1211
- {% endif %}
1212
- {% endfor %}
1213
- </div>
1214
- </div>
1215
- </div>
1216
- </div>
1217
- {% endblock %}
1218
- `;
1684
+ \`\`\`sh
1685
+ npx @jant/core site pull-media --path .
1686
+ \`\`\`
1219
1687
 
1220
- const TEMPLATE_ATOM = `<?xml version="1.0" encoding="utf-8"?>
1221
- <feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ lang }}">
1222
- <title>{% if section is defined and section.title %}{{ section.title }} · {% elif term is defined and taxonomy.name == "c" %}{% set term_meta = get_section(path='c/' ~ term.name ~ '/_index.md') %}{{ term_meta.title | default(value=term.name) }} · {% elif term is defined and term.name %}{{ term.name }} · {% endif %}{{ config.title }}</title>
1223
- {% if config.description %}
1224
- <subtitle>{{ config.description }}</subtitle>
1225
- {% endif %}
1226
- <link rel="self" type="application/atom+xml" href="{{ feed_url | safe }}" />
1227
- <link rel="alternate" type="text/html" href="{% if section is defined %}{{ section.permalink }}{% elif term is defined %}{{ term.permalink }}{% else %}{{ config.base_url }}{% endif %}" />
1228
- <id>{{ feed_url | safe }}</id>
1229
- {% if last_updated is defined %}
1230
- <updated>{{ last_updated | date(format="%+") }}</updated>
1231
- {% else %}
1232
- <updated>{{ config.extra.jant_export.generated_at | default(value="1970-01-01T00:00:00Z") }}</updated>
1233
- {% endif %}
1234
- {% set author_name = config.author | default(value="") %}
1235
- {% if author_name %}
1236
- <author><name>{{ author_name }}</name></author>
1237
- {% endif %}
1238
- {% for page in pages %}
1239
- {% if page.extra.visibility | default(value="public") == "public" %}
1240
- <entry>
1241
- {% set entry_title = page.title | default(value="") %}
1242
- {% set entry_summary = page.extra.summary_text | default(value="") %}
1243
- {% if entry_summary == "" %}
1244
- {% set entry_summary = page.summary | default(value=page.content) | striptags | trim %}
1245
- {% endif %}
1246
- {% if entry_summary == "" %}
1247
- {% set entry_summary = page.permalink %}
1248
- {% endif %}
1249
- <title>{{ entry_title }}</title>
1250
- <link rel="alternate" type="text/html" href="{{ page.permalink | safe }}" />
1251
- <published>{{ page.date | date(format="%+") }}</published>
1252
- <updated>{{ page.updated | default(value=page.date) | date(format="%+") }}</updated>
1253
- <id>{{ page.permalink | safe }}</id>
1254
- <summary type="text">{{ entry_summary }}</summary>
1255
- <content type="html">&lt;p&gt;{{ entry_summary }}&lt;/p&gt;</content>
1256
- </entry>
1257
- {% endif %}
1258
- {% endfor %}
1259
- </feed>
1260
- `;
1688
+ Safe to re-run; files already on disk are reused. Anything that fails to download keeps its original URL so the site still builds.
1261
1689
 
1262
- // ---------------------------------------------------------------------------
1263
- // Shared macro — single post card used by all list/detail templates
1264
- // ---------------------------------------------------------------------------
1690
+ ## Notes
1265
1691
 
1266
- const TEMPLATE_MACROS = `{% macro post_status_badges() %}
1267
- <div class="post-status-badges">
1268
- <span class="post-status-badge post-status-pinned">
1269
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
1270
- <line x1="12" x2="12" y1="17" y2="22" />
1271
- <path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" />
1272
- </svg>
1273
- Pinned
1274
- </span>
1275
- <span class="post-status-badge post-status-private">
1276
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
1277
- <path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
1278
- <path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
1279
- <path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
1280
- <path d="m2 2 20 20" />
1281
- </svg>
1282
- Private
1283
- </span>
1284
- </div>
1285
- {% endmacro %}
1286
-
1287
- {% macro post_rating(page) %}
1288
- {% if page.extra.rating %}
1289
- <div class="post-rating" aria-label="{{ page.extra.rating }} out of 5">
1290
- {% for i in range(end=5) %}
1291
- <span class="{% if i < page.extra.rating %}post-star-filled{% else %}post-star-empty{% endif %}">★</span>
1292
- {% endfor %}
1293
- </div>
1294
- {% endif %}
1295
- {% endmacro %}
1296
-
1297
- {% macro post_footer(page, detail=false) %}
1298
- {% set collections = page.taxonomies.c | default(value=[]) %}
1299
- <footer class="post-menu-footer{% if detail %} post-footer-detail{% endif %}" data-post-meta>
1300
- <div class="post-footer-meta">
1301
- <span class="post-footer-featured" title="Featured">
1302
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1303
- <path d="${FEATURED_SPARKLE_PATH}" />
1304
- </svg>
1305
- <span class="sr-only">Featured</span>
1306
- </span>
1307
- <a href="{{ page.permalink }}" class="u-url post-date-link">
1308
- <time class="dt-published" datetime="{{ page.date }}" title="{{ page.date }}">
1309
- {{ page.date | date(format="%b %e, %Y") }}
1310
- </time>
1311
- </a>
1312
- {% if page.extra.format == "link" and page.extra.link_url %}
1313
- <a href="{{ page.extra.link_url }}" class="post-footer-external-link" target="_blank" rel="noopener noreferrer" aria-label="Open external link">
1314
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1315
- <path d="M7 17 17 7" />
1316
- <path d="M9 7h8v8" />
1317
- </svg>
1318
- </a>
1319
- {% endif %}
1320
- {% if collections | length > 0 %}
1321
- {% set first_collection = collections | first %}
1322
- {% set first_collection_meta = get_section(path='c/' ~ first_collection ~ '/_index.md') %}
1323
- {% set hidden_collection_count = collections | length %}
1324
- <span class="post-collection-tags">
1325
- <span class="post-collection-sep" aria-hidden="true">&middot;</span>
1326
- <a href="{{ get_taxonomy_url(kind='c', name=first_collection) }}" class="post-collection-tag">{{ first_collection_meta.title | default(value=first_collection) }}</a>
1327
- {% if hidden_collection_count > 1 %}
1328
- <span class="post-collection-more-wrap">
1329
- <button type="button" class="post-collection-more" aria-haspopup="menu" data-collection-popover-trigger>
1330
- and {{ hidden_collection_count - 1 }} more
1331
- </button>
1332
- <span class="post-collection-popover" role="menu" data-collection-popover>
1333
- {% for col in collections %}
1334
- {% if not loop.first %}
1335
- {% set col_meta = get_section(path='c/' ~ col ~ '/_index.md') %}
1336
- <a href="{{ get_taxonomy_url(kind='c', name=col) }}" class="post-collection-popover-item" role="menuitem">{{ col_meta.title | default(value=col) }}</a>
1337
- {% endif %}
1338
- {% endfor %}
1339
- </span>
1340
- </span>
1341
- {% endif %}
1342
- </span>
1343
- {% endif %}
1344
- </div>
1345
- </footer>
1346
- {% endmacro %}
1347
-
1348
- {% macro note_card(page, detail=false) %}
1349
- <article
1350
- class="h-entry post-menu-target {% if detail %}post-detail-shell{% else %}post-card-shell{% endif %}"
1351
- {% if detail %}data-page="post"{% endif %}
1352
- data-post
1353
- data-format="note"
1354
- data-post-permalink="{{ page.permalink }}"
1355
- {% if page.title %}data-post-has-title{% endif %}
1356
- {% if page.extra.pinned %}data-post-pinned{% endif %}
1357
- {% if page.extra.featured %}data-post-featured{% endif %}
1358
- data-post-visibility="{{ page.extra.visibility | default(value='public') }}"
1359
- >
1360
- {{ self::post_status_badges() }}
1361
- {% if page.title %}
1362
- {% if detail %}
1363
- <div class="post-header-block">
1364
- <h1 class="p-name detail-title">{{ page.title }}</h1>
1365
- {{ self::post_rating(page=page) }}
1366
- </div>
1367
- {% else %}
1368
- <h2 class="p-name feed-note-title">
1369
- <a href="{{ page.permalink }}" class="u-url post-title-link">{{ page.title }}</a>
1370
- </h2>
1371
- {% endif %}
1372
- {% endif %}
1373
- {% if detail and page.content %}
1374
- <div class="e-content prose" data-post-body>{{ page.content | safe }}</div>
1375
- {% elif not detail and page.summary %}
1376
- <div class="e-content prose {% if page.title %}post-body-summary{% endif %}" data-post-body>{{ page.summary | safe }}</div>
1377
- {% elif page.content %}
1378
- <div class="e-content prose {% if page.title and not detail %}post-body-summary{% endif %}" data-post-body>{{ page.content | safe }}</div>
1379
- {% endif %}
1380
- {% if not detail or not page.title %}
1381
- {{ self::post_rating(page=page) }}
1382
- {% endif %}
1383
- {{ self::post_footer(page=page, detail=detail) }}
1384
- </article>
1385
- {% endmacro %}
1386
-
1387
- {% macro link_card(page, detail=false) %}
1388
- <article
1389
- class="h-entry post-menu-target {% if detail %}post-detail-shell post-detail-link{% else %}feed-link-post{% endif %}"
1390
- {% if detail %}data-page="post"{% endif %}
1391
- data-post
1392
- data-format="link"
1393
- data-post-permalink="{{ page.permalink }}"
1394
- {% if page.title %}data-post-has-title{% endif %}
1395
- {% if page.extra.pinned %}data-post-pinned{% endif %}
1396
- {% if page.extra.featured %}data-post-featured{% endif %}
1397
- data-post-visibility="{{ page.extra.visibility | default(value='public') }}"
1398
- >
1399
- {% if not detail %}
1400
- <div class="feed-card feed-card-link">
1401
- {% endif %}
1402
- {{ self::post_status_badges() }}
1403
- {% if page.extra.link_url %}
1404
- <a href="{{ page.extra.link_url }}" class="feed-link-domain" rel="noopener noreferrer" target="_blank">
1405
- <svg class="feed-link-domain-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
1406
- <path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
1407
- </svg>
1408
- <span>{{ page.extra.link_url | split(pat='//') | nth(n=1) | split(pat='/') | first }}</span>
1409
- </a>
1410
- {% endif %}
1411
- {% if page.title %}
1412
- {% if detail %}
1413
- <div class="post-header-block">
1414
- <h1 class="p-name detail-title feed-link-title">
1415
- <a href="{{ page.extra.link_url | default(value=page.permalink) }}" class="u-url feed-link-title-link" {% if page.extra.link_url %}target="_blank" rel="noopener noreferrer"{% endif %}>{{ page.title }}</a>
1416
- </h1>
1417
- {{ self::post_rating(page=page) }}
1418
- </div>
1419
- {% else %}
1420
- <h2 class="p-name feed-link-title">
1421
- <a href="{{ page.extra.link_url | default(value=page.permalink) }}" class="u-url feed-link-title-link" {% if page.extra.link_url %}target="_blank" rel="noopener noreferrer"{% endif %}>{{ page.title }}</a>
1422
- </h2>
1423
- {% endif %}
1424
- {% endif %}
1425
- {% if detail and page.content %}
1426
- <div class="e-content prose feed-link-summary" data-post-body>{{ page.content | safe }}</div>
1427
- {% elif not detail and page.summary %}
1428
- <div class="e-content prose feed-link-summary" data-post-body>{{ page.summary | safe }}</div>
1429
- {% elif page.content %}
1430
- <div class="e-content prose feed-link-summary" data-post-body>{{ page.content | safe }}</div>
1431
- {% endif %}
1432
- {% if not detail or not page.title %}
1433
- {{ self::post_rating(page=page) }}
1434
- {% endif %}
1435
- {% if not detail %}
1436
- </div>
1437
- {% endif %}
1438
- {{ self::post_footer(page=page, detail=detail) }}
1439
- </article>
1440
- {% endmacro %}
1441
-
1442
- {% macro quote_card(page, detail=false) %}
1443
- <article
1444
- class="h-entry post-menu-target feed-quote-post {% if detail %}post-detail-shell{% endif %}"
1445
- {% if detail %}data-page="post"{% endif %}
1446
- data-post
1447
- data-format="quote"
1448
- data-post-permalink="{{ page.permalink }}"
1449
- {% if page.extra.pinned %}data-post-pinned{% endif %}
1450
- {% if page.extra.featured %}data-post-featured{% endif %}
1451
- data-post-visibility="{{ page.extra.visibility | default(value='public') }}"
1452
- >
1453
- {{ self::post_status_badges() }}
1454
- {% if page.extra.quote_text %}
1455
- <blockquote class="feed-quote feed-quote-card">
1456
- ${DECORATIVE_QUOTE_MARK_SVG}
1457
- <div class="e-content feed-quote-content">{{ page.extra.quote_text }}</div>
1458
- </blockquote>
1459
- {% endif %}
1460
- {% if page.extra.source_name or page.extra.source_url %}
1461
- <div class="feed-quote-attribution">
1462
- {% if page.extra.source_url %}
1463
- <a href="{{ page.extra.source_url }}" class="feed-quote-source" target="_blank" rel="noopener noreferrer">
1464
- {{ page.extra.source_name | default(value=page.extra.source_url | split(pat='//') | nth(n=1) | split(pat='/') | first) }}
1465
- </a>
1466
- {% else %}
1467
- <span>{{ page.extra.source_name }}</span>
1468
- {% endif %}
1469
- </div>
1470
- {% endif %}
1471
- {% if detail and page.content %}
1472
- <div class="feed-quote-commentary prose" data-post-body>{{ page.content | safe }}</div>
1473
- {% elif not detail and page.summary %}
1474
- <div class="feed-quote-commentary prose" data-post-body>{{ page.summary | safe }}</div>
1475
- {% elif page.content %}
1476
- <div class="feed-quote-commentary prose" data-post-body>{{ page.content | safe }}</div>
1477
- {% endif %}
1478
- {{ self::post_rating(page=page) }}
1479
- {{ self::post_footer(page=page, detail=detail) }}
1480
- </article>
1481
- {% endmacro %}
1482
-
1483
- {% macro post_card(page, detail=false) %}
1484
- {% if page.extra.format == "link" %}
1485
- {{ self::link_card(page=page, detail=detail) }}
1486
- {% elif page.extra.format == "quote" %}
1487
- {{ self::quote_card(page=page, detail=detail) }}
1488
- {% else %}
1489
- {{ self::note_card(page=page, detail=detail) }}
1490
- {% endif %}
1491
- {% endmacro %}
1692
+ - Each thread is a Hugo branch bundle. Replies live as nested leaf bundles with \`build.render = "never"\` so they do not produce standalone URLs; they render inside the thread page.
1693
+ - \`/{reply-slug}/\` URLs are preserved via \`aliases:\` on the root post, so old links still land on the right thread anchor.
1694
+ - Media is emitted under \`static/media/{id}.ext\` and referenced from a flat \`media:\` array on each post. When a storage provider has a configured public URL (R2/S3/local proxy), the exporter links to the provider URL instead of re-bundling the bytes.
1695
+ - Posts with \`draft: true\` in front matter are only built when you pass \`--buildDrafts\` to \`hugo\` / \`hugo serve\`.
1492
1696
  `;
1697
+ }
1493
1698
 
1494
1699
  // ---------------------------------------------------------------------------
1495
- // CSS Jant "Organic Minimalism" approximation
1700
+ // Re-exports for consumers (kept so existing entry points compile)
1496
1701
  // ---------------------------------------------------------------------------
1497
1702
 
1498
- const STYLE_CSS = `/* Jant Export Theme */
1499
-
1500
- :root {
1501
- color-scheme: light;
1502
- --site-width: 500px;
1503
- --font-cjk-serif-fallback:
1504
- "Songti SC", STSong, SimSun, "Songti TC", PMingLiU, MingLiU,
1505
- "Noto Serif SC", "Noto Serif CJK SC", "Noto Serif TC", "Noto Serif CJK TC";
1506
- --font-body:
1507
- system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
1508
- "Helvetica Neue", Helvetica, Arial, "PingFang TC", "PingFang SC",
1509
- "Hiragino Sans CNS", "Hiragino Sans GB", "Microsoft JhengHei",
1510
- "Microsoft YaHei", "Noto Sans CJK TC", "Noto Sans CJK SC", sans-serif;
1511
- --font-heading:
1512
- "New York Small", "New York", "Iowan Old Style", Charter,
1513
- "Bitstream Charter", "Source Serif 4", Cambria, "Sitka Text", Georgia,
1514
- var(--font-cjk-serif-fallback), ui-serif, serif;
1515
- --font-site-title:
1516
- "New York Small", "New York", "Iowan Old Style", Charter,
1517
- "Bitstream Charter", "Source Serif 4", Cambria, "Sitka Text", Georgia,
1518
- var(--font-cjk-serif-fallback), ui-serif, serif;
1519
- --font-serif:
1520
- var(--font-cjk-serif-fallback), ui-serif, "New York Small", "New York",
1521
- "Iowan Old Style", Charter, Georgia, "Times New Roman", Times, serif;
1522
- --font-mono:
1523
- ui-monospace, Menlo, Monaco, Consolas, "Cascadia Code", "Courier New",
1524
- monospace;
1525
- --fw-regular: 400;
1526
- --fw-medium: 500;
1527
- --fw-semibold: 600;
1528
- --text-sm: 0.8125rem;
1529
- --text-base: 0.9375rem;
1530
- --feed-note-title-size: 1.25rem;
1531
- --feed-note-title-leading: 1.3;
1532
- --type-body-size: var(--text-base);
1533
- --type-body-leading: 1.66;
1534
- --type-body-tracking: 0.002em;
1535
- --type-heading-weight: var(--fw-medium);
1536
- --type-heading-leading: 1.26;
1537
- --type-heading-tracking: -0.02em;
1538
- --type-display-leading: 1.04;
1539
- --type-label-weight: var(--fw-medium);
1540
- --type-label-tracking: 0.08em;
1541
- --site-padding: 1.5rem;
1542
- --content-gap: 1rem;
1543
- --space-xl: 2rem;
1544
- --avatar-size: 28px;
1545
- --avatar-radius: 50%;
1546
- --media-radius: 0.5rem;
1547
- --background: oklch(0.975 0.015 92);
1548
- --foreground: oklch(0.29 0.01 70);
1549
- --card: oklch(0.975 0.015 92);
1550
- --primary: oklch(0.3633 0.0697 159.95);
1551
- --primary-foreground: oklch(0.985 0.008 92);
1552
- --muted: oklch(0.942 0.014 96);
1553
- --muted-foreground: oklch(0.52 0.008 70);
1554
- --accent: oklch(0.942 0.014 96);
1555
- --border: oklch(0.892 0.014 98);
1556
- --site-accent: oklch(0.4406 0.0568 159.95);
1557
- --site-accent-text: var(--primary-foreground);
1558
- --site-page-bg: var(--background);
1559
- --site-elevated-bg: var(--background);
1560
- --site-nav-hover-bg: var(--accent);
1561
- --site-text-primary: var(--foreground);
1562
- --site-text-secondary: var(--muted-foreground);
1563
- --site-text-placeholder: oklch(from var(--muted-foreground) l c h / 0.5);
1564
- --site-divider: var(--border);
1565
- --site-feed-card-bg: color-mix(
1566
- in srgb,
1567
- var(--site-elevated-bg) 88%,
1568
- var(--site-nav-hover-bg)
1569
- );
1570
- --site-feed-card-border: color-mix(
1571
- in srgb,
1572
- var(--site-divider) 78%,
1573
- transparent
1574
- );
1575
- --site-feed-card-shadow: color-mix(
1576
- in srgb,
1577
- var(--site-text-primary) 12%,
1578
- transparent
1579
- );
1580
- --site-feed-divider-color: color-mix(
1581
- in srgb,
1582
- var(--site-text-secondary) 30%,
1583
- transparent
1584
- );
1585
- }
1586
-
1587
- @media (prefers-color-scheme: dark) {
1588
- :root:not([data-theme-mode="light"]) {
1589
- color-scheme: dark;
1590
- --background: oklch(0.182 0.003 95);
1591
- --foreground: oklch(0.895 0.006 88);
1592
- --card: oklch(0.182 0.003 95);
1593
- --primary: oklch(0.6966 0.0528 159.95);
1594
- --primary-foreground: oklch(0.17 0.003 95);
1595
- --muted: oklch(0.238 0.003 95);
1596
- --muted-foreground: oklch(0.67 0.005 88);
1597
- --accent: oklch(0.238 0.003 95);
1598
- --border: oklch(0.305 0.003 95);
1599
- --site-accent: oklch(0.7306 0.0478 159.95);
1600
- }
1601
- }
1602
-
1603
- *,
1604
- *::before,
1605
- *::after {
1606
- box-sizing: border-box;
1607
- }
1608
-
1609
- html {
1610
- font-size: 16px;
1611
- background-color: var(--site-page-bg);
1612
- color: var(--site-text-primary);
1613
- }
1614
-
1615
- body {
1616
- margin: 0;
1617
- font-family: var(--font-body);
1618
- font-size: var(--type-body-size);
1619
- line-height: var(--type-body-leading);
1620
- letter-spacing: var(--type-body-tracking);
1621
- color: var(--site-text-primary);
1622
- background: var(--site-page-bg);
1623
- text-rendering: optimizeLegibility;
1624
- -webkit-font-smoothing: antialiased;
1625
- }
1626
-
1627
- a {
1628
- color: inherit;
1629
- text-decoration-thickness: 1px;
1630
- text-underline-offset: 3px;
1631
- }
1632
-
1633
- img,
1634
- svg,
1635
- video {
1636
- display: block;
1637
- max-width: 100%;
1638
- }
1639
-
1640
- img {
1641
- height: auto;
1642
- }
1643
-
1644
- .site-page {
1645
- min-height: 100vh;
1646
- min-height: 100dvh;
1647
- background-color: var(--site-page-bg);
1648
- }
1649
-
1650
- .site-header {
1651
- max-width: var(--site-width);
1652
- margin: 0 auto;
1653
- padding: 24px var(--site-padding) 0;
1654
- }
1655
-
1656
- .site-header-inner {
1657
- display: flex;
1658
- flex-direction: column;
1659
- align-items: flex-start;
1660
- }
1661
-
1662
- .site-header-top {
1663
- display: flex;
1664
- align-items: center;
1665
- justify-content: space-between;
1666
- gap: 0.85rem;
1667
- flex-wrap: wrap;
1668
- min-height: 2.5rem;
1669
- width: 100%;
1670
- }
1671
-
1672
- .site-header-top-bordered {
1673
- padding-bottom: 15px;
1674
- border-bottom: 0.5px solid
1675
- color-mix(in srgb, var(--site-divider) 72%, transparent);
1676
- }
1677
-
1678
- .site-header-top-home {
1679
- border-bottom-color: color-mix(in srgb, var(--site-divider) 72%, transparent);
1680
- padding-bottom: 14px;
1681
- }
1682
-
1683
- .site-header-right {
1684
- display: flex;
1685
- align-items: center;
1686
- gap: 0.55rem;
1687
- margin-left: auto;
1688
- min-width: 0;
1689
- }
1690
-
1691
- .site-logo {
1692
- display: inline-flex;
1693
- align-items: center;
1694
- gap: 10px;
1695
- padding: 0.15rem 0;
1696
- font-size: clamp(1.18rem, 1.08rem + 0.45vw, 1.34rem);
1697
- font-weight: var(--fw-medium);
1698
- font-family: var(--font-site-title);
1699
- letter-spacing: -0.03em;
1700
- color: var(--site-text-primary);
1701
- text-decoration: none;
1702
- line-height: var(--type-display-leading);
1703
- }
1704
-
1705
- .site-logo-avatar {
1706
- width: calc(var(--avatar-size) + 2px);
1707
- height: calc(var(--avatar-size) + 2px);
1708
- border-radius: var(--avatar-radius);
1709
- object-fit: cover;
1710
- box-shadow: 0 0 0 1px color-mix(in srgb, var(--site-divider) 82%, transparent);
1711
- }
1712
-
1713
- .site-header-nav {
1714
- display: flex;
1715
- align-items: center;
1716
- flex-wrap: wrap;
1717
- justify-content: flex-end;
1718
- gap: 1rem;
1719
- }
1720
-
1721
- .site-header-link {
1722
- display: inline-flex;
1723
- align-items: center;
1724
- position: relative;
1725
- min-height: 2rem;
1726
- padding: 0.2rem 0 0.5rem;
1727
- cursor: pointer;
1728
- font-size: 0.84rem;
1729
- line-height: 1;
1730
- letter-spacing: 0.01em;
1731
- color: var(--site-text-secondary);
1732
- text-decoration: none;
1733
- transition:
1734
- color 0.15s,
1735
- opacity 0.15s;
1736
- }
1737
-
1738
- .site-header-link::after {
1739
- content: "";
1740
- position: absolute;
1741
- right: 0;
1742
- bottom: 0;
1743
- left: 0;
1744
- height: 1.5px;
1745
- border-radius: 999px;
1746
- background: color-mix(in srgb, var(--site-accent) 62%, var(--site-divider));
1747
- opacity: 0;
1748
- transform: scaleX(0.52);
1749
- transform-origin: center;
1750
- transition:
1751
- opacity 0.18s ease,
1752
- transform 0.18s ease;
1753
- }
1754
-
1755
- .site-header-link:hover {
1756
- color: var(--site-text-primary);
1757
- opacity: 1;
1758
- }
1759
-
1760
- .site-header-link:hover::after {
1761
- opacity: 0.42;
1762
- transform: scaleX(0.82);
1763
- }
1764
-
1765
- .site-container {
1766
- max-width: var(--site-width);
1767
- margin: 0 auto;
1768
- }
1769
-
1770
- .site-content {
1771
- background-color: var(--site-elevated-bg);
1772
- padding: 1rem var(--site-padding) var(--space-xl);
1773
- }
1774
-
1775
- .site-content-home {
1776
- padding-top: 0.75rem;
1777
- padding-bottom: calc(var(--space-xl) - 0.25rem);
1778
- border-bottom: 0.5px solid
1779
- color-mix(in srgb, var(--site-divider) 84%, transparent);
1780
- }
1781
-
1782
- .site-home-header {
1783
- display: flex;
1784
- flex-direction: column;
1785
- gap: 0.15rem;
1786
- margin-bottom: var(--space-xl);
1787
- }
1788
-
1789
- .site-browse-nav {
1790
- display: flex;
1791
- align-items: baseline;
1792
- flex-wrap: wrap;
1793
- gap: 0.55rem;
1794
- padding: 14px 0 4px;
1795
- }
1796
-
1797
- .site-browse-link {
1798
- font-size: var(--text-base);
1799
- font-weight: var(--fw-regular);
1800
- color: var(--site-text-primary);
1801
- opacity: 0.42;
1802
- }
1803
-
1804
- .site-browse-link-active {
1805
- opacity: 1;
1806
- font-weight: var(--fw-medium);
1807
- }
1808
-
1809
- .page-context-label {
1810
- margin: 0 0 1rem;
1811
- color: var(--site-text-secondary);
1812
- font-size: var(--text-sm);
1813
- }
1814
-
1815
- .feed-item {
1816
- position: relative;
1817
- }
1818
-
1819
- .site-content hr.feed-divider {
1820
- border: none;
1821
- width: 30px;
1822
- height: 9px;
1823
- margin: 2.5rem auto;
1824
- color: var(--site-feed-divider-color);
1825
- background-color: currentColor;
1826
- -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 45 13'%3E%3Cpath fill='black' transform='translate(0,0) rotate(90 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(16,0) rotate(100 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(32,0) rotate(80 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3C/svg%3E");
1827
- mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 45 13'%3E%3Cpath fill='black' transform='translate(0,0) rotate(90 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(16,0) rotate(100 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3Cpath fill='black' transform='translate(32,0) rotate(80 6 6.5)' d='M6.765.5.177 6.093l2.61 5.966 8.39-3.17L6.765.5Z'/%3E%3C/svg%3E");
1828
- -webkit-mask-repeat: no-repeat;
1829
- mask-repeat: no-repeat;
1830
- -webkit-mask-position: center;
1831
- mask-position: center;
1832
- -webkit-mask-size: contain;
1833
- mask-size: contain;
1834
- }
1835
-
1836
- .post-menu-target {
1837
- position: relative;
1838
- }
1839
-
1840
- .post-card-shell,
1841
- .post-detail-shell {
1842
- position: relative;
1843
- padding: 0.45rem 0 0.35rem;
1844
- }
1845
-
1846
- .feed-card {
1847
- position: relative;
1848
- padding: 1rem 1.1rem 0.95rem;
1849
- border: 1px solid color-mix(in srgb, var(--site-divider) 78%, transparent);
1850
- border-radius: 18px;
1851
- background:
1852
- linear-gradient(
1853
- 180deg,
1854
- color-mix(in srgb, var(--site-accent) 4%, transparent),
1855
- transparent 58%
1856
- ),
1857
- color-mix(in srgb, var(--site-elevated-bg) 88%, var(--site-nav-hover-bg));
1858
- box-shadow: 0 20px 40px -36px var(--site-feed-card-shadow);
1859
- }
1860
-
1861
- .feed-card-link {
1862
- border-radius: 14px;
1863
- }
1864
-
1865
- .feed-link-post {
1866
- --feed-link-post-footer-inset: 0.65rem;
1867
- display: flex;
1868
- flex-direction: column;
1869
- }
1870
-
1871
- .feed-link-post > .post-menu-footer {
1872
- margin-top: 0.5rem;
1873
- padding-inline: 0;
1874
- padding-inline-start: var(--feed-link-post-footer-inset);
1875
- padding-inline-end: 0.15rem;
1876
- }
1877
-
1878
- .feed-link-domain {
1879
- display: inline-flex;
1880
- align-items: center;
1881
- gap: 0.25rem;
1882
- max-width: 100%;
1883
- margin-bottom: 0.65rem;
1884
- color: var(--site-text-secondary);
1885
- font-size: 0.75rem;
1886
- font-weight: var(--type-label-weight);
1887
- line-height: 1;
1888
- letter-spacing: var(--type-label-tracking);
1889
- text-decoration: none;
1890
- }
1891
-
1892
- .feed-link-domain-icon {
1893
- width: 0.72rem;
1894
- height: 0.72rem;
1895
- flex-shrink: 0;
1896
- }
1897
-
1898
- .feed-link-title,
1899
- .feed-note-title,
1900
- .detail-title,
1901
- .section-title,
1902
- .p-name {
1903
- font-family: var(--font-heading);
1904
- }
1905
-
1906
- .feed-link-title {
1907
- margin: 0;
1908
- font-size: 0.98rem;
1909
- font-weight: var(--type-heading-weight);
1910
- line-height: var(--type-heading-leading);
1911
- letter-spacing: var(--type-heading-tracking);
1912
- }
1913
-
1914
- .feed-link-title-link,
1915
- .post-title-link {
1916
- color: inherit;
1917
- text-decoration: none;
1918
- }
1919
-
1920
- .feed-link-title-link:hover,
1921
- .post-title-link:hover {
1922
- text-decoration: underline;
1923
- }
1924
-
1925
- .feed-note-title {
1926
- margin: 0 0 0.48rem;
1927
- font-size: var(--feed-note-title-size);
1928
- line-height: var(--feed-note-title-leading);
1929
- letter-spacing: var(--type-heading-tracking);
1930
- text-wrap: pretty;
1931
- }
1932
-
1933
- .post-header-block {
1934
- margin-bottom: 1rem;
1935
- }
1936
-
1937
- .post-header-block .feed-link-title,
1938
- .post-header-block .feed-note-title,
1939
- .post-header-block .detail-title {
1940
- margin-bottom: 0;
1941
- }
1942
-
1943
- .post-header-block .post-rating {
1944
- margin-top: 0.45rem;
1945
- }
1946
-
1947
- .detail-title {
1948
- margin: 0 0 1rem;
1949
- font-size: clamp(1.56rem, 1.34rem + 1vw, 2.02rem);
1950
- font-weight: var(--fw-medium);
1951
- line-height: 1.08;
1952
- letter-spacing: -0.03em;
1953
- text-wrap: balance;
1954
- }
1955
-
1956
- .feed-quote-post {
1957
- position: relative;
1958
- padding: 0.45rem 0 0.35rem;
1959
- }
1960
-
1961
- .feed-quote {
1962
- margin: 0;
1963
- }
1964
-
1965
- .feed-quote-card {
1966
- padding-left: 0;
1967
- border-left: none;
1968
- }
1969
-
1970
- .decorative-quote-mark {
1971
- display: block;
1972
- line-height: 0;
1973
- }
1974
-
1975
- .decorative-quote-mark svg {
1976
- display: block;
1977
- width: 100%;
1978
- height: auto;
1979
- }
1980
-
1981
- .feed-quote-mark {
1982
- width: clamp(1.46rem, 1.38rem + 0.36vw, 1.76rem);
1983
- margin-bottom: -0.1rem;
1984
- margin-left: -0.04rem;
1985
- color: color-mix(in srgb, var(--site-accent) 14%, var(--site-divider));
1986
- opacity: 0.66;
1987
- }
1988
-
1989
- .feed-quote-content {
1990
- font-family: var(--font-serif);
1991
- color: var(--site-text-primary);
1992
- font-size: clamp(1.34rem, 1.23rem + 0.44vw, 1.58rem);
1993
- line-height: 1.36;
1994
- letter-spacing: -0.02em;
1995
- text-wrap: pretty;
1996
- }
1997
-
1998
- .feed-quote-attribution {
1999
- display: flex;
2000
- align-items: center;
2001
- gap: 0.45rem;
2002
- flex-wrap: wrap;
2003
- margin-top: 0.95rem;
2004
- color: var(--site-text-secondary);
2005
- font-size: 0.75rem;
2006
- letter-spacing: 0.08em;
2007
- line-height: 1.4;
2008
- }
2009
-
2010
- .feed-quote-attribution::before {
2011
- content: "";
2012
- width: 0.9rem;
2013
- height: 1px;
2014
- background: color-mix(in srgb, var(--site-text-secondary) 38%, var(--site-divider));
2015
- }
2016
-
2017
- .feed-quote-source {
2018
- color: inherit;
2019
- text-decoration: underline;
2020
- text-decoration-color: color-mix(in srgb, var(--site-text-secondary) 55%, transparent);
2021
- }
2022
-
2023
- .feed-quote-commentary {
2024
- position: relative;
2025
- max-width: 34rem;
2026
- margin-top: 1.1rem;
2027
- padding-top: 0.95rem;
2028
- color: color-mix(in srgb, var(--site-text-secondary) 84%, var(--site-text-primary));
2029
- }
2030
-
2031
- .feed-quote-commentary::before {
2032
- content: "";
2033
- position: absolute;
2034
- left: 0;
2035
- right: 0;
2036
- top: 0;
2037
- height: 1px;
2038
- background: linear-gradient(
2039
- 90deg,
2040
- transparent 0%,
2041
- color-mix(in srgb, var(--site-divider) 48%, transparent) 16%,
2042
- color-mix(in srgb, var(--site-divider) 78%, transparent) 50%,
2043
- color-mix(in srgb, var(--site-divider) 48%, transparent) 84%,
2044
- transparent 100%
2045
- );
2046
- }
2047
-
2048
- .post-status-badges {
2049
- display: none;
2050
- align-items: center;
2051
- gap: 6px;
2052
- margin-bottom: 4px;
2053
- font-size: 11px;
2054
- font-weight: 500;
2055
- letter-spacing: 0.05em;
2056
- text-transform: uppercase;
2057
- color: var(--site-text-placeholder);
2058
- }
2059
-
2060
- article[data-post-pinned] .post-status-badges,
2061
- article[data-post-visibility="private"] .post-status-badges {
2062
- display: flex;
2063
- }
2064
-
2065
- .post-status-badge {
2066
- display: none;
2067
- align-items: center;
2068
- gap: 4px;
2069
- }
2070
-
2071
- article[data-post-pinned] .post-status-pinned,
2072
- article[data-post-visibility="private"] .post-status-private {
2073
- display: inline-flex;
2074
- }
2075
-
2076
- .post-status-badge svg {
2077
- width: 12px;
2078
- height: 12px;
2079
- }
2080
-
2081
- .post-footer-featured {
2082
- display: none;
2083
- align-items: center;
2084
- justify-content: center;
2085
- color: color-mix(in srgb, var(--search-mark-color) 72%, var(--site-text-secondary));
2086
- --icon-stroke: 1.35;
2087
- flex-shrink: 0;
2088
- }
2089
-
2090
- article[data-post-featured] .post-footer-featured {
2091
- display: inline-flex;
2092
- }
2093
-
2094
- .post-footer-featured svg {
2095
- width: 1.12rem;
2096
- height: 1.12rem;
2097
- opacity: 0.88;
2098
- }
2099
-
2100
- [data-page="featured"] article[data-post-featured] .post-footer-featured {
2101
- display: none;
2102
- }
2103
-
2104
- .sr-only {
2105
- position: absolute;
2106
- width: 1px;
2107
- height: 1px;
2108
- padding: 0;
2109
- margin: -1px;
2110
- overflow: hidden;
2111
- clip: rect(0, 0, 0, 0);
2112
- white-space: nowrap;
2113
- border: 0;
2114
- }
2115
-
2116
- .prose {
2117
- max-width: 35rem;
2118
- font-size: var(--type-body-size);
2119
- line-height: var(--type-body-leading);
2120
- letter-spacing: var(--type-body-tracking);
2121
- color: var(--site-text-primary);
2122
- }
2123
-
2124
- .post-body-summary {
2125
- color: color-mix(in srgb, var(--site-text-secondary) 88%, var(--site-text-primary));
2126
- }
2127
-
2128
- .prose > :first-child {
2129
- margin-top: 0;
2130
- }
2131
-
2132
- .prose > :last-child {
2133
- margin-bottom: 0;
2134
- }
2135
-
2136
- .prose p {
2137
- margin: 0;
2138
- }
2139
-
2140
- .prose p + p,
2141
- .prose ul,
2142
- .prose ol,
2143
- .prose blockquote,
2144
- .prose pre,
2145
- .prose table,
2146
- .prose figure {
2147
- margin-top: 1.05rem;
2148
- }
2149
-
2150
- .prose :is(h1, h2, h3, h4) {
2151
- margin: 1.25rem 0 0.35rem;
2152
- font-family: var(--font-heading);
2153
- font-weight: var(--type-heading-weight);
2154
- line-height: var(--type-heading-leading);
2155
- letter-spacing: var(--type-heading-tracking);
2156
- }
2157
-
2158
- .prose ul,
2159
- .prose ol {
2160
- padding-left: 1.3rem;
2161
- }
2162
-
2163
- .prose li {
2164
- margin: 0.2rem 0;
2165
- }
2166
-
2167
- .prose blockquote {
2168
- padding-left: 0.95rem;
2169
- border-left: 2px solid color-mix(in srgb, var(--site-divider) 75%, transparent);
2170
- color: var(--site-text-secondary);
2171
- }
2172
-
2173
- .prose code {
2174
- font-family: var(--font-mono);
2175
- font-size: 0.875em;
2176
- background: color-mix(in srgb, var(--site-nav-hover-bg) 80%, transparent);
2177
- padding: 0.1rem 0.35rem;
2178
- border-radius: 0.32rem;
2179
- }
2180
-
2181
- .prose pre {
2182
- overflow-x: auto;
2183
- padding: 0.9rem 1rem;
2184
- border-radius: 14px;
2185
- border: 1px solid color-mix(in srgb, var(--site-divider) 82%, transparent);
2186
- background: color-mix(in srgb, var(--site-nav-hover-bg) 78%, transparent);
2187
- }
2188
-
2189
- .prose pre code {
2190
- background: transparent;
2191
- padding: 0;
2192
- }
2193
-
2194
- .prose table {
2195
- width: 100%;
2196
- border-collapse: collapse;
2197
- font-size: var(--text-sm);
2198
- }
2199
-
2200
- .prose th,
2201
- .prose td {
2202
- border: 1px solid color-mix(in srgb, var(--site-divider) 86%, transparent);
2203
- padding: 0.45rem 0.7rem;
2204
- text-align: left;
2205
- }
2206
-
2207
- .prose th {
2208
- background: color-mix(in srgb, var(--site-nav-hover-bg) 74%, transparent);
2209
- }
2210
-
2211
- .prose img,
2212
- .prose video {
2213
- width: 100%;
2214
- border-radius: var(--media-radius);
2215
- }
2216
-
2217
- .prose figure[data-jant-node="image"] {
2218
- margin-inline: 0;
2219
- }
2220
-
2221
- .prose figcaption {
2222
- margin-top: 0.55rem;
2223
- color: var(--site-text-secondary);
2224
- font-size: var(--text-sm);
2225
- }
2226
-
2227
- [data-jant-node="attachments"] {
2228
- display: grid;
2229
- gap: 0.85rem;
2230
- margin-top: 1rem;
2231
- }
2232
-
2233
- [data-jant-node="attachment"] {
2234
- margin: 0;
2235
- padding: 0.95rem;
2236
- border: 1px solid color-mix(in srgb, var(--site-divider) 84%, transparent);
2237
- border-radius: 16px;
2238
- background: color-mix(in srgb, var(--site-nav-hover-bg) 66%, transparent);
2239
- }
2240
-
2241
- [data-jant-node="attachment"] > script[data-jant-meta] {
2242
- display: none;
2243
- }
2244
-
2245
- [data-jant-node="attachment"][data-jant-kind="image"] {
2246
- padding: 0;
2247
- overflow: hidden;
2248
- background: transparent;
2249
- }
2250
-
2251
- [data-jant-node="attachment"] audio,
2252
- [data-jant-node="attachment"] video {
2253
- width: 100%;
2254
- }
2255
-
2256
- [data-jant-node="attachment"] > a {
2257
- display: inline-flex;
2258
- font-weight: var(--fw-medium);
2259
- text-decoration: none;
2260
- }
2261
-
2262
- [data-jant-node="attachment"][data-jant-kind="text"] details {
2263
- width: 100%;
2264
- }
2265
-
2266
- [data-jant-node="attachment"][data-jant-kind="text"] summary {
2267
- cursor: pointer;
2268
- font-weight: var(--fw-medium);
2269
- }
2270
-
2271
- .jant-attachment-text-preview {
2272
- margin-top: 0.85rem;
2273
- }
2274
-
2275
- .jant-attachment-text-preview > :first-child {
2276
- margin-top: 0;
2277
- }
2278
-
2279
- .jant-attachment-text-preview > :last-child {
2280
- margin-bottom: 0;
2281
- }
2282
-
2283
- .post-rating {
2284
- display: flex;
2285
- gap: 1px;
2286
- margin-top: 8px;
2287
- font-size: 14px;
2288
- line-height: 1;
2289
- }
2290
-
2291
- .post-star-filled {
2292
- color: oklch(0.75 0.15 70);
2293
- }
2294
-
2295
- .post-star-empty {
2296
- color: var(--site-divider);
2297
- }
2298
-
2299
- .post-menu-footer {
2300
- display: flex;
2301
- justify-content: flex-start;
2302
- align-items: center;
2303
- margin-top: 0.9rem;
2304
- }
2305
-
2306
- .post-footer-detail {
2307
- margin-top: 1.35rem;
2308
- padding-top: 1rem;
2309
- border-top: 1px solid color-mix(in srgb, var(--site-divider) 86%, transparent);
2310
- }
2311
-
2312
- .post-footer-meta {
2313
- display: flex;
2314
- align-items: center;
2315
- gap: 5px;
2316
- flex-wrap: wrap;
2317
- line-height: 1.35;
2318
- min-width: 0;
2319
- }
2320
-
2321
- .post-date-link {
2322
- color: var(--site-text-secondary);
2323
- text-decoration: none;
2324
- font-size: 13px;
2325
- white-space: nowrap;
2326
- flex-shrink: 0;
2327
- }
2328
-
2329
- .post-date-link:hover {
2330
- color: var(--site-text-primary);
2331
- text-decoration: underline;
2332
- }
2333
-
2334
- .post-footer-detail .post-date-link {
2335
- font-size: inherit;
2336
- }
2337
-
2338
- .post-footer-external-link {
2339
- display: inline-flex;
2340
- align-items: center;
2341
- justify-content: center;
2342
- width: 0.9rem;
2343
- height: 0.9rem;
2344
- color: var(--site-text-secondary);
2345
- flex-shrink: 0;
2346
- }
2347
-
2348
- .post-footer-external-link svg {
2349
- width: 0.82rem;
2350
- height: 0.82rem;
2351
- }
2352
-
2353
- .post-collection-tags {
2354
- display: inline-flex;
2355
- align-items: center;
2356
- gap: 4px;
2357
- font-size: 13px;
2358
- min-width: 0;
2359
- max-width: 100%;
2360
- flex: 1 1 auto;
2361
- }
2362
-
2363
- .post-collection-sep {
2364
- color: var(--site-text-secondary);
2365
- }
2366
-
2367
- .post-collection-tag {
2368
- display: inline-block;
2369
- color: var(--site-text-secondary);
2370
- text-decoration: none;
2371
- min-width: 0;
2372
- max-width: min(100%, 22ch);
2373
- overflow: hidden;
2374
- text-overflow: ellipsis;
2375
- white-space: nowrap;
2376
- }
2377
-
2378
- .post-collection-tag:hover {
2379
- color: var(--site-text-primary);
2380
- text-decoration: underline;
2381
- }
2382
-
2383
- .post-collection-more-wrap {
2384
- position: relative;
2385
- display: inline-flex;
2386
- align-items: center;
2387
- flex-shrink: 0;
2388
- }
2389
-
2390
- .post-collection-more-wrap::after {
2391
- content: "";
2392
- position: absolute;
2393
- top: 100%;
2394
- left: -6px;
2395
- right: -6px;
2396
- height: 10px;
2397
- }
2398
-
2399
- .post-collection-more {
2400
- display: inline-flex;
2401
- align-items: center;
2402
- background: none;
2403
- border: none;
2404
- padding: 0;
2405
- font: inherit;
2406
- font-size: inherit;
2407
- color: var(--site-text-secondary);
2408
- text-decoration: underline dotted;
2409
- text-underline-offset: 2px;
2410
- cursor: pointer;
2411
- }
2412
-
2413
- .post-collection-more-wrap:hover .post-collection-more,
2414
- .post-collection-more-wrap:focus-within .post-collection-more {
2415
- color: var(--site-text-primary);
2416
- }
2417
-
2418
- .post-collection-popover {
2419
- display: none;
2420
- position: absolute;
2421
- top: calc(100% + 4px);
2422
- left: 0;
2423
- z-index: 50;
2424
- flex-direction: column;
2425
- min-width: 160px;
2426
- padding: 4px;
2427
- border-radius: 6px;
2428
- background: var(--site-elevated-bg);
2429
- border: 1px solid var(--site-divider);
2430
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
2431
- }
2432
-
2433
- .post-collection-more-wrap:hover .post-collection-popover,
2434
- .post-collection-more-wrap:focus-within .post-collection-popover {
2435
- display: flex;
2436
- }
2437
-
2438
- .post-collection-popover-item {
2439
- display: flex;
2440
- align-items: center;
2441
- gap: 6px;
2442
- padding: 6px 8px;
2443
- border-radius: 4px;
2444
- font-size: 12px;
2445
- color: var(--site-text-secondary);
2446
- text-decoration: none;
2447
- }
2448
-
2449
- .post-collection-popover-item:hover {
2450
- background: var(--site-nav-hover-bg);
2451
- color: var(--site-text-primary);
2452
- }
2453
-
2454
- .section-shell {
2455
- display: flex;
2456
- flex-direction: column;
2457
- gap: 1.15rem;
2458
- }
2459
-
2460
- .section-header {
2461
- display: flex;
2462
- flex-direction: column;
2463
- gap: 0.45rem;
2464
- }
2465
-
2466
- .section-title {
2467
- margin: 0;
2468
- font-size: clamp(1.45rem, 1.3rem + 0.5vw, 1.85rem);
2469
- font-weight: var(--fw-medium);
2470
- line-height: 1.12;
2471
- letter-spacing: -0.03em;
2472
- }
2473
-
2474
- .section-description {
2475
- margin: 0;
2476
- max-width: 38rem;
2477
- color: var(--site-text-secondary);
2478
- }
2479
-
2480
- .collection-list {
2481
- margin: 0;
2482
- padding: 0;
2483
- list-style: none;
2484
- counter-reset: collection-list;
2485
- }
2486
-
2487
- .collection-list-item {
2488
- counter-increment: collection-list;
2489
- border-top: 1px solid color-mix(in srgb, var(--site-divider) 84%, transparent);
2490
- }
2491
-
2492
- .collection-list-link {
2493
- display: grid;
2494
- grid-template-columns: 3.5ch minmax(0, 1fr);
2495
- gap: 0.8rem;
2496
- align-items: start;
2497
- padding: 0.95rem 0;
2498
- text-decoration: none;
2499
- }
2500
-
2501
- .collection-list-link:hover .collection-list-title {
2502
- text-decoration: underline;
2503
- }
2504
-
2505
- .collection-list-link:hover .collection-list-sequence {
2506
- color: var(--site-text-primary);
2507
- }
2508
-
2509
- .collection-list-sequence {
2510
- display: block;
2511
- width: 3.5ch;
2512
- padding-top: 0.2rem;
2513
- font-family: var(--font-mono);
2514
- font-size: 0.68rem;
2515
- font-variant-numeric: tabular-nums;
2516
- line-height: 1.18;
2517
- letter-spacing: 0.14em;
2518
- color: var(--site-text-secondary);
2519
- transition: color 0.15s ease;
2520
- }
2521
-
2522
- .collection-list-sequence::before {
2523
- content: counter(collection-list, decimal-leading-zero);
2524
- }
2525
-
2526
- .collection-list-content {
2527
- min-width: 0;
2528
- display: flex;
2529
- flex-direction: column;
2530
- gap: 0.28rem;
2531
- }
2532
-
2533
- .collection-list-title {
2534
- min-width: 0;
2535
- display: inline-flex;
2536
- align-items: center;
2537
- gap: 0.45rem;
2538
- font-family: var(--font-heading);
2539
- font-size: clamp(1rem, 1.5vw, 1.12rem);
2540
- font-weight: var(--fw-medium);
2541
- line-height: 1.18;
2542
- letter-spacing: -0.01em;
2543
- }
2544
-
2545
- .collection-list-meta {
2546
- display: flex;
2547
- flex-wrap: wrap;
2548
- gap: 0.5rem 1rem;
2549
- color: var(--site-text-secondary);
2550
- font-size: var(--text-sm);
2551
- }
2552
-
2553
- .archive-shell {
2554
- gap: 1.35rem;
2555
- }
2556
-
2557
- .archive-list {
2558
- display: grid;
2559
- gap: 0;
2560
- }
2561
-
2562
- .archive-month-group + .archive-month-group {
2563
- margin-top: 1.35rem;
2564
- }
2565
-
2566
- .archive-month-heading {
2567
- margin: 0 0 0.45rem;
2568
- color: var(--site-text-secondary);
2569
- font-size: 0.82rem;
2570
- font-weight: var(--fw-medium);
2571
- letter-spacing: 0.08em;
2572
- text-transform: uppercase;
2573
- }
2574
-
2575
- .archive-entry {
2576
- display: grid;
2577
- grid-template-columns: minmax(6.75rem, auto) minmax(0, 1fr);
2578
- gap: 0.9rem 1.25rem;
2579
- align-items: start;
2580
- padding: 0.9rem 0;
2581
- border-top: 1px solid color-mix(in srgb, var(--site-divider) 84%, transparent);
2582
- }
2583
-
2584
- .archive-entry:first-child {
2585
- border-top: none;
2586
- padding-top: 0;
2587
- }
2588
-
2589
- .archive-entry-date {
2590
- color: var(--site-text-secondary);
2591
- font-size: var(--text-sm);
2592
- line-height: 1.5;
2593
- text-decoration: none;
2594
- }
2595
-
2596
- .archive-entry-main {
2597
- min-width: 0;
2598
- }
2599
-
2600
- .archive-entry-title {
2601
- display: inline-block;
2602
- color: var(--site-text-primary);
2603
- font-family: var(--font-heading);
2604
- font-size: 1.03rem;
2605
- font-weight: var(--type-heading-weight);
2606
- line-height: 1.34;
2607
- letter-spacing: var(--type-heading-tracking);
2608
- text-decoration: none;
2609
- text-wrap: pretty;
2610
- }
2611
-
2612
- .archive-entry-title:hover {
2613
- text-decoration: underline;
2614
- }
2615
-
2616
- .archive-entry-meta {
2617
- display: flex;
2618
- align-items: center;
2619
- gap: 0.45rem;
2620
- flex-wrap: wrap;
2621
- margin-top: 0.38rem;
2622
- color: var(--site-text-secondary);
2623
- font-size: 0.72rem;
2624
- line-height: 1.45;
2625
- letter-spacing: 0.06em;
2626
- text-transform: uppercase;
2627
- }
2628
-
2629
- .archive-entry-format {
2630
- opacity: 0.7;
2631
- }
2632
-
2633
- .archive-entry-tag {
2634
- color: inherit;
2635
- text-decoration: none;
2636
- border-bottom: 0.5px solid color-mix(in srgb, var(--site-divider) 82%, transparent);
2637
- }
2638
-
2639
- .archive-entry-tag:hover {
2640
- color: var(--site-text-primary);
2641
- border-bottom-color: currentColor;
2642
- }
2643
-
2644
- .pagination {
2645
- display: grid;
2646
- grid-template-columns: 1fr auto 1fr;
2647
- align-items: center;
2648
- gap: 1rem;
2649
- padding: 2rem 0 0.5rem;
2650
- }
2651
-
2652
- .pagination-side-end {
2653
- text-align: right;
2654
- }
2655
-
2656
- .pagination-center {
2657
- text-align: center;
2658
- }
2659
-
2660
- .pagination-link {
2661
- color: var(--site-text-secondary);
2662
- text-decoration: underline;
2663
- }
2664
-
2665
- .pagination-link:hover {
2666
- color: var(--site-text-primary);
2667
- }
2668
-
2669
- .pagination-label {
2670
- color: var(--site-text-secondary);
2671
- font-size: var(--text-sm);
2672
- }
2673
-
2674
- .site-footer {
2675
- max-width: var(--site-width);
2676
- margin: var(--space-xl) auto 0;
2677
- padding: 0 var(--site-padding) var(--space-xl);
2678
- color: var(--site-text-secondary);
2679
- font-size: var(--text-sm);
2680
- }
2681
-
2682
- .site-footer > .site-container {
2683
- border-top: 0.5px solid var(--site-divider);
2684
- padding-top: var(--content-gap);
2685
- }
2686
-
2687
- .home-branding-credit {
2688
- margin-top: var(--space-xl);
2689
- text-align: center;
2690
- color: var(--site-text-secondary);
2691
- font-size: var(--text-sm);
2692
- }
2693
-
2694
- .home-branding-credit a {
2695
- display: inline-flex;
2696
- align-items: center;
2697
- gap: 0.38rem;
2698
- color: inherit;
2699
- text-decoration: none;
2700
- border-bottom: 0.5px solid
2701
- color-mix(in srgb, var(--site-text-secondary) 45%, transparent);
2702
- }
2703
-
2704
- .home-branding-credit a svg {
2705
- width: 1rem;
2706
- height: 1rem;
2707
- flex: none;
2708
- }
2709
-
2710
- @media (min-width: 700px) {
2711
- .site-header {
2712
- padding-top: 30px;
2713
- }
2714
-
2715
- .site-header-top-bordered {
2716
- padding-bottom: 18px;
2717
- }
2718
- }
2719
-
2720
- @media (max-width: 699px) {
2721
- :root {
2722
- --site-padding: 1.875rem;
2723
- }
2724
-
2725
- .site-header-right {
2726
- width: 100%;
2727
- justify-content: flex-start;
2728
- }
2729
-
2730
- .site-header-nav {
2731
- justify-content: flex-start;
2732
- }
2733
- }
2734
-
2735
- @media (max-width: 640px) {
2736
- .archive-entry {
2737
- grid-template-columns: 1fr;
2738
- gap: 0.3rem;
2739
- }
2740
-
2741
- .pagination {
2742
- grid-template-columns: 1fr;
2743
- gap: 0.55rem;
2744
- }
2745
-
2746
- .pagination-side-end,
2747
- .pagination-center {
2748
- text-align: left;
2749
- }
2750
- }
2751
- `;
1703
+ export {
1704
+ buildSiteIconAssets,
1705
+ buildExportedCollectionMetrics,
1706
+ buildExportedCollectionDirectoryItems,
1707
+ readStorageObjectBytes,
1708
+ getArchiveSummaryText,
1709
+ getMediaUrl,
1710
+ getPublicUrlForProvider,
1711
+ };