@jant/core 0.3.27 → 0.3.29

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 (314) hide show
  1. package/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -267
  172. package/dist/auth.js +0 -39
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
package/dist/i18n/i18n.js DELETED
@@ -1,49 +0,0 @@
1
- /**
2
- * i18n Runtime using @lingui/core
3
- *
4
- * The SWC Lingui plugin adds hash-based IDs to t() calls when imports come
5
- * from @lingui/react/macro. The runtimeConfigModule setting rewrites those
6
- * imports to our custom Hono JSX implementation at build time.
7
- */ import { I18n } from "@lingui/core";
8
- import { locales, baseLocale, isLocale } from "./locales.js";
9
- import { messages as messagesEn } from "./locales/en.js";
10
- import { messages as messagesZhHans } from "./locales/zh-Hans.js";
11
- import { messages as messagesZhHant } from "./locales/zh-Hant.js";
12
- export { locales, baseLocale, isLocale };
13
- // Pre-compute merged catalogs at module load time (done once, not per request)
14
- // For non-English locales, merge English as fallback so missing translations
15
- // fall back to English rather than showing hash IDs.
16
- const catalogEn = messagesEn;
17
- const catalogZhHans = {
18
- ...messagesEn,
19
- ...messagesZhHans
20
- };
21
- const catalogZhHant = {
22
- ...messagesEn,
23
- ...messagesZhHant
24
- };
25
- /**
26
- * Create a new i18n instance for a specific locale.
27
- * IMPORTANT: In Cloudflare Workers (concurrent environment), we must create
28
- * a new instance per request to avoid race conditions. Never use a global instance!
29
- */ export function createI18n(locale) {
30
- const i18n = new I18n({});
31
- i18n.load("en", catalogEn);
32
- i18n.load("zh-Hans", catalogZhHans);
33
- i18n.load("zh-Hant", catalogZhHant);
34
- i18n.activate(locale);
35
- return i18n;
36
- }
37
- /**
38
- * Helper to get the per-request i18n instance from Hono context.
39
- * Use this in route handlers.
40
- *
41
- * @example
42
- * import { msg } from "@lingui/core/macro";
43
- * import { getI18n } from "../i18n/index.js";
44
- *
45
- * const i18n = getI18n(c);
46
- * const title = i18n._(msg({ message: "Dashboard", comment: "@context: Page title" }));
47
- */ export function getI18n(c) {
48
- return c.get("i18n");
49
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * i18n Module
3
- *
4
- * IMPORTANT: This module is designed for concurrent environments (Cloudflare Workers).
5
- * We create a new i18n instance per request to avoid race conditions.
6
- *
7
- * This is a custom implementation compatible with Hono JSX (SSR), not React.
8
- * It provides a React-like API using Lingui macros with a custom context system.
9
- *
10
- * Usage:
11
- * ```tsx
12
- * import { useLingui, Trans, I18nProvider } from "../i18n/index.js";
13
- *
14
- * // Wrap your app in I18nProvider (automatically done by BaseLayout when c is provided)
15
- * c.html(
16
- * <I18nProvider c={c}>
17
- * <MyApp />
18
- * </I18nProvider>
19
- * );
20
- *
21
- * // Inside components, use useLingui() hook
22
- * function MyApp() {
23
- * const { t } = useLingui();
24
- *
25
- * return (
26
- * <div>
27
- * <h1>{t({ message: "Dashboard", comment: "@context: Page title" })}</h1>
28
- * <Trans comment="@context: Help text">
29
- * Read the <a href="/docs">documentation</a>
30
- * </Trans>
31
- * </div>
32
- * );
33
- * }
34
- * ```
35
- */ // Core i18n runtime
36
- export { createI18n, getI18n, locales, baseLocale, isLocale } from "./i18n.js";
37
- // I18nProvider and useLingui hook (custom implementation for Hono JSX, SSR-compatible)
38
- export { I18nProvider, useLingui } from "./context.js";
39
- // Trans component (simplified for Hono JSX)
40
- export { Trans } from "./Trans.js";
41
- // Language detection utilities
42
- export { isValidLanguage, getLanguageDisplayName, getSupportedLanguages } from "./detect.js";
43
- // Hono middleware
44
- export { i18nMiddleware } from "./middleware.js";
@@ -1 +0,0 @@
1
- /*eslint-disable*/ export const messages = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+MACwa\":[\"No collections yet.\"],\"+MH6k9\":[\"Add to nav\"],\"+bHzpy\":[\"Display text for the link\"],\"+owNNn\":[\"Posts\"],\"+zy2Nq\":[\"Type\"],\"/0D1Xp\":[\"Edit Collection\"],\"/Rj5P4\":[\"Your Name\"],\"/rkqRV\":[\"All pages are in your navigation.\"],\"07Epll\":[\"This will theme both your site and your dashboard. All color themes support dark mode.\"],\"0JkyS7\":[\"Create your first page\"],\"0a6MpL\":[\"New Redirect\"],\"0yIy82\":[\"No featured posts yet.\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"Notes\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"2N0qpv\":[\"Post title...\"],\"2cFU6q\":[\"Site Footer\"],\"2fUwEY\":[\"Select Media\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"Page title...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Yvsaz\":[\"302 (Temporary)\"],\"4/SFQS\":[\"View Site\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4KzVT6\":[\"Delete Page\"],\"4Ml90q\":[\"SEO\"],\"4b3oEV\":[\"Content\"],\"4mDPGp\":[\"The URL path for this page. Use lowercase letters, numbers, and hyphens.\"],\"6WdDG7\":[\"Page\"],\"6YtxFj\":[\"Name\"],\"7G4SBz\":[\"Page content (Markdown supported)...\"],\"7Mk+/h\":[\"Update Collection\"],\"7Q1KKN\":[\"From Path\"],\"7aECQB\":[\"Invalid or Expired Link\"],\"7nGhhM\":[\"What's on your mind?\"],\"7p5kLi\":[\"Dashboard\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8ZsakT\":[\"Password\"],\"9+vGLh\":[\"Custom CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"A1taO8\":[\"Search\"],\"AeXO77\":[\"Account\"],\"AyHO4m\":[\"What's this collection about?\"],\"B373X+\":[\"Edit Post\"],\"B495Gs\":[\"Archive\"],\"BjF0Jv\":[\"Lowercase letters, numbers, and hyphens only\"],\"D9Oea+\":[\"Permalink\"],\"DCKkhU\":[\"Current Password\"],\"DHhJ7s\":[\"Previous\"],\"DPfwMq\":[\"Done\"],\"DoJzLz\":[\"Collections\"],\"E80cJw\":[\"Deleting this media will remove it permanently from storage.\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"Complete Setup\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"Update\"],\"FGrimz\":[\"New Post\"],\"FkMol5\":[\"Featured\"],\"Fxf4jq\":[\"Description (optional)\"],\"GA5A5H\":[\"Delete Collection\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"Your site navigation\"],\"GX2VMa\":[\"Create your admin account.\"],\"GbVAnd\":[\"This password reset link is invalid or has expired. Please generate a new one.\"],\"GorKul\":[\"Welcome to Jant\"],\"GrZ6fH\":[\"New Page\"],\"GxkJXS\":[\"Uploading...\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"No posts found.\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"Save Changes\"],\"IagCbF\":[\"URL\"],\"J4FNfC\":[\"No posts in this collection.\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"Need help? Visit the <0>documentation</0>\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"Use this URL to embed the media in your posts.\"],\"KbS2K9\":[\"Reset Password\"],\"KiJn9B\":[\"Note\"],\"KmGXnO\":[\"Are you sure you want to delete this post? This cannot be undone.\"],\"L85WcV\":[\"Slug\"],\"LdyooL\":[\"link\"],\"LkvLQe\":[\"No pages yet.\"],\"M1RvTd\":[\"Click image to view full size\"],\"M2kIWU\":[\"Font theme\"],\"M8kJqa\":[\"Drafts\"],\"M9xgHy\":[\"Redirects\"],\"MHrjPM\":[\"Title\"],\"MLSRl9\":[\"Quote Text\"],\"MWBOxm\":[\"Collections (optional)\"],\"MZbQHL\":[\"No results found.\"],\"Mhf/H/\":[\"Create Redirect\"],\"MnbH31\":[\"page\"],\"MqghUt\":[\"Search posts...\"],\"N0APCr\":[\"This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.\"],\"N40H+G\":[\"All\"],\"NU2Fqi\":[\"Save CSS\"],\"O3oNi5\":[\"Email\"],\"OCNZaU\":[\"The path to redirect from\"],\"ODiSoW\":[\"No posts yet.\"],\"ONWvwQ\":[\"Upload\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pbm2/N\":[\"Create Collection\"],\"QEbNBb\":[\"Path (e.g. /archive) or full URL (e.g. https://example.com)\"],\"QLkhbH\":[\"The text being quoted...\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"Links\"],\"RwGhWy\":[\"Thread with \",[\"count\"],\" posts\"],\"RxsRD6\":[\"Time Zone\"],\"SJmfuf\":[\"Site Name\"],\"ST+lN2\":[\"No media uploaded yet.\"],\"TNFigk\":[\"Default Homepage View\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"Settings\"],\"U5v6Gh\":[\"Edit Page\"],\"UDMjsP\":[\"Quick Actions\"],\"UGT5vp\":[\"Save Settings\"],\"Ui5/i3\":[\"It's OK for search engines to index my site\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UxKoFf\":[\"Navigation\"],\"V4WsyL\":[\"Add Link\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VhMDMg\":[\"Change Password\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"General\"],\"WmZ/rP\":[\"To Path\"],\"Y+7JGK\":[\"Create Page\"],\"Y75ho6\":[\"Other pages\"],\"Z3FXyt\":[\"Loading...\"],\"ZQKLI1\":[\"Danger Zone\"],\"ZUpE9/\":[\"This is displayed at the bottom of all of your posts and pages. Markdown is supported.\"],\"ZhhOwV\":[\"Quote\"],\"aAIQg2\":[\"Appearance\"],\"aaGV/9\":[\"New Link\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"About this blog\"],\"b+/jO6\":[\"301 (Permanent)\"],\"bHYIks\":[\"Sign Out\"],\"biOepV\":[\"← Back to home\"],\"cnGeoo\":[\"Delete\"],\"dEgA5A\":[\"Cancel\"],\"dmCcPs\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square image at least 180x180 pixels.\"],\"e6Jr7Q\":[\"← Back to Collections\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"eneWvv\":[\"Draft\"],\"er8+x7\":[\"Demo account pre-filled. Just click Sign In.\"],\"f6e0Ry\":[\"Article\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"My Collection\"],\"g3mKmM\":[\"Un-nav\"],\"gDx5MG\":[\"Edit Link\"],\"hG89Ed\":[\"Image\"],\"hWOZIv\":[\"Enter your new password.\"],\"hXzOVo\":[\"Next\"],\"he3ygx\":[\"Copy\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"URL (optional)\"],\"iH8pgl\":[\"Back\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"Published pages are accessible via their slug. Drafts are not visible.\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jpctdh\":[\"View\"],\"jt/Ow/\":[\"posts\"],\"k1ifdL\":[\"Processing...\"],\"kI1qVD\":[\"Format\"],\"kNiQp6\":[\"Pinned\"],\"kd7eBB\":[\"Create Link\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"Sign In\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oYPBa0\":[\"Update Page\"],\"p2/GCq\":[\"Confirm Password\"],\"pRhYH2\":[\"Posts in Collection (\",[\"count\"],\")\"],\"pZq3aX\":[\"Upload failed. Please try again.\"],\"q+hNag\":[\"Collection\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qiXmlF\":[\"Add Media\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"Color theme\"],\"rdUucN\":[\"Preview\"],\"rzNUSl\":[\"Thread with 1 post\"],\"sGajR7\":[\"Thread start\"],\"smzF8S\":[\"Show \",[\"remainingCount\"],\" more \",[\"0\"]],\"ssqvZi\":[\"Save Profile\"],\"t/YqKh\":[\"Remove\"],\"tfDRzk\":[\"Save\"],\"tfrt7B\":[\"No redirects configured.\"],\"tiq7kl\":[\"Page \",[\"page\"]],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"Published\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"Status\"],\"vERlcd\":[\"Profile\"],\"vXIe7J\":[\"Language\"],\"vh0C9b\":[\"No navigation links yet. Add pages to navigation or create links.\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vzU4k9\":[\"New Collection\"],\"wEF6Ix\":[\"The destination path or URL\"],\"wK4OTM\":[\"Title (optional)\"],\"wL3cK8\":[\"Latest\"],\"wM5UXj\":[\"Delete Media\"],\"wRR604\":[\"Pages\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"Untitled\"],\"x+doid\":[\"Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.\"],\"x0mzE0\":[\"Create your first post\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"Media\"],\"y28hnO\":[\"Post\"],\"yQ2kGp\":[\"Load more\"],\"yjkELF\":[\"Confirm New Password\"],\"yzF66j\":[\"Link\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"Found 1 result\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"]}");
@@ -1 +0,0 @@
1
- /*eslint-disable*/ export const messages = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"Add to nav\"],\"+bHzpy\":[\"链接的显示文本\"],\"+owNNn\":[\"帖子\"],\"+zy2Nq\":[\"类型\"],\"/0D1Xp\":[\"编辑集合\"],\"/Rj5P4\":[\"您的姓名\"],\"/rkqRV\":[\"All pages are in your navigation.\"],\"07Epll\":[\"这将为您的网站和仪表板设置主题。所有颜色主题都支持暗黑模式。\"],\"0JkyS7\":[\"创建您的第一页\"],\"0a6MpL\":[\"新重定向\"],\"0yIy82\":[\"No featured posts yet.\"],\"1CU1Td\":[\"URL安全标识符(小写字母、数字、连字符)\"],\"1DBGsz\":[\"笔记\"],\"1o+wgo\":[\"例如:The Verge,John Doe\"],\"2N0qpv\":[\"帖子标题...\"],\"2cFU6q\":[\"Site Footer\"],\"2fUwEY\":[\"选择媒体\"],\"2q/Q7x\":[\"可见性\"],\"2rJGtU\":[\"页面标题...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Yvsaz\":[\"302(临时)\"],\"4/SFQS\":[\"查看网站\"],\"40TVQj\":[\"自定义路径(可选)\"],\"4KzVT6\":[\"删除页面\"],\"4Ml90q\":[\"SEO\"],\"4b3oEV\":[\"内容\"],\"4mDPGp\":[\"此页面的 URL 路径。使用小写字母、数字和连字符。\"],\"6WdDG7\":[\"页面\"],\"6YtxFj\":[\"姓名\"],\"7G4SBz\":[\"页面内容(支持Markdown)...\"],\"7Mk+/h\":[\"更新收藏夹\"],\"7Q1KKN\":[\"来源路径\"],\"7aECQB\":[\"无效或过期的链接\"],\"7nGhhM\":[\"你在想什么?\"],\"7p5kLi\":[\"仪表板\"],\"7vhWI8\":[\"新密码\"],\"87a/t/\":[\"标签\"],\"8ZsakT\":[\"密码\"],\"9+vGLh\":[\"Custom CSS\"],\"90Luob\":[[\"count\"],\" 条回复\"],\"A1taO8\":[\"搜索\"],\"AeXO77\":[\"账户\"],\"AyHO4m\":[\"这个系列是关于什么的?\"],\"B373X+\":[\"编辑帖子\"],\"B495Gs\":[\"档案馆\"],\"BjF0Jv\":[\"仅允许小写字母、数字和连字符\"],\"D9Oea+\":[\"永久链接\"],\"DCKkhU\":[\"当前密码\"],\"DHhJ7s\":[\"上一页\"],\"DPfwMq\":[\"完成\"],\"DoJzLz\":[\"收藏夹\"],\"E80cJw\":[\"删除此媒体将永久从存储中移除。\"],\"EEYbdt\":[\"发布\"],\"EGwzOK\":[\"完成设置\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"更新\"],\"FGrimz\":[\"新帖子\"],\"FkMol5\":[\"精选\"],\"Fxf4jq\":[\"描述(可选)\"],\"GA5A5H\":[\"删除收藏夹\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"Your site navigation\"],\"GX2VMa\":[\"创建您的管理员账户。\"],\"GbVAnd\":[\"此密码重置链接无效或已过期。请生成一个新的链接。\"],\"GorKul\":[\"欢迎来到Jant\"],\"GrZ6fH\":[\"新页面\"],\"GxkJXS\":[\"上传中...\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"安静(正常)\"],\"Hzi9AA\":[\"未找到帖子。\"],\"I6gXOa\":[\"路径\"],\"I8hDlV\":[\"图像帖子至少需要 1 张图片。\"],\"IUwGEM\":[\"保存更改\"],\"IagCbF\":[\"网址\"],\"J4FNfC\":[\"此集合中没有帖子。\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"需要帮助吗?访问<0>文档</0>。\"],\"JiP4aa\":[\"已发布的页面可以通过其路径访问。草稿不可见。\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"使用此 URL 将媒体嵌入到您的帖子中。\"],\"KbS2K9\":[\"重置密码\"],\"KiJn9B\":[\"注意\"],\"KmGXnO\":[\"Are you sure you want to delete this post? This cannot be undone.\"],\"L85WcV\":[\"缩略名\"],\"LdyooL\":[\"link\"],\"LkvLQe\":[\"还没有页面。\"],\"M1RvTd\":[\"点击图片查看完整尺寸\"],\"M2kIWU\":[\"Font theme\"],\"M8kJqa\":[\"草稿\"],\"M9xgHy\":[\"重定向\"],\"MHrjPM\":[\"标题\"],\"MLSRl9\":[\"Quote Text\"],\"MWBOxm\":[\"集合(可选)\"],\"MZbQHL\":[\"未找到结果。\"],\"Mhf/H/\":[\"创建重定向\"],\"MnbH31\":[\"page\"],\"MqghUt\":[\"搜索帖子...\"],\"N0APCr\":[\"This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.\"],\"N40H+G\":[\"所有\"],\"NU2Fqi\":[\"Save CSS\"],\"O3oNi5\":[\"电子邮件\"],\"OCNZaU\":[\"重定向的路径\"],\"ODiSoW\":[\"还没有帖子。\"],\"ONWvwQ\":[\"上传\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pbm2/N\":[\"创建集合\"],\"QEbNBb\":[\"路径(例如 /archive)或完整 URL(例如 https://example.com)\"],\"QLkhbH\":[\"The text being quoted...\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"链接\"],\"RwGhWy\":[\"包含 \",[\"count\"],\" 条帖子的话题\"],\"RxsRD6\":[\"Time Zone\"],\"SJmfuf\":[\"网站名称\"],\"ST+lN2\":[\"尚未上传任何媒体。\"],\"TNFigk\":[\"Default Homepage View\"],\"Tt5T6+\":[\"文章\"],\"TxE+Mj\":[\"1 条回复\"],\"Tz0i8g\":[\"设置\"],\"U5v6Gh\":[\"编辑页面\"],\"UDMjsP\":[\"快速操作\"],\"UGT5vp\":[\"保存设置\"],\"Ui5/i3\":[\"It's OK for search engines to index my site\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UxKoFf\":[\"导航\"],\"V4WsyL\":[\"Add Link\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VhMDMg\":[\"更改密码\"],\"WDcQq9\":[\"未列出\"],\"Weq9zb\":[\"常规\"],\"WmZ/rP\":[\"到路径\"],\"Y+7JGK\":[\"创建页面\"],\"Y75ho6\":[\"Other pages\"],\"Z3FXyt\":[\"加载中...\"],\"ZQKLI1\":[\"危险区域\"],\"ZUpE9/\":[\"This is displayed at the bottom of all of your posts and pages. Markdown is supported.\"],\"ZhhOwV\":[\"引用\"],\"aAIQg2\":[\"外观\"],\"aaGV/9\":[\"新链接\"],\"an5hVd\":[\"图片\"],\"anibOb\":[\"About this blog\"],\"b+/jO6\":[\"301(永久)\"],\"bHYIks\":[\"登出\"],\"biOepV\":[\"← Back to home\"],\"cnGeoo\":[\"删除\"],\"dEgA5A\":[\"取消\"],\"dmCcPs\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square image at least 180x180 pixels.\"],\"e6Jr7Q\":[\"← 返回收藏夹\"],\"ePK91l\":[\"编辑\"],\"eWLklq\":[\"引用\"],\"eneWvv\":[\"草稿\"],\"er8+x7\":[\"演示账户已预填。只需点击登录。\"],\"f6e0Ry\":[\"文章\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"我的收藏\"],\"g3mKmM\":[\"Un-nav\"],\"gDx5MG\":[\"编辑链接\"],\"hG89Ed\":[\"图像\"],\"hWOZIv\":[\"输入您的新密码。\"],\"hXzOVo\":[\"下一页\"],\"he3ygx\":[\"复制\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"URL (optional)\"],\"iH8pgl\":[\"返回\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"Published pages are accessible via their slug. Drafts are not visible.\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jpctdh\":[\"查看\"],\"jt/Ow/\":[\"posts\"],\"k1ifdL\":[\"处理中...\"],\"kI1qVD\":[\"Format\"],\"kNiQp6\":[\"Pinned\"],\"kd7eBB\":[\"创建链接\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"登录\"],\"oJFOZk\":[\"来源名称(可选)\"],\"oYPBa0\":[\"更新页面\"],\"p2/GCq\":[\"确认密码\"],\"pRhYH2\":[\"集合中的帖子 (\",[\"count\"],\")\"],\"pZq3aX\":[\"上传失败。请再试一次。\"],\"q+hNag\":[\"Collection\"],\"qMyM2u\":[\"源网址(可选)\"],\"qiXmlF\":[\"添加媒体\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"安静\"],\"rFmBG3\":[\"颜色主题\"],\"rdUucN\":[\"预览\"],\"rzNUSl\":[\"包含 1 条帖子的话题\"],\"sGajR7\":[\"线程开始\"],\"smzF8S\":[\"显示 \",[\"remainingCount\"],\" 个更多 \",[\"0\"]],\"ssqvZi\":[\"保存个人资料\"],\"t/YqKh\":[\"移除\"],\"tfDRzk\":[\"Save\"],\"tfrt7B\":[\"未配置重定向。\"],\"tiq7kl\":[\"页面 \",[\"page\"]],\"u2f7vd\":[\"网站描述\"],\"u3wRF+\":[\"已发布\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"状态\"],\"vERlcd\":[\"个人资料\"],\"vXIe7J\":[\"语言\"],\"vh0C9b\":[\"No navigation links yet. Add pages to navigation or create links.\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vzU4k9\":[\"新收藏\"],\"wEF6Ix\":[\"目标路径或 URL\"],\"wK4OTM\":[\"标题(可选)\"],\"wL3cK8\":[\"Latest\"],\"wM5UXj\":[\"删除媒体\"],\"wRR604\":[\"页面\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wdGjkd\":[\"未配置导航链接。\"],\"wja8aL\":[\"无标题\"],\"x+doid\":[\"图像会自动优化:调整大小至最大 1920px,转换为 WebP,并去除元数据。\"],\"x0mzE0\":[\"创建你的第一篇帖子\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"媒体\"],\"y28hnO\":[\"帖子\"],\"yQ2kGp\":[\"加载更多\"],\"yjkELF\":[\"确认新密码\"],\"yzF66j\":[\"链接\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"找到 1 个结果\"],\"zH6KqE\":[\"找到 \",[\"count\"],\" 个结果\"]}");
@@ -1 +0,0 @@
1
- /*eslint-disable*/ export const messages = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"Add to nav\"],\"+bHzpy\":[\"顯示連結的文字\"],\"+owNNn\":[\"文章\"],\"+zy2Nq\":[\"類型\"],\"/0D1Xp\":[\"編輯收藏集\"],\"/Rj5P4\":[\"您的姓名\"],\"/rkqRV\":[\"All pages are in your navigation.\"],\"07Epll\":[\"這將為您的網站和儀表板設置主題。所有顏色主題都支持深色模式。\"],\"0JkyS7\":[\"建立您的第一個頁面\"],\"0a6MpL\":[\"新重定向\"],\"0yIy82\":[\"No featured posts yet.\"],\"1CU1Td\":[\"網址安全識別碼(小寫、數字、連字符)\"],\"1DBGsz\":[\"筆記\"],\"1o+wgo\":[\"例如:The Verge,約翰·多伊\"],\"2N0qpv\":[\"文章標題...\"],\"2cFU6q\":[\"Site Footer\"],\"2fUwEY\":[\"選擇媒體\"],\"2q/Q7x\":[\"可見性\"],\"2rJGtU\":[\"頁面標題...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Yvsaz\":[\"302(臨時)\"],\"4/SFQS\":[\"查看網站\"],\"40TVQj\":[\"自訂路徑(選填)\"],\"4KzVT6\":[\"刪除頁面\"],\"4Ml90q\":[\"SEO\"],\"4b3oEV\":[\"內容\"],\"4mDPGp\":[\"此頁面的 URL 路徑。使用小寫字母、數字和連字符。\"],\"6WdDG7\":[\"頁面\"],\"6YtxFj\":[\"名稱\"],\"7G4SBz\":[\"頁面內容(支持Markdown)...\"],\"7Mk+/h\":[\"更新收藏集\"],\"7Q1KKN\":[\"來源路徑\"],\"7aECQB\":[\"無效或已過期的連結\"],\"7nGhhM\":[\"你在想什麼?\"],\"7p5kLi\":[\"儀表板\"],\"7vhWI8\":[\"新密碼\"],\"87a/t/\":[\"標籤\"],\"8ZsakT\":[\"密碼\"],\"9+vGLh\":[\"Custom CSS\"],\"90Luob\":[[\"count\"],\" 條回覆\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AyHO4m\":[\"這個收藏是關於什麼的?\"],\"B373X+\":[\"編輯文章\"],\"B495Gs\":[\"檔案館\"],\"BjF0Jv\":[\"僅限小寫字母、數字和連字符\"],\"D9Oea+\":[\"永久鏈接\"],\"DCKkhU\":[\"當前密碼\"],\"DHhJ7s\":[\"上一頁\"],\"DPfwMq\":[\"完成\"],\"DoJzLz\":[\"收藏夾\"],\"E80cJw\":[\"刪除此媒體將永久從存儲中移除它。\"],\"EEYbdt\":[\"發佈\"],\"EGwzOK\":[\"完成設置\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"更新\"],\"FGrimz\":[\"新帖子\"],\"FkMol5\":[\"精選\"],\"Fxf4jq\":[\"描述(可選)\"],\"GA5A5H\":[\"刪除收藏夾\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"Your site navigation\"],\"GX2VMa\":[\"建立您的管理員帳戶。\"],\"GbVAnd\":[\"此密碼重設連結無效或已過期。請生成一個新的連結。\"],\"GorKul\":[\"歡迎來到 Jant\"],\"GrZ6fH\":[\"新頁面\"],\"GxkJXS\":[\"上傳中...\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"安靜(正常)\"],\"Hzi9AA\":[\"未找到任何帖子。\"],\"I6gXOa\":[\"路徑\"],\"I8hDlV\":[\"至少需要 1 張圖片才能發佈圖片帖子。\"],\"IUwGEM\":[\"保存更改\"],\"IagCbF\":[\"網址\"],\"J4FNfC\":[\"此集合中沒有帖子。\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"需要幫助嗎?請訪問<0>文檔</0>。\"],\"JiP4aa\":[\"已發佈的頁面可以通過其路徑訪問。草稿不可見。\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"使用此 URL 將媒體嵌入到您的帖子中。\"],\"KbS2K9\":[\"重設密碼\"],\"KiJn9B\":[\"備註\"],\"KmGXnO\":[\"Are you sure you want to delete this post? This cannot be undone.\"],\"L85WcV\":[\"縮略名\"],\"LdyooL\":[\"link\"],\"LkvLQe\":[\"尚未有頁面。\"],\"M1RvTd\":[\"點擊圖片以查看完整大小\"],\"M2kIWU\":[\"Font theme\"],\"M8kJqa\":[\"草稿\"],\"M9xgHy\":[\"重定向\"],\"MHrjPM\":[\"標題\"],\"MLSRl9\":[\"Quote Text\"],\"MWBOxm\":[\"收藏(可選)\"],\"MZbQHL\":[\"未找到結果。\"],\"Mhf/H/\":[\"建立重定向\"],\"MnbH31\":[\"page\"],\"MqghUt\":[\"搜尋帖子...\"],\"N0APCr\":[\"This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.\"],\"N40H+G\":[\"所有\"],\"NU2Fqi\":[\"Save CSS\"],\"O3oNi5\":[\"電子郵件\"],\"OCNZaU\":[\"重定向來源的路徑\"],\"ODiSoW\":[\"尚未有帖子。\"],\"ONWvwQ\":[\"上傳\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pbm2/N\":[\"創建收藏夾\"],\"QEbNBb\":[\"路徑(例如 /archive)或完整網址(例如 https://example.com)\"],\"QLkhbH\":[\"The text being quoted...\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"連結\"],\"RwGhWy\":[\"包含 \",[\"count\"],\" 則帖子的主題\"],\"RxsRD6\":[\"Time Zone\"],\"SJmfuf\":[\"網站名稱\"],\"ST+lN2\":[\"尚未上傳任何媒體。\"],\"TNFigk\":[\"Default Homepage View\"],\"Tt5T6+\":[\"文章\"],\"TxE+Mj\":[\"1 條回覆\"],\"Tz0i8g\":[\"設定\"],\"U5v6Gh\":[\"編輯頁面\"],\"UDMjsP\":[\"快速操作\"],\"UGT5vp\":[\"保存設定\"],\"Ui5/i3\":[\"It's OK for search engines to index my site\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UxKoFf\":[\"導航\"],\"V4WsyL\":[\"Add Link\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VhMDMg\":[\"更改密碼\"],\"WDcQq9\":[\"不公開\"],\"Weq9zb\":[\"一般設定\"],\"WmZ/rP\":[\"到路徑\"],\"Y+7JGK\":[\"創建頁面\"],\"Y75ho6\":[\"Other pages\"],\"Z3FXyt\":[\"載入中...\"],\"ZQKLI1\":[\"危險區域\"],\"ZUpE9/\":[\"This is displayed at the bottom of all of your posts and pages. Markdown is supported.\"],\"ZhhOwV\":[\"引用\"],\"aAIQg2\":[\"外觀\"],\"aaGV/9\":[\"新連結\"],\"an5hVd\":[\"圖片\"],\"anibOb\":[\"About this blog\"],\"b+/jO6\":[\"301(永久)\"],\"bHYIks\":[\"登出\"],\"biOepV\":[\"← Back to home\"],\"cnGeoo\":[\"刪除\"],\"dEgA5A\":[\"取消\"],\"dmCcPs\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square image at least 180x180 pixels.\"],\"e6Jr7Q\":[\"← 返回收藏夾\"],\"ePK91l\":[\"編輯\"],\"eWLklq\":[\"引用\"],\"eneWvv\":[\"草稿\"],\"er8+x7\":[\"示範帳戶已預填。只需點擊登入。\"],\"f6e0Ry\":[\"文章\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"我的收藏\"],\"g3mKmM\":[\"Un-nav\"],\"gDx5MG\":[\"編輯連結\"],\"hG89Ed\":[\"圖片\"],\"hWOZIv\":[\"請輸入您的新密碼。\"],\"hXzOVo\":[\"下一頁\"],\"he3ygx\":[\"複製\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"URL (optional)\"],\"iH8pgl\":[\"返回\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"Published pages are accessible via their slug. Drafts are not visible.\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jpctdh\":[\"查看\"],\"jt/Ow/\":[\"posts\"],\"k1ifdL\":[\"處理中...\"],\"kI1qVD\":[\"Format\"],\"kNiQp6\":[\"Pinned\"],\"kd7eBB\":[\"建立連結\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"登入\"],\"oJFOZk\":[\"來源名稱(選填)\"],\"oYPBa0\":[\"更新頁面\"],\"p2/GCq\":[\"確認密碼\"],\"pRhYH2\":[\"收藏中的帖子 (\",[\"count\"],\")\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"q+hNag\":[\"Collection\"],\"qMyM2u\":[\"來源網址(選填)\"],\"qiXmlF\":[\"添加媒體\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"安靜\"],\"rFmBG3\":[\"顏色主題\"],\"rdUucN\":[\"預覽\"],\"rzNUSl\":[\"包含 1 則貼文的主題\"],\"sGajR7\":[\"線程開始\"],\"smzF8S\":[\"顯示 \",[\"remainingCount\"],\" 個更多 \",[\"0\"]],\"ssqvZi\":[\"保存個人資料\"],\"t/YqKh\":[\"移除\"],\"tfDRzk\":[\"Save\"],\"tfrt7B\":[\"未配置任何重定向。\"],\"tiq7kl\":[\"頁面 \",[\"page\"]],\"u2f7vd\":[\"網站描述\"],\"u3wRF+\":[\"已發佈\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"狀態\"],\"vERlcd\":[\"個人資料\"],\"vXIe7J\":[\"語言\"],\"vh0C9b\":[\"No navigation links yet. Add pages to navigation or create links.\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vzU4k9\":[\"新收藏集\"],\"wEF6Ix\":[\"目的地路徑或 URL\"],\"wK4OTM\":[\"標題(選填)\"],\"wL3cK8\":[\"Latest\"],\"wM5UXj\":[\"刪除媒體\"],\"wRR604\":[\"頁面\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wdGjkd\":[\"未配置導航連結。\"],\"wja8aL\":[\"無標題\"],\"x+doid\":[\"圖片會自動優化:調整大小至最大 1920 像素,轉換為 WebP 格式,並去除元數據。\"],\"x0mzE0\":[\"創建你的第一篇帖子\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"媒體\"],\"y28hnO\":[\"文章\"],\"yQ2kGp\":[\"載入更多\"],\"yjkELF\":[\"確認新密碼\"],\"yzF66j\":[\"連結\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"找到 1 個結果\"],\"zH6KqE\":[\"找到 \",[\"count\"],\" 個結果\"]}");
@@ -1,13 +0,0 @@
1
- /**
2
- * Centralized locale configuration
3
- */ export const locales = [
4
- "en",
5
- "zh-Hans",
6
- "zh-Hant"
7
- ];
8
- export const baseLocale = "en";
9
- /**
10
- * Check if a value is a valid locale
11
- */ export function isLocale(value) {
12
- return typeof value === "string" && locales.includes(value);
13
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * i18n Hono Middleware
3
- */ import { createI18n, isLocale, baseLocale } from "./i18n.js";
4
- /**
5
- * Hono middleware for internationalization.
6
- * Creates a per-request i18n instance to avoid race conditions in concurrent environments.
7
- *
8
- * Language is determined by the database SITE_LANGUAGE setting (single source of truth).
9
- * Falls back to the default locale (en) if not set.
10
- */ export function i18nMiddleware() {
11
- return async (c, next)=>{
12
- let lang = baseLocale;
13
- const services = c.get("services");
14
- if (services) {
15
- try {
16
- const siteLang = await services.settings.get("SITE_LANGUAGE");
17
- if (siteLang && isLocale(siteLang)) {
18
- lang = siteLang;
19
- }
20
- } catch {
21
- // Ignore errors, fall back to default locale
22
- }
23
- }
24
- // Create a new i18n instance for this request to avoid race conditions
25
- const i18n = createI18n(lang);
26
- c.set("lang", lang);
27
- c.set("i18n", i18n);
28
- await next();
29
- };
30
- }
@@ -1,134 +0,0 @@
1
- /**
2
- * Client-side Avatar Upload Handler
3
- *
4
- * Intercepts avatar file selection to generate favicon variants
5
- * before uploading. Generates:
6
- * - favicon.ico (ICO containing 16x16 + 32x32 PNGs)
7
- * - apple-touch-icon.png (180x180 PNG)
8
- *
9
- * Uses the `[data-avatar-upload]` attribute on file inputs.
10
- */ import { encodeIco } from "./favicon.js";
11
- /**
12
- * Load an image from a File object
13
- */ function loadImage(file) {
14
- return new Promise((resolve, reject)=>{
15
- const img = new Image();
16
- img.onload = ()=>{
17
- URL.revokeObjectURL(img.src);
18
- resolve(img);
19
- };
20
- img.onerror = ()=>reject(new Error("Failed to load image"));
21
- img.src = URL.createObjectURL(file);
22
- });
23
- }
24
- /**
25
- * Resize image to a square PNG using center crop.
26
- *
27
- * @param img - Source HTMLImageElement
28
- * @param size - Target width and height in pixels
29
- * @returns PNG Blob at the target size
30
- */ function resizeToSquarePng(img, size) {
31
- const canvas = document.createElement("canvas");
32
- canvas.width = size;
33
- canvas.height = size;
34
- const ctx = canvas.getContext("2d");
35
- if (!ctx) throw new Error("Failed to get canvas context");
36
- // Cover crop: scale to fill square, crop center
37
- const scale = Math.max(size / img.width, size / img.height);
38
- const sw = size / scale;
39
- const sh = size / scale;
40
- const sx = (img.width - sw) / 2;
41
- const sy = (img.height - sh) / 2;
42
- ctx.drawImage(img, sx, sy, sw, sh, 0, 0, size, size);
43
- return new Promise((resolve, reject)=>{
44
- canvas.toBlob((blob)=>{
45
- if (blob) resolve(blob);
46
- else reject(new Error("Failed to create PNG blob"));
47
- }, "image/png");
48
- });
49
- }
50
- /**
51
- * Process avatar file and upload with favicon variants.
52
- *
53
- * @param input - The file input element with `data-avatar-upload` attribute
54
- * @param file - The selected file
55
- */ async function handleAvatarUpload(input, file) {
56
- // Find the parent form for the loading button
57
- const form = input.closest("form");
58
- const label = form?.querySelector("label");
59
- const originalText = label?.textContent ?? "";
60
- try {
61
- // Show processing state
62
- if (label) label.textContent = input.dataset.textProcessing || "Processing...";
63
- // Load the image
64
- const img = await loadImage(file);
65
- // Generate variants in parallel
66
- const [png16, png32, png180] = await Promise.all([
67
- resizeToSquarePng(img, 16),
68
- resizeToSquarePng(img, 32),
69
- resizeToSquarePng(img, 180)
70
- ]);
71
- // Encode ICO with 16x16 and 32x32
72
- const [png16Buf, png32Buf] = await Promise.all([
73
- png16.arrayBuffer(),
74
- png32.arrayBuffer()
75
- ]);
76
- const icoBlob = encodeIco([
77
- {
78
- size: 16,
79
- png: png16Buf
80
- },
81
- {
82
- size: 32,
83
- png: png32Buf
84
- }
85
- ]);
86
- // Show uploading state
87
- if (label) label.textContent = input.dataset.textUploading || "Uploading...";
88
- // Build FormData with original + variants
89
- const formData = new FormData();
90
- formData.append("file", file);
91
- formData.append("favicon", icoBlob, "favicon.ico");
92
- formData.append("appleTouch", png180, "apple-touch-icon.png");
93
- // Upload
94
- const response = await fetch("/dash/settings/avatar", {
95
- method: "POST",
96
- body: formData
97
- });
98
- if (!response.ok) {
99
- throw new Error("Upload failed");
100
- }
101
- // Redirect on success
102
- window.location.href = "/dash/settings?saved";
103
- } catch {
104
- // Restore button text on error
105
- if (label) label.textContent = originalText;
106
- // Show error toast
107
- const errorMsg = input.dataset.textError || "Upload failed. Please try again.";
108
- const container = document.getElementById("toast-container");
109
- if (container) {
110
- const toast = document.createElement("div");
111
- toast.className = "toast toast-error";
112
- toast.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6M9 9l6 6"/></svg><span>${errorMsg}</span>`;
113
- container.appendChild(toast);
114
- setTimeout(()=>{
115
- toast.classList.add("toast-out");
116
- toast.addEventListener("animationend", ()=>toast.remove());
117
- }, 3000);
118
- }
119
- }
120
- // Reset file input so the same file can be re-selected
121
- input.value = "";
122
- }
123
- /**
124
- * Initialize avatar upload via event delegation
125
- */ function initAvatarUpload() {
126
- document.addEventListener("change", (e)=>{
127
- const input = e.target.closest("[data-avatar-upload]");
128
- if (!input?.files?.[0]) return;
129
- // Prevent default form submission (Datastar data-on:change)
130
- e.stopPropagation();
131
- handleAvatarUpload(input, input.files[0]);
132
- });
133
- }
134
- initAvatarUpload();
@@ -1,143 +0,0 @@
1
- /**
2
- * Unified Configuration System
3
- *
4
- * Provides a flexible configuration system with two priority modes:
5
- * - User-configurable (envOnly: false): Database > Environment > Default
6
- * - Environment-only (envOnly: true): Environment > Default
7
- *
8
- * All configuration fields are defined in CONFIG_FIELDS (types.ts).
9
- */ import { CONFIG_FIELDS } from "../types.js";
10
- /**
11
- * Get the fallback value for a config key (ENV > Default), skipping the database.
12
- * Used for placeholder values in forms where the DB value is shown separately.
13
- *
14
- * @param c - Hono context
15
- * @param key - Configuration key from CONFIG_FIELDS
16
- * @returns Fallback value from environment or default
17
- *
18
- * @example
19
- * ```typescript
20
- * const placeholder = getConfigFallback(c, "SITE_NAME");
21
- * // Returns: c.env.SITE_NAME ?? "Jant"
22
- * ```
23
- */ export function getConfigFallback(c, key) {
24
- const field = CONFIG_FIELDS[key];
25
- const envValue = c.env[key];
26
- if (envValue && typeof envValue === "string") return envValue;
27
- return field.defaultValue;
28
- }
29
- /**
30
- * Generic configuration getter that respects priority settings
31
- *
32
- * @param c - Hono context
33
- * @param key - Configuration key from CONFIG_FIELDS
34
- * @returns Configuration value following the defined priority
35
- *
36
- * @example
37
- * ```typescript
38
- * // For user-configurable configs (SITE_NAME):
39
- * // Returns: (DB: SITE_NAME) ?? c.env.SITE_NAME ?? "Jant"
40
- *
41
- * // For environment-only configs (SITE_URL):
42
- * // Returns: c.env.SITE_URL ?? ""
43
- * ```
44
- */ export async function getConfig(c, key) {
45
- const field = CONFIG_FIELDS[key];
46
- if (!field.envOnly) {
47
- // User-configurable: DB > ENV > Default
48
- // 1. Check database setting first
49
- const dbValue = await c.var.services.settings.get(key);
50
- if (dbValue) {
51
- return dbValue;
52
- }
53
- }
54
- // ENV > Default
55
- // 2. Check environment variable
56
- const envValue = c.env[key];
57
- if (envValue && typeof envValue === "string") {
58
- return envValue;
59
- }
60
- // 3. Default value
61
- return field.defaultValue;
62
- }
63
- /**
64
- * Get site name with fallback chain: DB > ENV > Default
65
- *
66
- * @param c - Hono context
67
- * @returns Site name
68
- *
69
- * @example
70
- * ```typescript
71
- * const siteName = await getSiteName(c);
72
- * // Returns: (DB: SITE_NAME) ?? c.env.SITE_NAME ?? "Jant"
73
- * ```
74
- */ export async function getSiteName(c) {
75
- return getConfig(c, "SITE_NAME");
76
- }
77
- /**
78
- * Get site description with fallback chain: DB > ENV > Default
79
- *
80
- * @param c - Hono context
81
- * @returns Site description
82
- *
83
- * @example
84
- * ```typescript
85
- * const description = await getSiteDescription(c);
86
- * // Returns: (DB: SITE_DESCRIPTION) ?? c.env.SITE_DESCRIPTION ?? "A microblog powered by Jant"
87
- * ```
88
- */ export async function getSiteDescription(c) {
89
- return getConfig(c, "SITE_DESCRIPTION");
90
- }
91
- /**
92
- * Get site language with fallback chain: DB > ENV > Default
93
- *
94
- * @param c - Hono context
95
- * @returns Site language code
96
- *
97
- * @example
98
- * ```typescript
99
- * const lang = await getSiteLanguage(c);
100
- * // Returns: (DB: SITE_LANGUAGE) ?? c.env.SITE_LANGUAGE ?? "en"
101
- * ```
102
- */ export async function getSiteLanguage(c) {
103
- return getConfig(c, "SITE_LANGUAGE");
104
- }
105
- /**
106
- * Get home default view with fallback chain: DB > ENV > Default
107
- *
108
- * @param c - Hono context
109
- * @returns Home default view ("latest" or "featured")
110
- *
111
- * @example
112
- * ```typescript
113
- * const view = await getHomeDefaultView(c);
114
- * // Returns: (DB: HOME_DEFAULT_VIEW) ?? "latest"
115
- * ```
116
- */ export async function getHomeDefaultView(c) {
117
- return getConfig(c, "HOME_DEFAULT_VIEW");
118
- }
119
- /**
120
- * Get timezone with fallback chain: DB > ENV > Default
121
- *
122
- * @param c - Hono context
123
- * @returns Timezone string (e.g. "Beijing", "UTC")
124
- */ export async function getTimeZone(c) {
125
- return getConfig(c, "TIME_ZONE");
126
- }
127
- /**
128
- * Get site footer markdown with fallback chain: DB > ENV > Default
129
- *
130
- * @param c - Hono context
131
- * @returns Footer markdown string (empty string if not set)
132
- */ export async function getSiteFooter(c) {
133
- return getConfig(c, "SITE_FOOTER");
134
- }
135
- /**
136
- * Check if search engine indexing is disabled
137
- *
138
- * @param c - Hono context
139
- * @returns true if NOINDEX is set to "true"
140
- */ export async function isNoIndex(c) {
141
- const value = await getConfig(c, "NOINDEX");
142
- return value === "true";
143
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * Application Constants
3
- */ /**
4
- * Reserved URL paths that cannot be used for pages
5
- */ export const RESERVED_PATHS = [
6
- "featured",
7
- "latest",
8
- "collections",
9
- "signin",
10
- "signout",
11
- "setup",
12
- "dash",
13
- "api",
14
- "feed",
15
- "search",
16
- "archive",
17
- "media",
18
- "pages",
19
- "reset",
20
- "p",
21
- "c",
22
- "static",
23
- "assets",
24
- "health"
25
- ];
26
- /**
27
- * Check if a path is reserved
28
- */ export function isReservedPath(path) {
29
- const firstSegment = path.split("/")[0]?.toLowerCase();
30
- return RESERVED_PATHS.includes(firstSegment);
31
- }
32
- /**
33
- * Default pagination size
34
- */ export const DEFAULT_PAGE_SIZE = 100;
35
- /**
36
- * Settings keys - derived from CONFIG_FIELDS (Single Source of Truth)
37
- *
38
- * Only non-envOnly fields and internal fields are stored in DB settings.
39
- * Environment-only fields (SITE_URL, AUTH_SECRET, etc.) are never in the DB.
40
- */ import { CONFIG_FIELDS } from "../types.js";
41
- export const SETTINGS_KEYS = Object.fromEntries(Object.entries(CONFIG_FIELDS).filter(([, field])=>!field.envOnly || "internal" in field).map(([key])=>[
42
- key,
43
- key
44
- ]));
45
- /**
46
- * Onboarding status values
47
- */ export const ONBOARDING_STATUS = {
48
- PENDING: "pending",
49
- COMPLETED: "completed"
50
- };
@@ -1,76 +0,0 @@
1
- /**
2
- * HTML Excerpt Utilities
3
- *
4
- * Generates paragraph-aware excerpts from HTML content for article
5
- * previews in timelines. Breaks only at paragraph boundaries.
6
- */ /**
7
- * Strips HTML tags from a string, returning plain text.
8
- *
9
- * @param html - HTML string to strip
10
- * @returns Plain text without HTML tags
11
- *
12
- * @example
13
- * ```typescript
14
- * stripHtml("<p>Hello <strong>world</strong></p>") // "Hello world"
15
- * ```
16
- */ export function stripHtml(html) {
17
- return html.replace(/<[^>]*>/g, "");
18
- }
19
- /**
20
- * Extracts a paragraph-aware HTML excerpt from body HTML.
21
- *
22
- * Uses a greedy algorithm: accumulates paragraphs until the total
23
- * plain-text length exceeds 500 characters, then stops. At least
24
- * one paragraph is always included.
25
- *
26
- * If the content contains a `<!--more-->` marker, the content before
27
- * the marker is used as the excerpt instead.
28
- *
29
- * @param bodyHtml - Full HTML body content
30
- * @returns Excerpt HTML and whether there is more content
31
- *
32
- * @example
33
- * ```typescript
34
- * // Short content — returned as-is with hasMore = false
35
- * getHtmlExcerpt("<p>Short post.</p>")
36
- * // { excerpt: "<p>Short post.</p>", hasMore: false }
37
- *
38
- * // Long content — truncated at paragraph boundary
39
- * getHtmlExcerpt("<p>" + "A".repeat(300) + "</p><p>" + "B".repeat(300) + "</p>")
40
- * // { excerpt: "<p>AAA...</p>", hasMore: true }
41
- *
42
- * // Manual break with <!--more-->
43
- * getHtmlExcerpt("<p>Intro</p><!--more--><p>Rest</p>")
44
- * // { excerpt: "<p>Intro</p>", hasMore: true }
45
- * ```
46
- */ export function getHtmlExcerpt(bodyHtml) {
47
- // Honor manual <!--more--> marker
48
- if (bodyHtml.includes("<!--more-->")) {
49
- const excerpt = bodyHtml.split("<!--more-->")[0];
50
- return {
51
- excerpt,
52
- hasMore: true
53
- };
54
- }
55
- const paragraphs = bodyHtml.match(/<p>[\s\S]*?<\/p>/g) || [];
56
- // No paragraphs found — return full content
57
- if (paragraphs.length === 0) {
58
- return {
59
- excerpt: bodyHtml,
60
- hasMore: false
61
- };
62
- }
63
- let excerpt = "";
64
- let charCount = 0;
65
- for (const p of paragraphs){
66
- const textLen = stripHtml(p).length;
67
- if (charCount + textLen > 500 && excerpt) break;
68
- excerpt += p;
69
- charCount += textLen;
70
- }
71
- const hasMore = excerpt.length < bodyHtml.length;
72
- return {
73
- excerpt,
74
- hasMore
75
- };
76
- }