@jant/core 0.3.39 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1162) hide show
  1. package/README.md +21 -33
  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.js +53 -0
  10. package/bin/commands/settings.js +93 -0
  11. package/bin/commands/site/export.js +34 -33
  12. package/bin/commands/site/{localize-media.js → pull-media.js} +17 -17
  13. package/bin/lib/d1-query.js +15 -3
  14. package/bin/lib/http-api.js +223 -0
  15. package/bin/lib/media-upload.js +206 -0
  16. package/bin/lib/migration-runner.js +64 -0
  17. package/bin/lib/runtime-target.js +7 -2
  18. package/bin/lib/site-media-parser.js +71 -16
  19. package/bin/lib/site-pull-media.js +698 -0
  20. package/bin/lib/site-snapshot.js +0 -1
  21. package/dist/app-B5db9mS0.js +5 -0
  22. package/dist/{app-BfoG98VD.js → app-CwCepTko.js} +23938 -23014
  23. package/dist/client/.vite/manifest.json +3485 -0
  24. package/dist/client/_assets/chunks/02611a045a7fe83a12014e3debc9f731-BqEDtwXC.woff2 +0 -0
  25. package/dist/client/_assets/chunks/033466ef683afe931f7f520cfb42d928-BluLHEh9.woff2 +0 -0
  26. package/dist/client/_assets/chunks/04b8bde59cff68eeee74fb1914eb09c7-D5j2CLTt.woff2 +0 -0
  27. package/dist/client/_assets/chunks/057a6a98bda7fe57105ddaa99ec82015-DY_AoijT.woff2 +0 -0
  28. package/dist/client/_assets/chunks/05ac821472e235943ed1435e4bb8ecad-CUm8GiH6.woff2 +0 -0
  29. package/dist/client/_assets/chunks/05da12edb9d52210581dc6ec4541031f-BXAOx2XP.woff2 +0 -0
  30. package/dist/client/_assets/chunks/074c1f8483d5a4d8c45c8c5f3e4cbb32-CDqdMQm8.woff2 +0 -0
  31. package/dist/client/_assets/chunks/0968e4861204b51f62a2f8e9f15dd5e0-D5tLgF_8.woff2 +0 -0
  32. package/dist/client/_assets/chunks/0acd1fe2b2ea1ad1bfee7ae1fa139d27-BKK1m412.woff2 +0 -0
  33. package/dist/client/_assets/chunks/0c2ab6b295e55f356f8020d4e7747522-CkCQjQw3.woff2 +0 -0
  34. package/dist/client/_assets/chunks/0c5f9492af03a4fa42c784de94649de1-CQlJqksa.woff2 +0 -0
  35. package/dist/client/_assets/chunks/0d5dec931dc885f07fe5cd5af8bed675-C74x-U_U.woff2 +0 -0
  36. package/dist/client/_assets/chunks/0d9a936885a4c39077438effd3779cbd-Bw0fYF4S.woff2 +0 -0
  37. package/dist/client/_assets/chunks/0e645da524f7cfc0e8c3c03fb2b08428-BVeI_5Th.woff2 +0 -0
  38. package/dist/client/_assets/chunks/0f5e1a18987dbc84ca05188c129e1936-B6_lZ6fE.woff2 +0 -0
  39. package/dist/client/_assets/chunks/1076f0f6f66d28d7a2f16427faad4413-ubiXnD1P.woff2 +0 -0
  40. package/dist/client/_assets/chunks/112743a4ab5fdd1498dfdf2b11336380-B5FzQxTK.woff2 +0 -0
  41. package/dist/client/_assets/chunks/1211f03d3ff5759a702631445793f60e-DDcSiyCt.woff2 +0 -0
  42. package/dist/client/_assets/chunks/12ef1ba76bd20b004b865266a1aa70b3-BwmaD87S.woff2 +0 -0
  43. package/dist/client/_assets/chunks/135e83b403475c5dc9e49b43501a5b84-C5mE_KZS.woff2 +0 -0
  44. package/dist/client/_assets/chunks/1380210a722ac9aeef54005ad7b015c9-CjA-MP82.woff2 +0 -0
  45. package/dist/client/_assets/chunks/13b2c53b30e6a3e4de7132dbc18dbdfc-B9WOB7kv.woff2 +0 -0
  46. package/dist/client/_assets/chunks/147b24c2f08d03bbed30887d4cb3311c-IeeFCJ13.woff2 +0 -0
  47. package/dist/client/_assets/chunks/152395634a207579552f8cb54db88599-C2y2JyNZ.woff2 +0 -0
  48. package/dist/client/_assets/chunks/15d95680dc31cc6ce20e0a5464106dc4-CpD8dqRm.woff2 +0 -0
  49. package/dist/client/_assets/chunks/16663e567f81f4725a1522f37e18f71f-BNoxS9pQ.woff2 +0 -0
  50. package/dist/client/_assets/chunks/1733f27476507ca68b68a803bc533cc2-DXL-14Ug.woff2 +0 -0
  51. package/dist/client/_assets/chunks/17c7f5d0a45e92ede0e5dec3a2c77efa-CNj3jQMl.woff2 +0 -0
  52. package/dist/client/_assets/chunks/1824321da801e6257b902f3d1d09054c-D4dgdyWB.woff2 +0 -0
  53. package/dist/client/_assets/chunks/188c2db794f3dd7a45889ddbc81da9bb-C9NbUBBO.woff2 +0 -0
  54. package/dist/client/_assets/chunks/1a08931435f885e707edb85978ea3bb6-BOiP2u7G.woff2 +0 -0
  55. package/dist/client/_assets/chunks/1a3a92c7c060a71a6c35819b9ebcdbb9-DFfonQfz.woff2 +0 -0
  56. package/dist/client/_assets/chunks/1be602ad456b0d75902d352116cb35fd-Bc-XQtQh.woff2 +0 -0
  57. package/dist/client/_assets/chunks/1cf737900dd49c2e88f1b3221a82a602-CfTmnns3.woff2 +0 -0
  58. package/dist/client/_assets/chunks/1fba7ec0e412e911bf31841de5a8a4e4-18ELMbFF.woff2 +0 -0
  59. package/dist/client/_assets/chunks/233d3a685ee18276b319363474599d47-BaFdJ0-G.woff2 +0 -0
  60. package/dist/client/_assets/chunks/2612c60f9dc5459ac42763591240a1d6-CTD69cq1.woff2 +0 -0
  61. package/dist/client/_assets/chunks/284b53bbefb06924cf236d24c8ed5641-CPDiuOv6.woff2 +0 -0
  62. package/dist/client/_assets/chunks/28a97af9ab9d38983d20f39ff09840e1-mFqcT3Jw.woff2 +0 -0
  63. package/dist/client/_assets/chunks/2a56eaf19d1d38a6b57e2a388f733676-CU52qBlE.woff2 +0 -0
  64. package/dist/client/_assets/chunks/2ab8cf1f23a5ac7939a7876054e711a7-B4T3dK7A.woff2 +0 -0
  65. package/dist/client/_assets/chunks/2abfbab82b6a7c04426afc054d2464da-D5PIIHde.woff2 +0 -0
  66. package/dist/client/_assets/chunks/2bfaadaf3479c72286248e6de0be0ec9-DEpU6O74.woff2 +0 -0
  67. package/dist/client/_assets/chunks/2c8c55e4cec85ce77e95cac9d330f5cf-BCTA96EZ.woff2 +0 -0
  68. package/dist/client/_assets/chunks/2e6f4bb71ef6b38765d51acc5a79f638-Mbs6PECD.woff2 +0 -0
  69. package/dist/client/_assets/chunks/2ed9981d2e8983365bd051159b976b6d-D8gg6NWM.woff2 +0 -0
  70. package/dist/client/_assets/chunks/2f4c633e923ba30c6ba376367379fc91-CIJCFztv.woff2 +0 -0
  71. package/dist/client/_assets/chunks/31194d303a67561926a544ed0e072aee-w3-eGYAi.woff2 +0 -0
  72. package/dist/client/_assets/chunks/31a197213ae61d7043c81013f52d10d6-DXZrrkJP.woff2 +0 -0
  73. package/dist/client/_assets/chunks/349965eee0a9b6c984a319ab96a4ece9-DByAVmXw.woff2 +0 -0
  74. package/dist/client/_assets/chunks/34e6750e00c3a911ef87dc66b336594d-DNbWesOu.woff2 +0 -0
  75. package/dist/client/_assets/chunks/3623d96698132c61e861f984ce11cc23-jqV9ErzX.woff2 +0 -0
  76. package/dist/client/_assets/chunks/36cda3eae13370b934bba4429fab9078-BPDcUlw9.woff2 +0 -0
  77. package/dist/client/_assets/chunks/36f3df4730cfca4fc77fe52b52e6a126-BwK10NC0.woff2 +0 -0
  78. package/dist/client/_assets/chunks/37887deb7a9c466cf2af6ee7ff372ea6-BrAerJB1.woff2 +0 -0
  79. package/dist/client/_assets/chunks/37dbd564820cce2f7b3331e442da0df3-Cql2kzIi.woff2 +0 -0
  80. package/dist/client/_assets/chunks/37dc8505b20b37a2477a9121b16d44a7-CoXw6XWV.woff2 +0 -0
  81. package/dist/client/_assets/chunks/39211b02f24c69cbd1b54f6d41b01933-4Xu7vvnq.woff2 +0 -0
  82. package/dist/client/_assets/chunks/3aa13f1843d7c8fa16109b6395a9a29d-BQFZUc3B.woff2 +0 -0
  83. package/dist/client/_assets/chunks/3aba89c4d4281553078de4622b498cbb-8QS1zDYk.woff2 +0 -0
  84. package/dist/client/_assets/chunks/3ac0a07878cacdc98bb889f40f5970d3-C-BaM6-y.woff2 +0 -0
  85. package/dist/client/_assets/chunks/3d88558097559d775231194d43745ba7-CR2aqGC7.woff2 +0 -0
  86. package/dist/client/_assets/chunks/3dc3e8c35524cad2d1236f3393104ccf-9sqBsF7B.woff2 +0 -0
  87. package/dist/client/_assets/chunks/3e24063b19abd2b051af986a6870046d-TlweM3SW.woff2 +0 -0
  88. package/dist/client/_assets/chunks/3e2c06bdd9dcab29aeaeb0cfb7fa608a-BaqU-dIt.woff2 +0 -0
  89. package/dist/client/_assets/chunks/424123f9371ba357087363263eeafcda-C8SVfEJi.woff2 +0 -0
  90. package/dist/client/_assets/chunks/43b3bbf43055c3e65db9cdf2c5b18c1c-CbHx6-Ki.woff2 +0 -0
  91. package/dist/client/_assets/chunks/4410f876d27c9821839474b6b1059fd7-ClYz91aY.woff2 +0 -0
  92. package/dist/client/_assets/chunks/44704471b60c18b3ecec5300a88f1102-DDmmy6-m.woff2 +0 -0
  93. package/dist/client/_assets/chunks/45140742dfa8686b58db2899ce89f89a-D5IMMzDM.woff2 +0 -0
  94. package/dist/client/_assets/chunks/46604090efbb99c2db1800928af0a239-JX2fpYX_.woff2 +0 -0
  95. package/dist/client/_assets/chunks/46998df85d31e629c0142f0556f5e7c5-BIkNG9Wh.woff2 +0 -0
  96. package/dist/client/_assets/chunks/469d2754e315e77270c12fa0ab28026c-D-y-vsfA.woff2 +0 -0
  97. package/dist/client/_assets/chunks/479329a39a3eb6c0047e6b0981919855-vZKCRx42.woff2 +0 -0
  98. package/dist/client/_assets/chunks/491bbba374093fff045f55803a22bfef-DSQXiDCd.woff2 +0 -0
  99. package/dist/client/_assets/chunks/49902f1c41b281f8eb3771abdd4dccb8-DxVxNg2W.woff2 +0 -0
  100. package/dist/client/_assets/chunks/4c0760284569b87d8f2290c324953973-Bqd-TKHH.woff2 +0 -0
  101. package/dist/client/_assets/chunks/4cba92c942694bdc479bc54101bb1aa4-BrfsrgrC.woff2 +0 -0
  102. package/dist/client/_assets/chunks/4d38e56738156625329d93bd5c8cc74a-DONkMP0y.woff2 +0 -0
  103. package/dist/client/_assets/chunks/4db4f31a16965baa42cb7dad667a8f04-Dt1Cy4gr.woff2 +0 -0
  104. package/dist/client/_assets/chunks/4e7d263c30bb48604069a3b4404d4eeb-DNPCkvKD.woff2 +0 -0
  105. package/dist/client/_assets/chunks/5279c7e465d9871edf51fd02e691eee7-CKhyhvVf.woff2 +0 -0
  106. package/dist/client/_assets/chunks/527ff336279465617aafbafe1a830632-CC3lv4pm.woff2 +0 -0
  107. package/dist/client/_assets/chunks/54bfdb6f21f9d6191e5100580239e14f-PGlqYvgl.woff2 +0 -0
  108. package/dist/client/_assets/chunks/56b40518ea0608e62826bd44ee27f5f9-BD9F9Oss.woff2 +0 -0
  109. package/dist/client/_assets/chunks/56be5e32fd01668fe5ba814d94905839-CSyFA-9l.woff2 +0 -0
  110. package/dist/client/_assets/chunks/572b9e28f1e6d89b765a16d809db84a1-BIYMjzEa.woff2 +0 -0
  111. package/dist/client/_assets/chunks/57e9a86651366c1ba299e47aa7e358c2-BHuZXB4W.woff2 +0 -0
  112. package/dist/client/_assets/chunks/57f89adeae55aa7fe2add3fc1801ce03-Cy3PNrfx.woff2 +0 -0
  113. package/dist/client/_assets/chunks/5921cef36750ff6092d94b83a802823b-hF-uymTh.woff2 +0 -0
  114. package/dist/client/_assets/chunks/59e95d8b5332dcdae894a19946ab767d-ClRfsjWZ.woff2 +0 -0
  115. package/dist/client/_assets/chunks/5acc7f736217259db79a42bf44241c48-FYQUuFOM.woff2 +0 -0
  116. package/dist/client/_assets/chunks/5c05f8b75c9d7f25b3c04ca711cf43b3-D-YWPkSq.woff2 +0 -0
  117. package/dist/client/_assets/chunks/5df9ce98c75f8c50fb4fff7f01a23925-DgoCENyW.woff2 +0 -0
  118. package/dist/client/_assets/chunks/5e0124c7265f1b4cf0fc797b94efbebc-BTBNHsSB.woff2 +0 -0
  119. package/dist/client/_assets/chunks/5ec349ff62b9ee5fe0c6dae867704dc4-B4dezhrz.woff2 +0 -0
  120. package/dist/client/_assets/chunks/60020a830d809c26970cf8e48ba1eeda-B6FDTSik.woff2 +0 -0
  121. package/dist/client/_assets/chunks/601f812e8b1eafecd5ba585a6a6d7962-gIVBlUU9.woff2 +0 -0
  122. package/dist/client/_assets/chunks/6025ddaf99d86cc8bfd781006b19fbd6-CS-ze337.woff2 +0 -0
  123. package/dist/client/_assets/chunks/60bcfa3ea7446eb42394aab02d28be40-C-PQfCu3.woff2 +0 -0
  124. package/dist/client/_assets/chunks/60f6e28b3f0400b0e8fe32c3c7116102-Dag9L2qU.woff2 +0 -0
  125. package/dist/client/_assets/chunks/615a7e0d63f257109d599b2a1977de68-D6Y93yoG.woff2 +0 -0
  126. package/dist/client/_assets/chunks/615b38e0b5bb3ea68426f44f47706e6f-B5yzIIer.woff2 +0 -0
  127. package/dist/client/_assets/chunks/61a2c80d0c924d5a6187c02e8d1d1642-BY3MmHbZ.woff2 +0 -0
  128. package/dist/client/_assets/chunks/62fc94f8d4c5a750b7f25e7387539910-3ql2_WDK.woff2 +0 -0
  129. package/dist/client/_assets/chunks/639c7c6b0b0c27c738702741cfa4b8c0-BCD3OGri.woff2 +0 -0
  130. package/dist/client/_assets/chunks/63fafcb069520613d0ea35ad3e6b1e42-DP0Rrj4o.woff2 +0 -0
  131. package/dist/client/_assets/chunks/64a404d675f1d726f0891b7a80794595-DU3L0I9s.woff2 +0 -0
  132. package/dist/client/_assets/chunks/65a60d87c64228d258a123cbe85f5f31-B4kbf-79.woff2 +0 -0
  133. package/dist/client/_assets/chunks/65e9c4585d71bf48a5c62f367010da16-CvzgzReN.woff2 +0 -0
  134. package/dist/client/_assets/chunks/68a3a4bf337f8a0722be76676e20b850-Dnaw8EPD.woff2 +0 -0
  135. package/dist/client/_assets/chunks/6968e889e18891d912809fe484c2e745-Bj1m5e0s.woff2 +0 -0
  136. package/dist/client/_assets/chunks/6abdd163d2c4b85698db8aa1ce149361-B5FVGJuo.woff2 +0 -0
  137. package/dist/client/_assets/chunks/6b75213eb0be40ce84241eb2bb438a2e-CqHmF-NR.woff2 +0 -0
  138. package/dist/client/_assets/chunks/70714891ad3fbfc3d5f10a8669dacc5a-BWk5nLFN.woff2 +0 -0
  139. package/dist/client/_assets/chunks/7158022889c6c177062ac85036e7af10-B3D4K22n.woff2 +0 -0
  140. package/dist/client/_assets/chunks/73b794d885b88f7befb7ea8ab1780a15-D0ZehnEI.woff2 +0 -0
  141. package/dist/client/_assets/chunks/743f290baf027d4626a86e22f3d44600-DDTDQdRC.woff2 +0 -0
  142. package/dist/client/_assets/chunks/7520ca6ca8c60eb1e62d50e71c8d30f1-CVRaGu2G.woff2 +0 -0
  143. package/dist/client/_assets/chunks/757019c01c5e2c2b0f54293cea3b5636-BTovUnoH.woff2 +0 -0
  144. package/dist/client/_assets/chunks/759a647b77791b1e98f99bc0ab5317a7-CrK6FIFz.woff2 +0 -0
  145. package/dist/client/_assets/chunks/761d84afca8d7f34eebefe538adba827-DRbiVuwX.woff2 +0 -0
  146. package/dist/client/_assets/chunks/76e07530323418ec723c5d7634c9bcca-DGtVJTTH.woff2 +0 -0
  147. package/dist/client/_assets/chunks/76e51630143b95b6322ef93ad330da7a-D-kV82C_.woff2 +0 -0
  148. package/dist/client/_assets/chunks/76f117a3baacda304a71965a17266a13-DFfvsFUm.woff2 +0 -0
  149. package/dist/client/_assets/chunks/773819b7b9b8fd404a929867c0fd677e-Bu1xCMZb.woff2 +0 -0
  150. package/dist/client/_assets/chunks/77747f17625a259fb405b7bbdd84ac4b-o_vNfh-6.woff2 +0 -0
  151. package/dist/client/_assets/chunks/7817dd16805145d8538ad57590f69f5a-DguSUP3O.woff2 +0 -0
  152. package/dist/client/_assets/chunks/788498548ddfb710d6ef3c7bff79fa97-3ffoQfRd.woff2 +0 -0
  153. package/dist/client/_assets/chunks/78ea6c40923ce95367e3517dd1e5a849-DvLDRhX9.woff2 +0 -0
  154. package/dist/client/_assets/chunks/79aa7c8c842c4a27cf57c0a3a1ffca5a-sDoCeoKI.woff2 +0 -0
  155. package/dist/client/_assets/chunks/7b59f0ec7792b18458dc5a361e37884c-DWvxkKmT.woff2 +0 -0
  156. package/dist/client/_assets/chunks/7c3945788a689a69356c1a622d69d48b-1JjIpss2.woff2 +0 -0
  157. package/dist/client/_assets/chunks/7cc72fd2c9105560422b6a67c6162945-Co3AF5_G.woff2 +0 -0
  158. package/dist/client/_assets/chunks/7d0ea0690e432462f4d05a23d720ec19-Dx7DK4C3.woff2 +0 -0
  159. package/dist/client/_assets/chunks/7dffa5c1bec57e0903fd62357401ff1a-DuQHmiiN.woff2 +0 -0
  160. package/dist/client/_assets/chunks/7e38ca789a9e76ac91d9256374e154f0-BtETgGRe.woff2 +0 -0
  161. package/dist/client/_assets/chunks/7f9a6c9286b68de9c72b0024f7beeb40-JpXdo4FC.woff2 +0 -0
  162. package/dist/client/_assets/chunks/815a0b797465190f60d8b1a04656c42e-Spzo94nU.woff2 +0 -0
  163. package/dist/client/_assets/chunks/823d1bb11097331238d103c7f72138dc-CGN6m2VF.woff2 +0 -0
  164. package/dist/client/_assets/chunks/82f6ccc063960eed1cdfd1d61ada8862-CsmAqx2a.woff2 +0 -0
  165. package/dist/client/_assets/chunks/835b6505bb9eea9678925a1fa885353d-C6mSBwUc.woff2 +0 -0
  166. package/dist/client/_assets/chunks/845b4b67564d62cf5cad242f7ac08ea5-DnvzrloJ.woff2 +0 -0
  167. package/dist/client/_assets/chunks/84c8e7bc0931008ed91f44ed12dfea94-C2Hvhh-b.woff2 +0 -0
  168. package/dist/client/_assets/chunks/8726558cf297cbda831cc19514897205-gSFV1vW0.woff2 +0 -0
  169. package/dist/client/_assets/chunks/8855472f3c02f6c7ebb3216617ebe4af-C1JsWVqq.woff2 +0 -0
  170. package/dist/client/_assets/chunks/887a11290ba78b1e66c6d2f67043005e-L_A1f5Fz.woff2 +0 -0
  171. package/dist/client/_assets/chunks/888ba53510213d5d1f6a7deb60e569b6-OP8E-ELN.woff2 +0 -0
  172. package/dist/client/_assets/chunks/88db1d042074fb6e66821ffc10941930-kZ6K3e62.woff2 +0 -0
  173. package/dist/client/_assets/chunks/892aa49b2529c89bb4076d4aa51fe30f-Bw0_Km2A.woff2 +0 -0
  174. package/dist/client/_assets/chunks/8b5c9ef81159f31d2ab35f45ca4373e0-Djw95sKC.woff2 +0 -0
  175. package/dist/client/_assets/chunks/8b91c7c2ed390f1278b9befa3aa87233-B2C3iFbQ.woff2 +0 -0
  176. package/dist/client/_assets/chunks/8dc18c0cebe6aa4bf4c45dbb831048ab-bUlANLit.woff2 +0 -0
  177. package/dist/client/_assets/chunks/8e231d707f0c4f8e3cde90a6b52a79aa-CvwbWlul.woff2 +0 -0
  178. package/dist/client/_assets/chunks/8fb45117a62d92dce44a80f0b729ead5-Djr4-o5-.woff2 +0 -0
  179. package/dist/client/_assets/chunks/9064c12fd72c7aba235d8c3881755b91-BnguxT-p.woff2 +0 -0
  180. package/dist/client/_assets/chunks/90b7848e9b1623b77bdcf155e90b839c-CFvPRnoM.woff2 +0 -0
  181. package/dist/client/_assets/chunks/911b9e53e9814de2998c60bf3115f560-DmPmj635.woff2 +0 -0
  182. package/dist/client/_assets/chunks/919a879bd2d580d8491a31a449390689-BY2w-8_Y.woff2 +0 -0
  183. package/dist/client/_assets/chunks/91da6cb174bebfb96976e2f1316be2fe-DztAJb6W.woff2 +0 -0
  184. package/dist/client/_assets/chunks/92fdc376bce277874e75db666f175b44-DJUnHEIN.woff2 +0 -0
  185. package/dist/client/_assets/chunks/930a23430f0eb64480d7fe5f82834e21-2VrDPT7E.woff2 +0 -0
  186. package/dist/client/_assets/chunks/934dfdbed2bb2c4b6129199d699a34fa-CtpHsjRD.woff2 +0 -0
  187. package/dist/client/_assets/chunks/93bed0e5f2dd5a21cf73304fcfbc149d-DJiaETX7.woff2 +0 -0
  188. package/dist/client/_assets/chunks/93f3ea6918533d96d9c252378b9a4af0-D-FJaGi-.woff2 +0 -0
  189. package/dist/client/_assets/chunks/95a0951d2a2722ff773a1a45e8c474d6-DuZg-BpP.woff2 +0 -0
  190. package/dist/client/_assets/chunks/95ff41191c76ef893ac70a1043e95e44-C08QDbJO.woff2 +0 -0
  191. package/dist/client/_assets/chunks/96c75862c8cec51a7c05ff025fea86cd-DBnRwCuW.woff2 +0 -0
  192. package/dist/client/_assets/chunks/97d7a17234f2b5030ae9697ae00aded2-CkUSaB-p.woff2 +0 -0
  193. package/dist/client/_assets/chunks/982cd1765ca6bbb3e6547e47834d5148-DkIybx1r.woff2 +0 -0
  194. package/dist/client/_assets/chunks/985d4d41afa0934a4eb2de35473fb9a2-CF7rgJMw.woff2 +0 -0
  195. package/dist/client/_assets/chunks/995c4fda62d25c3b79dd5987737df6ae-CY5ri8HW.woff2 +0 -0
  196. package/dist/client/_assets/chunks/9b0c3caac458c7bc9c9133e40405ddea-DhPrYOcz.woff2 +0 -0
  197. package/dist/client/_assets/chunks/9b21e8244f5930c48ad5073f83777b6c-CuE9-r-r.woff2 +0 -0
  198. package/dist/client/_assets/chunks/9bb38fd05201de666b88b82d56a386f7-nMl6lB8X.woff2 +0 -0
  199. package/dist/client/_assets/chunks/9d0afa53dc2f92ba42024f55f11f6779-Bozpx8bL.woff2 +0 -0
  200. package/dist/client/_assets/chunks/9e13a47242926f1cd7d68c08d9ef8889-DwKQjc8M.woff2 +0 -0
  201. package/dist/client/_assets/chunks/9eff2ab2a9d2ced47ab761bf6fdb11ec-D86-Zm1H.woff2 +0 -0
  202. package/dist/client/_assets/chunks/9f1963fe8d4d6878d717d872a8f3fdb5-DxjITUFq.woff2 +0 -0
  203. package/dist/client/_assets/chunks/9f3bdab4025dc7c5ba1a5871e62e8918-BqxDEFLJ.woff2 +0 -0
  204. package/dist/client/_assets/chunks/9fce02d1a09f464c36c9fcfb14a354c5-F2tMAB_j.woff2 +0 -0
  205. package/dist/client/_assets/chunks/a00e94d4f04df6aa659db9c4954c7efe-CRBKCj_3.woff2 +0 -0
  206. package/dist/client/_assets/chunks/a0a6755c7e3f9e4cf03387027dd9f16c-BAyRqF2w.woff2 +0 -0
  207. package/dist/client/_assets/chunks/a0d00fe4816c95a8c7dffd445ef00b03-CxcTiRLJ.woff2 +0 -0
  208. package/dist/client/_assets/chunks/a0f199077fa1a33bc2a1e01c64de7fe6-IdgVJmZD.woff2 +0 -0
  209. package/dist/client/_assets/chunks/a2b0597837e382aca19919c4b74e58c8-7iVOBjzI.woff2 +0 -0
  210. package/dist/client/_assets/chunks/a373e85c96aa0dc303092429fb837e51-Dvmw8mbX.woff2 +0 -0
  211. package/dist/client/_assets/chunks/a379ada89abd750c587ded29f77e731c-CgPBsEsv.woff2 +0 -0
  212. package/dist/client/_assets/chunks/a4475c442c6b259354c9eda62e64e7df-TZ0c8vK-.woff2 +0 -0
  213. package/dist/client/_assets/chunks/a48be56ce5d3a99dc8f8331398004412-BR_ixH1W.woff2 +0 -0
  214. package/dist/client/_assets/chunks/a4a39b5f048671aa37c2b2a9581ab08a-EDfDYsUd.woff2 +0 -0
  215. package/dist/client/_assets/chunks/a534449d67561ac9b06489f64e57ad98-C355TCrD.woff2 +0 -0
  216. package/dist/client/_assets/chunks/a6e044424780a57610833cc856c15bf5-x1b8FE4H.woff2 +0 -0
  217. package/dist/client/_assets/chunks/a7c35e42a659347e490c3cb7983b2e7f-BWpA0-oz.woff2 +0 -0
  218. package/dist/client/_assets/chunks/a7c58070d68dc1724e9d4e781afb78db-ql27pT-6.woff2 +0 -0
  219. package/dist/client/_assets/chunks/a806759e72987951d6d08b7cbdf36a1b-TvLDwB_u.woff2 +0 -0
  220. package/dist/client/_assets/chunks/a88481ec8bc7be11cb66e46781a79bb9-Ck-r_sGx.woff2 +0 -0
  221. package/dist/client/_assets/chunks/a919a8c6fec6d7e8bb21177965f2e9d7-CC5uWzwd.woff2 +0 -0
  222. package/dist/client/_assets/chunks/a9a016d4a93409f65278327e8a8bb38d-dylrNwF8.woff2 +0 -0
  223. package/dist/client/_assets/chunks/aa662092cf249707123ca4d575e2764b-CSxNsTUL.woff2 +0 -0
  224. package/dist/client/_assets/chunks/aae1eda193285ab817a2d1b408440326-CjyjBoRm.woff2 +0 -0
  225. package/dist/client/_assets/chunks/ab92d41062a7644faa45f50bf384454a-Cq0X8a9H.woff2 +0 -0
  226. package/dist/client/_assets/chunks/accc43762ad05edfbff66ba1380d192a-B5jIykaZ.woff2 +0 -0
  227. package/dist/client/_assets/chunks/ad47bcd6d4663026d648c132d969318d-BYdIbDge.woff2 +0 -0
  228. package/dist/client/_assets/chunks/ad67113f88b59582991cb0c5d33ea19f-DOH1vcIZ.woff2 +0 -0
  229. package/dist/client/_assets/chunks/adb6e613fb99c6dd660b727101f554a8-DJDw_ZZK.woff2 +0 -0
  230. package/dist/client/_assets/chunks/adc4a98a89870ef984ee8f4b57c8a3a5-D30vw26R.woff2 +0 -0
  231. package/dist/client/_assets/chunks/af57a2e8c72f9ba0efb1af771d32c124-D5Zwq9pS.woff2 +0 -0
  232. package/dist/client/_assets/chunks/b0ae5f374e43dac992a4a5d334cebc0b-Ccoi4dPt.woff2 +0 -0
  233. package/dist/client/_assets/chunks/b0cb664cb2e1371efda8943c0b7dcd1c-YzGFnelF.woff2 +0 -0
  234. package/dist/client/_assets/chunks/b0d1cdced482352cf0d3ae58638aacb9-IunWfc6R.woff2 +0 -0
  235. package/dist/client/_assets/chunks/b12379ab782468d725519cd07a7d15bc-IjbydKs2.woff2 +0 -0
  236. package/dist/client/_assets/chunks/b320f5d185b2cff933ac549c184031c5-IYnPrIz4.woff2 +0 -0
  237. package/dist/client/_assets/chunks/b326a8a9c33b554db570da94f60bc380-B6dzaCQg.woff2 +0 -0
  238. package/dist/client/_assets/chunks/b55486f8a459c838fe329d4e79a8c211-DW8q7oGV.woff2 +0 -0
  239. package/dist/client/_assets/chunks/b6117ff2993b11bb1fdc7ea3588a010c-JoMYL5W_.woff2 +0 -0
  240. package/dist/client/_assets/chunks/b61982951bd51b724143c30dfaaa9fe9-D3KhBKl6.woff2 +0 -0
  241. package/dist/client/_assets/chunks/b91234c10fd8b8c8abc88e03afe66a1f-BABNh2la.woff2 +0 -0
  242. package/dist/client/_assets/chunks/b9317198f118a1dfd8ddf2b82ec028f3-CiMecxeH.woff2 +0 -0
  243. package/dist/client/_assets/chunks/ba825ae79b1a6f7e0cce5215fcb5c96f-B6EWU_D5.woff2 +0 -0
  244. package/dist/client/_assets/chunks/ba9c59d7dfa4494db1bb764ada81467d-D0yPcC1m.woff2 +0 -0
  245. package/dist/client/_assets/chunks/bab547459d514f46206e340c4bb2dc88-YnBqlGhw.woff2 +0 -0
  246. package/dist/client/_assets/chunks/bb0b15492b8cdbbec57c0bcfa0aa9241-BjIFbEsN.woff2 +0 -0
  247. package/dist/client/_assets/chunks/bc6183ac08f0fac78c46f80c10cf7c92-B81nbRb3.woff2 +0 -0
  248. package/dist/client/_assets/chunks/bc8ccea5abf6598cf3cfa97eb59804bb-B_7IbluG.woff2 +0 -0
  249. package/dist/client/_assets/chunks/bc95a792cbca6639214c9b0da13392ff-CHQe0E-D.woff2 +0 -0
  250. package/dist/client/_assets/chunks/bcc39eda837bb7a7a3d37c8c60fffb81-CGd1Zk6S.woff2 +0 -0
  251. package/dist/client/_assets/chunks/bcfa62f35731856246c146d3a6932bf3-DMDq6w4v.woff2 +0 -0
  252. package/dist/client/_assets/chunks/bd73264d7f98776708d5d6f3c9b78fcc-d9QWWMym.woff2 +0 -0
  253. package/dist/client/_assets/chunks/bed610b217d500f5975cfc9fe6157570-B5t7s1bw.woff2 +0 -0
  254. package/dist/client/_assets/chunks/bedc74b423b7293b6ad0bdecc61c42cc-4Nnh7peP.woff2 +0 -0
  255. package/dist/client/_assets/chunks/bf8901f8f11d4f433ea17dabc9370ea6-B8dkfvGn.woff2 +0 -0
  256. package/dist/client/_assets/chunks/bfb00d4a4c48661bd0be99f300a0faae-OxsWZ7l-.woff2 +0 -0
  257. package/dist/client/_assets/chunks/c0098958e20db68cab90097b5e62516f-VpjQo3vV.woff2 +0 -0
  258. package/dist/client/_assets/chunks/c063897793f593eb26d6ff7b7baaba18-BWeJJ1sH.woff2 +0 -0
  259. package/dist/client/_assets/chunks/c1b0df29ae41d764904df84e9ac83d1e-D1N934vs.woff2 +0 -0
  260. package/dist/client/_assets/chunks/c4143bb9f2fe77b6ccf20088a8904650-nHfYM24e.woff2 +0 -0
  261. package/dist/client/_assets/chunks/c42c67070cfe99cf823df92d81c7fa6e-B_a405Ez.woff2 +0 -0
  262. package/dist/client/_assets/chunks/c4bfaa5e50798246e3770718b7a7c84a-B-r6Y_o0.woff2 +0 -0
  263. package/dist/client/_assets/chunks/c4d749e45ecd5a5aed5a0bb3ebfd355d-CkqE1fRi.woff2 +0 -0
  264. package/dist/client/_assets/chunks/c6c2971ad1f3221f6cf84028aa0f477e-D7BZGIcY.woff2 +0 -0
  265. package/dist/client/_assets/chunks/c7ff3f6bbdcd5f604b7343602ab904df-Bq7UjVrZ.woff2 +0 -0
  266. package/dist/client/_assets/chunks/c84598999133455503042e06f4ab79cb-DIXndJX3.woff2 +0 -0
  267. package/dist/client/_assets/chunks/c945c62368357d05a53206620460fb30-82gFEf6n.woff2 +0 -0
  268. package/dist/client/_assets/chunks/c96d83978add28b356c22c4c84916733-BL4VaPIE.woff2 +0 -0
  269. package/dist/client/_assets/chunks/c97f41eef722121d86f55d553c056a39-CjejgQO2.woff2 +0 -0
  270. package/dist/client/_assets/chunks/ca62704509932d3232d62918de97af3f-CKWF-3mX.woff2 +0 -0
  271. package/dist/client/_assets/chunks/ca9a533988d7019597a60d4e17127e0c-ClZ6ygZM.woff2 +0 -0
  272. package/dist/client/_assets/chunks/cb7ccd6494256f7a2977b7c2b0225592-Cxp8XObm.woff2 +0 -0
  273. package/dist/client/_assets/chunks/ce022e18a1377ac509443c3c3790b431-DpH0t6Tn.woff2 +0 -0
  274. package/dist/client/_assets/chunks/ce41d70ce6a069a498525c9e15c45cf2-DGtgv-_b.woff2 +0 -0
  275. package/dist/client/_assets/chunks/cf2b28f90f47276f7e2688a65e88a101-LVbNkXHj.woff2 +0 -0
  276. package/dist/client/_assets/chunks/cf9cffe56636322f62b40d61130fbc5e-DNCBMqAo.woff2 +0 -0
  277. package/dist/client/_assets/chunks/d1f064825fa5784b5c930652bd831cce-BU6qwjp8.woff2 +0 -0
  278. package/dist/client/_assets/chunks/d20d8944bc0b85f5b2aae4b24f343516-oY9l-NmC.woff2 +0 -0
  279. package/dist/client/_assets/chunks/d2faabcedd19f016e7b21bce073c0ec8-Dv6eFCf5.woff2 +0 -0
  280. package/dist/client/_assets/chunks/d320171e57480510f87dbbc7d5264b0a-CcCfjaFX.woff2 +0 -0
  281. package/dist/client/_assets/chunks/d36644d6502527e1fff205d0c7eca434-kVBj8zy_.woff2 +0 -0
  282. package/dist/client/_assets/chunks/d3a72a99d365dddfbca8d017a8011368-Bgdmhl3M.woff2 +0 -0
  283. package/dist/client/_assets/chunks/d40ab99c7a38026f411c8f112f742b48-D7Nm7fg6.woff2 +0 -0
  284. package/dist/client/_assets/chunks/d42aafa0f246ad3b4c16fe96d3a3a432-CRxTqZXU.woff2 +0 -0
  285. package/dist/client/_assets/chunks/d4aaf23c13ae808a4bed617afd13aa07-d_T1SKe9.woff2 +0 -0
  286. package/dist/client/_assets/chunks/d509ee5c8241bc7c3de2039d75564fa5-BpNFD9JE.woff2 +0 -0
  287. package/dist/client/_assets/chunks/d675d717cd329bfd4c0524f76ae1579c-Bd5_ctoQ.woff2 +0 -0
  288. package/dist/client/_assets/chunks/d689b1861d7e4377dd72ad3013482612-jPsVOU3F.woff2 +0 -0
  289. package/dist/client/_assets/chunks/d6bb686bddfbe8f38a36a68e609a8667-C_QduS-X.woff2 +0 -0
  290. package/dist/client/_assets/chunks/d9020ff69a83b2a6ee1f42ae480f7db0-DLBUPMCe.woff2 +0 -0
  291. package/dist/client/_assets/chunks/d9047070d72a816b3dba9d40c2d85e69-LTQ42vEp.woff2 +0 -0
  292. package/dist/client/_assets/chunks/da04549f3f4ed28076b01b8cd710d313-CDXOeduw.woff2 +0 -0
  293. package/dist/client/_assets/chunks/da7af303f8c645f9a9dbae0e6e32dd35-DCkS2jqi.woff2 +0 -0
  294. package/dist/client/_assets/chunks/da90a9012ab2d98f759e3fa0820ef502-D0D9d1zg.woff2 +0 -0
  295. package/dist/client/_assets/chunks/dc6e234ded795e91f76d6647f628fbf0-IkooXcRh.woff2 +0 -0
  296. package/dist/client/_assets/chunks/dc8f6256445e68199540be9ade33529f-umb5tWbZ.woff2 +0 -0
  297. package/dist/client/_assets/chunks/dd0cfdfdac0866e66d587e2b5a9e9961-bl4ozE0c.woff2 +0 -0
  298. package/dist/client/_assets/chunks/de304c8a02e45ded4f8dcc479d167198-ClRu89-4.woff2 +0 -0
  299. package/dist/client/_assets/chunks/df10d94bea357a43313e20da5d84bd30-CmAgh2oA.woff2 +0 -0
  300. package/dist/client/_assets/chunks/df9568257eb29b156449fdd4bec5ec76-BMz6aLRr.woff2 +0 -0
  301. package/dist/client/_assets/chunks/e067cd0ed76c90cd0a93c9339253f20b-6RxCQ4jy.woff2 +0 -0
  302. package/dist/client/_assets/chunks/e08b07772e7bed3cec2832d43f7fd339-CQCkeKsr.woff2 +0 -0
  303. package/dist/client/_assets/chunks/e12150d5a39b30be8f567968c7a527b0-DjdoWzeQ.woff2 +0 -0
  304. package/dist/client/_assets/chunks/e34b2b141e472dc776c86fdf8eea23b0-Cjk7H-Ev.woff2 +0 -0
  305. package/dist/client/_assets/chunks/e444fd88f1390636e603d0d681538ac8-Bx7CPOZh.woff2 +0 -0
  306. package/dist/client/_assets/chunks/e53fcb2381eee345db4f6f973dd95a3e-DmkkCUWz.woff2 +0 -0
  307. package/dist/client/_assets/chunks/e5bd313ef81f687d398aacb11cec3069-CwBEbcji.woff2 +0 -0
  308. package/dist/client/_assets/chunks/e79898628283edc27180fc39d0d769c1-By2pplQz.woff2 +0 -0
  309. package/dist/client/_assets/chunks/e8c15be62faf978d208925e79ea6a10d-Ct5M9lTP.woff2 +0 -0
  310. package/dist/client/_assets/chunks/e8c364c16daa04835bf32d293d2598db-3v4Tut_d.woff2 +0 -0
  311. package/dist/client/_assets/chunks/e91c4d941ed9f5c2f3e27f205a3a225e-BwOJ2Lst.woff2 +0 -0
  312. package/dist/client/_assets/chunks/e9c566be7a5d38a9085225f7372bc82b-BCtnCVML.woff2 +0 -0
  313. package/dist/client/_assets/chunks/e9ebc567a711eeb29019ddae3e0ce7fe-CnNhV49O.woff2 +0 -0
  314. package/dist/client/_assets/chunks/eaf332445a40942928e5f5750c1c3116-qZ5LSRrj.woff2 +0 -0
  315. package/dist/client/_assets/chunks/eb5afb3d952b8593782caec6026514b6-BXugZF2T.woff2 +0 -0
  316. package/dist/client/_assets/chunks/ec86e23683052da5cfdc3b77641bd15a-BeLcbcRc.woff2 +0 -0
  317. package/dist/client/_assets/chunks/ecb8875a56c7b038b35432fda41ae128-BVqgNf1o.woff2 +0 -0
  318. package/dist/client/_assets/chunks/eda1b0cb6d1719dd9bedcf3216a9e8de-BJTfLL5z.woff2 +0 -0
  319. package/dist/client/_assets/chunks/edd6a4f608d04fc0351d7688cfc321e4-d3V4U40c.woff2 +0 -0
  320. package/dist/client/_assets/chunks/ee6fce20b420a480714607c66d7f97e5-DEtC-G5G.woff2 +0 -0
  321. package/dist/client/_assets/chunks/eee3836d6ac17ebb2c450bbcbc9db121-C5a2TZjQ.woff2 +0 -0
  322. package/dist/client/_assets/chunks/et-book-bold-line-figures-BFJr2_zv.woff +0 -0
  323. package/dist/client/_assets/chunks/et-book-display-italic-old-style-figures-CoeRJAe8.woff +0 -0
  324. package/dist/client/_assets/chunks/et-book-roman-line-figures-CaA40oOf.woff +0 -0
  325. package/dist/client/_assets/chunks/f1661731474b78bdf81114daf10b254f-KnMKX_U3.woff2 +0 -0
  326. package/dist/client/_assets/chunks/f2c58dee206ba9355046fc23d05491f7-Cm4tCWav.woff2 +0 -0
  327. package/dist/client/_assets/chunks/f50ac27ea4358d67fdda403c2bb52467-WCfYHde1.woff2 +0 -0
  328. package/dist/client/_assets/chunks/f55334112f8e8be82d65db29887a663f-vDq1zH8k.woff2 +0 -0
  329. package/dist/client/_assets/chunks/f5738255e92d8dd34a46d1bcdf4c4074-tpghTzL1.woff2 +0 -0
  330. package/dist/client/_assets/chunks/f740e93f3c0277ecc616594103bca683-C4BgW_ko.woff2 +0 -0
  331. package/dist/client/_assets/chunks/f7633b5af033d76ff2fb3c3c266d77c5-BCkrZxwO.woff2 +0 -0
  332. package/dist/client/_assets/chunks/f7d8468cba2335a83ee414ea68291bab-CVnotkZU.woff2 +0 -0
  333. package/dist/client/_assets/chunks/f91cd722855f4269256eae1187df64ec-DJlAx1A_.woff2 +0 -0
  334. package/dist/client/_assets/chunks/f9695c6c4df2bf6bc03045ff79d4f01f-DDaYGYHT.woff2 +0 -0
  335. package/dist/client/_assets/chunks/fa6e58ce4b52695e7ae19bbea6336ec8-B30jEpze.woff2 +0 -0
  336. package/dist/client/_assets/chunks/fada6eaa68ff8816afe43d2a36c5423e-DlGAWNPb.woff2 +0 -0
  337. package/dist/client/_assets/chunks/fbcc4bf5367218951172bdee6f77d7a6-Dax2VSCd.woff2 +0 -0
  338. package/dist/client/_assets/chunks/fc895f5ce66b656f4a933097bf2a8775-DjjysNdf.woff2 +0 -0
  339. package/dist/client/_assets/chunks/fc9ae5b600fb711f2d67e93ce768cba4-D1lx7LBz.woff2 +0 -0
  340. package/dist/client/_assets/chunks/fca6720fd14c467d29a90f18ef3859b9-B3btrwq3.woff2 +0 -0
  341. package/dist/client/_assets/chunks/fd1bb507bcbf04856eeb7f7fd47ea579-NR1nGJTt.woff2 +0 -0
  342. package/dist/client/_assets/chunks/fd1bceb55d3e0183ac2454b8532fec7d-D3t-v6nn.woff2 +0 -0
  343. package/dist/client/_assets/chunks/fd2069e6e8588c70b4f11364093b81f2-BoXhYVgw.woff2 +0 -0
  344. package/dist/client/_assets/chunks/{url-CG0eolsk.js → url-pLre2DM_.js} +1 -1
  345. package/dist/client/_assets/client-C_kImWZj.css +2 -0
  346. package/dist/client/_assets/client-D95FNDg5.js +272 -0
  347. package/dist/client/_assets/client-auth-BRFl5zQA.js +4547 -0
  348. package/dist/client/_assets/client-cjk-jp-DZwrTzQC.css +1 -0
  349. package/dist/client/_assets/client-cjk-kr-_3ZNI2ZP.css +1 -0
  350. package/dist/env-wCpMcNXs.js +252 -0
  351. package/dist/github-api-CficQztC.js +176 -0
  352. package/dist/github-app-F4qZ05xk.js +275 -0
  353. package/dist/github-sync-zohnA9qv.js +4696 -0
  354. package/dist/index.js +88 -2
  355. package/dist/node.js +71 -30
  356. package/dist/url-FvvgARU9.js +305 -0
  357. package/package.json +16 -12
  358. package/src/__tests__/bin/content-cli.test.ts +179 -0
  359. package/src/__tests__/bin/media-cli.test.ts +192 -0
  360. package/src/__tests__/export-hugo-build.test.ts +178 -0
  361. package/src/__tests__/export-import-roundtrip.test.ts +265 -0
  362. package/src/__tests__/export-service.test.ts +763 -510
  363. package/src/__tests__/helpers/export-fixtures.ts +108 -0
  364. package/src/__tests__/import-site-command.test.ts +358 -350
  365. package/src/__tests__/site-pull-media.test.ts +256 -0
  366. package/src/app.tsx +65 -13
  367. package/src/client/__tests__/collection-form-bridge.test.ts +87 -7
  368. package/src/client/__tests__/collection-page-actions.test.ts +4 -4
  369. package/src/client/__tests__/collection-picker-order.test.ts +53 -0
  370. package/src/client/__tests__/collection-sort-menu.test.ts +2 -2
  371. package/src/client/__tests__/compose-bridge.test.ts +305 -13
  372. package/src/client/__tests__/compose-launch.test.ts +120 -0
  373. package/src/client/__tests__/compose-shortcuts.test.ts +141 -0
  374. package/src/client/__tests__/feed-video-player.test.ts +44 -0
  375. package/src/client/__tests__/site-header-nav.test.ts +250 -0
  376. package/src/client/__tests__/sortable-list.test.ts +6 -2
  377. package/src/client/__tests__/thread-context.test.ts +117 -0
  378. package/src/client/collection-form-bridge.ts +109 -4
  379. package/src/client/collection-page-actions.ts +3 -1
  380. package/src/client/collection-picker-order.ts +104 -0
  381. package/src/client/components/__tests__/{jant-collection-sidebar.test.ts → jant-collection-directory.test.ts} +145 -8
  382. package/src/client/components/__tests__/jant-collection-form.test.ts +40 -18
  383. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1285 -66
  384. package/src/client/components/__tests__/jant-compose-editor.test.ts +254 -19
  385. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +127 -1
  386. package/src/client/components/__tests__/jant-media-lightbox.test.ts +237 -0
  387. package/src/client/components/__tests__/jant-nav-manager.test.ts +326 -0
  388. package/src/client/components/__tests__/jant-post-menu.test.ts +372 -2
  389. package/src/client/components/__tests__/jant-settings-avatar.test.ts +61 -5
  390. package/src/client/components/__tests__/jant-settings-general.test.ts +29 -28
  391. package/src/client/components/__tests__/jant-text-preview.test.ts +206 -0
  392. package/src/client/components/collection-manager-types.ts +18 -1
  393. package/src/client/components/compose-types.ts +48 -0
  394. package/src/client/components/jant-collection-directory.ts +1448 -0
  395. package/src/client/components/jant-collection-form.ts +65 -15
  396. package/src/client/components/jant-command-palette.ts +544 -0
  397. package/src/client/components/jant-compose-dialog.ts +2490 -375
  398. package/src/client/components/jant-compose-editor.ts +420 -75
  399. package/src/client/components/jant-compose-fullscreen.ts +59 -8
  400. package/src/client/components/jant-media-lightbox.ts +373 -18
  401. package/src/client/components/jant-nav-manager.ts +624 -286
  402. package/src/client/components/jant-post-menu.ts +451 -126
  403. package/src/client/components/jant-repo-picker-types.ts +39 -0
  404. package/src/client/components/jant-repo-picker.ts +799 -0
  405. package/src/client/components/jant-settings-avatar.ts +16 -2
  406. package/src/client/components/jant-settings-general.ts +137 -35
  407. package/src/client/components/jant-text-preview.ts +239 -55
  408. package/src/client/components/nav-manager-types.ts +25 -9
  409. package/src/client/components/settings-types.ts +13 -2
  410. package/src/client/compose-bridge.ts +428 -68
  411. package/src/client/compose-launch.ts +41 -4
  412. package/src/client/compose-shortcuts.ts +77 -5
  413. package/src/client/feed-video-player.ts +374 -0
  414. package/src/client/image-processor.ts +53 -127
  415. package/src/client/media-lightbox-events.ts +1 -0
  416. package/src/client/media-scroll-hint.ts +66 -0
  417. package/src/client/multipart-upload.ts +4 -1
  418. package/src/client/palette-shortcuts.ts +35 -0
  419. package/src/client/search-rank.ts +74 -0
  420. package/src/client/settings-bridge.ts +7 -0
  421. package/src/client/site-header-nav.d.ts +1 -0
  422. package/src/client/site-header-nav.js +139 -51
  423. package/src/client/sortable-list.ts +2 -0
  424. package/src/client/thread-context.ts +54 -11
  425. package/src/client/tiptap/__tests__/block-insertion.test.ts +65 -0
  426. package/src/client/tiptap/__tests__/footnotes.test.ts +692 -0
  427. package/src/client/tiptap/__tests__/link-toolbar.test.ts +120 -4
  428. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +48 -0
  429. package/src/client/tiptap/block-insertion.ts +36 -0
  430. package/src/client/tiptap/bubble-menu.ts +34 -4
  431. package/src/client/tiptap/create-editor.ts +49 -30
  432. package/src/client/tiptap/embed-dialog.ts +301 -0
  433. package/src/client/tiptap/embed-node.ts +373 -0
  434. package/src/client/tiptap/embed-paste.ts +91 -0
  435. package/src/client/tiptap/extensions.ts +77 -28
  436. package/src/client/tiptap/footnotes.ts +718 -0
  437. package/src/client/tiptap/html-block-node.ts +222 -0
  438. package/src/client/tiptap/image-node.ts +49 -6
  439. package/src/client/tiptap/inline-image-upload.ts +200 -40
  440. package/src/client/tiptap/link-input-rules.ts +50 -16
  441. package/src/client/tiptap/link-toolbar.ts +156 -133
  442. package/src/client/tiptap/markdown-clipboard.ts +120 -0
  443. package/src/client/tiptap/more-break.ts +36 -3
  444. package/src/client/tiptap/paste-media.ts +6 -0
  445. package/src/client/tiptap/slash-commands.ts +167 -14
  446. package/src/client/tiptap/tab-indent.ts +184 -0
  447. package/src/client/tiptap/toolbar-mode.ts +15 -2
  448. package/src/client/tiptap/wrapping-input-rules.ts +102 -0
  449. package/src/client/types/sortablejs.d.ts +9 -1
  450. package/src/client/upload-session.ts +2 -1
  451. package/src/client/video-processor.ts +177 -20
  452. package/src/client-auth.ts +9 -1
  453. package/src/client-site.ts +15 -0
  454. package/src/client.ts +2 -0
  455. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  456. package/src/db/__tests__/migration-rehearsal.test.ts +5 -1
  457. package/src/db/__tests__/migrations.test.ts +25 -2
  458. package/src/db/backfills/0001_strip_collection_path_c_prefix.sql +11 -0
  459. package/src/db/backfills/0002_strip_collection_path_c_prefix_fix.sql +12 -0
  460. package/src/db/backfills/0003_clear_system_nav_default_labels.sql +15 -0
  461. package/src/db/dialect.ts +40 -0
  462. package/src/db/migrations/0005_busy_the_phantom.sql +39 -0
  463. package/src/db/migrations/0006_adorable_magdalene.sql +3 -0
  464. package/src/db/migrations/0007_unusual_warstar.sql +1 -0
  465. package/src/db/migrations/0008_nasty_lockheed.sql +1 -0
  466. package/src/db/migrations/0009_clear_fixer.sql +31 -0
  467. package/src/db/migrations/0010_futuristic_preak.sql +31 -0
  468. package/src/db/migrations/0011_bizarre_smasher.sql +30 -0
  469. package/src/db/migrations/0012_furry_thena.sql +52 -0
  470. package/src/db/migrations/0013_mixed_lightspeed.sql +39 -0
  471. package/src/db/migrations/0014_high_the_santerians.sql +41 -0
  472. package/src/db/migrations/0015_skinny_shinobi_shaw.sql +1 -0
  473. package/src/db/migrations/0016_remarkable_nicolaos.sql +17 -0
  474. package/src/db/migrations/0017_powerful_moonstone.sql +14 -0
  475. package/src/db/migrations/meta/0005_snapshot.json +1904 -0
  476. package/src/db/migrations/meta/0006_snapshot.json +1925 -0
  477. package/src/db/migrations/meta/0007_snapshot.json +1933 -0
  478. package/src/db/migrations/meta/0008_snapshot.json +1940 -0
  479. package/src/db/migrations/meta/0009_snapshot.json +1952 -0
  480. package/src/db/migrations/meta/0010_snapshot.json +1952 -0
  481. package/src/db/migrations/meta/0011_snapshot.json +1948 -0
  482. package/src/db/migrations/meta/0012_snapshot.json +1955 -0
  483. package/src/db/migrations/meta/0013_snapshot.json +1977 -0
  484. package/src/db/migrations/meta/0014_snapshot.json +1988 -0
  485. package/src/db/migrations/meta/0015_snapshot.json +1995 -0
  486. package/src/db/migrations/meta/0016_snapshot.json +2104 -0
  487. package/src/db/migrations/meta/0017_snapshot.json +2188 -0
  488. package/src/db/migrations/meta/_journal.json +92 -1
  489. package/src/db/migrations/pg/0003_motionless_norrin_radd.sql +21 -0
  490. package/src/db/migrations/pg/0004_nervous_captain_midlands.sql +3 -0
  491. package/src/db/migrations/pg/0005_romantic_mesmero.sql +1 -0
  492. package/src/db/migrations/pg/0006_perpetual_bruce_banner.sql +1 -0
  493. package/src/db/migrations/pg/0007_nav_item_placement.sql +1 -0
  494. package/src/db/migrations/pg/0008_yielding_frightful_four.sql +3 -0
  495. package/src/db/migrations/pg/0009_outstanding_ogun.sql +1 -0
  496. package/src/db/migrations/pg/0010_overjoyed_gertrude_yorkes.sql +28 -0
  497. package/src/db/migrations/pg/0011_fixed_hulk.sql +19 -0
  498. package/src/db/migrations/pg/0012_cute_shockwave.sql +2 -0
  499. package/src/db/migrations/pg/0013_bizarre_obadiah_stane.sql +1 -0
  500. package/src/db/migrations/pg/0014_tearful_grim_reaper.sql +16 -0
  501. package/src/db/migrations/pg/0015_daffy_mikhail_rasputin.sql +14 -0
  502. package/src/db/migrations/pg/meta/0003_snapshot.json +2482 -0
  503. package/src/db/migrations/pg/meta/0004_snapshot.json +2500 -0
  504. package/src/db/migrations/pg/meta/0005_snapshot.json +2507 -0
  505. package/src/db/migrations/pg/meta/0006_snapshot.json +2513 -0
  506. package/src/db/migrations/pg/meta/0008_snapshot.json +2524 -0
  507. package/src/db/migrations/pg/meta/0009_snapshot.json +2520 -0
  508. package/src/db/migrations/pg/meta/0010_snapshot.json +2526 -0
  509. package/src/db/migrations/pg/meta/0011_snapshot.json +2563 -0
  510. package/src/db/migrations/pg/meta/0012_snapshot.json +2573 -0
  511. package/src/db/migrations/pg/meta/0013_snapshot.json +2579 -0
  512. package/src/db/migrations/pg/meta/0014_snapshot.json +2702 -0
  513. package/src/db/migrations/pg/meta/0015_snapshot.json +2803 -0
  514. package/src/db/migrations/pg/meta/_journal.json +92 -1
  515. package/src/db/pg/__tests__/node.test.ts +38 -0
  516. package/src/db/pg/node.ts +149 -0
  517. package/src/db/pg/schema.ts +130 -12
  518. package/src/db/rehearsal-fixtures/demo-current.sql +47 -3
  519. package/src/db/schema.ts +139 -12
  520. package/src/i18n/Trans.tsx +3 -3
  521. package/src/i18n/__tests__/context.test.tsx +9 -6
  522. package/src/i18n/__tests__/detect.test.ts +65 -48
  523. package/src/i18n/__tests__/fallback.test.ts +40 -0
  524. package/src/i18n/__tests__/message-placeholders.test.ts +43 -0
  525. package/src/i18n/__tests__/middleware.test.ts +83 -0
  526. package/src/i18n/context.tsx +9 -44
  527. package/src/i18n/detect.ts +95 -28
  528. package/src/i18n/i18n.ts +41 -17
  529. package/src/i18n/index.ts +7 -5
  530. package/src/i18n/locales/{en.po → public/en.po} +298 -1151
  531. package/src/i18n/locales/public/en.ts +1 -0
  532. package/src/i18n/locales/public/zh-Hans.po +2327 -0
  533. package/src/i18n/locales/public/zh-Hans.ts +1 -0
  534. package/src/i18n/locales/public/zh-Hant.po +2327 -0
  535. package/src/i18n/locales/public/zh-Hant.ts +1 -0
  536. package/src/i18n/locales/settings/en.po +1622 -0
  537. package/src/i18n/locales/settings/en.ts +1 -0
  538. package/src/i18n/locales/settings/zh-Hans.po +1622 -0
  539. package/src/i18n/locales/settings/zh-Hans.ts +1 -0
  540. package/src/i18n/locales/settings/zh-Hant.po +1622 -0
  541. package/src/i18n/locales/settings/zh-Hant.ts +1 -0
  542. package/src/i18n/middleware.ts +37 -24
  543. package/src/index.ts +4 -5
  544. package/src/lib/__tests__/collection-groups.test.ts +80 -0
  545. package/src/lib/__tests__/constants.test.ts +1 -1
  546. package/src/lib/__tests__/csp-builder.test.ts +68 -0
  547. package/src/lib/__tests__/display-text.test.ts +27 -0
  548. package/src/lib/__tests__/embed-providers.test.ts +104 -0
  549. package/src/lib/__tests__/embed-render.test.ts +60 -0
  550. package/src/lib/__tests__/feed.test.ts +125 -63
  551. package/src/lib/__tests__/hosted-signin.test.ts +11 -2
  552. package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +88 -0
  553. package/src/lib/__tests__/markdown-to-tiptap.test.ts +59 -0
  554. package/src/lib/__tests__/markdown.test.ts +47 -5
  555. package/src/lib/__tests__/navigation.test.ts +148 -0
  556. package/src/lib/__tests__/post-display.test.ts +3 -2
  557. package/src/lib/__tests__/post-meta.test.ts +3 -0
  558. package/src/lib/__tests__/resolve-config.test.ts +30 -25
  559. package/src/lib/__tests__/schemas.test.ts +35 -0
  560. package/src/lib/__tests__/slug-format.test.ts +8 -0
  561. package/src/lib/__tests__/summary.test.ts +400 -1
  562. package/src/lib/__tests__/theme.test.ts +41 -2
  563. package/src/lib/__tests__/timeline.test.ts +114 -26
  564. package/src/lib/__tests__/tiptap-render.test.ts +224 -0
  565. package/src/lib/__tests__/tiptap-to-markdown.test.ts +51 -0
  566. package/src/lib/__tests__/video-playback.test.ts +48 -0
  567. package/src/lib/__tests__/view.test.ts +115 -8
  568. package/src/lib/__tests__/worker-response-cache.test.ts +311 -0
  569. package/src/lib/__tests__/youtube.test.ts +99 -0
  570. package/src/lib/api-media.ts +55 -0
  571. package/src/lib/api-posts.ts +99 -0
  572. package/src/lib/api-search.ts +47 -0
  573. package/src/lib/api-settings.ts +122 -0
  574. package/src/lib/asset-path.ts +20 -2
  575. package/src/lib/collection-groups.ts +70 -0
  576. package/src/lib/collection-paths.ts +40 -0
  577. package/src/lib/constants.ts +3 -4
  578. package/src/lib/csp-builder.ts +99 -0
  579. package/src/lib/decorative-quote-mark.ts +11 -0
  580. package/src/lib/display-text.ts +56 -0
  581. package/src/lib/embed-providers.ts +289 -0
  582. package/src/lib/embed-render.ts +151 -0
  583. package/src/lib/env.ts +70 -1
  584. package/src/lib/excerpt.ts +11 -2
  585. package/src/lib/feed.ts +89 -66
  586. package/src/lib/footnotes.ts +146 -0
  587. package/src/lib/github-api.ts +423 -0
  588. package/src/lib/github-app-state.ts +135 -0
  589. package/src/lib/github-app.ts +487 -0
  590. package/src/lib/github-sync-queue-handler.ts +69 -0
  591. package/src/lib/github-sync-site-config.ts +57 -0
  592. package/src/lib/github-sync-trigger.ts +199 -0
  593. package/src/lib/github-sync-worker.ts +72 -0
  594. package/src/lib/hosted-signin.ts +1 -1
  595. package/src/lib/hugo-markdown.ts +255 -0
  596. package/src/lib/job-queue-cf.ts +18 -0
  597. package/src/lib/job-queue-db.ts +149 -0
  598. package/src/lib/job-queue.ts +35 -0
  599. package/src/lib/markdown-manager.ts +864 -0
  600. package/src/lib/markdown-to-tiptap.ts +4 -323
  601. package/src/lib/markdown.ts +16 -24
  602. package/src/lib/media-helpers.ts +1 -0
  603. package/src/lib/navigation.ts +40 -8
  604. package/src/lib/post-display.ts +10 -22
  605. package/src/lib/public-storage.ts +6 -1
  606. package/src/lib/render.tsx +5 -2
  607. package/src/lib/resolve-config.ts +23 -11
  608. package/src/lib/rich-image.ts +89 -0
  609. package/src/lib/schemas.ts +186 -12
  610. package/src/lib/slug-format.ts +8 -0
  611. package/src/lib/summary.ts +125 -18
  612. package/src/lib/theme.ts +119 -0
  613. package/src/lib/timeline.ts +91 -63
  614. package/src/lib/tiptap-render.ts +265 -126
  615. package/src/lib/tiptap-to-markdown.ts +6 -329
  616. package/src/lib/upload.ts +97 -6
  617. package/src/lib/version.ts +18 -0
  618. package/src/lib/video-playback.ts +73 -0
  619. package/src/lib/view.ts +130 -90
  620. package/src/lib/webhook-signature.ts +65 -0
  621. package/src/lib/worker-response-cache.ts +220 -0
  622. package/src/lib/youtube.ts +119 -0
  623. package/src/middleware/__tests__/onboarding.test.ts +3 -3
  624. package/src/middleware/__tests__/secure-headers.test.ts +28 -1
  625. package/src/middleware/config.ts +7 -10
  626. package/src/middleware/cors.ts +49 -0
  627. package/src/middleware/secure-headers.ts +45 -55
  628. package/src/node/__tests__/cli-migrate.test.ts +111 -0
  629. package/src/node/__tests__/cli-runtime-target.test.ts +1 -1
  630. package/src/node/__tests__/cli-site-token-env.test.ts +2 -2
  631. package/src/preset.css +316 -12
  632. package/src/routes/__tests__/compose.test.ts +3 -1
  633. package/src/routes/api/__tests__/collections.test.ts +81 -27
  634. package/src/routes/api/__tests__/mcp.test.ts +389 -0
  635. package/src/routes/api/__tests__/nav-items.test.ts +163 -2
  636. package/src/routes/api/__tests__/posts.test.ts +190 -0
  637. package/src/routes/api/__tests__/settings.test.ts +99 -3
  638. package/src/routes/api/__tests__/upload-multipart.test.ts +10 -2
  639. package/src/routes/api/__tests__/upload.test.ts +132 -0
  640. package/src/routes/api/__tests__/uploads.test.ts +5 -3
  641. package/src/routes/api/collections.ts +67 -26
  642. package/src/routes/api/custom-urls.ts +1 -0
  643. package/src/routes/api/export.ts +8 -4
  644. package/src/routes/api/github-sync.tsx +445 -0
  645. package/src/routes/api/internal/__tests__/sites.test.ts +45 -2
  646. package/src/routes/api/internal/sites.ts +2 -0
  647. package/src/routes/api/internal/text-attachments.ts +60 -0
  648. package/src/routes/api/internal/uploads.ts +5 -1
  649. package/src/routes/api/mcp.ts +30 -0
  650. package/src/routes/api/nav-items.ts +30 -11
  651. package/src/routes/api/palette.ts +17 -0
  652. package/src/routes/api/posts.ts +37 -106
  653. package/src/routes/api/public/__tests__/posts.test.ts +372 -0
  654. package/src/routes/api/public/posts.ts +302 -0
  655. package/src/routes/api/search.ts +1 -47
  656. package/src/routes/api/settings.ts +44 -58
  657. package/src/routes/api/upload-multipart.ts +15 -5
  658. package/src/routes/api/upload.ts +62 -49
  659. package/src/routes/api/uploads.ts +1 -0
  660. package/src/routes/auth/__tests__/hosted-sso.test.ts +33 -0
  661. package/src/routes/auth/__tests__/setup.test.ts +29 -4
  662. package/src/routes/auth/hosted-sso-expired-page.tsx +89 -0
  663. package/src/routes/auth/hosted-sso.ts +42 -1
  664. package/src/routes/auth/reset.tsx +46 -32
  665. package/src/routes/auth/setup.tsx +53 -43
  666. package/src/routes/auth/signin.tsx +34 -24
  667. package/src/routes/compose.tsx +114 -1
  668. package/src/routes/dash/__tests__/font-theme.test.ts +6 -27
  669. package/src/routes/dash/__tests__/settings-avatar.test.ts +2 -3
  670. package/src/routes/dash/custom-urls.tsx +250 -112
  671. package/src/routes/dash/settings.tsx +1163 -70
  672. package/src/routes/feed/__tests__/{rss.test.ts → feed.test.ts} +62 -123
  673. package/src/routes/feed/feed.ts +386 -0
  674. package/src/routes/feed/manifest.ts +67 -0
  675. package/src/routes/feed/sitemap.ts +11 -2
  676. package/src/routes/pages/__tests__/collection-routing.test.ts +176 -0
  677. package/src/routes/pages/__tests__/collections.test.ts +2 -3
  678. package/src/routes/pages/__tests__/featured.test.ts +2 -3
  679. package/src/routes/pages/archive.tsx +523 -127
  680. package/src/routes/pages/collection.tsx +265 -59
  681. package/src/routes/pages/collections.tsx +5 -1
  682. package/src/routes/pages/home.tsx +2 -2
  683. package/src/routes/pages/latest.tsx +3 -3
  684. package/src/routes/pages/page.tsx +221 -3
  685. package/src/routes/pages/partials.tsx +5 -0
  686. package/src/routes/pages/search.tsx +17 -1
  687. package/src/routes/pages/theme-sample.tsx +1 -2
  688. package/src/runtime/__tests__/node.test.ts +1 -1
  689. package/src/runtime/__tests__/readiness.test.ts +89 -0
  690. package/src/runtime/readiness.ts +129 -0
  691. package/src/runtime/site.ts +9 -1
  692. package/src/services/__tests__/collection.test.ts +181 -53
  693. package/src/services/__tests__/github-app-installations.test.ts +181 -0
  694. package/src/services/__tests__/github-sync-classify.test.ts +189 -0
  695. package/src/services/__tests__/github-sync-push.test.ts +159 -0
  696. package/src/services/__tests__/media.test.ts +444 -7
  697. package/src/services/__tests__/navigation.test.ts +271 -14
  698. package/src/services/__tests__/post-timeline.test.ts +85 -17
  699. package/src/services/__tests__/post.test.ts +118 -10
  700. package/src/services/__tests__/settings.test.ts +43 -21
  701. package/src/services/auth.ts +2 -2
  702. package/src/services/bootstrap.ts +10 -7
  703. package/src/services/collection.ts +644 -191
  704. package/src/services/custom-url.ts +34 -7
  705. package/src/services/export-theme/assets/client-site.css +2 -0
  706. package/src/services/export-theme/assets/client-site.js +145 -0
  707. package/src/services/export-theme/layouts/_default/alias.html +49 -0
  708. package/src/services/export-theme/layouts/_default/baseof.html +21 -0
  709. package/src/services/export-theme/layouts/_default/list.html +104 -0
  710. package/src/services/export-theme/layouts/_default/rss.xml +160 -0
  711. package/src/services/export-theme/layouts/_default/single.html +15 -0
  712. package/src/services/export-theme/layouts/archive/list.html +31 -0
  713. package/src/services/export-theme/layouts/collection/single.html +19 -0
  714. package/src/services/export-theme/layouts/collections/list.html +111 -0
  715. package/src/services/export-theme/layouts/featured/list.html +28 -0
  716. package/src/services/export-theme/layouts/index.html +41 -0
  717. package/src/services/export-theme/layouts/partials/feed-post-content.xml +107 -0
  718. package/src/services/export-theme/layouts/partials/footer.html +32 -0
  719. package/src/services/export-theme/layouts/partials/head.html +52 -0
  720. package/src/services/export-theme/layouts/partials/header.html +115 -0
  721. package/src/services/export-theme/layouts/partials/media-gallery.html +246 -0
  722. package/src/services/export-theme/layouts/partials/pagination.html +55 -0
  723. package/src/services/export-theme/layouts/partials/post-card.html +127 -0
  724. package/src/services/export-theme/layouts/partials/reply.html +91 -0
  725. package/src/services/export-theme/layouts/partials/thread-preview.html +82 -0
  726. package/src/services/export-theme/layouts/post/list.html +129 -0
  727. package/src/services/export-theme/styles/main.css +1981 -0
  728. package/src/services/export-theme/theme.toml +12 -0
  729. package/src/services/export.ts +1365 -2412
  730. package/src/services/github-app-installations.ts +302 -0
  731. package/src/services/github-sync.ts +769 -0
  732. package/src/services/index.ts +15 -0
  733. package/src/services/mcp.ts +1324 -0
  734. package/src/services/media.ts +336 -25
  735. package/src/services/navigation.ts +115 -23
  736. package/src/services/path.ts +129 -28
  737. package/src/services/post.ts +779 -207
  738. package/src/services/search.ts +3 -0
  739. package/src/services/settings.ts +29 -15
  740. package/src/services/site-admin.ts +26 -6
  741. package/src/services/upload-session.ts +69 -34
  742. package/src/style-cjk-jp.css +3 -0
  743. package/src/style-cjk-kr.css +3 -0
  744. package/src/styles/components.css +160 -59
  745. package/src/styles/fonts/et-book/et-book-bold-line-figures.woff +0 -0
  746. package/src/styles/fonts/et-book/et-book-display-italic-old-style-figures.woff +0 -0
  747. package/src/styles/fonts/et-book/et-book-roman-line-figures.woff +0 -0
  748. package/src/styles/fonts/et-book.css +31 -0
  749. package/src/styles/fonts/latin.css +1 -0
  750. package/src/styles/fonts/noto-serif-jp/400/057a6a98bda7fe57105ddaa99ec82015.woff2 +0 -0
  751. package/src/styles/fonts/noto-serif-jp/400/0acd1fe2b2ea1ad1bfee7ae1fa139d27.woff2 +0 -0
  752. package/src/styles/fonts/noto-serif-jp/400/0c2ab6b295e55f356f8020d4e7747522.woff2 +0 -0
  753. package/src/styles/fonts/noto-serif-jp/400/0c5f9492af03a4fa42c784de94649de1.woff2 +0 -0
  754. package/src/styles/fonts/noto-serif-jp/400/0d9a936885a4c39077438effd3779cbd.woff2 +0 -0
  755. package/src/styles/fonts/noto-serif-jp/400/0e645da524f7cfc0e8c3c03fb2b08428.woff2 +0 -0
  756. package/src/styles/fonts/noto-serif-jp/400/135e83b403475c5dc9e49b43501a5b84.woff2 +0 -0
  757. package/src/styles/fonts/noto-serif-jp/400/13b2c53b30e6a3e4de7132dbc18dbdfc.woff2 +0 -0
  758. package/src/styles/fonts/noto-serif-jp/400/15d95680dc31cc6ce20e0a5464106dc4.woff2 +0 -0
  759. package/src/styles/fonts/noto-serif-jp/400/1733f27476507ca68b68a803bc533cc2.woff2 +0 -0
  760. package/src/styles/fonts/noto-serif-jp/400/188c2db794f3dd7a45889ddbc81da9bb.woff2 +0 -0
  761. package/src/styles/fonts/noto-serif-jp/400/1cf737900dd49c2e88f1b3221a82a602.woff2 +0 -0
  762. package/src/styles/fonts/noto-serif-jp/400/233d3a685ee18276b319363474599d47.woff2 +0 -0
  763. package/src/styles/fonts/noto-serif-jp/400/284b53bbefb06924cf236d24c8ed5641.woff2 +0 -0
  764. package/src/styles/fonts/noto-serif-jp/400/2a56eaf19d1d38a6b57e2a388f733676.woff2 +0 -0
  765. package/src/styles/fonts/noto-serif-jp/400/2abfbab82b6a7c04426afc054d2464da.woff2 +0 -0
  766. package/src/styles/fonts/noto-serif-jp/400/2e6f4bb71ef6b38765d51acc5a79f638.woff2 +0 -0
  767. package/src/styles/fonts/noto-serif-jp/400/31194d303a67561926a544ed0e072aee.woff2 +0 -0
  768. package/src/styles/fonts/noto-serif-jp/400/3623d96698132c61e861f984ce11cc23.woff2 +0 -0
  769. package/src/styles/fonts/noto-serif-jp/400/39211b02f24c69cbd1b54f6d41b01933.woff2 +0 -0
  770. package/src/styles/fonts/noto-serif-jp/400/3e24063b19abd2b051af986a6870046d.woff2 +0 -0
  771. package/src/styles/fonts/noto-serif-jp/400/4410f876d27c9821839474b6b1059fd7.woff2 +0 -0
  772. package/src/styles/fonts/noto-serif-jp/400/479329a39a3eb6c0047e6b0981919855.woff2 +0 -0
  773. package/src/styles/fonts/noto-serif-jp/400/527ff336279465617aafbafe1a830632.woff2 +0 -0
  774. package/src/styles/fonts/noto-serif-jp/400/56b40518ea0608e62826bd44ee27f5f9.woff2 +0 -0
  775. package/src/styles/fonts/noto-serif-jp/400/5acc7f736217259db79a42bf44241c48.woff2 +0 -0
  776. package/src/styles/fonts/noto-serif-jp/400/5c05f8b75c9d7f25b3c04ca711cf43b3.woff2 +0 -0
  777. package/src/styles/fonts/noto-serif-jp/400/5e0124c7265f1b4cf0fc797b94efbebc.woff2 +0 -0
  778. package/src/styles/fonts/noto-serif-jp/400/615a7e0d63f257109d599b2a1977de68.woff2 +0 -0
  779. package/src/styles/fonts/noto-serif-jp/400/61a2c80d0c924d5a6187c02e8d1d1642.woff2 +0 -0
  780. package/src/styles/fonts/noto-serif-jp/400/62fc94f8d4c5a750b7f25e7387539910.woff2 +0 -0
  781. package/src/styles/fonts/noto-serif-jp/400/639c7c6b0b0c27c738702741cfa4b8c0.woff2 +0 -0
  782. package/src/styles/fonts/noto-serif-jp/400/68a3a4bf337f8a0722be76676e20b850.woff2 +0 -0
  783. package/src/styles/fonts/noto-serif-jp/400/6abdd163d2c4b85698db8aa1ce149361.woff2 +0 -0
  784. package/src/styles/fonts/noto-serif-jp/400/73b794d885b88f7befb7ea8ab1780a15.woff2 +0 -0
  785. package/src/styles/fonts/noto-serif-jp/400/743f290baf027d4626a86e22f3d44600.woff2 +0 -0
  786. package/src/styles/fonts/noto-serif-jp/400/757019c01c5e2c2b0f54293cea3b5636.woff2 +0 -0
  787. package/src/styles/fonts/noto-serif-jp/400/76f117a3baacda304a71965a17266a13.woff2 +0 -0
  788. package/src/styles/fonts/noto-serif-jp/400/77747f17625a259fb405b7bbdd84ac4b.woff2 +0 -0
  789. package/src/styles/fonts/noto-serif-jp/400/788498548ddfb710d6ef3c7bff79fa97.woff2 +0 -0
  790. package/src/styles/fonts/noto-serif-jp/400/78ea6c40923ce95367e3517dd1e5a849.woff2 +0 -0
  791. package/src/styles/fonts/noto-serif-jp/400/7f9a6c9286b68de9c72b0024f7beeb40.woff2 +0 -0
  792. package/src/styles/fonts/noto-serif-jp/400/823d1bb11097331238d103c7f72138dc.woff2 +0 -0
  793. package/src/styles/fonts/noto-serif-jp/400/888ba53510213d5d1f6a7deb60e569b6.woff2 +0 -0
  794. package/src/styles/fonts/noto-serif-jp/400/892aa49b2529c89bb4076d4aa51fe30f.woff2 +0 -0
  795. package/src/styles/fonts/noto-serif-jp/400/95ff41191c76ef893ac70a1043e95e44.woff2 +0 -0
  796. package/src/styles/fonts/noto-serif-jp/400/96c75862c8cec51a7c05ff025fea86cd.woff2 +0 -0
  797. package/src/styles/fonts/noto-serif-jp/400/9b21e8244f5930c48ad5073f83777b6c.woff2 +0 -0
  798. package/src/styles/fonts/noto-serif-jp/400/9fce02d1a09f464c36c9fcfb14a354c5.woff2 +0 -0
  799. package/src/styles/fonts/noto-serif-jp/400/a0a6755c7e3f9e4cf03387027dd9f16c.woff2 +0 -0
  800. package/src/styles/fonts/noto-serif-jp/400/a4475c442c6b259354c9eda62e64e7df.woff2 +0 -0
  801. package/src/styles/fonts/noto-serif-jp/400/a4a39b5f048671aa37c2b2a9581ab08a.woff2 +0 -0
  802. package/src/styles/fonts/noto-serif-jp/400/a534449d67561ac9b06489f64e57ad98.woff2 +0 -0
  803. package/src/styles/fonts/noto-serif-jp/400/a919a8c6fec6d7e8bb21177965f2e9d7.woff2 +0 -0
  804. package/src/styles/fonts/noto-serif-jp/400/ad67113f88b59582991cb0c5d33ea19f.woff2 +0 -0
  805. package/src/styles/fonts/noto-serif-jp/400/adb6e613fb99c6dd660b727101f554a8.woff2 +0 -0
  806. package/src/styles/fonts/noto-serif-jp/400/adc4a98a89870ef984ee8f4b57c8a3a5.woff2 +0 -0
  807. package/src/styles/fonts/noto-serif-jp/400/b12379ab782468d725519cd07a7d15bc.woff2 +0 -0
  808. package/src/styles/fonts/noto-serif-jp/400/b55486f8a459c838fe329d4e79a8c211.woff2 +0 -0
  809. package/src/styles/fonts/noto-serif-jp/400/bb0b15492b8cdbbec57c0bcfa0aa9241.woff2 +0 -0
  810. package/src/styles/fonts/noto-serif-jp/400/bedc74b423b7293b6ad0bdecc61c42cc.woff2 +0 -0
  811. package/src/styles/fonts/noto-serif-jp/400/bfb00d4a4c48661bd0be99f300a0faae.woff2 +0 -0
  812. package/src/styles/fonts/noto-serif-jp/400/c1b0df29ae41d764904df84e9ac83d1e.woff2 +0 -0
  813. package/src/styles/fonts/noto-serif-jp/400/c42c67070cfe99cf823df92d81c7fa6e.woff2 +0 -0
  814. package/src/styles/fonts/noto-serif-jp/400/c4d749e45ecd5a5aed5a0bb3ebfd355d.woff2 +0 -0
  815. package/src/styles/fonts/noto-serif-jp/400/c96d83978add28b356c22c4c84916733.woff2 +0 -0
  816. package/src/styles/fonts/noto-serif-jp/400/cb7ccd6494256f7a2977b7c2b0225592.woff2 +0 -0
  817. package/src/styles/fonts/noto-serif-jp/400/ce41d70ce6a069a498525c9e15c45cf2.woff2 +0 -0
  818. package/src/styles/fonts/noto-serif-jp/400/d509ee5c8241bc7c3de2039d75564fa5.woff2 +0 -0
  819. package/src/styles/fonts/noto-serif-jp/400/d675d717cd329bfd4c0524f76ae1579c.woff2 +0 -0
  820. package/src/styles/fonts/noto-serif-jp/400/dc8f6256445e68199540be9ade33529f.woff2 +0 -0
  821. package/src/styles/fonts/noto-serif-jp/400/e8c15be62faf978d208925e79ea6a10d.woff2 +0 -0
  822. package/src/styles/fonts/noto-serif-jp/400/e91c4d941ed9f5c2f3e27f205a3a225e.woff2 +0 -0
  823. package/src/styles/fonts/noto-serif-jp/400/e9c566be7a5d38a9085225f7372bc82b.woff2 +0 -0
  824. package/src/styles/fonts/noto-serif-jp/400/e9ebc567a711eeb29019ddae3e0ce7fe.woff2 +0 -0
  825. package/src/styles/fonts/noto-serif-jp/400/ecb8875a56c7b038b35432fda41ae128.woff2 +0 -0
  826. package/src/styles/fonts/noto-serif-jp/400/f1661731474b78bdf81114daf10b254f.woff2 +0 -0
  827. package/src/styles/fonts/noto-serif-jp/400/f2c58dee206ba9355046fc23d05491f7.woff2 +0 -0
  828. package/src/styles/fonts/noto-serif-jp/400/f50ac27ea4358d67fdda403c2bb52467.woff2 +0 -0
  829. package/src/styles/fonts/noto-serif-jp/400/f55334112f8e8be82d65db29887a663f.woff2 +0 -0
  830. package/src/styles/fonts/noto-serif-jp/400/f740e93f3c0277ecc616594103bca683.woff2 +0 -0
  831. package/src/styles/fonts/noto-serif-jp/400/f7633b5af033d76ff2fb3c3c266d77c5.woff2 +0 -0
  832. package/src/styles/fonts/noto-serif-jp/400/f7d8468cba2335a83ee414ea68291bab.woff2 +0 -0
  833. package/src/styles/fonts/noto-serif-jp/400/fc9ae5b600fb711f2d67e93ce768cba4.woff2 +0 -0
  834. package/src/styles/fonts/noto-serif-jp/400/fd1bceb55d3e0183ac2454b8532fec7d.woff2 +0 -0
  835. package/src/styles/fonts/noto-serif-jp/700/02611a045a7fe83a12014e3debc9f731.woff2 +0 -0
  836. package/src/styles/fonts/noto-serif-jp/700/0f5e1a18987dbc84ca05188c129e1936.woff2 +0 -0
  837. package/src/styles/fonts/noto-serif-jp/700/112743a4ab5fdd1498dfdf2b11336380.woff2 +0 -0
  838. package/src/styles/fonts/noto-serif-jp/700/1211f03d3ff5759a702631445793f60e.woff2 +0 -0
  839. package/src/styles/fonts/noto-serif-jp/700/147b24c2f08d03bbed30887d4cb3311c.woff2 +0 -0
  840. package/src/styles/fonts/noto-serif-jp/700/16663e567f81f4725a1522f37e18f71f.woff2 +0 -0
  841. package/src/styles/fonts/noto-serif-jp/700/1824321da801e6257b902f3d1d09054c.woff2 +0 -0
  842. package/src/styles/fonts/noto-serif-jp/700/1a3a92c7c060a71a6c35819b9ebcdbb9.woff2 +0 -0
  843. package/src/styles/fonts/noto-serif-jp/700/1fba7ec0e412e911bf31841de5a8a4e4.woff2 +0 -0
  844. package/src/styles/fonts/noto-serif-jp/700/28a97af9ab9d38983d20f39ff09840e1.woff2 +0 -0
  845. package/src/styles/fonts/noto-serif-jp/700/2bfaadaf3479c72286248e6de0be0ec9.woff2 +0 -0
  846. package/src/styles/fonts/noto-serif-jp/700/2c8c55e4cec85ce77e95cac9d330f5cf.woff2 +0 -0
  847. package/src/styles/fonts/noto-serif-jp/700/2ed9981d2e8983365bd051159b976b6d.woff2 +0 -0
  848. package/src/styles/fonts/noto-serif-jp/700/2f4c633e923ba30c6ba376367379fc91.woff2 +0 -0
  849. package/src/styles/fonts/noto-serif-jp/700/349965eee0a9b6c984a319ab96a4ece9.woff2 +0 -0
  850. package/src/styles/fonts/noto-serif-jp/700/36cda3eae13370b934bba4429fab9078.woff2 +0 -0
  851. package/src/styles/fonts/noto-serif-jp/700/3aa13f1843d7c8fa16109b6395a9a29d.woff2 +0 -0
  852. package/src/styles/fonts/noto-serif-jp/700/3e2c06bdd9dcab29aeaeb0cfb7fa608a.woff2 +0 -0
  853. package/src/styles/fonts/noto-serif-jp/700/424123f9371ba357087363263eeafcda.woff2 +0 -0
  854. package/src/styles/fonts/noto-serif-jp/700/43b3bbf43055c3e65db9cdf2c5b18c1c.woff2 +0 -0
  855. package/src/styles/fonts/noto-serif-jp/700/44704471b60c18b3ecec5300a88f1102.woff2 +0 -0
  856. package/src/styles/fonts/noto-serif-jp/700/46604090efbb99c2db1800928af0a239.woff2 +0 -0
  857. package/src/styles/fonts/noto-serif-jp/700/46998df85d31e629c0142f0556f5e7c5.woff2 +0 -0
  858. package/src/styles/fonts/noto-serif-jp/700/49902f1c41b281f8eb3771abdd4dccb8.woff2 +0 -0
  859. package/src/styles/fonts/noto-serif-jp/700/4db4f31a16965baa42cb7dad667a8f04.woff2 +0 -0
  860. package/src/styles/fonts/noto-serif-jp/700/572b9e28f1e6d89b765a16d809db84a1.woff2 +0 -0
  861. package/src/styles/fonts/noto-serif-jp/700/5921cef36750ff6092d94b83a802823b.woff2 +0 -0
  862. package/src/styles/fonts/noto-serif-jp/700/59e95d8b5332dcdae894a19946ab767d.woff2 +0 -0
  863. package/src/styles/fonts/noto-serif-jp/700/5ec349ff62b9ee5fe0c6dae867704dc4.woff2 +0 -0
  864. package/src/styles/fonts/noto-serif-jp/700/601f812e8b1eafecd5ba585a6a6d7962.woff2 +0 -0
  865. package/src/styles/fonts/noto-serif-jp/700/6025ddaf99d86cc8bfd781006b19fbd6.woff2 +0 -0
  866. package/src/styles/fonts/noto-serif-jp/700/60f6e28b3f0400b0e8fe32c3c7116102.woff2 +0 -0
  867. package/src/styles/fonts/noto-serif-jp/700/64a404d675f1d726f0891b7a80794595.woff2 +0 -0
  868. package/src/styles/fonts/noto-serif-jp/700/7158022889c6c177062ac85036e7af10.woff2 +0 -0
  869. package/src/styles/fonts/noto-serif-jp/700/7e38ca789a9e76ac91d9256374e154f0.woff2 +0 -0
  870. package/src/styles/fonts/noto-serif-jp/700/815a0b797465190f60d8b1a04656c42e.woff2 +0 -0
  871. package/src/styles/fonts/noto-serif-jp/700/8726558cf297cbda831cc19514897205.woff2 +0 -0
  872. package/src/styles/fonts/noto-serif-jp/700/8855472f3c02f6c7ebb3216617ebe4af.woff2 +0 -0
  873. package/src/styles/fonts/noto-serif-jp/700/8e231d707f0c4f8e3cde90a6b52a79aa.woff2 +0 -0
  874. package/src/styles/fonts/noto-serif-jp/700/9064c12fd72c7aba235d8c3881755b91.woff2 +0 -0
  875. package/src/styles/fonts/noto-serif-jp/700/911b9e53e9814de2998c60bf3115f560.woff2 +0 -0
  876. package/src/styles/fonts/noto-serif-jp/700/92fdc376bce277874e75db666f175b44.woff2 +0 -0
  877. package/src/styles/fonts/noto-serif-jp/700/930a23430f0eb64480d7fe5f82834e21.woff2 +0 -0
  878. package/src/styles/fonts/noto-serif-jp/700/93bed0e5f2dd5a21cf73304fcfbc149d.woff2 +0 -0
  879. package/src/styles/fonts/noto-serif-jp/700/93f3ea6918533d96d9c252378b9a4af0.woff2 +0 -0
  880. package/src/styles/fonts/noto-serif-jp/700/95a0951d2a2722ff773a1a45e8c474d6.woff2 +0 -0
  881. package/src/styles/fonts/noto-serif-jp/700/982cd1765ca6bbb3e6547e47834d5148.woff2 +0 -0
  882. package/src/styles/fonts/noto-serif-jp/700/9b0c3caac458c7bc9c9133e40405ddea.woff2 +0 -0
  883. package/src/styles/fonts/noto-serif-jp/700/9f3bdab4025dc7c5ba1a5871e62e8918.woff2 +0 -0
  884. package/src/styles/fonts/noto-serif-jp/700/a00e94d4f04df6aa659db9c4954c7efe.woff2 +0 -0
  885. package/src/styles/fonts/noto-serif-jp/700/a2b0597837e382aca19919c4b74e58c8.woff2 +0 -0
  886. package/src/styles/fonts/noto-serif-jp/700/a373e85c96aa0dc303092429fb837e51.woff2 +0 -0
  887. package/src/styles/fonts/noto-serif-jp/700/a6e044424780a57610833cc856c15bf5.woff2 +0 -0
  888. package/src/styles/fonts/noto-serif-jp/700/a7c58070d68dc1724e9d4e781afb78db.woff2 +0 -0
  889. package/src/styles/fonts/noto-serif-jp/700/accc43762ad05edfbff66ba1380d192a.woff2 +0 -0
  890. package/src/styles/fonts/noto-serif-jp/700/ad47bcd6d4663026d648c132d969318d.woff2 +0 -0
  891. package/src/styles/fonts/noto-serif-jp/700/af57a2e8c72f9ba0efb1af771d32c124.woff2 +0 -0
  892. package/src/styles/fonts/noto-serif-jp/700/b0ae5f374e43dac992a4a5d334cebc0b.woff2 +0 -0
  893. package/src/styles/fonts/noto-serif-jp/700/b91234c10fd8b8c8abc88e03afe66a1f.woff2 +0 -0
  894. package/src/styles/fonts/noto-serif-jp/700/b9317198f118a1dfd8ddf2b82ec028f3.woff2 +0 -0
  895. package/src/styles/fonts/noto-serif-jp/700/ba825ae79b1a6f7e0cce5215fcb5c96f.woff2 +0 -0
  896. package/src/styles/fonts/noto-serif-jp/700/bc6183ac08f0fac78c46f80c10cf7c92.woff2 +0 -0
  897. package/src/styles/fonts/noto-serif-jp/700/bc8ccea5abf6598cf3cfa97eb59804bb.woff2 +0 -0
  898. package/src/styles/fonts/noto-serif-jp/700/bc95a792cbca6639214c9b0da13392ff.woff2 +0 -0
  899. package/src/styles/fonts/noto-serif-jp/700/bcfa62f35731856246c146d3a6932bf3.woff2 +0 -0
  900. package/src/styles/fonts/noto-serif-jp/700/c063897793f593eb26d6ff7b7baaba18.woff2 +0 -0
  901. package/src/styles/fonts/noto-serif-jp/700/c6c2971ad1f3221f6cf84028aa0f477e.woff2 +0 -0
  902. package/src/styles/fonts/noto-serif-jp/700/c97f41eef722121d86f55d553c056a39.woff2 +0 -0
  903. package/src/styles/fonts/noto-serif-jp/700/ca62704509932d3232d62918de97af3f.woff2 +0 -0
  904. package/src/styles/fonts/noto-serif-jp/700/d1f064825fa5784b5c930652bd831cce.woff2 +0 -0
  905. package/src/styles/fonts/noto-serif-jp/700/d2faabcedd19f016e7b21bce073c0ec8.woff2 +0 -0
  906. package/src/styles/fonts/noto-serif-jp/700/d320171e57480510f87dbbc7d5264b0a.woff2 +0 -0
  907. package/src/styles/fonts/noto-serif-jp/700/d3a72a99d365dddfbca8d017a8011368.woff2 +0 -0
  908. package/src/styles/fonts/noto-serif-jp/700/d42aafa0f246ad3b4c16fe96d3a3a432.woff2 +0 -0
  909. package/src/styles/fonts/noto-serif-jp/700/d4aaf23c13ae808a4bed617afd13aa07.woff2 +0 -0
  910. package/src/styles/fonts/noto-serif-jp/700/d6bb686bddfbe8f38a36a68e609a8667.woff2 +0 -0
  911. package/src/styles/fonts/noto-serif-jp/700/d9020ff69a83b2a6ee1f42ae480f7db0.woff2 +0 -0
  912. package/src/styles/fonts/noto-serif-jp/700/da90a9012ab2d98f759e3fa0820ef502.woff2 +0 -0
  913. package/src/styles/fonts/noto-serif-jp/700/df10d94bea357a43313e20da5d84bd30.woff2 +0 -0
  914. package/src/styles/fonts/noto-serif-jp/700/e34b2b141e472dc776c86fdf8eea23b0.woff2 +0 -0
  915. package/src/styles/fonts/noto-serif-jp/700/e444fd88f1390636e603d0d681538ac8.woff2 +0 -0
  916. package/src/styles/fonts/noto-serif-jp/700/e79898628283edc27180fc39d0d769c1.woff2 +0 -0
  917. package/src/styles/fonts/noto-serif-jp/700/eaf332445a40942928e5f5750c1c3116.woff2 +0 -0
  918. package/src/styles/fonts/noto-serif-jp/700/f91cd722855f4269256eae1187df64ec.woff2 +0 -0
  919. package/src/styles/fonts/noto-serif-jp/700/fc895f5ce66b656f4a933097bf2a8775.woff2 +0 -0
  920. package/src/styles/fonts/noto-serif-jp/700/fca6720fd14c467d29a90f18ef3859b9.woff2 +0 -0
  921. package/src/styles/fonts/noto-serif-jp/700/fd1bb507bcbf04856eeb7f7fd47ea579.woff2 +0 -0
  922. package/src/styles/fonts/noto-serif-jp/noto-serif-jp.css +3349 -0
  923. package/src/styles/fonts/noto-serif-kr/400/033466ef683afe931f7f520cfb42d928.woff2 +0 -0
  924. package/src/styles/fonts/noto-serif-kr/400/05da12edb9d52210581dc6ec4541031f.woff2 +0 -0
  925. package/src/styles/fonts/noto-serif-kr/400/0968e4861204b51f62a2f8e9f15dd5e0.woff2 +0 -0
  926. package/src/styles/fonts/noto-serif-kr/400/0d5dec931dc885f07fe5cd5af8bed675.woff2 +0 -0
  927. package/src/styles/fonts/noto-serif-kr/400/1076f0f6f66d28d7a2f16427faad4413.woff2 +0 -0
  928. package/src/styles/fonts/noto-serif-kr/400/12ef1ba76bd20b004b865266a1aa70b3.woff2 +0 -0
  929. package/src/styles/fonts/noto-serif-kr/400/152395634a207579552f8cb54db88599.woff2 +0 -0
  930. package/src/styles/fonts/noto-serif-kr/400/1a08931435f885e707edb85978ea3bb6.woff2 +0 -0
  931. package/src/styles/fonts/noto-serif-kr/400/31a197213ae61d7043c81013f52d10d6.woff2 +0 -0
  932. package/src/styles/fonts/noto-serif-kr/400/37dbd564820cce2f7b3331e442da0df3.woff2 +0 -0
  933. package/src/styles/fonts/noto-serif-kr/400/37dc8505b20b37a2477a9121b16d44a7.woff2 +0 -0
  934. package/src/styles/fonts/noto-serif-kr/400/3ac0a07878cacdc98bb889f40f5970d3.woff2 +0 -0
  935. package/src/styles/fonts/noto-serif-kr/400/491bbba374093fff045f55803a22bfef.woff2 +0 -0
  936. package/src/styles/fonts/noto-serif-kr/400/4d38e56738156625329d93bd5c8cc74a.woff2 +0 -0
  937. package/src/styles/fonts/noto-serif-kr/400/57e9a86651366c1ba299e47aa7e358c2.woff2 +0 -0
  938. package/src/styles/fonts/noto-serif-kr/400/57f89adeae55aa7fe2add3fc1801ce03.woff2 +0 -0
  939. package/src/styles/fonts/noto-serif-kr/400/5df9ce98c75f8c50fb4fff7f01a23925.woff2 +0 -0
  940. package/src/styles/fonts/noto-serif-kr/400/60020a830d809c26970cf8e48ba1eeda.woff2 +0 -0
  941. package/src/styles/fonts/noto-serif-kr/400/60bcfa3ea7446eb42394aab02d28be40.woff2 +0 -0
  942. package/src/styles/fonts/noto-serif-kr/400/63fafcb069520613d0ea35ad3e6b1e42.woff2 +0 -0
  943. package/src/styles/fonts/noto-serif-kr/400/65a60d87c64228d258a123cbe85f5f31.woff2 +0 -0
  944. package/src/styles/fonts/noto-serif-kr/400/6968e889e18891d912809fe484c2e745.woff2 +0 -0
  945. package/src/styles/fonts/noto-serif-kr/400/70714891ad3fbfc3d5f10a8669dacc5a.woff2 +0 -0
  946. package/src/styles/fonts/noto-serif-kr/400/761d84afca8d7f34eebefe538adba827.woff2 +0 -0
  947. package/src/styles/fonts/noto-serif-kr/400/76e07530323418ec723c5d7634c9bcca.woff2 +0 -0
  948. package/src/styles/fonts/noto-serif-kr/400/773819b7b9b8fd404a929867c0fd677e.woff2 +0 -0
  949. package/src/styles/fonts/noto-serif-kr/400/7817dd16805145d8538ad57590f69f5a.woff2 +0 -0
  950. package/src/styles/fonts/noto-serif-kr/400/7b59f0ec7792b18458dc5a361e37884c.woff2 +0 -0
  951. package/src/styles/fonts/noto-serif-kr/400/7c3945788a689a69356c1a622d69d48b.woff2 +0 -0
  952. package/src/styles/fonts/noto-serif-kr/400/835b6505bb9eea9678925a1fa885353d.woff2 +0 -0
  953. package/src/styles/fonts/noto-serif-kr/400/8b5c9ef81159f31d2ab35f45ca4373e0.woff2 +0 -0
  954. package/src/styles/fonts/noto-serif-kr/400/8b91c7c2ed390f1278b9befa3aa87233.woff2 +0 -0
  955. package/src/styles/fonts/noto-serif-kr/400/8fb45117a62d92dce44a80f0b729ead5.woff2 +0 -0
  956. package/src/styles/fonts/noto-serif-kr/400/90b7848e9b1623b77bdcf155e90b839c.woff2 +0 -0
  957. package/src/styles/fonts/noto-serif-kr/400/985d4d41afa0934a4eb2de35473fb9a2.woff2 +0 -0
  958. package/src/styles/fonts/noto-serif-kr/400/9bb38fd05201de666b88b82d56a386f7.woff2 +0 -0
  959. package/src/styles/fonts/noto-serif-kr/400/9eff2ab2a9d2ced47ab761bf6fdb11ec.woff2 +0 -0
  960. package/src/styles/fonts/noto-serif-kr/400/9f1963fe8d4d6878d717d872a8f3fdb5.woff2 +0 -0
  961. package/src/styles/fonts/noto-serif-kr/400/a0d00fe4816c95a8c7dffd445ef00b03.woff2 +0 -0
  962. package/src/styles/fonts/noto-serif-kr/400/a0f199077fa1a33bc2a1e01c64de7fe6.woff2 +0 -0
  963. package/src/styles/fonts/noto-serif-kr/400/a7c35e42a659347e490c3cb7983b2e7f.woff2 +0 -0
  964. package/src/styles/fonts/noto-serif-kr/400/aa662092cf249707123ca4d575e2764b.woff2 +0 -0
  965. package/src/styles/fonts/noto-serif-kr/400/aae1eda193285ab817a2d1b408440326.woff2 +0 -0
  966. package/src/styles/fonts/noto-serif-kr/400/b0cb664cb2e1371efda8943c0b7dcd1c.woff2 +0 -0
  967. package/src/styles/fonts/noto-serif-kr/400/b0d1cdced482352cf0d3ae58638aacb9.woff2 +0 -0
  968. package/src/styles/fonts/noto-serif-kr/400/b320f5d185b2cff933ac549c184031c5.woff2 +0 -0
  969. package/src/styles/fonts/noto-serif-kr/400/ba9c59d7dfa4494db1bb764ada81467d.woff2 +0 -0
  970. package/src/styles/fonts/noto-serif-kr/400/bab547459d514f46206e340c4bb2dc88.woff2 +0 -0
  971. package/src/styles/fonts/noto-serif-kr/400/bcc39eda837bb7a7a3d37c8c60fffb81.woff2 +0 -0
  972. package/src/styles/fonts/noto-serif-kr/400/bd73264d7f98776708d5d6f3c9b78fcc.woff2 +0 -0
  973. package/src/styles/fonts/noto-serif-kr/400/bed610b217d500f5975cfc9fe6157570.woff2 +0 -0
  974. package/src/styles/fonts/noto-serif-kr/400/bf8901f8f11d4f433ea17dabc9370ea6.woff2 +0 -0
  975. package/src/styles/fonts/noto-serif-kr/400/c4143bb9f2fe77b6ccf20088a8904650.woff2 +0 -0
  976. package/src/styles/fonts/noto-serif-kr/400/c7ff3f6bbdcd5f604b7343602ab904df.woff2 +0 -0
  977. package/src/styles/fonts/noto-serif-kr/400/c945c62368357d05a53206620460fb30.woff2 +0 -0
  978. package/src/styles/fonts/noto-serif-kr/400/ce022e18a1377ac509443c3c3790b431.woff2 +0 -0
  979. package/src/styles/fonts/noto-serif-kr/400/cf9cffe56636322f62b40d61130fbc5e.woff2 +0 -0
  980. package/src/styles/fonts/noto-serif-kr/400/d20d8944bc0b85f5b2aae4b24f343516.woff2 +0 -0
  981. package/src/styles/fonts/noto-serif-kr/400/d36644d6502527e1fff205d0c7eca434.woff2 +0 -0
  982. package/src/styles/fonts/noto-serif-kr/400/d40ab99c7a38026f411c8f112f742b48.woff2 +0 -0
  983. package/src/styles/fonts/noto-serif-kr/400/dc6e234ded795e91f76d6647f628fbf0.woff2 +0 -0
  984. package/src/styles/fonts/noto-serif-kr/400/dd0cfdfdac0866e66d587e2b5a9e9961.woff2 +0 -0
  985. package/src/styles/fonts/noto-serif-kr/400/de304c8a02e45ded4f8dcc479d167198.woff2 +0 -0
  986. package/src/styles/fonts/noto-serif-kr/400/e8c364c16daa04835bf32d293d2598db.woff2 +0 -0
  987. package/src/styles/fonts/noto-serif-kr/400/ec86e23683052da5cfdc3b77641bd15a.woff2 +0 -0
  988. package/src/styles/fonts/noto-serif-kr/400/eda1b0cb6d1719dd9bedcf3216a9e8de.woff2 +0 -0
  989. package/src/styles/fonts/noto-serif-kr/400/ee6fce20b420a480714607c66d7f97e5.woff2 +0 -0
  990. package/src/styles/fonts/noto-serif-kr/400/eee3836d6ac17ebb2c450bbcbc9db121.woff2 +0 -0
  991. package/src/styles/fonts/noto-serif-kr/400/f5738255e92d8dd34a46d1bcdf4c4074.woff2 +0 -0
  992. package/src/styles/fonts/noto-serif-kr/400/fa6e58ce4b52695e7ae19bbea6336ec8.woff2 +0 -0
  993. package/src/styles/fonts/noto-serif-kr/400/fd2069e6e8588c70b4f11364093b81f2.woff2 +0 -0
  994. package/src/styles/fonts/noto-serif-kr/700/04b8bde59cff68eeee74fb1914eb09c7.woff2 +0 -0
  995. package/src/styles/fonts/noto-serif-kr/700/05ac821472e235943ed1435e4bb8ecad.woff2 +0 -0
  996. package/src/styles/fonts/noto-serif-kr/700/074c1f8483d5a4d8c45c8c5f3e4cbb32.woff2 +0 -0
  997. package/src/styles/fonts/noto-serif-kr/700/1380210a722ac9aeef54005ad7b015c9.woff2 +0 -0
  998. package/src/styles/fonts/noto-serif-kr/700/17c7f5d0a45e92ede0e5dec3a2c77efa.woff2 +0 -0
  999. package/src/styles/fonts/noto-serif-kr/700/1be602ad456b0d75902d352116cb35fd.woff2 +0 -0
  1000. package/src/styles/fonts/noto-serif-kr/700/2612c60f9dc5459ac42763591240a1d6.woff2 +0 -0
  1001. package/src/styles/fonts/noto-serif-kr/700/2ab8cf1f23a5ac7939a7876054e711a7.woff2 +0 -0
  1002. package/src/styles/fonts/noto-serif-kr/700/34e6750e00c3a911ef87dc66b336594d.woff2 +0 -0
  1003. package/src/styles/fonts/noto-serif-kr/700/36f3df4730cfca4fc77fe52b52e6a126.woff2 +0 -0
  1004. package/src/styles/fonts/noto-serif-kr/700/37887deb7a9c466cf2af6ee7ff372ea6.woff2 +0 -0
  1005. package/src/styles/fonts/noto-serif-kr/700/3aba89c4d4281553078de4622b498cbb.woff2 +0 -0
  1006. package/src/styles/fonts/noto-serif-kr/700/3d88558097559d775231194d43745ba7.woff2 +0 -0
  1007. package/src/styles/fonts/noto-serif-kr/700/3dc3e8c35524cad2d1236f3393104ccf.woff2 +0 -0
  1008. package/src/styles/fonts/noto-serif-kr/700/45140742dfa8686b58db2899ce89f89a.woff2 +0 -0
  1009. package/src/styles/fonts/noto-serif-kr/700/469d2754e315e77270c12fa0ab28026c.woff2 +0 -0
  1010. package/src/styles/fonts/noto-serif-kr/700/4c0760284569b87d8f2290c324953973.woff2 +0 -0
  1011. package/src/styles/fonts/noto-serif-kr/700/4cba92c942694bdc479bc54101bb1aa4.woff2 +0 -0
  1012. package/src/styles/fonts/noto-serif-kr/700/4e7d263c30bb48604069a3b4404d4eeb.woff2 +0 -0
  1013. package/src/styles/fonts/noto-serif-kr/700/5279c7e465d9871edf51fd02e691eee7.woff2 +0 -0
  1014. package/src/styles/fonts/noto-serif-kr/700/54bfdb6f21f9d6191e5100580239e14f.woff2 +0 -0
  1015. package/src/styles/fonts/noto-serif-kr/700/56be5e32fd01668fe5ba814d94905839.woff2 +0 -0
  1016. package/src/styles/fonts/noto-serif-kr/700/615b38e0b5bb3ea68426f44f47706e6f.woff2 +0 -0
  1017. package/src/styles/fonts/noto-serif-kr/700/65e9c4585d71bf48a5c62f367010da16.woff2 +0 -0
  1018. package/src/styles/fonts/noto-serif-kr/700/6b75213eb0be40ce84241eb2bb438a2e.woff2 +0 -0
  1019. package/src/styles/fonts/noto-serif-kr/700/7520ca6ca8c60eb1e62d50e71c8d30f1.woff2 +0 -0
  1020. package/src/styles/fonts/noto-serif-kr/700/759a647b77791b1e98f99bc0ab5317a7.woff2 +0 -0
  1021. package/src/styles/fonts/noto-serif-kr/700/76e51630143b95b6322ef93ad330da7a.woff2 +0 -0
  1022. package/src/styles/fonts/noto-serif-kr/700/79aa7c8c842c4a27cf57c0a3a1ffca5a.woff2 +0 -0
  1023. package/src/styles/fonts/noto-serif-kr/700/7cc72fd2c9105560422b6a67c6162945.woff2 +0 -0
  1024. package/src/styles/fonts/noto-serif-kr/700/7d0ea0690e432462f4d05a23d720ec19.woff2 +0 -0
  1025. package/src/styles/fonts/noto-serif-kr/700/7dffa5c1bec57e0903fd62357401ff1a.woff2 +0 -0
  1026. package/src/styles/fonts/noto-serif-kr/700/82f6ccc063960eed1cdfd1d61ada8862.woff2 +0 -0
  1027. package/src/styles/fonts/noto-serif-kr/700/845b4b67564d62cf5cad242f7ac08ea5.woff2 +0 -0
  1028. package/src/styles/fonts/noto-serif-kr/700/84c8e7bc0931008ed91f44ed12dfea94.woff2 +0 -0
  1029. package/src/styles/fonts/noto-serif-kr/700/887a11290ba78b1e66c6d2f67043005e.woff2 +0 -0
  1030. package/src/styles/fonts/noto-serif-kr/700/88db1d042074fb6e66821ffc10941930.woff2 +0 -0
  1031. package/src/styles/fonts/noto-serif-kr/700/8dc18c0cebe6aa4bf4c45dbb831048ab.woff2 +0 -0
  1032. package/src/styles/fonts/noto-serif-kr/700/919a879bd2d580d8491a31a449390689.woff2 +0 -0
  1033. package/src/styles/fonts/noto-serif-kr/700/91da6cb174bebfb96976e2f1316be2fe.woff2 +0 -0
  1034. package/src/styles/fonts/noto-serif-kr/700/934dfdbed2bb2c4b6129199d699a34fa.woff2 +0 -0
  1035. package/src/styles/fonts/noto-serif-kr/700/97d7a17234f2b5030ae9697ae00aded2.woff2 +0 -0
  1036. package/src/styles/fonts/noto-serif-kr/700/995c4fda62d25c3b79dd5987737df6ae.woff2 +0 -0
  1037. package/src/styles/fonts/noto-serif-kr/700/9d0afa53dc2f92ba42024f55f11f6779.woff2 +0 -0
  1038. package/src/styles/fonts/noto-serif-kr/700/9e13a47242926f1cd7d68c08d9ef8889.woff2 +0 -0
  1039. package/src/styles/fonts/noto-serif-kr/700/a379ada89abd750c587ded29f77e731c.woff2 +0 -0
  1040. package/src/styles/fonts/noto-serif-kr/700/a48be56ce5d3a99dc8f8331398004412.woff2 +0 -0
  1041. package/src/styles/fonts/noto-serif-kr/700/a806759e72987951d6d08b7cbdf36a1b.woff2 +0 -0
  1042. package/src/styles/fonts/noto-serif-kr/700/a88481ec8bc7be11cb66e46781a79bb9.woff2 +0 -0
  1043. package/src/styles/fonts/noto-serif-kr/700/a9a016d4a93409f65278327e8a8bb38d.woff2 +0 -0
  1044. package/src/styles/fonts/noto-serif-kr/700/ab92d41062a7644faa45f50bf384454a.woff2 +0 -0
  1045. package/src/styles/fonts/noto-serif-kr/700/b326a8a9c33b554db570da94f60bc380.woff2 +0 -0
  1046. package/src/styles/fonts/noto-serif-kr/700/b6117ff2993b11bb1fdc7ea3588a010c.woff2 +0 -0
  1047. package/src/styles/fonts/noto-serif-kr/700/b61982951bd51b724143c30dfaaa9fe9.woff2 +0 -0
  1048. package/src/styles/fonts/noto-serif-kr/700/c0098958e20db68cab90097b5e62516f.woff2 +0 -0
  1049. package/src/styles/fonts/noto-serif-kr/700/c4bfaa5e50798246e3770718b7a7c84a.woff2 +0 -0
  1050. package/src/styles/fonts/noto-serif-kr/700/c84598999133455503042e06f4ab79cb.woff2 +0 -0
  1051. package/src/styles/fonts/noto-serif-kr/700/ca9a533988d7019597a60d4e17127e0c.woff2 +0 -0
  1052. package/src/styles/fonts/noto-serif-kr/700/cf2b28f90f47276f7e2688a65e88a101.woff2 +0 -0
  1053. package/src/styles/fonts/noto-serif-kr/700/d689b1861d7e4377dd72ad3013482612.woff2 +0 -0
  1054. package/src/styles/fonts/noto-serif-kr/700/d9047070d72a816b3dba9d40c2d85e69.woff2 +0 -0
  1055. package/src/styles/fonts/noto-serif-kr/700/da04549f3f4ed28076b01b8cd710d313.woff2 +0 -0
  1056. package/src/styles/fonts/noto-serif-kr/700/da7af303f8c645f9a9dbae0e6e32dd35.woff2 +0 -0
  1057. package/src/styles/fonts/noto-serif-kr/700/df9568257eb29b156449fdd4bec5ec76.woff2 +0 -0
  1058. package/src/styles/fonts/noto-serif-kr/700/e067cd0ed76c90cd0a93c9339253f20b.woff2 +0 -0
  1059. package/src/styles/fonts/noto-serif-kr/700/e08b07772e7bed3cec2832d43f7fd339.woff2 +0 -0
  1060. package/src/styles/fonts/noto-serif-kr/700/e12150d5a39b30be8f567968c7a527b0.woff2 +0 -0
  1061. package/src/styles/fonts/noto-serif-kr/700/e53fcb2381eee345db4f6f973dd95a3e.woff2 +0 -0
  1062. package/src/styles/fonts/noto-serif-kr/700/e5bd313ef81f687d398aacb11cec3069.woff2 +0 -0
  1063. package/src/styles/fonts/noto-serif-kr/700/eb5afb3d952b8593782caec6026514b6.woff2 +0 -0
  1064. package/src/styles/fonts/noto-serif-kr/700/edd6a4f608d04fc0351d7688cfc321e4.woff2 +0 -0
  1065. package/src/styles/fonts/noto-serif-kr/700/f9695c6c4df2bf6bc03045ff79d4f01f.woff2 +0 -0
  1066. package/src/styles/fonts/noto-serif-kr/700/fada6eaa68ff8816afe43d2a36c5423e.woff2 +0 -0
  1067. package/src/styles/fonts/noto-serif-kr/700/fbcc4bf5367218951172bdee6f77d7a6.woff2 +0 -0
  1068. package/src/styles/fonts/noto-serif-kr/noto-serif-kr.css +2601 -0
  1069. package/src/styles/site-media.css +823 -0
  1070. package/src/styles/tokens.css +292 -21
  1071. package/src/styles/ui.css +4217 -1869
  1072. package/src/types/bindings.ts +3 -1
  1073. package/src/types/config.ts +139 -9
  1074. package/src/types/constants.ts +46 -11
  1075. package/src/types/entities.ts +24 -8
  1076. package/src/types/operations.ts +80 -1
  1077. package/src/types/props.ts +12 -3
  1078. package/src/types/raw-assets.d.ts +39 -0
  1079. package/src/types/views.ts +28 -7
  1080. package/src/ui/__tests__/color-themes.test.ts +7 -40
  1081. package/src/ui/__tests__/font-themes.test.ts +30 -11
  1082. package/src/ui/color-themes.ts +311 -270
  1083. package/src/ui/compose/ComposeDialog.tsx +712 -394
  1084. package/src/ui/compose/ComposePrompt.tsx +15 -10
  1085. package/src/ui/dash/ActionButtons.tsx +27 -18
  1086. package/src/ui/dash/DangerZone.tsx +15 -10
  1087. package/src/ui/dash/FormatBadge.tsx +21 -8
  1088. package/src/ui/dash/StatusBadge.tsx +33 -22
  1089. package/src/ui/dash/appearance/AdvancedContent.tsx +44 -30
  1090. package/src/ui/dash/appearance/CodeInjectionContent.tsx +164 -0
  1091. package/src/ui/dash/appearance/ColorThemeContent.tsx +127 -160
  1092. package/src/ui/dash/appearance/FontThemeContent.tsx +20 -15
  1093. package/src/ui/dash/appearance/NavigationContent.tsx +368 -225
  1094. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +54 -0
  1095. package/src/ui/dash/settings/AccountContent.tsx +34 -23
  1096. package/src/ui/dash/settings/AccountMenuContent.tsx +226 -137
  1097. package/src/ui/dash/settings/ApiTokensContent.tsx +122 -79
  1098. package/src/ui/dash/settings/AvatarContent.tsx +73 -45
  1099. package/src/ui/dash/settings/DeleteAccountContent.tsx +106 -66
  1100. package/src/ui/dash/settings/GeneralContent.tsx +274 -157
  1101. package/src/ui/dash/settings/GitHubSyncContent.tsx +558 -0
  1102. package/src/ui/dash/settings/SessionsContent.tsx +79 -38
  1103. package/src/ui/dash/settings/SettingsRootContent.tsx +209 -109
  1104. package/src/ui/dash/settings/__tests__/AccountMenuContent.test.tsx +67 -0
  1105. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +75 -0
  1106. package/src/ui/feed/CuratedThreadPreview.tsx +24 -14
  1107. package/src/ui/feed/LinkCard.tsx +116 -84
  1108. package/src/ui/feed/LinkPreview.tsx +75 -0
  1109. package/src/ui/feed/NoteCard.tsx +60 -16
  1110. package/src/ui/feed/PostStatusBadges.tsx +15 -0
  1111. package/src/ui/feed/QuoteCard.tsx +10 -3
  1112. package/src/ui/feed/ThreadPreview.tsx +62 -36
  1113. package/src/ui/feed/TimelineFeed.tsx +2 -1
  1114. package/src/ui/feed/__tests__/thread-preview.test.ts +220 -11
  1115. package/src/ui/feed/__tests__/timeline-cards.test.ts +186 -5
  1116. package/src/ui/feed/thread-preview-state.ts +23 -10
  1117. package/src/ui/font-themes.ts +57 -97
  1118. package/src/ui/layouts/BaseLayout.tsx +221 -136
  1119. package/src/ui/layouts/SiteLayout.tsx +431 -143
  1120. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +76 -6
  1121. package/src/ui/layouts/__tests__/SiteLayout.test.tsx +76 -0
  1122. package/src/ui/pages/ArchivePage.tsx +320 -207
  1123. package/src/ui/pages/BrandPage.tsx +499 -341
  1124. package/src/ui/pages/CollectionEditorPage.tsx +50 -30
  1125. package/src/ui/pages/CollectionPage.tsx +201 -61
  1126. package/src/ui/pages/CollectionsPage.tsx +27 -18
  1127. package/src/ui/pages/ComposePage.tsx +15 -10
  1128. package/src/ui/pages/FeaturedPage.tsx +16 -11
  1129. package/src/ui/pages/HomePage.tsx +15 -10
  1130. package/src/ui/pages/PostPage.tsx +5 -1
  1131. package/src/ui/pages/SearchPage.tsx +93 -43
  1132. package/src/ui/pages/ThemeSamplePage.tsx +758 -520
  1133. package/src/ui/pages/__tests__/ArchivePage.test.tsx +72 -0
  1134. package/src/ui/shared/CollectionDirectory.tsx +232 -37
  1135. package/src/ui/shared/CollectionsManager.tsx +155 -75
  1136. package/src/ui/shared/DecorativeQuoteMark.tsx +12 -9
  1137. package/src/ui/shared/MediaGallery.tsx +350 -256
  1138. package/src/ui/shared/Pagination.tsx +36 -25
  1139. package/src/ui/shared/PostFooter.tsx +139 -96
  1140. package/src/ui/shared/__tests__/media-gallery.test.ts +27 -0
  1141. package/src/ui/shared/__tests__/navigation-labels.test.ts +79 -28
  1142. package/src/ui/shared/__tests__/post-footer.test.ts +42 -17
  1143. package/src/ui/shared/collection-management-labels.ts +109 -33
  1144. package/src/ui/shared/navigation-labels.ts +78 -8
  1145. package/bin/lib/site-localize-media.js +0 -427
  1146. package/dist/client/_assets/client-auth.js +0 -3251
  1147. package/dist/client/_assets/client.css +0 -2
  1148. package/dist/client/_assets/client.js +0 -380
  1149. package/src/__tests__/site-localize-media.test.ts +0 -150
  1150. package/src/client/components/jant-collection-sidebar.ts +0 -803
  1151. package/src/i18n/locales/en.ts +0 -1
  1152. package/src/i18n/locales/zh-Hans.po +0 -3183
  1153. package/src/i18n/locales/zh-Hans.ts +0 -1
  1154. package/src/i18n/locales/zh-Hant.po +0 -3183
  1155. package/src/i18n/locales/zh-Hant.ts +0 -1
  1156. package/src/routes/feed/rss.ts +0 -216
  1157. package/src/types/lingui-react-macro.d.ts +0 -34
  1158. /package/dist/client/_assets/chunks/{heic-to-XcUDQvtx.js → heic-to-DUUaO23q.js} +0 -0
  1159. /package/dist/client/_assets/chunks/{module-ChVQstFd.js → module-DcsAZQZ_.js} +0 -0
  1160. /package/dist/client/_assets/chunks/{native-CR5HLOyf.js → native-DpcrFAPh.js} +0 -0
  1161. /package/dist/client/_assets/{client-cjk.css → client-cjk-B7Z0snDu.css} +0 -0
  1162. /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,86 @@ 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";
22
31
  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
32
  import { render as renderMarkdown } from "../lib/markdown.js";
26
- import { toISOString } from "../lib/time.js";
33
+ import { formatRelativeAge, toISOString } from "../lib/time.js";
34
+ import {
35
+ formatFrontMatter,
36
+ type HugoCollectionRef,
37
+ type HugoFrontMatter,
38
+ type JantMedia,
39
+ } from "../lib/hugo-markdown.js";
40
+ // Shared design tokens — single source of truth for colors, typography,
41
+ // and layout variables. Consumed verbatim by both the main site (via
42
+ // Tailwind) and the Hugo export (written to static/tokens.css). Using
43
+ // ?raw inlines the file contents as a string at build time so the
44
+ // Worker bundle ships without any filesystem access.
45
+ import TOKENS_CSS from "../styles/tokens.css?raw";
46
+
47
+ // Placeholder Hugo theme files — real templates and styles land in Commit 5.
48
+ // We import them as Vite `?raw` strings so the Worker bundle has no runtime
49
+ // filesystem dependency.
50
+ import THEME_TOML from "./export-theme/theme.toml?raw";
51
+ import THEME_STYLE_MAIN_CSS from "./export-theme/styles/main.css?raw";
52
+ // Static-site client bundle — Lit-based lightbox, feed video autoplay, audio
53
+ // waveform, and gallery scroll hints. Built by `vite.config.site.ts` into
54
+ // `export-theme/assets/client-site.{js,css}` and shipped under the theme's
55
+ // reserved `_jant/` static directory so the theme stays version-aligned with
56
+ // `@jant/core`. The assets folder is regenerated on every build, so the
57
+ // checked-in files are source-of-truth for the most recent release.
58
+ import CLIENT_SITE_JS from "./export-theme/assets/client-site.js?raw";
59
+ import CLIENT_SITE_CSS from "./export-theme/assets/client-site.css?raw";
60
+ import LAYOUT_BASEOF from "./export-theme/layouts/_default/baseof.html?raw";
61
+ import LAYOUT_SINGLE from "./export-theme/layouts/_default/single.html?raw";
62
+ import LAYOUT_LIST from "./export-theme/layouts/_default/list.html?raw";
63
+ import LAYOUT_ALIAS from "./export-theme/layouts/_default/alias.html?raw";
64
+ import LAYOUT_INDEX from "./export-theme/layouts/index.html?raw";
65
+ import LAYOUT_POST_LIST from "./export-theme/layouts/post/list.html?raw";
66
+ import LAYOUT_FEATURED_LIST from "./export-theme/layouts/featured/list.html?raw";
67
+ import LAYOUT_ARCHIVE_LIST from "./export-theme/layouts/archive/list.html?raw";
68
+ import LAYOUT_COLLECTIONS_LIST from "./export-theme/layouts/collections/list.html?raw";
69
+ import LAYOUT_COLLECTION_SINGLE from "./export-theme/layouts/collection/single.html?raw";
70
+ import PARTIAL_HEAD from "./export-theme/layouts/partials/head.html?raw";
71
+ import PARTIAL_HEADER from "./export-theme/layouts/partials/header.html?raw";
72
+ import PARTIAL_FOOTER from "./export-theme/layouts/partials/footer.html?raw";
73
+ import PARTIAL_PAGINATION from "./export-theme/layouts/partials/pagination.html?raw";
74
+ import PARTIAL_POST_CARD from "./export-theme/layouts/partials/post-card.html?raw";
75
+ import PARTIAL_MEDIA_GALLERY from "./export-theme/layouts/partials/media-gallery.html?raw";
76
+ import PARTIAL_REPLY from "./export-theme/layouts/partials/reply.html?raw";
77
+ import PARTIAL_THREAD_PREVIEW from "./export-theme/layouts/partials/thread-preview.html?raw";
78
+ import LAYOUT_RSS from "./export-theme/layouts/_default/rss.xml?raw";
79
+ import PARTIAL_FEED_POST_CONTENT from "./export-theme/layouts/partials/feed-post-content.xml?raw";
80
+
27
81
  import type { StorageDriver } from "../lib/storage.js";
28
82
  import { base64ToUint8Array } from "../lib/favicon.js";
29
- import type {
30
- Post,
31
- Collection,
32
- Media,
33
- NavItem,
34
- TextAttachmentContent,
35
- } from "../types.js";
83
+ import type { Post, Collection, Media, NavItem } from "../types.js";
84
+
85
+ /** A file entry in the exported Hugo site. */
86
+ export interface ExportFile {
87
+ path: string;
88
+ content: string | Uint8Array;
89
+ }
36
90
 
37
91
  export interface ExportService {
38
- generateZolaSite(): Promise<Uint8Array>;
92
+ /** Generate a flat list of files for a complete Hugo site. */
93
+ generateHugoFiles(): Promise<ExportFile[]>;
94
+ /** Generate a ZIP archive of the Hugo site. */
95
+ generateHugoSite(): Promise<Uint8Array>;
39
96
  }
40
97
 
41
- interface SiteConfig {
98
+ export interface SiteConfig {
42
99
  siteName: string;
43
100
  siteUrl: string;
44
101
  siteDescription: string;
45
102
  siteLanguage: string;
46
103
  showJantBrandingOnHome: boolean;
47
104
  homeDefaultView: string;
48
- headerNavMaxVisible: number;
105
+ /** "latest" or "featured" — drives the default RSS nav link in the exported site. */
106
+ mainRssFeed: string;
49
107
  siteFooter: string;
50
108
  showHeaderAvatar: boolean;
51
109
  siteAvatarUrl: string;
@@ -66,31 +124,58 @@ interface SiteConfig {
66
124
  sitePathPrefix?: string;
67
125
  navItems: Pick<
68
126
  NavItem,
69
- "type" | "systemKey" | "label" | "url" | "position"
127
+ "type" | "systemKey" | "label" | "url" | "position" | "placement"
70
128
  >[];
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;
129
+ /** Items per page for Hugo pagination — kept in sync with the main site's PAGE_SIZE. */
130
+ pageSize: number;
131
+ /** Items per archive page — kept in sync with the main site's ARCHIVE_PAGE_SIZE. */
132
+ archivePageSize: number;
133
+ /** Max items per Atom feed — kept in sync with the main site's rssFeedLimit. */
134
+ rssFeedLimit: number;
90
135
  }
91
136
 
92
137
  type IconExportMode = "default" | "custom";
93
138
 
139
+ type ExportedCollectionDirectoryItem =
140
+ | {
141
+ type: "collection";
142
+ sequence: string;
143
+ slug: string;
144
+ title: string;
145
+ /** Rendered HTML of the collection description, or null if empty. */
146
+ descriptionHtml?: string | null;
147
+ entryCount?: number;
148
+ recentActivityLabel?: string | null;
149
+ recentActivityIso?: string | null;
150
+ }
151
+ | {
152
+ type: "divider";
153
+ label: string | null;
154
+ }
155
+ | {
156
+ type: "link";
157
+ sequence: string;
158
+ label: string;
159
+ url: string;
160
+ /** Rendered HTML of the link description, or null if empty. */
161
+ descriptionHtml?: string | null;
162
+ };
163
+
164
+ interface ExportCollectionDirectorySourceItem {
165
+ type: "collection" | "divider" | "link";
166
+ label?: string | null;
167
+ url?: string | null;
168
+ description?: string | null;
169
+ collection?: {
170
+ id: string;
171
+ slug: string;
172
+ title: string;
173
+ description?: string | null;
174
+ postCount?: number;
175
+ recentActivityAt?: number;
176
+ };
177
+ }
178
+
94
179
  interface SiteIconAssets {
95
180
  faviconBytes: Uint8Array;
96
181
  faviconMode: IconExportMode;
@@ -98,6 +183,41 @@ interface SiteIconAssets {
98
183
  appleTouchMode: IconExportMode;
99
184
  }
100
185
 
186
+ interface ExportedCollectionMetrics {
187
+ postCount: number;
188
+ recentActivityAt: number;
189
+ }
190
+
191
+ /**
192
+ * A single `collections:` front-matter entry, already resolved to its
193
+ * Hugo-visible slug. Assembled from
194
+ * `collectionService.getCollectionEntriesByPostIds` + `collectionSlugMap`.
195
+ */
196
+ interface ExportedCollectionEntry {
197
+ slug: string;
198
+ /**
199
+ * Denormalized collection title. Kept alongside the slug so the exported
200
+ * front matter can render a tag label without templates having to resolve
201
+ * another page. Optional because legacy call sites may not supply it.
202
+ */
203
+ title?: string;
204
+ /** Unix seconds. */
205
+ collectedAt: number;
206
+ position: number;
207
+ /** Unix seconds, or null when not pinned in this collection. */
208
+ pinnedAt: number | null;
209
+ }
210
+
211
+ function buildDefaultAppleTouchAsset(): Pick<
212
+ SiteIconAssets,
213
+ "appleTouchBytes" | "appleTouchMode"
214
+ > {
215
+ return {
216
+ appleTouchBytes: getDefaultJantAppleTouchIconBytes(),
217
+ appleTouchMode: "default",
218
+ };
219
+ }
220
+
101
221
  export function createExportService(
102
222
  services: {
103
223
  posts: PostService;
@@ -109,15 +229,22 @@ export function createExportService(
109
229
  deps: { storage?: StorageDriver | null } = {},
110
230
  ): ExportService {
111
231
  return {
112
- async generateZolaSite() {
232
+ async generateHugoFiles() {
233
+ const collectionDirectoryDataPromise =
234
+ typeof services.collections.listDirectoryData === "function"
235
+ ? services.collections.listDirectoryData()
236
+ : Promise.resolve(null);
237
+
113
238
  // 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
- ]);
239
+ const [allPosts, allCollections, collectionDirectoryData] =
240
+ await Promise.all([
241
+ services.posts.list({
242
+ excludeReplies: false,
243
+ limit: 10000,
244
+ }),
245
+ services.collections.list(),
246
+ collectionDirectoryDataPromise,
247
+ ]);
121
248
 
122
249
  const allPostIds = allPosts.map((p) => p.id);
123
250
  const roots = allPosts.filter((p) => p.replyToId === null);
@@ -126,23 +253,45 @@ export function createExportService(
126
253
 
127
254
  const [
128
255
  collectionsByPost,
256
+ collectionEntriesByPost,
129
257
  rawMediaByPost,
130
258
  slugMap,
131
259
  aliasMap,
132
260
  collectionSlugMap,
133
261
  ] = await Promise.all([
134
262
  services.collections.getCollectionsByPostIds(allPostIds),
263
+ services.collections.getCollectionEntriesByPostIds(allPostIds),
135
264
  services.media.getByPostIds(allPostIds),
136
265
  services.paths.getPostSlugMap(allPostIds),
137
266
  services.paths.getPostAliases(rootPostIds),
138
267
  services.paths.getCollectionSlugMap(allCollections.map((c) => c.id)),
139
268
  ]);
140
- const textAttachmentContents = await buildTextAttachmentContentMap(
141
- rawMediaByPost,
142
- services.media,
143
- deps.storage,
144
- );
269
+ // Denormalized title lookup so front-matter collection refs can
270
+ // include a title label without templates having to resolve another
271
+ // page. Source of truth is still `slug` on round-trip; `title` is
272
+ // refreshed from DB on every export.
273
+ const collectionTitleMap = new Map<string, string>();
274
+ for (const collection of allCollections) {
275
+ collectionTitleMap.set(collection.id, collection.title);
276
+ }
277
+
145
278
  const iconAssets = await buildSiteIconAssets(siteConfig, deps.storage);
279
+ const collectionMetrics = buildExportedCollectionMetrics(
280
+ allCollections,
281
+ allPosts,
282
+ collectionsByPost,
283
+ );
284
+ const exportedCollectionDirectoryItems =
285
+ buildExportedCollectionDirectoryItems(
286
+ collectionDirectoryData?.items ??
287
+ allCollections.map((collection) => ({
288
+ id: collection.id,
289
+ type: "collection" as const,
290
+ collection,
291
+ })),
292
+ collectionSlugMap,
293
+ collectionMetrics,
294
+ );
146
295
 
147
296
  // 2. Group replies by threadId
148
297
  const repliesByThread = new Map<string, Post[]>();
@@ -156,90 +305,236 @@ export function createExportService(
156
305
  list.sort((a, b) => a.createdAt - b.createdAt);
157
306
  }
158
307
 
159
- // 3. Build ZIP file structure
160
- const { zipSync } = await import("fflate");
161
- const files: Record<string, Uint8Array> = {};
308
+ // 3. Build file list
309
+ const exportFiles: ExportFile[] = [];
162
310
 
163
- // Generate post files
311
+ // Generate thread bundles (root _index.md + per-reply index.md).
164
312
  for (const root of roots) {
165
313
  const slug = slugMap.get(root.id) ?? root.slug;
166
314
  const threadReplies = repliesByThread.get(root.id) ?? [];
167
- const postCollections = collectionsByPost.get(root.id) ?? [];
168
315
  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
316
 
179
- const markdown = buildPostMarkdown(
317
+ const rootCollectionEntries = buildExportedCollectionEntriesForPost(
318
+ root.id,
319
+ collectionEntriesByPost,
320
+ collectionSlugMap,
321
+ collectionTitleMap,
322
+ );
323
+
324
+ const bundleFiles = await buildThreadBundle(
180
325
  root,
181
326
  threadReplies,
182
- postCollections,
183
- { rootAliases, zolaAliases },
327
+ slug,
328
+ rootAliases,
329
+ rootCollectionEntries,
184
330
  slugMap,
331
+ collectionEntriesByPost,
185
332
  collectionSlugMap,
186
- rootMedia,
333
+ collectionTitleMap,
187
334
  rawMediaByPost,
188
335
  siteConfig,
189
- textAttachmentContents,
336
+ deps.storage ?? null,
190
337
  );
191
-
192
- files[`content/${slug}/index.md`] = new TextEncoder().encode(markdown);
338
+ exportFiles.push(...bundleFiles);
193
339
  }
194
340
 
341
+ // Collection landing pages (`content/{slug}/_index.md`).
195
342
  for (const collection of allCollections) {
196
343
  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
- );
344
+ const entryCount = collectionMetrics.get(collection.id)?.postCount ?? 0;
345
+ exportFiles.push({
346
+ path: `content/${slug}/_index.md`,
347
+ content: await buildCollectionSection(collection, slug, entryCount),
348
+ });
201
349
  }
202
350
 
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
- );
351
+ // Section + home scaffolding.
352
+ exportFiles.push({
353
+ path: "hugo.toml",
354
+ content: buildHugoToml(siteConfig),
355
+ });
356
+ exportFiles.push({
357
+ path: "content/_index.md",
358
+ content: await buildHomeSection(siteConfig),
359
+ });
360
+ exportFiles.push({
361
+ path: "content/collections/_index.md",
362
+ content: await buildCollectionsSection(),
363
+ });
364
+ exportFiles.push({
365
+ path: "content/archive/_index.md",
366
+ content: await buildArchiveSection(),
367
+ });
368
+
369
+ const usedSlugs = new Set<string>();
370
+ for (const s of slugMap.values()) usedSlugs.add(s);
371
+ for (const s of collectionSlugMap.values()) usedSlugs.add(s);
372
+ if (!usedSlugs.has("featured")) {
373
+ exportFiles.push({
374
+ path: "content/featured/_index.md",
375
+ content: await buildFeaturedSection(),
376
+ });
377
+ }
378
+
379
+ // Single data file consumed by templates via `hugo.Data.jant`. The
380
+ // collection directory lives on the same object as `directory` so
381
+ // everything Jant owns round-trips in one place.
382
+ exportFiles.push({
383
+ path: "data/jant.toml",
384
+ content: buildJantDataToml(
385
+ siteConfig,
386
+ iconAssets,
387
+ exportedCollectionDirectoryItems,
388
+ ),
389
+ });
390
+
391
+ // Theme scaffolding (real templates + styles land in Commit 5).
392
+ exportFiles.push({
393
+ path: "themes/jant/theme.toml",
394
+ content: THEME_TOML,
395
+ });
396
+ exportFiles.push({
397
+ path: "themes/jant/layouts/_default/baseof.html",
398
+ content: LAYOUT_BASEOF,
399
+ });
400
+ exportFiles.push({
401
+ path: "themes/jant/layouts/_default/single.html",
402
+ content: LAYOUT_SINGLE,
403
+ });
404
+ exportFiles.push({
405
+ path: "themes/jant/layouts/_default/list.html",
406
+ content: LAYOUT_LIST,
407
+ });
408
+ exportFiles.push({
409
+ path: "themes/jant/layouts/_default/alias.html",
410
+ content: LAYOUT_ALIAS,
411
+ });
412
+ exportFiles.push({
413
+ path: "themes/jant/layouts/index.html",
414
+ content: LAYOUT_INDEX,
415
+ });
416
+ exportFiles.push({
417
+ path: "themes/jant/layouts/post/list.html",
418
+ content: LAYOUT_POST_LIST,
419
+ });
420
+ exportFiles.push({
421
+ path: "themes/jant/layouts/featured/list.html",
422
+ content: LAYOUT_FEATURED_LIST,
423
+ });
424
+ exportFiles.push({
425
+ path: "themes/jant/layouts/archive/list.html",
426
+ content: LAYOUT_ARCHIVE_LIST,
427
+ });
428
+ exportFiles.push({
429
+ path: "themes/jant/layouts/collections/list.html",
430
+ content: LAYOUT_COLLECTIONS_LIST,
431
+ });
432
+ exportFiles.push({
433
+ path: "themes/jant/layouts/collection/single.html",
434
+ content: LAYOUT_COLLECTION_SINGLE,
435
+ });
436
+ exportFiles.push({
437
+ path: "themes/jant/layouts/partials/head.html",
438
+ content: PARTIAL_HEAD,
439
+ });
440
+ exportFiles.push({
441
+ path: "themes/jant/layouts/partials/header.html",
442
+ content: PARTIAL_HEADER,
443
+ });
444
+ exportFiles.push({
445
+ path: "themes/jant/layouts/partials/footer.html",
446
+ content: PARTIAL_FOOTER,
447
+ });
448
+ exportFiles.push({
449
+ path: "themes/jant/layouts/partials/pagination.html",
450
+ content: PARTIAL_PAGINATION,
451
+ });
452
+ exportFiles.push({
453
+ path: "themes/jant/layouts/partials/post-card.html",
454
+ content: PARTIAL_POST_CARD,
455
+ });
456
+ exportFiles.push({
457
+ path: "themes/jant/layouts/partials/media-gallery.html",
458
+ content: PARTIAL_MEDIA_GALLERY,
459
+ });
460
+ exportFiles.push({
461
+ path: "themes/jant/layouts/partials/reply.html",
462
+ content: PARTIAL_REPLY,
463
+ });
464
+ exportFiles.push({
465
+ path: "themes/jant/layouts/partials/thread-preview.html",
466
+ content: PARTIAL_THREAD_PREVIEW,
467
+ });
468
+ exportFiles.push({
469
+ path: "themes/jant/layouts/_default/rss.xml",
470
+ content: LAYOUT_RSS,
471
+ });
472
+ exportFiles.push({
473
+ path: "themes/jant/layouts/partials/feed-post-content.xml",
474
+ content: PARTIAL_FEED_POST_CONTENT,
475
+ });
476
+
477
+ // Static assets. Load order in the template's <head> is
478
+ // tokens → main → theme → custom (wired up by the Commit 5 partial).
479
+ exportFiles.push({
480
+ path: "themes/jant/static/tokens.css",
481
+ content: TOKENS_CSS,
482
+ });
483
+ exportFiles.push({
484
+ path: "themes/jant/static/main.css",
485
+ content: THEME_STYLE_MAIN_CSS,
486
+ });
487
+ exportFiles.push({
488
+ path: "themes/jant/static/theme.css",
489
+ content: siteConfig.themeCss ?? "",
490
+ });
491
+ exportFiles.push({
492
+ path: "themes/jant/static/custom.css",
493
+ content: siteConfig.customCss ?? "",
494
+ });
495
+ // Client-side interactions: media lightbox, feed video autoplay,
496
+ // audio waveform, gallery scroll hints. Reserved namespace keeps these
497
+ // from colliding with user-authored static files.
498
+ exportFiles.push({
499
+ path: "themes/jant/static/_jant/client-site.js",
500
+ content: CLIENT_SITE_JS,
501
+ });
502
+ exportFiles.push({
503
+ path: "themes/jant/static/_jant/client-site.css",
504
+ content: CLIENT_SITE_CSS,
505
+ });
506
+ exportFiles.push({
507
+ path: "themes/jant/static/favicon.ico",
508
+ content: iconAssets.faviconBytes,
509
+ });
510
+ exportFiles.push({
511
+ path: "themes/jant/static/apple-touch-icon.png",
512
+ content: iconAssets.appleTouchBytes,
513
+ });
514
+
515
+ exportFiles.push({
516
+ path: "README.md",
517
+ content: buildReadme(siteConfig.siteName),
518
+ });
519
+ exportFiles.push({
520
+ path: ".gitignore",
521
+ content: buildGitignore(),
522
+ });
523
+
524
+ return exportFiles;
525
+ },
242
526
 
527
+ async generateHugoSite() {
528
+ const exportFiles = await this.generateHugoFiles();
529
+ const { zipSync } = await import("fflate");
530
+ const encoder = new TextEncoder();
531
+ const files: Record<string, Uint8Array> = {};
532
+ for (const file of exportFiles) {
533
+ files[file.path] =
534
+ typeof file.content === "string"
535
+ ? encoder.encode(file.content)
536
+ : file.content;
537
+ }
243
538
  return zipSync(files);
244
539
  },
245
540
  };
@@ -272,25 +567,38 @@ async function buildSiteIconAssets(
272
567
  return {
273
568
  faviconBytes,
274
569
  faviconMode,
275
- appleTouchBytes: getDefaultJantAppleTouchIconBytes(),
276
- appleTouchMode: "default",
570
+ ...buildDefaultAppleTouchAsset(),
277
571
  };
278
572
  }
279
573
 
280
574
  if (!storage) {
281
- throw new Error(
282
- "Custom apple-touch icon is configured but no storage driver is available for export",
575
+ return {
576
+ faviconBytes,
577
+ faviconMode,
578
+ ...buildDefaultAppleTouchAsset(),
579
+ };
580
+ }
581
+
582
+ let appleTouchBytes: Uint8Array | null;
583
+ try {
584
+ appleTouchBytes = await readStorageObjectBytes(
585
+ storage,
586
+ config.appleTouchIconStorageKey,
283
587
  );
588
+ } catch {
589
+ return {
590
+ faviconBytes,
591
+ faviconMode,
592
+ ...buildDefaultAppleTouchAsset(),
593
+ };
284
594
  }
285
595
 
286
- const appleTouchBytes = await readStorageObjectBytes(
287
- storage,
288
- config.appleTouchIconStorageKey,
289
- );
290
596
  if (!appleTouchBytes) {
291
- throw new Error(
292
- `Custom apple-touch icon "${config.appleTouchIconStorageKey}" is unavailable for export`,
293
- );
597
+ return {
598
+ faviconBytes,
599
+ faviconMode,
600
+ ...buildDefaultAppleTouchAsset(),
601
+ };
294
602
  }
295
603
 
296
604
  return {
@@ -302,563 +610,1033 @@ async function buildSiteIconAssets(
302
610
  }
303
611
 
304
612
  // ---------------------------------------------------------------------------
305
- // Markdown generation
613
+ // Thread bundle generation
306
614
  // ---------------------------------------------------------------------------
307
615
 
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");
616
+ function buildExportedCollectionEntriesForPost(
617
+ postId: string,
618
+ collectionEntriesByPost: Map<
619
+ string,
620
+ {
621
+ collectionId: string;
622
+ createdAt: number;
623
+ position: number;
624
+ pinnedAt: number | null;
625
+ }[]
626
+ >,
627
+ collectionSlugMap: Map<string, string>,
628
+ collectionTitleMap: Map<string, string>,
629
+ ): ExportedCollectionEntry[] {
630
+ const entries = collectionEntriesByPost.get(postId) ?? [];
631
+ const resolved: ExportedCollectionEntry[] = [];
632
+ for (const entry of entries) {
633
+ const slug = collectionSlugMap.get(entry.collectionId);
634
+ if (!slug) continue;
635
+ const title = collectionTitleMap.get(entry.collectionId);
636
+ resolved.push({
637
+ slug,
638
+ title,
639
+ collectedAt: entry.createdAt,
640
+ position: entry.position,
641
+ pinnedAt: entry.pinnedAt,
642
+ });
643
+ }
644
+ return resolved;
645
+ }
646
+
647
+ function collectionEntriesToRefs(
648
+ entries: readonly ExportedCollectionEntry[],
649
+ ): HugoCollectionRef[] {
650
+ return entries.map((entry) => ({
651
+ slug: entry.slug,
652
+ title: entry.title,
653
+ collected_at: toISOString(entry.collectedAt),
654
+ position: entry.position,
655
+ pinned_at: entry.pinnedAt !== null ? toISOString(entry.pinnedAt) : null,
656
+ }));
657
+ }
658
+
659
+ interface MediaEmission {
660
+ /** Flat `media:` front-matter entry. */
661
+ entry: JantMedia;
662
+ /**
663
+ * Site-relative path under `static/` where the primary bytes should
664
+ * land, or null when the media links to a remote public URL and no
665
+ * bytes need to be emitted.
666
+ */
667
+ inlinePath: string | null;
668
+ /**
669
+ * Site-relative path under `static/` where the poster bytes should
670
+ * land, or null when there's no poster or the poster is linked via
671
+ * a public URL.
672
+ */
673
+ inlinePosterPath: string | null;
674
+ }
675
+
676
+ /**
677
+ * Build a flat `media:` entry for a Media record plus a decision about
678
+ * whether the primary bytes (and poster) should be bundled into the
679
+ * export's `static/media/` directory.
680
+ *
681
+ * When the media's provider has a reachable public URL (R2/S3/local
682
+ * proxy configured with a `*_public_url`), `src` points at that absolute
683
+ * URL and no bytes are emitted — the exported site stays small and the
684
+ * media keeps being served from wherever it already lives. Without a
685
+ * public URL the bytes are written to `static/media/{id}.ext` and `src`
686
+ * is the site-relative path.
687
+ */
688
+ function buildMediaEmission(
689
+ media: Media,
690
+ siteConfig: SiteConfig,
691
+ ): MediaEmission {
692
+ const publicUrl = getPublicUrlForProvider(
693
+ media.provider,
694
+ siteConfig.r2PublicUrl,
695
+ siteConfig.s3PublicUrl,
696
+ siteConfig.localPublicUrl,
697
+ );
698
+ const hasPublic = Boolean(publicUrl);
699
+
700
+ const ext = extOfFilename(media.filename);
701
+ const localName = `${media.id}${ext}`;
702
+ const localPath = `/media/${localName}`;
703
+ const src = hasPublic ? getMediaUrl(media.storageKey, publicUrl) : localPath;
704
+
705
+ const entry: JantMedia = {
706
+ id: media.id,
707
+ kind: media.mediaKind,
708
+ src,
709
+ position: parsePositionForSort(media.position),
710
+ };
711
+ if (media.alt !== null && media.alt !== "") entry.alt = media.alt;
712
+ if (media.width !== null) entry.width = media.width;
713
+ if (media.height !== null) entry.height = media.height;
714
+ if (media.blurhash !== null && media.blurhash !== "")
715
+ entry.blurhash = media.blurhash;
716
+ if (media.originalName) entry.original_name = media.originalName;
717
+ if (media.mimeType) entry.mime_type = media.mimeType;
718
+ if (typeof media.size === "number") entry.size = media.size;
719
+ if (media.waveform) entry.waveform = media.waveform;
720
+ if (media.summary) entry.summary = media.summary;
721
+ if (typeof media.chars === "number") entry.chars = media.chars;
722
+ if (media.durationSeconds !== null && media.durationSeconds !== undefined) {
723
+ entry.duration_seconds = media.durationSeconds;
724
+ }
725
+ entry.provider = media.provider;
726
+ entry.storage_key = media.storageKey;
727
+
728
+ let inlinePosterPath: string | null = null;
729
+ if (media.posterKey) {
730
+ const posterExt = extOfStorageKey(media.posterKey);
731
+ const posterLocalName = `${media.id}-poster.${posterExt}`;
732
+ entry.poster = hasPublic
733
+ ? getMediaUrl(media.posterKey, publicUrl)
734
+ : `/media/${posterLocalName}`;
735
+ entry.poster_key = media.posterKey;
736
+ if (!hasPublic) {
737
+ inlinePosterPath = `static/media/${posterLocalName}`;
738
+ }
739
+ }
740
+
741
+ return {
742
+ entry,
743
+ inlinePath: hasPublic ? null : `static/media/${localName}`,
744
+ inlinePosterPath,
745
+ };
315
746
  }
316
747
 
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")}"`;
748
+ function parsePositionForSort(position: string): number {
749
+ // Fractional indexing keys sort lexicographically, but downstream
750
+ // consumers (and the UI) expect a numeric fallback. Keep a stable
751
+ // ordering by hashing the string into an integer.
752
+ let hash = 0;
753
+ for (let i = 0; i < position.length; i++) {
754
+ hash = (hash * 31 + position.charCodeAt(i)) | 0;
330
755
  }
331
- return value;
756
+ return hash;
757
+ }
758
+
759
+ function extOfFilename(filename: string): string {
760
+ const dot = filename.lastIndexOf(".");
761
+ return dot >= 0 ? filename.slice(dot) : "";
762
+ }
763
+
764
+ function extOfStorageKey(key: string): string {
765
+ const dot = key.lastIndexOf(".");
766
+ return dot >= 0 ? key.slice(dot + 1) : "webp";
332
767
  }
333
768
 
334
- function buildPostMarkdown(
769
+ /**
770
+ * Build a complete set of ExportFile entries for a single thread bundle:
771
+ * the root `_index.md`, one `index.md` per reply, and resource blobs for
772
+ * attached media when the storage driver can fetch them.
773
+ */
774
+ async function buildThreadBundle(
335
775
  root: Post,
336
776
  threadReplies: Post[],
337
- postCollections: Collection[],
338
- aliasData: {
339
- rootAliases: string[];
340
- zolaAliases: string[];
341
- },
777
+ rootSlug: string,
778
+ rootAliases: string[],
779
+ rootCollectionEntries: ExportedCollectionEntry[],
342
780
  slugMap: Map<string, string>,
781
+ collectionEntriesByPost: Map<
782
+ string,
783
+ {
784
+ collectionId: string;
785
+ createdAt: number;
786
+ position: number;
787
+ pinnedAt: number | null;
788
+ }[]
789
+ >,
343
790
  collectionSlugMap: Map<string, string>,
344
- rootMedia: Media[],
791
+ collectionTitleMap: Map<string, string>,
345
792
  mediaByPost: Map<string, Media[]>,
346
793
  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");
794
+ storage: StorageDriver | null,
795
+ ): Promise<ExportFile[]> {
796
+ const files: ExportFile[] = [];
797
+
798
+ // Root aliases = historical root slugs + every reply slug (so
799
+ // /{reply-slug}/ gets a Hugo alias page that redirects/anchors to
800
+ // the thread root).
801
+ const aliases = [...rootAliases];
802
+ for (const reply of threadReplies) {
803
+ const replySlug = slugMap.get(reply.id) ?? reply.slug;
804
+ aliases.push(`/${replySlug}/`);
365
805
  }
366
806
 
367
- const slug = slugMap.get(root.id) ?? root.slug;
368
- parts.push(`slug: ${yamlString(slug)}`);
807
+ // Root front matter.
808
+ const rootMedia = mediaByPost.get(root.id) ?? [];
809
+ const rootEmissions = rootMedia.map((m) => buildMediaEmission(m, siteConfig));
810
+ const rootMediaList = rootEmissions.map((e) => e.entry);
811
+ const rootFrontMatter: HugoFrontMatter = {
812
+ id: root.id,
813
+ title: root.format !== "quote" ? (root.title ?? undefined) : undefined,
814
+ date:
815
+ root.publishedAt !== null
816
+ ? toISOString(root.publishedAt)
817
+ : toISOString(root.createdAt),
818
+ updated:
819
+ root.updatedAt && root.updatedAt !== root.publishedAt
820
+ ? toISOString(root.updatedAt)
821
+ : undefined,
822
+ // `updated` only reflects edits to the root post itself; when a reply
823
+ // lands, it does NOT bump. For RSS we want the thread's last activity
824
+ // (max of all published reply timestamps) so readers re-surface a
825
+ // thread when a new reply appears. Kept alongside `updated` so the
826
+ // importer round-trips cleanly.
827
+ last_activity_at:
828
+ root.lastActivityAt !== null && root.lastActivityAt !== root.publishedAt
829
+ ? toISOString(root.lastActivityAt)
830
+ : undefined,
831
+ slug: rootSlug,
832
+ type: "post",
833
+ draft:
834
+ root.status === "draft" || root.visibility === "private"
835
+ ? true
836
+ : undefined,
837
+ aliases: aliases.length > 0 ? aliases : undefined,
838
+ format: root.format,
839
+ status: root.status,
840
+ visibility: root.visibility,
841
+ summary_text: getArchiveSummaryText(root) ?? undefined,
842
+ link_url: root.format === "link" && root.url ? root.url : undefined,
843
+ source_name: root.format === "quote" && root.title ? root.title : undefined,
844
+ source_url: root.format === "quote" && root.url ? root.url : undefined,
845
+ quote_text: root.quoteText ?? undefined,
846
+ rating: root.rating ?? undefined,
847
+ featured_at:
848
+ root.featuredAt !== null ? toISOString(root.featuredAt) : undefined,
849
+ pinned_at: root.pinnedAt !== null ? toISOString(root.pinnedAt) : undefined,
850
+ root_aliases: rootAliases.length > 0 ? rootAliases : undefined,
851
+ collections:
852
+ rootCollectionEntries.length > 0
853
+ ? collectionEntriesToRefs(rootCollectionEntries)
854
+ : undefined,
855
+ media: rootMediaList.length > 0 ? rootMediaList : undefined,
856
+ };
369
857
 
370
- if (aliasData.zolaAliases.length > 0) {
371
- parts.push("aliases:");
372
- for (const a of aliasData.zolaAliases) {
373
- parts.push(` - ${yamlString(a)}`);
858
+ const rootBody = root.body ? tiptapJsonToMarkdown(root.body) : "";
859
+ files.push({
860
+ path: `content/${rootSlug}/_index.md`,
861
+ content: `${await formatFrontMatter(rootFrontMatter)}\n${rootBody}${rootBody.endsWith("\n") ? "" : "\n"}`,
862
+ });
863
+
864
+ // Emit media bytes under static/media/ for any media without a
865
+ // reachable public URL. Media whose provider has a configured public
866
+ // URL keeps `src` pointing at that absolute URL and skips inlining —
867
+ // this avoids re-downloading every attachment when the site is going
868
+ // to keep serving media from the existing CDN/proxy anyway.
869
+ for (const { emission, media } of rootEmissions.map((e, i) => ({
870
+ emission: e,
871
+ media: rootMedia[i] as Media,
872
+ }))) {
873
+ if (emission.inlinePath) {
874
+ const file = await readMediaResourceFile(
875
+ storage,
876
+ media.storageKey,
877
+ emission.inlinePath,
878
+ );
879
+ if (file) files.push(file);
374
880
  }
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)}`);
881
+ if (emission.inlinePosterPath && media.posterKey) {
882
+ const posterFile = await readMediaResourceFile(
883
+ storage,
884
+ media.posterKey,
885
+ emission.inlinePosterPath,
886
+ );
887
+ if (posterFile) files.push(posterFile);
384
888
  }
385
889
  }
386
890
 
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
- }
891
+ // Replies as nested leaf bundles.
892
+ for (const reply of threadReplies) {
893
+ const replySlug = slugMap.get(reply.id) ?? reply.slug;
894
+ const replyMedia = mediaByPost.get(reply.id) ?? [];
895
+ const replyEmissions = replyMedia.map((m) =>
896
+ buildMediaEmission(m, siteConfig),
897
+ );
898
+ const replyMediaList = replyEmissions.map((e) => e.entry);
899
+ const replyCollectionEntries = buildExportedCollectionEntriesForPost(
900
+ reply.id,
901
+ collectionEntriesByPost,
902
+ collectionSlugMap,
903
+ collectionTitleMap,
904
+ );
424
905
 
425
- parts.push("---");
426
- parts.push("");
906
+ const replyFrontMatter: HugoFrontMatter = {
907
+ id: reply.id,
908
+ title: reply.format !== "quote" ? (reply.title ?? undefined) : undefined,
909
+ date:
910
+ reply.publishedAt !== null
911
+ ? toISOString(reply.publishedAt)
912
+ : toISOString(reply.createdAt),
913
+ updated:
914
+ reply.updatedAt && reply.updatedAt !== reply.publishedAt
915
+ ? toISOString(reply.updatedAt)
916
+ : undefined,
917
+ slug: replySlug,
918
+ type: "post",
919
+ draft:
920
+ reply.status === "draft" || reply.visibility === "private"
921
+ ? true
922
+ : undefined,
923
+ build: { render: "never", list: "local" },
924
+ format: reply.format,
925
+ status: reply.status,
926
+ visibility: reply.visibility,
927
+ summary_text: getArchiveSummaryText(reply) ?? undefined,
928
+ link_url: reply.format === "link" && reply.url ? reply.url : undefined,
929
+ source_name:
930
+ reply.format === "quote" && reply.title ? reply.title : undefined,
931
+ source_url: reply.format === "quote" && reply.url ? reply.url : undefined,
932
+ quote_text: reply.quoteText ?? undefined,
933
+ rating: reply.rating ?? undefined,
934
+ featured_at:
935
+ reply.featuredAt !== null ? toISOString(reply.featuredAt) : undefined,
936
+ pinned_at:
937
+ reply.pinnedAt !== null ? toISOString(reply.pinnedAt) : undefined,
938
+ collections:
939
+ replyCollectionEntries.length > 0
940
+ ? collectionEntriesToRefs(replyCollectionEntries)
941
+ : undefined,
942
+ media: replyMediaList.length > 0 ? replyMediaList : undefined,
943
+ };
427
944
 
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"));
945
+ const replyBody = reply.body ? tiptapJsonToMarkdown(reply.body) : "";
946
+ files.push({
947
+ path: `content/${rootSlug}/${replySlug}/index.md`,
948
+ content: `${await formatFrontMatter(replyFrontMatter)}\n${replyBody}${replyBody.endsWith("\n") ? "" : "\n"}`,
949
+ });
950
+
951
+ for (const { emission, media } of replyEmissions.map((e, i) => ({
952
+ emission: e,
953
+ media: replyMedia[i] as Media,
954
+ }))) {
955
+ if (emission.inlinePath) {
956
+ const file = await readMediaResourceFile(
957
+ storage,
958
+ media.storageKey,
959
+ emission.inlinePath,
960
+ );
961
+ if (file) files.push(file);
962
+ }
963
+ if (emission.inlinePosterPath && media.posterKey) {
964
+ const posterFile = await readMediaResourceFile(
965
+ storage,
966
+ media.posterKey,
967
+ emission.inlinePosterPath,
968
+ );
969
+ if (posterFile) files.push(posterFile);
970
+ }
971
+ }
435
972
  }
436
973
 
437
- // Thread replies
438
- for (const reply of threadReplies) {
439
- parts.push("");
974
+ return files;
975
+ }
440
976
 
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}"`;
977
+ /**
978
+ * Read a media record's bytes from storage and return an ExportFile so
979
+ * they can be bundled next to the post as a Hugo page resource. Returns
980
+ * null when storage is unavailable or the object cannot be read, in
981
+ * which case the front matter entry still points at the resource name
982
+ * and the CLI's pull-media step (or a later sync) can fill it in.
983
+ */
984
+ async function readMediaResourceFile(
985
+ storage: StorageDriver | null,
986
+ storageKey: string,
987
+ bundlePath: string,
988
+ ): Promise<ExportFile | null> {
989
+ if (!storage) return null;
990
+ try {
991
+ const bytes = await readStorageObjectBytes(storage, storageKey);
992
+ if (!bytes) return null;
993
+ return { path: bundlePath, content: bytes };
994
+ } catch {
995
+ return null;
996
+ }
997
+ }
445
998
 
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 += " -->";
999
+ // ---------------------------------------------------------------------------
1000
+ // Section + landing pages
1001
+ // ---------------------------------------------------------------------------
465
1002
 
466
- parts.push(marker);
467
- parts.push("");
1003
+ async function buildHomeSection(siteConfig: SiteConfig): Promise<string> {
1004
+ const frontMatter: HugoFrontMatter = {
1005
+ title: siteConfig.siteName,
1006
+ type: "home",
1007
+ };
1008
+ return `${await formatFrontMatter(frontMatter)}\n`;
1009
+ }
468
1010
 
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
- }
1011
+ async function buildCollectionsSection(): Promise<string> {
1012
+ const frontMatter: HugoFrontMatter = {
1013
+ title: "Collections",
1014
+ type: "collections",
1015
+ };
1016
+ return `${await formatFrontMatter(frontMatter)}\n`;
1017
+ }
481
1018
 
482
- return parts.join("\n");
1019
+ async function buildArchiveSection(): Promise<string> {
1020
+ const frontMatter: HugoFrontMatter = {
1021
+ title: "Archive",
1022
+ type: "archive",
1023
+ // Opt into Atom output at /archive/index.xml.
1024
+ outputs: ["html", "rss"],
1025
+ };
1026
+ return `${await formatFrontMatter(frontMatter)}\n`;
483
1027
  }
484
1028
 
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");
1029
+ async function buildFeaturedSection(): Promise<string> {
1030
+ const frontMatter: HugoFrontMatter = {
1031
+ title: "Featured",
1032
+ type: "featured",
1033
+ // Opt into Atom output at /featured/index.xml.
1034
+ outputs: ["html", "rss"],
1035
+ };
1036
+ return `${await formatFrontMatter(frontMatter)}\n`;
1037
+ }
1038
+
1039
+ async function buildCollectionSection(
1040
+ collection: Collection,
1041
+ slug: string,
1042
+ entryCount: number,
1043
+ ): Promise<string> {
1044
+ const frontMatter: HugoFrontMatter = {
1045
+ title: collection.title,
1046
+ slug,
1047
+ type: "collection",
1048
+ summary_text: collection.description ?? undefined,
1049
+ sort_order: collection.sortOrder,
1050
+ entry_count: entryCount,
1051
+ // Opt into Atom output at /{slug}/index.xml.
1052
+ outputs: ["html", "rss"],
1053
+ };
1054
+ return `${await formatFrontMatter(frontMatter)}\n`;
498
1055
  }
499
1056
 
1057
+ // ---------------------------------------------------------------------------
1058
+ // Summary extraction (kept from the previous exporter)
1059
+ // ---------------------------------------------------------------------------
1060
+
500
1061
  function normalizeArchiveText(text: string | null | undefined): string {
501
1062
  return (text ?? "").replace(/\s+/g, " ").trim();
502
1063
  }
503
1064
 
504
1065
  function getArchiveSummaryText(post: Post): string | null {
1066
+ // `summary_text` is a plain-text projection of the post's primary content,
1067
+ // used for `<meta name="description">`, `og:description`, and archive/card
1068
+ // fallbacks when the rendered body is empty. The candidate list differs
1069
+ // per format so the description reflects the right "primary content":
1070
+ //
1071
+ // - Quote: body (commentary) → quoteText. Quotes have no title, so we
1072
+ // fall back to the quote itself to guarantee a meaningful description.
1073
+ // - Link: body only. Links have a title + domain; the URL is already
1074
+ // serialized as `link_url` and rendered as a domain badge, so using
1075
+ // it as `summary_text` would duplicate that information.
1076
+ // - Note: body only. If the body is empty, there's nothing to describe.
505
1077
  const candidates =
506
1078
  post.format === "quote"
507
- ? [post.summary, post.quoteText, post.bodyText, post.url]
508
- : [post.summary, post.bodyText, post.quoteText, post.url];
1079
+ ? [post.summary, post.bodyText, post.quoteText]
1080
+ : [post.summary, post.bodyText];
509
1081
 
510
1082
  for (const candidate of candidates) {
511
1083
  const normalized = normalizeArchiveText(candidate);
512
1084
  if (normalized) return normalized;
513
1085
  }
514
-
515
1086
  return null;
516
1087
  }
517
1088
 
518
- function buildAttachmentBlock(
519
- mediaList: Media[],
520
- siteConfig: SiteConfig,
521
- textAttachmentContents: Map<string, TextAttachmentContent>,
522
- ): string {
523
- if (mediaList.length === 0) return "";
1089
+ // ---------------------------------------------------------------------------
1090
+ // Collection metrics + directory items (kept from the previous exporter)
1091
+ // ---------------------------------------------------------------------------
524
1092
 
525
- const figures = mediaList
526
- .map((media) =>
527
- buildAttachmentFigure(media, siteConfig, textAttachmentContents),
528
- )
529
- .join("\n");
1093
+ function formatCollectionActivityLabel(
1094
+ timestamp: number | undefined,
1095
+ ): string | null {
1096
+ if (typeof timestamp !== "number") {
1097
+ return null;
1098
+ }
530
1099
 
531
- return `<div data-jant-node="attachments">\n${figures}\n</div>`;
1100
+ return formatRelativeAge(timestamp);
532
1101
  }
533
1102
 
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>`;
1103
+ function formatCollectionActivityIso(
1104
+ timestamp: number | undefined,
1105
+ ): string | null {
1106
+ if (typeof timestamp !== "number") {
1107
+ return null;
566
1108
  }
567
1109
 
568
- const src = meta.src;
569
- if (!src) {
570
- throw new Error(`Attachment ${media.id} is missing an export URL`);
571
- }
1110
+ return toISOString(timestamp);
1111
+ }
572
1112
 
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>`;
1113
+ /**
1114
+ * Group-aware sequence labels for the exported collection directory.
1115
+ *
1116
+ * Mirrors the main site's `computeSequenceLabels` in
1117
+ * `ui/shared/CollectionDirectory.tsx` so Hugo exports render the same numeric
1118
+ * indices (e.g. "00" "01" under the first divider, "10" "11" under the
1119
+ * second). Dividers themselves receive an empty string — their slot is
1120
+ * reserved so the returned array is index-aligned with the source list.
1121
+ */
1122
+ function computeCollectionDirectorySequenceLabels(
1123
+ items: readonly ExportCollectionDirectorySourceItem[],
1124
+ ): string[] {
1125
+ const isContentItem = (item: ExportCollectionDirectorySourceItem) =>
1126
+ (item.type === "collection" && item.collection) ||
1127
+ (item.type === "link" && item.label && item.url);
1128
+
1129
+ const groupSizes: number[] = [];
1130
+ let seenDivider = false;
1131
+ let ungroupedCount = 0;
1132
+ for (const item of items) {
1133
+ if (item.type === "divider") {
1134
+ seenDivider = true;
1135
+ groupSizes.push(0);
1136
+ } else if (isContentItem(item)) {
1137
+ if (seenDivider) {
1138
+ const lastGroupIndex = groupSizes.length - 1;
1139
+ const lastGroupSize = groupSizes[lastGroupIndex];
1140
+ if (lastGroupSize !== undefined) {
1141
+ groupSizes[lastGroupIndex] = lastGroupSize + 1;
1142
+ }
1143
+ } else {
1144
+ ungroupedCount += 1;
1145
+ }
1146
+ }
580
1147
  }
581
1148
 
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
- }
1149
+ const hasGroups = groupSizes.length > 0;
1150
+ const maxGroupIndex = Math.max(0, groupSizes.length - 1);
1151
+ const groupWidth = hasGroups
1152
+ ? Math.max(1, maxGroupIndex.toString(36).length)
1153
+ : 0;
1154
+ const ungroupedItemWidth = Math.max(
1155
+ 2,
1156
+ String(Math.max(0, ungroupedCount - 1)).length,
1157
+ );
594
1158
 
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>`;
1159
+ const labels: string[] = [];
1160
+ let groupIndex = -1;
1161
+ let itemIndex = 0;
1162
+
1163
+ for (const item of items) {
1164
+ if (item.type === "divider") {
1165
+ groupIndex += 1;
1166
+ itemIndex = 0;
1167
+ labels.push("");
1168
+ } else if (isContentItem(item)) {
1169
+ if (hasGroups) {
1170
+ const g = Math.max(0, groupIndex)
1171
+ .toString(36)
1172
+ .padStart(groupWidth, "0");
1173
+ const i = itemIndex.toString(36);
1174
+ labels.push(g + i);
1175
+ } else {
1176
+ labels.push(String(itemIndex).padStart(ungroupedItemWidth, "0"));
1177
+ }
1178
+ itemIndex += 1;
1179
+ } else {
1180
+ labels.push("");
1181
+ }
601
1182
  }
602
1183
 
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>`;
1184
+ return labels;
612
1185
  }
613
1186
 
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
- }
1187
+ function buildExportedCollectionDirectoryItems(
1188
+ items: readonly ExportCollectionDirectorySourceItem[],
1189
+ collectionSlugMap: Map<string, string>,
1190
+ collectionMetrics: Map<string, ExportedCollectionMetrics>,
1191
+ ): ExportedCollectionDirectoryItem[] {
1192
+ const sequenceLabels = computeCollectionDirectorySequenceLabels(items);
1193
+ const exportedItems: ExportedCollectionDirectoryItem[] = [];
1194
+
1195
+ items.forEach((item, index) => {
1196
+ if (item.type === "divider") {
1197
+ exportedItems.push({
1198
+ type: "divider",
1199
+ label: item.label ?? null,
1200
+ });
1201
+ return;
1202
+ }
620
1203
 
621
- if (media.summary?.trim()) {
622
- return media.summary.trim();
623
- }
1204
+ if (item.type === "link") {
1205
+ if (!item.label || !item.url) {
1206
+ return;
1207
+ }
624
1208
 
625
- return media.mimeType;
626
- }
1209
+ const description = item.description?.trim();
1210
+ exportedItems.push({
1211
+ type: "link",
1212
+ sequence: sequenceLabels[index] ?? "",
1213
+ label: item.label,
1214
+ url: item.url,
1215
+ descriptionHtml: description ? renderMarkdown(description) : null,
1216
+ });
1217
+ return;
1218
+ }
627
1219
 
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
- );
1220
+ const collection = item.collection;
1221
+ if (!collection?.id) {
1222
+ return;
638
1223
  }
639
1224
 
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
- };
1225
+ const slug = collectionSlugMap.get(collection.id) ?? collection.slug;
1226
+ if (!slug) {
1227
+ return;
1228
+ }
1229
+ const metrics = collectionMetrics.get(collection.id);
1230
+ const activityTimestamp =
1231
+ metrics?.recentActivityAt ?? collection.recentActivityAt;
1232
+
1233
+ const collectionDescription = collection.description?.trim();
1234
+ exportedItems.push({
1235
+ type: "collection",
1236
+ sequence: sequenceLabels[index] ?? "",
1237
+ slug,
1238
+ title: collection.title || slug,
1239
+ descriptionHtml: collectionDescription
1240
+ ? renderMarkdown(collectionDescription)
1241
+ : null,
1242
+ entryCount:
1243
+ metrics?.postCount ??
1244
+ (typeof collection.postCount === "number"
1245
+ ? collection.postCount
1246
+ : undefined),
1247
+ recentActivityLabel: formatCollectionActivityLabel(activityTimestamp),
1248
+ recentActivityIso: formatCollectionActivityIso(activityTimestamp),
1249
+ });
1250
+ });
1251
+
1252
+ return exportedItems;
1253
+ }
1254
+
1255
+ function buildExportedCollectionMetrics(
1256
+ collections: readonly Collection[],
1257
+ posts: readonly Post[],
1258
+ collectionsByPost: ReadonlyMap<string, readonly Collection[]>,
1259
+ ): Map<string, ExportedCollectionMetrics> {
1260
+ const metrics = new Map<string, ExportedCollectionMetrics>();
1261
+
1262
+ for (const collection of collections) {
1263
+ metrics.set(collection.id, {
1264
+ postCount: 0,
1265
+ recentActivityAt: collection.updatedAt,
1266
+ });
656
1267
  }
657
1268
 
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");
1269
+ for (const post of posts) {
1270
+ if (post.deletedAt !== null) {
1271
+ continue;
1272
+ }
1273
+ // Drafts and private posts are excluded — they won't reach Hugo.
1274
+ if (post.status === "draft" || post.visibility === "private") {
1275
+ continue;
1276
+ }
1277
+ // Replies roll up into their thread root for directory metrics.
1278
+ if (post.replyToId !== null) {
1279
+ continue;
1280
+ }
693
1281
 
694
- if (textAttachments.length === 0) {
695
- return new Map();
696
- }
1282
+ const activityAt =
1283
+ post.lastActivityAt ??
1284
+ post.publishedAt ??
1285
+ post.updatedAt ??
1286
+ post.createdAt;
1287
+ const postCollections = collectionsByPost.get(post.id) ?? [];
1288
+
1289
+ for (const collection of postCollections) {
1290
+ const current = metrics.get(collection.id);
1291
+ if (!current) {
1292
+ continue;
1293
+ }
697
1294
 
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`,
1295
+ if (current.postCount === 0) {
1296
+ current.recentActivityAt = activityAt;
1297
+ } else {
1298
+ current.recentActivityAt = Math.max(
1299
+ current.recentActivityAt,
1300
+ activityAt,
707
1301
  );
708
1302
  }
709
- return [media.id, content] as const;
710
- }),
711
- );
1303
+ current.postCount += 1;
1304
+ }
1305
+ }
712
1306
 
713
- return new Map(contents);
1307
+ return metrics;
714
1308
  }
715
1309
 
716
- function escapeCommentAttribute(value: string): string {
717
- return value
718
- .replace(/\\/g, "\\\\")
719
- .replace(/"/g, '\\"')
720
- .replace(/\n/g, "\\n");
721
- }
1310
+ // ---------------------------------------------------------------------------
1311
+ // Nav item resolution
1312
+ // ---------------------------------------------------------------------------
722
1313
 
723
- function safeJsonForHtml(value: unknown): string {
724
- return JSON.stringify(value)
725
- .replace(/</g, "\\u003c")
726
- .replace(/>/g, "\\u003e")
727
- .replace(/&/g, "\\u0026");
1314
+ /**
1315
+ * System nav items on the main site store an empty `label` in the DB and
1316
+ * resolve their display text at render time through i18n
1317
+ * (`getNavItemDisplayLabel`). The Hugo export has no i18n runtime, so fall
1318
+ * back to these English defaults when serializing to `data/jant.toml`.
1319
+ * Users can still override by setting a custom label on the nav item.
1320
+ */
1321
+ const SYSTEM_NAV_FALLBACK_LABELS: Record<string, string> = {
1322
+ latest: "Latest",
1323
+ featured: "Featured",
1324
+ collections: "Collections",
1325
+ archive: "Archive",
1326
+ rss: "RSS",
1327
+ settings: "Settings",
1328
+ };
1329
+
1330
+ function resolveNavItemLabel(item: SiteConfig["navItems"][number]): string {
1331
+ if (item.label) return item.label;
1332
+ if (item.systemKey) {
1333
+ const fallback = SYSTEM_NAV_FALLBACK_LABELS[item.systemKey];
1334
+ if (fallback) return fallback;
1335
+ }
1336
+ return item.label;
728
1337
  }
729
1338
 
730
- function buildConfigToml(
731
- config: SiteConfig,
732
- iconAssets: SiteIconAssets,
1339
+ /**
1340
+ * Resolve a nav item's final href for the Hugo export.
1341
+ *
1342
+ * Mirrors the runtime logic in `lib/view.ts:toNavItemView`. System URLs
1343
+ * stored in the DB ("/latest", "/featured") are not real routes — they get
1344
+ * rewritten to "/" when they match `homeDefaultView`, otherwise they
1345
+ * resolve to the dedicated path.
1346
+ *
1347
+ * The "rss" system nav item points at whichever Atom feed the site has
1348
+ * configured as its main feed: `mainRssFeed === "featured"` → the featured
1349
+ * section feed at `/featured/index.xml`, otherwise the home feed at
1350
+ * `/index.xml` (which mirrors the homepage's "latest" timeline).
1351
+ */
1352
+ function resolveNavItemUrl(
1353
+ item: SiteConfig["navItems"][number],
1354
+ homeDefaultView: string,
1355
+ mainRssFeed: string,
733
1356
  ): 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",
1357
+ if (item.systemKey === "latest") {
1358
+ return homeDefaultView === "latest" ? "/" : "/latest/";
1359
+ }
1360
+ if (item.systemKey === "featured") {
1361
+ return homeDefaultView === "featured" ? "/" : "/featured/";
1362
+ }
1363
+ if (item.systemKey === "collections") return "/collections/";
1364
+ if (item.systemKey === "archive") return "/archive/";
1365
+ if (item.systemKey === "rss") {
1366
+ return mainRssFeed === "featured" ? "/featured/index.xml" : "/index.xml";
1367
+ }
1368
+ return item.url;
1369
+ }
1370
+
1371
+ // ---------------------------------------------------------------------------
1372
+ // hugo.toml + data TOMLs
1373
+ // ---------------------------------------------------------------------------
1374
+
1375
+ /** Escape a string for use inside a TOML double-quoted value. */
1376
+ function escapeTomlString(value: string): string {
1377
+ return value
1378
+ .replace(/\\/g, "\\\\")
1379
+ .replace(/"/g, '\\"')
1380
+ .replace(/\r/g, "\\r")
1381
+ .replace(/\n/g, "\\n");
1382
+ }
1383
+
1384
+ function buildHugoToml(config: SiteConfig): string {
1385
+ const baseUrl = (config.siteUrl || "https://example.com").replace(/\/+$/, "");
1386
+ // Hugo requires language codes to be all lowercase (it rejects the BCP-47
1387
+ // casing `zh-Hant` / `zh-Hans` with "must be all lower case and no spaces").
1388
+ const language = config.siteLanguage.toLowerCase();
1389
+ const parts: string[] = [
1390
+ `baseURL = "${escapeTomlString(baseUrl)}/"`,
1391
+ `title = "${escapeTomlString(config.siteName)}"`,
1392
+ `languageCode = "${escapeTomlString(language)}"`,
1393
+ `defaultContentLanguage = "${escapeTomlString(language)}"`,
1394
+ 'theme = "jant"',
1395
+ `paginate = ${config.pageSize}`,
1396
+ "enableRobotsTXT = true",
1397
+ // Disable Hugo's built-in taxonomies — jant has no tags or categories
1398
+ // and the default empty /tags/ and /categories/ pages are noise. This
1399
+ // must stay at the root (before any `[table]` header) so TOML doesn't
1400
+ // nest it under the previous table.
1401
+ "disableKinds = ['taxonomy', 'term']",
1402
+ "",
1403
+ "[permalinks]",
1404
+ ' post = "/:slug/"',
1405
+ "",
1406
+ "[markup]",
1407
+ " [markup.goldmark]",
1408
+ " [markup.goldmark.renderer]",
1409
+ " unsafe = true",
742
1410
  "",
743
- 'feed_filenames = ["atom.xml"]',
1411
+ // Emit an Atom 2005 feed at the site root (/index.xml). Per-section
1412
+ // feeds (featured, archive, each collection) are opted in via each
1413
+ // section's front matter `outputs: ["html", "rss"]` — enabling RSS on
1414
+ // `section` globally here would also create a feed at every root post's
1415
+ // URL, since root posts are themselves branch bundles / sections.
1416
+ // Override the built-in RSS output format to emit Atom instead of
1417
+ // RSS 2.0 so the wire format mirrors the main site's `lib/feed.ts`.
1418
+ "[outputs]",
1419
+ ' home = ["html", "rss"]',
1420
+ // Hugo's default for sections is ["html", "rss"] — without overriding
1421
+ // it here every root post (which is a section) would get its own
1422
+ // /{slug}/index.xml. Turn sections off by default and re-enable RSS
1423
+ // on just featured, archive, and each collection via per-section
1424
+ // front matter `outputs: ["html", "rss"]`.
1425
+ ' section = ["html"]',
744
1426
  "",
745
- "[extra.jant_export]",
1427
+ "[outputFormats]",
1428
+ " [outputFormats.RSS]",
1429
+ ' mediaType = "application/atom+xml"',
1430
+ ' baseName = "index"',
1431
+ // Use text/template (not html/template) so Hugo doesn't HTML-escape
1432
+ // the XML prologue, CDATA markers, or tag literals. Every dynamic
1433
+ // value inside the template passes through `transform.XMLEscape`.
1434
+ " isPlainText = true",
1435
+ ' rel = "alternate"',
1436
+ "",
1437
+ "[mediaTypes]",
1438
+ ' [mediaTypes."application/atom+xml"]',
1439
+ ' suffixes = ["xml"]',
1440
+ "",
1441
+ "[params]",
1442
+ ` description = "${escapeTomlString(config.siteDescription)}"`,
1443
+ ` home_default_view = "${escapeTomlString(config.homeDefaultView)}"`,
1444
+ ` main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
1445
+ ` show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
1446
+ ` show_header_avatar = ${config.showHeaderAvatar}`,
1447
+ ` noindex = ${config.noindex}`,
1448
+ ` theme_id = "${escapeTomlString(config.themeId)}"`,
1449
+ ` default_theme_id = "${escapeTomlString(config.defaultThemeId)}"`,
1450
+ ` font_theme_id = "${escapeTomlString(config.fontThemeId)}"`,
1451
+ ` theme_mode = "${escapeTomlString(config.themeMode)}"`,
1452
+ ` page_size = ${config.pageSize}`,
1453
+ ` archive_page_size = ${config.archivePageSize}`,
1454
+ ` rss_feed_limit = ${config.rssFeedLimit}`,
1455
+ ];
1456
+ if (config.siteAvatarUrl) {
1457
+ parts.push(
1458
+ ` site_avatar_url = "${escapeTomlString(config.siteAvatarUrl)}"`,
1459
+ );
1460
+ }
1461
+ if (config.faviconVersion) {
1462
+ parts.push(
1463
+ ` favicon_version = "${escapeTomlString(config.faviconVersion)}"`,
1464
+ );
1465
+ }
1466
+
1467
+ return `${parts.join("\n")}\n`;
1468
+ }
1469
+
1470
+ function buildJantDataToml(
1471
+ config: SiteConfig,
1472
+ iconAssets: SiteIconAssets,
1473
+ directoryItems: readonly ExportedCollectionDirectoryItem[],
1474
+ ): string {
1475
+ const footerHtml = config.siteFooter ? renderMarkdown(config.siteFooter) : "";
1476
+ const parts: string[] = [
746
1477
  'format = "jant-site"',
747
1478
  "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}`,
1479
+ `generated_at = "${escapeTomlString(toISOString(Math.floor(Date.now() / 1000)))}"`,
1480
+ `site_name = "${escapeTomlString(config.siteName)}"`,
1481
+ `site_description = "${escapeTomlString(config.siteDescription)}"`,
1482
+ `site_language = "${escapeTomlString(config.siteLanguage)}"`,
1483
+ `home_default_view = "${escapeTomlString(config.homeDefaultView)}"`,
1484
+ `main_rss_feed = "${escapeTomlString(config.mainRssFeed)}"`,
753
1485
  `show_jant_branding_on_home = ${config.showJantBrandingOnHome}`,
754
1486
  `show_header_avatar = ${config.showHeaderAvatar}`,
755
1487
  `noindex = ${config.noindex}`,
756
1488
  `site_avatar_mode = "${config.siteAvatarUrl ? "custom" : "none"}"`,
757
1489
  `favicon_mode = "${iconAssets.faviconMode}"`,
758
1490
  `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)}"`,
1491
+ `theme_id = "${escapeTomlString(config.themeId)}"`,
1492
+ `default_theme_id = "${escapeTomlString(config.defaultThemeId)}"`,
1493
+ `font_theme_id = "${escapeTomlString(config.fontThemeId)}"`,
1494
+ `theme_mode = "${escapeTomlString(config.themeMode)}"`,
1495
+ `page_size = ${config.pageSize}`,
1496
+ `archive_page_size = ${config.archivePageSize}`,
1497
+ `rss_feed_limit = ${config.rssFeedLimit}`,
1498
+ 'favicon_path = "/favicon.ico"',
1499
+ 'apple_touch_icon_path = "/apple-touch-icon.png"',
764
1500
  ];
765
-
766
1501
  if (config.siteAvatarUrl) {
767
- parts.push(`site_avatar_url = "${escapeToml(config.siteAvatarUrl)}"`);
1502
+ parts.push(`site_avatar_url = "${escapeTomlString(config.siteAvatarUrl)}"`);
768
1503
  }
769
1504
  if (config.faviconVersion) {
770
- parts.push(`favicon_version = "${escapeToml(config.faviconVersion)}"`);
1505
+ parts.push(
1506
+ `favicon_version = "${escapeTomlString(config.faviconVersion)}"`,
1507
+ );
771
1508
  }
772
1509
  if (footerHtml) {
773
- parts.push(`site_footer_html = "${escapeToml(footerHtml)}"`);
1510
+ parts.push(`site_footer_html = "${escapeTomlString(footerHtml)}"`);
774
1511
  }
775
1512
  if (config.siteFooter) {
776
- parts.push(`site_footer_markdown = "${escapeToml(config.siteFooter)}"`);
1513
+ parts.push(
1514
+ `site_footer_markdown = "${escapeTomlString(config.siteFooter)}"`,
1515
+ );
777
1516
  }
778
1517
 
779
1518
  for (const item of config.navItems) {
1519
+ // `settings` is authenticated-only and has no corresponding page in the
1520
+ // static Hugo site — drop it at export time so it never shows up in nav.
1521
+ if (item.systemKey === "settings") continue;
780
1522
  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
- }
1523
+ parts.push("[[nav]]");
1524
+ parts.push(`type = "${escapeTomlString(item.type)}"`);
1525
+ parts.push(`label = "${escapeTomlString(resolveNavItemLabel(item))}"`);
1526
+ parts.push(
1527
+ `url = "${escapeTomlString(resolveNavItemUrl(item, config.homeDefaultView, config.mainRssFeed))}"`,
1528
+ );
1529
+ parts.push(`system_key = "${escapeTomlString(item.systemKey ?? "")}"`);
1530
+ parts.push(`placement = "${escapeTomlString(item.placement ?? "header")}"`);
788
1531
  }
789
1532
 
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"');
1533
+ for (const item of directoryItems) {
1534
+ parts.push("");
1535
+ parts.push("[[directory]]");
1536
+ parts.push(`type = "${escapeTomlString(item.type)}"`);
1537
+ if (item.type === "collection") {
1538
+ parts.push(`sequence = "${escapeTomlString(item.sequence)}"`);
1539
+ parts.push(`slug = "${escapeTomlString(item.slug)}"`);
1540
+ parts.push(`title = "${escapeTomlString(item.title)}"`);
1541
+ if (item.descriptionHtml) {
1542
+ parts.push(
1543
+ `description_html = "${escapeTomlString(item.descriptionHtml)}"`,
1544
+ );
1545
+ }
1546
+ if (typeof item.entryCount === "number") {
1547
+ parts.push(`entry_count = ${item.entryCount}`);
1548
+ }
1549
+ if (item.recentActivityLabel) {
1550
+ parts.push(
1551
+ `recent_activity_label = "${escapeTomlString(item.recentActivityLabel)}"`,
1552
+ );
1553
+ }
1554
+ if (item.recentActivityIso) {
1555
+ parts.push(
1556
+ `recent_activity_iso = "${escapeTomlString(item.recentActivityIso)}"`,
1557
+ );
1558
+ }
1559
+ } else if (item.type === "divider") {
1560
+ if (item.label !== null) {
1561
+ parts.push(`label = "${escapeTomlString(item.label)}"`);
1562
+ }
1563
+ } else {
1564
+ parts.push(`sequence = "${escapeTomlString(item.sequence)}"`);
1565
+ parts.push(`label = "${escapeTomlString(item.label)}"`);
1566
+ parts.push(`url = "${escapeTomlString(item.url)}"`);
1567
+ if (item.descriptionHtml) {
1568
+ parts.push(
1569
+ `description_html = "${escapeTomlString(item.descriptionHtml)}"`,
1570
+ );
1571
+ }
1572
+ }
1573
+ }
798
1574
 
799
- return `${parts.join("\n")}
800
- `;
1575
+ return `${parts.join("\n")}\n`;
801
1576
  }
802
1577
 
803
- function buildRootSection(): string {
804
- return `+++
805
- sort_by = "date"
806
- paginate_by = 20
807
- +++
808
- `;
809
- }
1578
+ // ---------------------------------------------------------------------------
1579
+ // README + .gitignore
1580
+ // ---------------------------------------------------------------------------
1581
+
1582
+ function buildGitignore(): string {
1583
+ return `# Hugo build output
1584
+ public/
1585
+ resources/
1586
+ .hugo_build.lock
1587
+
1588
+ # OS
1589
+ .DS_Store
1590
+ Thumbs.db
810
1591
 
811
- function buildArchiveSection(): string {
812
- return `+++
813
- title = "Archive"
814
- template = "archive.html"
815
- +++
1592
+ # Editors
1593
+ .vscode/
1594
+ .idea/
1595
+ *.swp
816
1596
  `;
817
1597
  }
818
1598
 
819
1599
  function buildReadme(siteName: string): string {
820
- return `# ${siteName} — Zola Export
1600
+ return `# ${siteName} — Hugo Export
821
1601
 
822
- This is a static site exported from [Jant](https://github.com/jant-me/jant), ready to build with [Zola](https://www.getzola.org/).
1602
+ This is a static site exported from [Jant](https://github.com/jant-me/jant), ready to build with [Hugo](https://gohugo.io/).
823
1603
 
824
- ## Install Zola
1604
+ ## Install Hugo
1605
+
1606
+ This export targets Hugo **extended 0.160.1+**.
825
1607
 
826
1608
  **macOS (Homebrew):**
827
1609
 
828
1610
  \`\`\`sh
829
- brew install zola
1611
+ brew install hugo
830
1612
  \`\`\`
831
1613
 
832
1614
  **Windows (Scoop):**
833
1615
 
834
1616
  \`\`\`sh
835
- scoop install zola
1617
+ scoop install hugo-extended
836
1618
  \`\`\`
837
1619
 
838
- **Linux (Snap):**
839
-
840
- \`\`\`sh
841
- snap install zola --edge
842
- \`\`\`
1620
+ **Linux:**
843
1621
 
844
- Or download a binary from <https://github.com/getzola/zola/releases>.
1622
+ Download the extended build from <https://github.com/gohugoio/hugo/releases>.
845
1623
 
846
- See the [Zola installation docs](https://www.getzola.org/documentation/getting-started/installation/) for more options.
1624
+ See the [Hugo installation docs](https://gohugo.io/installation/) for more options.
847
1625
 
848
1626
  ## Quick start
849
1627
 
850
1628
  Preview locally:
851
1629
 
852
1630
  \`\`\`sh
853
- zola serve
1631
+ hugo serve
854
1632
  \`\`\`
855
1633
 
856
- Then open <http://127.0.0.1:1111> in your browser.
1634
+ Then open <http://localhost:1313> in your browser.
857
1635
 
858
1636
  Build the site for deployment:
859
1637
 
860
1638
  \`\`\`sh
861
- zola build
1639
+ hugo --minify
862
1640
  \`\`\`
863
1641
 
864
1642
  The output goes to the \`public/\` directory. Upload it to any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, etc.).
@@ -866,1886 +1644,61 @@ The output goes to the \`public/\` directory. Upload it to any static host (Netl
866
1644
  ## Project structure
867
1645
 
868
1646
  \`\`\`
869
- config.toml — Site configuration (title, URL, language)
1647
+ hugo.toml — Site configuration (baseURL, title, theme, params)
870
1648
  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)
1649
+ _index.md Home section
1650
+ archive/_index.md Archive section
1651
+ collections/_index.md Collections directory section
1652
+ featured/_index.md Featured section
1653
+ {slug}/
1654
+ _index.md Thread root (branch bundle)
1655
+ {reply-slug}/
1656
+ index.md Reply (leaf bundle, not rendered as its own URL)
1657
+ data/
1658
+ jant.toml Nav items, branding, display preferences, ordered collections directory
1659
+ themes/jant/ — Bundled Hugo theme (overrideable via layouts/ at the site root)
1660
+ static/ — Copy files here to add them to the published site
881
1661
  \`\`\`
882
1662
 
883
1663
  ## Customizing
884
1664
 
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 %}
1665
+ - **Site settings** — edit \`hugo.toml\` to change the baseURL, title, or pagination.
1666
+ - **Jant metadata** — \`data/jant.toml\` drives nav and the collections directory, and is preserved across round-trip import.
1667
+ - **Styles** — edit \`themes/jant/static/main.css\`, or drop a \`static/main.css\` at the site root to override.
1668
+ - **Templates** — add files under \`layouts/\` at the site root to override the bundled theme.
1669
+ - **Debugging** — from a Jant site project, run \`npx jant site export --directory ./my-site\`, then \`cd my-site && hugo serve\`.
1056
1670
 
1057
- {% block title %}{% if page.title %}{{ page.title }} &mdash; {% endif %}{{ config.title }}{% endblock %}
1671
+ ## Fetching media locally
1058
1672
 
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
- `;
1673
+ 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
1674
 
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
- `;
1675
+ 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
1676
 
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
- `;
1677
+ \`\`\`sh
1678
+ npx @jant/core site pull-media --path .
1679
+ \`\`\`
1219
1680
 
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
- `;
1681
+ 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
1682
 
1262
- // ---------------------------------------------------------------------------
1263
- // Shared macro — single post card used by all list/detail templates
1264
- // ---------------------------------------------------------------------------
1683
+ ## Notes
1265
1684
 
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 %}
1685
+ - 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.
1686
+ - \`/{reply-slug}/\` URLs are preserved via \`aliases:\` on the root post, so old links still land on the right thread anchor.
1687
+ - 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.
1688
+ - Posts with \`draft: true\` in front matter are only built when you pass \`--buildDrafts\` to \`hugo\` / \`hugo serve\`.
1492
1689
  `;
1690
+ }
1493
1691
 
1494
1692
  // ---------------------------------------------------------------------------
1495
- // CSS Jant "Organic Minimalism" approximation
1693
+ // Re-exports for consumers (kept so existing entry points compile)
1496
1694
  // ---------------------------------------------------------------------------
1497
1695
 
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
- `;
1696
+ export {
1697
+ buildSiteIconAssets,
1698
+ buildExportedCollectionMetrics,
1699
+ buildExportedCollectionDirectoryItems,
1700
+ readStorageObjectBytes,
1701
+ getArchiveSummaryText,
1702
+ getMediaUrl,
1703
+ getPublicUrlForProvider,
1704
+ };