@jant/core 0.7.0 → 0.7.1

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 (158) hide show
  1. package/bin/commands/export.js +3 -1
  2. package/bin/commands/import-site.js +689 -216
  3. package/bin/commands/setup.js +136 -0
  4. package/bin/commands/site/export.js +71 -34
  5. package/bin/commands/site/pull-media.js +2 -6
  6. package/bin/commands/site/snapshot/export.js +18 -60
  7. package/bin/commands/site/snapshot/import.js +22 -23
  8. package/bin/lib/d1-query.js +87 -2
  9. package/bin/lib/hugo-markdown.js +4 -0
  10. package/bin/lib/site-pull-media.js +21 -28
  11. package/bin/lib/site-selection.js +10 -1
  12. package/bin/lib/site-snapshot.js +338 -3
  13. package/bin/lib/sql-export.js +68 -5
  14. package/bin/lib/wrangler-cli.js +9 -0
  15. package/bin/lib/zip-archive.js +187 -0
  16. package/dist/{app-K_Aa1MMn.js → app-ZcaI1kPN.js} +732 -217
  17. package/dist/client/.vite/manifest.json +20 -20
  18. package/dist/client/_assets/chunks/{create-editor-CD3FhrOB.js → create-editor-B-m7X7S5.js} +50 -46
  19. package/dist/client/_assets/chunks/{sortable-list-CgaL2jCs.js → sortable-list-BJyd-LXE.js} +1 -1
  20. package/dist/client/_assets/chunks/{unsafe-svg-0QCkP0vZ.js → unsafe-svg-BkscJx69.js} +2 -2
  21. package/dist/client/_assets/{client-GiYENVw8.css → client-DAhoqPdr.css} +1 -1
  22. package/dist/client/_assets/{client-auth-k8gJ6QJj.js → client-auth-CAMfTrKW.js} +1 -1
  23. package/dist/client/_assets/{client-compose-B_kDtWkW.js → client-compose-D6rExKVK.js} +1 -1
  24. package/dist/client/_assets/{client-VnFxJN7G.js → client-knSEyUJO.js} +1 -1
  25. package/dist/client/_assets/{client-manage-M90aSTOk.js → client-manage-sVOkqVbH.js} +1 -1
  26. package/dist/client/_assets/{client-settings-DfYs9n1F.js → client-settings-CGR_nZ78.js} +7 -7
  27. package/dist/{github-sync-BPAvT999.js → github-sync-Gw4orAHk.js} +799 -110
  28. package/dist/index.js +2 -2
  29. package/dist/node.js +9 -5
  30. package/package.json +13 -3
  31. package/src/__tests__/dev-scripts.test.ts +203 -0
  32. package/src/__tests__/export-collection-order.test.ts +276 -0
  33. package/src/__tests__/export-feed-ids.test.ts +184 -0
  34. package/src/__tests__/export-feed-order.test.ts +294 -0
  35. package/src/__tests__/export-hugo-build.test.ts +130 -0
  36. package/src/__tests__/export-import-roundtrip.test.ts +94 -0
  37. package/src/__tests__/export-service.test.ts +611 -28
  38. package/src/__tests__/export-smart-collection.test.ts +329 -0
  39. package/src/__tests__/helpers/hugo-site.ts +146 -0
  40. package/src/__tests__/import-site-command.test.ts +487 -1
  41. package/src/__tests__/mise-config.test.ts +75 -4
  42. package/src/__tests__/node-dev-tasks.test.ts +264 -0
  43. package/src/__tests__/site-export-canonical-import.test.ts +149 -0
  44. package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
  45. package/src/__tests__/snapshot-settings.test.ts +97 -0
  46. package/src/__tests__/snapshot-tables.test.ts +219 -0
  47. package/src/__tests__/sql-export.test.ts +173 -0
  48. package/src/__tests__/zip-archive.test.ts +106 -0
  49. package/src/app.tsx +15 -6
  50. package/src/client/components/__tests__/jant-settings-avatar.test.ts +1 -1
  51. package/src/client/components/__tests__/jant-settings-general.test.ts +21 -1
  52. package/src/client/components/jant-repo-picker-types.ts +6 -1
  53. package/src/client/components/jant-repo-picker.ts +4 -7
  54. package/src/client/components/jant-settings-general.ts +17 -12
  55. package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
  56. package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
  57. package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
  58. package/src/client/tiptap/extensions.ts +0 -3
  59. package/src/client/tiptap/structural-keymap.ts +158 -47
  60. package/src/db/__tests__/d1-query.test.ts +56 -1
  61. package/src/i18n/locales/settings/en.po +8 -8
  62. package/src/i18n/locales/settings/en.ts +1 -1
  63. package/src/i18n/locales/settings/zh-Hans.po +8 -8
  64. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  65. package/src/i18n/locales/settings/zh-Hant.po +8 -8
  66. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  67. package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
  68. package/src/lib/__tests__/image.test.ts +27 -1
  69. package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
  70. package/src/lib/__tests__/markdown.test.ts +10 -0
  71. package/src/lib/__tests__/resolve-config.test.ts +67 -0
  72. package/src/lib/__tests__/schemas.test.ts +27 -1
  73. package/src/lib/__tests__/timeline.test.ts +87 -0
  74. package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
  75. package/src/lib/discover.ts +3 -1
  76. package/src/lib/github-sync-repo-name.ts +45 -0
  77. package/src/lib/hugo-markdown.ts +46 -0
  78. package/src/lib/image.ts +17 -4
  79. package/src/lib/markdown-manager.ts +392 -2
  80. package/src/lib/post-body-html.ts +11 -4
  81. package/src/lib/resolve-config.ts +58 -1
  82. package/src/lib/schemas.ts +50 -5
  83. package/src/lib/thread-fold.ts +3 -3
  84. package/src/lib/timeline.ts +1 -1
  85. package/src/lib/tiptap-to-markdown.ts +13 -7
  86. package/src/lib/url.ts +20 -0
  87. package/src/lib/view.ts +2 -2
  88. package/src/node/__tests__/cli-setup.test.ts +163 -0
  89. package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
  90. package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
  91. package/src/node/__tests__/runtime.test.ts +38 -0
  92. package/src/node/index.ts +2 -0
  93. package/src/node/request-handler.ts +3 -1
  94. package/src/routes/api/__tests__/posts.test.ts +24 -0
  95. package/src/routes/api/__tests__/upload.test.ts +34 -0
  96. package/src/routes/api/export.ts +3 -3
  97. package/src/routes/api/internal/sites.ts +0 -1
  98. package/src/routes/api/posts.ts +2 -0
  99. package/src/routes/api/public/posts.ts +21 -1
  100. package/src/routes/api/upload.ts +10 -3
  101. package/src/routes/compose.tsx +4 -0
  102. package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
  103. package/src/routes/dash/settings.tsx +212 -70
  104. package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
  105. package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
  106. package/src/routes/pages/archive.tsx +17 -11
  107. package/src/routes/pages/featured.tsx +11 -5
  108. package/src/routes/pages/page.tsx +53 -31
  109. package/src/routes/pages/search.tsx +4 -2
  110. package/src/runtime/__tests__/readiness.test.ts +23 -0
  111. package/src/runtime/node.ts +49 -0
  112. package/src/runtime/readiness.ts +15 -0
  113. package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
  114. package/src/services/__tests__/custom-url.test.ts +34 -0
  115. package/src/services/__tests__/github-app-installations.test.ts +153 -0
  116. package/src/services/__tests__/github-sync-push.test.ts +46 -0
  117. package/src/services/__tests__/media.test.ts +31 -0
  118. package/src/services/__tests__/path.test.ts +56 -0
  119. package/src/services/__tests__/post-timeline.test.ts +127 -0
  120. package/src/services/__tests__/post.test.ts +74 -0
  121. package/src/services/bootstrap.ts +263 -44
  122. package/src/services/custom-url.ts +2 -2
  123. package/src/services/export-theme/layouts/_default/alias.html +27 -1
  124. package/src/services/export-theme/layouts/_default/list.html +2 -63
  125. package/src/services/export-theme/layouts/_default/rss.xml +27 -38
  126. package/src/services/export-theme/layouts/collections/list.html +3 -3
  127. package/src/services/export-theme/layouts/featured/list.html +1 -4
  128. package/src/services/export-theme/layouts/index.html +40 -26
  129. package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
  130. package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
  131. package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
  132. package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
  133. package/src/services/export-theme/layouts/partials/footer.html +1 -1
  134. package/src/services/export-theme/layouts/partials/head.html +1 -1
  135. package/src/services/export-theme/layouts/partials/header.html +5 -3
  136. package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
  137. package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
  138. package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
  139. package/src/services/export-theme/layouts/partials/thread-preview.html +1 -1
  140. package/src/services/export-theme/layouts/post/list.html +1 -1
  141. package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
  142. package/src/services/export-theme/styles/main.css +0 -1
  143. package/src/services/export-theme/theme.toml +1 -1
  144. package/src/services/export.ts +620 -71
  145. package/src/services/github-app-installations.ts +171 -4
  146. package/src/services/github-sync.ts +69 -15
  147. package/src/services/mcp.ts +19 -1
  148. package/src/services/media.ts +8 -1
  149. package/src/services/path.ts +34 -1
  150. package/src/services/post.ts +214 -39
  151. package/src/services/site-admin.ts +3 -7
  152. package/src/services/site.ts +83 -31
  153. package/src/styles/ui.css +7 -1
  154. package/src/types/app-context.ts +16 -0
  155. package/src/types/bindings.ts +5 -0
  156. package/src/types/operations.ts +10 -0
  157. package/src/ui/dash/settings/GeneralContent.tsx +10 -8
  158. package/src/client/tiptap/exitable-marks.ts +0 -73
@@ -1,4 +1,4 @@
1
- import { $ as getHostedControlPlaneBaseUrl, $t as HOME_BRANDING_LINK_LABEL, A as upgradeLegacyFootnotes, An as toAbsoluteSiteUrl, At as MAX_SITE_DESCRIPTION_LENGTH, Bt as SMART_COLLECTION_SORT_ORDERS, C as extractSummary, Cn as isFullUrl, Ct as EARLIEST_FILTERABLE_YEAR, D as renderTiptapDocumentAroundBoundary, Dn as sanitizeUrl, Dt as MAX_COLLECTION_DESCRIPTION_LENGTH, E as renderTiptapDocument, En as normalizeSiteUrl, Et as LATEST_FILTERABLE_YEAR, F as formatYearMonth, Ft as PATH_KINDS, G as getConfiguredStorageDriver, Gt as SYSTEM_NAV_KEY_VALUES, H as getConfiguredSingleSiteOrigin, Ht as STATUSES, I as formatYearMonthLabel, In as escapeHtml, It as PUBLIC_ARCHIVE_VISIBILITIES, J as getDiscoverDefault, Jt as VISIBILITIES, K as getCorsOrigins, Kt as TEXT_ATTACHMENT_CONTENT_FORMATS, L as now, Ln as __commonJSMin, Lt as SITE_DOMAIN_KINDS, M as formatRelativeAge, Mn as toPublicHref, Mt as MEDIA_KINDS, N as formatRelativeTime, Nn as toPublicPath, Nt as NAV_ITEM_PLACEMENTS, O as renderTiptapJson, On as stripSitePathPrefix, P as formatTime, Pn as toSameSitePath, Pt as NAV_ITEM_TYPES, Q as getGitHubAppConfig, Qt as getPublicUrlForProvider, Rn as __exportAll, Rt as SITE_MEMBER_ROLES, S as extractBodyText, Sn as getSitePathPrefix, St as DEFAULT_NAVIGATION_PROFILE, T as extractTimelineSummary, Tn as normalizePath, Tt as GITHUB_APP_ACCOUNT_TYPES, U as getConfiguredSingleSitePathPrefix, Ut as STORAGE_DRIVERS, V as getAuthSecret, Vt as SORT_ORDERS, W as getConfiguredSingleSiteUrl, Wt as SYSTEM_NAV_KEYS, X as getDiscoverPingUrl, Xt as getImageUrl, Y as getDiscoverDirectoryBaseUrl, Yt as isFeedNavKey, Z as getEnvString, Zt as getMediaUrl, _ as markdownToTiptapJson, _n as base64ToUint8Array, _t as ARCHIVE_VISIBILITIES, an as getDefaultJantFaviconIcoBytes, at as getInternalAdminToken, b as toPlainText, bn as extractDomain, bt as COLLECTION_SORT_ORDERS, cn as getJantIconFilename, ct as getSiteResolutionMode, d as buildInstallUrl, dn as getJantLogoFills, en as HOME_BRANDING_PREFIX, et as getHostedControlPlaneDomainCheckSecret, f as getInstallation, fn as getJantLogoHref, ft as shouldUseSecureCookies, g as tiptapJsonToMarkdown, gn as arrayBufferToBase64, gt as ARCHIVE_LAYOUTS, h as searchInstallationRepos, hn as JANT_LOGO_VIEW_BOX, ht as THEME_MODES, i as createGitHubClient, in as getDefaultJantAppleTouchIconBytes, it as getHostedControlPlaneSsoSecret, j as formatDate, jn as toInternalPath, jt as MAX_SITE_FOOTER_LENGTH, k as trimTiptapBody, kn as toAbsoluteAssetUrl, l as buildRootActivityExpr, ln as getJantIconHref, lt as getTelegramBotPool, m as listInstallationReposPage, mn as JANT_LOGO_PATH_DATA, mt as CONFIG_FIELDS, n as createGitHubSyncService, nn as JANT_HOME_URL, nt as getHostedControlPlaneInternalToken, o as parseRepoSlug, on as getJantBrandPackHref, ot as getLocalStoragePath, pn as getJantPositiveLogoPngHref, pt as coalesceDisplayText, q as getDevApiToken, qt as UPLOAD_SESSION_STATES, rn as JANT_POSITIVE_LOGO_PNG_FILENAME, rt as getHostedControlPlaneProviderLabel$1, s as createExportService, sn as getJantBundledAsset, tn as JANT_BRAND_PACK_FILENAME, tt as getHostedControlPlaneInternalBaseUrl, u as rootActivityColumns, un as getJantLogoFilename, ut as getTelegramWebhookSecret, vn as buildSiteUrl, vt as COLLECTION_DIRECTORY_ENTRY_TYPES, w as extractSummaryHtml, wn as isSafeInternalRedirect, wt as FORMATS, x as NOTE_SUMMARY_MAX_CHARS, xn as getSiteOrigin, xt as CONTENT_DISPOSITIONS, y as render, yn as extractDisplayDomain, yt as COLLECTION_FRESHNESS_WINDOW_SECONDS, z as toISOString, zn as __toESM, zt as SITE_STATUSES } from "./github-sync-BPAvT999.js";
1
+ import { $ as getGitHubAppConfig, $t as getPublicUrlForProvider, A as trimTiptapBody, An as stripSitePathPrefix, B as toISOString, Bn as __exportAll, Bt as SITE_STATUSES, C as extractBodyText, Cn as getSiteOrigin, Ct as DEFAULT_NAVIGATION_PROFILE, D as renderTiptapDocument, Dn as normalizePath, Dt as LATEST_FILTERABLE_YEAR, E as extractTimelineSummary, En as isSafeInternalRedirect, Et as GITHUB_APP_ACCOUNT_TYPES, F as formatTime, Fn as toPublicPath, Ft as NAV_ITEM_TYPES, G as getConfiguredSingleSiteUrl, Gt as SYSTEM_NAV_KEYS, H as getAuthSecret, Ht as SORT_ORDERS, I as formatYearMonth, In as toSameSitePath, It as PATH_KINDS, J as getDevApiToken, Jt as UPLOAD_SESSION_STATES, K as getConfiguredStorageDriver, Kt as SYSTEM_NAV_KEY_VALUES, L as formatYearMonthLabel, Lt as PUBLIC_ARCHIVE_VISIBILITIES, M as formatDate, Mn as toAbsoluteSiteUrl, Mt as MAX_SITE_FOOTER_LENGTH, N as formatRelativeAge, Nn as toInternalPath, Nt as MEDIA_KINDS, O as renderTiptapDocumentAroundBoundary, On as normalizeSiteUrl, Ot as MAX_COLLECTION_DESCRIPTION_LENGTH, P as formatRelativeTime, Pn as toPublicHref, Pt as NAV_ITEM_PLACEMENTS, Q as getEnvString, Qt as getMediaUrl, R as now, Rn as escapeHtml, Rt as SITE_DOMAIN_KINDS, S as NOTE_SUMMARY_MAX_CHARS, Sn as getPostPath, St as CONTENT_DISPOSITIONS, T as extractSummaryHtml, Tn as isFullUrl, Tt as FORMATS, U as getConfiguredSingleSiteOrigin, Ut as STATUSES, Vn as __toESM, Vt as SMART_COLLECTION_SORT_ORDERS, W as getConfiguredSingleSitePathPrefix, Wt as STORAGE_DRIVERS, X as getDiscoverDirectoryBaseUrl, Xt as isFeedNavKey, Y as getDiscoverDefault, Yt as VISIBILITIES, Z as getDiscoverPingUrl, Zt as getImageUrl, _ as markdownToTiptapJson, _n as arrayBufferToBase64, _t as ARCHIVE_LAYOUTS, an as getDefaultJantAppleTouchIconBytes, at as getHostedControlPlaneSsoSecret, b as toPlainText, bn as extractDisplayDomain, bt as COLLECTION_FRESHNESS_WINDOW_SECONDS, c as buildRootActivityExpr, cn as getJantBundledAsset, d as buildInstallUrl, dn as getJantLogoFilename, dt as getTelegramWebhookSecret, en as HOME_BRANDING_LINK_LABEL, et as getHostedControlPlaneBaseUrl, f as getInstallation, fn as getJantLogoFills, g as tiptapJsonToMarkdown, gn as JANT_LOGO_VIEW_BOX, gt as THEME_MODES, h as searchInstallationRepos, hn as JANT_LOGO_PATH_DATA, ht as CONFIG_FIELDS, i as createGitHubClient, in as JANT_POSITIVE_LOGO_PNG_FILENAME, it as getHostedControlPlaneProviderLabel$1, j as upgradeLegacyFootnotes, jn as toAbsoluteAssetUrl, jt as MAX_SITE_DESCRIPTION_LENGTH, k as renderTiptapJson, kn as sanitizeUrl, l as rootActivityColumns, ln as getJantIconFilename, lt as getSiteResolutionMode, m as listInstallationReposPage, mn as getJantPositiveLogoPngHref, mt as coalesceDisplayText, n as createGitHubSyncService, nn as JANT_BRAND_PACK_FILENAME, nt as getHostedControlPlaneInternalBaseUrl, o as parseRepoSlug, on as getDefaultJantFaviconIcoBytes, ot as getInternalAdminToken, pn as getJantLogoHref, pt as shouldUseSecureCookies, q as getCorsOrigins, qt as TEXT_ATTACHMENT_CONTENT_FORMATS, rn as JANT_HOME_URL, rt as getHostedControlPlaneInternalToken, s as createExportService, sn as getJantBrandPackHref, st as getLocalStoragePath, tn as HOME_BRANDING_PREFIX, tt as getHostedControlPlaneDomainCheckSecret, u as suggestSyncRepoName, un as getJantIconHref, ut as getTelegramBotPool, vn as base64ToUint8Array, vt as ARCHIVE_VISIBILITIES, w as extractSummary, wn as getSitePathPrefix, wt as EARLIEST_FILTERABLE_YEAR, x as fillRequiredContent, xn as extractDomain, xt as COLLECTION_SORT_ORDERS, y as render, yn as buildSiteUrl, yt as COLLECTION_DIRECTORY_ENTRY_TYPES, zn as __commonJSMin, zt as SITE_MEMBER_ROLES } from "./github-sync-Gw4orAHk.js";
2
2
  import { I18n } from "@lingui/core";
3
3
  import * as lucideIcons from "lucide-static";
4
4
  import { z } from "zod";
@@ -2009,13 +2009,13 @@ var Hono = class extends Hono$1 {
2009
2009
  var messages$3 = JSON.parse("{\"+3Gnxr\":[\"New Smart Collection\"],\"+4u2g6\":[\"A ready-made 1:1 PNG for decks, mockups, directories, and other square placements.\"],\"+G8qqW\":[\"Collection saved.\"],\"+GO7f+\":[\"All\"],\"+IJm1Z\":[\"Muted\"],\"+Irvp3\":[\"Everything on this page is ready to use for articles, launch posts, directories, and product coverage.\"],\"+Qaboy\":[\"Favicon\"],\"+fWu2O\":[\"A calmer, warmer accent makes the default theme feel quieter and more intentional.\"],\"+jw/c1\":[\"Delete image\"],\"+nHhRH\":[\"Use \",[\"brandColorName\"]],\"+siMqD\":[\"Journal\"],\"/0D1Xp\":[\"Edit Collection\"],\"/DFKdU\":[\"Type the quote...\"],\"/PfPLc\":[\"Label (optional)\"],\"/PoNoq\":[\"Edit link\"],\"/Ui2OV\":[\"Use the reverse logo on dark backgrounds.\"],\"/Ybds4\":[\"Primary Jant logo for websites, docs, press coverage, and editorial layouts.\"],\"/kBuYR\":[\"The same posts as the home page.\"],\"/rTz0M\":[\"Audio\"],\"0CuAor\":[\"Conditions\"],\"0EcUWz\":[\"Discard changes?\"],\"0Lj7or\":[\"Save text attachment?\"],\"0Szk2t\":[\"Automatically collects: \",[\"conditions\"]],\"0Tc16F\":[\"Every published post, including those kept off the home page.\"],\"0Vlj/m\":[\"This post isn’t published.\"],\"0XDp7X\":[\"Links should read clearly without glowing against the page.\"],\"0cspe/\":[\"Delete row\"],\"0ieXE7\":[\"Highest rated\"],\"11h9eK\":[\"Includes\"],\"121hRB\":[\"Show the original\"],\"15++NM\":[\"Inline emphasis\"],\"1DBGsz\":[\"Notes\"],\"1NeeWI\":[\"Square assets for avatars, apps, browsers, and shared links\"],\"1THMr2\":[\"Brand pack\"],\"1njn7W\":[\"Light\"],\"27KTPP\":[\"Draft preview\"],\"2B7HLH\":[\"New post\"],\"2BBAbc\":[\"List\"],\"2C7mSG\":[\"Collection link\"],\"2ETv7R\":[\"Tune color in a real reading context\"],\"2HbvFp\":[\"Real post components\"],\"2MXb5X\":[\"Field notes on quiet design\"],\"2koDOQ\":[\"Thread accents\"],\"2lKpcz\":[\"Write your first post to get started.\"],\"2q/Q7x\":[\"Visibility\"],\"2sCqzD\":[\"Use this for websites, docs, articles, and other light or neutral surfaces.\"],\"3Cw1AI\":[\"Add Collection\"],\"3PAU4M\":[\"Year\"],\"3lJk5u\":[\"Keep the artwork unchanged.\"],\"3mdteM\":[\"before deciding whether the accent is carrying too much product energy.\"],\"3neqtf\":[\"Thread accent\"],\"3qkggm\":[\"Fullscreen\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"3xi01/\":[\"Look at the footer metadata last, to make sure the accent is not fighting the typography.\"],\"47iMgt\":[\"Editorial interfaces worth borrowing from\"],\"4GQThz\":[\"No collections yet. Start one to organize threads by topic.\"],\"4J/OYU\":[\"Collection created.\"],\"4OjqAQ\":[\"Keep editing\"],\"4eiXo+\":[\"Leave blank to generate one automatically.\"],\"4pV0kE\":[\"Avatar-ready\"],\"5dcjwM\":[\"Choose today or an earlier date, or leave it blank to publish now.\"],\"5pAjd8\":[\"Accent should feel present, not loud.\"],\"5sEkBi\":[\"Open raw asset\"],\"61i3cO\":[\"Toggle header row\"],\"6QCZMo\":[\"Add row below\"],\"6UTABI\":[\"Collection order updated.\"],\"6WAK+2\":[\"Use current date\"],\"6Y4BBO\":[\"An abstract editorial layout in warm paper colors\"],\"6cjUDB\":[\"Brand assets\"],\"6gxN21\":[\"Read from what you write — looks like \",[\"language\"]],\"6lGV3K\":[\"Show less\"],\"6p0JeQ\":[\"to make sure both still feel like they belong to the same product.\"],\"6sVyMq\":[\"Add a URL before posting this link.\"],\"6uSxjS\":[\"Edit Smart Collection\"],\"6yCv8j\":[\"Save these changes to the text attachment, discard them, or keep editing.\"],\"74kJNs\":[\"View earlier notes in this thread\"],\"7DvUqV\":[\"Read the page from top to bottom without looking at the swatches.\"],\"7aris6\":[\"March 15\"],\"7d1a0d\":[\"Public\"],\"7hYXO0\":[\"Use this on dark backgrounds, image-backed surfaces, and any placement where the green logo would lose contrast.\"],\"7ibPpM\":[\"Add column after\"],\"7kMW54\":[\"Open raw SVG\"],\"7nGhhM\":[\"What's on your mind?\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8Btgys\":[\"Draft deleted.\"],\"8WX0J+\":[\"Your thoughts (optional)\"],\"8ZsakT\":[\"Password\"],\"8eC78s\":[\"A calmer accent makes\"],\"8tM8+a\":[\"Save as draft\"],\"8vETh9\":[\"Show\"],\"90IRF2\":[\"This article is here to answer a specific question: does the default accent still feel calm once it has to carry a full reading experience?\"],\"9aloPG\":[\"References\"],\"9dr9Nh\":[\"Open external link\"],\"9j5qCS\":[\"Table options\"],\"A1D8Yt\":[\"What the accent should do\"],\"A1taO8\":[\"Search\"],\"A2Vg/u\":[\"Navigation and reading states\"],\"ADCve+\":[\"Add condition\"],\"AjHkcv\":[\"Default preview image for social shares and link unfurls.\"],\"AmQ/h/\":[\"Sort order\"],\"AnhYQu\":[\"Counting…\"],\"AyHO4m\":[\"What's this collection about?\"],\"B1FFMj\":[\"Download Brand Pack\"],\"B495Gs\":[\"Archive\"],\"Bdha5y\":[\"Publishing...\"],\"BdjLtf\":[\"thread\"],\"Bmaby2\":[\"All formats\"],\"C+9df9\":[\"Quoted or highlighted passages should feel like annotations, not warnings.\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"C4TjpG\":[\"Read less\"],\"CAh1km\":[\"collections\"],\"CH3bgf\":[\"RSS feed for this view\"],\"CXDHcv\":[\"Grid\"],\"CmBCXY\":[\"Link updated.\"],\"D4em/+\":[\"Logos\"],\"DHhJ7s\":[\"Previous\"],\"DJLY+/\":[\" and try again.\"],\"DOx286\":[\"Draft restored.\"],\"DPfwMq\":[\"Done\"],\"DSJXZM\":[\"Enter a valid date.\"],\"DYlMYF\":[\"Built-in background\"],\"DoJzLz\":[\"Collections\"],\"Du2B9f\":[\"The default accent should support reading first. Start by comparing it against the\"],\"DxwUcG\":[\"Read the palette as content first\"],\"Dyu6u6\":[\"A longer introduction to feeds and readers, at aboutfeeds.com\"],\"E3NcGH\":[\"Square logo PNG\"],\"EDl9kS\":[\"Subscribe\"],\"EEYbdt\":[\"Publish\"],\"EHWwm1\":[\"The default accent should feel written, not branded.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"EQNPYo\":[\"Featured on \",[\"date\"],\" at \",[\"time\"]],\"EQtz4D\":[\"Open a few links and check whether they still feel native to the page.\"],\"EU3tBD\":[\"Link removed.\"],\"EetoJL\":[\"Guide the eye without taking over the layout.\"],\"Eiv3bO\":[\"Buttons can stay steady, but links, thread markers, and subtle emphasis should feel closer to ink on paper than dashboard chrome.\"],\"ElTnWL\":[\"Published on\"],\"EmQw8O\":[\"If this article still feels like a page you want to keep reading, the palette is probably close.\"],\"EsJdRp\":[\"Save theme\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FEr96N\":[\"Theme\"],\"FGySZL\":[\"The default accent works best when it reads like a fountain-pen underline. Compare it against the\"],\"FM+KeU\":[\"No drafts yet. Save a draft to find it here.\"],\"FVjDK8\":[\"Hide the original\"],\"Fdv5k7\":[\"What to look for while tuning it\"],\"FkMol5\":[\"Featured\"],\"FoRBv8\":[\"Could not save. Try again.\"],\"FqCHF/\":[\"Threads\"],\"Fxf4jq\":[\"Description (optional)\"],\"G2u/aQ\":[\"Download official Jant logos, icons, and preview assets.\"],\"GAohqx\":[\"Delete column\"],\"GY/1J4\":[\"Jant fallback canary string\"],\"GbIOhd\":[\"Reply quietly\"],\"GiRWtR\":[\"Why the default accent should feel written, not branded\"],\"GkpIs2\":[\"Remove this link from Collections? The destination won't change.\"],\"GxkJXS\":[\"Uploading...\"],\"H29JXm\":[\"+ ALT\"],\"H4lgRd\":[\"Authentication isn't set up. Check your server config.\"],\"HA0Tht\":[\"RSS\"],\"HFPGej\":[\"No threads match these filters. Try adjusting your selection or clear all filters.\"],\"HG79RB\":[\"Post as Private\"],\"HNEHJP\":[\"Demo credentials are pre-filled — hit Sign In to continue.\"],\"HSI88F\":[\"Delete table\"],\"HbAIQc\":[\"A reference link for checking whether the accent feels editorial instead of promotional.\"],\"HrC0ab\":[\"All posts\"],\"Ht1V3q\":[\"For the same reason, inline code should stay neutral. Something like theme.siteAccent = soften(green, 12%) should not suddenly become the loudest thing on the page.\"],\"I22eN0\":[\"Shared links\"],\"I6zLrz\":[\"Use these when you need a transparent square logo, a shaped tile with a built-in background, a browser icon, or a default preview image.\"],\"IBrKQ8\":[\"Collections and filtered archive views have their own feeds. Look for this icon on those pages.\"],\"ICsA6P\":[\"You have unsaved changes\"],\"IUX7p+\":[\"White logo on the Jant green rounded tile for app icon mockups, touch icons, directory listings, and other square placements that should feel softer.\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Reply\"],\"IsWPTn\":[\"Detect\"],\"ItUf28\":[\"Automatically collects every post.\"],\"IvUVJl\":[\"Show as a list with full posts\"],\"Ixlk5e\":[\"Follow site default\"],\"J+2Rls\":[\"Leave blank to publish now. Use an earlier date when importing older posts.\"],\"J4tAHl\":[\"Headings should keep their hierarchy even when the accent gets softer.\"],\"JWzXie\":[\"Couldn't publish. Try again.\"],\"JYj5R2\":[\"Browse files\"],\"JcD7qf\":[\"More actions\"],\"JqJ5Xv\":[\"Latest\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"JwLPQ/\":[\"This sign-in link has expired. Return to \"],\"KOqvXP\":[\"Do not recolor, stretch, rotate, outline, or add effects to the logo.\"],\"KPGTsG\":[\"This link is taken. Choose another.\"],\"KbS2K9\":[\"Reset Password\"],\"KdSsVl\":[\"Author (optional)\"],\"Khu3PV\":[\"Publish settings\"],\"KiJn9B\":[\"Note\"],\"KlZ+t+\":[\"%name% + %count% more\"],\"KsvRin\":[\"Hide from Latest\"],\"KzmC5L\":[\"Controls\"],\"L7svJg\":[\"Reading\"],\"LBG2oq\":[\"Move attachment earlier\"],\"LDuJfz\":[\"Language: \",[\"language\"]],\"Lbkbwy\":[\"A quote card for judging accent color against softer, citation-heavy content.\"],\"LcvzvX\":[\"Tap to retry\"],\"LkA8jz\":[\"Add alt text\"],\"LxRg6f\":[\"live theme controls\"],\"M4tzVU\":[\"Latest posts\"],\"M8kJqa\":[\"Drafts\"],\"MDd5rF\":[\"Read it in \",[\"language\"]],\"MHrjPM\":[\"Title\"],\"MILa7n\":[\"Square tile\"],\"MRYGql\":[\"Open image URL\"],\"MSc/Yq\":[\"Do you want to publish your changes or discard them?\"],\"MZTcRq\":[\"Image unavailable\"],\"Mavvsz\":[\"The original\"],\"Mc7+6G\":[\"Enter a valid URL starting with http://, https://, or mailto:.\"],\"MdMyne\":[\"Source link (optional)\"],\"MgW3St\":[\"Changing the link breaks the old one immediately.\"],\"MiMY3Q\":[\"Apple touch icon\"],\"MiyoI7\":[\"default note sample\"],\"MqghUt\":[\"Search posts...\"],\"Myqkib\":[\"Create a collection to get started.\"],\"N40H+G\":[\"All\"],\"N8UzTV\":[\"Replies\"],\"NAFbuE\":[\"Search snippet\"],\"NGSThJ\":[\"Smart Collection\"],\"NH9Z1R\":[\"Start here\"],\"NcNIaO\":[\"Could not open this smart collection. Try again.\"],\"NqsRbb\":[\"Jant logo\"],\"Nu4oKW\":[\"Description\"],\"NvXuWk\":[\"Won't move the thread to the top of latest.\"],\"O1367B\":[\"All collections\"],\"O3oNi5\":[\"Email\"],\"OEdMhi\":[\"The best default color is the one you notice only after reading for a while.\"],\"OEt/to\":[\"Guidelines\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OaoJcz\":[\"Social preview\"],\"OmfDbR\":[\"Site accent\"],\"Oo7//P\":[\"Edit draft\"],\"Ovks1h\":[\"A softer blue feels more like ink than product chrome.\"],\"P/sHNL\":[\"Use this page to judge buttons, links, cards, forms, thread accents, and quiet surfaces before changing a theme globally.\"],\"P5WtnY\":[\"Without media\"],\"PBxg/E\":[\"Not now\"],\"Q/uoSA\":[\"Quiet here for now.\"],\"Q2mGA7\":[\"Clear filter\"],\"QBqVyM\":[\"Home screen icon for iPhone and iPad shortcuts.\"],\"QHJmUs\":[\"of \",[\"total\"],\" \",[\"unit\"]],\"QJAQiR\":[\"With media\"],\"QOhkyl\":[\"Compose\"],\"QebAts\":[\"Link added.\"],\"Qgbxdw\":[\"Designing a calmer default accent for Jant\"],\"Qn9Ao8\":[\"Circle tile\"],\"Qoq+GP\":[\"Read more\"],\"QyDt3L\":[\"File uploaded.\"],\"R5CMuK\":[\"Jant looks best when the accent feels editorial. Buttons can stay sturdy, but inline emphasis should feel like a pen mark, not a dashboard highlight.\"],\"R8AthW\":[\"Divider\"],\"R9Khdg\":[\"Auto\"],\"RAv3u7\":[\"Compare it against the theme controls\"],\"ROa4Ti\":[\"Interfaces for reading should guide the eye, not keep asking for attention.\"],\"RZOWDv\":[\"Add a custom shortcut to any page or site.\"],\"RdmNnl\":[\"Browser tab\"],\"RfGczC\":[\"Square logo\"],\"Rj01Fz\":[\"Links\"],\"S37om9\":[\"Included assets\"],\"S8NCfs\":[\"Save to drafts to edit and post at a later time.\"],\"SJGVAw\":[\"Feel editorial and slightly quieter.\"],\"SSsoa4\":[\"Add to Navigation\"],\"SaNhJE\":[\"feel deliberate instead of washed out.\"],\"SbNisn\":[\"Sort by when each thread was last added to\"],\"SpTWH3\":[\"Download SVG\"],\"SvRuJt\":[\"Field Notes on Interface Tone\"],\"T/R+Qz\":[\"Primary\"],\"TNZKpI\":[\"Danger\"],\"Tn1w2R\":[\"Draft actions\"],\"TvaTxw\":[\"Doesn't appear in Latest. Still appears in collections you add it to.\"],\"UGA1Z/\":[\"Translation of “\",[\"title\"],\"”\"],\"UIMXHD\":[\"Remove Divider\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uc5y7o\":[\"Choose the standard logo for websites, docs, directories, and editorial layouts.\"],\"V0fyg5\":[\"Collection added to navigation.\"],\"V18SVO\":[\"Use the logo on light backgrounds.\"],\"V4WsyL\":[\"Add Link\"],\"VCA6B2\":[\"These are actual feed components with real footers, summaries, and inline links. Use this section to judge whether the theme still feels calm once it is applied to realistic content.\"],\"VNqFYa\":[\"Loading post...\"],\"VvcACM\":[\"Conditions choose what belongs here, not you. Posts you write later join on their own.\"],\"WCOanD\":[\"This reference is useful because it treats links and citations as part of the reading rhythm. Keep that in mind while tuning the\"],\"WIF6ui\":[\"Continue writing this draft\"],\"WIUZaK\":[\"Nothing matches these conditions yet.\"],\"WbIbzR\":[\"Checking link...\"],\"WcWS//\":[\"Download file\"],\"WhsN3P\":[\"A good default accent in Jant should feel like editorial structure, not product branding. That means links, emphasis, and thread cues can be visible without turning the page into UI chrome.\"],\"Wn+/rH\":[\"Transparent square\"],\"X8QJSU\":[\"What a feed reader does\"],\"XU7b+L\":[\"Primary logo files\"],\"XV1mAn\":[\"Only visible when signed in.\"],\"Xm/s+u\":[\"Display\"],\"XrnWzN\":[\"Published!\"],\"XvjC4F\":[\"Saving...\"],\"Y7WAtz\":[\"An image couldn't be saved to your library — its original link was kept.\"],\"YIix5Y\":[\"Search...\"],\"YOzD/a\":[\"Replace image\"],\"YUglt2\":[\"Generating a link...\"],\"YXiA6e\":[\"Primary button\"],\"Ygx3Yl\":[\"Small browser icon used in tabs and bookmarks.\"],\"Z4OdvO\":[\"Adding…\"],\"Z6NwTi\":[\"Save as Draft\"],\"ZBH98o\":[\"Show as a grid of tiles\"],\"ZV5ykW\":[\"Download PNG\"],\"ZhhOwV\":[\"Quote\"],\"ZmSeP+\":[\"Save to drafts?\"],\"ZuF6fX\":[\"Smart collection saved.\"],\"ZxFuun\":[[\"count\",\"plural\",{\"one\":[\"Found \",\"#\",\" result\"],\"other\":[\"Found \",\"#\",\" results\"]}]],\"a5j82I\":[\"No collections match that search. Try a different name.\"],\"aBFXGQ\":[\"This collection is empty. Add threads from the editor.\"],\"aHTB7P\":[\"Supplementary content attached to your post\"],\"aKSJTV\":[\"Translating\"],\"aMEyv0\":[\"Stay sturdy and readable.\"],\"aN6wx0\":[\"Nothing in Featured yet. Mark a post as featured to show it here.\"],\"aUhIoF\":[\"Delete this smart collection? Its link stops working.\"],\"aYpXKS\":[\"and checking whether the accent is guiding attention or pulling too hard.\"],\"aaGV/9\":[\"New Link\"],\"af+9p6\":[\"Quiet metadata\"],\"an5hVd\":[\"Images\"],\"ao77hr\":[[\"count\",\"plural\",{\"one\":[\"#\",\" hidden post\"],\"other\":[\"#\",\" hidden posts\"]}]],\"auFlOr\":[\"Icons and previews\"],\"avuFKG\":[\"threads\"],\"b+dane\":[\"Insert %rows% by %cols% table\"],\"bFpC86\":[\"Everything in one download\"],\"bGtMpA\":[\"Add a label and URL.\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bLkb/4\":[\"This browser can't keep a local copy. Save as a draft.\"],\"bZ9ges\":[\"Add column before\"],\"bbdNeX\":[\"Sign in\"],\"bfCbdi\":[\"Current post\"],\"bkBJmZ\":[\"This is useful as a color check because it puts the accent next to quotation styling, metadata, and a quieter explanatory paragraph. Compare it back to the\"],\"bzSI52\":[\"Discard\"],\"c0YnrL\":[\"Subscribe\"],\"c2JRUS\":[\"Generate automatically\"],\"cH5kXP\":[\"Now\"],\"cIoW7X\":[\"Inline link\"],\"cTUByn\":[\"Newest first\"],\"cXkHKY\":[\"Open the page this feed carries\"],\"cb7FR8\":[\"White logo on the Jant green square tile for platforms and layouts that expect a true edge-to-edge square.\"],\"cgmi4V\":[\"Delete Draft\"],\"cnGeoo\":[\"Delete\"],\"d+F4pf\":[\"The image should sit quietly inside the article instead of feeling like a card preview.\"],\"d/o/BH\":[\"Couldn't publish. Saved as draft.\"],\"d0DHp4\":[\"Delete this collection permanently? Threads inside won't be removed.\"],\"dD7NPy\":[\"Outline\"],\"dEgA5A\":[\"Cancel\"],\"dUsGbd\":[\"The right accent should disappear into the writing until you need it.\"],\"dXoieq\":[\"Summary\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dbUuAj\":[\"Appears in Latest.\"],\"df4a/r\":[\"Couldn't load this post. Try again.\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"eYZPF3\":[\"/…\"],\"eneWvv\":[\"Draft\"],\"f+m8jj\":[\"Feed URL copied.\"],\"f4MAoA\":[\"Some uploads failed. Saved as draft.\"],\"f5s9EI\":[\"Press N to write\"],\"f6Hub0\":[\"Sort\"],\"f8fH8W\":[\"Design\"],\"fD+f7T\":[\"RSS feed\"],\"fKrDxS\":[\"Brand tile\"],\"fMPkxb\":[\"Show more\"],\"fqDzSu\":[\"Rate\"],\"fttd2R\":[\"My Collection\"],\"fuWzQl\":[\"Nothing here yet. Add threads to one of these collections to fill this view.\"],\"gCcxP/\":[\"Threads can include up to \",[\"count\"],\" posts.\"],\"gFdWl+\":[\"A long-form article sample for checking the default palette in a true reading context.\"],\"gGx5tM\":[\"Editing\"],\"gNKz6Z\":[\"Collection deleted.\"],\"gXH9r/\":[\"Open raw PNG\"],\"gpaPhA\":[\"Helps screen readers describe the image\"],\"h/4eh5\":[[\"count\"],\" of \",[\"total\"],\" threads\"],\"h5RcXU\":[\"Post hidden\"],\"hLlWo5\":[\"A few simple rules.\"],\"hXzOVo\":[\"Next\"],\"hZEpmD\":[\"Posts matching all of these\"],\"hb854a\":[\"Not enough text to tell yet — publishes in \",[\"language\"]],\"he3ygx\":[\"Copy\"],\"heSQoS\":[\"Paste a URL...\"],\"hqeXKW\":[\"Single posts\"],\"hrkGms\":[\"Search\"],\"i5+Y7d\":[\"Download the official Jant logo, icons, and preview files.\"],\"i6kro6\":[\"Edit custom link\"],\"i6nDCI\":[\"Choose a new password.\"],\"iA6v2d\":[\"This site publishes Atom feeds. Copy one of these addresses into a feed reader.\"],\"iG7KNr\":[\"Logo\"],\"iH8pgl\":[\"Back\"],\"iTUxHp\":[\"Read from what you write\"],\"iZYiBK\":[\"newest changes first\"],\"ilSmIt\":[\"Hard edge\"],\"jAXE5p\":[\"Reverse logo\"],\"jAqB/k\":[\"Post privately\"],\"jQflRT\":[\"This uses the real single-post detail rendering with a longer article, inline image, tables, lists, quotes, and code. The content column stays at the same width as the live site.\"],\"jd+8Mm\":[\"Social preview image\"],\"jdJOV1\":[\"Settings\"],\"ji7oVU\":[\"Edit post\"],\"jlr9KB\":[\"A feed reader checks these addresses for new posts and collects them in one place. Most are an app on your phone or computer; some are a website you sign in to.\"],\"jpctdh\":[\"View\"],\"jrsUoG\":[\"Type / for commands\"],\"jvyYZG\":[\"What's on your mind...\"],\"k3Iw35\":[\"Switch to the white logo when the standard green version would lose contrast.\"],\"kI1qVD\":[\"Format\"],\"kNiQp6\":[\"Pinned\"],\"kPMIr+\":[\"Give it a title...\"],\"kXNp07\":[\"Could not open this collection. Try again.\"],\"lETeSG\":[\"Could not copy. Select the address and copy it.\"],\"laT1IJ\":[\"iOS home screen\"],\"lb+Xwx\":[\"Custom link\"],\"m16xKo\":[\"Add\"],\"mFCTqD\":[\"Only the posts marked as featured.\"],\"mKT7g0\":[\"Text attachment\"],\"mc/vLq\":[\"This link is already in use. Choose something else.\"],\"muKqfV\":[\"Featured\"],\"n1ekoW\":[\"Sign In\"],\"n3ReIn\":[\"Collections\"],\"n4rRYL\":[\"Nothing in \",[\"language\"],\" here yet.\"],\"n6QD94\":[\"Oldest first\"],\"nFukaP\":[\"Wrong email or password. Check your credentials and try again.\"],\"nJ4U1U\":[[\"count\"],\" images couldn't be saved to your library — their original links were kept.\"],\"nMXCVH\":[\"No conditions yet. Add one to choose what lands here.\"],\"nV6twc\":[\"Organize\"],\"nd8Puv\":[\"White logo on the Jant green circle for profile images, badges, and other round placements where you want a ready-made asset.\"],\"ndrEYW\":[\"When the accent is slightly warmer and less literal, the whole page feels more like a writing space and less like product UI.\"],\"oO0hKx\":[[\"count\",\"plural\",{\"one\":[\"#\",\" more post\"],\"other\":[\"#\",\" more posts\"]}]],\"oTYNft\":[\"Subscribing creates no account here. To stop, delete the address from your reader.\"],\"oTu7Wt\":[\"Combined Collections\"],\"ode0+L\":[\"Theme sample\"],\"ovBPCi\":[\"Default\"],\"p1Z67P\":[\"When primary is too rigid, the whole page starts reading like product UI instead of writing space.\"],\"p2/GCq\":[\"Confirm Password\"],\"pB0OKE\":[\"New Divider\"],\"pBHx39\":[\"Dark backgrounds\"],\"pKeaoC\":[\"Sort by when each thread was published\"],\"pVrU5x\":[\"If this page feels too branded, the first place to soften is the default theme’s site accent, not the border or body text.\"],\"pvnfJD\":[\"Dark\"],\"q+bMmy\":[\"Table controls\"],\"q+hNag\":[\"Collection\"],\"q5YRzz\":[\"Color check\"],\"q8RviX\":[\"Titled\"],\"qV0ubv\":[\"Also available in\"],\"qc+12k\":[\"Choose table size\"],\"qiN9NB\":[\"Surface\"],\"qt89I8\":[\"Draft saved.\"],\"quvfGs\":[\"instead of judging it as an isolated swatch.\"],\"r7kcaA\":[\"Drag collections, links, and dividers into the order you want.\"],\"rA2TFI\":[\"Switch the palette and mode without opening settings or changing the active site theme.\"],\"rDtrfr\":[\"A smart collection needs a title and a link.\"],\"rV8ZnP\":[\"Edit publish date\"],\"rdU729\":[\"Layout\"],\"rdUucN\":[\"Preview\"],\"s8G5Or\":[\"This upload would exceed your shared hosted media limit. Remove files or upgrade storage to continue.\"],\"s9gHf5\":[\"your-post-link\"],\"sER+bs\":[\"Files\"],\"sQpDn6\":[\"Exit fullscreen\"],\"sR6lTH\":[\"Remove condition\"],\"sgr2wQ\":[\"collection\"],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"syiAKf\":[\"note treatment\"],\"t42hIC\":[\"Everything most people need is in one ZIP.\"],\"tCctex\":[\"The brand pack includes SVG logos, a transparent square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and the default social preview image.\"],\"tKlWWY\":[\"Emoji\"],\"tSWVu5\":[\"Published on \",[\"date\"],\" at \",[\"time\"]],\"tT/bSk\":[\"Add row above\"],\"tZ1tJc\":[\"Edit Navigation\"],\"tfDRzk\":[\"Save\"],\"tg5MRw\":[\"Sign in to start writing.\"],\"tgSBSE\":[\"Remove Link\"],\"uNPC+z\":[\"Couldn't add this collection to navigation. Try again.\"],\"uowbPn\":[\"Remove attachment\"],\"v3E8iS\":[\"A practical checklist\"],\"vClA0M\":[\"Translating into \",[\"language\"]],\"vLyv1R\":[\"Hide\"],\"vSJd18\":[\"Video\"],\"vSYKYI\":[\"Main feed\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vXIe7J\":[\"Language\"],\"vcpc5o\":[\"Close menu\"],\"vdFnYM\":[\"Reset link\"],\"vdvpU5\":[\"/archive?format=quote or https://example.com\"],\"vgpfCi\":[\"Save draft\"],\"vzU4k9\":[\"New Collection\"],\"w0Emel\":[\"Suggested link\"],\"w1bUkO\":[\"Last edited on \",[\"date\"],\" at \",[\"time\"]],\"w6mlns\":[\"Article detail page\"],\"wJ+GRy\":[\"All visibility\"],\"wL3cK8\":[\"Latest\"],\"wMKe2I\":[\"A collection needs a title and a link.\"],\"wja8aL\":[\"Untitled\"],\"wlnK1t\":[\"A single ZIP with the main logo, reverse logo, square PNG, rounded, square, and circle tiles, plus favicon, Apple touch icon, and social preview image.\"],\"wm3Zlr\":[\"All years\"],\"wtFxok\":[\"Move attachment later\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xVrkxi\":[\"quiet design\"],\"xVvw1i\":[\"This reset link is no longer valid. Request a new one to continue.\"],\"xYilR2\":[\"Media\"],\"xeiujy\":[\"Text\"],\"xhTx3y\":[\"Choose the standard logo for most placements and the reverse logo when you need more contrast.\"],\"xjHB3/\":[\"Continue →\"],\"y28hnO\":[\"Post\"],\"y2o/Y0\":[\"This Link Has Expired\"],\"y9p10B\":[\"Couldn't reach the server. Try again.\"],\"yGZVl1\":[\"More\"],\"yGl/vL\":[\"Open the original in a new tab\"],\"yQ2kGp\":[\"Load more\"],\"ycM1Xg\":[\"No results. Try different keywords.\"],\"ye7QWw\":[\"Order by\"],\"yjxohI\":[\"Smart collection deleted.\"],\"ynMAhG\":[\"Default logo\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"zBFr9G\":[\"Paste a long article, AI response, or any text...\\n\\nMarkdown formatting will be preserved.\"],\"zJDAbh\":[\"Don't save\"],\"zcDmsG\":[\"Featured posts\"],\"zoK+eO\":[\"Add a title before posting this link.\"],\"zucql+\":[\"Menu\"],\"zwBp5t\":[\"Private\"]}");
2010
2010
  //#endregion
2011
2011
  //#region src/i18n/locales/settings/en.ts
2012
- var messages$2 = JSON.parse("{\"+4Z6iP\":[\"Create the repository on GitHub first — it can be empty.\"],\"+9JI/F\":[\"Connecting will sync your site onto \",[\"repo\"],\"'s default branch on top of its existing history. Existing files outside Jant's managed paths are kept. This can't be undone.\"],\"+AXdXp\":[\"Label and URL are required\"],\"+K0AvT\":[\"Disconnect\"],\"+V/jXc\":[\"Links straight to /feed — the raw Atom file, currently your \",[\"feed\"],\" feed. Best for readers who already use a feed reader.\"],\"+VEqyj\":[\"Make primary\"],\"+jXHMG\":[[\"count\",\"plural\",{\"one\":[\"#\",\" post is\"],\"other\":[\"#\",\" posts are\"]}],\" still written in \",[\"language\"],\". Change their language, or keep the language.\"],\"+wgt7C\":[\"Muted red-brown background, earthy and warm.\"],\"+zy2Nq\":[\"Type\"],\"/0D1Xp\":[\"Edit Collection\"],\"/3H2/s\":[\"This hosted site signs in through \",[\"providerLabel\"],\". Manage password and hosted access there.\"],\"/PXXBT\":[\"Nearly white, with a touch of warmth.\"],\"/PoNoq\":[\"Edit link\"],\"/zOUxl\":[\"QR code linking to the Telegram bot\"],\"05DXsb\":[\"Connect or manage the bot used to post from Telegram.\"],\"0OGSSc\":[\"Avatar display updated.\"],\"0UzCUX\":[\"Update the password you use to sign in\"],\"0VVvqu\":[\"Multilingual content is on.\"],\"0bdA9b\":[\"Open Telegram to connect\"],\"0dieZ/\":[\"Which layout /archive opens with. Readers can switch layouts from the page.\"],\"0gECZD\":[\"Want to write a fuller introduction?\"],\"0uIjy/\":[\"Light sage-green with a matching green accent.\"],\"1DahsC\":[\"Search settings\"],\"1F6Mzc\":[\"No navigation items yet. Add links or enable system items below.\"],\"1H7gng\":[\"Dashboard language\"],\"1MYU3o\":[\"Couldn't create the page. Check the details and try again.\"],\"1mbBbL\":[\"Connect manually\"],\"1njn7W\":[\"Light\"],\"1qGdnL\":[\"The default. Calm and easy for long reading.\"],\"1wdDm9\":[\"Add language\"],\"21mg6u\":[\"Choose a titled note that isn't already in navigation, or create a new page.\"],\"2C7mSG\":[\"Collection link\"],\"2DoBvq\":[\"Feeds\"],\"2Et8jU\":[\"Set results per search page; reset to inherit the main page size.\"],\"2FYpfJ\":[\"More\"],\"2Ithfh\":[\"Message the bot any text and it's published as a note.\"],\"2PTjMB\":[\"I want to delete \",[\"siteName\"]],\"2QRni+\":[\"Time zone updated.\"],\"2UrpGC\":[\"Feed address sent. A directory reads a newly announced feed within \",[\"hours\"],\" hours.\"],\"2ZqpRB\":[\"Turn off\"],\"2cFU6q\":[\"Site Footer\"],\"2fPEPI\":[\"A calm, cool backdrop.\"],\"2lkk2l\":[\"Yellow-tinted paper with an olive-green accent.\"],\"2oWZo7\":[\"Last commit\"],\"2r8pkt\":[\"Couldn't load pages. Try again in a moment.\"],\"2uuy4H\":[\"Connected via Personal Access Token\"],\"2wK4BX\":[\"Edit About page\"],\"35x8eZ\":[\"Showing \",[\"shown\"],\" of \",[\"total\"]],\"39QGku\":[\"Open the bot and send the binding code, then anything you message it becomes a note.\"],\"3B0RD1\":[\"Upload the profile image and generated site icons.\"],\"3Cw1AI\":[\"Add Collection\"],\"3VrybB\":[\"Redirect\"],\"3Yvsaz\":[\"302 (Temporary)\"],\"3n0zbB\":[\"Session management is off in demo mode. Use the shared demo session instead.\"],\"3sYJi5\":[\"Download a Hugo-compatible archive — host it statically or move to another Jant.\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"49Bsal\":[\"Feed settings updated.\"],\"4D09NB\":[\"Link to your collections page\"],\"4J/OYU\":[\"Collection created.\"],\"4Jge8E\":[\"Active Sessions\"],\"4KIa+q\":[\"Export downloaded.\"],\"4Q0iPK\":[\"Follow the device appearance or force a light or dark theme.\"],\"4cEClj\":[\"Sessions\"],\"4n7WY4\":[\"Name, visibility, feeds, and time zone\"],\"4tcgrg\":[\"View these posts\"],\"4yyXWu\":[\"Pale bone-white with a muted green accent.\"],\"4zGJ5E\":[\"Delete Account Permanently\"],\"5QlUIt\":[\"Empty repository. Ready to connect.\"],\"5VQnR3\":[\"Use these when you want a feed URL that never changes.\"],\"5dpcN1\":[\"type to search all\"],\"5f1Wo9\":[\"Connected as \",[\"account\"]],\"5o8R81\":[\"Very light ivory with a faint tea-green accent.\"],\"6ArdBh\":[\"Uses featured posts for /feed.\"],\"6DjeBT\":[\"Demo sites always stay hidden from search engines.\"],\"6E3aK4\":[\"Manage Hosting\"],\"6FFB7q\":[\"Uses the latest public posts for /feed.\"],\"6K1Vef\":[\"Delete this blog permanently? This cannot be undone.\"],\"6NpNLc\":[\"This repository has existing content.\"],\"6Pa5AP\":[\"Connect or manage automatic content backups to GitHub.\"],\"6V3Ea3\":[\"Copied\"],\"6WdDG7\":[\"Page\"],\"71WIgc\":[\"Get a new code\"],\"71of4k\":[\"Limit posts included in each RSS feed.\"],\"746NHh\":[\"this blog\"],\"7811AW\":[\"This repository is already backing up this site.\"],\"7FCZPo\":[\"Content language\"],\"7FaY4u\":[\"Usage\"],\"7G9YLi\":[\"Allow search engines to index my site\"],\"7GISOt\":[\"Save bot token\"],\"7MZxzw\":[\"Password changed.\"],\"7p5kLi\":[\"Dashboard\"],\"7vhWI8\":[\"New Password\"],\"81nFIS\":[\"Passwords don't match. Make sure both fields are identical.\"],\"87a/t/\":[\"Label\"],\"89Upyo\":[\"That theme isn't available. Pick another one.\"],\"8BfEpW\":[\"Hosted account\"],\"8NIu3g\":[\"That page has no title yet, so a menu has nothing to show.\"],\"8T46pB\":[\"Bot token\"],\"8U2Z7f\":[\"New Custom URL\"],\"8ZsakT\":[\"Password\"],\"8bpHix\":[\"Couldn't create your account. Check the details and try again.\"],\"8gEghq\":[\"Bright warm-white with a moss-green accent.\"],\"9+vGLh\":[\"Custom CSS\"],\"9As8Nu\":[\"Create one on GitHub\"],\"9EjI4j\":[\"Warm sand\"],\"9Fa2Ns\":[\"The root address goes back to showing every language, and addresses like \",[\"prefix\"],\" redirect to it, so existing links and feed subscriptions keep working. Post addresses and each post's language are unchanged, so you can turn this back on any time.\"],\"9FctRm\":[\"Use lowercase letters, numbers, and hyphens.\"],\"9J6wUO\":[\"Suggested links\"],\"9Lsvt5\":[\"Signed in \",[\"date\"]],\"9SHZas\":[\"Shows 'Settings' when logged in, 'Sign in' when logged out\"],\"9T7Cwm\":[\"Redirects, vanity paths, and URL control\"],\"9aUyym\":[\"See where you're signed in and revoke old sessions\"],\"9mqQnx\":[\"Limit paragraphs used in automatically generated summaries.\"],\"A7tRC9\":[\"Which post stream the canonical /feed URL returns.\"],\"ADTn/D\":[\"Allow published content to be read without an API token.\"],\"AELGTd\":[\"Add it to navigation now or open the editor to add details.\"],\"ANzR96\":[\"Config Editor\"],\"AQL8b1\":[\"What language do you write in?\"],\"AeXO77\":[\"Account\"],\"AnY+O9\":[\"Show \\\"Build with Jant\\\" at the bottom of the home page\"],\"ApZDMk\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square PNG with a solid background at least 512x512 pixels.\"],\"Aysjjh\":[\"Choose the color palette used across Jant.\"],\"B495Gs\":[\"Archive\"],\"B4ESok\":[\"API reference\"],\"B5P9HE\":[\"Announce my site\"],\"BJ0tsF\":[\"Search runtime settings and open advanced configuration\"],\"BTYdze\":[\"Link added to navigation.\"],\"BszQc4\":[\"The root address (/, /feed) shows this language.\"],\"BzEFor\":[\"or\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"CDAdlf\":[\"Remove bot\"],\"CTAEes\":[\"Select a repository\"],\"CjZZgz\":[\"This repository already has commits\"],\"Cl92kR\":[\"Create new collection\"],\"D35jh6\":[\"The language of your admin pages. Only you see this.\"],\"D8k2s6\":[\"Connect Telegram\"],\"DCKkhU\":[\"Current Password\"],\"DKKKeF\":[\"Manage password and hosted access in \",[\"providerLabel\"]],\"DVFXa6\":[\"The cleanest, most neutral option.\"],\"DdXUay\":[\"Show the site avatar in the public header.\"],\"E3hjKv\":[\"Add as link\"],\"EKvXGx\":[\"That address is on another site. Navigation holds it as a link.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"Eax/et\":[\"Accent\"],\"EbDLD+\":[\"Choose the typography used across Jant.\"],\"Enslfm\":[\"Destination\"],\"F53UDu\":[\"Cool white\"],\"F7FKwe\":[\"Anything you paste here has full access to your visitors' browsers. Only use code from sources you trust.\"],\"F8SNqr\":[\"Warm orange\"],\"FBgjRN\":[\"Search pages, or paste an address\"],\"FMUJSP\":[\"Account & Data\"],\"FUfPPw\":[[\"next\"],\" will be served at the root address (/, /feed, /archive), and \",[\"previous\"],\" moves to \",[\"prefix\"],\". Post addresses do not change, but anyone subscribed to /feed starts receiving \",[\"next\"],\" posts.\"],\"Fe1cvJ\":[\"Delay new posts and replies before they appear in feeds. Use 0 to turn this off.\"],\"Fk3SSD\":[\"Calm and natural, easy on the eyes.\"],\"FkMol5\":[\"Featured\"],\"FwL2Zk\":[[\"count\",\"plural\",{\"one\":[\"#\",\" post is\"],\"other\":[\"#\",\" posts are\"]}],\" written in \",[\"language\"],\", which is not on this list. Add it back, or change their language first.\"],\"G/1oP+\":[\"Remove the webhook and stop syncing. Your repository content will not be deleted.\"],\"G0qJsQ\":[\"Security token missing. Refresh the page and try again.\"],\"G0tHaW\":[\"Create a public page that won't appear in Latest.\"],\"G2fuEb\":[\"Locked\"],\"G39wnK\":[\"Back up and sync content with a GitHub repository\"],\"G5oCui\":[\"URL structure, per-language feeds, and linking translations.\"],\"G9peMt\":[\"Create the account you write from.\"],\"GAmD3h\":[\"Languages\"],\"GBkMNw\":[[\"name\"],\" is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts.\"],\"GXsAby\":[\"Revoke\"],\"GdGiNi\":[\"Step \",[\"current\"],\" of \",[\"total\"]],\"GorKul\":[\"Welcome to Jant\"],\"GxkJXS\":[\"Uploading...\"],\"GzKzUa\":[\"Demo limits\"],\"HCNlq3\":[\"Clean and high-contrast, close to white.\"],\"HHDyGw\":[\"Soft green\"],\"HKH+W+\":[\"Data\"],\"Hf/w/z\":[\"Demo sites are never listed in Discover.\"],\"Hp1l6f\":[\"Current\"],\"HxTUmw\":[\"Add \",[\"language\"]],\"HxlY7t\":[\"Changing this updates what subscribers get from /feed.\"],\"HxuOlm\":[\"Site Header\"],\"I0ijRI\":[\"All feed addresses\"],\"I4HRxU\":[\"Every published post, including ones hidden from Latest.\"],\"I6gXOa\":[\"Path\"],\"I76CzF\":[\"Neutral gray\"],\"ID38tA\":[\"Account deletion is off in demo mode. The shared demo resets separately.\"],\"IF9tPu\":[\"When to use site export, database backups, and recovery drills.\"],\"IW5PBo\":[\"Copy Token\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"Repository\"],\"J36Xsm\":[\"Nothing at \",[\"address\"],\". Check it, or search by title.\"],\"J6bLeg\":[\"Add a custom link to any URL\"],\"JL7LF5\":[\"available CSS variables, data attributes, and examples.\"],\"JcD7qf\":[\"More actions\"],\"JjX0OO\":[\"Copy your token now — it won't be shown again.\"],\"JrFTcr\":[\"Connecting…\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"K+0Hu0\":[\"No matching pages available to add. Try another title or create a new page.\"],\"K/F6pa\":[\"Saving…\"],\"KDw4GX\":[\"Try again\"],\"KSgo21\":[\"Pick a repository\"],\"KVVYBh\":[\"Add collection to navigation\"],\"KiJn9B\":[\"Note\"],\"Kk7jwL\":[\"Set posts per archive page; reset to inherit the main page size.\"],\"KwOLJF\":[\"The original Jant palette, and a solid everyday pick.\"],\"L+rMC9\":[\"Reset to default\"],\"L27RpE\":[\"Page created.\"],\"L3DEwT\":[\"Remove this avatar? Your favicon and header icon will go back to the default.\"],\"L4t4/q\":[\"March 14\"],\"L86zmP\":[\"Edit the multi-line introduction used on your home page and in metadata.\"],\"LdyooL\":[\"link\"],\"LhMjLm\":[\"Time\"],\"LjwaJ/\":[\"Sign-in, export, and deletion\"],\"M/D8PK\":[\"+ Install on another account\"],\"M/haSd\":[\"Always show the light version of the theme.\"],\"M/kDIW\":[\"The language your readers and search engines see.\"],\"M1co/O\":[\"Configured\"],\"M2hGyf\":[\"Links to /subscribe, a page listing your feeds with copy buttons. Best for readers who don't already use one.\"],\"M2kIWU\":[\"Font theme\"],\"M4tzVU\":[\"Latest posts\"],\"M6CbAU\":[\"Toggle edit panel\"],\"M7wPsD\":[\"Switch\"],\"MHrjPM\":[\"Title\"],\"MKIM3K\":[\"Search pages\"],\"MaYYE6\":[\"Post notes by messaging a Telegram bot\"],\"Me5t5H\":[\"Connect a GitHub repository to automatically back up your posts as Markdown files. Edits on GitHub sync back to your site.\"],\"MeCJlL\":[\"Announcing your site. Reload to see the result.\"],\"MnbH31\":[\"page\"],\"Mq9FZ1\":[\"Creating page…\"],\"Mr4QPw\":[\"Disconnect Telegram? You can reconnect any time with a new binding code.\"],\"MtENL9\":[\"Tune how your site looks, reads, and runs.\"],\"N/8NPV\":[\"Before deleting, download a site export. You won't be able to recover this account after deletion.\"],\"N7UNHY\":[\"Featured feed\"],\"NHnUHF\":[\"Favicon and the profile mark in your header\"],\"NU2Fqi\":[\"Save CSS\"],\"NVjhde\":[\"Warm parchment\"],\"Nldjdr\":[\"No custom URLs yet. Create one to add redirects or custom paths for posts.\"],\"O3oNi5\":[\"Email\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OSJXFg\":[\"Applies to your entire site, including admin pages. Pick a palette, then choose whether it follows the system or stays fixed.\"],\"OeUWA7\":[\"Add Page\"],\"OuuMXJ\":[\"Add site-wide HTML before the closing body tag.\"],\"Ox3+3h\":[\"No matches.\"],\"P7Eeay\":[\"Full posts\"],\"PEUV5I\":[\"Code injection updated.\"],\"PHh52z\":[\"Warm and rich, with strong brown tones.\"],\"PXj9lw\":[\"Stop accepting posts from Telegram. Your existing notes stay published.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pbm2/N\":[\"Create Collection\"],\"Pwqkdw\":[\"Loading…\"],\"PxJ9W6\":[\"Generate Token\"],\"Q/6Y+2\":[\"Needs Contents (read/write) and Webhooks (read/write) on the target repository.\"],\"Q/O0X4\":[\"This setting wasn't saved. Check the value and try again.\"],\"Q30z/l\":[\"Remove this collection from navigation? The collection itself won't be deleted.\"],\"Q99OtV\":[\"Pin a collection to your navigation bar. An asterisk (*) appears next to collections updated in the last 48 hours.\"],\"QCwsv1\":[\"Warm off-white\"],\"QKvrmL\":[\"Warm-neutral and easy for long reading.\"],\"QZmz0H\":[\"Built-in links\"],\"Qnrzvb\":[\"Active Tokens\"],\"R6Z4LE\":[\"Download failed. Please try again.\"],\"R9Khdg\":[\"Auto\"],\"RDjuBN\":[\"Setup\"],\"RRo9kN\":[\"Language updated.\"],\"RcdDOS\":[\"Create a bot by messaging @BotFather on Telegram, then paste the token it gives you.\"],\"RdVIcf\":[\"Cream and coffee brown\"],\"Ri/9PY\":[\"This site still limits Discover to featured posts, and no post is marked Featured, so your feed carries nothing to show. Untick and tick the box to let Discover read every public post.\"],\"Rn2p1h\":[\"Nothing matches this search. Try a different name or description.\"],\"RxsRD6\":[\"Time Zone\"],\"SDND4q\":[\"Not configured\"],\"SJmfuf\":[\"Site Name\"],\"SKZhW9\":[\"Token name\"],\"SSsoa4\":[\"Add to Navigation\"],\"SVQQPe\":[\"Couldn't connect. Check the error and try again.\"],\"SWb0z+\":[\"That address is reserved. Choose another one.\"],\"SYGk01\":[\"The languages served under a URL prefix. Managed on the Language page.\"],\"SchpMp\":[\"Telegram\"],\"SeY4vo\":[[\"language\"],\" is the only other language on this site, so removing it turns multilingual content off. The root address goes back to showing every language, \",[\"prefix\"],\" stops working, and each post keeps the language it is written in.\"],\"SqKp3o\":[\"The title used across your site, browser tabs, and feeds.\"],\"SrGs4T\":[\"Light blue-gray background, cool and even.\"],\"T/R+Qz\":[\"Primary\"],\"T17dXm\":[\"Whether each language gets its own home page, archive, and feed. Managed on the Language page.\"],\"T41PG1\":[\"Limit characters used in automatically generated summaries.\"],\"TN0mN4\":[\"Search and edit runtime settings. Changes apply immediately; reset restores the environment or built-in default.\"],\"TSCeuF\":[\"Couldn't create the collection. Check the details and try again.\"],\"TpF3v+\":[\"Injected before </head>. Use for analytics, custom meta tags, and styles that must load early.\"],\"Tu6bMZ\":[\"Create About page\"],\"Tz0i8g\":[\"Settings\"],\"U5J7jI\":[\"Site visibility updated.\"],\"U5v6Gh\":[\"Edit Page\"],\"UFK415\":[\"Site-wide HTML for analytics and widgets\"],\"UTvFQq\":[\"Open \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" and send:\"],\"UUn+Y5\":[\"Open setting\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UsODUn\":[\"Select an account\"],\"UxKoFf\":[\"Navigation\"],\"UyMSeQ\":[\"At that address\"],\"V+bhUy\":[\"Install GitHub App\"],\"V0fyg5\":[\"Collection added to navigation.\"],\"V4WsyL\":[\"Add Link\"],\"V5pZwT\":[\"Search settings updated.\"],\"V7dQi9\":[\"Language removed.\"],\"VXUPla\":[\"Connect with GitHub App\"],\"Vh+JIV\":[\"Multilingual guide\"],\"VhMDMg\":[\"Change Password\"],\"Vn3jYy\":[\"Navigation items\"],\"VoZYGU\":[\"This will permanently delete all your data — posts, media, collections, settings, and your account. Your blog will be reset to its initial setup state. This cannot be undone.\"],\"VoarBQ\":[\"Post addresses do not change, and you can turn this off again at any time.\"],\"VqyJ7Y\":[\"Primary language changed.\"],\"WL7KjI\":[\"Tile grid\"],\"WUnFK2\":[\"Pale cool-white with deep indigo text. High contrast.\"],\"Wa9q4P\":[\"Pure white\"],\"Wb7EHo\":[\"About page\"],\"Weq9zb\":[\"General\"],\"Wi9i06\":[\"Follow each visitor's system preference.\"],\"Wildi8\":[\"No pages available. Create one to add it to navigation.\"],\"Wx1M8N\":[\"Install the GitHub App to grant access without managing personal tokens. Permissions are scoped per repository and revocable from GitHub.\"],\"X+8FMk\":[\"Current password doesn't match. Try again.\"],\"X1G9eY\":[\"Navigation Preview\"],\"X2Cbc2\":[\"Archive feed\"],\"X5P6yv\":[\"Recently updated\"],\"X9Hujr\":[\"Manual Push\"],\"XQQOyj\":[\"That page is a draft. Publish it, then add it.\"],\"XeRkls\":[[\"count\",\"plural\",{\"one\":[\"#\",\" existing post\"],\"other\":[\"#\",\" existing posts\"]}],\" with no language yet will be marked as \",[\"language\"],\".\"],\"Xsu5WL\":[\"Add site-wide CSS in the dedicated code editor.\"],\"XtBJV8\":[\"Checking repository…\"],\"Xtc16w\":[\"Refresh repository list\"],\"Y+7JGK\":[\"Create Page\"],\"Y/F35r\":[\"Create a post with curl:\"],\"Y/N5N7\":[\"Soft cream background with a muted green accent.\"],\"YF6zHf\":[\"Site settings updated.\"],\"YVcVlW\":[\"Remove and turn off\"],\"Ya/FAk\":[\"Add at least one more language to turn this on.\"],\"YdG2RF\":[\"Export Site\"],\"YkgZi7\":[\"Connect a Telegram bot, then anything you message it gets published as a note.\"],\"YwhjRx\":[\"Manage Account\"],\"Yxp859\":[\"Warm cream\"],\"Z5HWHd\":[\"On\"],\"ZDY7Fy\":[\"Syncing…\"],\"ZQKLI1\":[\"Danger Zone\"],\"ZS/CBL\":[\"Delete this navigation link? Visitors won't see it in your site header anymore.\"],\"ZXZrAo\":[\"Keep the page address under 200 characters.\"],\"ZgZX+d\":[\"Pure white with neutral grays. No color tint.\"],\"Zgq+c2\":[\"Set the default number of items shown per page.\"],\"ZhhOwV\":[\"Quote\"],\"ZiooJI\":[\"API Tokens\"],\"Zm7Qb0\":[\"Backup & Restore Guide\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"a14mj8\":[\"Unknown device\"],\"a1iEgy\":[\"Warm cream background with deep coffee-brown accents.\"],\"a3LDKx\":[\"Security\"],\"aAIQg2\":[\"Appearance\"],\"aFkzVF\":[\"The slug of the target post or collection\"],\"aR3U86\":[\"Multilingual content is on. \",[\"count\",\"plural\",{\"one\":[\"#\",\" post was marked\"],\"other\":[\"#\",\" posts were marked\"]}],\" as \",[\"language\"],\".\"],\"aV6XWN\":[\"Multilingual content is off. Your languages are still saved.\"],\"alKG0+\":[\"Font Theme\"],\"anibOb\":[\"About this blog\"],\"any7NR\":[\"Theming guide\"],\"asq0vL\":[\"The language you write in.\"],\"b+/jO6\":[\"301 (Permanent)\"],\"b+FyBD\":[\"Add page to navigation\"],\"b1FYSK\":[\"Primary language\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bHYIks\":[\"Sign Out\"],\"bV2vng\":[\"Warm terracotta\"],\"bbR5vW\":[\"This palette\"],\"bbzY7X\":[\"Manage sign-in security, site exports, and irreversible actions.\"],\"bfHZ7r\":[\"Choose the time zone used to display dates and times.\"],\"bi10qS\":[\"Add it to navigation now or open the editor to add content.\"],\"bkMuwo\":[\"Link to posts you've marked as featured.\"],\"bmrL08\":[\"Demo mode hides sessions, password changes, and account deletion. Export still works.\"],\"bph1Dd\":[\"Search engine indexing is off, so this site is not listed by default. Ticking the box above lists it anyway.\"],\"brfpw9\":[\"Edit the multi-line footer rendered at the bottom of public pages.\"],\"bviiKV\":[\"Headings, body text, and links in this theme.\"],\"c1BGrV\":[\"Already in navigation. Drag it in the list above to move it.\"],\"c1iSrq\":[\"Discover community rules\"],\"c3MN2z\":[\"all available endpoints and request formats.\"],\"cHh/Zu\":[\"Turn on multilingual content\"],\"cS7/bk\":[\"Remove the saved bot token? Its webhook is deleted and any connected account is disconnected.\"],\"cSDy01\":[\"Custom CSS updated.\"],\"clzoNp\":[\"Always show the dark version of the theme.\"],\"cnGeoo\":[\"Delete\"],\"cwaLCZ\":[\"Page added to navigation.\"],\"d1lzY/\":[\"Language added.\"],\"d3FRkY\":[\"Could not copy. Try again.\"],\"d5oGUo\":[\"Create a new repository on GitHub\"],\"dB1Nr4\":[\"Start writing\"],\"dEgA5A\":[\"Cancel\"],\"dTXUY+\":[\"Confirm account deletion\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"Permanently delete all data and reset the blog\"],\"ds0nJk\":[\"Add site-wide HTML before the closing head tag.\"],\"dsWkIw\":[\"Disconnect from GitHub? The webhook will be removed. Your repository content will not be deleted.\"],\"dwcK1n\":[\"Or submit your address by hand\"],\"e/tSI5\":[\"Navigation order updated.\"],\"eOL7vn\":[\"Minimal color, low distraction.\"],\"ePK91l\":[\"Edit\"],\"ebQKK7\":[\"Site\"],\"eeamrQ\":[\"Discover reads your Atom feed, so it needs feeds turned on.\"],\"egK+Yy\":[\"Bearer tokens for scripts and automation\"],\"egOXl0\":[\"Jant Discover\"],\"ehj/zN\":[\"Redirect Type\"],\"ej6Rji\":[\"Any post written in another language can be corrected from its own menu afterwards.\"],\"eneWvv\":[\"Draft\"],\"erTMh7\":[\"Last synced\"],\"f+m8jj\":[\"Feed URL copied.\"],\"f8fH8W\":[\"Design\"],\"fDz6PV\":[\"Choose the repository used for content backups.\"],\"fKRAwQ\":[\"Sandy background with a green accent.\"],\"fWYqkz\":[\"Code Injection\"],\"fYXQnC\":[\"Cozy and bold among the warm tones.\"],\"fttd2R\":[\"My Collection\"],\"gZ5owP\":[\"Search repositories\"],\"gbqbh6\":[\"Safe to leave this page — syncing continues in the background.\"],\"gkFvVN\":[\"Injected before </body>. Use for chat widgets and scripts that should not block page load.\"],\"gtQsRO\":[\"Create Custom URL\"],\"hBO/y4\":[\"Security token expired. Refresh the page and try again.\"],\"hGmyDl\":[\"Tokens let you access the API from scripts, shortcuts, and other tools without signing in.\"],\"hIHkRy\":[\"Connected via GitHub App\"],\"hJHHsU\":[\"Publish Atom feeds for the site, archive, and collections.\"],\"hcH8MY\":[\"Set up your site\"],\"hdSi1b\":[\"Type \",[\"repo\"],\" to confirm\"],\"he3ygx\":[\"Copy\"],\"hjeS3W\":[\"Link to your latest posts. Your homepage shows this feed.\"],\"hjvSlw\":[\"Language removed. Multilingual content is off.\"],\"hvGGMK\":[\"Remove this page from navigation? The page itself won't be deleted.\"],\"i0qMbr\":[\"Home\"],\"iEUzMn\":[\"system\"],\"iH8pgl\":[\"Back\"],\"iMkWoF\":[\"Turn on\"],\"iSLIjg\":[\"Connect\"],\"iVOMRi\":[\"Home settings updated.\"],\"icB4Cv\":[\"Drag links here to show them under the More menu\"],\"id3vuh\":[\"Telegram is set up, but the bot couldn't be reached. Check the bot token and try again.\"],\"idD8Ev\":[\"Saved\"],\"ihn4zD\":[\"Search…\"],\"iiDXZc\":[\"Displayed at the bottom of all posts and pages.\"],\"itS31B\":[\"A warmer look, like slightly aged paper.\"],\"iwQZvS\":[\"Cool blue-gray\"],\"j4VrG6\":[\"Download Export ZIP\"],\"j5nQL2\":[\"e.g. iOS Shortcuts\"],\"jNE7O+\":[\"Nothing published yet. jant.me lists a blog once it has \",[\"minCount\",\"plural\",{\"one\":[\"one public post\"],\"other\":[\"#\",\" public posts\"]}],\".\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jdVYS8\":[\"Pick the one that fits your writing.\"],\"jgBjXJ\":[\"Revoke this token? Any scripts using it will stop working.\"],\"jpctdh\":[\"View\"],\"k1ifdL\":[\"Processing...\"],\"kLw03Y\":[\"Bright paper white\"],\"kMXclu\":[\"Download a site export\"],\"kNiQp6\":[\"Pinned\"],\"kQ3Otm\":[\"Create new page\"],\"kRhzWq\":[\"GitHub Sync\"],\"kVQs7s\":[\"Fine-grained styling overrides\"],\"ke1gWS\":[\"Custom URLs\"],\"kfcRb0\":[\"Avatar\"],\"kp8wiR\":[\"Checking address…\"],\"kxDZ2i\":[\"This code runs on every page of your site.\"],\"kzvWob\":[\"Link to the post archive\"],\"lLW3vJ\":[\"Target Slug\"],\"lLsfOE\":[\"Turn off multilingual content?\"],\"lV04bQ\":[\"Warm and earthy, but still light.\"],\"lYHJih\":[\"Revoke this session? That device will need to sign in again.\"],\"m16xKo\":[\"Add\"],\"mLOk1i\":[\"Push all posts to GitHub right now instead of waiting for the next automatic sync.\"],\"mSNmrX\":[\"List posts:\"],\"n8+EXe\":[\"Add common destinations that already exist on your site.\"],\"nG2qTk\":[\"Example link\"],\"nK07ni\":[\"Choose a typographic direction for your site. Each theme changes both the font pairing and the reading rhythm.\"],\"nbfdhU\":[\"Integrations\"],\"ntJYyh\":[\"Domains, plan, and billing in \",[\"providerLabel\"]],\"o/vNDE\":[\"lets you override any theme variable.\"],\"o5rj+L\":[\"No collections yet. Create one here to add it to navigation.\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"We'll prefill the name \",[\"name\"],\". The list refreshes on return.\"],\"oKOOsY\":[\"Color Theme\"],\"oL535e\":[\"Not synced yet\"],\"oNA4If\":[\"All collections are already in your navigation.\"],\"oUn9Z7\":[\"Near-neutral light gray with a steel-blue accent.\"],\"ofdy2l\":[\"Orange-tinted background. The warmest palette.\"],\"olouYg\":[\"Navigation holds that address as a link.\"],\"oxj6Pk\":[\"Other languages\"],\"pIpTqF\":[\"Give each language its own home page, archive, and feed. Once it's on, you pick a language when you publish, and can link an existing post to a version of it in another language.\"],\"pNXxtS\":[\"Choose the content language announced to readers and search engines.\"],\"pZq3aX\":[\"Upload failed. Please try again.\"],\"pgJImo\":[\"Change the primary language?\"],\"pgTIrt\":[\"Choose the GitHub account and repository to sync with this site.\"],\"psoxDF\":[\"That font theme isn't available. Pick another one.\"],\"pt4OhQ\":[\"/about is already used. Rename that item before creating an About page.\"],\"pvnfJD\":[\"Dark\"],\"q+hNag\":[\"Collection\"],\"q/T5GS\":[\"The language used by the private Jant dashboard.\"],\"qSgO/Y\":[\"Follow content language\"],\"qZ9jzv\":[\"Site visibility\"],\"qdcESc\":[\"Create a new repository\"],\"qkSJzV\":[\"Searching pages…\"],\"r5EW6f\":[\"This repository is already backing up another Jant site (\",[\"host\"],\"). Pick a different repository.\"],\"rCPrbS\":[\"Options for \",[\"language\"]],\"rEspiY\":[\"Navigation placement updated.\"],\"rFmBG3\":[\"Color theme\"],\"re0mF0\":[\"The page is public but stays out of Latest.\"],\"rlonmB\":[\"Couldn't delete. Try again in a moment.\"],\"s3jOk7\":[\"Enter a page title.\"],\"sROtqR\":[\"The directory could not be reached: \",[\"reason\"]],\"sRmGLp\":[\"You can change all of this later in Settings.\"],\"satWc6\":[\"Main RSS feed\"],\"sgr2wQ\":[\"collection\"],\"sj4LLc\":[\"Show only modified\"],\"sjej3N\":[\"Remove \",[\"language\"]],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"soRdOu\":[\"Off-white paper with a deep forest-green accent.\"],\"sqxcaY\":[\"Created \",[\"date\"]],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"t3hvHq\":[\"Sync Now\"],\"tJ4H0O\":[\"your Telegram account\"],\"tfDRzk\":[\"Save\"],\"tgWuMB\":[\"Modified\"],\"tvgAq5\":[\"No accounts authorized yet\"],\"u1VTd3\":[\"Palette, surface tone, and overall mood\"],\"u3wRF+\":[\"Published\"],\"u6KOjV\":[\"Want more control?\"],\"uKLe0J\":[[\"count\"],\" settings shown\"],\"uVZcIA\":[\"That address is already in use. Choose another one.\"],\"udPwLB\":[\"Header\"],\"ugHDtd\":[[\"about\"],\" A post you mark Featured appears on the Discover home page \",[\"hours\"],\" hours later, and link and quote posts appear on the \",[\"links\"],\" and \",[\"quotes\"],\" lists \",[\"hours\"],\" hours after they are published. You can keep editing them in the meantime. See the \",[\"rules\"],\".\"],\"ui6aMF\":[\"These devices are currently signed in to your account. Revoke any session you don't recognize.\"],\"vBEKwo\":[\"Manage this site's active sessions here. Password and hosted access are managed through \",[\"providerLabel\"],\".\"],\"vOnuOa\":[\"Show or hide built-in destinations in the header and More menu.\"],\"vRldcl\":[\"Typography choices and reading texture\"],\"vSYKYI\":[\"Main feed\"],\"vTuib7\":[\"This controls what /feed returns.\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vXIe7J\":[\"Language\"],\"vXsx3x\":[\"That page is private, so nobody could open it from a menu.\"],\"vdFnYM\":[\"Reset link\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vpSPA1\":[\"Auth secret is missing. Check your environment variables.\"],\"vzX5FB\":[\"Delete Account\"],\"w615zC\":[\"Multilingual content\"],\"w8Rv8T\":[\"Label is required\"],\"wL3cK8\":[\"Latest\"],\"wW6NCp\":[\"Last error\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wuLtXn\":[\"No active sessions right now. Signed-in devices show up here.\"],\"x+HGBk\":[\"Ask search engines not to index public pages or include them in results.\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xCbjQn\":[\"Allow \",[\"name\"],\" to list my site\"],\"xGVfLh\":[\"Continue\"],\"xHt036\":[\"Personal Access Token\"],\"xKeZ0l\":[\"An earthier, warmer tone.\"],\"xYbozN\":[\"Soft ivory\"],\"xqViCq\":[\"Warm ivory\"],\"xxaahd\":[\"When you want a cooler, sharper look.\"],\"y/awtV\":[\"Header links, built-in destinations, and overflow menu\"],\"y28hnO\":[\"Post\"],\"y9W9vo\":[\"Creating collection…\"],\"yNCqOt\":[\"Latest feed\"],\"yQ3kNF\":[\"Type the following phrase to confirm:\"],\"ydq1k2\":[\"Pick an account first\"],\"yjjCV8\":[\"Fixed feed URLs\"],\"yjkELF\":[\"Confirm New Password\"],\"ym+ccl\":[\"Show the Jant credit on the home page.\"],\"yzF66j\":[\"Link\"],\"z6wakA\":[\"A short intro shown on your home page.\"],\"zEizrk\":[\"Last used \",[\"date\"]],\"zPqly9\":[\"This site has no directory to announce to.\"],\"zSURJW\":[\"No repositories match.\"],\"zUlHGd\":[\"Page address\"],\"zYRGIR\":[\"Not announced yet. No directory has been told this site exists.\"],\"zcDmsG\":[\"Featured posts\"],\"zlcDd2\":[\"Delete this custom URL? Visitors using it won't be redirected anymore.\"],\"ztPYIR\":[\"One-time change to your existing posts\"],\"zwBp5t\":[\"Private\"],\"zxexio\":[\"Content language, dashboard language, multilingual\"]}");
2012
+ var messages$2 = JSON.parse("{\"+4Z6iP\":[\"Create the repository on GitHub first — it can be empty.\"],\"+9JI/F\":[\"Connecting will sync your site onto \",[\"repo\"],\"'s default branch on top of its existing history. Existing files outside Jant's managed paths are kept. This can't be undone.\"],\"+AXdXp\":[\"Label and URL are required\"],\"+K0AvT\":[\"Disconnect\"],\"+V/jXc\":[\"Links straight to /feed — the raw Atom file, currently your \",[\"feed\"],\" feed. Best for readers who already use a feed reader.\"],\"+VEqyj\":[\"Make primary\"],\"+jXHMG\":[[\"count\",\"plural\",{\"one\":[\"#\",\" post is\"],\"other\":[\"#\",\" posts are\"]}],\" still written in \",[\"language\"],\". Change their language, or keep the language.\"],\"+wgt7C\":[\"Muted red-brown background, earthy and warm.\"],\"+zy2Nq\":[\"Type\"],\"/0D1Xp\":[\"Edit Collection\"],\"/3H2/s\":[\"This hosted site signs in through \",[\"providerLabel\"],\". Manage password and hosted access there.\"],\"/PXXBT\":[\"Nearly white, with a touch of warmth.\"],\"/PoNoq\":[\"Edit link\"],\"/zOUxl\":[\"QR code linking to the Telegram bot\"],\"05DXsb\":[\"Connect or manage the bot used to post from Telegram.\"],\"0OGSSc\":[\"Avatar display updated.\"],\"0UzCUX\":[\"Update the password you use to sign in\"],\"0VVvqu\":[\"Multilingual content is on.\"],\"0bdA9b\":[\"Open Telegram to connect\"],\"0dieZ/\":[\"Which layout /archive opens with. Readers can switch layouts from the page.\"],\"0gECZD\":[\"Want to write a fuller introduction?\"],\"0uIjy/\":[\"Light sage-green with a matching green accent.\"],\"1DahsC\":[\"Search settings\"],\"1F6Mzc\":[\"No navigation items yet. Add links or enable system items below.\"],\"1H7gng\":[\"Dashboard language\"],\"1MYU3o\":[\"Couldn't create the page. Check the details and try again.\"],\"1mbBbL\":[\"Connect manually\"],\"1njn7W\":[\"Light\"],\"1pD+7W\":[\"Demo sites are never listed in \",[\"name\"],\".\"],\"1qGdnL\":[\"The default. Calm and easy for long reading.\"],\"1wdDm9\":[\"Add language\"],\"21mg6u\":[\"Choose a titled note that isn't already in navigation, or create a new page.\"],\"2C7mSG\":[\"Collection link\"],\"2DoBvq\":[\"Feeds\"],\"2Et8jU\":[\"Set results per search page; reset to inherit the main page size.\"],\"2FYpfJ\":[\"More\"],\"2Ithfh\":[\"Message the bot any text and it's published as a note.\"],\"2PTjMB\":[\"I want to delete \",[\"siteName\"]],\"2QRni+\":[\"Time zone updated.\"],\"2UrpGC\":[\"Feed address sent. A directory reads a newly announced feed within \",[\"hours\"],\" hours.\"],\"2ZqpRB\":[\"Turn off\"],\"2cFU6q\":[\"Site Footer\"],\"2fPEPI\":[\"A calm, cool backdrop.\"],\"2lkk2l\":[\"Yellow-tinted paper with an olive-green accent.\"],\"2oWZo7\":[\"Last commit\"],\"2r8pkt\":[\"Couldn't load pages. Try again in a moment.\"],\"2uuy4H\":[\"Connected via Personal Access Token\"],\"2wK4BX\":[\"Edit About page\"],\"35x8eZ\":[\"Showing \",[\"shown\"],\" of \",[\"total\"]],\"39QGku\":[\"Open the bot and send the binding code, then anything you message it becomes a note.\"],\"3B0RD1\":[\"Upload the profile image and generated site icons.\"],\"3Cw1AI\":[\"Add Collection\"],\"3VrybB\":[\"Redirect\"],\"3Yvsaz\":[\"302 (Temporary)\"],\"3n0zbB\":[\"Session management is off in demo mode. Use the shared demo session instead.\"],\"3sYJi5\":[\"Download a Hugo-compatible archive — host it statically or move to another Jant.\"],\"3vMdv3\":[\"This link is reserved. Choose something else.\"],\"3wKq0C\":[\"Couldn't save. Try again in a moment.\"],\"49Bsal\":[\"Feed settings updated.\"],\"4D09NB\":[\"Link to your collections page\"],\"4J/OYU\":[\"Collection created.\"],\"4Jge8E\":[\"Active Sessions\"],\"4KIa+q\":[\"Export downloaded.\"],\"4Q0iPK\":[\"Follow the device appearance or force a light or dark theme.\"],\"4cEClj\":[\"Sessions\"],\"4n7WY4\":[\"Name, visibility, feeds, and time zone\"],\"4tcgrg\":[\"View these posts\"],\"4yyXWu\":[\"Pale bone-white with a muted green accent.\"],\"4zGJ5E\":[\"Delete Account Permanently\"],\"5QlUIt\":[\"Empty repository. Ready to connect.\"],\"5VQnR3\":[\"Use these when you want a feed URL that never changes.\"],\"5dpcN1\":[\"type to search all\"],\"5f1Wo9\":[\"Connected as \",[\"account\"]],\"5o8R81\":[\"Very light ivory with a faint tea-green accent.\"],\"6ArdBh\":[\"Uses featured posts for /feed.\"],\"6DjeBT\":[\"Demo sites always stay hidden from search engines.\"],\"6E3aK4\":[\"Manage Hosting\"],\"6FFB7q\":[\"Uses the latest public posts for /feed.\"],\"6K1Vef\":[\"Delete this blog permanently? This cannot be undone.\"],\"6NpNLc\":[\"This repository has existing content.\"],\"6Pa5AP\":[\"Connect or manage automatic content backups to GitHub.\"],\"6V3Ea3\":[\"Copied\"],\"6WdDG7\":[\"Page\"],\"71WIgc\":[\"Get a new code\"],\"71of4k\":[\"Limit posts included in each RSS feed.\"],\"746NHh\":[\"this blog\"],\"7811AW\":[\"This repository is already backing up this site.\"],\"7FCZPo\":[\"Content language\"],\"7FaY4u\":[\"Usage\"],\"7G9YLi\":[\"Allow search engines to index my site\"],\"7GISOt\":[\"Save bot token\"],\"7MZxzw\":[\"Password changed.\"],\"7p5kLi\":[\"Dashboard\"],\"7vhWI8\":[\"New Password\"],\"81nFIS\":[\"Passwords don't match. Make sure both fields are identical.\"],\"87a/t/\":[\"Label\"],\"89Upyo\":[\"That theme isn't available. Pick another one.\"],\"8BfEpW\":[\"Hosted account\"],\"8NIu3g\":[\"That page has no title yet, so a menu has nothing to show.\"],\"8T46pB\":[\"Bot token\"],\"8U2Z7f\":[\"New Custom URL\"],\"8ZsakT\":[\"Password\"],\"8bpHix\":[\"Couldn't create your account. Check the details and try again.\"],\"8gEghq\":[\"Bright warm-white with a moss-green accent.\"],\"9+vGLh\":[\"Custom CSS\"],\"9As8Nu\":[\"Create one on GitHub\"],\"9EjI4j\":[\"Warm sand\"],\"9Fa2Ns\":[\"The root address goes back to showing every language, and addresses like \",[\"prefix\"],\" redirect to it, so existing links and feed subscriptions keep working. Post addresses and each post's language are unchanged, so you can turn this back on any time.\"],\"9FctRm\":[\"Use lowercase letters, numbers, and hyphens.\"],\"9J6wUO\":[\"Suggested links\"],\"9Lsvt5\":[\"Signed in \",[\"date\"]],\"9SHZas\":[\"Shows 'Settings' when logged in, 'Sign in' when logged out\"],\"9T7Cwm\":[\"Redirects, vanity paths, and URL control\"],\"9aUyym\":[\"See where you're signed in and revoke old sessions\"],\"9mqQnx\":[\"Limit paragraphs used in automatically generated summaries.\"],\"A7tRC9\":[\"Which post stream the canonical /feed URL returns.\"],\"ADTn/D\":[\"Allow published content to be read without an API token.\"],\"AELGTd\":[\"Add it to navigation now or open the editor to add details.\"],\"ANzR96\":[\"Config Editor\"],\"AQL8b1\":[\"What language do you write in?\"],\"AeXO77\":[\"Account\"],\"AnY+O9\":[\"Show \\\"Build with Jant\\\" at the bottom of the home page\"],\"ApZDMk\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square PNG with a solid background at least 512x512 pixels.\"],\"Aysjjh\":[\"Choose the color palette used across Jant.\"],\"B495Gs\":[\"Archive\"],\"B4ESok\":[\"API reference\"],\"B5P9HE\":[\"Announce my site\"],\"BJ0tsF\":[\"Search runtime settings and open advanced configuration\"],\"BTYdze\":[\"Link added to navigation.\"],\"BszQc4\":[\"The root address (/, /feed) shows this language.\"],\"BzEFor\":[\"or\"],\"C0/57J\":[\"This is the last part of the collection link.\"],\"CDAdlf\":[\"Remove bot\"],\"CTAEes\":[\"Select a repository\"],\"CjZZgz\":[\"This repository already has commits\"],\"Cl92kR\":[\"Create new collection\"],\"D35jh6\":[\"The language of your admin pages. Only you see this.\"],\"D8k2s6\":[\"Connect Telegram\"],\"DCKkhU\":[\"Current Password\"],\"DKKKeF\":[\"Manage password and hosted access in \",[\"providerLabel\"]],\"DVFXa6\":[\"The cleanest, most neutral option.\"],\"DdXUay\":[\"Show the site avatar in the public header.\"],\"E3hjKv\":[\"Add as link\"],\"EKvXGx\":[\"That address is on another site. Navigation holds it as a link.\"],\"EO3I6h\":[\"Upload didn't go through. Try again in a moment.\"],\"Eax/et\":[\"Accent\"],\"EbDLD+\":[\"Choose the typography used across Jant.\"],\"Enslfm\":[\"Destination\"],\"F53UDu\":[\"Cool white\"],\"F7FKwe\":[\"Anything you paste here has full access to your visitors' browsers. Only use code from sources you trust.\"],\"F8SNqr\":[\"Warm orange\"],\"FBgjRN\":[\"Search pages, or paste an address\"],\"FMUJSP\":[\"Account & Data\"],\"FUfPPw\":[[\"next\"],\" will be served at the root address (/, /feed, /archive), and \",[\"previous\"],\" moves to \",[\"prefix\"],\". Post addresses do not change, but anyone subscribed to /feed starts receiving \",[\"next\"],\" posts.\"],\"Fe1cvJ\":[\"Delay new posts and replies before they appear in feeds. Use 0 to turn this off.\"],\"Fk3SSD\":[\"Calm and natural, easy on the eyes.\"],\"FkMol5\":[\"Featured\"],\"FwL2Zk\":[[\"count\",\"plural\",{\"one\":[\"#\",\" post is\"],\"other\":[\"#\",\" posts are\"]}],\" written in \",[\"language\"],\", which is not on this list. Add it back, or change their language first.\"],\"G/1oP+\":[\"Remove the webhook and stop syncing. Your repository content will not be deleted.\"],\"G0qJsQ\":[\"Security token missing. Refresh the page and try again.\"],\"G0tHaW\":[\"Create a public page that won't appear in Latest.\"],\"G2fuEb\":[\"Locked\"],\"G39wnK\":[\"Back up and sync content with a GitHub repository\"],\"G5oCui\":[\"URL structure, per-language feeds, and linking translations.\"],\"G9peMt\":[\"Create the account you write from.\"],\"GAmD3h\":[\"Languages\"],\"GBkMNw\":[[\"name\"],\" is a directory of Jant blogs, curated by hand by the Jant community to help people find new Jant blogs and posts.\"],\"GXsAby\":[\"Revoke\"],\"GdGiNi\":[\"Step \",[\"current\"],\" of \",[\"total\"]],\"GorKul\":[\"Welcome to Jant\"],\"GxkJXS\":[\"Uploading...\"],\"GzKzUa\":[\"Demo limits\"],\"HCNlq3\":[\"Clean and high-contrast, close to white.\"],\"HHDyGw\":[\"Soft green\"],\"HKH+W+\":[\"Data\"],\"Hp1l6f\":[\"Current\"],\"HxTUmw\":[\"Add \",[\"language\"]],\"HxlY7t\":[\"Changing this updates what subscribers get from /feed.\"],\"HxuOlm\":[\"Site Header\"],\"I0ijRI\":[\"All feed addresses\"],\"I4HRxU\":[\"Every published post, including ones hidden from Latest.\"],\"I6gXOa\":[\"Path\"],\"I76CzF\":[\"Neutral gray\"],\"ID38tA\":[\"Account deletion is off in demo mode. The shared demo resets separately.\"],\"IF9tPu\":[\"When to use site export, database backups, and recovery drills.\"],\"IW5PBo\":[\"Copy Token\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"Repository\"],\"J36Xsm\":[\"Nothing at \",[\"address\"],\". Check it, or search by title.\"],\"J6bLeg\":[\"Add a custom link to any URL\"],\"JL7LF5\":[\"available CSS variables, data attributes, and examples.\"],\"JcD7qf\":[\"More actions\"],\"JjX0OO\":[\"Copy your token now — it won't be shown again.\"],\"JrFTcr\":[\"Connecting…\"],\"JuN5GC\":[\"No file selected. Choose a file to upload.\"],\"K+0Hu0\":[\"No matching pages available to add. Try another title or create a new page.\"],\"K/F6pa\":[\"Saving…\"],\"KDw4GX\":[\"Try again\"],\"KSgo21\":[\"Pick a repository\"],\"KVVYBh\":[\"Add collection to navigation\"],\"KiJn9B\":[\"Note\"],\"Kk7jwL\":[\"Set posts per archive page; reset to inherit the main page size.\"],\"KwOLJF\":[\"The original Jant palette, and a solid everyday pick.\"],\"L+rMC9\":[\"Reset to default\"],\"L27RpE\":[\"Page created.\"],\"L3DEwT\":[\"Remove this avatar? Your favicon and header icon will go back to the default.\"],\"L4t4/q\":[\"March 14\"],\"L86zmP\":[\"Edit the multi-line introduction used on your home page and in metadata.\"],\"LdyooL\":[\"link\"],\"LhMjLm\":[\"Time\"],\"LjwaJ/\":[\"Sign-in, export, and deletion\"],\"M/D8PK\":[\"+ Install on another account\"],\"M/haSd\":[\"Always show the light version of the theme.\"],\"M/kDIW\":[\"The language your readers and search engines see.\"],\"M1co/O\":[\"Configured\"],\"M2hGyf\":[\"Links to /subscribe, a page listing your feeds with copy buttons. Best for readers who don't already use one.\"],\"M2kIWU\":[\"Font theme\"],\"M4tzVU\":[\"Latest posts\"],\"M6CbAU\":[\"Toggle edit panel\"],\"M7wPsD\":[\"Switch\"],\"MHrjPM\":[\"Title\"],\"MKIM3K\":[\"Search pages\"],\"MaYYE6\":[\"Post notes by messaging a Telegram bot\"],\"Me5t5H\":[\"Connect a GitHub repository to automatically back up your posts as Markdown files. Edits on GitHub sync back to your site.\"],\"MeCJlL\":[\"Announcing your site. Reload to see the result.\"],\"MnbH31\":[\"page\"],\"Mq9FZ1\":[\"Creating page…\"],\"Mr4QPw\":[\"Disconnect Telegram? You can reconnect any time with a new binding code.\"],\"MtENL9\":[\"Tune how your site looks, reads, and runs.\"],\"N/8NPV\":[\"Before deleting, download a site export. You won't be able to recover this account after deletion.\"],\"N7UNHY\":[\"Featured feed\"],\"NHnUHF\":[\"Favicon and the profile mark in your header\"],\"NU2Fqi\":[\"Save CSS\"],\"NVjhde\":[\"Warm parchment\"],\"Nldjdr\":[\"No custom URLs yet. Create one to add redirects or custom paths for posts.\"],\"O3oNi5\":[\"Email\"],\"OJxdgi\":[\"Keep this link under 200 characters.\"],\"OSJXFg\":[\"Applies to your entire site, including admin pages. Pick a palette, then choose whether it follows the system or stays fixed.\"],\"OeUWA7\":[\"Add Page\"],\"OuuMXJ\":[\"Add site-wide HTML before the closing body tag.\"],\"Ox3+3h\":[\"No matches.\"],\"P7Eeay\":[\"Full posts\"],\"PEUV5I\":[\"Code injection updated.\"],\"PHh52z\":[\"Warm and rich, with strong brown tones.\"],\"PXj9lw\":[\"Stop accepting posts from Telegram. Your existing notes stay published.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pbm2/N\":[\"Create Collection\"],\"Pwqkdw\":[\"Loading…\"],\"PxJ9W6\":[\"Generate Token\"],\"Q/6Y+2\":[\"Needs Contents (read/write) and Webhooks (read/write) on the target repository.\"],\"Q/O0X4\":[\"This setting wasn't saved. Check the value and try again.\"],\"Q30z/l\":[\"Remove this collection from navigation? The collection itself won't be deleted.\"],\"Q99OtV\":[\"Pin a collection to your navigation bar. An asterisk (*) appears next to collections updated in the last 48 hours.\"],\"QCwsv1\":[\"Warm off-white\"],\"QKvrmL\":[\"Warm-neutral and easy for long reading.\"],\"QZmz0H\":[\"Built-in links\"],\"Qnrzvb\":[\"Active Tokens\"],\"R6Z4LE\":[\"Download failed. Please try again.\"],\"R9Khdg\":[\"Auto\"],\"RDjuBN\":[\"Setup\"],\"RRo9kN\":[\"Language updated.\"],\"RcdDOS\":[\"Create a bot by messaging @BotFather on Telegram, then paste the token it gives you.\"],\"RdVIcf\":[\"Cream and coffee brown\"],\"Ri/9PY\":[\"This site still limits Discover to featured posts, and no post is marked Featured, so your feed carries nothing to show. Untick and tick the box to let Discover read every public post.\"],\"Rn2p1h\":[\"Nothing matches this search. Try a different name or description.\"],\"RxsRD6\":[\"Time Zone\"],\"SDND4q\":[\"Not configured\"],\"SJmfuf\":[\"Site Name\"],\"SKZhW9\":[\"Token name\"],\"SSsoa4\":[\"Add to Navigation\"],\"SVQQPe\":[\"Couldn't connect. Check the error and try again.\"],\"SWb0z+\":[\"That address is reserved. Choose another one.\"],\"SYGk01\":[\"The languages served under a URL prefix. Managed on the Language page.\"],\"SchpMp\":[\"Telegram\"],\"SeY4vo\":[[\"language\"],\" is the only other language on this site, so removing it turns multilingual content off. The root address goes back to showing every language, \",[\"prefix\"],\" stops working, and each post keeps the language it is written in.\"],\"SqKp3o\":[\"The title used across your site, browser tabs, and feeds.\"],\"SrGs4T\":[\"Light blue-gray background, cool and even.\"],\"T/R+Qz\":[\"Primary\"],\"T17dXm\":[\"Whether each language gets its own home page, archive, and feed. Managed on the Language page.\"],\"T41PG1\":[\"Limit characters used in automatically generated summaries.\"],\"TN0mN4\":[\"Search and edit runtime settings. Changes apply immediately; reset restores the environment or built-in default.\"],\"TSCeuF\":[\"Couldn't create the collection. Check the details and try again.\"],\"TpF3v+\":[\"Injected before </head>. Use for analytics, custom meta tags, and styles that must load early.\"],\"Tu6bMZ\":[\"Create About page\"],\"Tz0i8g\":[\"Settings\"],\"U5J7jI\":[\"Site visibility updated.\"],\"U5v6Gh\":[\"Edit Page\"],\"UFK415\":[\"Site-wide HTML for analytics and widgets\"],\"UTvFQq\":[\"Open \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" and send:\"],\"UUn+Y5\":[\"Open setting\"],\"UaZwcz\":[\"More options are available after you create it.\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UsODUn\":[\"Select an account\"],\"UxKoFf\":[\"Navigation\"],\"UyMSeQ\":[\"At that address\"],\"V+bhUy\":[\"Install GitHub App\"],\"V0fyg5\":[\"Collection added to navigation.\"],\"V4WsyL\":[\"Add Link\"],\"V5pZwT\":[\"Search settings updated.\"],\"V7dQi9\":[\"Language removed.\"],\"VXUPla\":[\"Connect with GitHub App\"],\"Vh+JIV\":[\"Multilingual guide\"],\"VhMDMg\":[\"Change Password\"],\"Vn3jYy\":[\"Navigation items\"],\"VoZYGU\":[\"This will permanently delete all your data — posts, media, collections, settings, and your account. Your blog will be reset to its initial setup state. This cannot be undone.\"],\"VoarBQ\":[\"Post addresses do not change, and you can turn this off again at any time.\"],\"VqyJ7Y\":[\"Primary language changed.\"],\"WL7KjI\":[\"Tile grid\"],\"WUnFK2\":[\"Pale cool-white with deep indigo text. High contrast.\"],\"Wa9q4P\":[\"Pure white\"],\"Wb7EHo\":[\"About page\"],\"Weq9zb\":[\"General\"],\"Wi9i06\":[\"Follow each visitor's system preference.\"],\"Wildi8\":[\"No pages available. Create one to add it to navigation.\"],\"Wx1M8N\":[\"Install the GitHub App to grant access without managing personal tokens. Permissions are scoped per repository and revocable from GitHub.\"],\"X+8FMk\":[\"Current password doesn't match. Try again.\"],\"X1G9eY\":[\"Navigation Preview\"],\"X2Cbc2\":[\"Archive feed\"],\"X5P6yv\":[\"Recently updated\"],\"X9Hujr\":[\"Manual Push\"],\"XQQOyj\":[\"That page is a draft. Publish it, then add it.\"],\"XeRkls\":[[\"count\",\"plural\",{\"one\":[\"#\",\" existing post\"],\"other\":[\"#\",\" existing posts\"]}],\" with no language yet will be marked as \",[\"language\"],\".\"],\"Xsu5WL\":[\"Add site-wide CSS in the dedicated code editor.\"],\"XtBJV8\":[\"Checking repository…\"],\"Xtc16w\":[\"Refresh repository list\"],\"Y+7JGK\":[\"Create Page\"],\"Y/F35r\":[\"Create a post with curl:\"],\"Y/N5N7\":[\"Soft cream background with a muted green accent.\"],\"Y4oXje\":[\"Name prefilled as \",[\"name\"],\". The list refreshes when you return.\"],\"YF6zHf\":[\"Site settings updated.\"],\"YVcVlW\":[\"Remove and turn off\"],\"Ya/FAk\":[\"Add at least one more language to turn this on.\"],\"YdG2RF\":[\"Export Site\"],\"YkgZi7\":[\"Connect a Telegram bot, then anything you message it gets published as a note.\"],\"YwhjRx\":[\"Manage Account\"],\"Yxp859\":[\"Warm cream\"],\"Z5HWHd\":[\"On\"],\"ZDY7Fy\":[\"Syncing…\"],\"ZQKLI1\":[\"Danger Zone\"],\"ZS/CBL\":[\"Delete this navigation link? Visitors won't see it in your site header anymore.\"],\"ZXZrAo\":[\"Keep the page address under 200 characters.\"],\"ZgZX+d\":[\"Pure white with neutral grays. No color tint.\"],\"Zgq+c2\":[\"Set the default number of items shown per page.\"],\"ZhhOwV\":[\"Quote\"],\"ZiooJI\":[\"API Tokens\"],\"Zm7Qb0\":[\"Backup & Restore Guide\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"a14mj8\":[\"Unknown device\"],\"a1iEgy\":[\"Warm cream background with deep coffee-brown accents.\"],\"a3LDKx\":[\"Security\"],\"aAIQg2\":[\"Appearance\"],\"aFkzVF\":[\"The slug of the target post or collection\"],\"aR3U86\":[\"Multilingual content is on. \",[\"count\",\"plural\",{\"one\":[\"#\",\" post was marked\"],\"other\":[\"#\",\" posts were marked\"]}],\" as \",[\"language\"],\".\"],\"aV6XWN\":[\"Multilingual content is off. Your languages are still saved.\"],\"alKG0+\":[\"Font Theme\"],\"anibOb\":[\"About this blog\"],\"any7NR\":[\"Theming guide\"],\"asq0vL\":[\"The language you write in.\"],\"b+/jO6\":[\"301 (Permanent)\"],\"b+FyBD\":[\"Add page to navigation\"],\"b1FYSK\":[\"Primary language\"],\"bHOiy1\":[\"Password changes are off in demo mode. Sign in with the shared demo credentials.\"],\"bHYIks\":[\"Sign Out\"],\"bV2vng\":[\"Warm terracotta\"],\"bbR5vW\":[\"This palette\"],\"bbzY7X\":[\"Manage sign-in security, site exports, and irreversible actions.\"],\"bfHZ7r\":[\"Choose the time zone used to display dates and times.\"],\"bi10qS\":[\"Add it to navigation now or open the editor to add content.\"],\"bkMuwo\":[\"Link to posts you've marked as featured.\"],\"bmrL08\":[\"Demo mode hides sessions, password changes, and account deletion. Export still works.\"],\"bph1Dd\":[\"Search engine indexing is off, so this site is not listed by default. Ticking the box above lists it anyway.\"],\"brfpw9\":[\"Edit the multi-line footer rendered at the bottom of public pages.\"],\"bviiKV\":[\"Headings, body text, and links in this theme.\"],\"c1BGrV\":[\"Already in navigation. Drag it in the list above to move it.\"],\"c1iSrq\":[\"Discover community rules\"],\"c3MN2z\":[\"all available endpoints and request formats.\"],\"cHh/Zu\":[\"Turn on multilingual content\"],\"cS7/bk\":[\"Remove the saved bot token? Its webhook is deleted and any connected account is disconnected.\"],\"cSDy01\":[\"Custom CSS updated.\"],\"clzoNp\":[\"Always show the dark version of the theme.\"],\"cnGeoo\":[\"Delete\"],\"cwaLCZ\":[\"Page added to navigation.\"],\"d1lzY/\":[\"Language added.\"],\"d3FRkY\":[\"Could not copy. Try again.\"],\"d5oGUo\":[\"Create a new repository on GitHub\"],\"dB1Nr4\":[\"Start writing\"],\"dEgA5A\":[\"Cancel\"],\"dTXUY+\":[\"Confirm account deletion\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"Permanently delete all data and reset the blog\"],\"ds0nJk\":[\"Add site-wide HTML before the closing head tag.\"],\"dsWkIw\":[\"Disconnect from GitHub? The webhook will be removed. Your repository content will not be deleted.\"],\"dwcK1n\":[\"Or submit your address by hand\"],\"e/tSI5\":[\"Navigation order updated.\"],\"eOL7vn\":[\"Minimal color, low distraction.\"],\"ePK91l\":[\"Edit\"],\"ebQKK7\":[\"Site\"],\"eeamrQ\":[\"Discover reads your Atom feed, so it needs feeds turned on.\"],\"egK+Yy\":[\"Bearer tokens for scripts and automation\"],\"egOXl0\":[\"Jant Discover\"],\"ehj/zN\":[\"Redirect Type\"],\"ej6Rji\":[\"Any post written in another language can be corrected from its own menu afterwards.\"],\"eneWvv\":[\"Draft\"],\"erTMh7\":[\"Last synced\"],\"f+m8jj\":[\"Feed URL copied.\"],\"f8fH8W\":[\"Design\"],\"fDz6PV\":[\"Choose the repository used for content backups.\"],\"fKRAwQ\":[\"Sandy background with a green accent.\"],\"fWYqkz\":[\"Code Injection\"],\"fYXQnC\":[\"Cozy and bold among the warm tones.\"],\"fttd2R\":[\"My Collection\"],\"gZ5owP\":[\"Search repositories\"],\"gbqbh6\":[\"Safe to leave this page — syncing continues in the background.\"],\"gkFvVN\":[\"Injected before </body>. Use for chat widgets and scripts that should not block page load.\"],\"gtQsRO\":[\"Create Custom URL\"],\"hBO/y4\":[\"Security token expired. Refresh the page and try again.\"],\"hGmyDl\":[\"Tokens let you access the API from scripts, shortcuts, and other tools without signing in.\"],\"hIHkRy\":[\"Connected via GitHub App\"],\"hJHHsU\":[\"Publish Atom feeds for the site, archive, and collections.\"],\"hcH8MY\":[\"Set up your site\"],\"hdSi1b\":[\"Type \",[\"repo\"],\" to confirm\"],\"he3ygx\":[\"Copy\"],\"hjeS3W\":[\"Link to your latest posts. Your homepage shows this feed.\"],\"hjvSlw\":[\"Language removed. Multilingual content is off.\"],\"hvGGMK\":[\"Remove this page from navigation? The page itself won't be deleted.\"],\"i0qMbr\":[\"Home\"],\"iEUzMn\":[\"system\"],\"iH8pgl\":[\"Back\"],\"iMkWoF\":[\"Turn on\"],\"iSLIjg\":[\"Connect\"],\"iVOMRi\":[\"Home settings updated.\"],\"icB4Cv\":[\"Drag links here to show them under the More menu\"],\"id3vuh\":[\"Telegram is set up, but the bot couldn't be reached. Check the bot token and try again.\"],\"idD8Ev\":[\"Saved\"],\"ihn4zD\":[\"Search…\"],\"iiDXZc\":[\"Displayed at the bottom of all posts and pages.\"],\"itS31B\":[\"A warmer look, like slightly aged paper.\"],\"iwQZvS\":[\"Cool blue-gray\"],\"j4VrG6\":[\"Download Export ZIP\"],\"j5nQL2\":[\"e.g. iOS Shortcuts\"],\"jNE7O+\":[\"Nothing published yet. jant.me lists a blog once it has \",[\"minCount\",\"plural\",{\"one\":[\"one public post\"],\"other\":[\"#\",\" public posts\"]}],\".\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jdVYS8\":[\"Pick the one that fits your writing.\"],\"jgBjXJ\":[\"Revoke this token? Any scripts using it will stop working.\"],\"jpctdh\":[\"View\"],\"k1ifdL\":[\"Processing...\"],\"kLw03Y\":[\"Bright paper white\"],\"kMXclu\":[\"Download a site export\"],\"kNiQp6\":[\"Pinned\"],\"kQ3Otm\":[\"Create new page\"],\"kRhzWq\":[\"GitHub Sync\"],\"kVQs7s\":[\"Fine-grained styling overrides\"],\"ke1gWS\":[\"Custom URLs\"],\"kfcRb0\":[\"Avatar\"],\"kp8wiR\":[\"Checking address…\"],\"kxDZ2i\":[\"This code runs on every page of your site.\"],\"kzvWob\":[\"Link to the post archive\"],\"lLW3vJ\":[\"Target Slug\"],\"lLsfOE\":[\"Turn off multilingual content?\"],\"lV04bQ\":[\"Warm and earthy, but still light.\"],\"lYHJih\":[\"Revoke this session? That device will need to sign in again.\"],\"m16xKo\":[\"Add\"],\"mLOk1i\":[\"Push all posts to GitHub right now instead of waiting for the next automatic sync.\"],\"mSNmrX\":[\"List posts:\"],\"n8+EXe\":[\"Add common destinations that already exist on your site.\"],\"nG2qTk\":[\"Example link\"],\"nK07ni\":[\"Choose a typographic direction for your site. Each theme changes both the font pairing and the reading rhythm.\"],\"nbfdhU\":[\"Integrations\"],\"ntJYyh\":[\"Domains, plan, and billing in \",[\"providerLabel\"]],\"o/vNDE\":[\"lets you override any theme variable.\"],\"o5rj+L\":[\"No collections yet. Create one here to add it to navigation.\"],\"oGC9uP\":[\"owner/repo\"],\"oKOOsY\":[\"Color Theme\"],\"oL535e\":[\"Not synced yet\"],\"oNA4If\":[\"All collections are already in your navigation.\"],\"oUn9Z7\":[\"Near-neutral light gray with a steel-blue accent.\"],\"ofdy2l\":[\"Orange-tinted background. The warmest palette.\"],\"olouYg\":[\"Navigation holds that address as a link.\"],\"oxj6Pk\":[\"Other languages\"],\"pIpTqF\":[\"Give each language its own home page, archive, and feed. Once it's on, you pick a language when you publish, and can link an existing post to a version of it in another language.\"],\"pNXxtS\":[\"Choose the content language announced to readers and search engines.\"],\"pZq3aX\":[\"Upload failed. Please try again.\"],\"pgJImo\":[\"Change the primary language?\"],\"pgTIrt\":[\"Choose the GitHub account and repository to sync with this site.\"],\"psoxDF\":[\"That font theme isn't available. Pick another one.\"],\"pt4OhQ\":[\"/about is already used. Rename that item before creating an About page.\"],\"pvnfJD\":[\"Dark\"],\"q+hNag\":[\"Collection\"],\"q/T5GS\":[\"The language used by the private Jant dashboard.\"],\"qSgO/Y\":[\"Follow content language\"],\"qZ9jzv\":[\"Site visibility\"],\"qdcESc\":[\"Create a new repository\"],\"qkSJzV\":[\"Searching pages…\"],\"r5EW6f\":[\"This repository is already backing up another Jant site (\",[\"host\"],\"). Pick a different repository.\"],\"rCPrbS\":[\"Options for \",[\"language\"]],\"rEspiY\":[\"Navigation placement updated.\"],\"rFmBG3\":[\"Color theme\"],\"re0mF0\":[\"The page is public but stays out of Latest.\"],\"rlonmB\":[\"Couldn't delete. Try again in a moment.\"],\"s3jOk7\":[\"Enter a page title.\"],\"sROtqR\":[\"The directory could not be reached: \",[\"reason\"]],\"sRmGLp\":[\"You can change all of this later in Settings.\"],\"satWc6\":[\"Main RSS feed\"],\"sgr2wQ\":[\"collection\"],\"sj4LLc\":[\"Show only modified\"],\"sjej3N\":[\"Remove \",[\"language\"]],\"slujBW\":[\"Use lowercase letters, numbers, and hyphens only.\"],\"soRdOu\":[\"Off-white paper with a deep forest-green accent.\"],\"sqxcaY\":[\"Created \",[\"date\"]],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"t3hvHq\":[\"Sync Now\"],\"tJ4H0O\":[\"your Telegram account\"],\"tfDRzk\":[\"Save\"],\"tgWuMB\":[\"Modified\"],\"tvgAq5\":[\"No accounts authorized yet\"],\"u1VTd3\":[\"Palette, surface tone, and overall mood\"],\"u3wRF+\":[\"Published\"],\"u6KOjV\":[\"Want more control?\"],\"uKLe0J\":[[\"count\"],\" settings shown\"],\"uVZcIA\":[\"That address is already in use. Choose another one.\"],\"udPwLB\":[\"Header\"],\"ugHDtd\":[[\"about\"],\" A post you mark Featured appears on the Discover home page \",[\"hours\"],\" hours later, and link and quote posts appear on the \",[\"links\"],\" and \",[\"quotes\"],\" lists \",[\"hours\"],\" hours after they are published. You can keep editing them in the meantime. See the \",[\"rules\"],\".\"],\"ui6aMF\":[\"These devices are currently signed in to your account. Revoke any session you don't recognize.\"],\"vBEKwo\":[\"Manage this site's active sessions here. Password and hosted access are managed through \",[\"providerLabel\"],\".\"],\"vOnuOa\":[\"Show or hide built-in destinations in the header and More menu.\"],\"vRldcl\":[\"Typography choices and reading texture\"],\"vSYKYI\":[\"Main feed\"],\"vTuib7\":[\"This controls what /feed returns.\"],\"vXCC6J\":[\"Something doesn't look right. Check the form and try again.\"],\"vXIe7J\":[\"Language\"],\"vXsx3x\":[\"That page is private, so nobody could open it from a menu.\"],\"vdFnYM\":[\"Reset link\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vpSPA1\":[\"Auth secret is missing. Check your environment variables.\"],\"vzX5FB\":[\"Delete Account\"],\"w615zC\":[\"Multilingual content\"],\"w8Rv8T\":[\"Label is required\"],\"wL3cK8\":[\"Latest\"],\"wW6NCp\":[\"Last error\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wuLtXn\":[\"No active sessions right now. Signed-in devices show up here.\"],\"x+HGBk\":[\"Ask search engines not to index public pages or include them in results.\"],\"xCWek4\":[\"File storage isn't set up. Check your server config.\"],\"xCbjQn\":[\"Allow \",[\"name\"],\" to list my site\"],\"xGVfLh\":[\"Continue\"],\"xHt036\":[\"Personal Access Token\"],\"xKeZ0l\":[\"An earthier, warmer tone.\"],\"xYbozN\":[\"Soft ivory\"],\"xqViCq\":[\"Warm ivory\"],\"xxaahd\":[\"When you want a cooler, sharper look.\"],\"y/awtV\":[\"Header links, built-in destinations, and overflow menu\"],\"y28hnO\":[\"Post\"],\"y9W9vo\":[\"Creating collection…\"],\"yNCqOt\":[\"Latest feed\"],\"yQ3kNF\":[\"Type the following phrase to confirm:\"],\"ydq1k2\":[\"Pick an account first\"],\"yjjCV8\":[\"Fixed feed URLs\"],\"yjkELF\":[\"Confirm New Password\"],\"ym+ccl\":[\"Show the Jant credit on the home page.\"],\"yzF66j\":[\"Link\"],\"z6wakA\":[\"A short intro shown on your home page.\"],\"zEizrk\":[\"Last used \",[\"date\"]],\"zPqly9\":[\"This site has no directory to announce to.\"],\"zSURJW\":[\"No repositories match.\"],\"zUlHGd\":[\"Page address\"],\"zYRGIR\":[\"Not announced yet. No directory has been told this site exists.\"],\"zcDmsG\":[\"Featured posts\"],\"zlcDd2\":[\"Delete this custom URL? Visitors using it won't be redirected anymore.\"],\"ztPYIR\":[\"One-time change to your existing posts\"],\"zwBp5t\":[\"Private\"],\"zxexio\":[\"Content language, dashboard language, multilingual\"]}");
2013
2013
  //#endregion
2014
2014
  //#region src/i18n/locales/settings/zh-Hans.ts
2015
- var messages$1 = JSON.parse("{\"+4Z6iP\":[\"请先在 GitHub 上创建仓库 — 可以为空。\"],\"+9JI/F\":[\"连接后会将你的网站同步到 \",[\"repo\"],\" 的默认分支,并追加到其现有历史之上。Jant 管理路径之外的现有文件会被保留。此操作无法撤销。\"],\"+AXdXp\":[\"标签和 URL 为必填项\"],\"+K0AvT\":[\"断开连接\"],\"+V/jXc\":[\"直接指向 /feed,即 Atom 原始文件,目前返回的是「\",[\"feed\"],\"」。适合已经在用阅读器的读者。\"],\"+VEqyj\":[\"设为主语言\"],\"+jXHMG\":[[\"count\",\"plural\",{\"other\":[\"还有 \",\"#\",\" 篇\",[\"language\"],\"帖子。先修改它们的语言,或保留该语言。\"]}]],\"+wgt7C\":[\"低饱和的红棕底,温暖、有泥土感。\"],\"+zy2Nq\":[\"类型\"],\"/0D1Xp\":[\"编辑合集\"],\"/3H2/s\":[\"此托管站点通过 \",[\"providerLabel\"],\" 登录。请在那里管理密码和托管访问权限。\"],\"/PXXBT\":[\"近乎纯白,带一点暖意。\"],\"/PoNoq\":[\"编辑链接\"],\"/zOUxl\":[\"链接到 Telegram 机器人的二维码\"],\"05DXsb\":[\"连接或管理用于从 Telegram 发帖的 Bot。\"],\"0OGSSc\":[\"头像显示已更新。\"],\"0UzCUX\":[\"更新你用于登录的密码\"],\"0VVvqu\":[\"多语言已启用。\"],\"0bdA9b\":[\"打开 Telegram 以连接\"],\"0dieZ/\":[\"/archive 打开时用哪种版式。读者可以在页面上自行切换。\"],\"0gECZD\":[\"想写更完整的介绍?\"],\"0uIjy/\":[\"浅鼠尾草绿底,配同色系的绿。\"],\"1DahsC\":[\"搜索设置\"],\"1F6Mzc\":[\"当前还没有导航项目。添加链接或在下方启用系统项目。\"],\"1H7gng\":[\"后台语言\"],\"1MYU3o\":[\"无法创建页面。请检查详情并重试。\"],\"1mbBbL\":[\"手动绑定\"],\"1njn7W\":[\"浅色\"],\"1qGdnL\":[\"默认主题。沉静,适合长时间阅读。\"],\"1wdDm9\":[\"添加语言\"],\"21mg6u\":[\"选择一篇尚未添加到导航的有标题笔记,或创建新页面。\"],\"2C7mSG\":[\"合集链接\"],\"2DoBvq\":[\"订阅源\"],\"2Et8jU\":[\"设置每页搜索结果数;重置后继承默认分页大小。\"],\"2FYpfJ\":[\"更多\"],\"2Ithfh\":[\"向机器人发送任意文本,它会作为笔记已发布。\"],\"2PTjMB\":[\"我想删除 \",[\"siteName\"]],\"2QRni+\":[\"时区已更新。\"],\"2UrpGC\":[\"订阅源地址已送达。目录会在 \",[\"hours\"],\" 小时内首次读取。\"],\"2ZqpRB\":[\"关闭\"],\"2cFU6q\":[\"网站页脚\"],\"2fPEPI\":[\"沉静、偏冷的底色。\"],\"2lkk2l\":[\"微黄的纸底,配橄榄绿。\"],\"2oWZo7\":[\"最近一次提交\"],\"2r8pkt\":[\"无法加载页面。请稍后再试。\"],\"2uuy4H\":[\"通过个人访问令牌连接\"],\"2wK4BX\":[\"编辑 About 页面\"],\"35x8eZ\":[\"显示 \",[\"shown\"],\" 共 \",[\"total\"]],\"39QGku\":[\"打开机器人并发送绑定码,然后你发送给它的任何消息都会变成一条笔记。\"],\"3B0RD1\":[\"上传个人资料图片和生成的网站图标。\"],\"3Cw1AI\":[\"添加合集\"],\"3VrybB\":[\"重定向\"],\"3Yvsaz\":[\"302 (临时)\"],\"3n0zbB\":[\"会话管理在演示模式下已关闭。请使用共享的演示会话。\"],\"3sYJi5\":[\"下载一个与 Hugo 兼容的归档 — 将其静态托管或迁移到另一个 Jant。\"],\"3vMdv3\":[\"此链接为保留链接。请选择其他链接。\"],\"3wKq0C\":[\"保存失败。请稍后再试。\"],\"49Bsal\":[\"订阅源设置已更新。\"],\"4D09NB\":[\"链接到合集页。\"],\"4J/OYU\":[\"合集已创建。\"],\"4Jge8E\":[\"活动会话\"],\"4KIa+q\":[\"导出文件已下载。\"],\"4Q0iPK\":[\"跟随设备外观或强制使用浅色或深色主题。\"],\"4cEClj\":[\"会话\"],\"4n7WY4\":[\"站名、可见性、订阅源与时区\"],\"4tcgrg\":[\"查看这些帖子\"],\"4yyXWu\":[\"浅骨白底,配低饱和的绿。\"],\"4zGJ5E\":[\"永久删除账号\"],\"5QlUIt\":[\"仓库为空。准备连接。\"],\"5VQnR3\":[\"当你想要一个永远不变的订阅源 URL 时使用它们。\"],\"5dpcN1\":[\"输入以搜索全部\"],\"5f1Wo9\":[\"已连接为 \",[\"account\"]],\"5o8R81\":[\"极浅的象牙底,配淡淡的茶绿。\"],\"6ArdBh\":[\"将 Featured 帖子用于 /feed。\"],\"6DjeBT\":[\"演示站点始终对搜索引擎隐藏。\"],\"6E3aK4\":[\"管理托管\"],\"6FFB7q\":[\"为 /feed 使用最新的公开帖子。\"],\"6K1Vef\":[\"永久删除此博客?此操作不可撤销。\"],\"6NpNLc\":[\"此仓库已有内容。\"],\"6Pa5AP\":[\"连接或管理 GitHub 内容自动备份。\"],\"6V3Ea3\":[\"已复制\"],\"6WdDG7\":[\"页面\"],\"71WIgc\":[\"获取新绑定码\"],\"71of4k\":[\"限制每个 RSS feed 中包含的帖子数。\"],\"746NHh\":[\"此博客\"],\"7811AW\":[\"此仓库已在备份此站点。\"],\"7FCZPo\":[\"内容语言\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允许搜索引擎索引我的网站\"],\"7GISOt\":[\"保存机器人令牌\"],\"7MZxzw\":[\"密码已更改。\"],\"7p5kLi\":[\"后台\"],\"7vhWI8\":[\"新密码\"],\"81nFIS\":[\"密码不匹配。请确保两个字段相同。\"],\"87a/t/\":[\"标签\"],\"89Upyo\":[\"该主题不可用。请选择其他主题。\"],\"8BfEpW\":[\"托管账户\"],\"8NIu3g\":[\"该页面尚无标题,因此菜单没有可显示的内容。\"],\"8T46pB\":[\"机器人令牌\"],\"8U2Z7f\":[\"新建自定义 URL\"],\"8ZsakT\":[\"密码\"],\"8bpHix\":[\"账户创建失败,检查填写内容后重试。\"],\"8gEghq\":[\"明亮的暖白底,配苔绿点缀。\"],\"9+vGLh\":[\"自定义 CSS\"],\"9As8Nu\":[\"在 GitHub 上创建一个\"],\"9EjI4j\":[\"暖沙色\"],\"9Fa2Ns\":[\"根地址将恢复显示所有语言,\",[\"prefix\"],\" 这类地址会重定向到它,已有的链接和订阅仍然有效。帖子地址和每篇帖子的语言都不受影响,随时可以重新启用。\"],\"9FctRm\":[\"使用小写字母、数字和连字符。\"],\"9J6wUO\":[\"建议添加的链接\"],\"9Lsvt5\":[\"于 \",[\"date\"],\" 登录\"],\"9SHZas\":[\"登录后显示「设置」,未登录时显示「登录」。\"],\"9T7Cwm\":[\"重定向、个性化路径和 URL 控制\"],\"9aUyym\":[\"查看你的登录位置并撤销旧会话\"],\"9mqQnx\":[\"限制自动生成摘要所使用的段落数。\"],\"A7tRC9\":[\"规范的 /feed URL 返回哪个帖子流。\"],\"ADTn/D\":[\"允许无需 API 令牌读取已发布内容。\"],\"AELGTd\":[\"现在添加到导航,或打开编辑器补充详细信息。\"],\"ANzR96\":[\"配置编辑器\"],\"AQL8b1\":[\"你写作的主要语言是?\"],\"AeXO77\":[\"账户\"],\"AnY+O9\":[\"在主页底部显示 \\\"Build with Jant\\\"\"],\"ApZDMk\":[\"此图用于你的 favicon 和 apple-touch-icon。为获得最佳效果,请上传至少 512×512 像素、纯色背景的方形 PNG。\"],\"Aysjjh\":[\"选择在整个 Jant 中使用的配色方案。\"],\"B495Gs\":[\"归档\"],\"B4ESok\":[\"API 参考\"],\"B5P9HE\":[\"提交我的站点\"],\"BJ0tsF\":[\"搜索运行时设置并打开高级配置\"],\"BTYdze\":[\"链接已添加到导航。\"],\"BszQc4\":[\"根地址(/、/feed)显示这个语言的内容。\"],\"BzEFor\":[\"或\"],\"C0/57J\":[\"这是合集链接的最后一部分。\"],\"CDAdlf\":[\"移除机器人\"],\"CTAEes\":[\"选择仓库\"],\"CjZZgz\":[\"该仓库已有提交\"],\"Cl92kR\":[\"创建新合集\"],\"D35jh6\":[\"管理后台的显示语言,只有你能看到。\"],\"D8k2s6\":[\"连接 Telegram\"],\"DCKkhU\":[\"当前密码\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密码和托管访问\"],\"DVFXa6\":[\"最干净、最中性的一档。\"],\"DdXUay\":[\"在公共页眉中显示站点头像。\"],\"E3hjKv\":[\"添加为链接\"],\"EKvXGx\":[\"该地址在另一个网站上。Navigation 将其保留为链接。\"],\"EO3I6h\":[\"上传未成功。请稍后再试。\"],\"Eax/et\":[\"强调色\"],\"EbDLD+\":[\"选择 Jant 全局使用的字体。\"],\"Enslfm\":[\"目标地址\"],\"F53UDu\":[\"冷白\"],\"F7FKwe\":[\"你在此处粘贴的任何内容都将完全访问访客的浏览器。仅使用来自你信任的来源的代码。\"],\"F8SNqr\":[\"暖橘\"],\"FBgjRN\":[\"搜索页面,或粘贴地址\"],\"FMUJSP\":[\"账户与数据\"],\"FUfPPw\":[\"根地址(/、/feed、/archive)将改为提供「\",[\"next\"],\"」的内容,「\",[\"previous\"],\"」移至 \",[\"prefix\"],\"。帖子地址不会改变,但订阅了 /feed 的读者从此收到的是「\",[\"next\"],\"」的帖子。\"],\"Fe1cvJ\":[\"延迟新帖子和回复进入订阅源的时间。设为 0 可关闭延迟。\"],\"Fk3SSD\":[\"平静自然,看着舒服。\"],\"FkMol5\":[\"Featured\"],\"FwL2Zk\":[[\"count\",\"plural\",{\"other\":[\"还有 \",\"#\",\" 篇\",[\"language\"],\"帖子,但列表里没有这个语言。把它加回列表,或先修改这些帖子的语言。\"]}]],\"G/1oP+\":[\"移除 webhook 并停止同步。你的仓库内容不会被删除。\"],\"G0qJsQ\":[\"缺少安全令牌。刷新页面后重试。\"],\"G0tHaW\":[\"创建一个不会出现在 Latest 的公开页面。\"],\"G2fuEb\":[\"已锁定\"],\"G39wnK\":[\"将内容备份并与 GitHub 仓库同步\"],\"G5oCui\":[\"URL 结构、每种语言的订阅源,以及译本关联。\"],\"G9peMt\":[\"创建账户\"],\"GAmD3h\":[\"语言\"],\"GBkMNw\":[[\"name\"],\" 是 Jant 社区人工维护的目录,旨在帮助用户发现新的 Jant 博客和内容。\"],\"GXsAby\":[\"撤销\"],\"GdGiNi\":[\"第 \",[\"current\"],\" 步,共 \",[\"total\"],\" 步\"],\"GorKul\":[\"欢迎使用 Jant\"],\"GxkJXS\":[\"正在上传...\"],\"GzKzUa\":[\"演示限制\"],\"HCNlq3\":[\"干净、高对比,接近纯白。\"],\"HHDyGw\":[\"柔绿\"],\"HKH+W+\":[\"数据\"],\"Hf/w/z\":[\"演示站点不会出现在 Discover 中。\"],\"Hp1l6f\":[\"当前\"],\"HxTUmw\":[\"添加 \",[\"language\"]],\"HxlY7t\":[\"更改此项会更新订阅者从 /feed 获取的内容。\"],\"HxuOlm\":[\"网站头部\"],\"I0ijRI\":[\"全部 feed 地址\"],\"I4HRxU\":[\"已发布的全部帖子,包括从 Latest 中隐藏的帖子。\"],\"I6gXOa\":[\"路径\"],\"I76CzF\":[\"中性灰\"],\"ID38tA\":[\"演示模式下已禁用账号删除。共享演示会单独重置。\"],\"IF9tPu\":[\"何时使用站点导出、数据库备份和恢复演练。\"],\"IW5PBo\":[\"复制令牌\"],\"IagCbF\":[\"网址\"],\"IreQBq\":[\"仓库\"],\"J36Xsm\":[\"在 \",[\"address\"],\" 没有任何内容。检查它,或按标题搜索。\"],\"J6bLeg\":[\"向任意 URL 添加自定义链接\"],\"JL7LF5\":[\"可用的 CSS 变量,数据属性,和 示例。\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"现在复制你的令牌 — 它不会再次显示。\"],\"JrFTcr\":[\"连接中…\"],\"JuN5GC\":[\"未选择文件。请选择要上传的文件。\"],\"K+0Hu0\":[\"没有可添加的匹配页面。换个标题搜索,或创建新页面。\"],\"K/F6pa\":[\"正在保存…\"],\"KDw4GX\":[\"重试\"],\"KSgo21\":[\"选择仓库\"],\"KVVYBh\":[\"向导航添加合集\"],\"KiJn9B\":[\"笔记\"],\"Kk7jwL\":[\"设置每页归档帖子数;重置后继承默认分页大小。\"],\"KwOLJF\":[\"Jant 最初的配色,日常稳妥之选。\"],\"L+rMC9\":[\"重置为默认\"],\"L27RpE\":[\"页面已创建。\"],\"L3DEwT\":[\"移除此头像?你的网站图标和站点顶部图标将恢复为默认设置。\"],\"L4t4/q\":[\"3月14日\"],\"L86zmP\":[\"在完整表单中编辑首页和元数据使用的多行简介。\"],\"LdyooL\":[\"链接\"],\"LhMjLm\":[\"时间\"],\"LjwaJ/\":[\"登录、导出和删除\"],\"M/D8PK\":[\"+ 在其他账户上安装\"],\"M/haSd\":[\"始终显示浅色主题。\"],\"M/kDIW\":[\"搜索引擎看到的语言。\"],\"M1co/O\":[\"已配置\"],\"M2hGyf\":[\"指向 /subscribe,一个列出你所有 feed 地址、带复制按钮的页面。适合还没在用阅读器的读者。\"],\"M2kIWU\":[\"字体主题\"],\"M4tzVU\":[\"Latest 帖子\"],\"M6CbAU\":[\"切换编辑面板\"],\"M7wPsD\":[\"切换\"],\"MHrjPM\":[\"标题\"],\"MKIM3K\":[\"搜索页面\"],\"MaYYE6\":[\"通过向 Telegram 机器人发送消息来发布笔记\"],\"Me5t5H\":[\"连接 GitHub 仓库,自动把帖子备份成 Markdown。在 GitHub 上的修改会同步回站点。\"],\"MeCJlL\":[\"正在提交站点,刷新后可以看到结果。\"],\"MnbH31\":[\"页面\"],\"Mq9FZ1\":[\"正在创建页面…\"],\"Mr4QPw\":[\"断开 Telegram?你可以随时使用新的绑定码重新连接。\"],\"MtENL9\":[\"调整你的网站的外观、阅读体验和运行方式。\"],\"N/8NPV\":[\"在删除之前,请下载站点导出。删除后将无法恢复此账户。\"],\"N7UNHY\":[\"Featured 订阅源\"],\"NHnUHF\":[\"Favicon 和站点顶部的个人标识\"],\"NU2Fqi\":[\"保存 CSS\"],\"NVjhde\":[\"暖羊皮纸\"],\"Nldjdr\":[\"还没有自定义 URL。新建一个,给帖子加重定向或自定义路径。\"],\"O3oNi5\":[\"邮箱\"],\"OJxdgi\":[\"链接不能超过 200 个字符。\"],\"OSJXFg\":[\"应用于整个站点,包括管理页面。选择一个调色板,然后选择它是随系统变化还是保持固定。\"],\"OeUWA7\":[\"添加页面\"],\"OuuMXJ\":[\"在 </body> 闭合标签之前添加全站 HTML。\"],\"Ox3+3h\":[\"无匹配结果。\"],\"P7Eeay\":[\"完整帖子\"],\"PEUV5I\":[\"代码注入已更新。\"],\"PHh52z\":[\"暖而厚重,棕调明显。\"],\"PXj9lw\":[\"停止接受来自 Telegram 的帖子。你的现有笔记将保持已发布。\"],\"PZ7HJ8\":[\"博客头像\"],\"Pbm2/N\":[\"创建合集\"],\"Pwqkdw\":[\"正在加载…\"],\"PxJ9W6\":[\"生成令牌\"],\"Q/6Y+2\":[\"需要对目标仓库的 Contents(读/写)和 Webhooks(读/写)。\"],\"Q/O0X4\":[\"此设置未保存。请检查该值并重试。\"],\"Q30z/l\":[\"要从导航中移除此合集吗?合集本身不会被删除。\"],\"Q99OtV\":[\"将合集固定到导航栏。在过去 48 小时内更新的合集旁会出现一个 * 号。\"],\"QCwsv1\":[\"暖白\"],\"QKvrmL\":[\"暖中性,适合长读。\"],\"QZmz0H\":[\"内置链接\"],\"Qnrzvb\":[\"活动令牌\"],\"R6Z4LE\":[\"下载失败。请重试。\"],\"R9Khdg\":[\"自动\"],\"RDjuBN\":[\"初始设置\"],\"RRo9kN\":[\"语言已更新。\"],\"RcdDOS\":[\"在 Telegram 上给 @BotFather 发送消息创建一个机器人,然后粘贴它提供的令牌。\"],\"RdVIcf\":[\"奶油配深咖\"],\"Ri/9PY\":[\"当前仍只向 Discover 提供精选帖子,但没有任何帖子标为 Featured,订阅源里没有可展示的内容。取消勾选再重新勾选,即可让 Discover 读取全部公开帖子。\"],\"Rn2p1h\":[\"没有匹配此搜索的结果。试试其他名称或描述。\"],\"RxsRD6\":[\"时区\"],\"SDND4q\":[\"未配置\"],\"SJmfuf\":[\"站点名称\"],\"SKZhW9\":[\"令牌名称\"],\"SSsoa4\":[\"添加到导航\"],\"SVQQPe\":[\"无法连接。检查错误并重试。\"],\"SWb0z+\":[\"此地址为保留地址。请选择其他地址。\"],\"SYGk01\":[\"以 URL 前缀提供的语言。在「语言」页管理。\"],\"SchpMp\":[\"Telegram\"],\"SeY4vo\":[\"移除 \",[\"language\"],\" 后就只剩主语言了,多语言会一并关闭。根地址将恢复显示所有语言,\",[\"prefix\"],\" 不再可用,每篇帖子的语言保持不变。\"],\"SqKp3o\":[\"在整个网站、浏览器标签页和订阅源中使用的标题。\"],\"SrGs4T\":[\"浅蓝灰底,冷静而均匀。\"],\"T/R+Qz\":[\"主语言\"],\"T17dXm\":[\"每种语言是否拥有独立的首页、归档和订阅源。在「语言」页管理。\"],\"T41PG1\":[\"限制自动生成摘要所使用的字符数。\"],\"TN0mN4\":[\"搜索和编辑运行时设置。更改会立即生效;重置会恢复环境变量或内置默认值。\"],\"TSCeuF\":[\"无法创建合集。请检查信息后重试。\"],\"TpF3v+\":[\"注入到 </head> 之前。用于分析、自定义元标签以及必须尽早加载的样式。\"],\"Tu6bMZ\":[\"创建 About 页面\"],\"Tz0i8g\":[\"设置\"],\"U5J7jI\":[\"站点可见性已更新。\"],\"U5v6Gh\":[\"编辑页面\"],\"UFK415\":[\"用于分析和小部件的全站 HTML\"],\"UTvFQq\":[\"打开 \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" 并发送:\"],\"UUn+Y5\":[\"打开设置\"],\"UaZwcz\":[\"创建后可以设置更多选项。\"],\"Uj/btJ\":[\"在我的站点顶部显示头像\"],\"UsODUn\":[\"选择账户\"],\"UxKoFf\":[\"导航栏\"],\"UyMSeQ\":[\"在该地址\"],\"V+bhUy\":[\"安装 GitHub App\"],\"V0fyg5\":[\"合集已添加到导航。\"],\"V4WsyL\":[\"添加链接\"],\"V5pZwT\":[\"搜索设置已更新。\"],\"V7dQi9\":[\"语言已移除。\"],\"VXUPla\":[\"使用 GitHub 应用连接\"],\"Vh+JIV\":[\"多语言指南\"],\"VhMDMg\":[\"更改密码\"],\"Vn3jYy\":[\"导航项\"],\"VoZYGU\":[\"这会永久删除你的全部数据——帖子、媒体、合集、设置和账户。博客会重置为初始设置状态。此操作无法撤销。\"],\"VoarBQ\":[\"所有帖子的地址都不会改变,也可以随时再关闭。\"],\"VqyJ7Y\":[\"主语言已更改。\"],\"WL7KjI\":[\"卡片网格\"],\"WUnFK2\":[\"偏冷的浅白底,配深靛蓝文字。高对比。\"],\"Wa9q4P\":[\"纯白\"],\"Wb7EHo\":[\"About 页面\"],\"Weq9zb\":[\"常规\"],\"Wi9i06\":[\"遵循每位访客的系统偏好。\"],\"Wildi8\":[\"没有可添加的页面。创建一个页面后即可添加到导航。\"],\"Wx1M8N\":[\"安装 GitHub App 以在无需管理个人令牌的情况下授予访问权限。权限按仓库范围授予,可在 GitHub 上撤销。\"],\"X+8FMk\":[\"当前密码不正确。请重试。\"],\"X1G9eY\":[\"导航栏预览\"],\"X2Cbc2\":[\"归档订阅源\"],\"X5P6yv\":[\"最近更新\"],\"X9Hujr\":[\"手动推送\"],\"XQQOyj\":[\"该页面是草稿 (未发布内容)。先发布它,然后再添加。\"],\"XeRkls\":[[\"count\",\"plural\",{\"other\":[\"其中 \",\"#\",\" 篇还没有语言,会标记为「\",[\"language\"],\"」。\"]}]],\"Xsu5WL\":[\"在专用代码编辑器中添加全站 CSS。\"],\"XtBJV8\":[\"正在检查仓库…\"],\"Xtc16w\":[\"刷新仓库列表\"],\"Y+7JGK\":[\"创建页面\"],\"Y/F35r\":[\"使用 curl 创建帖子:\"],\"Y/N5N7\":[\"柔和的奶油底,配低饱和的绿。\"],\"YF6zHf\":[\"站点设置已更新。\"],\"YVcVlW\":[\"移除并关闭\"],\"Ya/FAk\":[\"至少再添加一种语言才能启用。\"],\"YdG2RF\":[\"导出站点\"],\"YkgZi7\":[\"连接一个 Telegram 机器人,然后你发送给它的任何消息都会作为笔记已发布。\"],\"YwhjRx\":[\"管理账户\"],\"Yxp859\":[\"暖奶油色\"],\"Z5HWHd\":[\"已启用\"],\"ZDY7Fy\":[\"正在同步…\"],\"ZQKLI1\":[\"危险操作\"],\"ZS/CBL\":[\"删除此导航链接?访客将不再在你的网站导航栏中看到它。\"],\"ZXZrAo\":[\"页面地址不能超过 200 个字符。\"],\"ZgZX+d\":[\"纯白底配中性灰。不带任何色偏。\"],\"Zgq+c2\":[\"设置默认每页显示的条目数。\"],\"ZhhOwV\":[\"引用\"],\"ZiooJI\":[\"API 令牌\"],\"Zm7Qb0\":[\"备份与恢复指南\"],\"ZmUkwN\":[\"向导航添加自定义链接\"],\"a14mj8\":[\"未知设备\"],\"a1iEgy\":[\"暖奶油底,配深咖啡棕点缀。\"],\"a3LDKx\":[\"安全\"],\"aAIQg2\":[\"外观\"],\"aFkzVF\":[\"目标帖子或合集的 slug\"],\"aR3U86\":[\"多语言已启用。\",[\"count\",\"plural\",{\"other\":[\"#\",\" 篇帖子\"]}],\"已标记为「\",[\"language\"],\"」。\"],\"aV6XWN\":[\"多语言已关闭。你的语言设置仍然保留。\"],\"alKG0+\":[\"字体主题\"],\"anibOb\":[\"关于本博客\"],\"any7NR\":[\"主题指南\"],\"asq0vL\":[\"你写作使用的语言。\"],\"b+/jO6\":[\"301 (永久)\"],\"b+FyBD\":[\"向导航添加页面\"],\"b1FYSK\":[\"主语言\"],\"bHOiy1\":[\"演示模式下已禁用密码更改。请使用共享的演示凭证登录。\"],\"bHYIks\":[\"退出登录\"],\"bV2vng\":[\"暖陶土色\"],\"bbR5vW\":[\"这套配色\"],\"bbzY7X\":[\"管理登录安全、站点导出和不可恢复的操作。\"],\"bfHZ7r\":[\"选择用于显示日期和时间的时区。\"],\"bi10qS\":[\"现在添加到导航,或打开编辑器补充内容。\"],\"bkMuwo\":[\"链接到你标为 Featured 的帖子。\"],\"bmrL08\":[\"演示模式会隐藏会话、密码更改和账号删除。导出仍然可用。\"],\"bph1Dd\":[\"已关闭搜索引擎索引,所以默认不出现在 Jant Discover。勾选上面的选项仍会加入。\"],\"brfpw9\":[\"在完整表单中编辑显示在公共页面底部的多行页脚。\"],\"bviiKV\":[\"这个主题下的标题、正文和链接。\"],\"c1BGrV\":[\"已在导航中。将其在上方列表中拖动以移动。\"],\"c1iSrq\":[\"Discover 社区规则\"],\"c3MN2z\":[\"所有可用的端点和请求格式。\"],\"cHh/Zu\":[\"启用多语言\"],\"cS7/bk\":[\"删除保存的机器人令牌?其 webhook 会被删除,任何已连接的账号将被断开连接。\"],\"cSDy01\":[\"自定义 CSS 已更新。\"],\"clzoNp\":[\"始终显示暗色主题。\"],\"cnGeoo\":[\"删除\"],\"cwaLCZ\":[\"页面已添加到导航。\"],\"d1lzY/\":[\"语言已添加。\"],\"d3FRkY\":[\"无法复制。请再试一次。\"],\"d5oGUo\":[\"在 GitHub 上创建新仓库\"],\"dB1Nr4\":[\"开始写作\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"确认删除账户\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"永久删除所有数据并重置博客\"],\"ds0nJk\":[\"在 </head> 结束标签之前添加全站 HTML。\"],\"dsWkIw\":[\"要与 GitHub 断开连接吗?该 webhook 将被移除。你的仓库内容不会被删除。\"],\"dwcK1n\":[\"或手动提交地址\"],\"e/tSI5\":[\"导航顺序已更新。\"],\"eOL7vn\":[\"色彩极少,干扰最小。\"],\"ePK91l\":[\"编辑\"],\"ebQKK7\":[\"站点\"],\"eeamrQ\":[\"Discover 读取的是 Atom 订阅源,需要先开启订阅源。\"],\"egK+Yy\":[\"用于脚本和自动化的 Bearer 令牌\"],\"egOXl0\":[\"Jant Discover\"],\"ehj/zN\":[\"重定向类型\"],\"ej6Rji\":[\"若有别的语言的帖子,之后可以在那篇帖子的菜单里单独更正。\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"订阅源 URL 已复制。\"],\"f8fH8W\":[\"设计\"],\"fDz6PV\":[\"选择用于内容备份的仓库。\"],\"fKRAwQ\":[\"沙色底,配绿色点缀。\"],\"fWYqkz\":[\"代码注入\"],\"fYXQnC\":[\"暖色里最浓的一档,温馨。\"],\"fttd2R\":[\"我的合集\"],\"gZ5owP\":[\"搜索仓库\"],\"gbqbh6\":[\"放心离开此页面 — 同步会在后台继续进行。\"],\"gkFvVN\":[\"注入到 </body> 之前。用于聊天小部件和不应阻塞页面加载的脚本。\"],\"gtQsRO\":[\"创建自定义 URL\"],\"hBO/y4\":[\"安全令牌已过期。刷新页面后重试。\"],\"hGmyDl\":[\"令牌让你无需登录即可从脚本、快捷方式和其他工具访问 API。\"],\"hIHkRy\":[\"已通过 GitHub App 连接\"],\"hJHHsU\":[\"发布站点、归档和合集的 Atom feeds。\"],\"hcH8MY\":[\"设置你的站点\"],\"hdSi1b\":[\"输入 \",[\"repo\"],\" 以确认\"],\"he3ygx\":[\"复制\"],\"hjeS3W\":[\"链接到 Latest。首页展示的就是这个列表。\"],\"hjvSlw\":[\"语言已移除,多语言已关闭。\"],\"hvGGMK\":[\"将此页面从导航中移除?页面本身不会被删除。\"],\"i0qMbr\":[\"首页\"],\"iEUzMn\":[\"系统\"],\"iH8pgl\":[\"返回\"],\"iMkWoF\":[\"启用\"],\"iSLIjg\":[\"连接\"],\"iVOMRi\":[\"主页设置已更新。\"],\"icB4Cv\":[\"将链接拖到此处以在更多菜单下显示它们\"],\"id3vuh\":[\"Telegram 已设置,但无法连接到机器人。请检查机器人令牌并重试。\"],\"idD8Ev\":[\"已保存\"],\"ihn4zD\":[\"搜索…\"],\"iiDXZc\":[\"显示在所有帖子和页面的底部。\"],\"itS31B\":[\"更暖一些,像略旧的纸。\"],\"iwQZvS\":[\"冷蓝灰\"],\"j4VrG6\":[\"下载导出 ZIP\"],\"j5nQL2\":[\"例如 iOS Shortcuts\"],\"jNE7O+\":[\"还没有公开帖子。jant.me 的收录条件是\",[\"minCount\",\"plural\",{\"other\":[\"满 \",\"#\",\" 篇\"]}],\"。\"],\"jUV7CU\":[\"上传头像\"],\"jVUmOK\":[\"支持 Markdown\"],\"jdVYS8\":[\"选一个适合你文字的。\"],\"jgBjXJ\":[\"撤销此令牌?任何使用它的脚本将停止工作。\"],\"jpctdh\":[\"查看\"],\"k1ifdL\":[\"处理中...\"],\"kLw03Y\":[\"亮纸白\"],\"kMXclu\":[\"下载网站导出\"],\"kNiQp6\":[\"已置顶\"],\"kQ3Otm\":[\"创建新页面\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"细粒度样式覆盖\"],\"ke1gWS\":[\"自定义 URL\"],\"kfcRb0\":[\"头像\"],\"kp8wiR\":[\"正在检查地址…\"],\"kxDZ2i\":[\"此代码将在你网站的每个页面上运行。\"],\"kzvWob\":[\"链接到帖子归档。\"],\"lLW3vJ\":[\"目标 slug\"],\"lLsfOE\":[\"关闭多语言?\"],\"lV04bQ\":[\"温暖、有泥土感,但不显暗。\"],\"lYHJih\":[\"撤销此会话?该设备需要重新登录。\"],\"m16xKo\":[\"添加\"],\"mLOk1i\":[\"立即将所有帖子推送到 GitHub,而不是等待下一次自动同步。\"],\"mSNmrX\":[\"列出帖子:\"],\"n8+EXe\":[\"添加站点中已经存在的常用入口。\"],\"nG2qTk\":[\"示例链接\"],\"nK07ni\":[\"为你的网站选择一种排版方向。每个主题都会同时改变字体搭配和阅读节奏。\"],\"nbfdhU\":[\"集成\"],\"ntJYyh\":[\"在 \",[\"providerLabel\"],\" 管理域名、套餐和计费\"],\"o/vNDE\":[\"允许你覆盖任何主题变量。\"],\"o5rj+L\":[\"还没有合集。在这里创建一个并添加到导航。\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"我们会预先填充名称 \",[\"name\"],\"。返回时列表会刷新。\"],\"oKOOsY\":[\"颜色主题\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有合集已在你的导航中。\"],\"oUn9Z7\":[\"近中性的浅灰,配钢蓝点缀。\"],\"ofdy2l\":[\"带橘调的底色。最暖的一套。\"],\"olouYg\":[\"导航将该地址作为链接保存。\"],\"oxj6Pk\":[\"其他语言\"],\"pIpTqF\":[\"每种语言都有独立的首页、归档和订阅源。启用后,发布时可以选择不同的语言,也可以为已有的帖子关联一篇其他语言的帖子。\"],\"pNXxtS\":[\"选择向读者和搜索引擎声明的内容语言。\"],\"pZq3aX\":[\"上传失败。请重试。\"],\"pgJImo\":[\"把主语言改为其他语言?\"],\"pgTIrt\":[\"选择要与此站点同步的 GitHub 账户和仓库。\"],\"psoxDF\":[\"该字体主题不可用。请选择另一个。\"],\"pt4OhQ\":[\"/about 已被占用。重命名该项目后再创建 About 页面。\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"合集\"],\"q/T5GS\":[\"私有 Jant 仪表板使用的语言。\"],\"qSgO/Y\":[\"跟随内容语言\"],\"qZ9jzv\":[\"站点可见性\"],\"qdcESc\":[\"创建新仓库\"],\"qkSJzV\":[\"正在搜索页面…\"],\"r5EW6f\":[\"此仓库已在为另一个 Jant 站点 (\",[\"host\"],\") 进行备份。请选择其他仓库。\"],\"rCPrbS\":[[\"language\"],\"的操作\"],\"rEspiY\":[\"导航位置已更新。\"],\"rFmBG3\":[\"配色主题\"],\"re0mF0\":[\"页面是公开的,但不会出现在 Latest。\"],\"rlonmB\":[\"删除失败。请稍后再试。\"],\"s3jOk7\":[\"请输入页面标题。\"],\"sROtqR\":[\"无法连接目录:\",[\"reason\"]],\"sRmGLp\":[\"之后可以在设置中更改。\"],\"satWc6\":[\"主 RSS 源\"],\"sgr2wQ\":[\"合集\"],\"sj4LLc\":[\"仅显示已修改项\"],\"sjej3N\":[\"移除「\",[\"language\"],\"」\"],\"slujBW\":[\"只能使用小写字母、数字和连字符。\"],\"soRdOu\":[\"米白纸底,配深森绿。\"],\"sqxcaY\":[\"创建于 \",[\"date\"]],\"sxkWRg\":[\"高级\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tJ4H0O\":[\"你的 Telegram 账号\"],\"tfDRzk\":[\"保存\"],\"tgWuMB\":[\"已修改\"],\"tvgAq5\":[\"尚未授权任何账户\"],\"u1VTd3\":[\"调色板、表面色调与整体氛围\"],\"u3wRF+\":[\"已发布\"],\"u6KOjV\":[\"想要更多控制?\"],\"uKLe0J\":[[\"count\"],\" 个设置已显示\"],\"uVZcIA\":[\"此地址已被使用。请选择其他地址。\"],\"udPwLB\":[\"导航栏\"],\"ugHDtd\":[[\"about\"],\"帖子设为 Featured \",[\"hours\"],\" 小时后会出现在 Discover 首页,Link 和 Quote 类型的帖子发布 \",[\"hours\"],\" 小时后出现在 \",[\"links\"],\" 和 \",[\"quotes\"],\" 列表。这段时间里你可以继续修改,详见 \",[\"rules\"],\"。\"],\"ui6aMF\":[\"以下设备当前已登录到你的账户。撤销任何你不认识的会话。\"],\"vBEKwo\":[\"在此管理本站点的活动会话。密码和托管访问通过 \",[\"providerLabel\"],\" 管理。\"],\"vOnuOa\":[\"在导航栏和更多菜单中显示或隐藏内置目标。\"],\"vRldcl\":[\"排版选项与阅读质感\"],\"vSYKYI\":[\"主订阅源\"],\"vTuib7\":[\"这将控制 /feed 返回的内容。\"],\"vXCC6J\":[\"有内容填写不正确,检查后重试。\"],\"vXIe7J\":[\"语言\"],\"vXsx3x\":[\"该页面是私有的,因此任何人都无法通过菜单打开它。\"],\"vdFnYM\":[\"重置链接\"],\"vmQmHx\":[\"添加自定义 CSS 以覆盖任何样式。使用数据属性如 [data-page], [data-post], [data-format] 来定位特定元素。\"],\"vpSPA1\":[\"缺少认证密钥,检查环境变量配置。\"],\"vzX5FB\":[\"删除账号\"],\"w615zC\":[\"多语言\"],\"w8Rv8T\":[\"标签为必填项\"],\"wL3cK8\":[\"Latest\"],\"wW6NCp\":[\"上次错误\"],\"wc+17X\":[\"/* 在此填写你的自定义 CSS */\"],\"wuLtXn\":[\"当前没有活动会话。 已登录的设备会显示在此处。\"],\"x+HGBk\":[\"请求搜索引擎不要对公开页面建立索引或将其包含在搜索结果中。\"],\"xCWek4\":[\"文件存储尚未设置。请检查服务器配置。\"],\"xCbjQn\":[\"允许我的博客出现在 \",[\"name\"],\" 中\"],\"xGVfLh\":[\"继续\"],\"xHt036\":[\"个人访问令牌\"],\"xKeZ0l\":[\"更偏暖、偏土的色调。\"],\"xYbozN\":[\"柔象牙白\"],\"xqViCq\":[\"暖象牙白\"],\"xxaahd\":[\"想要更冷、更利落的观感时。\"],\"y/awtV\":[\"导航链接、内置目标和更多菜单\"],\"y28hnO\":[\"帖子\"],\"y9W9vo\":[\"正在创建合集…\"],\"yNCqOt\":[\"Latest 订阅源\"],\"yQ3kNF\":[\"输入以下短语以确认:\"],\"ydq1k2\":[\"请先选择一个账号\"],\"yjjCV8\":[\"固定订阅源 URL\"],\"yjkELF\":[\"确认新密码\"],\"ym+ccl\":[\"在主页显示 Jant 署名。\"],\"yzF66j\":[\"链接\"],\"z6wakA\":[\"在你的主页上显示的简短介绍。\"],\"zEizrk\":[\"上次使用 \",[\"date\"]],\"zPqly9\":[\"这个站点没有可提交的目录。\"],\"zSURJW\":[\"没有匹配的仓库。\"],\"zUlHGd\":[\"页面地址\"],\"zYRGIR\":[\"尚未提交。还没有目录知道这个站点存在。\"],\"zcDmsG\":[\"Featured 帖子\"],\"zlcDd2\":[\"要删除此自定义 URL 吗?使用它的访问者将不再被重定向。\"],\"ztPYIR\":[\"启用时会一次性标记已有帖子\"],\"zwBp5t\":[\"私有\"],\"zxexio\":[\"内容语言、界面语言、多语言\"]}");
2015
+ var messages$1 = JSON.parse("{\"+4Z6iP\":[\"请先在 GitHub 上创建仓库 — 可以为空。\"],\"+9JI/F\":[\"连接后会将你的网站同步到 \",[\"repo\"],\" 的默认分支,并追加到其现有历史之上。Jant 管理路径之外的现有文件会被保留。此操作无法撤销。\"],\"+AXdXp\":[\"标签和 URL 为必填项\"],\"+K0AvT\":[\"断开连接\"],\"+V/jXc\":[\"直接指向 /feed,即 Atom 原始文件,目前返回的是「\",[\"feed\"],\"」。适合已经在用阅读器的读者。\"],\"+VEqyj\":[\"设为主语言\"],\"+jXHMG\":[[\"count\",\"plural\",{\"other\":[\"还有 \",\"#\",\" 篇\",[\"language\"],\"帖子。先修改它们的语言,或保留该语言。\"]}]],\"+wgt7C\":[\"低饱和的红棕底,温暖、有泥土感。\"],\"+zy2Nq\":[\"类型\"],\"/0D1Xp\":[\"编辑合集\"],\"/3H2/s\":[\"此托管站点通过 \",[\"providerLabel\"],\" 登录。请在那里管理密码和托管访问权限。\"],\"/PXXBT\":[\"近乎纯白,带一点暖意。\"],\"/PoNoq\":[\"编辑链接\"],\"/zOUxl\":[\"链接到 Telegram 机器人的二维码\"],\"05DXsb\":[\"连接或管理用于从 Telegram 发帖的 Bot。\"],\"0OGSSc\":[\"头像显示已更新。\"],\"0UzCUX\":[\"更新你用于登录的密码\"],\"0VVvqu\":[\"多语言已启用。\"],\"0bdA9b\":[\"打开 Telegram 以连接\"],\"0dieZ/\":[\"/archive 打开时用哪种版式。读者可以在页面上自行切换。\"],\"0gECZD\":[\"想写更完整的介绍?\"],\"0uIjy/\":[\"浅鼠尾草绿底,配同色系的绿。\"],\"1DahsC\":[\"搜索设置\"],\"1F6Mzc\":[\"当前还没有导航项目。添加链接或在下方启用系统项目。\"],\"1H7gng\":[\"后台语言\"],\"1MYU3o\":[\"无法创建页面。请检查详情并重试。\"],\"1mbBbL\":[\"手动绑定\"],\"1njn7W\":[\"浅色\"],\"1pD+7W\":[\"演示站点不会出现在 \",[\"name\"],\" 中。\"],\"1qGdnL\":[\"默认主题。沉静,适合长时间阅读。\"],\"1wdDm9\":[\"添加语言\"],\"21mg6u\":[\"选择一篇尚未添加到导航的有标题笔记,或创建新页面。\"],\"2C7mSG\":[\"合集链接\"],\"2DoBvq\":[\"订阅源\"],\"2Et8jU\":[\"设置每页搜索结果数;重置后继承默认分页大小。\"],\"2FYpfJ\":[\"更多\"],\"2Ithfh\":[\"向机器人发送任意文本,它会作为笔记已发布。\"],\"2PTjMB\":[\"我想删除 \",[\"siteName\"]],\"2QRni+\":[\"时区已更新。\"],\"2UrpGC\":[\"订阅源地址已送达。目录会在 \",[\"hours\"],\" 小时内首次读取。\"],\"2ZqpRB\":[\"关闭\"],\"2cFU6q\":[\"网站页脚\"],\"2fPEPI\":[\"沉静、偏冷的底色。\"],\"2lkk2l\":[\"微黄的纸底,配橄榄绿。\"],\"2oWZo7\":[\"最近一次提交\"],\"2r8pkt\":[\"无法加载页面。请稍后再试。\"],\"2uuy4H\":[\"通过个人访问令牌连接\"],\"2wK4BX\":[\"编辑 About 页面\"],\"35x8eZ\":[\"显示 \",[\"shown\"],\" 共 \",[\"total\"]],\"39QGku\":[\"打开机器人并发送绑定码,然后你发送给它的任何消息都会变成一条笔记。\"],\"3B0RD1\":[\"上传个人资料图片和生成的网站图标。\"],\"3Cw1AI\":[\"添加合集\"],\"3VrybB\":[\"重定向\"],\"3Yvsaz\":[\"302 (临时)\"],\"3n0zbB\":[\"会话管理在演示模式下已关闭。请使用共享的演示会话。\"],\"3sYJi5\":[\"下载一个与 Hugo 兼容的归档 — 将其静态托管或迁移到另一个 Jant。\"],\"3vMdv3\":[\"此链接为保留链接。请选择其他链接。\"],\"3wKq0C\":[\"保存失败。请稍后再试。\"],\"49Bsal\":[\"订阅源设置已更新。\"],\"4D09NB\":[\"链接到合集页。\"],\"4J/OYU\":[\"合集已创建。\"],\"4Jge8E\":[\"活动会话\"],\"4KIa+q\":[\"导出文件已下载。\"],\"4Q0iPK\":[\"跟随设备外观或强制使用浅色或深色主题。\"],\"4cEClj\":[\"会话\"],\"4n7WY4\":[\"站名、可见性、订阅源与时区\"],\"4tcgrg\":[\"查看这些帖子\"],\"4yyXWu\":[\"浅骨白底,配低饱和的绿。\"],\"4zGJ5E\":[\"永久删除账号\"],\"5QlUIt\":[\"仓库为空。准备连接。\"],\"5VQnR3\":[\"当你想要一个永远不变的订阅源 URL 时使用它们。\"],\"5dpcN1\":[\"输入以搜索全部\"],\"5f1Wo9\":[\"已连接为 \",[\"account\"]],\"5o8R81\":[\"极浅的象牙底,配淡淡的茶绿。\"],\"6ArdBh\":[\"将 Featured 帖子用于 /feed。\"],\"6DjeBT\":[\"演示站点始终对搜索引擎隐藏。\"],\"6E3aK4\":[\"管理托管\"],\"6FFB7q\":[\"为 /feed 使用最新的公开帖子。\"],\"6K1Vef\":[\"永久删除此博客?此操作不可撤销。\"],\"6NpNLc\":[\"此仓库已有内容。\"],\"6Pa5AP\":[\"连接或管理 GitHub 内容自动备份。\"],\"6V3Ea3\":[\"已复制\"],\"6WdDG7\":[\"页面\"],\"71WIgc\":[\"获取新绑定码\"],\"71of4k\":[\"限制每个 RSS feed 中包含的帖子数。\"],\"746NHh\":[\"此博客\"],\"7811AW\":[\"此仓库已在备份此站点。\"],\"7FCZPo\":[\"内容语言\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允许搜索引擎索引我的网站\"],\"7GISOt\":[\"保存机器人令牌\"],\"7MZxzw\":[\"密码已更改。\"],\"7p5kLi\":[\"后台\"],\"7vhWI8\":[\"新密码\"],\"81nFIS\":[\"密码不匹配。请确保两个字段相同。\"],\"87a/t/\":[\"标签\"],\"89Upyo\":[\"该主题不可用。请选择其他主题。\"],\"8BfEpW\":[\"托管账户\"],\"8NIu3g\":[\"该页面尚无标题,因此菜单没有可显示的内容。\"],\"8T46pB\":[\"机器人令牌\"],\"8U2Z7f\":[\"新建自定义 URL\"],\"8ZsakT\":[\"密码\"],\"8bpHix\":[\"账户创建失败,检查填写内容后重试。\"],\"8gEghq\":[\"明亮的暖白底,配苔绿点缀。\"],\"9+vGLh\":[\"自定义 CSS\"],\"9As8Nu\":[\"在 GitHub 上创建一个\"],\"9EjI4j\":[\"暖沙色\"],\"9Fa2Ns\":[\"根地址将恢复显示所有语言,\",[\"prefix\"],\" 这类地址会重定向到它,已有的链接和订阅仍然有效。帖子地址和每篇帖子的语言都不受影响,随时可以重新启用。\"],\"9FctRm\":[\"使用小写字母、数字和连字符。\"],\"9J6wUO\":[\"建议添加的链接\"],\"9Lsvt5\":[\"于 \",[\"date\"],\" 登录\"],\"9SHZas\":[\"登录后显示「设置」,未登录时显示「登录」。\"],\"9T7Cwm\":[\"重定向、个性化路径和 URL 控制\"],\"9aUyym\":[\"查看你的登录位置并撤销旧会话\"],\"9mqQnx\":[\"限制自动生成摘要所使用的段落数。\"],\"A7tRC9\":[\"规范的 /feed URL 返回哪个帖子流。\"],\"ADTn/D\":[\"允许无需 API 令牌读取已发布内容。\"],\"AELGTd\":[\"现在添加到导航,或打开编辑器补充详细信息。\"],\"ANzR96\":[\"配置编辑器\"],\"AQL8b1\":[\"你写作的主要语言是?\"],\"AeXO77\":[\"账户\"],\"AnY+O9\":[\"在主页底部显示 \\\"Build with Jant\\\"\"],\"ApZDMk\":[\"此图用于你的 favicon 和 apple-touch-icon。为获得最佳效果,请上传至少 512×512 像素、纯色背景的方形 PNG。\"],\"Aysjjh\":[\"选择在整个 Jant 中使用的配色方案。\"],\"B495Gs\":[\"归档\"],\"B4ESok\":[\"API 参考\"],\"B5P9HE\":[\"提交我的站点\"],\"BJ0tsF\":[\"搜索运行时设置并打开高级配置\"],\"BTYdze\":[\"链接已添加到导航。\"],\"BszQc4\":[\"根地址(/、/feed)显示这个语言的内容。\"],\"BzEFor\":[\"或\"],\"C0/57J\":[\"这是合集链接的最后一部分。\"],\"CDAdlf\":[\"移除机器人\"],\"CTAEes\":[\"选择仓库\"],\"CjZZgz\":[\"该仓库已有提交\"],\"Cl92kR\":[\"创建新合集\"],\"D35jh6\":[\"管理后台的显示语言,只有你能看到。\"],\"D8k2s6\":[\"连接 Telegram\"],\"DCKkhU\":[\"当前密码\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密码和托管访问\"],\"DVFXa6\":[\"最干净、最中性的一档。\"],\"DdXUay\":[\"在公共页眉中显示站点头像。\"],\"E3hjKv\":[\"添加为链接\"],\"EKvXGx\":[\"该地址在另一个网站上。Navigation 将其保留为链接。\"],\"EO3I6h\":[\"上传未成功。请稍后再试。\"],\"Eax/et\":[\"强调色\"],\"EbDLD+\":[\"选择 Jant 全局使用的字体。\"],\"Enslfm\":[\"目标地址\"],\"F53UDu\":[\"冷白\"],\"F7FKwe\":[\"你在此处粘贴的任何内容都将完全访问访客的浏览器。仅使用来自你信任的来源的代码。\"],\"F8SNqr\":[\"暖橘\"],\"FBgjRN\":[\"搜索页面,或粘贴地址\"],\"FMUJSP\":[\"账户与数据\"],\"FUfPPw\":[\"根地址(/、/feed、/archive)将改为提供「\",[\"next\"],\"」的内容,「\",[\"previous\"],\"」移至 \",[\"prefix\"],\"。帖子地址不会改变,但订阅了 /feed 的读者从此收到的是「\",[\"next\"],\"」的帖子。\"],\"Fe1cvJ\":[\"延迟新帖子和回复进入订阅源的时间。设为 0 可关闭延迟。\"],\"Fk3SSD\":[\"平静自然,看着舒服。\"],\"FkMol5\":[\"Featured\"],\"FwL2Zk\":[[\"count\",\"plural\",{\"other\":[\"还有 \",\"#\",\" 篇\",[\"language\"],\"帖子,但列表里没有这个语言。把它加回列表,或先修改这些帖子的语言。\"]}]],\"G/1oP+\":[\"移除 webhook 并停止同步。你的仓库内容不会被删除。\"],\"G0qJsQ\":[\"缺少安全令牌。刷新页面后重试。\"],\"G0tHaW\":[\"创建一个不会出现在 Latest 的公开页面。\"],\"G2fuEb\":[\"已锁定\"],\"G39wnK\":[\"将内容备份并与 GitHub 仓库同步\"],\"G5oCui\":[\"URL 结构、每种语言的订阅源,以及译本关联。\"],\"G9peMt\":[\"创建账户\"],\"GAmD3h\":[\"语言\"],\"GBkMNw\":[[\"name\"],\" 是 Jant 社区人工维护的目录,旨在帮助用户发现新的 Jant 博客和内容。\"],\"GXsAby\":[\"撤销\"],\"GdGiNi\":[\"第 \",[\"current\"],\" 步,共 \",[\"total\"],\" 步\"],\"GorKul\":[\"欢迎使用 Jant\"],\"GxkJXS\":[\"正在上传...\"],\"GzKzUa\":[\"演示限制\"],\"HCNlq3\":[\"干净、高对比,接近纯白。\"],\"HHDyGw\":[\"柔绿\"],\"HKH+W+\":[\"数据\"],\"Hp1l6f\":[\"当前\"],\"HxTUmw\":[\"添加 \",[\"language\"]],\"HxlY7t\":[\"更改此项会更新订阅者从 /feed 获取的内容。\"],\"HxuOlm\":[\"网站头部\"],\"I0ijRI\":[\"全部 feed 地址\"],\"I4HRxU\":[\"已发布的全部帖子,包括从 Latest 中隐藏的帖子。\"],\"I6gXOa\":[\"路径\"],\"I76CzF\":[\"中性灰\"],\"ID38tA\":[\"演示模式下已禁用账号删除。共享演示会单独重置。\"],\"IF9tPu\":[\"何时使用站点导出、数据库备份和恢复演练。\"],\"IW5PBo\":[\"复制令牌\"],\"IagCbF\":[\"网址\"],\"IreQBq\":[\"仓库\"],\"J36Xsm\":[\"在 \",[\"address\"],\" 没有任何内容。检查它,或按标题搜索。\"],\"J6bLeg\":[\"向任意 URL 添加自定义链接\"],\"JL7LF5\":[\"可用的 CSS 变量,数据属性,和 示例。\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"现在复制你的令牌 — 它不会再次显示。\"],\"JrFTcr\":[\"连接中…\"],\"JuN5GC\":[\"未选择文件。请选择要上传的文件。\"],\"K+0Hu0\":[\"没有可添加的匹配页面。换个标题搜索,或创建新页面。\"],\"K/F6pa\":[\"正在保存…\"],\"KDw4GX\":[\"重试\"],\"KSgo21\":[\"选择仓库\"],\"KVVYBh\":[\"向导航添加合集\"],\"KiJn9B\":[\"笔记\"],\"Kk7jwL\":[\"设置每页归档帖子数;重置后继承默认分页大小。\"],\"KwOLJF\":[\"Jant 最初的配色,日常稳妥之选。\"],\"L+rMC9\":[\"重置为默认\"],\"L27RpE\":[\"页面已创建。\"],\"L3DEwT\":[\"移除此头像?你的网站图标和站点顶部图标将恢复为默认设置。\"],\"L4t4/q\":[\"3月14日\"],\"L86zmP\":[\"在完整表单中编辑首页和元数据使用的多行简介。\"],\"LdyooL\":[\"链接\"],\"LhMjLm\":[\"时间\"],\"LjwaJ/\":[\"登录、导出和删除\"],\"M/D8PK\":[\"+ 在其他账户上安装\"],\"M/haSd\":[\"始终显示浅色主题。\"],\"M/kDIW\":[\"搜索引擎看到的语言。\"],\"M1co/O\":[\"已配置\"],\"M2hGyf\":[\"指向 /subscribe,一个列出你所有 feed 地址、带复制按钮的页面。适合还没在用阅读器的读者。\"],\"M2kIWU\":[\"字体主题\"],\"M4tzVU\":[\"Latest 帖子\"],\"M6CbAU\":[\"切换编辑面板\"],\"M7wPsD\":[\"切换\"],\"MHrjPM\":[\"标题\"],\"MKIM3K\":[\"搜索页面\"],\"MaYYE6\":[\"通过向 Telegram 机器人发送消息来发布笔记\"],\"Me5t5H\":[\"连接 GitHub 仓库,自动把帖子备份成 Markdown。在 GitHub 上的修改会同步回站点。\"],\"MeCJlL\":[\"正在提交站点,刷新后可以看到结果。\"],\"MnbH31\":[\"页面\"],\"Mq9FZ1\":[\"正在创建页面…\"],\"Mr4QPw\":[\"断开 Telegram?你可以随时使用新的绑定码重新连接。\"],\"MtENL9\":[\"调整你的网站的外观、阅读体验和运行方式。\"],\"N/8NPV\":[\"在删除之前,请下载站点导出。删除后将无法恢复此账户。\"],\"N7UNHY\":[\"Featured 订阅源\"],\"NHnUHF\":[\"Favicon 和站点顶部的个人标识\"],\"NU2Fqi\":[\"保存 CSS\"],\"NVjhde\":[\"暖羊皮纸\"],\"Nldjdr\":[\"还没有自定义 URL。新建一个,给帖子加重定向或自定义路径。\"],\"O3oNi5\":[\"邮箱\"],\"OJxdgi\":[\"链接不能超过 200 个字符。\"],\"OSJXFg\":[\"应用于整个站点,包括管理页面。选择一个调色板,然后选择它是随系统变化还是保持固定。\"],\"OeUWA7\":[\"添加页面\"],\"OuuMXJ\":[\"在 </body> 闭合标签之前添加全站 HTML。\"],\"Ox3+3h\":[\"无匹配结果。\"],\"P7Eeay\":[\"完整帖子\"],\"PEUV5I\":[\"代码注入已更新。\"],\"PHh52z\":[\"暖而厚重,棕调明显。\"],\"PXj9lw\":[\"停止接受来自 Telegram 的帖子。你的现有笔记将保持已发布。\"],\"PZ7HJ8\":[\"博客头像\"],\"Pbm2/N\":[\"创建合集\"],\"Pwqkdw\":[\"正在加载…\"],\"PxJ9W6\":[\"生成令牌\"],\"Q/6Y+2\":[\"需要对目标仓库的 Contents(读/写)和 Webhooks(读/写)。\"],\"Q/O0X4\":[\"此设置未保存。请检查该值并重试。\"],\"Q30z/l\":[\"要从导航中移除此合集吗?合集本身不会被删除。\"],\"Q99OtV\":[\"将合集固定到导航栏。在过去 48 小时内更新的合集旁会出现一个 * 号。\"],\"QCwsv1\":[\"暖白\"],\"QKvrmL\":[\"暖中性,适合长读。\"],\"QZmz0H\":[\"内置链接\"],\"Qnrzvb\":[\"活动令牌\"],\"R6Z4LE\":[\"下载失败。请重试。\"],\"R9Khdg\":[\"自动\"],\"RDjuBN\":[\"初始设置\"],\"RRo9kN\":[\"语言已更新。\"],\"RcdDOS\":[\"在 Telegram 上给 @BotFather 发送消息创建一个机器人,然后粘贴它提供的令牌。\"],\"RdVIcf\":[\"奶油配深咖\"],\"Ri/9PY\":[\"当前仍只向 Discover 提供精选帖子,但没有任何帖子标为 Featured,订阅源里没有可展示的内容。取消勾选再重新勾选,即可让 Discover 读取全部公开帖子。\"],\"Rn2p1h\":[\"没有匹配此搜索的结果。试试其他名称或描述。\"],\"RxsRD6\":[\"时区\"],\"SDND4q\":[\"未配置\"],\"SJmfuf\":[\"站点名称\"],\"SKZhW9\":[\"令牌名称\"],\"SSsoa4\":[\"添加到导航\"],\"SVQQPe\":[\"无法连接。检查错误并重试。\"],\"SWb0z+\":[\"此地址为保留地址。请选择其他地址。\"],\"SYGk01\":[\"以 URL 前缀提供的语言。在「语言」页管理。\"],\"SchpMp\":[\"Telegram\"],\"SeY4vo\":[\"移除 \",[\"language\"],\" 后就只剩主语言了,多语言会一并关闭。根地址将恢复显示所有语言,\",[\"prefix\"],\" 不再可用,每篇帖子的语言保持不变。\"],\"SqKp3o\":[\"在整个网站、浏览器标签页和订阅源中使用的标题。\"],\"SrGs4T\":[\"浅蓝灰底,冷静而均匀。\"],\"T/R+Qz\":[\"主语言\"],\"T17dXm\":[\"每种语言是否拥有独立的首页、归档和订阅源。在「语言」页管理。\"],\"T41PG1\":[\"限制自动生成摘要所使用的字符数。\"],\"TN0mN4\":[\"搜索和编辑运行时设置。更改会立即生效;重置会恢复环境变量或内置默认值。\"],\"TSCeuF\":[\"无法创建合集。请检查信息后重试。\"],\"TpF3v+\":[\"注入到 </head> 之前。用于分析、自定义元标签以及必须尽早加载的样式。\"],\"Tu6bMZ\":[\"创建 About 页面\"],\"Tz0i8g\":[\"设置\"],\"U5J7jI\":[\"站点可见性已更新。\"],\"U5v6Gh\":[\"编辑页面\"],\"UFK415\":[\"用于分析和小部件的全站 HTML\"],\"UTvFQq\":[\"打开 \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" 并发送:\"],\"UUn+Y5\":[\"打开设置\"],\"UaZwcz\":[\"创建后可以设置更多选项。\"],\"Uj/btJ\":[\"在我的站点顶部显示头像\"],\"UsODUn\":[\"选择账户\"],\"UxKoFf\":[\"导航栏\"],\"UyMSeQ\":[\"在该地址\"],\"V+bhUy\":[\"安装 GitHub App\"],\"V0fyg5\":[\"合集已添加到导航。\"],\"V4WsyL\":[\"添加链接\"],\"V5pZwT\":[\"搜索设置已更新。\"],\"V7dQi9\":[\"语言已移除。\"],\"VXUPla\":[\"使用 GitHub 应用连接\"],\"Vh+JIV\":[\"多语言指南\"],\"VhMDMg\":[\"更改密码\"],\"Vn3jYy\":[\"导航项\"],\"VoZYGU\":[\"这会永久删除你的全部数据——帖子、媒体、合集、设置和账户。博客会重置为初始设置状态。此操作无法撤销。\"],\"VoarBQ\":[\"所有帖子的地址都不会改变,也可以随时再关闭。\"],\"VqyJ7Y\":[\"主语言已更改。\"],\"WL7KjI\":[\"卡片网格\"],\"WUnFK2\":[\"偏冷的浅白底,配深靛蓝文字。高对比。\"],\"Wa9q4P\":[\"纯白\"],\"Wb7EHo\":[\"About 页面\"],\"Weq9zb\":[\"常规\"],\"Wi9i06\":[\"遵循每位访客的系统偏好。\"],\"Wildi8\":[\"没有可添加的页面。创建一个页面后即可添加到导航。\"],\"Wx1M8N\":[\"安装 GitHub App 以在无需管理个人令牌的情况下授予访问权限。权限按仓库范围授予,可在 GitHub 上撤销。\"],\"X+8FMk\":[\"当前密码不正确。请重试。\"],\"X1G9eY\":[\"导航栏预览\"],\"X2Cbc2\":[\"归档订阅源\"],\"X5P6yv\":[\"最近更新\"],\"X9Hujr\":[\"手动推送\"],\"XQQOyj\":[\"该页面是草稿 (未发布内容)。先发布它,然后再添加。\"],\"XeRkls\":[[\"count\",\"plural\",{\"other\":[\"其中 \",\"#\",\" 篇还没有语言,会标记为「\",[\"language\"],\"」。\"]}]],\"Xsu5WL\":[\"在专用代码编辑器中添加全站 CSS。\"],\"XtBJV8\":[\"正在检查仓库…\"],\"Xtc16w\":[\"刷新仓库列表\"],\"Y+7JGK\":[\"创建页面\"],\"Y/F35r\":[\"使用 curl 创建帖子:\"],\"Y/N5N7\":[\"柔和的奶油底,配低饱和的绿。\"],\"Y4oXje\":[\"名称预填为 \",[\"name\"],\"。返回后列表会刷新。\"],\"YF6zHf\":[\"站点设置已更新。\"],\"YVcVlW\":[\"移除并关闭\"],\"Ya/FAk\":[\"至少再添加一种语言才能启用。\"],\"YdG2RF\":[\"导出站点\"],\"YkgZi7\":[\"连接一个 Telegram 机器人,然后你发送给它的任何消息都会作为笔记已发布。\"],\"YwhjRx\":[\"管理账户\"],\"Yxp859\":[\"暖奶油色\"],\"Z5HWHd\":[\"已启用\"],\"ZDY7Fy\":[\"正在同步…\"],\"ZQKLI1\":[\"危险操作\"],\"ZS/CBL\":[\"删除此导航链接?访客将不再在你的网站导航栏中看到它。\"],\"ZXZrAo\":[\"页面地址不能超过 200 个字符。\"],\"ZgZX+d\":[\"纯白底配中性灰。不带任何色偏。\"],\"Zgq+c2\":[\"设置默认每页显示的条目数。\"],\"ZhhOwV\":[\"引用\"],\"ZiooJI\":[\"API 令牌\"],\"Zm7Qb0\":[\"备份与恢复指南\"],\"ZmUkwN\":[\"向导航添加自定义链接\"],\"a14mj8\":[\"未知设备\"],\"a1iEgy\":[\"暖奶油底,配深咖啡棕点缀。\"],\"a3LDKx\":[\"安全\"],\"aAIQg2\":[\"外观\"],\"aFkzVF\":[\"目标帖子或合集的 slug\"],\"aR3U86\":[\"多语言已启用。\",[\"count\",\"plural\",{\"other\":[\"#\",\" 篇帖子\"]}],\"已标记为「\",[\"language\"],\"」。\"],\"aV6XWN\":[\"多语言已关闭。你的语言设置仍然保留。\"],\"alKG0+\":[\"字体主题\"],\"anibOb\":[\"关于本博客\"],\"any7NR\":[\"主题指南\"],\"asq0vL\":[\"你写作使用的语言。\"],\"b+/jO6\":[\"301 (永久)\"],\"b+FyBD\":[\"向导航添加页面\"],\"b1FYSK\":[\"主语言\"],\"bHOiy1\":[\"演示模式下已禁用密码更改。请使用共享的演示凭证登录。\"],\"bHYIks\":[\"退出登录\"],\"bV2vng\":[\"暖陶土色\"],\"bbR5vW\":[\"这套配色\"],\"bbzY7X\":[\"管理登录安全、站点导出和不可恢复的操作。\"],\"bfHZ7r\":[\"选择用于显示日期和时间的时区。\"],\"bi10qS\":[\"现在添加到导航,或打开编辑器补充内容。\"],\"bkMuwo\":[\"链接到你标为 Featured 的帖子。\"],\"bmrL08\":[\"演示模式会隐藏会话、密码更改和账号删除。导出仍然可用。\"],\"bph1Dd\":[\"已关闭搜索引擎索引,所以默认不出现在 Jant Discover。勾选上面的选项仍会加入。\"],\"brfpw9\":[\"在完整表单中编辑显示在公共页面底部的多行页脚。\"],\"bviiKV\":[\"这个主题下的标题、正文和链接。\"],\"c1BGrV\":[\"已在导航中。将其在上方列表中拖动以移动。\"],\"c1iSrq\":[\"Discover 社区规则\"],\"c3MN2z\":[\"所有可用的端点和请求格式。\"],\"cHh/Zu\":[\"启用多语言\"],\"cS7/bk\":[\"删除保存的机器人令牌?其 webhook 会被删除,任何已连接的账号将被断开连接。\"],\"cSDy01\":[\"自定义 CSS 已更新。\"],\"clzoNp\":[\"始终显示暗色主题。\"],\"cnGeoo\":[\"删除\"],\"cwaLCZ\":[\"页面已添加到导航。\"],\"d1lzY/\":[\"语言已添加。\"],\"d3FRkY\":[\"无法复制。请再试一次。\"],\"d5oGUo\":[\"在 GitHub 上创建新仓库\"],\"dB1Nr4\":[\"开始写作\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"确认删除账户\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"永久删除所有数据并重置博客\"],\"ds0nJk\":[\"在 </head> 结束标签之前添加全站 HTML。\"],\"dsWkIw\":[\"要与 GitHub 断开连接吗?该 webhook 将被移除。你的仓库内容不会被删除。\"],\"dwcK1n\":[\"或手动提交地址\"],\"e/tSI5\":[\"导航顺序已更新。\"],\"eOL7vn\":[\"色彩极少,干扰最小。\"],\"ePK91l\":[\"编辑\"],\"ebQKK7\":[\"站点\"],\"eeamrQ\":[\"Discover 读取的是 Atom 订阅源,需要先开启订阅源。\"],\"egK+Yy\":[\"用于脚本和自动化的 Bearer 令牌\"],\"egOXl0\":[\"Jant Discover\"],\"ehj/zN\":[\"重定向类型\"],\"ej6Rji\":[\"若有别的语言的帖子,之后可以在那篇帖子的菜单里单独更正。\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"订阅源 URL 已复制。\"],\"f8fH8W\":[\"设计\"],\"fDz6PV\":[\"选择用于内容备份的仓库。\"],\"fKRAwQ\":[\"沙色底,配绿色点缀。\"],\"fWYqkz\":[\"代码注入\"],\"fYXQnC\":[\"暖色里最浓的一档,温馨。\"],\"fttd2R\":[\"我的合集\"],\"gZ5owP\":[\"搜索仓库\"],\"gbqbh6\":[\"放心离开此页面 — 同步会在后台继续进行。\"],\"gkFvVN\":[\"注入到 </body> 之前。用于聊天小部件和不应阻塞页面加载的脚本。\"],\"gtQsRO\":[\"创建自定义 URL\"],\"hBO/y4\":[\"安全令牌已过期。刷新页面后重试。\"],\"hGmyDl\":[\"令牌让你无需登录即可从脚本、快捷方式和其他工具访问 API。\"],\"hIHkRy\":[\"已通过 GitHub App 连接\"],\"hJHHsU\":[\"发布站点、归档和合集的 Atom feeds。\"],\"hcH8MY\":[\"设置你的站点\"],\"hdSi1b\":[\"输入 \",[\"repo\"],\" 以确认\"],\"he3ygx\":[\"复制\"],\"hjeS3W\":[\"链接到 Latest。首页展示的就是这个列表。\"],\"hjvSlw\":[\"语言已移除,多语言已关闭。\"],\"hvGGMK\":[\"将此页面从导航中移除?页面本身不会被删除。\"],\"i0qMbr\":[\"首页\"],\"iEUzMn\":[\"系统\"],\"iH8pgl\":[\"返回\"],\"iMkWoF\":[\"启用\"],\"iSLIjg\":[\"连接\"],\"iVOMRi\":[\"主页设置已更新。\"],\"icB4Cv\":[\"将链接拖到此处以在更多菜单下显示它们\"],\"id3vuh\":[\"Telegram 已设置,但无法连接到机器人。请检查机器人令牌并重试。\"],\"idD8Ev\":[\"已保存\"],\"ihn4zD\":[\"搜索…\"],\"iiDXZc\":[\"显示在所有帖子和页面的底部。\"],\"itS31B\":[\"更暖一些,像略旧的纸。\"],\"iwQZvS\":[\"冷蓝灰\"],\"j4VrG6\":[\"下载导出 ZIP\"],\"j5nQL2\":[\"例如 iOS Shortcuts\"],\"jNE7O+\":[\"还没有公开帖子。jant.me 的收录条件是\",[\"minCount\",\"plural\",{\"other\":[\"满 \",\"#\",\" 篇\"]}],\"。\"],\"jUV7CU\":[\"上传头像\"],\"jVUmOK\":[\"支持 Markdown\"],\"jdVYS8\":[\"选一个适合你文字的。\"],\"jgBjXJ\":[\"撤销此令牌?任何使用它的脚本将停止工作。\"],\"jpctdh\":[\"查看\"],\"k1ifdL\":[\"处理中...\"],\"kLw03Y\":[\"亮纸白\"],\"kMXclu\":[\"下载网站导出\"],\"kNiQp6\":[\"已置顶\"],\"kQ3Otm\":[\"创建新页面\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"细粒度样式覆盖\"],\"ke1gWS\":[\"自定义 URL\"],\"kfcRb0\":[\"头像\"],\"kp8wiR\":[\"正在检查地址…\"],\"kxDZ2i\":[\"此代码将在你网站的每个页面上运行。\"],\"kzvWob\":[\"链接到帖子归档。\"],\"lLW3vJ\":[\"目标 slug\"],\"lLsfOE\":[\"关闭多语言?\"],\"lV04bQ\":[\"温暖、有泥土感,但不显暗。\"],\"lYHJih\":[\"撤销此会话?该设备需要重新登录。\"],\"m16xKo\":[\"添加\"],\"mLOk1i\":[\"立即将所有帖子推送到 GitHub,而不是等待下一次自动同步。\"],\"mSNmrX\":[\"列出帖子:\"],\"n8+EXe\":[\"添加站点中已经存在的常用入口。\"],\"nG2qTk\":[\"示例链接\"],\"nK07ni\":[\"为你的网站选择一种排版方向。每个主题都会同时改变字体搭配和阅读节奏。\"],\"nbfdhU\":[\"集成\"],\"ntJYyh\":[\"在 \",[\"providerLabel\"],\" 管理域名、套餐和计费\"],\"o/vNDE\":[\"允许你覆盖任何主题变量。\"],\"o5rj+L\":[\"还没有合集。在这里创建一个并添加到导航。\"],\"oGC9uP\":[\"owner/repo\"],\"oKOOsY\":[\"颜色主题\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有合集已在你的导航中。\"],\"oUn9Z7\":[\"近中性的浅灰,配钢蓝点缀。\"],\"ofdy2l\":[\"带橘调的底色。最暖的一套。\"],\"olouYg\":[\"导航将该地址作为链接保存。\"],\"oxj6Pk\":[\"其他语言\"],\"pIpTqF\":[\"每种语言都有独立的首页、归档和订阅源。启用后,发布时可以选择不同的语言,也可以为已有的帖子关联一篇其他语言的帖子。\"],\"pNXxtS\":[\"选择向读者和搜索引擎声明的内容语言。\"],\"pZq3aX\":[\"上传失败。请重试。\"],\"pgJImo\":[\"把主语言改为其他语言?\"],\"pgTIrt\":[\"选择要与此站点同步的 GitHub 账户和仓库。\"],\"psoxDF\":[\"该字体主题不可用。请选择另一个。\"],\"pt4OhQ\":[\"/about 已被占用。重命名该项目后再创建 About 页面。\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"合集\"],\"q/T5GS\":[\"私有 Jant 仪表板使用的语言。\"],\"qSgO/Y\":[\"跟随内容语言\"],\"qZ9jzv\":[\"站点可见性\"],\"qdcESc\":[\"创建新仓库\"],\"qkSJzV\":[\"正在搜索页面…\"],\"r5EW6f\":[\"此仓库已在为另一个 Jant 站点 (\",[\"host\"],\") 进行备份。请选择其他仓库。\"],\"rCPrbS\":[[\"language\"],\"的操作\"],\"rEspiY\":[\"导航位置已更新。\"],\"rFmBG3\":[\"配色主题\"],\"re0mF0\":[\"页面是公开的,但不会出现在 Latest。\"],\"rlonmB\":[\"删除失败。请稍后再试。\"],\"s3jOk7\":[\"请输入页面标题。\"],\"sROtqR\":[\"无法连接目录:\",[\"reason\"]],\"sRmGLp\":[\"之后可以在设置中更改。\"],\"satWc6\":[\"主 RSS 源\"],\"sgr2wQ\":[\"合集\"],\"sj4LLc\":[\"仅显示已修改项\"],\"sjej3N\":[\"移除「\",[\"language\"],\"」\"],\"slujBW\":[\"只能使用小写字母、数字和连字符。\"],\"soRdOu\":[\"米白纸底,配深森绿。\"],\"sqxcaY\":[\"创建于 \",[\"date\"]],\"sxkWRg\":[\"高级\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tJ4H0O\":[\"你的 Telegram 账号\"],\"tfDRzk\":[\"保存\"],\"tgWuMB\":[\"已修改\"],\"tvgAq5\":[\"尚未授权任何账户\"],\"u1VTd3\":[\"调色板、表面色调与整体氛围\"],\"u3wRF+\":[\"已发布\"],\"u6KOjV\":[\"想要更多控制?\"],\"uKLe0J\":[[\"count\"],\" 个设置已显示\"],\"uVZcIA\":[\"此地址已被使用。请选择其他地址。\"],\"udPwLB\":[\"导航栏\"],\"ugHDtd\":[[\"about\"],\"帖子设为 Featured \",[\"hours\"],\" 小时后会出现在 Discover 首页,Link 和 Quote 类型的帖子发布 \",[\"hours\"],\" 小时后出现在 \",[\"links\"],\" 和 \",[\"quotes\"],\" 列表。这段时间里你可以继续修改,详见 \",[\"rules\"],\"。\"],\"ui6aMF\":[\"以下设备当前已登录到你的账户。撤销任何你不认识的会话。\"],\"vBEKwo\":[\"在此管理本站点的活动会话。密码和托管访问通过 \",[\"providerLabel\"],\" 管理。\"],\"vOnuOa\":[\"在导航栏和更多菜单中显示或隐藏内置目标。\"],\"vRldcl\":[\"排版选项与阅读质感\"],\"vSYKYI\":[\"主订阅源\"],\"vTuib7\":[\"这将控制 /feed 返回的内容。\"],\"vXCC6J\":[\"有内容填写不正确,检查后重试。\"],\"vXIe7J\":[\"语言\"],\"vXsx3x\":[\"该页面是私有的,因此任何人都无法通过菜单打开它。\"],\"vdFnYM\":[\"重置链接\"],\"vmQmHx\":[\"添加自定义 CSS 以覆盖任何样式。使用数据属性如 [data-page], [data-post], [data-format] 来定位特定元素。\"],\"vpSPA1\":[\"缺少认证密钥,检查环境变量配置。\"],\"vzX5FB\":[\"删除账号\"],\"w615zC\":[\"多语言\"],\"w8Rv8T\":[\"标签为必填项\"],\"wL3cK8\":[\"Latest\"],\"wW6NCp\":[\"上次错误\"],\"wc+17X\":[\"/* 在此填写你的自定义 CSS */\"],\"wuLtXn\":[\"当前没有活动会话。 已登录的设备会显示在此处。\"],\"x+HGBk\":[\"请求搜索引擎不要对公开页面建立索引或将其包含在搜索结果中。\"],\"xCWek4\":[\"文件存储尚未设置。请检查服务器配置。\"],\"xCbjQn\":[\"允许我的博客出现在 \",[\"name\"],\" 中\"],\"xGVfLh\":[\"继续\"],\"xHt036\":[\"个人访问令牌\"],\"xKeZ0l\":[\"更偏暖、偏土的色调。\"],\"xYbozN\":[\"柔象牙白\"],\"xqViCq\":[\"暖象牙白\"],\"xxaahd\":[\"想要更冷、更利落的观感时。\"],\"y/awtV\":[\"导航链接、内置目标和更多菜单\"],\"y28hnO\":[\"帖子\"],\"y9W9vo\":[\"正在创建合集…\"],\"yNCqOt\":[\"Latest 订阅源\"],\"yQ3kNF\":[\"输入以下短语以确认:\"],\"ydq1k2\":[\"请先选择一个账号\"],\"yjjCV8\":[\"固定订阅源 URL\"],\"yjkELF\":[\"确认新密码\"],\"ym+ccl\":[\"在主页显示 Jant 署名。\"],\"yzF66j\":[\"链接\"],\"z6wakA\":[\"在你的主页上显示的简短介绍。\"],\"zEizrk\":[\"上次使用 \",[\"date\"]],\"zPqly9\":[\"这个站点没有可提交的目录。\"],\"zSURJW\":[\"没有匹配的仓库。\"],\"zUlHGd\":[\"页面地址\"],\"zYRGIR\":[\"尚未提交。还没有目录知道这个站点存在。\"],\"zcDmsG\":[\"Featured 帖子\"],\"zlcDd2\":[\"要删除此自定义 URL 吗?使用它的访问者将不再被重定向。\"],\"ztPYIR\":[\"启用时会一次性标记已有帖子\"],\"zwBp5t\":[\"私有\"],\"zxexio\":[\"内容语言、界面语言、多语言\"]}");
2016
2016
  //#endregion
2017
2017
  //#region src/i18n/locales/settings/zh-Hant.ts
2018
- var messages = JSON.parse("{\"+4Z6iP\":[\"先在 GitHub 上建立儲存庫 — 可以是空的。\"],\"+9JI/F\":[\"連線後會將你的網站同步到 \",[\"repo\"],\" 的預設分支,並疊加在其現有歷史之上。Jant 管理路徑外的既有檔案會保留。此操作無法復原。\"],\"+AXdXp\":[\"標籤與 URL 為必填\"],\"+K0AvT\":[\"解除連線\"],\"+V/jXc\":[\"直接指向 /feed,即 Atom 原始檔案,目前返回的是「\",[\"feed\"],\"」。適合已經在用閱讀器的讀者。\"],\"+VEqyj\":[\"設為主語言\"],\"+jXHMG\":[[\"count\",\"plural\",{\"other\":[\"還有 \",\"#\",\" 篇\",[\"language\"],\"貼文。先修改它們的語言,或保留該語言。\"]}]],\"+wgt7C\":[\"低飽和的紅棕底,溫暖、有泥土感。\"],\"+zy2Nq\":[\"類型\"],\"/0D1Xp\":[\"編輯選集\"],\"/3H2/s\":[\"此託管網站透過 \",[\"providerLabel\"],\" 登入。請在該處管理密碼與託管存取權限。\"],\"/PXXBT\":[\"近乎純白,帶一點暖意。\"],\"/PoNoq\":[\"編輯連結\"],\"/zOUxl\":[\"連結至 Telegram 機器人的 QR 碼\"],\"05DXsb\":[\"連接或管理用於從 Telegram 發文的 Bot。\"],\"0OGSSc\":[\"頭像顯示已更新。\"],\"0UzCUX\":[\"更新你用來登入的密碼\"],\"0VVvqu\":[\"多語言已啟用。\"],\"0bdA9b\":[\"啟用 Telegram 以連線\"],\"0dieZ/\":[\"/archive 開啟時用哪種版式。讀者可以在頁面上自行切換。\"],\"0gECZD\":[\"想寫更完整的介紹?\"],\"0uIjy/\":[\"淺鼠尾草綠底,配同色系的綠。\"],\"1DahsC\":[\"搜尋設定\"],\"1F6Mzc\":[\"目前尚無導覽項目。請新增連結或在下方啟用系統項目。\"],\"1H7gng\":[\"後台語言\"],\"1MYU3o\":[\"無法建立頁面。請檢查資料後再試一次。\"],\"1mbBbL\":[\"手動連接\"],\"1njn7W\":[\"淺色\"],\"1qGdnL\":[\"預設主題。沉靜,適合長時間閱讀。\"],\"1wdDm9\":[\"新增語言\"],\"21mg6u\":[\"選擇一則尚未加入導覽的有標題筆記,或建立新頁面。\"],\"2C7mSG\":[\"選集連結\"],\"2DoBvq\":[\"訂閱來源\"],\"2Et8jU\":[\"設定每頁搜尋結果數;重設後繼承預設分頁大小。\"],\"2FYpfJ\":[\"更多\"],\"2Ithfh\":[\"傳送任何文字給機器人,該文字會作為筆記已發佈。\"],\"2PTjMB\":[\"我想刪除 \",[\"siteName\"]],\"2QRni+\":[\"時區已更新。\"],\"2UrpGC\":[\"訂閱源網址已送達。目錄會在 \",[\"hours\"],\" 小時內首次讀取。\"],\"2ZqpRB\":[\"關閉\"],\"2cFU6q\":[\"網站頁尾\"],\"2fPEPI\":[\"沉靜、偏冷的底色。\"],\"2lkk2l\":[\"微黃的紙底,配橄欖綠。\"],\"2oWZo7\":[\"最近一次提交\"],\"2r8pkt\":[\"無法載入頁面。請稍後再試。\"],\"2uuy4H\":[\"已透過個人存取權杖連線\"],\"2wK4BX\":[\"編輯 About 頁面\"],\"35x8eZ\":[\"顯示 \",[\"shown\"],\" 共 \",[\"total\"]],\"39QGku\":[\"啟用機器人並傳送綁定碼,之後你傳給它的任何訊息都會成為筆記。\"],\"3B0RD1\":[\"上傳個人資料圖片與產生的網站圖示。\"],\"3Cw1AI\":[\"新增選集\"],\"3VrybB\":[\"重新導向\"],\"3Yvsaz\":[\"302 (暫時)\"],\"3n0zbB\":[\"在示範模式中已停用工作階段管理。請改用共用示範工作階段。\"],\"3sYJi5\":[\"下載與 Hugo 相容的封存 — 以靜態方式託管或移轉到另一個 Jant\"],\"3vMdv3\":[\"此連結為保留連結。請選擇其他連結。\"],\"3wKq0C\":[\"無法儲存。請稍後再試。\"],\"49Bsal\":[\"Feed 設定已更新。\"],\"4D09NB\":[\"連結到選集頁。\"],\"4J/OYU\":[\"選集已建立。\"],\"4Jge8E\":[\"目前的工作階段\"],\"4KIa+q\":[\"已下載匯出檔案。\"],\"4Q0iPK\":[\"跟隨裝置外觀或強制使用淺色或深色主題。\"],\"4cEClj\":[\"工作階段\"],\"4n7WY4\":[\"站名、可見性、訂閱來源與時區\"],\"4tcgrg\":[\"查看這些貼文\"],\"4yyXWu\":[\"淺骨白底,配低飽和的綠。\"],\"4zGJ5E\":[\"永久刪除帳戶\"],\"5QlUIt\":[\"倉庫為空。準備連線。\"],\"5VQnR3\":[\"當你想要一個永遠不會改變的 feed URL 時,請使用這些。\"],\"5dpcN1\":[\"輸入以搜尋全部\"],\"5f1Wo9\":[\"已以 \",[\"account\"],\" 連線\"],\"5o8R81\":[\"極淺的象牙底,配淡淡的茶綠。\"],\"6ArdBh\":[\"將 Featured 貼文用於 /feed。\"],\"6DjeBT\":[\"示範網站會始終對搜尋引擎保持隱藏。\"],\"6E3aK4\":[\"管理託管\"],\"6FFB7q\":[\"使用最新的公開貼文作為 /feed。\"],\"6K1Vef\":[\"確定要永久刪除此部落格嗎?此動作無法復原。\"],\"6NpNLc\":[\"此儲存庫已有內容。\"],\"6Pa5AP\":[\"連接或管理 GitHub 內容自動備份。\"],\"6V3Ea3\":[\"已複製\"],\"6WdDG7\":[\"頁面\"],\"71WIgc\":[\"取得新代碼\"],\"71of4k\":[\"限制每個 RSS feed 中包含的貼文數。\"],\"746NHh\":[\"此部落格\"],\"7811AW\":[\"此儲存庫已在備份本網站。\"],\"7FCZPo\":[\"內容語言\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允許搜尋引擎索引我的網站\"],\"7GISOt\":[\"儲存機器人權杖\"],\"7MZxzw\":[\"密碼已變更。\"],\"7p5kLi\":[\"後台\"],\"7vhWI8\":[\"新密碼\"],\"81nFIS\":[\"密碼不符。請確認兩個欄位相同。\"],\"87a/t/\":[\"標籤\"],\"89Upyo\":[\"該主題目前不可用。請選擇其他主題。\"],\"8BfEpW\":[\"託管帳號\"],\"8NIu3g\":[\"該頁面尚未有標題,因此選單沒有可顯示的項目。\"],\"8T46pB\":[\"機器人權杖\"],\"8U2Z7f\":[\"新增自訂 URL\"],\"8ZsakT\":[\"密碼\"],\"8bpHix\":[\"帳戶建立失敗,檢查填寫內容後重試。\"],\"8gEghq\":[\"明亮的暖白底,配苔綠點綴。\"],\"9+vGLh\":[\"自訂 CSS\"],\"9As8Nu\":[\"在 GitHub 上建立一個\"],\"9EjI4j\":[\"暖沙色\"],\"9Fa2Ns\":[\"根位址將恢復顯示所有語言,\",[\"prefix\"],\" 這類位址會重新導向到它,既有的連結和訂閱仍然有效。貼文網址和每篇貼文的語言都不受影響,隨時可以重新啟用。\"],\"9FctRm\":[\"使用小寫字母、數字和連字號。\"],\"9J6wUO\":[\"建議新增的連結\"],\"9Lsvt5\":[\"已於 \",[\"date\"],\" 登入\"],\"9SHZas\":[\"登入後顯示「設定」,未登入時顯示「登入」。\"],\"9T7Cwm\":[\"重新導向、自訂路徑與網址控制\"],\"9aUyym\":[\"查看你在哪裡已登入,並撤銷舊的工作階段\"],\"9mqQnx\":[\"限制自動產生摘要所使用的段落數。\"],\"A7tRC9\":[\"正規的 /feed URL 會回傳哪個貼文串流。\"],\"ADTn/D\":[\"允許不使用 API 權杖讀取已發佈內容。\"],\"AELGTd\":[\"立即加入導覽,或啟用編輯器補充詳細資料。\"],\"ANzR96\":[\"設定編輯器\"],\"AQL8b1\":[\"你寫作的主要語言是?\"],\"AeXO77\":[\"帳戶\"],\"AnY+O9\":[\"在首頁底部顯示「Build with Jant」\"],\"ApZDMk\":[\"此圖會用於你的 favicon 和 apple-touch-icon。為達最佳效果,請上傳至少 512x512 像素、背景為純色的正方形 PNG。\"],\"Aysjjh\":[\"選擇 Jant 全域使用的配色方案。\"],\"B495Gs\":[\"封存\"],\"B4ESok\":[\"API 參考\"],\"B5P9HE\":[\"提交我的網站\"],\"BJ0tsF\":[\"搜尋執行階段設定並啟用進階設定\"],\"BTYdze\":[\"連結已新增到導覽。\"],\"BszQc4\":[\"根位址(/、/feed)顯示這個語言的內容。\"],\"BzEFor\":[\"或\"],\"C0/57J\":[\"這是選集連結的最後一部分。\"],\"CDAdlf\":[\"移除機器人\"],\"CTAEes\":[\"選擇儲存庫\"],\"CjZZgz\":[\"此儲存庫已有提交紀錄\"],\"Cl92kR\":[\"建立新選集\"],\"D35jh6\":[\"管理後台的顯示語言,只有你看得到。\"],\"D8k2s6\":[\"連接 Telegram\"],\"DCKkhU\":[\"目前密碼\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密碼與託管存取\"],\"DVFXa6\":[\"最乾淨、最中性的一檔。\"],\"DdXUay\":[\"在公開標頭顯示網站頭像。\"],\"E3hjKv\":[\"新增為連結\"],\"EKvXGx\":[\"該位址位於其他網站。Navigation 將其視為連結。\"],\"EO3I6h\":[\"上傳未成功。請稍後再試。\"],\"Eax/et\":[\"強調色\"],\"EbDLD+\":[\"選擇 Jant 全域使用的字體。\"],\"Enslfm\":[\"目標網址\"],\"F53UDu\":[\"冷白\"],\"F7FKwe\":[\"你在此處貼上的任何內容都能完全存取訪客的瀏覽器。僅使用來自你信任來源的程式碼。\"],\"F8SNqr\":[\"暖橘\"],\"FBgjRN\":[\"搜尋頁面,或貼上位址\"],\"FMUJSP\":[\"帳戶與資料\"],\"FUfPPw\":[\"根位址(/、/feed、/archive)將改為提供「\",[\"next\"],\"」的內容,「\",[\"previous\"],\"」移至 \",[\"prefix\"],\"。貼文網址不會改變,但訂閱 /feed 的讀者從此收到的是「\",[\"next\"],\"」的貼文。\"],\"Fe1cvJ\":[\"延遲新貼文和回覆進入訂閱源的時間。設為 0 可關閉延遲。\"],\"Fk3SSD\":[\"平靜自然,看著舒服。\"],\"FkMol5\":[\"Featured\"],\"FwL2Zk\":[[\"count\",\"plural\",{\"other\":[\"還有 \",\"#\",\" 篇\",[\"language\"],\"貼文,但列表裡沒有這個語言。把它加回列表,或先修改這些貼文的語言。\"]}]],\"G/1oP+\":[\"移除 webhook 並停止同步。你的儲存庫內容不會被刪除。\"],\"G0qJsQ\":[\"找不到安全權杖。請重新整理頁面後再試一次。\"],\"G0tHaW\":[\"建立一個不會出現在 Latest 的公開頁面。\"],\"G2fuEb\":[\"已鎖定\"],\"G39wnK\":[\"將內容備份並與 GitHub 儲存庫同步\"],\"G5oCui\":[\"URL 結構、每種語言的訂閱來源,以及譯本關聯。\"],\"G9peMt\":[\"建立帳戶\"],\"GAmD3h\":[\"語言\"],\"GBkMNw\":[[\"name\"],\" 是 Jant 社群人工維護的目錄,旨在幫助使用者發現新的 Jant 部落格和內容。\"],\"GXsAby\":[\"撤銷\"],\"GdGiNi\":[\"第 \",[\"current\"],\" 步,共 \",[\"total\"],\" 步\"],\"GorKul\":[\"歡迎使用 Jant\"],\"GxkJXS\":[\"上傳中...\"],\"GzKzUa\":[\"試用限制\"],\"HCNlq3\":[\"乾淨、高對比,接近純白。\"],\"HHDyGw\":[\"柔綠\"],\"HKH+W+\":[\"資料\"],\"Hf/w/z\":[\"示範網站不會出現在 Discover 中。\"],\"Hp1l6f\":[\"目前\"],\"HxTUmw\":[\"新增 \",[\"language\"]],\"HxlY7t\":[\"變更此設定會更新訂閱者從 /feed 取得的內容。\"],\"HxuOlm\":[\"網站頁首\"],\"I0ijRI\":[\"全部 feed 網址\"],\"I4HRxU\":[\"已發布的全部貼文,包括從 Latest 中隱藏的那些。\"],\"I6gXOa\":[\"路徑\"],\"I76CzF\":[\"中性灰\"],\"ID38tA\":[\"示範模式下帳號刪除已停用。共用示範會另行重置。\"],\"IF9tPu\":[\"何時使用網站匯出、資料庫備份與復原演練。\"],\"IW5PBo\":[\"複製權杖\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"儲存庫\"],\"J36Xsm\":[\"在 \",[\"address\"],\" 找不到任何頁面。請檢查位址,或以標題搜尋。\"],\"J6bLeg\":[\"新增自訂連結到任何 URL\"],\"JL7LF5\":[\"可用的 CSS 變數、data 屬性與範例。\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"請立即複製你的權杖 — 它不會再顯示。\"],\"JrFTcr\":[\"連線中…\"],\"JuN5GC\":[\"未選取檔案。請選擇要上傳的檔案。\"],\"K+0Hu0\":[\"找不到可加入的相符頁面。換個標題搜尋,或建立新頁面。\"],\"K/F6pa\":[\"儲存中…\"],\"KDw4GX\":[\"重試\"],\"KSgo21\":[\"選擇一個儲存庫\"],\"KVVYBh\":[\"新增選集到導覽\"],\"KiJn9B\":[\"筆記\"],\"Kk7jwL\":[\"設定每頁封存貼文數;重設後繼承預設分頁大小。\"],\"KwOLJF\":[\"Jant 最初的配色,日常穩妥之選。\"],\"L+rMC9\":[\"還原為預設值\"],\"L27RpE\":[\"頁面已建立。\"],\"L3DEwT\":[\"移除這個頭像?你的 favicon 與頁首圖示會回復為預設。\"],\"L4t4/q\":[\"3月14日\"],\"L86zmP\":[\"在完整表單中編輯首頁和中繼資料使用的多行簡介。\"],\"LdyooL\":[\"連結\"],\"LhMjLm\":[\"時間\"],\"LjwaJ/\":[\"登入、匯出和刪除\"],\"M/D8PK\":[\"+ 安裝到其他帳戶\"],\"M/haSd\":[\"永遠顯示主題的淺色版本。\"],\"M/kDIW\":[\"搜尋引擎看到的語言。\"],\"M1co/O\":[\"已設定\"],\"M2hGyf\":[\"指向 /subscribe,一個列出你所有 feed 網址、帶複製按鈕的頁面。適合還沒在用閱讀器的讀者。\"],\"M2kIWU\":[\"字型主題\"],\"M4tzVU\":[\"Latest 貼文\"],\"M6CbAU\":[\"切換編輯面板\"],\"M7wPsD\":[\"切換\"],\"MHrjPM\":[\"標題\"],\"MKIM3K\":[\"搜尋頁面\"],\"MaYYE6\":[\"透過向 Telegram 機器人傳送訊息來發佈筆記\"],\"Me5t5H\":[\"連接 GitHub 倉庫,自動把貼文備份成 Markdown。GitHub 上的修改會同步回網站。\"],\"MeCJlL\":[\"正在提交網站,重新整理後可以看到結果。\"],\"MnbH31\":[\"頁面\"],\"Mq9FZ1\":[\"正在建立頁面…\"],\"Mr4QPw\":[\"要斷開 Telegram?你可以隨時使用新的綁定代碼重新連接。\"],\"MtENL9\":[\"調整你的網站外觀、可讀性與執行效能。\"],\"N/8NPV\":[\"在刪除前,請先下載網站匯出檔案。刪除後無法恢復此帳號。\"],\"N7UNHY\":[\"Featured RSS 來源\"],\"NHnUHF\":[\"頁首上的網站圖示與個人標記\"],\"NU2Fqi\":[\"儲存 CSS\"],\"NVjhde\":[\"暖羊皮紙\"],\"Nldjdr\":[\"還沒有自訂 URL。建立一個,給貼文加重新導向或自訂路徑。\"],\"O3oNi5\":[\"電子郵件\"],\"OJxdgi\":[\"連結不能超過 200 個字元。\"],\"OSJXFg\":[\"套用於整個網站,包括管理頁面。選擇一個調色盤,然後決定它是跟隨系統還是維持固定。\"],\"OeUWA7\":[\"加入頁面\"],\"OuuMXJ\":[\"在 </body> 結束標籤之前新增全站 HTML。\"],\"Ox3+3h\":[\"無相符結果。\"],\"P7Eeay\":[\"完整貼文\"],\"PEUV5I\":[\"程式碼注入已更新。\"],\"PHh52z\":[\"暖而厚重,棕調明顯。\"],\"PXj9lw\":[\"停止接受來自 Telegram 的貼文。你現有的筆記會保持已發佈。\"],\"PZ7HJ8\":[\"部落格大頭貼\"],\"Pbm2/N\":[\"建立選集\"],\"Pwqkdw\":[\"載入中…\"],\"PxJ9W6\":[\"產生權杖\"],\"Q/6Y+2\":[\"需要在目標儲存庫上擁有 Contents(讀/寫)和 Webhooks(讀/寫)權限。\"],\"Q/O0X4\":[\"此設定未儲存。請檢查設定值並再試一次。\"],\"Q30z/l\":[\"要從導覽移除這個選集嗎?選集本身不會被刪除。\"],\"Q99OtV\":[\"將選集釘選到導覽列。最近 48 小時內更新的選集旁會顯示一個 * 號。\"],\"QCwsv1\":[\"暖白\"],\"QKvrmL\":[\"暖中性,適合長讀。\"],\"QZmz0H\":[\"內建連結\"],\"Qnrzvb\":[\"已啟用的權杖\"],\"R6Z4LE\":[\"下載失敗。請再試一次。\"],\"R9Khdg\":[\"自動\"],\"RDjuBN\":[\"初始設定\"],\"RRo9kN\":[\"語言已更新。\"],\"RcdDOS\":[\"在 Telegram 上向 @BotFather 發送訊息以建立機器人,然後貼上它提供給你的權杖\"],\"RdVIcf\":[\"奶油配深咖\"],\"Ri/9PY\":[\"目前仍只向 Discover 提供精選貼文,但沒有任何貼文標為 Featured,訂閱源裡沒有可顯示的內容。取消勾選再重新勾選,即可讓 Discover 讀取全部公開貼文。\"],\"Rn2p1h\":[\"沒有任何項目符合此搜尋。請嘗試不同的名稱或描述。\"],\"RxsRD6\":[\"時區\"],\"SDND4q\":[\"未設定\"],\"SJmfuf\":[\"網站名稱\"],\"SKZhW9\":[\"權杖名稱 (API 權杖名稱欄位。)\"],\"SSsoa4\":[\"加入導覽\"],\"SVQQPe\":[\"無法連線。請檢查錯誤並再試一次。\"],\"SWb0z+\":[\"此網址為保留網址。請選擇其他網址。\"],\"SYGk01\":[\"以網址前綴提供的語言。在「語言」頁管理。\"],\"SchpMp\":[\"Telegram\"],\"SeY4vo\":[\"移除 \",[\"language\"],\" 後就只剩主語言了,多語言會一併關閉。根位址將恢復顯示所有語言,\",[\"prefix\"],\" 不再可用,每篇貼文的語言維持不變。\"],\"SqKp3o\":[\"此標題會用於整個網站、瀏覽器分頁與訂閱來源。\"],\"SrGs4T\":[\"淺藍灰底,冷靜而均勻。\"],\"T/R+Qz\":[\"主語言\"],\"T17dXm\":[\"每種語言是否擁有獨立的首頁、彙整和訂閱來源。在「語言」頁管理。\"],\"T41PG1\":[\"限制自動產生摘要所使用的字元數。\"],\"TN0mN4\":[\"搜尋和編輯執行階段設定。變更會立即生效;重設會恢復環境變數或內建預設值。\"],\"TSCeuF\":[\"無法建立選集。請檢查資料後再試一次。\"],\"TpF3v+\":[\"在 </head> 之前注入。用於分析、自訂 meta 標籤,以及必須提前載入的樣式。\"],\"Tu6bMZ\":[\"建立 About 頁面\"],\"Tz0i8g\":[\"設定\"],\"U5J7jI\":[\"網站可見性已更新。\"],\"U5v6Gh\":[\"編輯頁面\"],\"UFK415\":[\"用於分析與小工具的網站全域 HTML\"],\"UTvFQq\":[\"啟用 \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" 並傳送:\"],\"UUn+Y5\":[\"啟用設定\"],\"UaZwcz\":[\"建立後可以設定更多選項。\"],\"Uj/btJ\":[\"在我的網站頁首顯示大頭貼\"],\"UsODUn\":[\"選擇一個帳戶\"],\"UxKoFf\":[\"導覽列\"],\"UyMSeQ\":[\"在該位址\"],\"V+bhUy\":[\"安裝 GitHub App\"],\"V0fyg5\":[\"選集已加入導覽。\"],\"V4WsyL\":[\"新增連結\"],\"V5pZwT\":[\"搜尋設定已更新。\"],\"V7dQi9\":[\"語言已移除。\"],\"VXUPla\":[\"使用 GitHub App 連線\"],\"Vh+JIV\":[\"多語言指南\"],\"VhMDMg\":[\"變更密碼\"],\"Vn3jYy\":[\"導覽項目\"],\"VoZYGU\":[\"這會永久刪除你的全部資料——貼文、媒體、選集、設定和帳戶。部落格會重設為初始設定狀態。此操作無法復原。\"],\"VoarBQ\":[\"所有貼文的網址都不會改變,也可以隨時再關閉。\"],\"VqyJ7Y\":[\"主要語言已變更。\"],\"WL7KjI\":[\"卡片網格\"],\"WUnFK2\":[\"偏冷的淺白底,配深靛藍文字。高對比。\"],\"Wa9q4P\":[\"純白\"],\"Wb7EHo\":[\"About 頁面\"],\"Weq9zb\":[\"一般\"],\"Wi9i06\":[\"依照每位訪客的系統偏好。\"],\"Wildi8\":[\"沒有可加入的頁面。建立頁面後即可加入導覽。\"],\"Wx1M8N\":[\"安裝 GitHub App,以授予存取權而無需管理個人權杖。權限以每個儲存庫為範圍,並可在 GitHub 上撤銷。\"],\"X+8FMk\":[\"目前的密碼不符。請再試一次。\"],\"X1G9eY\":[\"導覽列預覽\"],\"X2Cbc2\":[\"封存 RSS 來源\"],\"X5P6yv\":[\"最近更新\"],\"X9Hujr\":[\"手動推送\"],\"XQQOyj\":[\"該頁面為草稿。請先發佈,然後再加入。\"],\"XeRkls\":[[\"count\",\"plural\",{\"other\":[\"其中 \",\"#\",\" 篇還沒有語言,會標記為「\",[\"language\"],\"」。\"]}]],\"Xsu5WL\":[\"在專用程式碼編輯器中新增全站 CSS。\"],\"XtBJV8\":[\"正在檢查儲存庫…\"],\"Xtc16w\":[\"重新整理儲存庫清單\"],\"Y+7JGK\":[\"建立頁面\"],\"Y/F35r\":[\"使用 curl 建立貼文:\"],\"Y/N5N7\":[\"柔和的奶油底,配低飽和的綠。\"],\"YF6zHf\":[\"網站設定已更新。\"],\"YVcVlW\":[\"移除並關閉\"],\"Ya/FAk\":[\"至少再新增一種語言才能啟用。\"],\"YdG2RF\":[\"匯出網站\"],\"YkgZi7\":[\"連接一個 Telegram 機器人,之後你傳給它的任何訊息都會以筆記形式已發佈。\"],\"YwhjRx\":[\"管理帳戶\"],\"Yxp859\":[\"暖奶油色\"],\"Z5HWHd\":[\"已啟用\"],\"ZDY7Fy\":[\"同步中…\"],\"ZQKLI1\":[\"危險區域\"],\"ZS/CBL\":[\"刪除此導覽連結?訪客將不再在你的網站頁首看到它。\"],\"ZXZrAo\":[\"頁面網址不能超過 200 個字元。\"],\"ZgZX+d\":[\"純白底配中性灰。不帶任何色偏。\"],\"Zgq+c2\":[\"設定預設每頁顯示的項目數。\"],\"ZhhOwV\":[\"引用\"],\"ZiooJI\":[\"API 權杖\"],\"Zm7Qb0\":[\"備份與還原指南\"],\"ZmUkwN\":[\"新增自訂連結到導覽\"],\"a14mj8\":[\"未知裝置\"],\"a1iEgy\":[\"暖奶油底,配深咖啡棕點綴。\"],\"a3LDKx\":[\"安全性\"],\"aAIQg2\":[\"外觀\"],\"aFkzVF\":[\"目標貼文或選集的 slug\"],\"aR3U86\":[\"多語言已啟用。\",[\"count\",\"plural\",{\"other\":[\"#\",\" 篇貼文\"]}],\"已標記為「\",[\"language\"],\"」。\"],\"aV6XWN\":[\"多語言已關閉。你的語言設定仍然保留。\"],\"alKG0+\":[\"字型主題\"],\"anibOb\":[\"關於本部落格\"],\"any7NR\":[\"主題指南\"],\"asq0vL\":[\"你寫作使用的語言。\"],\"b+/jO6\":[\"301 (永久)\"],\"b+FyBD\":[\"將頁面加入導覽\"],\"b1FYSK\":[\"主要語言\"],\"bHOiy1\":[\"示範模式已停用變更密碼功能。請使用共用示範帳號登入。\"],\"bHYIks\":[\"登出\"],\"bV2vng\":[\"暖陶土色\"],\"bbR5vW\":[\"這套配色\"],\"bbzY7X\":[\"管理登入安全、站點匯出和無法復原的操作。\"],\"bfHZ7r\":[\"選擇用於顯示日期和時間的時區。\"],\"bi10qS\":[\"立即加入導覽,或啟用編輯器補充內容。\"],\"bkMuwo\":[\"連結到你標為 Featured 的貼文。\"],\"bmrL08\":[\"示範模式會隱藏會話、密碼更改與帳號刪除。匯出功能仍可使用。\"],\"bph1Dd\":[\"已關閉搜尋引擎索引,所以預設不會出現在 Jant Discover。勾選上面的選項仍會加入。\"],\"brfpw9\":[\"在完整表單中編輯顯示在公開頁面底部的多行頁尾。\"],\"bviiKV\":[\"這個主題下的標題、正文和連結。\"],\"c1BGrV\":[\"已在導覽中。請在上方清單中拖曳以移動\"],\"c1iSrq\":[\"Discover 社群規則\"],\"c3MN2z\":[\"所有可用的端點與請求格式。\"],\"cHh/Zu\":[\"啟用多語言\"],\"cS7/bk\":[\"移除已儲存的機器人權杖?其 webhook 會被刪除,任何已連接的帳號將會被斷線。\"],\"cSDy01\":[\"自訂 CSS 已更新。\"],\"clzoNp\":[\"始終顯示深色主題。\"],\"cnGeoo\":[\"刪除\"],\"cwaLCZ\":[\"頁面已加入導覽。\"],\"d1lzY/\":[\"語言已新增。\"],\"d3FRkY\":[\"無法複製。請再試一次。\"],\"d5oGUo\":[\"在 GitHub 建立新儲存庫\"],\"dB1Nr4\":[\"開始寫作\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"確認刪除帳號\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"永久刪除所有資料並重設部落格\"],\"ds0nJk\":[\"在 head 標籤的閉合標記之前加入全站 HTML。\"],\"dsWkIw\":[\"要與 GitHub 斷開連線嗎?webhook 會被移除。你的儲存庫內容不會被刪除。\"],\"dwcK1n\":[\"或手動提交網址\"],\"e/tSI5\":[\"導覽順序已更新。\"],\"eOL7vn\":[\"色彩極少,干擾最小。\"],\"ePK91l\":[\"編輯\"],\"ebQKK7\":[\"網站\"],\"eeamrQ\":[\"Discover 讀取的是 Atom 訂閱源,需要先開啟訂閱源。\"],\"egK+Yy\":[\"供腳本與自動化使用的 Bearer 權杖\"],\"egOXl0\":[\"Jant Discover\"],\"ehj/zN\":[\"重新導向類型\"],\"ej6Rji\":[\"若有別的語言的貼文,之後可以在那篇貼文的選單裡單獨更正。\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"已複製訂閱網址。\"],\"f8fH8W\":[\"設計\"],\"fDz6PV\":[\"選擇用於內容備份的儲存庫。\"],\"fKRAwQ\":[\"沙色底,配綠色點綴。\"],\"fWYqkz\":[\"程式碼注入\"],\"fYXQnC\":[\"暖色裡最濃的一檔,溫馨。\"],\"fttd2R\":[\"我的選集\"],\"gZ5owP\":[\"搜尋儲存庫\"],\"gbqbh6\":[\"可以放心離開此頁面 — 同步會在背景繼續進行。\"],\"gkFvVN\":[\"在 </body> 之前注入。用於聊天小工具和不應阻塞頁面載入的腳本。\"],\"gtQsRO\":[\"建立自訂網址\"],\"hBO/y4\":[\"安全權杖已過期。請重新整理頁面並再試一次。\"],\"hGmyDl\":[\"權杖讓你從腳本,捷徑和其他工具存取 API 無需登入\"],\"hIHkRy\":[\"已透過 GitHub 應用程式連線\"],\"hJHHsU\":[\"發佈網站、封存與選集的 Atom feeds。\"],\"hcH8MY\":[\"設定你的網站\"],\"hdSi1b\":[\"輸入 \",[\"repo\"],\" 以確認\"],\"he3ygx\":[\"複製\"],\"hjeS3W\":[\"連結到 Latest。首頁展示的就是這個列表。\"],\"hjvSlw\":[\"語言已移除,多語言已關閉。\"],\"hvGGMK\":[\"從導覽中移除此頁面?頁面本身不會被刪除。\"],\"i0qMbr\":[\"首頁\"],\"iEUzMn\":[\"系統\"],\"iH8pgl\":[\"返回\"],\"iMkWoF\":[\"啟用\"],\"iSLIjg\":[\"連接\"],\"iVOMRi\":[\"首頁設定已更新。\"],\"icB4Cv\":[\"將連結拖到此處以顯示於「更多」選單下方\"],\"id3vuh\":[\"已設定 Telegram,但無法連線到機器人。請檢查機器人權杖並再試一次。\"],\"idD8Ev\":[\"已儲存\"],\"ihn4zD\":[\"搜尋…\"],\"iiDXZc\":[\"顯示於所有貼文與頁面的底部。\"],\"itS31B\":[\"更暖一些,像略舊的紙。\"],\"iwQZvS\":[\"冷藍灰\"],\"j4VrG6\":[\"下載匯出 ZIP\"],\"j5nQL2\":[\"例如 iOS 捷徑\"],\"jNE7O+\":[\"還沒有公開貼文。jant.me 的收錄條件是\",[\"minCount\",\"plural\",{\"other\":[\"滿 \",\"#\",\" 篇\"]}],\"。\"],\"jUV7CU\":[\"上傳大頭貼\"],\"jVUmOK\":[\"支援 Markdown\"],\"jdVYS8\":[\"選一個適合你文字的。\"],\"jgBjXJ\":[\"要撤銷這個權杖嗎?任何使用它的腳本都會停止運作。\"],\"jpctdh\":[\"檢視\"],\"k1ifdL\":[\"處理中...\"],\"kLw03Y\":[\"亮紙白\"],\"kMXclu\":[\"下載網站匯出檔案\"],\"kNiQp6\":[\"已釘選\"],\"kQ3Otm\":[\"建立新頁面\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"細緻的樣式覆寫\"],\"ke1gWS\":[\"自訂 URL\"],\"kfcRb0\":[\"頭像\"],\"kp8wiR\":[\"正在檢查網址…\"],\"kxDZ2i\":[\"此程式碼會在你網站的每個頁面上執行。\"],\"kzvWob\":[\"連結到貼文歸檔。\"],\"lLW3vJ\":[\"目標 slug\"],\"lLsfOE\":[\"關閉多語言?\"],\"lV04bQ\":[\"溫暖、有泥土感,但不顯暗。\"],\"lYHJih\":[\"撤銷此工作階段?該裝置將需要重新登入。\"],\"m16xKo\":[\"新增\"],\"mLOk1i\":[\"立即將所有貼文推送到 GitHub,而不是等待下一次自動同步。\"],\"mSNmrX\":[\"列出貼文:\"],\"n8+EXe\":[\"新增網站中已經存在的常用入口。\"],\"nG2qTk\":[\"示例連結\"],\"nK07ni\":[\"為你的網站選擇一種排版風格。每個主題會同時改變字體配對與閱讀節奏。\"],\"nbfdhU\":[\"第三方整合\"],\"ntJYyh\":[\"在 \",[\"providerLabel\"],\" 管理網域、方案和計費\"],\"o/vNDE\":[\"讓你覆寫任何主題變數。\"],\"o5rj+L\":[\"目前還沒有選集。在這裡建立一個並加入導覽。\"],\"oGC9uP\":[\"owner/repo\"],\"oH2JHg\":[\"我們會預先填入名稱 \",[\"name\"],\"。返回時清單會重新整理。\"],\"oKOOsY\":[\"色彩主題\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有選集已經在你的導覽中。\"],\"oUn9Z7\":[\"近中性的淺灰,配鋼藍點綴。\"],\"ofdy2l\":[\"帶橘調的底色。最暖的一套。\"],\"olouYg\":[\"導覽會將該位址儲存為連結。\"],\"oxj6Pk\":[\"其他語言\"],\"pIpTqF\":[\"每種語言都有獨立的首頁、彙整和訂閱來源。啟用後,發佈時可以選擇不同的語言,也可以為既有的貼文關聯一篇其他語言的貼文。\"],\"pNXxtS\":[\"選擇要向讀者與搜尋引擎宣告的內容語言。\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pgJImo\":[\"把主要語言改為其他語言?\"],\"pgTIrt\":[\"選擇要與此網站同步的 GitHub 帳號和儲存庫。\"],\"psoxDF\":[\"該字型主題無法使用。請選擇其他主題。\"],\"pt4OhQ\":[\"/about 已被佔用。重新命名該項目後再建立 About 頁面。\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"選集\"],\"q/T5GS\":[\"私人 Jant 儀表板使用的語言。\"],\"qSgO/Y\":[\"跟隨內容語言\"],\"qZ9jzv\":[\"網站可見性\"],\"qdcESc\":[\"建立新儲存庫\"],\"qkSJzV\":[\"正在搜尋頁面…\"],\"r5EW6f\":[\"此儲存庫已在備份另一個 Jant 網站 (\",[\"host\"],\"). 請選擇其他儲存庫。\"],\"rCPrbS\":[[\"language\"],\"的操作\"],\"rEspiY\":[\"導覽位置已更新。\"],\"rFmBG3\":[\"色彩主題\"],\"re0mF0\":[\"頁面是公開的,但不會出現在 Latest。\"],\"rlonmB\":[\"無法刪除。請稍後再試。\"],\"s3jOk7\":[\"請輸入頁面標題。\"],\"sROtqR\":[\"無法連線目錄:\",[\"reason\"]],\"sRmGLp\":[\"之後可以在設定中更改。\"],\"satWc6\":[\"主要 RSS 訂閱\"],\"sgr2wQ\":[\"選集\"],\"sj4LLc\":[\"僅顯示已修改項目\"],\"sjej3N\":[\"移除「\",[\"language\"],\"」\"],\"slujBW\":[\"只能使用小寫字母、數字和連字號。\"],\"soRdOu\":[\"米白紙底,配深森綠。\"],\"sqxcaY\":[\"建立於 \",[\"date\"]],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tJ4H0O\":[\"你的 Telegram 帳號\"],\"tfDRzk\":[\"儲存\"],\"tgWuMB\":[\"已修改\"],\"tvgAq5\":[\"尚未授權任何帳戶\"],\"u1VTd3\":[\"調色盤、表面色調與整體氛圍\"],\"u3wRF+\":[\"已發佈\"],\"u6KOjV\":[\"想要更細緻的控制?\"],\"uKLe0J\":[[\"count\"],\" 個設定已顯示\"],\"uVZcIA\":[\"此網址已被使用。請選擇其他網址。\"],\"udPwLB\":[\"頁首\"],\"ugHDtd\":[[\"about\"],\"貼文設為 Featured \",[\"hours\"],\" 小時後會出現在 Discover 首頁,Link 和 Quote 類型的貼文發佈 \",[\"hours\"],\" 小時後出現在 \",[\"links\"],\" 和 \",[\"quotes\"],\" 列表。這段期間你可以繼續修改,詳見 \",[\"rules\"],\"。\"],\"ui6aMF\":[\"這些裝置目前已登入你的帳號。撤銷任何你不認識的工作階段。\"],\"vBEKwo\":[\"在此管理本網站的活動工作階段。密碼與託管存取由 \",[\"providerLabel\"],\" 管理。\"],\"vOnuOa\":[\"在頁首和更多選單中顯示或隱藏內建目的地。\"],\"vRldcl\":[\"字體選擇與閱讀質感\"],\"vSYKYI\":[\"主要訂閱來源\"],\"vTuib7\":[\"這會控制 /feed 回傳的內容。\"],\"vXCC6J\":[\"有欄位填寫不正確,檢查後重試。\"],\"vXIe7J\":[\"語言\"],\"vXsx3x\":[\"該頁面為私人頁面,因此沒有人能從選單開啟它。\"],\"vdFnYM\":[\"重設連結\"],\"vmQmHx\":[\"新增自訂 CSS 以覆寫任何樣式。使用像 [data-page]、[data-post]、[data-format] 這類資料屬性來選取特定元素。\"],\"vpSPA1\":[\"缺少驗證金鑰,檢查環境變數設定。\"],\"vzX5FB\":[\"刪除帳號\"],\"w615zC\":[\"多語言\"],\"w8Rv8T\":[\"標籤為必填\"],\"wL3cK8\":[\"Latest\"],\"wW6NCp\":[\"上次錯誤\"],\"wc+17X\":[\"/* 在此放入你的自訂 CSS */\"],\"wuLtXn\":[\"目前沒有任何活動中的工作階段。已登入的裝置會顯示在此處。\"],\"x+HGBk\":[\"要求搜尋引擎不要索引公開頁面或將它們包含在搜尋結果中。\"],\"xCWek4\":[\"檔案儲存尚未設定。請檢查你的伺服器設定。\"],\"xCbjQn\":[\"允許我的部落格出現在 \",[\"name\"],\" 中\"],\"xGVfLh\":[\"繼續\"],\"xHt036\":[\"個人存取權杖\"],\"xKeZ0l\":[\"更偏暖、偏土的色調。\"],\"xYbozN\":[\"柔象牙白\"],\"xqViCq\":[\"暖象牙白\"],\"xxaahd\":[\"想要更冷、更利落的觀感時。\"],\"y/awtV\":[\"頁首連結、內建目的地與更多選單\"],\"y28hnO\":[\"貼文\"],\"y9W9vo\":[\"正在建立選集…\"],\"yNCqOt\":[\"Latest RSS 來源\"],\"yQ3kNF\":[\"請輸入以下短語以確認:\"],\"ydq1k2\":[\"請先選擇一個帳戶\"],\"yjjCV8\":[\"固定的 RSS 檔案網址\"],\"yjkELF\":[\"確認新密碼\"],\"ym+ccl\":[\"在首頁顯示 Jant 的署名。\"],\"yzF66j\":[\"連結\"],\"z6wakA\":[\"顯示在你的主頁上的簡短介紹。\"],\"zEizrk\":[\"最後使用於 \",[\"date\"]],\"zPqly9\":[\"這個站點沒有可提交的目錄。\"],\"zSURJW\":[\"沒有符合的儲存庫。\"],\"zUlHGd\":[\"頁面網址\"],\"zYRGIR\":[\"尚未提交。還沒有目錄知道這個網站存在。\"],\"zcDmsG\":[\"Featured 貼文\"],\"zlcDd2\":[\"刪除此自訂 URL?使用該 URL 的訪客將不會再被重新導向。\"],\"ztPYIR\":[\"啟用時會一次性標記既有貼文\"],\"zwBp5t\":[\"私有\"],\"zxexio\":[\"內容語言、介面語言、多語言\"]}");
2018
+ var messages = JSON.parse("{\"+4Z6iP\":[\"先在 GitHub 上建立儲存庫 — 可以是空的。\"],\"+9JI/F\":[\"連線後會將你的網站同步到 \",[\"repo\"],\" 的預設分支,並疊加在其現有歷史之上。Jant 管理路徑外的既有檔案會保留。此操作無法復原。\"],\"+AXdXp\":[\"標籤與 URL 為必填\"],\"+K0AvT\":[\"解除連線\"],\"+V/jXc\":[\"直接指向 /feed,即 Atom 原始檔案,目前返回的是「\",[\"feed\"],\"」。適合已經在用閱讀器的讀者。\"],\"+VEqyj\":[\"設為主語言\"],\"+jXHMG\":[[\"count\",\"plural\",{\"other\":[\"還有 \",\"#\",\" 篇\",[\"language\"],\"貼文。先修改它們的語言,或保留該語言。\"]}]],\"+wgt7C\":[\"低飽和的紅棕底,溫暖、有泥土感。\"],\"+zy2Nq\":[\"類型\"],\"/0D1Xp\":[\"編輯選集\"],\"/3H2/s\":[\"此託管網站透過 \",[\"providerLabel\"],\" 登入。請在該處管理密碼與託管存取權限。\"],\"/PXXBT\":[\"近乎純白,帶一點暖意。\"],\"/PoNoq\":[\"編輯連結\"],\"/zOUxl\":[\"連結至 Telegram 機器人的 QR 碼\"],\"05DXsb\":[\"連接或管理用於從 Telegram 發文的 Bot。\"],\"0OGSSc\":[\"頭像顯示已更新。\"],\"0UzCUX\":[\"更新你用來登入的密碼\"],\"0VVvqu\":[\"多語言已啟用。\"],\"0bdA9b\":[\"啟用 Telegram 以連線\"],\"0dieZ/\":[\"/archive 開啟時用哪種版式。讀者可以在頁面上自行切換。\"],\"0gECZD\":[\"想寫更完整的介紹?\"],\"0uIjy/\":[\"淺鼠尾草綠底,配同色系的綠。\"],\"1DahsC\":[\"搜尋設定\"],\"1F6Mzc\":[\"目前尚無導覽項目。請新增連結或在下方啟用系統項目。\"],\"1H7gng\":[\"後台語言\"],\"1MYU3o\":[\"無法建立頁面。請檢查資料後再試一次。\"],\"1mbBbL\":[\"手動連接\"],\"1njn7W\":[\"淺色\"],\"1pD+7W\":[\"示範網站不會出現在 \",[\"name\"],\" 中。\"],\"1qGdnL\":[\"預設主題。沉靜,適合長時間閱讀。\"],\"1wdDm9\":[\"新增語言\"],\"21mg6u\":[\"選擇一則尚未加入導覽的有標題筆記,或建立新頁面。\"],\"2C7mSG\":[\"選集連結\"],\"2DoBvq\":[\"訂閱來源\"],\"2Et8jU\":[\"設定每頁搜尋結果數;重設後繼承預設分頁大小。\"],\"2FYpfJ\":[\"更多\"],\"2Ithfh\":[\"傳送任何文字給機器人,該文字會作為筆記已發佈。\"],\"2PTjMB\":[\"我想刪除 \",[\"siteName\"]],\"2QRni+\":[\"時區已更新。\"],\"2UrpGC\":[\"訂閱源網址已送達。目錄會在 \",[\"hours\"],\" 小時內首次讀取。\"],\"2ZqpRB\":[\"關閉\"],\"2cFU6q\":[\"網站頁尾\"],\"2fPEPI\":[\"沉靜、偏冷的底色。\"],\"2lkk2l\":[\"微黃的紙底,配橄欖綠。\"],\"2oWZo7\":[\"最近一次提交\"],\"2r8pkt\":[\"無法載入頁面。請稍後再試。\"],\"2uuy4H\":[\"已透過個人存取權杖連線\"],\"2wK4BX\":[\"編輯 About 頁面\"],\"35x8eZ\":[\"顯示 \",[\"shown\"],\" 共 \",[\"total\"]],\"39QGku\":[\"啟用機器人並傳送綁定碼,之後你傳給它的任何訊息都會成為筆記。\"],\"3B0RD1\":[\"上傳個人資料圖片與產生的網站圖示。\"],\"3Cw1AI\":[\"新增選集\"],\"3VrybB\":[\"重新導向\"],\"3Yvsaz\":[\"302 (暫時)\"],\"3n0zbB\":[\"在示範模式中已停用工作階段管理。請改用共用示範工作階段。\"],\"3sYJi5\":[\"下載與 Hugo 相容的封存 — 以靜態方式託管或移轉到另一個 Jant\"],\"3vMdv3\":[\"此連結為保留連結。請選擇其他連結。\"],\"3wKq0C\":[\"無法儲存。請稍後再試。\"],\"49Bsal\":[\"Feed 設定已更新。\"],\"4D09NB\":[\"連結到選集頁。\"],\"4J/OYU\":[\"選集已建立。\"],\"4Jge8E\":[\"目前的工作階段\"],\"4KIa+q\":[\"已下載匯出檔案。\"],\"4Q0iPK\":[\"跟隨裝置外觀或強制使用淺色或深色主題。\"],\"4cEClj\":[\"工作階段\"],\"4n7WY4\":[\"站名、可見性、訂閱來源與時區\"],\"4tcgrg\":[\"查看這些貼文\"],\"4yyXWu\":[\"淺骨白底,配低飽和的綠。\"],\"4zGJ5E\":[\"永久刪除帳戶\"],\"5QlUIt\":[\"倉庫為空。準備連線。\"],\"5VQnR3\":[\"當你想要一個永遠不會改變的 feed URL 時,請使用這些。\"],\"5dpcN1\":[\"輸入以搜尋全部\"],\"5f1Wo9\":[\"已以 \",[\"account\"],\" 連線\"],\"5o8R81\":[\"極淺的象牙底,配淡淡的茶綠。\"],\"6ArdBh\":[\"將 Featured 貼文用於 /feed。\"],\"6DjeBT\":[\"示範網站會始終對搜尋引擎保持隱藏。\"],\"6E3aK4\":[\"管理託管\"],\"6FFB7q\":[\"使用最新的公開貼文作為 /feed。\"],\"6K1Vef\":[\"確定要永久刪除此部落格嗎?此動作無法復原。\"],\"6NpNLc\":[\"此儲存庫已有內容。\"],\"6Pa5AP\":[\"連接或管理 GitHub 內容自動備份。\"],\"6V3Ea3\":[\"已複製\"],\"6WdDG7\":[\"頁面\"],\"71WIgc\":[\"取得新代碼\"],\"71of4k\":[\"限制每個 RSS feed 中包含的貼文數。\"],\"746NHh\":[\"此部落格\"],\"7811AW\":[\"此儲存庫已在備份本網站。\"],\"7FCZPo\":[\"內容語言\"],\"7FaY4u\":[\"用法\"],\"7G9YLi\":[\"允許搜尋引擎索引我的網站\"],\"7GISOt\":[\"儲存機器人權杖\"],\"7MZxzw\":[\"密碼已變更。\"],\"7p5kLi\":[\"後台\"],\"7vhWI8\":[\"新密碼\"],\"81nFIS\":[\"密碼不符。請確認兩個欄位相同。\"],\"87a/t/\":[\"標籤\"],\"89Upyo\":[\"該主題目前不可用。請選擇其他主題。\"],\"8BfEpW\":[\"託管帳號\"],\"8NIu3g\":[\"該頁面尚未有標題,因此選單沒有可顯示的項目。\"],\"8T46pB\":[\"機器人權杖\"],\"8U2Z7f\":[\"新增自訂 URL\"],\"8ZsakT\":[\"密碼\"],\"8bpHix\":[\"帳戶建立失敗,檢查填寫內容後重試。\"],\"8gEghq\":[\"明亮的暖白底,配苔綠點綴。\"],\"9+vGLh\":[\"自訂 CSS\"],\"9As8Nu\":[\"在 GitHub 上建立一個\"],\"9EjI4j\":[\"暖沙色\"],\"9Fa2Ns\":[\"根位址將恢復顯示所有語言,\",[\"prefix\"],\" 這類位址會重新導向到它,既有的連結和訂閱仍然有效。貼文網址和每篇貼文的語言都不受影響,隨時可以重新啟用。\"],\"9FctRm\":[\"使用小寫字母、數字和連字號。\"],\"9J6wUO\":[\"建議新增的連結\"],\"9Lsvt5\":[\"已於 \",[\"date\"],\" 登入\"],\"9SHZas\":[\"登入後顯示「設定」,未登入時顯示「登入」。\"],\"9T7Cwm\":[\"重新導向、自訂路徑與網址控制\"],\"9aUyym\":[\"查看你在哪裡已登入,並撤銷舊的工作階段\"],\"9mqQnx\":[\"限制自動產生摘要所使用的段落數。\"],\"A7tRC9\":[\"正規的 /feed URL 會回傳哪個貼文串流。\"],\"ADTn/D\":[\"允許不使用 API 權杖讀取已發佈內容。\"],\"AELGTd\":[\"立即加入導覽,或啟用編輯器補充詳細資料。\"],\"ANzR96\":[\"設定編輯器\"],\"AQL8b1\":[\"你寫作的主要語言是?\"],\"AeXO77\":[\"帳戶\"],\"AnY+O9\":[\"在首頁底部顯示「Build with Jant」\"],\"ApZDMk\":[\"此圖會用於你的 favicon 和 apple-touch-icon。為達最佳效果,請上傳至少 512x512 像素、背景為純色的正方形 PNG。\"],\"Aysjjh\":[\"選擇 Jant 全域使用的配色方案。\"],\"B495Gs\":[\"封存\"],\"B4ESok\":[\"API 參考\"],\"B5P9HE\":[\"提交我的網站\"],\"BJ0tsF\":[\"搜尋執行階段設定並啟用進階設定\"],\"BTYdze\":[\"連結已新增到導覽。\"],\"BszQc4\":[\"根位址(/、/feed)顯示這個語言的內容。\"],\"BzEFor\":[\"或\"],\"C0/57J\":[\"這是選集連結的最後一部分。\"],\"CDAdlf\":[\"移除機器人\"],\"CTAEes\":[\"選擇儲存庫\"],\"CjZZgz\":[\"此儲存庫已有提交紀錄\"],\"Cl92kR\":[\"建立新選集\"],\"D35jh6\":[\"管理後台的顯示語言,只有你看得到。\"],\"D8k2s6\":[\"連接 Telegram\"],\"DCKkhU\":[\"目前密碼\"],\"DKKKeF\":[\"在 \",[\"providerLabel\"],\" 管理密碼與託管存取\"],\"DVFXa6\":[\"最乾淨、最中性的一檔。\"],\"DdXUay\":[\"在公開標頭顯示網站頭像。\"],\"E3hjKv\":[\"新增為連結\"],\"EKvXGx\":[\"該位址位於其他網站。Navigation 將其視為連結。\"],\"EO3I6h\":[\"上傳未成功。請稍後再試。\"],\"Eax/et\":[\"強調色\"],\"EbDLD+\":[\"選擇 Jant 全域使用的字體。\"],\"Enslfm\":[\"目標網址\"],\"F53UDu\":[\"冷白\"],\"F7FKwe\":[\"你在此處貼上的任何內容都能完全存取訪客的瀏覽器。僅使用來自你信任來源的程式碼。\"],\"F8SNqr\":[\"暖橘\"],\"FBgjRN\":[\"搜尋頁面,或貼上位址\"],\"FMUJSP\":[\"帳戶與資料\"],\"FUfPPw\":[\"根位址(/、/feed、/archive)將改為提供「\",[\"next\"],\"」的內容,「\",[\"previous\"],\"」移至 \",[\"prefix\"],\"。貼文網址不會改變,但訂閱 /feed 的讀者從此收到的是「\",[\"next\"],\"」的貼文。\"],\"Fe1cvJ\":[\"延遲新貼文和回覆進入訂閱源的時間。設為 0 可關閉延遲。\"],\"Fk3SSD\":[\"平靜自然,看著舒服。\"],\"FkMol5\":[\"Featured\"],\"FwL2Zk\":[[\"count\",\"plural\",{\"other\":[\"還有 \",\"#\",\" 篇\",[\"language\"],\"貼文,但列表裡沒有這個語言。把它加回列表,或先修改這些貼文的語言。\"]}]],\"G/1oP+\":[\"移除 webhook 並停止同步。你的儲存庫內容不會被刪除。\"],\"G0qJsQ\":[\"找不到安全權杖。請重新整理頁面後再試一次。\"],\"G0tHaW\":[\"建立一個不會出現在 Latest 的公開頁面。\"],\"G2fuEb\":[\"已鎖定\"],\"G39wnK\":[\"將內容備份並與 GitHub 儲存庫同步\"],\"G5oCui\":[\"URL 結構、每種語言的訂閱來源,以及譯本關聯。\"],\"G9peMt\":[\"建立帳戶\"],\"GAmD3h\":[\"語言\"],\"GBkMNw\":[[\"name\"],\" 是 Jant 社群人工維護的目錄,旨在幫助使用者發現新的 Jant 部落格和內容。\"],\"GXsAby\":[\"撤銷\"],\"GdGiNi\":[\"第 \",[\"current\"],\" 步,共 \",[\"total\"],\" 步\"],\"GorKul\":[\"歡迎使用 Jant\"],\"GxkJXS\":[\"上傳中...\"],\"GzKzUa\":[\"試用限制\"],\"HCNlq3\":[\"乾淨、高對比,接近純白。\"],\"HHDyGw\":[\"柔綠\"],\"HKH+W+\":[\"資料\"],\"Hp1l6f\":[\"目前\"],\"HxTUmw\":[\"新增 \",[\"language\"]],\"HxlY7t\":[\"變更此設定會更新訂閱者從 /feed 取得的內容。\"],\"HxuOlm\":[\"網站頁首\"],\"I0ijRI\":[\"全部 feed 網址\"],\"I4HRxU\":[\"已發布的全部貼文,包括從 Latest 中隱藏的那些。\"],\"I6gXOa\":[\"路徑\"],\"I76CzF\":[\"中性灰\"],\"ID38tA\":[\"示範模式下帳號刪除已停用。共用示範會另行重置。\"],\"IF9tPu\":[\"何時使用網站匯出、資料庫備份與復原演練。\"],\"IW5PBo\":[\"複製權杖\"],\"IagCbF\":[\"URL\"],\"IreQBq\":[\"儲存庫\"],\"J36Xsm\":[\"在 \",[\"address\"],\" 找不到任何頁面。請檢查位址,或以標題搜尋。\"],\"J6bLeg\":[\"新增自訂連結到任何 URL\"],\"JL7LF5\":[\"可用的 CSS 變數、data 屬性與範例。\"],\"JcD7qf\":[\"更多操作\"],\"JjX0OO\":[\"請立即複製你的權杖 — 它不會再顯示。\"],\"JrFTcr\":[\"連線中…\"],\"JuN5GC\":[\"未選取檔案。請選擇要上傳的檔案。\"],\"K+0Hu0\":[\"找不到可加入的相符頁面。換個標題搜尋,或建立新頁面。\"],\"K/F6pa\":[\"儲存中…\"],\"KDw4GX\":[\"重試\"],\"KSgo21\":[\"選擇一個儲存庫\"],\"KVVYBh\":[\"新增選集到導覽\"],\"KiJn9B\":[\"筆記\"],\"Kk7jwL\":[\"設定每頁封存貼文數;重設後繼承預設分頁大小。\"],\"KwOLJF\":[\"Jant 最初的配色,日常穩妥之選。\"],\"L+rMC9\":[\"還原為預設值\"],\"L27RpE\":[\"頁面已建立。\"],\"L3DEwT\":[\"移除這個頭像?你的 favicon 與頁首圖示會回復為預設。\"],\"L4t4/q\":[\"3月14日\"],\"L86zmP\":[\"在完整表單中編輯首頁和中繼資料使用的多行簡介。\"],\"LdyooL\":[\"連結\"],\"LhMjLm\":[\"時間\"],\"LjwaJ/\":[\"登入、匯出和刪除\"],\"M/D8PK\":[\"+ 安裝到其他帳戶\"],\"M/haSd\":[\"永遠顯示主題的淺色版本。\"],\"M/kDIW\":[\"搜尋引擎看到的語言。\"],\"M1co/O\":[\"已設定\"],\"M2hGyf\":[\"指向 /subscribe,一個列出你所有 feed 網址、帶複製按鈕的頁面。適合還沒在用閱讀器的讀者。\"],\"M2kIWU\":[\"字型主題\"],\"M4tzVU\":[\"Latest 貼文\"],\"M6CbAU\":[\"切換編輯面板\"],\"M7wPsD\":[\"切換\"],\"MHrjPM\":[\"標題\"],\"MKIM3K\":[\"搜尋頁面\"],\"MaYYE6\":[\"透過向 Telegram 機器人傳送訊息來發佈筆記\"],\"Me5t5H\":[\"連接 GitHub 倉庫,自動把貼文備份成 Markdown。GitHub 上的修改會同步回網站。\"],\"MeCJlL\":[\"正在提交網站,重新整理後可以看到結果。\"],\"MnbH31\":[\"頁面\"],\"Mq9FZ1\":[\"正在建立頁面…\"],\"Mr4QPw\":[\"要斷開 Telegram?你可以隨時使用新的綁定代碼重新連接。\"],\"MtENL9\":[\"調整你的網站外觀、可讀性與執行效能。\"],\"N/8NPV\":[\"在刪除前,請先下載網站匯出檔案。刪除後無法恢復此帳號。\"],\"N7UNHY\":[\"Featured RSS 來源\"],\"NHnUHF\":[\"頁首上的網站圖示與個人標記\"],\"NU2Fqi\":[\"儲存 CSS\"],\"NVjhde\":[\"暖羊皮紙\"],\"Nldjdr\":[\"還沒有自訂 URL。建立一個,給貼文加重新導向或自訂路徑。\"],\"O3oNi5\":[\"電子郵件\"],\"OJxdgi\":[\"連結不能超過 200 個字元。\"],\"OSJXFg\":[\"套用於整個網站,包括管理頁面。選擇一個調色盤,然後決定它是跟隨系統還是維持固定。\"],\"OeUWA7\":[\"加入頁面\"],\"OuuMXJ\":[\"在 </body> 結束標籤之前新增全站 HTML。\"],\"Ox3+3h\":[\"無相符結果。\"],\"P7Eeay\":[\"完整貼文\"],\"PEUV5I\":[\"程式碼注入已更新。\"],\"PHh52z\":[\"暖而厚重,棕調明顯。\"],\"PXj9lw\":[\"停止接受來自 Telegram 的貼文。你現有的筆記會保持已發佈。\"],\"PZ7HJ8\":[\"部落格大頭貼\"],\"Pbm2/N\":[\"建立選集\"],\"Pwqkdw\":[\"載入中…\"],\"PxJ9W6\":[\"產生權杖\"],\"Q/6Y+2\":[\"需要在目標儲存庫上擁有 Contents(讀/寫)和 Webhooks(讀/寫)權限。\"],\"Q/O0X4\":[\"此設定未儲存。請檢查設定值並再試一次。\"],\"Q30z/l\":[\"要從導覽移除這個選集嗎?選集本身不會被刪除。\"],\"Q99OtV\":[\"將選集釘選到導覽列。最近 48 小時內更新的選集旁會顯示一個 * 號。\"],\"QCwsv1\":[\"暖白\"],\"QKvrmL\":[\"暖中性,適合長讀。\"],\"QZmz0H\":[\"內建連結\"],\"Qnrzvb\":[\"已啟用的權杖\"],\"R6Z4LE\":[\"下載失敗。請再試一次。\"],\"R9Khdg\":[\"自動\"],\"RDjuBN\":[\"初始設定\"],\"RRo9kN\":[\"語言已更新。\"],\"RcdDOS\":[\"在 Telegram 上向 @BotFather 發送訊息以建立機器人,然後貼上它提供給你的權杖\"],\"RdVIcf\":[\"奶油配深咖\"],\"Ri/9PY\":[\"目前仍只向 Discover 提供精選貼文,但沒有任何貼文標為 Featured,訂閱源裡沒有可顯示的內容。取消勾選再重新勾選,即可讓 Discover 讀取全部公開貼文。\"],\"Rn2p1h\":[\"沒有任何項目符合此搜尋。請嘗試不同的名稱或描述。\"],\"RxsRD6\":[\"時區\"],\"SDND4q\":[\"未設定\"],\"SJmfuf\":[\"網站名稱\"],\"SKZhW9\":[\"權杖名稱 (API 權杖名稱欄位。)\"],\"SSsoa4\":[\"加入導覽\"],\"SVQQPe\":[\"無法連線。請檢查錯誤並再試一次。\"],\"SWb0z+\":[\"此網址為保留網址。請選擇其他網址。\"],\"SYGk01\":[\"以網址前綴提供的語言。在「語言」頁管理。\"],\"SchpMp\":[\"Telegram\"],\"SeY4vo\":[\"移除 \",[\"language\"],\" 後就只剩主語言了,多語言會一併關閉。根位址將恢復顯示所有語言,\",[\"prefix\"],\" 不再可用,每篇貼文的語言維持不變。\"],\"SqKp3o\":[\"此標題會用於整個網站、瀏覽器分頁與訂閱來源。\"],\"SrGs4T\":[\"淺藍灰底,冷靜而均勻。\"],\"T/R+Qz\":[\"主語言\"],\"T17dXm\":[\"每種語言是否擁有獨立的首頁、彙整和訂閱來源。在「語言」頁管理。\"],\"T41PG1\":[\"限制自動產生摘要所使用的字元數。\"],\"TN0mN4\":[\"搜尋和編輯執行階段設定。變更會立即生效;重設會恢復環境變數或內建預設值。\"],\"TSCeuF\":[\"無法建立選集。請檢查資料後再試一次。\"],\"TpF3v+\":[\"在 </head> 之前注入。用於分析、自訂 meta 標籤,以及必須提前載入的樣式。\"],\"Tu6bMZ\":[\"建立 About 頁面\"],\"Tz0i8g\":[\"設定\"],\"U5J7jI\":[\"網站可見性已更新。\"],\"U5v6Gh\":[\"編輯頁面\"],\"UFK415\":[\"用於分析與小工具的網站全域 HTML\"],\"UTvFQq\":[\"啟用 \",[\"linkOpen\"],\"@\",[\"botUsername\"],[\"linkClose\"],\" 並傳送:\"],\"UUn+Y5\":[\"啟用設定\"],\"UaZwcz\":[\"建立後可以設定更多選項。\"],\"Uj/btJ\":[\"在我的網站頁首顯示大頭貼\"],\"UsODUn\":[\"選擇一個帳戶\"],\"UxKoFf\":[\"導覽列\"],\"UyMSeQ\":[\"在該位址\"],\"V+bhUy\":[\"安裝 GitHub App\"],\"V0fyg5\":[\"選集已加入導覽。\"],\"V4WsyL\":[\"新增連結\"],\"V5pZwT\":[\"搜尋設定已更新。\"],\"V7dQi9\":[\"語言已移除。\"],\"VXUPla\":[\"使用 GitHub App 連線\"],\"Vh+JIV\":[\"多語言指南\"],\"VhMDMg\":[\"變更密碼\"],\"Vn3jYy\":[\"導覽項目\"],\"VoZYGU\":[\"這會永久刪除你的全部資料——貼文、媒體、選集、設定和帳戶。部落格會重設為初始設定狀態。此操作無法復原。\"],\"VoarBQ\":[\"所有貼文的網址都不會改變,也可以隨時再關閉。\"],\"VqyJ7Y\":[\"主要語言已變更。\"],\"WL7KjI\":[\"卡片網格\"],\"WUnFK2\":[\"偏冷的淺白底,配深靛藍文字。高對比。\"],\"Wa9q4P\":[\"純白\"],\"Wb7EHo\":[\"About 頁面\"],\"Weq9zb\":[\"一般\"],\"Wi9i06\":[\"依照每位訪客的系統偏好。\"],\"Wildi8\":[\"沒有可加入的頁面。建立頁面後即可加入導覽。\"],\"Wx1M8N\":[\"安裝 GitHub App,以授予存取權而無需管理個人權杖。權限以每個儲存庫為範圍,並可在 GitHub 上撤銷。\"],\"X+8FMk\":[\"目前的密碼不符。請再試一次。\"],\"X1G9eY\":[\"導覽列預覽\"],\"X2Cbc2\":[\"封存 RSS 來源\"],\"X5P6yv\":[\"最近更新\"],\"X9Hujr\":[\"手動推送\"],\"XQQOyj\":[\"該頁面為草稿。請先發佈,然後再加入。\"],\"XeRkls\":[[\"count\",\"plural\",{\"other\":[\"其中 \",\"#\",\" 篇還沒有語言,會標記為「\",[\"language\"],\"」。\"]}]],\"Xsu5WL\":[\"在專用程式碼編輯器中新增全站 CSS。\"],\"XtBJV8\":[\"正在檢查儲存庫…\"],\"Xtc16w\":[\"重新整理儲存庫清單\"],\"Y+7JGK\":[\"建立頁面\"],\"Y/F35r\":[\"使用 curl 建立貼文:\"],\"Y/N5N7\":[\"柔和的奶油底,配低飽和的綠。\"],\"Y4oXje\":[\"名稱預填為 \",[\"name\"],\"。返回後清單會重新整理。\"],\"YF6zHf\":[\"網站設定已更新。\"],\"YVcVlW\":[\"移除並關閉\"],\"Ya/FAk\":[\"至少再新增一種語言才能啟用。\"],\"YdG2RF\":[\"匯出網站\"],\"YkgZi7\":[\"連接一個 Telegram 機器人,之後你傳給它的任何訊息都會以筆記形式已發佈。\"],\"YwhjRx\":[\"管理帳戶\"],\"Yxp859\":[\"暖奶油色\"],\"Z5HWHd\":[\"已啟用\"],\"ZDY7Fy\":[\"同步中…\"],\"ZQKLI1\":[\"危險區域\"],\"ZS/CBL\":[\"刪除此導覽連結?訪客將不再在你的網站頁首看到它。\"],\"ZXZrAo\":[\"頁面網址不能超過 200 個字元。\"],\"ZgZX+d\":[\"純白底配中性灰。不帶任何色偏。\"],\"Zgq+c2\":[\"設定預設每頁顯示的項目數。\"],\"ZhhOwV\":[\"引用\"],\"ZiooJI\":[\"API 權杖\"],\"Zm7Qb0\":[\"備份與還原指南\"],\"ZmUkwN\":[\"新增自訂連結到導覽\"],\"a14mj8\":[\"未知裝置\"],\"a1iEgy\":[\"暖奶油底,配深咖啡棕點綴。\"],\"a3LDKx\":[\"安全性\"],\"aAIQg2\":[\"外觀\"],\"aFkzVF\":[\"目標貼文或選集的 slug\"],\"aR3U86\":[\"多語言已啟用。\",[\"count\",\"plural\",{\"other\":[\"#\",\" 篇貼文\"]}],\"已標記為「\",[\"language\"],\"」。\"],\"aV6XWN\":[\"多語言已關閉。你的語言設定仍然保留。\"],\"alKG0+\":[\"字型主題\"],\"anibOb\":[\"關於本部落格\"],\"any7NR\":[\"主題指南\"],\"asq0vL\":[\"你寫作使用的語言。\"],\"b+/jO6\":[\"301 (永久)\"],\"b+FyBD\":[\"將頁面加入導覽\"],\"b1FYSK\":[\"主要語言\"],\"bHOiy1\":[\"示範模式已停用變更密碼功能。請使用共用示範帳號登入。\"],\"bHYIks\":[\"登出\"],\"bV2vng\":[\"暖陶土色\"],\"bbR5vW\":[\"這套配色\"],\"bbzY7X\":[\"管理登入安全、站點匯出和無法復原的操作。\"],\"bfHZ7r\":[\"選擇用於顯示日期和時間的時區。\"],\"bi10qS\":[\"立即加入導覽,或啟用編輯器補充內容。\"],\"bkMuwo\":[\"連結到你標為 Featured 的貼文。\"],\"bmrL08\":[\"示範模式會隱藏會話、密碼更改與帳號刪除。匯出功能仍可使用。\"],\"bph1Dd\":[\"已關閉搜尋引擎索引,所以預設不會出現在 Jant Discover。勾選上面的選項仍會加入。\"],\"brfpw9\":[\"在完整表單中編輯顯示在公開頁面底部的多行頁尾。\"],\"bviiKV\":[\"這個主題下的標題、正文和連結。\"],\"c1BGrV\":[\"已在導覽中。請在上方清單中拖曳以移動\"],\"c1iSrq\":[\"Discover 社群規則\"],\"c3MN2z\":[\"所有可用的端點與請求格式。\"],\"cHh/Zu\":[\"啟用多語言\"],\"cS7/bk\":[\"移除已儲存的機器人權杖?其 webhook 會被刪除,任何已連接的帳號將會被斷線。\"],\"cSDy01\":[\"自訂 CSS 已更新。\"],\"clzoNp\":[\"始終顯示深色主題。\"],\"cnGeoo\":[\"刪除\"],\"cwaLCZ\":[\"頁面已加入導覽。\"],\"d1lzY/\":[\"語言已新增。\"],\"d3FRkY\":[\"無法複製。請再試一次。\"],\"d5oGUo\":[\"在 GitHub 建立新儲存庫\"],\"dB1Nr4\":[\"開始寫作\"],\"dEgA5A\":[\"取消\"],\"dTXUY+\":[\"確認刪除帳號\"],\"dYKrp3\":[\"Hidden from Latest\"],\"dk7TCH\":[\"永久刪除所有資料並重設部落格\"],\"ds0nJk\":[\"在 head 標籤的閉合標記之前加入全站 HTML。\"],\"dsWkIw\":[\"要與 GitHub 斷開連線嗎?webhook 會被移除。你的儲存庫內容不會被刪除。\"],\"dwcK1n\":[\"或手動提交網址\"],\"e/tSI5\":[\"導覽順序已更新。\"],\"eOL7vn\":[\"色彩極少,干擾最小。\"],\"ePK91l\":[\"編輯\"],\"ebQKK7\":[\"網站\"],\"eeamrQ\":[\"Discover 讀取的是 Atom 訂閱源,需要先開啟訂閱源。\"],\"egK+Yy\":[\"供腳本與自動化使用的 Bearer 權杖\"],\"egOXl0\":[\"Jant Discover\"],\"ehj/zN\":[\"重新導向類型\"],\"ej6Rji\":[\"若有別的語言的貼文,之後可以在那篇貼文的選單裡單獨更正。\"],\"eneWvv\":[\"草稿\"],\"erTMh7\":[\"上次同步\"],\"f+m8jj\":[\"已複製訂閱網址。\"],\"f8fH8W\":[\"設計\"],\"fDz6PV\":[\"選擇用於內容備份的儲存庫。\"],\"fKRAwQ\":[\"沙色底,配綠色點綴。\"],\"fWYqkz\":[\"程式碼注入\"],\"fYXQnC\":[\"暖色裡最濃的一檔,溫馨。\"],\"fttd2R\":[\"我的選集\"],\"gZ5owP\":[\"搜尋儲存庫\"],\"gbqbh6\":[\"可以放心離開此頁面 — 同步會在背景繼續進行。\"],\"gkFvVN\":[\"在 </body> 之前注入。用於聊天小工具和不應阻塞頁面載入的腳本。\"],\"gtQsRO\":[\"建立自訂網址\"],\"hBO/y4\":[\"安全權杖已過期。請重新整理頁面並再試一次。\"],\"hGmyDl\":[\"權杖讓你從腳本,捷徑和其他工具存取 API 無需登入\"],\"hIHkRy\":[\"已透過 GitHub 應用程式連線\"],\"hJHHsU\":[\"發佈網站、封存與選集的 Atom feeds。\"],\"hcH8MY\":[\"設定你的網站\"],\"hdSi1b\":[\"輸入 \",[\"repo\"],\" 以確認\"],\"he3ygx\":[\"複製\"],\"hjeS3W\":[\"連結到 Latest。首頁展示的就是這個列表。\"],\"hjvSlw\":[\"語言已移除,多語言已關閉。\"],\"hvGGMK\":[\"從導覽中移除此頁面?頁面本身不會被刪除。\"],\"i0qMbr\":[\"首頁\"],\"iEUzMn\":[\"系統\"],\"iH8pgl\":[\"返回\"],\"iMkWoF\":[\"啟用\"],\"iSLIjg\":[\"連接\"],\"iVOMRi\":[\"首頁設定已更新。\"],\"icB4Cv\":[\"將連結拖到此處以顯示於「更多」選單下方\"],\"id3vuh\":[\"已設定 Telegram,但無法連線到機器人。請檢查機器人權杖並再試一次。\"],\"idD8Ev\":[\"已儲存\"],\"ihn4zD\":[\"搜尋…\"],\"iiDXZc\":[\"顯示於所有貼文與頁面的底部。\"],\"itS31B\":[\"更暖一些,像略舊的紙。\"],\"iwQZvS\":[\"冷藍灰\"],\"j4VrG6\":[\"下載匯出 ZIP\"],\"j5nQL2\":[\"例如 iOS 捷徑\"],\"jNE7O+\":[\"還沒有公開貼文。jant.me 的收錄條件是\",[\"minCount\",\"plural\",{\"other\":[\"滿 \",\"#\",\" 篇\"]}],\"。\"],\"jUV7CU\":[\"上傳大頭貼\"],\"jVUmOK\":[\"支援 Markdown\"],\"jdVYS8\":[\"選一個適合你文字的。\"],\"jgBjXJ\":[\"要撤銷這個權杖嗎?任何使用它的腳本都會停止運作。\"],\"jpctdh\":[\"檢視\"],\"k1ifdL\":[\"處理中...\"],\"kLw03Y\":[\"亮紙白\"],\"kMXclu\":[\"下載網站匯出檔案\"],\"kNiQp6\":[\"已釘選\"],\"kQ3Otm\":[\"建立新頁面\"],\"kRhzWq\":[\"GitHub 同步\"],\"kVQs7s\":[\"細緻的樣式覆寫\"],\"ke1gWS\":[\"自訂 URL\"],\"kfcRb0\":[\"頭像\"],\"kp8wiR\":[\"正在檢查網址…\"],\"kxDZ2i\":[\"此程式碼會在你網站的每個頁面上執行。\"],\"kzvWob\":[\"連結到貼文歸檔。\"],\"lLW3vJ\":[\"目標 slug\"],\"lLsfOE\":[\"關閉多語言?\"],\"lV04bQ\":[\"溫暖、有泥土感,但不顯暗。\"],\"lYHJih\":[\"撤銷此工作階段?該裝置將需要重新登入。\"],\"m16xKo\":[\"新增\"],\"mLOk1i\":[\"立即將所有貼文推送到 GitHub,而不是等待下一次自動同步。\"],\"mSNmrX\":[\"列出貼文:\"],\"n8+EXe\":[\"新增網站中已經存在的常用入口。\"],\"nG2qTk\":[\"示例連結\"],\"nK07ni\":[\"為你的網站選擇一種排版風格。每個主題會同時改變字體配對與閱讀節奏。\"],\"nbfdhU\":[\"第三方整合\"],\"ntJYyh\":[\"在 \",[\"providerLabel\"],\" 管理網域、方案和計費\"],\"o/vNDE\":[\"讓你覆寫任何主題變數。\"],\"o5rj+L\":[\"目前還沒有選集。在這裡建立一個並加入導覽。\"],\"oGC9uP\":[\"owner/repo\"],\"oKOOsY\":[\"色彩主題\"],\"oL535e\":[\"尚未同步\"],\"oNA4If\":[\"所有選集已經在你的導覽中。\"],\"oUn9Z7\":[\"近中性的淺灰,配鋼藍點綴。\"],\"ofdy2l\":[\"帶橘調的底色。最暖的一套。\"],\"olouYg\":[\"導覽會將該位址儲存為連結。\"],\"oxj6Pk\":[\"其他語言\"],\"pIpTqF\":[\"每種語言都有獨立的首頁、彙整和訂閱來源。啟用後,發佈時可以選擇不同的語言,也可以為既有的貼文關聯一篇其他語言的貼文。\"],\"pNXxtS\":[\"選擇要向讀者與搜尋引擎宣告的內容語言。\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pgJImo\":[\"把主要語言改為其他語言?\"],\"pgTIrt\":[\"選擇要與此網站同步的 GitHub 帳號和儲存庫。\"],\"psoxDF\":[\"該字型主題無法使用。請選擇其他主題。\"],\"pt4OhQ\":[\"/about 已被佔用。重新命名該項目後再建立 About 頁面。\"],\"pvnfJD\":[\"深色\"],\"q+hNag\":[\"選集\"],\"q/T5GS\":[\"私人 Jant 儀表板使用的語言。\"],\"qSgO/Y\":[\"跟隨內容語言\"],\"qZ9jzv\":[\"網站可見性\"],\"qdcESc\":[\"建立新儲存庫\"],\"qkSJzV\":[\"正在搜尋頁面…\"],\"r5EW6f\":[\"此儲存庫已在備份另一個 Jant 網站 (\",[\"host\"],\"). 請選擇其他儲存庫。\"],\"rCPrbS\":[[\"language\"],\"的操作\"],\"rEspiY\":[\"導覽位置已更新。\"],\"rFmBG3\":[\"色彩主題\"],\"re0mF0\":[\"頁面是公開的,但不會出現在 Latest。\"],\"rlonmB\":[\"無法刪除。請稍後再試。\"],\"s3jOk7\":[\"請輸入頁面標題。\"],\"sROtqR\":[\"無法連線目錄:\",[\"reason\"]],\"sRmGLp\":[\"之後可以在設定中更改。\"],\"satWc6\":[\"主要 RSS 訂閱\"],\"sgr2wQ\":[\"選集\"],\"sj4LLc\":[\"僅顯示已修改項目\"],\"sjej3N\":[\"移除「\",[\"language\"],\"」\"],\"slujBW\":[\"只能使用小寫字母、數字和連字號。\"],\"soRdOu\":[\"米白紙底,配深森綠。\"],\"sqxcaY\":[\"建立於 \",[\"date\"]],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"t3hvHq\":[\"立即同步\"],\"tJ4H0O\":[\"你的 Telegram 帳號\"],\"tfDRzk\":[\"儲存\"],\"tgWuMB\":[\"已修改\"],\"tvgAq5\":[\"尚未授權任何帳戶\"],\"u1VTd3\":[\"調色盤、表面色調與整體氛圍\"],\"u3wRF+\":[\"已發佈\"],\"u6KOjV\":[\"想要更細緻的控制?\"],\"uKLe0J\":[[\"count\"],\" 個設定已顯示\"],\"uVZcIA\":[\"此網址已被使用。請選擇其他網址。\"],\"udPwLB\":[\"頁首\"],\"ugHDtd\":[[\"about\"],\"貼文設為 Featured \",[\"hours\"],\" 小時後會出現在 Discover 首頁,Link 和 Quote 類型的貼文發佈 \",[\"hours\"],\" 小時後出現在 \",[\"links\"],\" 和 \",[\"quotes\"],\" 列表。這段期間你可以繼續修改,詳見 \",[\"rules\"],\"。\"],\"ui6aMF\":[\"這些裝置目前已登入你的帳號。撤銷任何你不認識的工作階段。\"],\"vBEKwo\":[\"在此管理本網站的活動工作階段。密碼與託管存取由 \",[\"providerLabel\"],\" 管理。\"],\"vOnuOa\":[\"在頁首和更多選單中顯示或隱藏內建目的地。\"],\"vRldcl\":[\"字體選擇與閱讀質感\"],\"vSYKYI\":[\"主要訂閱來源\"],\"vTuib7\":[\"這會控制 /feed 回傳的內容。\"],\"vXCC6J\":[\"有欄位填寫不正確,檢查後重試。\"],\"vXIe7J\":[\"語言\"],\"vXsx3x\":[\"該頁面為私人頁面,因此沒有人能從選單開啟它。\"],\"vdFnYM\":[\"重設連結\"],\"vmQmHx\":[\"新增自訂 CSS 以覆寫任何樣式。使用像 [data-page]、[data-post]、[data-format] 這類資料屬性來選取特定元素。\"],\"vpSPA1\":[\"缺少驗證金鑰,檢查環境變數設定。\"],\"vzX5FB\":[\"刪除帳號\"],\"w615zC\":[\"多語言\"],\"w8Rv8T\":[\"標籤為必填\"],\"wL3cK8\":[\"Latest\"],\"wW6NCp\":[\"上次錯誤\"],\"wc+17X\":[\"/* 在此放入你的自訂 CSS */\"],\"wuLtXn\":[\"目前沒有任何活動中的工作階段。已登入的裝置會顯示在此處。\"],\"x+HGBk\":[\"要求搜尋引擎不要索引公開頁面或將它們包含在搜尋結果中。\"],\"xCWek4\":[\"檔案儲存尚未設定。請檢查你的伺服器設定。\"],\"xCbjQn\":[\"允許我的部落格出現在 \",[\"name\"],\" 中\"],\"xGVfLh\":[\"繼續\"],\"xHt036\":[\"個人存取權杖\"],\"xKeZ0l\":[\"更偏暖、偏土的色調。\"],\"xYbozN\":[\"柔象牙白\"],\"xqViCq\":[\"暖象牙白\"],\"xxaahd\":[\"想要更冷、更利落的觀感時。\"],\"y/awtV\":[\"頁首連結、內建目的地與更多選單\"],\"y28hnO\":[\"貼文\"],\"y9W9vo\":[\"正在建立選集…\"],\"yNCqOt\":[\"Latest RSS 來源\"],\"yQ3kNF\":[\"請輸入以下短語以確認:\"],\"ydq1k2\":[\"請先選擇一個帳戶\"],\"yjjCV8\":[\"固定的 RSS 檔案網址\"],\"yjkELF\":[\"確認新密碼\"],\"ym+ccl\":[\"在首頁顯示 Jant 的署名。\"],\"yzF66j\":[\"連結\"],\"z6wakA\":[\"顯示在你的主頁上的簡短介紹。\"],\"zEizrk\":[\"最後使用於 \",[\"date\"]],\"zPqly9\":[\"這個站點沒有可提交的目錄。\"],\"zSURJW\":[\"沒有符合的儲存庫。\"],\"zUlHGd\":[\"頁面網址\"],\"zYRGIR\":[\"尚未提交。還沒有目錄知道這個網站存在。\"],\"zcDmsG\":[\"Featured 貼文\"],\"zlcDd2\":[\"刪除此自訂 URL?使用該 URL 的訪客將不會再被重新導向。\"],\"ztPYIR\":[\"啟用時會一次性標記既有貼文\"],\"zwBp5t\":[\"私有\"],\"zxexio\":[\"內容語言、介面語言、多語言\"]}");
2019
2019
  //#endregion
2020
2020
  //#region src/i18n/i18n.ts
2021
2021
  /**
@@ -3863,21 +3863,21 @@ function normalizeThemeColorForMeta(color) {
3863
3863
  *
3864
3864
  * The dev flag itself lives in `build-env.ts`, which client bundles can import
3865
3865
  * without these Worker-only globals.
3866
- */ var CORE_VERSION = "0.7.0-db482685dd630599";
3867
- var CLIENT_JS_FILE = "/_assets/client-VnFxJN7G.js";
3868
- var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-k8gJ6QJj.js";
3866
+ */ var CORE_VERSION = "0.7.1-380517d00e9b9119";
3867
+ var CLIENT_JS_FILE = "/_assets/client-knSEyUJO.js";
3868
+ var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-CAMfTrKW.js";
3869
3869
  var CLIENT_COMPOSE_PRELOAD = [
3870
- "/_assets/client-compose-B_kDtWkW.js",
3871
- "/_assets/chunks/create-editor-CD3FhrOB.js",
3870
+ "/_assets/client-compose-D6rExKVK.js",
3871
+ "/_assets/chunks/create-editor-B-m7X7S5.js",
3872
3872
  "/_assets/chunks/types-D5iF3H1a.js",
3873
3873
  "/_assets/chunks/lit-DW8VmJAT.js",
3874
- "/_assets/chunks/sortable-list-CgaL2jCs.js",
3874
+ "/_assets/chunks/sortable-list-BJyd-LXE.js",
3875
3875
  "/_assets/chunks/unsafe-html-B5ZEXaxg.js",
3876
3876
  "/_assets/chunks/slug-format-CRXO1AL5.js",
3877
- "/_assets/chunks/unsafe-svg-0QCkP0vZ.js",
3877
+ "/_assets/chunks/unsafe-svg-BkscJx69.js",
3878
3878
  "/_assets/chunks/footnote-rail-CmeoVtnU.js"
3879
3879
  ];
3880
- var CLIENT_CSS_FILE = "/_assets/client-GiYENVw8.css";
3880
+ var CLIENT_CSS_FILE = "/_assets/client-DAhoqPdr.css";
3881
3881
  var CLIENT_AUTHOR_CSS_FILE = "/_assets/client-author-DQ8R-8KR.css";
3882
3882
  var CLIENT_CJK_CSS_FILE = "/_assets/client-cjk-B7Z0snDu.css";
3883
3883
  var CLIENT_CJK_TC_CSS_FILE = "/_assets/client-cjk-tc-BesJYrb2.css";
@@ -4462,7 +4462,7 @@ var IconSprite = () => {
4462
4462
  const authorStylesheetPath = resolvedClientBundle === "full" ? IS_VITE_DEV ? assetPath("/src/style-author.css") : toPublicAssetPath(CLIENT_AUTHOR_CSS_FILE, assetBasePath) : null;
4463
4463
  const clientScriptPath = IS_VITE_DEV ? resolvedClientBundle === "full" ? assetPath("/src/client-auth.ts") : assetPath("/src/client.ts") : toPublicAssetPath(resolvedClientBundle === "full" ? CLIENT_AUTH_JS_FILE : CLIENT_JS_FILE, assetBasePath);
4464
4464
  const composePreloadPaths = !IS_VITE_DEV && resolvedClientBundle === "full" ? CLIENT_COMPOSE_PRELOAD.map((file) => toPublicAssetPath(file, assetBasePath)) : [];
4465
- const faviconAssetVersion = resolvedFaviconVersion || "0.7.0-db482685dd630599";
4465
+ const faviconAssetVersion = resolvedFaviconVersion || "0.7.1-380517d00e9b9119";
4466
4466
  const resolvedFaviconHref = faviconHref ?? (faviconAssetVersion ? toPublicPath(`/favicon.ico?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/favicon.ico", sitePathPrefix));
4467
4467
  const resolvedAppleTouchHref = appleTouchHref ?? (faviconAssetVersion ? toPublicPath(`/apple-touch-icon.png?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/apple-touch-icon.png", sitePathPrefix));
4468
4468
  const socialImageHref = resolvedSocialImagePath ? toAbsoluteAssetUrl(resolvedSocialImagePath, appConfig?.siteUrl || "", sitePathPrefix) : "";
@@ -6472,9 +6472,13 @@ z.enum(SORT_ORDERS);
6472
6472
  z.enum(NAV_ITEM_TYPES);
6473
6473
  var SystemNavKeySchema = z.enum(SYSTEM_NAV_KEY_VALUES);
6474
6474
  /**
6475
- * Redirect type enum schema
6476
- * Form input validation for redirect type (stored as number in DB)
6477
- */ var RedirectTypeSchema = z.enum(["301", "302"]);
6475
+ * Redirect type, as forms send it ("301") or as the API answers it (301).
6476
+ * Parses to the string form; stored as a number.
6477
+ */ var RedirectTypeSchema = z.union([
6478
+ z.enum(["301", "302"]),
6479
+ z.literal(301).transform(() => "301"),
6480
+ z.literal(302).transform(() => "302")
6481
+ ]);
6478
6482
  z.enum([
6479
6483
  "post",
6480
6484
  "collection",
@@ -6656,8 +6660,19 @@ function refineCreatePostFormatShape(schema) {
6656
6660
  path: ["path"]
6657
6661
  });
6658
6662
  }
6659
- refineSlugPathExclusivity(refineCreatePostFormatShape(refineBodyExclusivity(PostFieldsSchema)));
6660
- var CreatePostApiSchema = refineSlugPathExclusivity(refineCreatePostFormatShape(refineBodyExclusivity(ApiPostFieldsSchema)));
6663
+ /**
6664
+ * Create only: a post's own record timestamps, in Unix seconds. An import or
6665
+ * a migration restores them, so "last edited" (feed `<updated>`, sitemap
6666
+ * `lastmod`) and Thread order (the root, then replies by creation time, then
6667
+ * ID) survive the move.
6668
+ * Omitted, both are the time of the request; `updatedAt` alone defaults to
6669
+ * `createdAt`.
6670
+ */ var RestoredTimestampFields = {
6671
+ createdAt: z.number().int().positive().optional(),
6672
+ updatedAt: z.number().int().positive().optional()
6673
+ };
6674
+ refineSlugPathExclusivity(refineCreatePostFormatShape(refineBodyExclusivity(PostFieldsSchema.extend(RestoredTimestampFields))));
6675
+ var CreatePostApiSchema = refineSlugPathExclusivity(refineCreatePostFormatShape(refineBodyExclusivity(ApiPostFieldsSchema.extend(RestoredTimestampFields))));
6661
6676
  /**
6662
6677
  * API request body schema for creating a thread (multiple chained posts atomically).
6663
6678
  * posts[0] is the root; subsequent posts are sequential replies.
@@ -6926,6 +6941,19 @@ function normalizeEditableSettingValue(key, value) {
6926
6941
  * schema by reading the stored name rather than trusting a field.
6927
6942
  */ var SetupSiteSchema = SetupLanguageSchema.extend({ siteName: z.string({ error: "Site name is required" }).min(1, "Site name is required") });
6928
6943
  /**
6944
+ * Both setup screens' answers at once, for `jant setup`: a self-hosted install
6945
+ * set up by its deployment rather than in a browser.
6946
+ *
6947
+ * Only the credentials are required. Everything the second screen asks has
6948
+ * the default an empty answer gets there, and a browser's guesses — its
6949
+ * language, its time zone — have no counterpart here.
6950
+ */ var InstanceSetupSchema = SetupAccountSchema.extend({
6951
+ siteId: createTypeIdSchema(ID_PREFIX.site).optional(),
6952
+ siteName: z.string().trim().min(1, "Site name cannot be empty").max(120).optional(),
6953
+ siteLanguage: ContentLanguageSchema.optional(),
6954
+ timeZone: z.string().refine(isSupportedTimeZone, "Choose a valid time zone.").transform(normalizeTimeZone).optional()
6955
+ });
6956
+ /**
6929
6957
  * Sign-in form validation schema
6930
6958
  */ var SigninSchema = z.object({
6931
6959
  email: z.string().transform(normalizeEmail).pipe(z.string().email("Invalid email address")),
@@ -7669,7 +7697,9 @@ var DISCOVER_SETTINGS = [
7669
7697
  * than on the checkbox label. The settings page and the setup screen both
7670
7698
  * render their line through here, so the two can never link different words;
7671
7699
  * setup's line is the settings line's opening sentence, so only the name is
7672
- * there to find.
7700
+ * there to find. The notice that replaces the settings line while the box is
7701
+ * locked in demo mode goes through here too, and it also names only the
7702
+ * directory.
7673
7703
  *
7674
7704
  * @param intro - The translated help line
7675
7705
  * @param terms - The translated directory and rules-page names, as `intro`
@@ -9209,7 +9239,7 @@ function clipPreviewText(text, maxChars) {
9209
9239
  * @returns Render-ready PostView with pre-computed fields
9210
9240
  */ function toPostView(post, ctx, threadCollections, isLastInThread, aliasPath, pinnedInCollection) {
9211
9241
  const id = post.id;
9212
- const permalink = toPublicPath(aliasPath ?? `/${post.slug}`, ctx.sitePathPrefix);
9242
+ const permalink = toPublicPath(getPostPath(post.slug, aliasPath), ctx.sitePathPrefix);
9213
9243
  const timeZone = ctx.timeZone ?? "UTC";
9214
9244
  const publishedAt = post.publishedAt ?? post.updatedAt;
9215
9245
  const featuredAt = post.featuredAt;
@@ -11289,7 +11319,7 @@ async function buildCuratedThreadItems(c, rootIds, threadsByRootId, display) {
11289
11319
  const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl, mediaCtx.localPublicUrl, mediaCtx.sitePathPrefix);
11290
11320
  return orderedThreads.reduce((items, thread) => {
11291
11321
  const rootEntry = thread.posts[0];
11292
- if (!rootEntry || rootEntry.position !== 0) return items;
11322
+ if (!rootEntry || rootEntry.post.id !== rootEntry.post.threadId) return items;
11293
11323
  const lastPostId = thread.posts[thread.posts.length - 1]?.post.id;
11294
11324
  const renderedPosts = thread.posts.map(({ post, position }) => ({
11295
11325
  position,
@@ -13542,9 +13572,9 @@ var TimelineItemFromPost = ({ post, mode, display }) => {
13542
13572
  *
13543
13573
  * Takes the same slices the SQL path ranks for: the first
13544
13574
  * `THREAD_LEADING_REPLIES`, and the last `THREAD_TRAILING_REPLIES` with the
13545
- * newest as the hero. `getPublishedThreads` orders replies by
13546
- * `(threadId, createdAt, id)` ascending, which is the window function's own
13547
- * `ORDER BY`, so position in this array *is* the rank — no approximation.
13575
+ * newest as the hero. `getPublishedThreads` returns each Thread in Thread
13576
+ * order, the same `threadOrder` the window functions rank by, so once the root
13577
+ * is set aside position in this array *is* the rank — no approximation.
13548
13578
  *
13549
13579
  * @param replies - Every published reply, oldest first
13550
13580
  * @returns The fold, or null when the thread has no replies
@@ -16097,18 +16127,20 @@ var archiveRoutes = new Hono();
16097
16127
  const { params, collections: preloadedCollections } = await parseArchiveParams(c, queryOverrides);
16098
16128
  if (queryOverrides && archiveQueryRequiresAuth(params) && !c.var.isAuthenticated) return c.notFound();
16099
16129
  if (params.collectionMissing) return c.notFound();
16100
- const navData = await getNavigationData(c);
16130
+ const navDataPromise = getNavigationData(c);
16131
+ const isAuthenticated = c.var.isAuthenticated;
16101
16132
  const filters = buildArchivePostFilters(params, {
16102
- isAuthenticated: navData.isAuthenticated,
16133
+ isAuthenticated,
16103
16134
  lang: getViewLang(c) ?? void 0
16104
16135
  });
16105
16136
  const isListView = (params.layout ?? appConfig.archiveDefaultLayout) === "list";
16106
16137
  const baselineFilters = hasActiveArchiveFilter(params.selection) ? buildArchivePostFilters(params, {
16107
- isAuthenticated: navData.isAuthenticated,
16138
+ isAuthenticated,
16108
16139
  lang: filters.lang,
16109
16140
  selection: {}
16110
16141
  }) : void 0;
16111
- const [totalCount, baselineCount, monthlyCounts, posts, availableYears, listedCollections] = await Promise.all([
16142
+ const [navData, totalCount, baselineCount, monthlyCounts, posts, availableYears, listedCollections] = await Promise.all([
16143
+ navDataPromise,
16112
16144
  services.posts.count(filters),
16113
16145
  baselineFilters ? services.posts.count(baselineFilters) : Promise.resolve(void 0),
16114
16146
  isListView ? Promise.resolve([]) : services.posts.countByYearMonth(filters),
@@ -16129,10 +16161,6 @@ var archiveRoutes = new Hono();
16129
16161
  const dimensionCtx = { collections: buildCollectionVocabulary(allCollections) };
16130
16162
  const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
16131
16163
  const mediaCtx = createMediaContext(appConfig);
16132
- const allPostIds = posts.map((p) => p.id);
16133
- const archiveAliasesMap = await c.var.services.paths.getPostAliases(allPostIds);
16134
- const archiveAliasMap = /* @__PURE__ */ new Map();
16135
- for (const [id, aliases] of archiveAliasesMap) if (aliases[0]) archiveAliasMap.set(id, aliases[0]);
16136
16164
  let groups = [];
16137
16165
  let flatItems;
16138
16166
  if (isListView) flatItems = await assembleTimelineItems(c, posts);
@@ -16149,7 +16177,13 @@ var archiveRoutes = new Hono();
16149
16177
  }
16150
16178
  const monthlyCountMap = new Map(monthlyCounts.map((row) => [row.yearMonth, row.count]));
16151
16179
  const postIds = posts.map((p) => p.id);
16152
- const [rawMediaMap, replyCounts] = await Promise.all([services.media.getByPostIds(postIds), services.posts.getReplyCounts(postIds)]);
16180
+ const [rawMediaMap, replyCounts, archiveAliasesMap] = await Promise.all([
16181
+ services.media.getByPostIds(postIds),
16182
+ services.posts.getReplyCounts(postIds),
16183
+ services.paths.getPostAliases(postIds)
16184
+ ]);
16185
+ const archiveAliasMap = /* @__PURE__ */ new Map();
16186
+ for (const [id, aliases] of archiveAliasesMap) if (aliases[0]) archiveAliasMap.set(id, aliases[0]);
16153
16187
  const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl, mediaCtx.localPublicUrl, mediaCtx.sitePathPrefix);
16154
16188
  for (const [key, monthPosts] of grouped) grouped.set(key, monthPosts.map((post) => ({
16155
16189
  ...post,
@@ -18339,15 +18373,15 @@ feedRoutes.get("/all/atom.xml", (c) => {
18339
18373
  * @param c - Hono context
18340
18374
  * @returns Featured page response
18341
18375
  */ async function renderFeaturedPage(c) {
18342
- const navData = await getNavigationData(c);
18376
+ const navDataPromise = getNavigationData(c);
18343
18377
  const i18n = getI18n(c);
18344
18378
  const page = parsePageNumber(c.req.query("page"));
18345
18379
  const featuredTitle = i18n._({ id: "FkMol5" });
18346
18380
  const paginatedPageTitle = formatPageLabel(page);
18347
- const { items, currentPage, totalPages } = await assembleFeaturedTimeline(c, {
18381
+ const [navData, { items, currentPage, totalPages }] = await Promise.all([navDataPromise, assembleFeaturedTimeline(c, {
18348
18382
  page,
18349
- isAuthenticated: navData.isAuthenticated
18350
- });
18383
+ isAuthenticated: c.var.isAuthenticated
18384
+ })]);
18351
18385
  return renderPublicPage(c, {
18352
18386
  title: page > 1 ? buildPageTitle(featuredTitle, paginatedPageTitle, navData.siteName) : buildPageTitle(featuredTitle, navData.siteName),
18353
18387
  alternateLanguages: buildSurfaceAlternates(c),
@@ -21390,7 +21424,8 @@ function createMediaService(db, siteId, databaseSchema = sqliteSchemaBundle, dat
21390
21424
  const limit = filters?.limit ?? 100;
21391
21425
  const conditions = [eq(media.siteId, siteId)];
21392
21426
  if (filters?.mimePrefix) conditions.push(sql`${media.mimeType} LIKE ${filters.mimePrefix + "%"}`);
21393
- return (await db.select().from(media).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(media.createdAt)).limit(limit)).map(toMedia);
21427
+ if (filters?.cursor) conditions.push(lt(media.id, filters.cursor));
21428
+ return (await db.select().from(media).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(media.id)).limit(limit)).map(toMedia);
21394
21429
  },
21395
21430
  async validateIds(ids) {
21396
21431
  if (ids.length === 0) return;
@@ -22092,10 +22127,10 @@ function readSort(value, fallback, showRatingSort) {
22092
22127
  * content at every reply URL. Point the canonical to the thread root so
22093
22128
  * crawlers consolidate ranking on one URL.
22094
22129
  *
22095
- * The root post is always at index 0 of `threadPostViews` (getThread orders
22096
- * by createdAt ASC, and the DB check constraint guarantees root has the
22097
- * smallest createdAt in its thread). When `threadPostViews` is undefined the
22098
- * post is not part of a multi-post thread, so the post itself is the root.
22130
+ * The root post is always at index 0 of `threadPostViews`: Thread order puts
22131
+ * the root first even when a reply is older than it. When `threadPostViews` is
22132
+ * undefined the post is not part of a multi-post thread, so the post itself is
22133
+ * the root.
22099
22134
  */ function buildPostCanonicalHref(postView, threadPostViews, siteUrl) {
22100
22135
  const rootPermalink = threadPostViews?.[0]?.permalink ?? postView.permalink;
22101
22136
  if (!siteUrl) return rootPermalink;
@@ -22300,6 +22335,22 @@ pageRoutes.get("/preview/:slug", requireAuth(), async (c) => {
22300
22335
  });
22301
22336
  });
22302
22337
  /**
22338
+ * Resolve a stored path, reusing the lookup the stored-redirect middleware
22339
+ * already made for this request's own address.
22340
+ *
22341
+ * Only that one address is reusable: a `/feed` suffix or a text deep link asks
22342
+ * about a different path, and a language view strips its prefix before asking,
22343
+ * so the stored form is compared rather than assumed.
22344
+ *
22345
+ * @param c - Hono context
22346
+ * @param storedPath - Registry path form, without a leading slash
22347
+ * @returns The resolved record, or null when nothing is registered there
22348
+ */ async function resolveStoredPath(c, storedPath) {
22349
+ const lookup = c.var.pathLookup;
22350
+ if (lookup && lookup.path === normalizePath(storedPath)) return lookup.record;
22351
+ return c.var.services.paths.resolve(storedPath);
22352
+ }
22353
+ /**
22303
22354
  * Resolve a path through the path registry: posts, collections, their aliases,
22304
22355
  * archive URLs, stored redirects, and text-attachment deep links.
22305
22356
  *
@@ -22316,7 +22367,7 @@ pageRoutes.get("/preview/:slug", requireAuth(), async (c) => {
22316
22367
  const sitePathPrefix = c.var.appConfig.sitePathPrefix;
22317
22368
  const inLanguageView = isPrefixedLanguageView(c);
22318
22369
  if (isReservedPath(fullPath)) return c.notFound();
22319
- const resolved = await c.var.services.paths.resolve(fullPath);
22370
+ const resolved = await resolveStoredPath(c, fullPath);
22320
22371
  if (resolved?.kind === "redirect" && resolved.redirectToPath) {
22321
22372
  const target = `/${resolved.redirectToPath}`;
22322
22373
  return c.redirect(toPublicHref(target) === target ? toViewPath(c, target) : toPublicHref(target, sitePathPrefix), resolved.redirectType ?? 301);
@@ -22348,15 +22399,11 @@ pageRoutes.get("/preview/:slug", requireAuth(), async (c) => {
22348
22399
  if (!slugPart || !mediaId) return c.notFound();
22349
22400
  const resolvedPost = await c.var.services.paths.resolve(slugPart);
22350
22401
  if (resolvedPost?.postId) {
22351
- const post = await c.var.services.posts.getById(resolvedPost.postId);
22402
+ const loaded = await c.var.services.posts.getWithCanonicalAlias(resolvedPost.postId);
22403
+ const post = loaded?.post;
22352
22404
  if (!post || post.status === "draft") return c.notFound();
22353
- if (post.visibility === "private") {
22354
- if (!(await getNavigationData(c)).isAuthenticated) return c.notFound();
22355
- }
22356
- if (resolvedPost.kind === "slug") {
22357
- const alias = await c.var.services.customUrls.getByTarget("post", post.id);
22358
- if (alias) return c.redirect(toPublicPath(`/${alias.path}/text/${mediaId}`, sitePathPrefix), 301);
22359
- }
22405
+ if (post.visibility === "private" && !c.var.isAuthenticated) return c.notFound();
22406
+ if (resolvedPost.kind === "slug" && loaded.canonicalAlias) return c.redirect(toPublicPath(`${loaded.canonicalAlias}/text/${mediaId}`, sitePathPrefix), 301);
22360
22407
  if (inLanguageView) return c.redirect(toPublicPath(`/${fullPath}`, sitePathPrefix), 301);
22361
22408
  const media = await c.var.services.media.getById(mediaId);
22362
22409
  if (!media || media.postId !== post.id || !isTextAttachment(media)) return c.notFound();
@@ -22375,18 +22422,13 @@ pageRoutes.get("/preview/:slug", requireAuth(), async (c) => {
22375
22422
  if (!resolved) return c.notFound();
22376
22423
  if (resolved.kind === "archive" && resolved.archiveQuery) return renderArchivePage(c, Object.fromEntries(new URLSearchParams(resolved.archiveQuery)));
22377
22424
  if (resolved.postId) {
22378
- const post = await c.var.services.posts.getById(resolved.postId);
22425
+ const loaded = await c.var.services.posts.getWithCanonicalAlias(resolved.postId);
22426
+ const post = loaded?.post;
22379
22427
  if (!post) return c.notFound();
22380
22428
  const allowDraft = canRenderDraftAboutEditor(c, fullPath, post);
22381
22429
  if (post.status === "draft" && !allowDraft) return c.notFound();
22382
- if (post.visibility === "private") {
22383
- if (!(await getNavigationData(c)).isAuthenticated) return c.notFound();
22384
- }
22385
- let canonicalPath = `/${fullPath}`;
22386
- if (resolved.kind === "slug") {
22387
- const alias = await c.var.services.customUrls.getByTarget("post", post.id);
22388
- if (alias) canonicalPath = `/${alias.path}`;
22389
- }
22430
+ if (post.visibility === "private" && !c.var.isAuthenticated) return c.notFound();
22431
+ const canonicalPath = resolved.kind === "slug" && loaded.canonicalAlias ? loaded.canonicalAlias : `/${fullPath}`;
22390
22432
  if (inLanguageView || canonicalPath !== `/${fullPath}`) return c.redirect(toPublicPath(canonicalPath, sitePathPrefix), 301);
22391
22433
  return renderPost(c, post, { allowDraft });
22392
22434
  }
@@ -22625,7 +22667,7 @@ var SearchPage = ({ query, results, error, hasMore, page, basePath = "", isAuthe
22625
22667
  const query = c.req.query("q") || "";
22626
22668
  const pageParam = c.req.query("page");
22627
22669
  const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
22628
- const navData = await getNavigationData(c);
22670
+ const navDataPromise = getNavigationData(c);
22629
22671
  let results = [];
22630
22672
  let error;
22631
22673
  let hasMore = false;
@@ -22645,7 +22687,11 @@ var SearchPage = ({ query, results, error, hasMore, page, basePath = "", isAuthe
22645
22687
  }
22646
22688
  const mediaCtx = createMediaContext(c.var.appConfig);
22647
22689
  const postIds = results.map((r) => r.post.id);
22648
- const [aliasesMap, collectionsMap] = await Promise.all([c.var.services.paths.getPostAliases(postIds), c.var.services.collections.getCollectionsByPostIds(postIds)]);
22690
+ const [navData, aliasesMap, collectionsMap] = await Promise.all([
22691
+ navDataPromise,
22692
+ c.var.services.paths.getPostAliases(postIds),
22693
+ c.var.services.collections.getCollectionsByPostIds(postIds)
22694
+ ]);
22649
22695
  const aliasMap = /* @__PURE__ */ new Map();
22650
22696
  for (const [id, aliases] of aliasesMap) if (aliases[0]) aliasMap.set(id, aliases[0]);
22651
22697
  const resultViews = toSearchResultViews(results, mediaCtx, query, aliasMap, collectionsMap);
@@ -25786,7 +25832,7 @@ function GeneralContent({ siteName, siteDescription, siteNameFallback, siteDescr
25786
25832
  discoverSearchOff: i18n._({ id: "bph1Dd" }),
25787
25833
  discoverAnnounce: i18n._({ id: "B5P9HE" }),
25788
25834
  discoverAnnounceManual: i18n._({ id: "dwcK1n" }),
25789
- discoverDemoLocked: i18n._({ id: "Hf/w/z" }),
25835
+ discoverDemoLocked: i18n._({ id: "1pD+7W" }, { name: discoverCopy.name }),
25790
25836
  discoverFeedsOffLocked: i18n._({ id: "eeamrQ" }),
25791
25837
  save: i18n._({ id: "tfDRzk" }),
25792
25838
  cancel: i18n._({ id: "dEgA5A" }),
@@ -28706,8 +28752,49 @@ function createHostedControlPlaneClient(env, fetchImpl = fetch) {
28706
28752
  * ENV > Default (for envOnly fields)
28707
28753
  */
28708
28754
  /**
28755
+ * Whether an empty stored value means "never configured" for this field.
28756
+ *
28757
+ * The test is whether the editor could have produced the empty value, and it
28758
+ * mirrors what `normalizeConfigEditorDefinitionValue` accepts. A boolean takes
28759
+ * only `true` or `false`; a number has to parse; an enum has to name one of its
28760
+ * options. None of those can be stored empty, so an empty row for one of them
28761
+ * is not a choice anybody made — it comes from a path that skips validation,
28762
+ * and a snapshot's `db.sql`, replayed as raw SQL, is exactly that. Read as a
28763
+ * configured value it silences the environment variable the operator did set,
28764
+ * for as long as the row exists.
28765
+ *
28766
+ * Text is the opposite: clearing a description or a footer is an ordinary edit,
28767
+ * so an empty value there is a decision and keeps its precedence. So is an enum
28768
+ * that lists `""` among its options, the way `DASHBOARD_LANGUAGE` does, and one
28769
+ * whose options come from a runtime source, where nothing here can rule it
28770
+ * out.
28771
+ *
28772
+ * @param field - Config registry entry for the key being resolved
28773
+ * @returns True when an empty stored value should fall through to env/default
28774
+ *
28775
+ * @example
28776
+ * ```ts
28777
+ * isUnsetWhenEmpty(CONFIG_FIELDS.NOINDEX); // true (boolean)
28778
+ * isUnsetWhenEmpty(CONFIG_FIELDS.SITE_FOOTER); // false (string)
28779
+ * ```
28780
+ */ function isUnsetWhenEmpty(field) {
28781
+ if (!("editor" in field)) return false;
28782
+ const editor = field.editor;
28783
+ switch (editor.type) {
28784
+ case "boolean":
28785
+ case "number": return true;
28786
+ case "enum": return "options" in editor && !editor.options.includes("");
28787
+ default: return false;
28788
+ }
28789
+ }
28790
+ /**
28709
28791
  * Resolve a single config value following priority rules.
28710
28792
  *
28793
+ * Settings saved in the dashboard outrank the environment, which outranks the
28794
+ * default. The exception is an empty boolean or numeric value — see
28795
+ * {@link isUnsetWhenEmpty}, which explains why that is absence rather than
28796
+ * choice.
28797
+ *
28711
28798
  * @param key - CONFIG_FIELDS key
28712
28799
  * @param allSettings - DB settings map
28713
28800
  * @param env - Worker bindings
@@ -28716,7 +28803,10 @@ function createHostedControlPlaneClient(env, fetchImpl = fetch) {
28716
28803
  const field = CONFIG_FIELDS[key];
28717
28804
  if (!field) return "";
28718
28805
  const envKeys = "envKeys" in field ? field.envKeys : void 0;
28719
- if (!field.envOnly && Object.hasOwn(allSettings, key)) return allSettings[key] ?? "";
28806
+ if (!field.envOnly && Object.hasOwn(allSettings, key)) {
28807
+ const stored = allSettings[key] ?? "";
28808
+ if (stored !== "" || !isUnsetWhenEmpty(field)) return stored;
28809
+ }
28720
28810
  const envValue = getEnvString(env, ...envKeys ?? []);
28721
28811
  if (envValue) return envValue;
28722
28812
  if (field.defaultValue) return field.defaultValue;
@@ -29030,8 +29120,8 @@ async function syncHostedControlPlaneSiteAvatar(input) {
29030
29120
  return;
29031
29121
  }
29032
29122
  await markSyncPending(settings);
29033
- const { createGitHubSyncService } = await import("./github-sync-BPAvT999.js").then((n) => n.r);
29034
- const { getGitHubAppConfig } = await import("./github-sync-BPAvT999.js").then((n) => n.B);
29123
+ const { createGitHubSyncService } = await import("./github-sync-Gw4orAHk.js").then((n) => n.r);
29124
+ const { getGitHubAppConfig } = await import("./github-sync-Gw4orAHk.js").then((n) => n.V);
29035
29125
  const run = runBackgroundSync(settings, createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
29036
29126
  storage: c.var.storage,
29037
29127
  githubApp: getGitHubAppConfig(c.env)
@@ -29396,6 +29486,18 @@ function aboutEditPath(c) {
29396
29486
  return sessionId;
29397
29487
  }
29398
29488
  /**
29489
+ * The user behind the current request.
29490
+ *
29491
+ * Same contract as `requireSessionId`: `requireAuth` has already proven the
29492
+ * session belongs to a member of this site, so an absent user is a wiring
29493
+ * mistake. Used wherever a decision is scoped to the person rather than to
29494
+ * the site — the GitHub App installations they may reuse, most of all.
29495
+ */ function requireSessionUserId(c) {
29496
+ const userId = c.var.session?.user?.id;
29497
+ if (!userId) throw new UnauthorizedError();
29498
+ return userId;
29499
+ }
29500
+ /**
29399
29501
  * Breadcrumb labels for admin settings pages.
29400
29502
  *
29401
29503
  * These duplicate labels defined in the corresponding UI components (e.g.
@@ -30230,7 +30332,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
30230
30332
  if (getGitHubAppConfig(c.env)) return dsToast("This deployment uses GitHub App authentication. Use Install GitHub App instead.", "error");
30231
30333
  const body = await c.req.json();
30232
30334
  if (!body.token?.trim() || !body.repo?.trim()) return dsToast("Token and repository are required.", "error");
30233
- const { parseRepoSlug, createGitHubClient } = await import("./github-sync-BPAvT999.js").then((n) => n.a);
30335
+ const { parseRepoSlug, createGitHubClient } = await import("./github-sync-Gw4orAHk.js").then((n) => n.a);
30234
30336
  const parsed = parseRepoSlug(body.repo);
30235
30337
  if (!parsed) return dsToast("Invalid repository format. Use owner/repo.", "error");
30236
30338
  const client = createGitHubClient(body.token);
@@ -30249,7 +30351,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
30249
30351
  await c.var.services.settings.set("GITHUB_SYNC_AUTH_MODE", "pat");
30250
30352
  await c.var.services.settings.set("GITHUB_SYNC_APP_INSTALLATION_ID", "");
30251
30353
  await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
30252
- const { createGitHubSyncService } = await import("./github-sync-BPAvT999.js").then((n) => n.r);
30354
+ const { createGitHubSyncService } = await import("./github-sync-Gw4orAHk.js").then((n) => n.r);
30253
30355
  const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
30254
30356
  storage: c.var.storage,
30255
30357
  githubApp: getGitHubAppConfig(c.env)
@@ -30268,7 +30370,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
30268
30370
  return dsRedirect(publicPath(c, "/settings/github-sync"));
30269
30371
  });
30270
30372
  settingsRoutes.post("/github-sync/push", async (c) => {
30271
- const { createGitHubSyncService } = await import("./github-sync-BPAvT999.js").then((n) => n.r);
30373
+ const { createGitHubSyncService } = await import("./github-sync-Gw4orAHk.js").then((n) => n.r);
30272
30374
  const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
30273
30375
  storage: c.var.storage,
30274
30376
  githubApp: getGitHubAppConfig(c.env)
@@ -30288,11 +30390,39 @@ settingsRoutes.post("/github-sync/push", async (c) => {
30288
30390
  });
30289
30391
  });
30290
30392
  settingsRoutes.post("/github-sync/disconnect", async (c) => {
30291
- const { createGitHubSyncService } = await import("./github-sync-BPAvT999.js").then((n) => n.r);
30393
+ const { createGitHubSyncService } = await import("./github-sync-Gw4orAHk.js").then((n) => n.r);
30292
30394
  await createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), { githubApp: getGitHubAppConfig(c.env) }).teardownWebhook();
30293
30395
  return dsRedirect(publicPath(c, "/settings/github-sync"));
30294
30396
  });
30295
30397
  /**
30398
+ * Recreate installation bindings for sites that sync through the App
30399
+ * but have no row to prove it.
30400
+ *
30401
+ * GitHub is the only source for the account behind an installation id,
30402
+ * so the repair costs one call per distinct id — bounded by how many
30403
+ * sites the author has, and only ever reached when they would otherwise
30404
+ * be shown a GitHub page that cannot lead anywhere. Ids GitHub no longer
30405
+ * recognizes are skipped: an uninstalled App has nothing to rebuild.
30406
+ */ async function rebuildInstallationBindings(c, app, userId) {
30407
+ const syncing = await c.var.services.githubAppInstallations.listSyncingInstallationsForUser(userId);
30408
+ if (syncing.length === 0) return;
30409
+ const accounts = /* @__PURE__ */ new Map();
30410
+ for (const { installationId } of syncing) {
30411
+ if (accounts.has(installationId)) continue;
30412
+ try {
30413
+ const installation = await getInstallation(app, installationId);
30414
+ accounts.set(installationId, installation.account);
30415
+ } catch {
30416
+ continue;
30417
+ }
30418
+ }
30419
+ for (const { siteId, installationId } of syncing) {
30420
+ const account = accounts.get(installationId);
30421
+ if (!account) continue;
30422
+ await c.var.services.githubAppInstallations.upsertInstallation(installationId, siteId, account);
30423
+ }
30424
+ }
30425
+ /**
30296
30426
  * Redirect the user to GitHub to install the App on their account/org.
30297
30427
  *
30298
30428
  * Only available when GitHub App env vars are configured. Uses a signed
@@ -30301,11 +30431,17 @@ settingsRoutes.post("/github-sync/disconnect", async (c) => {
30301
30431
  const app = getGitHubAppConfig(c.env);
30302
30432
  if (!app) return c.text("GitHub App is not configured on this deployment.", 404);
30303
30433
  if (!(c.req.query("force") === "new")) {
30304
- if ((await c.var.services.githubAppInstallations.listInstallationsForSite(c.var.currentSite.id)).length > 0) {
30434
+ const userId = requireSessionUserId(c);
30435
+ let existing = await c.var.services.githubAppInstallations.listInstallationsForUser(userId);
30436
+ if (existing.length === 0) {
30437
+ await rebuildInstallationBindings(c, app, userId);
30438
+ existing = await c.var.services.githubAppInstallations.listInstallationsForUser(userId);
30439
+ }
30440
+ if (existing.length > 0) {
30305
30441
  const navData = await getNavigationData(c);
30306
30442
  const base = publicPath(c, "/settings/github-sync");
30307
- const labels = buildRepoPickerLabels(c);
30308
- const suggestedRepoName = buildSuggestedRepoName(c);
30443
+ const suggestedRepoName = suggestSyncRepoName(c.var.appConfig.siteUrl);
30444
+ const labels = buildRepoPickerLabels(c, suggestedRepoName);
30309
30445
  return renderPublicPage(c, {
30310
30446
  title: buildPageTitle("GitHub Sync — Pick Repository", navData.siteName),
30311
30447
  navData,
@@ -30366,20 +30502,22 @@ settingsRoutes.post("/github-sync/disconnect", async (c) => {
30366
30502
  const currentHost = new URL(c.var.appConfig.siteUrl).host;
30367
30503
  if (!payload || payload.host !== currentHost) return c.text("State signature invalid for this host.", 400);
30368
30504
  }
30505
+ try {
30506
+ const installation = await getInstallation(app, installationId);
30507
+ await c.var.services.githubAppInstallations.upsertInstallation(installationId, c.var.currentSite.id, installation.account);
30508
+ } catch {
30509
+ return c.text("The App was installed, but GitHub did not answer with the account details. Reload this page to try again.", 502);
30510
+ }
30369
30511
  setCookie(c, "jant_gh_app_state", "", {
30370
30512
  httpOnly: true,
30371
30513
  sameSite: "Lax",
30372
30514
  path: "/",
30373
30515
  maxAge: 0
30374
30516
  });
30375
- try {
30376
- const installation = await getInstallation(app, installationId);
30377
- await c.var.services.githubAppInstallations.upsertInstallation(installationId, c.var.currentSite.id, installation.account);
30378
- } catch {}
30379
30517
  const navData = await getNavigationData(c);
30380
30518
  const base = publicPath(c, "/settings/github-sync");
30381
- const labels = buildRepoPickerLabels(c);
30382
- const suggestedRepoName = buildSuggestedRepoName(c);
30519
+ const suggestedRepoName = suggestSyncRepoName(c.var.appConfig.siteUrl);
30520
+ const labels = buildRepoPickerLabels(c, suggestedRepoName);
30383
30521
  return renderPublicPage(c, {
30384
30522
  title: buildPageTitle("GitHub Sync — Pick Repository", navData.siteName),
30385
30523
  navData,
@@ -30391,7 +30529,7 @@ settingsRoutes.post("/github-sync/disconnect", async (c) => {
30391
30529
  labels,
30392
30530
  "api-base": `${base}/app`,
30393
30531
  "connect-url": `${base}/app/connect`,
30394
- "install-url": `${base}/app/install`,
30532
+ "install-url": `${base}/app/install?force=new`,
30395
30533
  "cancel-url": publicPath(c, "/settings"),
30396
30534
  "create-repo-name-hint": suggestedRepoName,
30397
30535
  children: /*#__PURE__*/ jsxDEV$1("div", {
@@ -30408,22 +30546,30 @@ settingsRoutes.post("/github-sync/disconnect", async (c) => {
30408
30546
  });
30409
30547
  });
30410
30548
  /**
30411
- * Derive a default repository name to prefill on github.com/new.
30549
+ * Values that keep a placeholder literal for the client to fill in.
30412
30550
  *
30413
- * Uses the site's host — the first DNS label is a stable, URL-safe
30414
- * identifier tied to this specific Jant instance. Fallback to
30415
- * "jant-site-sync" when the host parse fails so we never hand GitHub an
30416
- * empty `name=`. The `-jant-sync` suffix disambiguates the sync mirror
30417
- * from a user's own `{slug}-jant` source repo.
30418
- */ function buildSuggestedRepoName(c) {
30419
- let firstLabel = "";
30420
- try {
30421
- firstLabel = new URL(c.var.appConfig.siteUrl).host.split(".")[0] ?? "";
30422
- } catch {}
30423
- const slug = firstLabel.toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "");
30424
- return slug ? `${slug}-jant-sync` : "jant-site-sync";
30551
+ * `i18n._()` renders a placeholder it has no value for as an empty string, so
30552
+ * a label whose value only exists in the browser — a repo count, the picked
30553
+ * repo — must be formatted with the placeholder's own token as its value.
30554
+ * `{repo}` then survives into the serialized labels and the component's
30555
+ * `.replace("{repo}", …)` finds it.
30556
+ *
30557
+ * @param names - Placeholder names the client interpolates.
30558
+ * @returns A values object mapping each name to its own `{name}` token.
30559
+ * @example
30560
+ * i18n._(msg({ message: "Type {repo} to confirm" }), keepForClient("repo"));
30561
+ * // "Type {repo} to confirm"
30562
+ */ function keepForClient(...names) {
30563
+ return Object.fromEntries(names.map((name) => [name, `{${name}}`]));
30425
30564
  }
30426
- function buildRepoPickerLabels(c) {
30565
+ /**
30566
+ * Serialize the picker's strings for the client component.
30567
+ *
30568
+ * @param c - Request context, for the active locale.
30569
+ * @param suggestedRepoName - Repository name prefilled on github.com/new;
30570
+ * interpolated here because the client cannot format ICU messages.
30571
+ * @returns The labels as a JSON string for the `labels` attribute.
30572
+ */ function buildRepoPickerLabels(c, suggestedRepoName) {
30427
30573
  const i18n = getI18n(c);
30428
30574
  return JSON.stringify({
30429
30575
  pageTitle: i18n._({ id: "KSgo21" }),
@@ -30438,19 +30584,19 @@ function buildRepoPickerLabels(c) {
30438
30584
  repoSearchPlaceholder: i18n._({ id: "gZ5owP" }),
30439
30585
  repoEmpty: i18n._({ id: "zSURJW" }),
30440
30586
  repoLoading: i18n._({ id: "Pwqkdw" }),
30441
- repoShowingOf: i18n._({ id: "35x8eZ" }),
30587
+ repoShowingOf: i18n._({ id: "35x8eZ" }, keepForClient("shown", "total")),
30442
30588
  repoSearchHint: i18n._({ id: "5dpcN1" }),
30443
30589
  refreshRepos: i18n._({ id: "Xtc16w" }),
30444
30590
  createOnGitHub: i18n._({ id: "d5oGUo" }),
30445
- createOnGitHubHint: i18n._({ id: "oH2JHg" }),
30591
+ createOnGitHubHint: i18n._({ id: "Y4oXje" }, { name: suggestedRepoName }),
30446
30592
  classifyLoading: i18n._({ id: "XtBJV8" }),
30447
30593
  classificationEmpty: i18n._({ id: "5QlUIt" }),
30448
30594
  classificationOwned: i18n._({ id: "7811AW" }),
30449
- classificationOwnedByOther: i18n._({ id: "r5EW6f" }),
30595
+ classificationOwnedByOther: i18n._({ id: "r5EW6f" }, keepForClient("host")),
30450
30596
  classificationForeign: i18n._({ id: "6NpNLc" }),
30451
30597
  confirmHeading: i18n._({ id: "CjZZgz" }),
30452
- confirmBody: i18n._({ id: "+9JI/F" }),
30453
- confirmInputLabel: i18n._({ id: "hdSi1b" }),
30598
+ confirmBody: i18n._({ id: "+9JI/F" }, keepForClient("repo")),
30599
+ confirmInputLabel: i18n._({ id: "hdSi1b" }, keepForClient("repo")),
30454
30600
  confirmInputPlaceholder: i18n._({ id: "oGC9uP" }),
30455
30601
  cancel: i18n._({ id: "dEgA5A" }),
30456
30602
  connect: i18n._({ id: "iSLIjg" }),
@@ -30476,10 +30622,12 @@ function buildRepoPickerLabels(c) {
30476
30622
  const repo = String(body.repo ?? "").trim();
30477
30623
  const confirmForeign = body.confirmForeign === true || body.confirmForeign === "true";
30478
30624
  if (!installationId || !repo) return wantsJson ? c.json({ error: "Missing installationId or repo." }, 400) : c.text("Missing installationId or repo.", 400);
30479
- const { parseRepoSlug, createGitHubClient } = await import("./github-sync-BPAvT999.js").then((n) => n.a);
30625
+ const installation = await findVisibleInstallation(c, installationId);
30626
+ if (!installation) return wantsJson ? c.json({ error: "Unknown installation." }, 404) : c.text("Unknown installation.", 404);
30627
+ const { parseRepoSlug, createGitHubClient } = await import("./github-sync-Gw4orAHk.js").then((n) => n.a);
30480
30628
  const parsed = parseRepoSlug(repo);
30481
30629
  if (!parsed) return wantsJson ? c.json({ error: "Invalid repository format." }, 400) : c.text("Invalid repository format.", 400);
30482
- const { classifyRepoForSync } = await import("./github-sync-BPAvT999.js").then((n) => n.r);
30630
+ const { classifyRepoForSync } = await import("./github-sync-Gw4orAHk.js").then((n) => n.r);
30483
30631
  const ghClient = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
30484
30632
  let classification;
30485
30633
  try {
@@ -30504,12 +30652,13 @@ function buildRepoPickerLabels(c) {
30504
30652
  defaultBranch: classification.defaultBranch
30505
30653
  }, 409) : c.text(msg, 409);
30506
30654
  }
30655
+ await c.var.services.githubAppInstallations.upsertInstallation(installationId, c.var.currentSite.id, installation.account);
30507
30656
  await c.var.services.settings.set("GITHUB_SYNC_AUTH_MODE", "app");
30508
30657
  await c.var.services.settings.set("GITHUB_SYNC_APP_INSTALLATION_ID", installationId);
30509
30658
  await c.var.services.settings.set("GITHUB_SYNC_REPO", repo);
30510
30659
  await c.var.services.settings.set("GITHUB_SYNC_TOKEN", "");
30511
30660
  await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
30512
- const { createGitHubSyncService } = await import("./github-sync-BPAvT999.js").then((n) => n.r);
30661
+ const { createGitHubSyncService } = await import("./github-sync-Gw4orAHk.js").then((n) => n.r);
30513
30662
  const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
30514
30663
  storage: c.var.storage,
30515
30664
  githubApp: app
@@ -30548,18 +30697,29 @@ function requireGitHubApp(c) {
30548
30697
  * build a client for classification without adding the helper to the
30549
30698
  * top-level imports (the module already lazy-imports github-api).
30550
30699
  */ async function getInstallationTokenFromApp(app, installationId) {
30551
- const { getInstallationToken } = await import("./github-sync-BPAvT999.js").then((n) => n.p);
30700
+ const { getInstallationToken } = await import("./github-sync-Gw4orAHk.js").then((n) => n.p);
30552
30701
  return getInstallationToken(app, installationId);
30553
30702
  }
30554
- /** List GitHub App installations authorized for this site. */ settingsRoutes.get("/github-sync/app/installations", async (c) => {
30555
- const installations = await c.var.services.githubAppInstallations.listInstallationsForSite(c.var.currentSite.id);
30556
- return c.json({ installations: installations.map((entry) => ({
30557
- installationId: entry.installationId,
30558
- account: entry.account,
30559
- addedAt: entry.addedAt
30560
- })) });
30703
+ /**
30704
+ * List the GitHub App installations the signed-in author may connect
30705
+ * this site to: everything authorized on any site they belong to, this
30706
+ * one included.
30707
+ */ settingsRoutes.get("/github-sync/app/installations", async (c) => {
30708
+ const installations = await c.var.services.githubAppInstallations.listInstallationsForUser(requireSessionUserId(c));
30709
+ return c.json({ installations });
30561
30710
  });
30562
30711
  /**
30712
+ * Resolve an `installationId` that arrived in a request into an
30713
+ * installation the caller is allowed to act on.
30714
+ *
30715
+ * Every endpoint below takes the id from the client, and an installation
30716
+ * id is a small integer — without this check a signed-in author on one
30717
+ * site could name another tenant's installation and read their private
30718
+ * repository names, or push this site's content into one of their repos.
30719
+ */ async function findVisibleInstallation(c, installationId) {
30720
+ return c.var.services.githubAppInstallations.findInstallationForUser(installationId, requireSessionUserId(c));
30721
+ }
30722
+ /**
30563
30723
  * List (or search) repositories accessible via an installation.
30564
30724
  *
30565
30725
  * Query params:
@@ -30577,10 +30737,10 @@ function requireGitHubApp(c) {
30577
30737
  const q = c.req.query("q")?.trim() ?? "";
30578
30738
  const pageParam = Number(c.req.query("page") ?? "1");
30579
30739
  const page = Number.isFinite(pageParam) && pageParam > 0 ? Math.floor(pageParam) : 1;
30740
+ const installation = await findVisibleInstallation(c, installationId);
30741
+ if (!installation) return c.json({ error: "Unknown installation." }, 404);
30580
30742
  try {
30581
30743
  if (q) {
30582
- const installation = (await c.var.services.githubAppInstallations.listInstallationsForSite(c.var.currentSite.id)).find((i) => i.installationId === installationId);
30583
- if (!installation) return c.json({ error: "Unknown installation." }, 404);
30584
30744
  const result = await searchInstallationRepos(app, installationId, installation.account.login, q);
30585
30745
  return c.json({
30586
30746
  repos: result.repos,
@@ -30601,7 +30761,7 @@ function requireGitHubApp(c) {
30601
30761
  } catch (err) {
30602
30762
  const detail = err instanceof Error ? err.message : String(err);
30603
30763
  if (/\b401\b/.test(detail) || /\b404\b/.test(detail)) {
30604
- await c.var.services.githubAppInstallations.removeInstallation(installationId, c.var.currentSite.id);
30764
+ await c.var.services.githubAppInstallations.removeInstallationForUser(installationId, requireSessionUserId(c));
30605
30765
  return c.json({
30606
30766
  error: "Installation is no longer accessible.",
30607
30767
  removed: true
@@ -30622,10 +30782,11 @@ function requireGitHubApp(c) {
30622
30782
  const installationId = String(body.installationId ?? "").trim();
30623
30783
  const repo = String(body.repo ?? "").trim();
30624
30784
  if (!installationId || !repo) return c.json({ error: "Missing installationId or repo." }, 400);
30625
- const { parseRepoSlug, createGitHubClient } = await import("./github-sync-BPAvT999.js").then((n) => n.a);
30785
+ if (!await findVisibleInstallation(c, installationId)) return c.json({ error: "Unknown installation." }, 404);
30786
+ const { parseRepoSlug, createGitHubClient } = await import("./github-sync-Gw4orAHk.js").then((n) => n.a);
30626
30787
  const parsed = parseRepoSlug(repo);
30627
30788
  if (!parsed) return c.json({ error: "Invalid repository format." }, 400);
30628
- const { classifyRepoForSync } = await import("./github-sync-BPAvT999.js").then((n) => n.r);
30789
+ const { classifyRepoForSync } = await import("./github-sync-Gw4orAHk.js").then((n) => n.r);
30629
30790
  const client = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
30630
30791
  try {
30631
30792
  const classification = await classifyRepoForSync(client, parsed.owner, parsed.repo, c.var.currentSite.id);
@@ -31418,7 +31579,9 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
31418
31579
  quietReply: body.quietReply,
31419
31580
  language: body.language,
31420
31581
  translationOfId: body.translationOfId,
31421
- publishedAt: body.publishedAt
31582
+ publishedAt: body.publishedAt,
31583
+ createdAt: body.createdAt,
31584
+ updatedAt: body.updatedAt
31422
31585
  }, body.attachments, {
31423
31586
  media: c.var.services.media,
31424
31587
  storage: c.var.storage,
@@ -32172,7 +32335,8 @@ function toApiMedia(media, appConfig) {
32172
32335
  */ var uploadApiRoutes = new Hono();
32173
32336
  var ListMediaQuerySchema = z.object({
32174
32337
  limit: z.coerce.number().int().min(1).max(200).optional().default(50),
32175
- mimePrefix: z.string().trim().min(1).optional()
32338
+ mimePrefix: z.string().trim().min(1).optional(),
32339
+ cursor: MediaIdSchema.optional()
32176
32340
  });
32177
32341
  var UpdateMediaSchema = z.object({ alt: z.string().max(500).transform((value) => value.trim()) });
32178
32342
  uploadApiRoutes.use("*", requireAuthApi());
@@ -32377,12 +32541,16 @@ uploadApiRoutes.post("/", async (c) => {
32377
32541
  }
32378
32542
  });
32379
32543
  uploadApiRoutes.get("/", async (c) => {
32380
- const { limit, mimePrefix } = parseValidated(ListMediaQuerySchema, c.req.query());
32544
+ const { limit, mimePrefix, cursor } = parseValidated(ListMediaQuerySchema, c.req.query());
32381
32545
  const mediaList = await c.var.services.media.list({
32382
32546
  limit,
32383
- mimePrefix
32547
+ mimePrefix,
32548
+ cursor
32549
+ });
32550
+ return c.json({
32551
+ media: mediaList.map((media) => toApiMedia(media, c.var.appConfig)),
32552
+ nextCursor: mediaList.length === limit ? mediaList.at(-1)?.id ?? null : null
32384
32553
  });
32385
- return c.json({ media: mediaList.map((media) => toApiMedia(media, c.var.appConfig)) });
32386
32554
  });
32387
32555
  uploadApiRoutes.get("/:id", async (c) => {
32388
32556
  const id = parseIdParam(c.req.param("id"), ID_PREFIX.media);
@@ -32898,6 +33066,7 @@ var SearchPostsToolSchema = z.object({
32898
33066
  });
32899
33067
  var ListMediaToolSchema = z.object({
32900
33068
  limit: z.coerce.number().int().min(1).max(200).optional().default(50),
33069
+ cursor: z.string().optional(),
32901
33070
  mimePrefix: z.string().trim().min(1).optional()
32902
33071
  });
32903
33072
  var GetMediaToolSchema = z.object({ id: MediaIdSchema });
@@ -33038,6 +33207,14 @@ var mcpTools = [
33038
33207
  replyToId: { type: "string" },
33039
33208
  quietReply: { type: "boolean" },
33040
33209
  publishedAt: { type: "integer" },
33210
+ createdAt: {
33211
+ type: "integer",
33212
+ description: "Unix seconds. When moving a post from another site, the time it was written."
33213
+ },
33214
+ updatedAt: {
33215
+ type: "integer",
33216
+ description: "Unix seconds. When moving a post from another site, the time it was last edited."
33217
+ },
33041
33218
  attachments: {
33042
33219
  type: "array",
33043
33220
  items: { type: "object" }
@@ -33065,7 +33242,9 @@ var mcpTools = [
33065
33242
  collectionIds: input.collectionIds,
33066
33243
  replyToId: input.replyToId,
33067
33244
  quietReply: input.quietReply,
33068
- publishedAt: input.publishedAt
33245
+ publishedAt: input.publishedAt,
33246
+ createdAt: input.createdAt,
33247
+ updatedAt: input.updatedAt
33069
33248
  }, input.attachments, {
33070
33249
  media: context.services.media,
33071
33250
  storage: context.storage,
@@ -33285,7 +33464,7 @@ var mcpTools = [
33285
33464
  },
33286
33465
  {
33287
33466
  name: "jant_media_list",
33288
- description: "List uploaded media, optionally filtered by MIME prefix.",
33467
+ description: "List uploaded media, newest first, optionally filtered by MIME prefix. Pass nextCursor back as cursor for the next page.",
33289
33468
  inputSchema: {
33290
33469
  type: "object",
33291
33470
  properties: {
@@ -33295,16 +33474,22 @@ var mcpTools = [
33295
33474
  maximum: 200,
33296
33475
  default: 50
33297
33476
  },
33298
- mimePrefix: { type: "string" }
33477
+ mimePrefix: { type: "string" },
33478
+ cursor: { type: "string" }
33299
33479
  },
33300
33480
  additionalProperties: false
33301
33481
  },
33302
33482
  async execute(args, context) {
33303
33483
  const input = ListMediaToolSchema.parse(args ?? {});
33304
- return { media: (await context.services.media.list({
33484
+ const media = await context.services.media.list({
33305
33485
  limit: input.limit,
33306
- mimePrefix: input.mimePrefix
33307
- })).map((item) => serializeMedia(item, context.appConfig)) };
33486
+ mimePrefix: input.mimePrefix,
33487
+ cursor: input.cursor
33488
+ });
33489
+ return {
33490
+ media: media.map((item) => serializeMedia(item, context.appConfig)),
33491
+ nextCursor: media.length === input.limit ? media.at(-1)?.id ?? null : null
33492
+ };
33308
33493
  }
33309
33494
  },
33310
33495
  {
@@ -33868,7 +34053,7 @@ exportApiRoutes.post("/hugo", requireAuthApi(), async (c) => {
33868
34053
  const { services, appConfig, allSettings, themeStyle } = c.var;
33869
34054
  const navItems = await services.navItems.list();
33870
34055
  const appleTouchKey = allSettings["SITE_FAVICON_APPLE_TOUCH"] ?? "";
33871
- const zip = await createExportService(services, {
34056
+ const exportService = createExportService(services, {
33872
34057
  siteName: appConfig.siteName,
33873
34058
  siteUrl: appConfig.siteUrl,
33874
34059
  siteDescription: appConfig.siteDescription,
@@ -33902,11 +34087,10 @@ exportApiRoutes.post("/hugo", requireAuthApi(), async (c) => {
33902
34087
  pageSize: appConfig.pageSize,
33903
34088
  archivePageSize: appConfig.archivePageSize,
33904
34089
  rssFeedLimit: appConfig.rssFeedLimit
33905
- }, { storage: c.var.storage }).generateHugoSite();
33906
- return new Response(zip, { headers: {
34090
+ }, { storage: c.var.storage });
34091
+ return new Response(await exportService.generateHugoSite(), { headers: {
33907
34092
  "Content-Type": "application/zip",
33908
- "Content-Disposition": "attachment; filename=\"jant-export.zip\"",
33909
- "Content-Length": String(zip.byteLength)
34093
+ "Content-Disposition": "attachment; filename=\"jant-export.zip\""
33910
34094
  } });
33911
34095
  });
33912
34096
  //#endregion
@@ -34015,8 +34199,9 @@ internalApiTokensRoutes.post("/purge", requireInternalAdminApi(), async (c) => {
34015
34199
  return result.ok ? result.html : "";
34016
34200
  }
34017
34201
  /**
34018
- * Normalize recognized historical footnotes and render the current HTML
34019
- * projection in one parse pass.
34202
+ * Normalize recognized historical footnotes, fill in children the schema
34203
+ * requires (an empty list item gets its paragraph), and render the current
34204
+ * HTML projection in one parse pass.
34020
34205
  *
34021
34206
  * @param postId - Immutable post TypeID
34022
34207
  * @param body - Canonical TipTap JSON
@@ -34034,10 +34219,12 @@ internalApiTokensRoutes.post("/purge", requireInternalAdminApi(), async (c) => {
34034
34219
  error: "TipTap body root must be a doc node."
34035
34220
  };
34036
34221
  const upgraded = upgradeLegacyFootnotes(parsed);
34222
+ const doc = fillRequiredContent(upgraded.doc);
34223
+ const canonical = JSON.stringify(doc);
34037
34224
  return {
34038
34225
  ok: true,
34039
- body: upgraded.upgraded ? JSON.stringify(upgraded.doc) : body,
34040
- html: renderTiptapDocument(upgraded.doc, { namespace: postId }),
34226
+ body: upgraded.upgraded || canonical !== JSON.stringify(upgraded.doc) ? canonical : body,
34227
+ html: renderTiptapDocument(doc, { namespace: postId }),
34041
34228
  upgradedLegacyFootnotes: upgraded.upgraded
34042
34229
  };
34043
34230
  } catch (error) {
@@ -65891,6 +66078,9 @@ function createPathService(db, siteId, databaseSchema = sqliteSchemaBundle) {
65891
66078
  async deleteByPostId(postId) {
65892
66079
  await db.delete(pathRegistry).where(and(eq(pathRegistry.siteId, siteId), eq(pathRegistry.postId, postId)));
65893
66080
  },
66081
+ async listStandalonePaths() {
66082
+ return (await db.select().from(pathRegistry).where(and(eq(pathRegistry.siteId, siteId), inArray(pathRegistry.kind, ["redirect", "archive"]), isNull(pathRegistry.postId), isNull(pathRegistry.collectionId), isNull(pathRegistry.smartCollectionId))).orderBy(asc(pathRegistry.createdAt), asc(pathRegistry.id))).map(toPathRecord);
66083
+ },
65894
66084
  async getPostAliases(postIds) {
65895
66085
  if (postIds.length === 0) return /* @__PURE__ */ new Map();
65896
66086
  return batchQuery(postIds, async (chunk) => {
@@ -66058,6 +66248,7 @@ function createPathService(db, siteId, databaseSchema = sqliteSchemaBundle) {
66058
66248
  });
66059
66249
  }
66060
66250
  var SLUG_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
66251
+ var INVALID_POST_BODY_MESSAGE = "The body isn't a TipTap JSON document. Send a doc node as a JSON string, or send Markdown as bodyMarkdown.";
66061
66252
  function isValidSlug(value) {
66062
66253
  return SLUG_RE.test(value);
66063
66254
  }
@@ -66140,6 +66331,31 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
66140
66331
  if (filters.sortBy === "thread_updated") return posts.threadUpdatedAt;
66141
66332
  return posts.publishedAt;
66142
66333
  }
66334
+ /**
66335
+ * Thread order: the root first, then replies by creation time, then ID.
66336
+ *
66337
+ * The root leads whatever its own `createdAt` says. A reply can be older
66338
+ * than its root — a post moved into a Thread keeps its creation time, and an
66339
+ * export without `created` dates every post by publication — and ordering
66340
+ * on time alone would open such a Thread on a reply. Every surface that
66341
+ * reads the first post as the root would then lose it: the Thread page's
66342
+ * canonical URL, Featured, Collections, the feeds, and the reply guard's
66343
+ * idea of where the Thread ends. Every query that orders a Thread's posts
66344
+ * goes through here so they agree.
66345
+ *
66346
+ * @param direction - `desc` walks from the Thread's end back to its root
66347
+ * @returns ORDER BY terms, placed after any partitioning column
66348
+ * @example
66349
+ * db.select().from(posts).orderBy(posts.threadId, ...threadOrder());
66350
+ * sql`ROW_NUMBER() OVER (ORDER BY ${sql.join(threadOrder(), sql`, `)})`;
66351
+ */ function threadOrder(direction = "asc") {
66352
+ const terms = [
66353
+ sql`CASE WHEN ${posts.replyToId} IS NULL THEN 0 ELSE 1 END`,
66354
+ sql`${posts.createdAt}`,
66355
+ sql`${posts.id}`
66356
+ ];
66357
+ return direction === "asc" ? terms : terms.map((term) => desc(term));
66358
+ }
66143
66359
  function buildYearMonthExpr(column) {
66144
66360
  return databaseDialect === "pg" ? sql`to_char(timezone('UTC', to_timestamp(${column})), 'YYYY-MM')` : sql`strftime('%Y-%m', ${column}, 'unixepoch')`;
66145
66361
  }
@@ -66469,21 +66685,58 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
66469
66685
  updatedAt: row.updatedAt
66470
66686
  };
66471
66687
  }
66472
- async function hydratePost(row) {
66688
+ /**
66689
+ * Thread visibility the loaded rows already answer, plus the thread ids they
66690
+ * do not.
66691
+ *
66692
+ * A reply's own `visibility` column is null — it inherits the root's — so a
66693
+ * row only speaks for its thread when it *is* the root. List surfaces query
66694
+ * thread roots exclusively (`excludeReplies`), so for them this covers every
66695
+ * thread and the follow-up read never runs.
66696
+ *
66697
+ * Entries are set only for a non-null visibility, matching
66698
+ * {@link getThreadVisibilityMap}: a root row with no visibility is treated as
66699
+ * unreadable by both paths.
66700
+ */ function seedThreadVisibility(rows) {
66701
+ const known = /* @__PURE__ */ new Map();
66702
+ const covered = /* @__PURE__ */ new Set();
66703
+ for (const row of rows) {
66704
+ if (row.id !== row.threadId) continue;
66705
+ covered.add(row.threadId);
66706
+ if (row.visibility) known.set(row.threadId, ensurePostVisibility(row.visibility, Error));
66707
+ }
66708
+ return {
66709
+ known,
66710
+ missingThreadIds: [...new Set(rows.map((row) => row.threadId).filter((threadId) => !covered.has(threadId)))]
66711
+ };
66712
+ }
66713
+ /**
66714
+ * Slug lookup and root-visibility lookup are independent, so they go out
66715
+ * together rather than one after the other.
66716
+ */ async function loadHydrationMaps(rows) {
66717
+ const { known, missingThreadIds } = seedThreadVisibility(rows);
66718
+ const [slugMap, fetched] = await Promise.all([resolvedPaths.getPostSlugMap(rows.map((row) => row.id)), missingThreadIds.length > 0 ? getThreadVisibilityMap(missingThreadIds) : Promise.resolve(/* @__PURE__ */ new Map())]);
66719
+ for (const [threadId, visibility] of fetched) known.set(threadId, visibility);
66720
+ return {
66721
+ slugMap,
66722
+ visibilityMap: known
66723
+ };
66724
+ }
66725
+ async function hydratePost(row, knownSlug) {
66473
66726
  if (!row) return null;
66474
- const slug = await resolvedPaths.getPostSlug(row.id);
66727
+ const { known, missingThreadIds } = seedThreadVisibility([row]);
66728
+ const [slug, fetched] = await Promise.all([knownSlug ?? resolvedPaths.getPostSlug(row.id), missingThreadIds.length > 0 ? getThreadVisibilityMap(missingThreadIds) : Promise.resolve(/* @__PURE__ */ new Map())]);
66475
66729
  if (!slug) return null;
66476
- const visibility = (await getThreadVisibilityMap([row.threadId])).get(row.threadId) ?? row.visibility;
66730
+ const visibility = known.get(row.threadId) ?? fetched.get(row.threadId) ?? row.visibility;
66477
66731
  if (!visibility) return null;
66478
66732
  return toPost(row, slug, visibility);
66479
66733
  }
66480
66734
  async function hydratePosts(rows) {
66481
66735
  if (rows.length === 0) return [];
66482
- const slugMap = await resolvedPaths.getPostSlugMap(rows.map((row) => row.id));
66483
- const rootVisibilityMap = await getThreadVisibilityMap(rows.map((row) => row.threadId));
66736
+ const { slugMap, visibilityMap } = await loadHydrationMaps(rows);
66484
66737
  return rows.map((row) => {
66485
66738
  const slug = slugMap.get(row.id);
66486
- const visibility = rootVisibilityMap.get(row.threadId) ?? row.visibility;
66739
+ const visibility = visibilityMap.get(row.threadId) ?? row.visibility;
66487
66740
  return slug && visibility ? toPost(row, slug, visibility) : null;
66488
66741
  }).filter((row) => row !== null);
66489
66742
  }
@@ -66647,6 +66900,30 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
66647
66900
  async getById(id) {
66648
66901
  return hydratePost((await db.select().from(posts).where(and(eq(posts.siteId, siteId), eq(posts.id, id))).limit(1))[0]);
66649
66902
  },
66903
+ async getWithCanonicalAlias(postId) {
66904
+ const rows = await db.select({
66905
+ post: posts,
66906
+ path: pathRegistry
66907
+ }).from(posts).leftJoin(pathRegistry, and(eq(pathRegistry.siteId, siteId), eq(pathRegistry.postId, posts.id))).where(and(eq(posts.siteId, siteId), eq(posts.id, postId))).orderBy(asc(pathRegistry.createdAt), asc(pathRegistry.id));
66908
+ const row = rows[0];
66909
+ if (!row) return null;
66910
+ let slug = null;
66911
+ let canonicalAlias = null;
66912
+ for (const { path } of rows) {
66913
+ if (!path) continue;
66914
+ if (path.kind === "slug") {
66915
+ slug = path.path;
66916
+ continue;
66917
+ }
66918
+ if (path.kind === "alias") canonicalAlias = `/${path.path}`;
66919
+ }
66920
+ if (!slug) return null;
66921
+ const post = await hydratePost(row.post, slug);
66922
+ return post ? {
66923
+ post,
66924
+ canonicalAlias
66925
+ } : null;
66926
+ },
66650
66927
  async getBodyContent(id) {
66651
66928
  const post = await this.getById(id);
66652
66929
  if (!post) return null;
@@ -66662,7 +66939,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
66662
66939
  async getBySlug(slug) {
66663
66940
  const resolved = await resolvedPaths.resolve(slug);
66664
66941
  if (!resolved || resolved.kind !== "slug" || !resolved.postId) return null;
66665
- return this.getById(resolved.postId);
66942
+ return hydratePost((await db.select().from(posts).where(and(eq(posts.siteId, siteId), eq(posts.id, resolved.postId))).limit(1))[0], resolved.path);
66666
66943
  },
66667
66944
  async suggestSlug(input) {
66668
66945
  return generatePostSlug({
@@ -66839,6 +67116,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
66839
67116
  const rawBody = data.bodyMarkdown ? markdownToTiptapJson(data.bodyMarkdown) : data.body ?? null;
66840
67117
  const trimmedBody = rawBody ? trimTiptapBody(rawBody) : null;
66841
67118
  const preparedBody = trimmedBody ? tryPreparePostBodyHtml(id, trimmedBody) : null;
67119
+ if (preparedBody && !preparedBody.ok) throw new ValidationError(INVALID_POST_BODY_MESSAGE);
66842
67120
  const body = preparedBody?.ok ? preparedBody.body : trimmedBody;
66843
67121
  const title = data.title?.trim() || null;
66844
67122
  const quoteText = data.quoteText?.trim() || null;
@@ -66898,6 +67176,8 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
66898
67176
  if (status === "draft" && resolvedFeaturedAt !== null) throw new ConflictError("Publish this post before featuring it.");
66899
67177
  assertDraftPublishedAt(status, data.publishedAt);
66900
67178
  const publishedAt = status === "published" ? data.publishedAt ?? timestamp : null;
67179
+ const createdAt = data.createdAt ?? timestamp;
67180
+ const updatedAt = data.updatedAt ?? createdAt;
66901
67181
  let slug;
66902
67182
  let aliasPath = null;
66903
67183
  const titleForSlug = format === "quote" ? void 0 : title ?? void 0;
@@ -66987,10 +67267,10 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
66987
67267
  translationGroupId,
66988
67268
  quietReply: isQuietReply,
66989
67269
  publishedAt,
66990
- lastActivityAt: publishedAt ?? timestamp,
66991
- threadUpdatedAt: publishedAt ?? timestamp,
66992
- createdAt: timestamp,
66993
- updatedAt: timestamp
67270
+ lastActivityAt: publishedAt ?? updatedAt,
67271
+ threadUpdatedAt: publishedAt ?? updatedAt,
67272
+ createdAt,
67273
+ updatedAt
66994
67274
  }));
66995
67275
  writeQueries.push(db.insert(pathRegistry).values({
66996
67276
  id: createEntityId("path"),
@@ -67046,10 +67326,10 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
67046
67326
  translationGroupId,
67047
67327
  quietReply: isQuietReply,
67048
67328
  publishedAt,
67049
- lastActivityAt: publishedAt ?? timestamp,
67050
- threadUpdatedAt: publishedAt ?? timestamp,
67051
- createdAt: timestamp,
67052
- updatedAt: timestamp
67329
+ lastActivityAt: publishedAt ?? updatedAt,
67330
+ threadUpdatedAt: publishedAt ?? updatedAt,
67331
+ createdAt,
67332
+ updatedAt
67053
67333
  });
67054
67334
  await tx.insert(pathRegistry).values({
67055
67335
  id: createEntityId("path"),
@@ -67195,6 +67475,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
67195
67475
  const rawBody = data.bodyMarkdown ? markdownToTiptapJson(data.bodyMarkdown) : data.body ?? null;
67196
67476
  const normalizedBody = rawBody ? trimTiptapBody(rawBody) : null;
67197
67477
  const preparedBody = normalizedBody ? tryPreparePostBodyHtml(existing.id, normalizedBody) : null;
67478
+ if (preparedBody && !preparedBody.ok) throw new ValidationError(INVALID_POST_BODY_MESSAGE);
67198
67479
  updatedBody = preparedBody?.ok ? preparedBody.body : normalizedBody;
67199
67480
  updates.body = updatedBody;
67200
67481
  updates.bodyHtml = preparedBody?.ok ? preparedBody.html : updatedBody ? renderPostBodyHtml(existing.id, updatedBody) : null;
@@ -67408,7 +67689,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
67408
67689
  return this.delete(id, deps);
67409
67690
  },
67410
67691
  async getThread(rootId) {
67411
- return hydratePosts(await db.select().from(posts).where(and(eq(posts.siteId, siteId), eq(posts.threadId, rootId))).orderBy(posts.createdAt, posts.id));
67692
+ return hydratePosts(await db.select().from(posts).where(and(eq(posts.siteId, siteId), eq(posts.threadId, rootId))).orderBy(...threadOrder()));
67412
67693
  },
67413
67694
  async getThreadPosition(postId) {
67414
67695
  const target = (await db.select({
@@ -67679,17 +67960,15 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
67679
67960
  const rankedReplies = db.select({
67680
67961
  id: posts.id,
67681
67962
  threadId: posts.threadId,
67682
- createdAt: posts.createdAt,
67683
67963
  previewRank: sql`CAST(ROW_NUMBER() OVER (
67684
67964
  PARTITION BY ${posts.threadId}
67685
- ORDER BY ${posts.createdAt}, ${posts.id}
67965
+ ORDER BY ${sql.join(threadOrder(), sql`, `)}
67686
67966
  ) AS INTEGER)`.as("preview_rank")
67687
67967
  }).from(posts).where(and(eq(posts.siteId, siteId), inArray(posts.threadId, rootIds), eq(posts.status, "published"), isNotNull(posts.replyToId))).as("ranked_replies");
67688
67968
  const rankedRows = await db.select({
67689
67969
  id: rankedReplies.id,
67690
- threadId: rankedReplies.threadId,
67691
- createdAt: rankedReplies.createdAt
67692
- }).from(rankedReplies).where(lte(rankedReplies.previewRank, previewCount)).orderBy(rankedReplies.threadId, rankedReplies.createdAt, rankedReplies.id);
67970
+ threadId: rankedReplies.threadId
67971
+ }).from(rankedReplies).where(lte(rankedReplies.previewRank, previewCount)).orderBy(rankedReplies.threadId, sql`${rankedReplies.previewRank}`);
67693
67972
  const hydratedPosts = await hydratePostsById(rankedRows.map((row) => row.id));
67694
67973
  const result = /* @__PURE__ */ new Map();
67695
67974
  for (const row of rankedRows) {
@@ -67711,11 +67990,11 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
67711
67990
  threadId: posts.threadId,
67712
67991
  firstReplyRank: sql`CAST(ROW_NUMBER() OVER (
67713
67992
  PARTITION BY ${posts.threadId}
67714
- ORDER BY ${posts.createdAt}, ${posts.id}
67993
+ ORDER BY ${sql.join(threadOrder(), sql`, `)}
67715
67994
  ) AS INTEGER)`.as("first_reply_rank"),
67716
67995
  latestReplyRank: sql`CAST(ROW_NUMBER() OVER (
67717
67996
  PARTITION BY ${posts.threadId}
67718
- ORDER BY ${posts.createdAt} DESC, ${posts.id} DESC
67997
+ ORDER BY ${sql.join(threadOrder("desc"), sql`, `)}
67719
67998
  ) AS INTEGER)`.as("latest_reply_rank"),
67720
67999
  totalReplyCount: sql`CAST(COUNT(*) OVER (
67721
68000
  PARTITION BY ${posts.threadId}
@@ -67785,7 +68064,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
67785
68064
  const threadPosition = sql`CAST(
67786
68065
  row_number() OVER (
67787
68066
  PARTITION BY ${posts.threadId}
67788
- ORDER BY ${posts.createdAt}, ${posts.id}
68067
+ ORDER BY ${sql.join(threadOrder(), sql`, `)}
67789
68068
  ) - 1 AS INTEGER
67790
68069
  )`.as("thread_position");
67791
68070
  const threadPostCount = sql`CAST(
@@ -67941,7 +68220,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
67941
68220
  eq(posts.status, "published")
67942
68221
  ];
67943
68222
  if (options.publishedBefore !== void 0) conditions.push(sql`${posts.publishedAt} < ${options.publishedBefore}`);
67944
- return db.select().from(posts).where(and(...conditions)).orderBy(posts.threadId, posts.createdAt, posts.id);
68223
+ return db.select().from(posts).where(and(...conditions)).orderBy(posts.threadId, ...threadOrder());
67945
68224
  });
67946
68225
  for (const post of await hydratePosts(rows)) {
67947
68226
  const thread = result.get(post.threadId);
@@ -67957,7 +68236,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
67957
68236
  const rows = await db.select({
67958
68237
  threadId: posts.threadId,
67959
68238
  id: posts.id
67960
- }).from(posts).where(and(eq(posts.siteId, siteId), inArray(posts.threadId, unique), ...options?.includeDrafts ? [] : [eq(posts.status, "published")])).orderBy(posts.threadId, desc(posts.createdAt), desc(posts.id));
68239
+ }).from(posts).where(and(eq(posts.siteId, siteId), inArray(posts.threadId, unique), ...options?.includeDrafts ? [] : [eq(posts.status, "published")])).orderBy(posts.threadId, ...threadOrder("desc"));
67961
68240
  for (const row of rows) if (!result.has(row.threadId)) result.set(row.threadId, row.id);
67962
68241
  return result;
67963
68242
  },
@@ -68193,7 +68472,6 @@ internalSitesRoutes.get("/:siteId/export", requireInternalAdminApi(), async (c)
68193
68472
  });
68194
68473
  return new Response(archive.zip, { headers: {
68195
68474
  "Content-Disposition": `attachment; filename="${archive.filename}"`,
68196
- "Content-Length": String(archive.zip.byteLength),
68197
68475
  "Content-Type": "application/zip"
68198
68476
  } });
68199
68477
  });
@@ -69303,6 +69581,18 @@ var ListPublicPostsQuerySchema = z.object({
69303
69581
  content: z.enum(["markdown"]).optional()
69304
69582
  });
69305
69583
  var PublicPostContentQuerySchema = z.object({ content: z.enum(["markdown"]).optional() });
69584
+ /**
69585
+ * The public Markdown for a post, or null for a historical body that isn't
69586
+ * TipTap JSON. One unreadable row answers null and is logged, rather than
69587
+ * failing the whole listing; `bodyHtml` still falls back to stored HTML.
69588
+ */ function toPublicBodyMarkdown(postId, body) {
69589
+ try {
69590
+ return tiptapJsonToMarkdown(body);
69591
+ } catch (error) {
69592
+ console.error(`Couldn't convert the body of post ${postId} to Markdown`, error);
69593
+ return null;
69594
+ }
69595
+ }
69306
69596
  function isPublicDetailVisible(post) {
69307
69597
  return post !== null && post.status === "published" && post.visibility !== "private";
69308
69598
  }
@@ -69348,7 +69638,7 @@ function toPublicPost(post, mediaList, threadCollections, appConfig, options) {
69348
69638
  url: toPublicPath(getCollectionPagePath(collection.slug), sitePathPrefix)
69349
69639
  }))
69350
69640
  };
69351
- const contentFields = options?.content === "markdown" ? { bodyMarkdown: post.body ? tiptapJsonToMarkdown(post.body) : null } : {
69641
+ const contentFields = options?.content === "markdown" ? { bodyMarkdown: post.body ? toPublicBodyMarkdown(post.id, post.body) : null } : {
69352
69642
  bodyHtml: post.bodyHtml,
69353
69643
  bodyText: post.bodyText
69354
69644
  };
@@ -69588,7 +69878,9 @@ composeRoutes.post("/", async (c) => {
69588
69878
  quietReply: data.quietReply,
69589
69879
  language: data.language,
69590
69880
  translationOfId: data.translationOfId,
69591
- publishedAt: data.publishedAt
69881
+ publishedAt: data.publishedAt,
69882
+ createdAt: data.createdAt,
69883
+ updatedAt: data.updatedAt
69592
69884
  }, data.attachments, {
69593
69885
  media: c.var.services.media,
69594
69886
  storage: c.var.storage,
@@ -69676,7 +69968,9 @@ composeRoutes.post("/thread", async (c) => {
69676
69968
  quietReply: data.quietReply,
69677
69969
  language: index === 0 ? data.language : void 0,
69678
69970
  translationOfId: index === 0 ? data.translationOfId : void 0,
69679
- publishedAt: data.publishedAt
69971
+ publishedAt: data.publishedAt,
69972
+ createdAt: data.createdAt,
69973
+ updatedAt: data.updatedAt
69680
69974
  },
69681
69975
  attachments: data.attachments
69682
69976
  })), storageOpts, summaryConfig))[0];
@@ -69891,7 +70185,7 @@ manifestRoutes.get("/manifest.webmanifest", (c) => {
69891
70185
  });
69892
70186
  //#endregion
69893
70187
  //#region ../../docs/skill.md?raw
69894
- var skill_default = "---\nname: jant-site\ndescription: >-\n Work with the Jant site identified in this document through its HTTP API or\n MCP interface. Use when the user asks an AI assistant to read, publish, edit,\n organize, search, or migrate content, upload media, manage Collections, or\n update site settings.\n---\n\n# Jant Site Skill\n\nThis page is written for AI assistants working with one Jant site. Use it when the user asks you to read, publish, edit, organize, search, or migrate site content through Jant's HTTP API or MCP interface.\n\nThe full HTTP API reference lives at <https://jant.me/docs/API.md>. This skill explains how to choose the right interface, work safely, understand Jant's content model, and run common workflows. Consult the reference for complete request and response schemas instead of guessing fields.\n\n---\n\n## Scope and Safety\n\n- Use this skill only for the target site identified below. Do not substitute another site without the user's confirmation.\n- Start with read-only inspection when the state of the site or the user's intent is unclear.\n- Get explicit confirmation before bulk publishing, bulk deletion, destructive settings changes, or any action that is difficult to reverse. A request to perform a specific ordinary write, such as publishing one post, already supplies that confirmation.\n- For uncertain or bulk content changes, ask whether to save posts as drafts or publish them immediately.\n- Treat API tokens like passwords. Never log, commit, or repeat a token back to the user after they provide it.\n- Use the public API or MCP interface. Do not edit the database directly and do not call `/api/internal/*`; those endpoints belong to the hosted control plane.\n- Keep bulk writes sequential. Save returned IDs after each successful write so interrupted work can resume without creating duplicates.\n\n---\n\n## Choose an Interface\n\nJant exposes the same site-owner capabilities through two interfaces:\n\n| Interface | Use it when |\n| ----------------------- | ---------------------------------------------------------------------------------------------------- |\n| HTTP JSON API | Writing scripts, making a few direct requests, or running a one-time migration. This is the default. |\n| MCP at `<site>/api/mcp` | The caller already supports MCP and benefits from tool discovery and structured tool calls. |\n\nFor MCP, call `initialize`, then `tools/list`, and use the returned schemas rather than assuming tool arguments. The tool groups cover posts, media, attachments, Collections, settings, and search.\n\nFor HTTP, these are the main entry points:\n\n| Task | Endpoint |\n| ---------------------------- | ---------------------------------------------------------------- |\n| Read public posts | `GET /api/public/posts`, `GET /api/public/posts/:slug` |\n| List or inspect all posts | `GET /api/posts`, `GET /api/posts/:id` |\n| Create, update, delete | `POST /api/posts`, `PUT /api/posts/:id`, `DELETE /api/posts/:id` |\n| Upload or manage media | `/api/upload` or `/api/uploads` |\n| Manage Collections | `/api/collections` |\n| Search published content | `GET /api/search` |\n| Read or update site settings | `GET /api/settings`, `PUT /api/settings` |\n\nPublic post, Collection, navigation, and search reads can work without a token. A site can disable anonymous API reads; `/api/public/*` then becomes unavailable, while Collection, navigation, and search JSON reads require a browser session or Bearer token. Private content and all writes always require authentication.\n\n---\n\n## Confirm the Target and Authenticate\n\nBefore an authenticated or multi-step operation, confirm:\n\n- The Jant site URL (e.g. `https://example.com`).\n- The exact outcome the user wants, including which content may change.\n- For bulk work, whether new posts should use `status: \"published\"` or `status: \"draft\"`.\n\nHave the user create an API token when the operation needs authentication:\n\n1. Sign in to Jant at `<site>/signin`.\n2. Open **Settings → API Tokens**.\n3. Create a token and copy it immediately. It is shown only once.\n\nSend the token as `Authorization: Bearer jnt_...`.\n\n```bash\nexport JANT_API_TOKEN=jnt_...\nexport JANT_SITE=https://example.com\n```\n\n---\n\n## Understand the Content Model\n\nJant is a single-author microblog. There are no users, comments, likes, followers, or social relationships to manage.\n\nThree concepts shape almost every content operation:\n\n1. **Posts have three formats** — `note`, `link`, and `quote`. Preserve the semantic format instead of coercing everything into `note`.\n2. **Threads connect posts** — a reply is a post whose `replyToId` points to another post. There is no separate comments table.\n3. **Collections curate Threads** — they are intentional groupings, not tags. A Thread can belong to multiple Collections; its root and replies share the same memberships.\n\n### `note` — original writing\n\nUse for essays, journal entries, status updates, photo posts with captions, and other content written by the site owner.\n\n- Required: `format: \"note\"`\n- Recommended: `bodyMarkdown`\n- Optional: `title`\n\n### `link` — a shared reference\n\nUse when the post is fundamentally pointing readers to another resource.\n\n- Required: `format: \"link\"`, `title`, `url`\n- Optional: `bodyMarkdown` for the owner's commentary\n\n### `quote` — a cited passage\n\nUse when the post is built around someone else's words.\n\n- Required: `format: \"quote\"`, `quoteText`\n- Optional: `sourceName`, `sourceUrl`, and `bodyMarkdown` for commentary\n- Do not send `title` or `url`; quote posts use `sourceName` and `sourceUrl` instead.\n\n---\n\n## Common Content Operations\n\nRead the current state before modifying existing content. Use `GET /api/posts/:id` when you need fields that may not appear in a list response, including Thread-level Collection memberships.\n\nCreate a note with an explicit status and visibility:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/posts\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"format\": \"note\",\n \"bodyMarkdown\": \"Hello from Jant.\",\n \"status\": \"published\",\n \"visibility\": \"public\"\n }'\n```\n\nUse `PUT /api/posts/:id` to update a post and `DELETE /api/posts/:id` to delete one. Fetch the post first, preserve fields the user did not ask to change, and confirm before deletion.\n\nUse `POST /api/upload` for an ordinary single-file script or one-time migration. Use the session-based `/api/uploads` flow for large files, unreliable connections, or clients that need resumable transport. Save the returned `med_*` ID before attaching media to a post.\n\nUse `GET /api/settings` before `PUT /api/settings`. Settings values are strings in the HTTP API; preserve settings outside the user's requested change.\n\nFor exact fields, filters, pagination, and response schemas, read the [full HTTP API reference](https://jant.me/docs/API.md).\n\n---\n\n## Import Content from Another Platform\n\nUse this workflow when the user provides an export, archive, feed, database dump, or folder of source files from another blog or CMS.\n\n### Plan before writing\n\n1. Inspect the target site so you know whether it already contains content.\n2. Inspect 3–5 representative source posts end to end before deciding how to transform anything.\n3. Report what you found: source formats, item counts, dates, media, internal links, categories, and anything unsupported.\n4. Ask how to handle ambiguous categories or tags. Map them to Collections, drop them, or fold them into post bodies only with the user's direction.\n5. Agree on draft versus published status and on a duplicate policy before creating posts.\n6. Build a resumable source-ID map and persist `{ sourceId -> jantId, slug }` after every successful write.\n\nA slug or path collision returns `409 CONFLICT`, but that does not make an import automatically idempotent. Items with new or generated slugs can still be duplicated, so use the source-ID map rather than relying on conflicts.\n\n### Read the source\n\nCommon source shapes include:\n\n| Source | Typical export |\n| --------------------- | ----------------------------------------------------------------------- |\n| WordPress | WXR XML (`Tools → Export`), with media URLs pointing at the source host |\n| Ghost | JSON export from `Settings → Labs → Export` |\n| Substack | ZIP with `posts.csv` and `posts/*.html` |\n| Medium | ZIP with `posts/*.html` |\n| Tumblr | API dump or `tumblr-utils` archive |\n| Hugo / Jekyll / Astro | Markdown files with YAML or TOML front matter |\n| Notion | Markdown plus a media ZIP |\n| Custom | SQL dump, HTML folder, RSS feed, or another structured source |\n\nDo not assume the source schema from the platform name alone. Check titles, bodies, dates, embedded media, internal links, categories, and source identifiers in real records.\n\n### Map source formats\n\nUse these heuristics as a starting point:\n\n- WordPress, Ghost, and Substack standard posts → `note` with `title` and `bodyMarkdown`.\n- Linklog posts that are primarily an external URL plus commentary → `link`.\n- WordPress or Tumblr quote posts → `quote`.\n- Tumblr photo posts with captions → `note` with uploaded images.\n- Tumblr link posts → `link`.\n- Reblogs or reposts → ask the user. Jant has no native reblog concept; `quote` with `sourceUrl` is often the closest representation.\n\nWhen uncertain, prefer `note`. Turning a real `link` or `quote` into a `note` loses semantic information, while turning original writing into a `link` or `quote` invents structure.\n\n### Convert bodies to Markdown\n\nSend `bodyMarkdown`, not `body`. The `body` field expects TipTap JSON and is intended for editor integrations.\n\n- Convert HTML with a real HTML-to-Markdown tool such as Turndown or Pandoc. Do not strip tags with regular expressions.\n- Preserve headings, lists, tables, fenced code blocks, blockquotes, and inline formatting.\n- A blank line starts a new paragraph. Use two trailing spaces or a backslash for a hard line break.\n- Preserve `<!--more-->` when the source has an intentional excerpt break.\n- Upload embedded media to Jant instead of leaving remote `<img>` references that may disappear with the old site.\n- Rewrite internal links after the destination slugs are known. Either build the slug map first or use a two-pass process: create posts, then update their bodies.\n\n### Upload media first\n\nPosts reference uploaded media by `med_*` ID. For a one-time migration, the one-shot endpoint is usually enough:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/upload\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -F \"file=@./photo.jpg\" \\\n -F \"alt=A red door\"\n# → { \"id\": \"med_...\", \"url\": \"/media/med_....jpg\", ... }\n```\n\nUse `/api/uploads` when a file is large or the connection is unreliable. Its session flow initializes the upload, transfers one or more parts, optionally adds a video poster, and completes the session to produce the final `med_*` record.\n\nPreserve source alt text. If the source has no alt text, leave it unset rather than inventing a description.\n\n### Create Collections when needed\n\nCreate approved Collections before the posts that use them:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/collections\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"slug\": \"reading\",\n \"title\": \"Reading\",\n \"description\": \"Books worth coming back to\",\n \"sortOrder\": \"newest\"\n }'\n```\n\nSave each returned `col_*` ID. Send `collectionIds` on standalone posts and Thread roots, not on replies.\n\n### Create posts\n\n`POST /api/posts` creates every post format. Send one post at a time and record the result before continuing.\n\nMinimal `note`:\n\n```json\n{\n \"format\": \"note\",\n \"title\": \"Hello world\",\n \"bodyMarkdown\": \"First post on the new site.\",\n \"publishedAt\": 1706000000,\n \"status\": \"draft\"\n}\n```\n\nMinimal `link`:\n\n```json\n{\n \"format\": \"link\",\n \"title\": \"An interesting article\",\n \"url\": \"https://example.com/post\",\n \"bodyMarkdown\": \"Worth your fifteen minutes.\"\n}\n```\n\nMinimal `quote`:\n\n```json\n{\n \"format\": \"quote\",\n \"quoteText\": \"What stands in the way becomes the way.\",\n \"sourceName\": \"Marcus Aurelius\",\n \"sourceUrl\": \"https://example.com/meditations\"\n}\n```\n\nFields commonly needed during migration:\n\n| Field | Purpose |\n| --------------- | ------------------------------------------------------------------------------------------ |\n| `publishedAt` | Original publication time as Unix seconds, preserving archive chronology. |\n| `slug` | Original stable slug when it should remain the canonical URL. |\n| `path` | Alternative to `slug` for an original path such as `2024/01/hello-world`; never send both. |\n| `status` | `published` or `draft`; set it explicitly according to the import plan. |\n| `visibility` | `public`, `latest_hidden`, or `private`. |\n| `pinned` | Preserve a pinned state when the source had one; replies cannot be pinned. |\n| `featured` | Preserve a featured or starred state when it has the same meaning. |\n| `collectionIds` | Approved `col_*` IDs on standalone posts and Thread roots. |\n| `attachments` | Ordered media or text attachments. |\n| `replyToId` | Destination `pst_*` ID of the parent when importing a Thread reply. |\n\nRemember these validation rules:\n\n- Send either `body` or `bodyMarkdown`, never both.\n- Send either `slug` or `path`, never both.\n- `note` rejects link- and quote-specific fields.\n- `link` requires `title` and `url`.\n- `quote` requires `quoteText`, uses `sourceName` and `sourceUrl`, and rejects `title` and `url`.\n\nA successful create returns `201` with the full post. Save `id`, `slug`, and `threadId` under the source ID for replies, link rewriting, verification, and recovery.\n\n### Rebuild Threads\n\n1. Import the Thread root first.\n2. Import replies in parent-first order with `replyToId` set to the destination parent's `pst_*` ID.\n3. Do not set `threadId`; Jant derives it from `replyToId`.\n4. Set `collectionIds` on the root only. Replies share the Thread's Collection memberships.\n5. Replies inherit root visibility and status unless explicitly created as drafts. They cannot be pinned or receive independent visibility changes.\n\n### Verify and report\n\nAfter the import:\n\n- Compare source and destination counts by format, media type, Collection, and Thread.\n- Fetch representative posts through `GET /api/posts/:id` and verify dates, slugs, bodies, attachments, Collection memberships, and reply relationships.\n- Open representative public URLs and verify rendered Markdown, media, and rewritten internal links.\n- Report anything skipped, transformed, duplicated, or unresolved, with a short list of URLs for manual review.\n\nIf an import stops partway through, resume from the persisted source-ID map. If cleanup is required, identify only the posts, media, and Collections created by that run, show the user the cleanup scope, and get confirmation before deleting them. Do not use account deletion as an automated recovery strategy.\n\n---\n\n## Handle Errors\n\nHTTP API errors use this shape:\n\n```json\n{ \"error\": \"...\", \"code\": \"VALIDATION_ERROR\", \"details\": {} }\n```\n\n| Code | Response |\n| --------------------- | ------------------------------------------------------------------------------------------------- |\n| `VALIDATION_ERROR` | Read `details.fieldErrors`, correct the request, and do not retry unchanged input. |\n| `UNAUTHORIZED` | Stop. The token is missing, invalid, or revoked; ask the user to create or provide a valid token. |\n| `FORBIDDEN` | Stop and explain which operation was denied. Do not look for an internal bypass. |\n| `NOT_FOUND` | Re-check the target site and resource ID before deciding whether the item was removed. |\n| `CONFLICT` | Resolve the reported state conflict; never silently change a user-selected canonical slug. |\n| `RATE_LIMIT` | Respect `Retry-After` when present, back off, and keep bulk writes sequential. |\n| `CONFIGURATION_ERROR` | Surface the server message; the site owner may need to correct deployment configuration. |\n\nFor MCP, transport and protocol failures use JSON-RPC errors. Tool validation and domain failures are returned as tool results with `isError: true`; inspect their structured content before retrying.\n\n---\n\n## Reference\n\n- Full HTTP API: <https://jant.me/docs/API.md> — endpoints, fields, filters, and response schemas.\n- Automation overview: <https://jant.me/docs/automation-and-api.md> — tokens, HTTP, MCP, and debugging.\n- Round-trip Jant exports: <https://jant.me/docs/export-and-import.md> — use `site export` and `site import` when both ends are Jant.\n- Public documentation index: <https://jant.me/docs/SUMMARY.md>.\n";
70188
+ var skill_default = "---\nname: jant-site\ndescription: >-\n Work with the Jant site identified in this document through its HTTP API or\n MCP interface. Use when the user asks an AI assistant to read, publish, edit,\n organize, search, or migrate content, upload media, manage Collections, or\n update site settings.\n---\n\n# Jant Site Skill\n\nThis page is written for AI assistants working with one Jant site. Use it when the user asks you to read, publish, edit, organize, search, or migrate site content through Jant's HTTP API or MCP interface.\n\nThe full HTTP API reference lives at <https://jant.me/docs/API.md>. This skill explains how to choose the right interface, work safely, understand Jant's content model, and run common workflows. Consult the reference for complete request and response schemas instead of guessing fields.\n\n---\n\n## Scope and Safety\n\n- Use this skill only for the target site identified below. Do not substitute another site without the user's confirmation.\n- Start with read-only inspection when the state of the site or the user's intent is unclear.\n- Get explicit confirmation before bulk publishing, bulk deletion, destructive settings changes, or any action that is difficult to reverse. A request to perform a specific ordinary write, such as publishing one post, already supplies that confirmation.\n- For uncertain or bulk content changes, ask whether to save posts as drafts or publish them immediately.\n- Treat API tokens like passwords. Never log, commit, or repeat a token back to the user after they provide it.\n- Use the public API or MCP interface. Do not edit the database directly and do not call `/api/internal/*`; those endpoints belong to the hosted control plane.\n- Keep bulk writes sequential. Save returned IDs after each successful write so interrupted work can resume without creating duplicates.\n\n---\n\n## Choose an Interface\n\nJant exposes the same site-owner capabilities through two interfaces:\n\n| Interface | Use it when |\n| ----------------------- | ---------------------------------------------------------------------------------------------------- |\n| HTTP JSON API | Writing scripts, making a few direct requests, or running a one-time migration. This is the default. |\n| MCP at `<site>/api/mcp` | The caller already supports MCP and benefits from tool discovery and structured tool calls. |\n\nFor MCP, call `initialize`, then `tools/list`, and use the returned schemas rather than assuming tool arguments. The tool groups cover posts, media, attachments, Collections, settings, and search.\n\nFor HTTP, these are the main entry points:\n\n| Task | Endpoint |\n| ---------------------------- | ---------------------------------------------------------------- |\n| Read public posts | `GET /api/public/posts`, `GET /api/public/posts/:slug` |\n| List or inspect all posts | `GET /api/posts`, `GET /api/posts/:id` |\n| Create, update, delete | `POST /api/posts`, `PUT /api/posts/:id`, `DELETE /api/posts/:id` |\n| Upload or manage media | `/api/upload` or `/api/uploads` |\n| Manage Collections | `/api/collections` |\n| Search published content | `GET /api/search` |\n| Read or update site settings | `GET /api/settings`, `PUT /api/settings` |\n\nPublic post, Collection, navigation, and search reads can work without a token. A site can disable anonymous API reads; `/api/public/*` then becomes unavailable, while Collection, navigation, and search JSON reads require a browser session or Bearer token. Private content and all writes always require authentication.\n\n---\n\n## Confirm the Target and Authenticate\n\nBefore an authenticated or multi-step operation, confirm:\n\n- The Jant site URL (e.g. `https://example.com`).\n- The exact outcome the user wants, including which content may change.\n- For bulk work, whether new posts should use `status: \"published\"` or `status: \"draft\"`.\n\nHave the user create an API token when the operation needs authentication:\n\n1. Sign in to Jant at `<site>/signin`.\n2. Open **Settings → API Tokens**.\n3. Create a token and copy it immediately. It is shown only once.\n\nSend the token as `Authorization: Bearer jnt_...`.\n\n```bash\nexport JANT_API_TOKEN=jnt_...\nexport JANT_SITE=https://example.com\n```\n\n---\n\n## Understand the Content Model\n\nJant is a single-author microblog. There are no users, comments, likes, followers, or social relationships to manage.\n\nThree concepts shape almost every content operation:\n\n1. **Posts have three formats** — `note`, `link`, and `quote`. Preserve the semantic format instead of coercing everything into `note`.\n2. **Threads connect posts** — a reply is a post whose `replyToId` points to another post. There is no separate comments table.\n3. **Collections curate Threads** — they are intentional groupings, not tags. A Thread can belong to multiple Collections; its root and replies share the same memberships.\n\n### `note` — original writing\n\nUse for essays, journal entries, status updates, photo posts with captions, and other content written by the site owner.\n\n- Required: `format: \"note\"`\n- Recommended: `bodyMarkdown`\n- Optional: `title`\n\n### `link` — a shared reference\n\nUse when the post is fundamentally pointing readers to another resource.\n\n- Required: `format: \"link\"`, `title`, `url`\n- Optional: `bodyMarkdown` for the owner's commentary\n\n### `quote` — a cited passage\n\nUse when the post is built around someone else's words.\n\n- Required: `format: \"quote\"`, `quoteText`\n- Optional: `sourceName`, `sourceUrl`, and `bodyMarkdown` for commentary\n- Do not send `title` or `url`; quote posts use `sourceName` and `sourceUrl` instead.\n\n---\n\n## Common Content Operations\n\nRead the current state before modifying existing content. Use `GET /api/posts/:id` when you need fields that may not appear in a list response, including Thread-level Collection memberships.\n\nCreate a note with an explicit status and visibility:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/posts\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"format\": \"note\",\n \"bodyMarkdown\": \"Hello from Jant.\",\n \"status\": \"published\",\n \"visibility\": \"public\"\n }'\n```\n\nUse `PUT /api/posts/:id` to update a post and `DELETE /api/posts/:id` to delete one. Fetch the post first, preserve fields the user did not ask to change, and confirm before deletion.\n\nUse `POST /api/upload` for an ordinary single-file script or one-time migration. Use the session-based `/api/uploads` flow for large files, unreliable connections, or clients that need resumable transport. Save the returned `med_*` ID before attaching media to a post.\n\nUse `GET /api/settings` before `PUT /api/settings`. Settings values are strings in the HTTP API; preserve settings outside the user's requested change.\n\nFor exact fields, filters, pagination, and response schemas, read the [full HTTP API reference](https://jant.me/docs/API.md).\n\n---\n\n## Import Content from Another Platform\n\nUse this workflow when the user wants to move an old blog, site, or archive into this Jant site. The request may arrive as one sentence, with no export and no token yet. Lead the user through the rest.\n\n### Lead the user\n\nAsk for one thing at a time, when the next step needs it. Name the menus the user clicks, not API endpoints.\n\n1. Check that you can send HTTP requests to `<site>` and read files the user gives you. If you cannot, say so first and suggest an assistant that can, such as a coding agent with shell access. Do not hand over a script unless the user asks for one.\n2. Ask which platform the old blog is on and where it lives.\n3. Get the content. Use an export if the user has one. If not, tell them where their platform exports it (the table under \"Read the source\" lists the usual places) and wait for the file. When the old blog is public and its feed carries full posts, you can read the feed instead. Feeds usually hold only recent posts, so compare the count with the old blog's archive before relying on one.\n4. Ask for the API token just before the first write, using the steps in \"Confirm the Target and Authenticate\". Do not ask for it at the start.\n\n### Plan before writing\n\n1. Inspect the target site so you know whether it already contains content.\n2. Inspect 3–5 representative source posts end to end before deciding how to transform anything.\n3. Report what you found: source formats, item counts, dates, media, internal links, categories, and anything unsupported.\n4. Ask how to handle ambiguous categories or tags. Map them to Collections, drop them, or fold them into post bodies only with the user's direction.\n5. Agree on draft versus published status and on a duplicate policy before creating posts.\n6. Build a resumable source-ID map and persist `{ sourceId -> jantId, slug }` after every successful write.\n\nA slug or path collision returns `409 CONFLICT`, but that does not make an import automatically idempotent. Items with new or generated slugs can still be duplicated, so use the source-ID map rather than relying on conflicts.\n\n### Read the source\n\nCommon source shapes include:\n\n| Source | Typical export |\n| --------------------- | ----------------------------------------------------------------------- |\n| WordPress | WXR XML (`Tools → Export`), with media URLs pointing at the source host |\n| Ghost | JSON export from `Settings → Labs → Export` |\n| Substack | ZIP with `posts.csv` and `posts/*.html` |\n| Medium | ZIP with `posts/*.html` |\n| Tumblr | API dump or `tumblr-utils` archive |\n| Hugo / Jekyll / Astro | Markdown files with YAML or TOML front matter |\n| Notion | Markdown plus a media ZIP |\n| Custom | SQL dump, HTML folder, RSS feed, or another structured source |\n\nDo not assume the source schema from the platform name alone. Check titles, bodies, dates, embedded media, internal links, categories, and source identifiers in real records.\n\n### Map source formats\n\nUse these heuristics as a starting point:\n\n- WordPress, Ghost, and Substack standard posts → `note` with `title` and `bodyMarkdown`.\n- Linklog posts that are primarily an external URL plus commentary → `link`.\n- WordPress or Tumblr quote posts → `quote`.\n- Tumblr photo posts with captions → `note` with uploaded images.\n- Tumblr link posts → `link`.\n- Reblogs or reposts → ask the user. Jant has no native reblog concept; `quote` with `sourceUrl` is often the closest representation.\n\nWhen uncertain, prefer `note`. Turning a real `link` or `quote` into a `note` loses semantic information, while turning original writing into a `link` or `quote` invents structure.\n\n### Convert bodies to Markdown\n\nSend `bodyMarkdown`, not `body`. The `body` field expects TipTap JSON and is intended for editor integrations.\n\n- Convert HTML with a real HTML-to-Markdown tool such as Turndown or Pandoc. Do not strip tags with regular expressions.\n- Preserve headings, lists, tables, fenced code blocks, blockquotes, and inline formatting.\n- A blank line starts a new paragraph. Use two trailing spaces or a backslash for a hard line break.\n- Preserve `<!--more-->` when the source has an intentional excerpt break.\n- Upload embedded media to Jant instead of leaving remote `<img>` references that may disappear with the old site.\n- Rewrite internal links after the destination slugs are known. Either build the slug map first or use a two-pass process: create posts, then update their bodies.\n\n### Upload media first\n\nPosts reference uploaded media by `med_*` ID. For a one-time migration, the one-shot endpoint is usually enough:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/upload\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -F \"file=@./photo.jpg\" \\\n -F \"alt=A red door\"\n# → { \"id\": \"med_...\", \"url\": \"/media/med_....jpg\", ... }\n```\n\nUse `/api/uploads` when a file is large or the connection is unreliable. Its session flow initializes the upload, transfers one or more parts, optionally adds a video poster, and completes the session to produce the final `med_*` record.\n\nPreserve source alt text. If the source has no alt text, leave it unset rather than inventing a description.\n\n### Create Collections when needed\n\nCreate approved Collections before the posts that use them:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/collections\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"slug\": \"reading\",\n \"title\": \"Reading\",\n \"description\": \"Books worth coming back to\",\n \"sortOrder\": \"newest\"\n }'\n```\n\nSave each returned `col_*` ID. Send `collectionIds` on standalone posts and Thread roots, not on replies.\n\n### Create posts\n\n`POST /api/posts` creates every post format. Send one post at a time and record the result before continuing.\n\nMinimal `note`:\n\n```json\n{\n \"format\": \"note\",\n \"title\": \"Hello world\",\n \"bodyMarkdown\": \"First post on the new site.\",\n \"publishedAt\": 1706000000,\n \"status\": \"draft\"\n}\n```\n\nMinimal `link`:\n\n```json\n{\n \"format\": \"link\",\n \"title\": \"An interesting article\",\n \"url\": \"https://example.com/post\",\n \"bodyMarkdown\": \"Worth your fifteen minutes.\"\n}\n```\n\nMinimal `quote`:\n\n```json\n{\n \"format\": \"quote\",\n \"quoteText\": \"What stands in the way becomes the way.\",\n \"sourceName\": \"Marcus Aurelius\",\n \"sourceUrl\": \"https://example.com/meditations\"\n}\n```\n\nFields commonly needed during migration:\n\n| Field | Purpose |\n| --------------- | ------------------------------------------------------------------------------------------ |\n| `publishedAt` | Original publication time as Unix seconds, preserving archive chronology. |\n| `slug` | Original stable slug when it should remain the canonical URL. |\n| `path` | Alternative to `slug` for an original path such as `2024/01/hello-world`; never send both. |\n| `status` | `published` or `draft`; set it explicitly according to the import plan. |\n| `visibility` | `public`, `latest_hidden`, or `private`. |\n| `pinned` | Preserve a pinned state when the source had one; replies cannot be pinned. |\n| `featured` | Preserve a featured or starred state when it has the same meaning. |\n| `collectionIds` | Approved `col_*` IDs on standalone posts and Thread roots. |\n| `attachments` | Ordered media or text attachments. |\n| `replyToId` | Destination `pst_*` ID of the parent when importing a Thread reply. |\n\nRemember these validation rules:\n\n- Send either `body` or `bodyMarkdown`, never both.\n- Send either `slug` or `path`, never both.\n- `note` rejects link- and quote-specific fields.\n- `link` requires `title` and `url`.\n- `quote` requires `quoteText`, uses `sourceName` and `sourceUrl`, and rejects `title` and `url`.\n\nA successful create returns `201` with the full post. Save `id`, `slug`, and `threadId` under the source ID for replies, link rewriting, verification, and recovery.\n\n### Rebuild Threads\n\n1. Import the Thread root first.\n2. Import replies in parent-first order with `replyToId` set to the destination parent's `pst_*` ID.\n3. Do not set `threadId`; Jant derives it from `replyToId`.\n4. Set `collectionIds` on the root only. Replies share the Thread's Collection memberships.\n5. Replies inherit root visibility and status unless explicitly created as drafts. They cannot be pinned or receive independent visibility changes.\n\n### Verify and report\n\nAfter the import:\n\n- Compare source and destination counts by format, media type, Collection, and Thread.\n- Fetch representative posts through `GET /api/posts/:id` and verify dates, slugs, bodies, attachments, Collection memberships, and reply relationships.\n- Open representative public URLs and verify rendered Markdown, media, and rewritten internal links.\n- Report anything skipped, transformed, duplicated, or unresolved, with a short list of URLs for manual review.\n\nIf an import stops partway through, resume from the persisted source-ID map. If cleanup is required, identify only the posts, media, and Collections created by that run, show the user the cleanup scope, and get confirmation before deleting them. Do not use account deletion as an automated recovery strategy.\n\n---\n\n## Handle Errors\n\nHTTP API errors use this shape:\n\n```json\n{ \"error\": \"...\", \"code\": \"VALIDATION_ERROR\", \"details\": {} }\n```\n\n| Code | Response |\n| --------------------- | ------------------------------------------------------------------------------------------------- |\n| `VALIDATION_ERROR` | Read `details.fieldErrors`, correct the request, and do not retry unchanged input. |\n| `UNAUTHORIZED` | Stop. The token is missing, invalid, or revoked; ask the user to create or provide a valid token. |\n| `FORBIDDEN` | Stop and explain which operation was denied. Do not look for an internal bypass. |\n| `NOT_FOUND` | Re-check the target site and resource ID before deciding whether the item was removed. |\n| `CONFLICT` | Resolve the reported state conflict; never silently change a user-selected canonical slug. |\n| `RATE_LIMIT` | Respect `Retry-After` when present, back off, and keep bulk writes sequential. |\n| `CONFIGURATION_ERROR` | Surface the server message; the site owner may need to correct deployment configuration. |\n\nFor MCP, transport and protocol failures use JSON-RPC errors. Tool validation and domain failures are returned as tool results with `isError: true`; inspect their structured content before retrying.\n\n---\n\n## Reference\n\n- Full HTTP API: <https://jant.me/docs/API.md> — endpoints, fields, filters, and response schemas.\n- Automation overview: <https://jant.me/docs/automation-and-api.md> — tokens, HTTP, MCP, and debugging.\n- Round-trip Jant exports: <https://jant.me/docs/export-and-import.md> — use `site export` and `site import` when both ends are Jant.\n- Public documentation index: <https://jant.me/docs/SUMMARY.md>.\n";
69895
70189
  //#endregion
69896
70190
  //#region src/lib/site-skill.ts
69897
70191
  var INTRODUCTION = "This page is written for AI assistants working with one Jant site. Use it when the user asks you to read, publish, edit, organize, search, or migrate site content through Jant's HTTP API or MCP interface.";
@@ -69920,7 +70214,7 @@ function replaceLiteral(content, search, replacement) {
69920
70214
  const normalizedSiteUrl = normalizeSiteUrl(siteUrl);
69921
70215
  if (!normalizedSiteUrl) throw new Error("A public site URL is required to render /skill.md");
69922
70216
  const targetSiteUrl = normalizedSiteUrl.replace(/\/$/, "");
69923
- if (!"---\nname: jant-site\ndescription: >-\n Work with the Jant site identified in this document through its HTTP API or\n MCP interface. Use when the user asks an AI assistant to read, publish, edit,\n organize, search, or migrate content, upload media, manage Collections, or\n update site settings.\n---\n\n# Jant Site Skill\n\nThis page is written for AI assistants working with one Jant site. Use it when the user asks you to read, publish, edit, organize, search, or migrate site content through Jant's HTTP API or MCP interface.\n\nThe full HTTP API reference lives at <https://jant.me/docs/API.md>. This skill explains how to choose the right interface, work safely, understand Jant's content model, and run common workflows. Consult the reference for complete request and response schemas instead of guessing fields.\n\n---\n\n## Scope and Safety\n\n- Use this skill only for the target site identified below. Do not substitute another site without the user's confirmation.\n- Start with read-only inspection when the state of the site or the user's intent is unclear.\n- Get explicit confirmation before bulk publishing, bulk deletion, destructive settings changes, or any action that is difficult to reverse. A request to perform a specific ordinary write, such as publishing one post, already supplies that confirmation.\n- For uncertain or bulk content changes, ask whether to save posts as drafts or publish them immediately.\n- Treat API tokens like passwords. Never log, commit, or repeat a token back to the user after they provide it.\n- Use the public API or MCP interface. Do not edit the database directly and do not call `/api/internal/*`; those endpoints belong to the hosted control plane.\n- Keep bulk writes sequential. Save returned IDs after each successful write so interrupted work can resume without creating duplicates.\n\n---\n\n## Choose an Interface\n\nJant exposes the same site-owner capabilities through two interfaces:\n\n| Interface | Use it when |\n| ----------------------- | ---------------------------------------------------------------------------------------------------- |\n| HTTP JSON API | Writing scripts, making a few direct requests, or running a one-time migration. This is the default. |\n| MCP at `<site>/api/mcp` | The caller already supports MCP and benefits from tool discovery and structured tool calls. |\n\nFor MCP, call `initialize`, then `tools/list`, and use the returned schemas rather than assuming tool arguments. The tool groups cover posts, media, attachments, Collections, settings, and search.\n\nFor HTTP, these are the main entry points:\n\n| Task | Endpoint |\n| ---------------------------- | ---------------------------------------------------------------- |\n| Read public posts | `GET /api/public/posts`, `GET /api/public/posts/:slug` |\n| List or inspect all posts | `GET /api/posts`, `GET /api/posts/:id` |\n| Create, update, delete | `POST /api/posts`, `PUT /api/posts/:id`, `DELETE /api/posts/:id` |\n| Upload or manage media | `/api/upload` or `/api/uploads` |\n| Manage Collections | `/api/collections` |\n| Search published content | `GET /api/search` |\n| Read or update site settings | `GET /api/settings`, `PUT /api/settings` |\n\nPublic post, Collection, navigation, and search reads can work without a token. A site can disable anonymous API reads; `/api/public/*` then becomes unavailable, while Collection, navigation, and search JSON reads require a browser session or Bearer token. Private content and all writes always require authentication.\n\n---\n\n## Confirm the Target and Authenticate\n\nBefore an authenticated or multi-step operation, confirm:\n\n- The Jant site URL (e.g. `https://example.com`).\n- The exact outcome the user wants, including which content may change.\n- For bulk work, whether new posts should use `status: \"published\"` or `status: \"draft\"`.\n\nHave the user create an API token when the operation needs authentication:\n\n1. Sign in to Jant at `<site>/signin`.\n2. Open **Settings → API Tokens**.\n3. Create a token and copy it immediately. It is shown only once.\n\nSend the token as `Authorization: Bearer jnt_...`.\n\n```bash\nexport JANT_API_TOKEN=jnt_...\nexport JANT_SITE=https://example.com\n```\n\n---\n\n## Understand the Content Model\n\nJant is a single-author microblog. There are no users, comments, likes, followers, or social relationships to manage.\n\nThree concepts shape almost every content operation:\n\n1. **Posts have three formats** — `note`, `link`, and `quote`. Preserve the semantic format instead of coercing everything into `note`.\n2. **Threads connect posts** — a reply is a post whose `replyToId` points to another post. There is no separate comments table.\n3. **Collections curate Threads** — they are intentional groupings, not tags. A Thread can belong to multiple Collections; its root and replies share the same memberships.\n\n### `note` — original writing\n\nUse for essays, journal entries, status updates, photo posts with captions, and other content written by the site owner.\n\n- Required: `format: \"note\"`\n- Recommended: `bodyMarkdown`\n- Optional: `title`\n\n### `link` — a shared reference\n\nUse when the post is fundamentally pointing readers to another resource.\n\n- Required: `format: \"link\"`, `title`, `url`\n- Optional: `bodyMarkdown` for the owner's commentary\n\n### `quote` — a cited passage\n\nUse when the post is built around someone else's words.\n\n- Required: `format: \"quote\"`, `quoteText`\n- Optional: `sourceName`, `sourceUrl`, and `bodyMarkdown` for commentary\n- Do not send `title` or `url`; quote posts use `sourceName` and `sourceUrl` instead.\n\n---\n\n## Common Content Operations\n\nRead the current state before modifying existing content. Use `GET /api/posts/:id` when you need fields that may not appear in a list response, including Thread-level Collection memberships.\n\nCreate a note with an explicit status and visibility:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/posts\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"format\": \"note\",\n \"bodyMarkdown\": \"Hello from Jant.\",\n \"status\": \"published\",\n \"visibility\": \"public\"\n }'\n```\n\nUse `PUT /api/posts/:id` to update a post and `DELETE /api/posts/:id` to delete one. Fetch the post first, preserve fields the user did not ask to change, and confirm before deletion.\n\nUse `POST /api/upload` for an ordinary single-file script or one-time migration. Use the session-based `/api/uploads` flow for large files, unreliable connections, or clients that need resumable transport. Save the returned `med_*` ID before attaching media to a post.\n\nUse `GET /api/settings` before `PUT /api/settings`. Settings values are strings in the HTTP API; preserve settings outside the user's requested change.\n\nFor exact fields, filters, pagination, and response schemas, read the [full HTTP API reference](https://jant.me/docs/API.md).\n\n---\n\n## Import Content from Another Platform\n\nUse this workflow when the user provides an export, archive, feed, database dump, or folder of source files from another blog or CMS.\n\n### Plan before writing\n\n1. Inspect the target site so you know whether it already contains content.\n2. Inspect 3–5 representative source posts end to end before deciding how to transform anything.\n3. Report what you found: source formats, item counts, dates, media, internal links, categories, and anything unsupported.\n4. Ask how to handle ambiguous categories or tags. Map them to Collections, drop them, or fold them into post bodies only with the user's direction.\n5. Agree on draft versus published status and on a duplicate policy before creating posts.\n6. Build a resumable source-ID map and persist `{ sourceId -> jantId, slug }` after every successful write.\n\nA slug or path collision returns `409 CONFLICT`, but that does not make an import automatically idempotent. Items with new or generated slugs can still be duplicated, so use the source-ID map rather than relying on conflicts.\n\n### Read the source\n\nCommon source shapes include:\n\n| Source | Typical export |\n| --------------------- | ----------------------------------------------------------------------- |\n| WordPress | WXR XML (`Tools → Export`), with media URLs pointing at the source host |\n| Ghost | JSON export from `Settings → Labs → Export` |\n| Substack | ZIP with `posts.csv` and `posts/*.html` |\n| Medium | ZIP with `posts/*.html` |\n| Tumblr | API dump or `tumblr-utils` archive |\n| Hugo / Jekyll / Astro | Markdown files with YAML or TOML front matter |\n| Notion | Markdown plus a media ZIP |\n| Custom | SQL dump, HTML folder, RSS feed, or another structured source |\n\nDo not assume the source schema from the platform name alone. Check titles, bodies, dates, embedded media, internal links, categories, and source identifiers in real records.\n\n### Map source formats\n\nUse these heuristics as a starting point:\n\n- WordPress, Ghost, and Substack standard posts → `note` with `title` and `bodyMarkdown`.\n- Linklog posts that are primarily an external URL plus commentary → `link`.\n- WordPress or Tumblr quote posts → `quote`.\n- Tumblr photo posts with captions → `note` with uploaded images.\n- Tumblr link posts → `link`.\n- Reblogs or reposts → ask the user. Jant has no native reblog concept; `quote` with `sourceUrl` is often the closest representation.\n\nWhen uncertain, prefer `note`. Turning a real `link` or `quote` into a `note` loses semantic information, while turning original writing into a `link` or `quote` invents structure.\n\n### Convert bodies to Markdown\n\nSend `bodyMarkdown`, not `body`. The `body` field expects TipTap JSON and is intended for editor integrations.\n\n- Convert HTML with a real HTML-to-Markdown tool such as Turndown or Pandoc. Do not strip tags with regular expressions.\n- Preserve headings, lists, tables, fenced code blocks, blockquotes, and inline formatting.\n- A blank line starts a new paragraph. Use two trailing spaces or a backslash for a hard line break.\n- Preserve `<!--more-->` when the source has an intentional excerpt break.\n- Upload embedded media to Jant instead of leaving remote `<img>` references that may disappear with the old site.\n- Rewrite internal links after the destination slugs are known. Either build the slug map first or use a two-pass process: create posts, then update their bodies.\n\n### Upload media first\n\nPosts reference uploaded media by `med_*` ID. For a one-time migration, the one-shot endpoint is usually enough:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/upload\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -F \"file=@./photo.jpg\" \\\n -F \"alt=A red door\"\n# → { \"id\": \"med_...\", \"url\": \"/media/med_....jpg\", ... }\n```\n\nUse `/api/uploads` when a file is large or the connection is unreliable. Its session flow initializes the upload, transfers one or more parts, optionally adds a video poster, and completes the session to produce the final `med_*` record.\n\nPreserve source alt text. If the source has no alt text, leave it unset rather than inventing a description.\n\n### Create Collections when needed\n\nCreate approved Collections before the posts that use them:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/collections\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"slug\": \"reading\",\n \"title\": \"Reading\",\n \"description\": \"Books worth coming back to\",\n \"sortOrder\": \"newest\"\n }'\n```\n\nSave each returned `col_*` ID. Send `collectionIds` on standalone posts and Thread roots, not on replies.\n\n### Create posts\n\n`POST /api/posts` creates every post format. Send one post at a time and record the result before continuing.\n\nMinimal `note`:\n\n```json\n{\n \"format\": \"note\",\n \"title\": \"Hello world\",\n \"bodyMarkdown\": \"First post on the new site.\",\n \"publishedAt\": 1706000000,\n \"status\": \"draft\"\n}\n```\n\nMinimal `link`:\n\n```json\n{\n \"format\": \"link\",\n \"title\": \"An interesting article\",\n \"url\": \"https://example.com/post\",\n \"bodyMarkdown\": \"Worth your fifteen minutes.\"\n}\n```\n\nMinimal `quote`:\n\n```json\n{\n \"format\": \"quote\",\n \"quoteText\": \"What stands in the way becomes the way.\",\n \"sourceName\": \"Marcus Aurelius\",\n \"sourceUrl\": \"https://example.com/meditations\"\n}\n```\n\nFields commonly needed during migration:\n\n| Field | Purpose |\n| --------------- | ------------------------------------------------------------------------------------------ |\n| `publishedAt` | Original publication time as Unix seconds, preserving archive chronology. |\n| `slug` | Original stable slug when it should remain the canonical URL. |\n| `path` | Alternative to `slug` for an original path such as `2024/01/hello-world`; never send both. |\n| `status` | `published` or `draft`; set it explicitly according to the import plan. |\n| `visibility` | `public`, `latest_hidden`, or `private`. |\n| `pinned` | Preserve a pinned state when the source had one; replies cannot be pinned. |\n| `featured` | Preserve a featured or starred state when it has the same meaning. |\n| `collectionIds` | Approved `col_*` IDs on standalone posts and Thread roots. |\n| `attachments` | Ordered media or text attachments. |\n| `replyToId` | Destination `pst_*` ID of the parent when importing a Thread reply. |\n\nRemember these validation rules:\n\n- Send either `body` or `bodyMarkdown`, never both.\n- Send either `slug` or `path`, never both.\n- `note` rejects link- and quote-specific fields.\n- `link` requires `title` and `url`.\n- `quote` requires `quoteText`, uses `sourceName` and `sourceUrl`, and rejects `title` and `url`.\n\nA successful create returns `201` with the full post. Save `id`, `slug`, and `threadId` under the source ID for replies, link rewriting, verification, and recovery.\n\n### Rebuild Threads\n\n1. Import the Thread root first.\n2. Import replies in parent-first order with `replyToId` set to the destination parent's `pst_*` ID.\n3. Do not set `threadId`; Jant derives it from `replyToId`.\n4. Set `collectionIds` on the root only. Replies share the Thread's Collection memberships.\n5. Replies inherit root visibility and status unless explicitly created as drafts. They cannot be pinned or receive independent visibility changes.\n\n### Verify and report\n\nAfter the import:\n\n- Compare source and destination counts by format, media type, Collection, and Thread.\n- Fetch representative posts through `GET /api/posts/:id` and verify dates, slugs, bodies, attachments, Collection memberships, and reply relationships.\n- Open representative public URLs and verify rendered Markdown, media, and rewritten internal links.\n- Report anything skipped, transformed, duplicated, or unresolved, with a short list of URLs for manual review.\n\nIf an import stops partway through, resume from the persisted source-ID map. If cleanup is required, identify only the posts, media, and Collections created by that run, show the user the cleanup scope, and get confirmation before deleting them. Do not use account deletion as an automated recovery strategy.\n\n---\n\n## Handle Errors\n\nHTTP API errors use this shape:\n\n```json\n{ \"error\": \"...\", \"code\": \"VALIDATION_ERROR\", \"details\": {} }\n```\n\n| Code | Response |\n| --------------------- | ------------------------------------------------------------------------------------------------- |\n| `VALIDATION_ERROR` | Read `details.fieldErrors`, correct the request, and do not retry unchanged input. |\n| `UNAUTHORIZED` | Stop. The token is missing, invalid, or revoked; ask the user to create or provide a valid token. |\n| `FORBIDDEN` | Stop and explain which operation was denied. Do not look for an internal bypass. |\n| `NOT_FOUND` | Re-check the target site and resource ID before deciding whether the item was removed. |\n| `CONFLICT` | Resolve the reported state conflict; never silently change a user-selected canonical slug. |\n| `RATE_LIMIT` | Respect `Retry-After` when present, back off, and keep bulk writes sequential. |\n| `CONFIGURATION_ERROR` | Surface the server message; the site owner may need to correct deployment configuration. |\n\nFor MCP, transport and protocol failures use JSON-RPC errors. Tool validation and domain failures are returned as tool results with `isError: true`; inspect their structured content before retrying.\n\n---\n\n## Reference\n\n- Full HTTP API: <https://jant.me/docs/API.md> — endpoints, fields, filters, and response schemas.\n- Automation overview: <https://jant.me/docs/automation-and-api.md> — tokens, HTTP, MCP, and debugging.\n- Round-trip Jant exports: <https://jant.me/docs/export-and-import.md> — use `site export` and `site import` when both ends are Jant.\n- Public documentation index: <https://jant.me/docs/SUMMARY.md>.\n".includes(INTRODUCTION)) throw new Error("The site skill template introduction could not be found");
70217
+ if (!"---\nname: jant-site\ndescription: >-\n Work with the Jant site identified in this document through its HTTP API or\n MCP interface. Use when the user asks an AI assistant to read, publish, edit,\n organize, search, or migrate content, upload media, manage Collections, or\n update site settings.\n---\n\n# Jant Site Skill\n\nThis page is written for AI assistants working with one Jant site. Use it when the user asks you to read, publish, edit, organize, search, or migrate site content through Jant's HTTP API or MCP interface.\n\nThe full HTTP API reference lives at <https://jant.me/docs/API.md>. This skill explains how to choose the right interface, work safely, understand Jant's content model, and run common workflows. Consult the reference for complete request and response schemas instead of guessing fields.\n\n---\n\n## Scope and Safety\n\n- Use this skill only for the target site identified below. Do not substitute another site without the user's confirmation.\n- Start with read-only inspection when the state of the site or the user's intent is unclear.\n- Get explicit confirmation before bulk publishing, bulk deletion, destructive settings changes, or any action that is difficult to reverse. A request to perform a specific ordinary write, such as publishing one post, already supplies that confirmation.\n- For uncertain or bulk content changes, ask whether to save posts as drafts or publish them immediately.\n- Treat API tokens like passwords. Never log, commit, or repeat a token back to the user after they provide it.\n- Use the public API or MCP interface. Do not edit the database directly and do not call `/api/internal/*`; those endpoints belong to the hosted control plane.\n- Keep bulk writes sequential. Save returned IDs after each successful write so interrupted work can resume without creating duplicates.\n\n---\n\n## Choose an Interface\n\nJant exposes the same site-owner capabilities through two interfaces:\n\n| Interface | Use it when |\n| ----------------------- | ---------------------------------------------------------------------------------------------------- |\n| HTTP JSON API | Writing scripts, making a few direct requests, or running a one-time migration. This is the default. |\n| MCP at `<site>/api/mcp` | The caller already supports MCP and benefits from tool discovery and structured tool calls. |\n\nFor MCP, call `initialize`, then `tools/list`, and use the returned schemas rather than assuming tool arguments. The tool groups cover posts, media, attachments, Collections, settings, and search.\n\nFor HTTP, these are the main entry points:\n\n| Task | Endpoint |\n| ---------------------------- | ---------------------------------------------------------------- |\n| Read public posts | `GET /api/public/posts`, `GET /api/public/posts/:slug` |\n| List or inspect all posts | `GET /api/posts`, `GET /api/posts/:id` |\n| Create, update, delete | `POST /api/posts`, `PUT /api/posts/:id`, `DELETE /api/posts/:id` |\n| Upload or manage media | `/api/upload` or `/api/uploads` |\n| Manage Collections | `/api/collections` |\n| Search published content | `GET /api/search` |\n| Read or update site settings | `GET /api/settings`, `PUT /api/settings` |\n\nPublic post, Collection, navigation, and search reads can work without a token. A site can disable anonymous API reads; `/api/public/*` then becomes unavailable, while Collection, navigation, and search JSON reads require a browser session or Bearer token. Private content and all writes always require authentication.\n\n---\n\n## Confirm the Target and Authenticate\n\nBefore an authenticated or multi-step operation, confirm:\n\n- The Jant site URL (e.g. `https://example.com`).\n- The exact outcome the user wants, including which content may change.\n- For bulk work, whether new posts should use `status: \"published\"` or `status: \"draft\"`.\n\nHave the user create an API token when the operation needs authentication:\n\n1. Sign in to Jant at `<site>/signin`.\n2. Open **Settings → API Tokens**.\n3. Create a token and copy it immediately. It is shown only once.\n\nSend the token as `Authorization: Bearer jnt_...`.\n\n```bash\nexport JANT_API_TOKEN=jnt_...\nexport JANT_SITE=https://example.com\n```\n\n---\n\n## Understand the Content Model\n\nJant is a single-author microblog. There are no users, comments, likes, followers, or social relationships to manage.\n\nThree concepts shape almost every content operation:\n\n1. **Posts have three formats** — `note`, `link`, and `quote`. Preserve the semantic format instead of coercing everything into `note`.\n2. **Threads connect posts** — a reply is a post whose `replyToId` points to another post. There is no separate comments table.\n3. **Collections curate Threads** — they are intentional groupings, not tags. A Thread can belong to multiple Collections; its root and replies share the same memberships.\n\n### `note` — original writing\n\nUse for essays, journal entries, status updates, photo posts with captions, and other content written by the site owner.\n\n- Required: `format: \"note\"`\n- Recommended: `bodyMarkdown`\n- Optional: `title`\n\n### `link` — a shared reference\n\nUse when the post is fundamentally pointing readers to another resource.\n\n- Required: `format: \"link\"`, `title`, `url`\n- Optional: `bodyMarkdown` for the owner's commentary\n\n### `quote` — a cited passage\n\nUse when the post is built around someone else's words.\n\n- Required: `format: \"quote\"`, `quoteText`\n- Optional: `sourceName`, `sourceUrl`, and `bodyMarkdown` for commentary\n- Do not send `title` or `url`; quote posts use `sourceName` and `sourceUrl` instead.\n\n---\n\n## Common Content Operations\n\nRead the current state before modifying existing content. Use `GET /api/posts/:id` when you need fields that may not appear in a list response, including Thread-level Collection memberships.\n\nCreate a note with an explicit status and visibility:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/posts\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"format\": \"note\",\n \"bodyMarkdown\": \"Hello from Jant.\",\n \"status\": \"published\",\n \"visibility\": \"public\"\n }'\n```\n\nUse `PUT /api/posts/:id` to update a post and `DELETE /api/posts/:id` to delete one. Fetch the post first, preserve fields the user did not ask to change, and confirm before deletion.\n\nUse `POST /api/upload` for an ordinary single-file script or one-time migration. Use the session-based `/api/uploads` flow for large files, unreliable connections, or clients that need resumable transport. Save the returned `med_*` ID before attaching media to a post.\n\nUse `GET /api/settings` before `PUT /api/settings`. Settings values are strings in the HTTP API; preserve settings outside the user's requested change.\n\nFor exact fields, filters, pagination, and response schemas, read the [full HTTP API reference](https://jant.me/docs/API.md).\n\n---\n\n## Import Content from Another Platform\n\nUse this workflow when the user wants to move an old blog, site, or archive into this Jant site. The request may arrive as one sentence, with no export and no token yet. Lead the user through the rest.\n\n### Lead the user\n\nAsk for one thing at a time, when the next step needs it. Name the menus the user clicks, not API endpoints.\n\n1. Check that you can send HTTP requests to `<site>` and read files the user gives you. If you cannot, say so first and suggest an assistant that can, such as a coding agent with shell access. Do not hand over a script unless the user asks for one.\n2. Ask which platform the old blog is on and where it lives.\n3. Get the content. Use an export if the user has one. If not, tell them where their platform exports it (the table under \"Read the source\" lists the usual places) and wait for the file. When the old blog is public and its feed carries full posts, you can read the feed instead. Feeds usually hold only recent posts, so compare the count with the old blog's archive before relying on one.\n4. Ask for the API token just before the first write, using the steps in \"Confirm the Target and Authenticate\". Do not ask for it at the start.\n\n### Plan before writing\n\n1. Inspect the target site so you know whether it already contains content.\n2. Inspect 3–5 representative source posts end to end before deciding how to transform anything.\n3. Report what you found: source formats, item counts, dates, media, internal links, categories, and anything unsupported.\n4. Ask how to handle ambiguous categories or tags. Map them to Collections, drop them, or fold them into post bodies only with the user's direction.\n5. Agree on draft versus published status and on a duplicate policy before creating posts.\n6. Build a resumable source-ID map and persist `{ sourceId -> jantId, slug }` after every successful write.\n\nA slug or path collision returns `409 CONFLICT`, but that does not make an import automatically idempotent. Items with new or generated slugs can still be duplicated, so use the source-ID map rather than relying on conflicts.\n\n### Read the source\n\nCommon source shapes include:\n\n| Source | Typical export |\n| --------------------- | ----------------------------------------------------------------------- |\n| WordPress | WXR XML (`Tools → Export`), with media URLs pointing at the source host |\n| Ghost | JSON export from `Settings → Labs → Export` |\n| Substack | ZIP with `posts.csv` and `posts/*.html` |\n| Medium | ZIP with `posts/*.html` |\n| Tumblr | API dump or `tumblr-utils` archive |\n| Hugo / Jekyll / Astro | Markdown files with YAML or TOML front matter |\n| Notion | Markdown plus a media ZIP |\n| Custom | SQL dump, HTML folder, RSS feed, or another structured source |\n\nDo not assume the source schema from the platform name alone. Check titles, bodies, dates, embedded media, internal links, categories, and source identifiers in real records.\n\n### Map source formats\n\nUse these heuristics as a starting point:\n\n- WordPress, Ghost, and Substack standard posts → `note` with `title` and `bodyMarkdown`.\n- Linklog posts that are primarily an external URL plus commentary → `link`.\n- WordPress or Tumblr quote posts → `quote`.\n- Tumblr photo posts with captions → `note` with uploaded images.\n- Tumblr link posts → `link`.\n- Reblogs or reposts → ask the user. Jant has no native reblog concept; `quote` with `sourceUrl` is often the closest representation.\n\nWhen uncertain, prefer `note`. Turning a real `link` or `quote` into a `note` loses semantic information, while turning original writing into a `link` or `quote` invents structure.\n\n### Convert bodies to Markdown\n\nSend `bodyMarkdown`, not `body`. The `body` field expects TipTap JSON and is intended for editor integrations.\n\n- Convert HTML with a real HTML-to-Markdown tool such as Turndown or Pandoc. Do not strip tags with regular expressions.\n- Preserve headings, lists, tables, fenced code blocks, blockquotes, and inline formatting.\n- A blank line starts a new paragraph. Use two trailing spaces or a backslash for a hard line break.\n- Preserve `<!--more-->` when the source has an intentional excerpt break.\n- Upload embedded media to Jant instead of leaving remote `<img>` references that may disappear with the old site.\n- Rewrite internal links after the destination slugs are known. Either build the slug map first or use a two-pass process: create posts, then update their bodies.\n\n### Upload media first\n\nPosts reference uploaded media by `med_*` ID. For a one-time migration, the one-shot endpoint is usually enough:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/upload\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -F \"file=@./photo.jpg\" \\\n -F \"alt=A red door\"\n# → { \"id\": \"med_...\", \"url\": \"/media/med_....jpg\", ... }\n```\n\nUse `/api/uploads` when a file is large or the connection is unreliable. Its session flow initializes the upload, transfers one or more parts, optionally adds a video poster, and completes the session to produce the final `med_*` record.\n\nPreserve source alt text. If the source has no alt text, leave it unset rather than inventing a description.\n\n### Create Collections when needed\n\nCreate approved Collections before the posts that use them:\n\n```bash\ncurl -X POST \"$JANT_SITE/api/collections\" \\\n -H \"Authorization: Bearer $JANT_API_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"slug\": \"reading\",\n \"title\": \"Reading\",\n \"description\": \"Books worth coming back to\",\n \"sortOrder\": \"newest\"\n }'\n```\n\nSave each returned `col_*` ID. Send `collectionIds` on standalone posts and Thread roots, not on replies.\n\n### Create posts\n\n`POST /api/posts` creates every post format. Send one post at a time and record the result before continuing.\n\nMinimal `note`:\n\n```json\n{\n \"format\": \"note\",\n \"title\": \"Hello world\",\n \"bodyMarkdown\": \"First post on the new site.\",\n \"publishedAt\": 1706000000,\n \"status\": \"draft\"\n}\n```\n\nMinimal `link`:\n\n```json\n{\n \"format\": \"link\",\n \"title\": \"An interesting article\",\n \"url\": \"https://example.com/post\",\n \"bodyMarkdown\": \"Worth your fifteen minutes.\"\n}\n```\n\nMinimal `quote`:\n\n```json\n{\n \"format\": \"quote\",\n \"quoteText\": \"What stands in the way becomes the way.\",\n \"sourceName\": \"Marcus Aurelius\",\n \"sourceUrl\": \"https://example.com/meditations\"\n}\n```\n\nFields commonly needed during migration:\n\n| Field | Purpose |\n| --------------- | ------------------------------------------------------------------------------------------ |\n| `publishedAt` | Original publication time as Unix seconds, preserving archive chronology. |\n| `slug` | Original stable slug when it should remain the canonical URL. |\n| `path` | Alternative to `slug` for an original path such as `2024/01/hello-world`; never send both. |\n| `status` | `published` or `draft`; set it explicitly according to the import plan. |\n| `visibility` | `public`, `latest_hidden`, or `private`. |\n| `pinned` | Preserve a pinned state when the source had one; replies cannot be pinned. |\n| `featured` | Preserve a featured or starred state when it has the same meaning. |\n| `collectionIds` | Approved `col_*` IDs on standalone posts and Thread roots. |\n| `attachments` | Ordered media or text attachments. |\n| `replyToId` | Destination `pst_*` ID of the parent when importing a Thread reply. |\n\nRemember these validation rules:\n\n- Send either `body` or `bodyMarkdown`, never both.\n- Send either `slug` or `path`, never both.\n- `note` rejects link- and quote-specific fields.\n- `link` requires `title` and `url`.\n- `quote` requires `quoteText`, uses `sourceName` and `sourceUrl`, and rejects `title` and `url`.\n\nA successful create returns `201` with the full post. Save `id`, `slug`, and `threadId` under the source ID for replies, link rewriting, verification, and recovery.\n\n### Rebuild Threads\n\n1. Import the Thread root first.\n2. Import replies in parent-first order with `replyToId` set to the destination parent's `pst_*` ID.\n3. Do not set `threadId`; Jant derives it from `replyToId`.\n4. Set `collectionIds` on the root only. Replies share the Thread's Collection memberships.\n5. Replies inherit root visibility and status unless explicitly created as drafts. They cannot be pinned or receive independent visibility changes.\n\n### Verify and report\n\nAfter the import:\n\n- Compare source and destination counts by format, media type, Collection, and Thread.\n- Fetch representative posts through `GET /api/posts/:id` and verify dates, slugs, bodies, attachments, Collection memberships, and reply relationships.\n- Open representative public URLs and verify rendered Markdown, media, and rewritten internal links.\n- Report anything skipped, transformed, duplicated, or unresolved, with a short list of URLs for manual review.\n\nIf an import stops partway through, resume from the persisted source-ID map. If cleanup is required, identify only the posts, media, and Collections created by that run, show the user the cleanup scope, and get confirmation before deleting them. Do not use account deletion as an automated recovery strategy.\n\n---\n\n## Handle Errors\n\nHTTP API errors use this shape:\n\n```json\n{ \"error\": \"...\", \"code\": \"VALIDATION_ERROR\", \"details\": {} }\n```\n\n| Code | Response |\n| --------------------- | ------------------------------------------------------------------------------------------------- |\n| `VALIDATION_ERROR` | Read `details.fieldErrors`, correct the request, and do not retry unchanged input. |\n| `UNAUTHORIZED` | Stop. The token is missing, invalid, or revoked; ask the user to create or provide a valid token. |\n| `FORBIDDEN` | Stop and explain which operation was denied. Do not look for an internal bypass. |\n| `NOT_FOUND` | Re-check the target site and resource ID before deciding whether the item was removed. |\n| `CONFLICT` | Resolve the reported state conflict; never silently change a user-selected canonical slug. |\n| `RATE_LIMIT` | Respect `Retry-After` when present, back off, and keep bulk writes sequential. |\n| `CONFIGURATION_ERROR` | Surface the server message; the site owner may need to correct deployment configuration. |\n\nFor MCP, transport and protocol failures use JSON-RPC errors. Tool validation and domain failures are returned as tool results with `isError: true`; inspect their structured content before retrying.\n\n---\n\n## Reference\n\n- Full HTTP API: <https://jant.me/docs/API.md> — endpoints, fields, filters, and response schemas.\n- Automation overview: <https://jant.me/docs/automation-and-api.md> — tokens, HTTP, MCP, and debugging.\n- Round-trip Jant exports: <https://jant.me/docs/export-and-import.md> — use `site export` and `site import` when both ends are Jant.\n- Public documentation index: <https://jant.me/docs/SUMMARY.md>.\n".includes(INTRODUCTION)) throw new Error("The site skill template introduction could not be found");
69924
70218
  const missingMarker = GENERIC_SITE_MARKERS.find((marker) => !skill_default.includes(marker));
69925
70219
  if (missingMarker) throw new Error(`The site skill template target marker could not be found: ${missingMarker}`);
69926
70220
  let content = skill_default;
@@ -71769,7 +72063,7 @@ function createSettingsService(db, siteId, databaseSchema = sqliteSchemaBundle,
71769
72063
  return result[0] ? toCustomUrl(result[0]) : null;
71770
72064
  },
71771
72065
  async getByTarget(targetType, targetId) {
71772
- const result = await db.select().from(pathRegistry).where(and(eq(pathRegistry.siteId, siteId), eq(pathRegistry.kind, "alias"), targetType === "post" ? eq(pathRegistry.postId, targetId) : eq(pathRegistry.collectionId, targetId))).orderBy(desc(pathRegistry.createdAt)).limit(1);
72066
+ const result = await db.select().from(pathRegistry).where(and(eq(pathRegistry.siteId, siteId), eq(pathRegistry.kind, "alias"), targetType === "post" ? eq(pathRegistry.postId, targetId) : eq(pathRegistry.collectionId, targetId))).orderBy(desc(pathRegistry.createdAt), desc(pathRegistry.id)).limit(1);
71773
72067
  return result[0] ? toCustomUrl(result[0]) : null;
71774
72068
  },
71775
72069
  async create(data) {
@@ -71809,7 +72103,7 @@ function createSettingsService(db, siteId, databaseSchema = sqliteSchemaBundle,
71809
72103
  return (await db.select({ count: sql`CAST(count(*) AS INTEGER)`.as("count") }).from(pathRegistry).where(and(eq(pathRegistry.siteId, siteId), ne(pathRegistry.kind, "slug"))))[0]?.count ?? 0;
71810
72104
  },
71811
72105
  async list(opts) {
71812
- let q = db.select().from(pathRegistry).where(and(eq(pathRegistry.siteId, siteId), ne(pathRegistry.kind, "slug"))).orderBy(desc(pathRegistry.createdAt)).$dynamic();
72106
+ let q = db.select().from(pathRegistry).where(and(eq(pathRegistry.siteId, siteId), ne(pathRegistry.kind, "slug"))).orderBy(desc(pathRegistry.createdAt), desc(pathRegistry.id)).$dynamic();
71813
72107
  if (opts?.limit !== void 0) q = q.limit(opts.limit);
71814
72108
  if (opts?.offset !== void 0) q = q.offset(opts.offset);
71815
72109
  return (await q).map(toCustomUrl);
@@ -71847,6 +72141,7 @@ function toSiteDomain$1(row) {
71847
72141
  };
71848
72142
  }
71849
72143
  var TRANSIENT_SINGLE_SITE_ID = "sit_pending";
72144
+ /** Joined-row ceiling for the single-site read — see `loadSingleSiteWithDomain`. */ var SINGLE_SITE_JOIN_SCAN_LIMIT = 25;
71850
72145
  function createTransientSite(key = "default") {
71851
72146
  return {
71852
72147
  id: TRANSIENT_SINGLE_SITE_ID,
@@ -71869,6 +72164,37 @@ function createSiteService(db, databaseSchema = sqliteSchemaBundle) {
71869
72164
  if (rows.length > 1) throw createSingleSiteModeConfigurationError(rows);
71870
72165
  return rows[0];
71871
72166
  }
72167
+ /**
72168
+ * The instance's one site together with its oldest domain, in one read.
72169
+ *
72170
+ * Every request resolves the site before it can do anything else, so the two
72171
+ * reads this replaces sat at the head of the chain, one waiting on the other.
72172
+ * Joining them keeps the domain ordering the separate read had — oldest
72173
+ * first — and the multi-site guard that read carried.
72174
+ *
72175
+ * The limit bounds the scan, but it now bounds *joined* rows rather than
72176
+ * sites, so one site with many domains could in principle fill the window and
72177
+ * hide a second site from the guard. That case falls back to
72178
+ * {@link loadSingleSiteRow}, which bounds sites; a single-site install has a
72179
+ * handful of domains, so it is not reached in practice.
72180
+ */ async function loadSingleSiteWithDomain() {
72181
+ const rows = await db.select({
72182
+ site: sites,
72183
+ domain: siteDomains
72184
+ }).from(sites).leftJoin(siteDomains, eq(siteDomains.siteId, sites.id)).orderBy(asc(sites.createdAt), asc(siteDomains.createdAt)).limit(SINGLE_SITE_JOIN_SCAN_LIMIT);
72185
+ const distinctSites = /* @__PURE__ */ new Map();
72186
+ for (const row of rows) distinctSites.set(row.site.id, row.site);
72187
+ if (distinctSites.size > 1) throw createSingleSiteModeConfigurationError([...distinctSites.values()]);
72188
+ const domain = rows.find((row) => row.domain)?.domain ?? void 0;
72189
+ if (rows.length === SINGLE_SITE_JOIN_SCAN_LIMIT) return {
72190
+ site: await loadSingleSiteRow(),
72191
+ domain
72192
+ };
72193
+ return {
72194
+ site: rows[0]?.site,
72195
+ domain
72196
+ };
72197
+ }
71872
72198
  return {
71873
72199
  async list() {
71874
72200
  return (await db.select().from(sites).orderBy(asc(sites.createdAt))).map(toSite$1);
@@ -71893,10 +72219,14 @@ function createSiteService(db, databaseSchema = sqliteSchemaBundle) {
71893
72219
  },
71894
72220
  async resolveSingleSite(options = {}) {
71895
72221
  const shouldCreateIfMissing = options.createIfMissing ?? false;
71896
- const existingRow = await loadSingleSiteRow();
72222
+ const loaded = options.host ? await loadSingleSiteWithDomain() : {
72223
+ site: await loadSingleSiteRow(),
72224
+ domain: void 0
72225
+ };
72226
+ const existingRow = loaded.site;
71897
72227
  const timestamp = now();
71898
72228
  const created = existingRow ? existingRow : shouldCreateIfMissing ? (await db.insert(sites).values({
71899
- id: createEntityId("site"),
72229
+ id: options.id ?? createEntityId("site"),
71900
72230
  key: options.key?.trim() || "default",
71901
72231
  status: "active",
71902
72232
  createdAt: timestamp,
@@ -71906,20 +72236,17 @@ function createSiteService(db, databaseSchema = sqliteSchemaBundle) {
71906
72236
  site: createTransientSite(options.key?.trim() || "default"),
71907
72237
  domain: null
71908
72238
  };
71909
- let domainRow;
71910
- if (options.host) {
71911
- domainRow = (await db.select().from(siteDomains).where(eq(siteDomains.siteId, created.id)).orderBy(asc(siteDomains.createdAt)).limit(1))[0];
71912
- if (!domainRow && shouldCreateIfMissing) domainRow = (await db.insert(siteDomains).values({
71913
- id: createEntityId("siteDomain"),
71914
- siteId: created.id,
71915
- host: options.host,
71916
- pathPrefix: options.pathPrefix?.trim() || null,
71917
- kind: "primary",
71918
- redirectToPrimary: true,
71919
- createdAt: timestamp,
71920
- updatedAt: timestamp
71921
- }).returning())[0];
71922
- }
72239
+ let domainRow = existingRow ? loaded.domain : void 0;
72240
+ if (options.host && !domainRow && shouldCreateIfMissing) domainRow = (await db.insert(siteDomains).values({
72241
+ id: createEntityId("siteDomain"),
72242
+ siteId: created.id,
72243
+ host: options.host,
72244
+ pathPrefix: options.pathPrefix?.trim() || null,
72245
+ kind: "primary",
72246
+ redirectToPrimary: true,
72247
+ createdAt: timestamp,
72248
+ updatedAt: timestamp
72249
+ }).returning())[0];
71923
72250
  return {
71924
72251
  site: toSite$1(created),
71925
72252
  domain: domainRow ? toSiteDomain$1(domainRow) : null
@@ -73966,6 +74293,9 @@ function createApiTokenService(db, siteId, databaseSchema = sqliteSchemaBundle)
73966
74293
  * the second act runs, against the site the request resolved — the same site
73967
74294
  * the route's membership check reads. On a hosted install the database holds
73968
74295
  * every tenant, so "the only site" is not a question with an answer there.
74296
+ *
74297
+ * A self-hosted install that is deployed rather than clicked through runs both
74298
+ * acts back to back without a browser — see `setUpInstance`.
73969
74299
  */
73970
74300
  /**
73971
74301
  * Create the first-run setup service for one request.
@@ -73989,43 +74319,136 @@ function createApiTokenService(db, siteId, databaseSchema = sqliteSchemaBundle)
73989
74319
  if (siteId === "sit_pending") throw new Error(`${step} needs an existing site, but the request resolved none.`);
73990
74320
  return siteId;
73991
74321
  }
74322
+ function settingsFor(targetSiteId) {
74323
+ return createSettingsService(db, targetSiteId, databaseSchema, dialect);
74324
+ }
73992
74325
  /**
73993
- * The site the account step stands up. A self-hosted install has no row
73994
- * until now, so this is the step that creates it — along with its domain,
73995
- * when one is configured.
73996
- *
73997
74326
  * A host-based install refuses before anything is written. The control
73998
74327
  * plane creates each hosted site already provisioned, and its owner arrives
73999
74328
  * through the hosted handoff, the only way anyone becomes a member of one.
74000
74329
  * Attaching whoever just signed in to the site the host resolved would let
74001
74330
  * any account in a database every tenant shares take a site that had lost
74002
74331
  * its onboarding status.
74003
- */ async function siteToProvision() {
74004
- if (siteResolutionMode === "host-based") throw new Error("provisionOwnerAccount runs on self-hosted installs only. A hosted site's owner arrives through the control plane's handoff.");
74005
- const { site } = await createSiteService(db, databaseSchema).ensureSingleSite(options?.bootstrapSite);
74332
+ */ function assertSelfHosted(step) {
74333
+ if (siteResolutionMode === "host-based") throw new Error(`${step} runs on self-hosted installs only. A hosted site's owner arrives through the control plane's handoff.`);
74334
+ }
74335
+ /**
74336
+ * Stand the site up around its owner. A self-hosted install has no row until
74337
+ * now, so this is the step that creates it — along with its domain, when one
74338
+ * is configured.
74339
+ *
74340
+ * @returns The site's id
74341
+ */ async function provisionSite(step, ownerUserId, siteOptions) {
74342
+ assertSelfHosted(step);
74343
+ const { site } = await createSiteService(db, databaseSchema).ensureSingleSite(siteOptions);
74344
+ const navItems = createNavItemService(db, site.id, databaseSchema);
74345
+ await createSiteMemberService(db, databaseSchema).ensure(site.id, ownerUserId, "owner");
74346
+ await navItems.materializeDefaultNavigation();
74347
+ await settingsFor(site.id).markSiteProvisioned();
74006
74348
  return site.id;
74007
74349
  }
74350
+ async function recordSiteAnswers(targetSiteId, data, opts, deps) {
74351
+ const settings = settingsFor(targetSiteId);
74352
+ const siteName = data.siteName?.trim();
74353
+ if (siteName) {
74354
+ await settings.set("SITE_NAME", siteName);
74355
+ await deps?.updateCurrentUserName?.(siteName);
74356
+ }
74357
+ if (data.timeZone !== void 0) await settings.set("TIME_ZONE", data.timeZone ?? "UTC");
74358
+ await settings.confirmFirstRunLanguage({
74359
+ siteLanguage: data.siteLanguage ?? "",
74360
+ browserLanguage: data.browserLanguage
74361
+ }, opts);
74362
+ }
74363
+ /**
74364
+ * The account a browserless setup stands the site up around: a new one when
74365
+ * the install has none, or the one an interrupted run left, claimed with its
74366
+ * own password — the way the first screen resumes by signing in.
74367
+ */ async function claimOwnerAccount(email, password) {
74368
+ const { account, user } = databaseSchema;
74369
+ const [existing] = await db.select({
74370
+ id: user.id,
74371
+ passwordHash: account.password
74372
+ }).from(user).leftJoin(account, and(eq(account.userId, user.id), eq(account.providerId, "credential"))).where(eq(user.email, email)).limit(1);
74373
+ if (existing) {
74374
+ if (!(!!existing.passwordHash && await verifyPassword$1({
74375
+ hash: existing.passwordHash,
74376
+ password
74377
+ }))) throw new Error(`An earlier setup created the account for ${email}, and the password given does not match it.`);
74378
+ return {
74379
+ userId: existing.id,
74380
+ created: false
74381
+ };
74382
+ }
74383
+ const [other] = await db.select({ id: user.id }).from(user).limit(1);
74384
+ if (other) throw new Error(`This install already has an account under another address. Pass that account's address and password to finish setup.`);
74385
+ const userId = createTypeId(AUTH_ID_PREFIX.user);
74386
+ const timestamp = /* @__PURE__ */ new Date(now() * 1e3);
74387
+ await db.insert(user).values({
74388
+ id: userId,
74389
+ email,
74390
+ emailVerified: false,
74391
+ image: null,
74392
+ name: deriveAccountName(email),
74393
+ role: "admin",
74394
+ createdAt: timestamp,
74395
+ updatedAt: timestamp
74396
+ });
74397
+ await db.insert(account).values({
74398
+ id: createTypeId(AUTH_ID_PREFIX.account),
74399
+ accountId: userId,
74400
+ userId,
74401
+ providerId: "credential",
74402
+ password: await hashPassword(password),
74403
+ accessToken: null,
74404
+ accessTokenExpiresAt: null,
74405
+ idToken: null,
74406
+ refreshToken: null,
74407
+ refreshTokenExpiresAt: null,
74408
+ scope: null,
74409
+ createdAt: timestamp,
74410
+ updatedAt: timestamp
74411
+ });
74412
+ return {
74413
+ userId,
74414
+ created: true
74415
+ };
74416
+ }
74008
74417
  return {
74009
74418
  async provisionOwnerAccount(data) {
74010
- const provisionedSiteId = await siteToProvision();
74011
- const settings = createSettingsService(db, provisionedSiteId, databaseSchema, dialect);
74012
- const navItems = createNavItemService(db, provisionedSiteId, databaseSchema);
74013
- await createSiteMemberService(db, databaseSchema).ensure(provisionedSiteId, data.ownerUserId, "owner");
74014
- await navItems.materializeDefaultNavigation();
74015
- await settings.markSiteProvisioned();
74419
+ await provisionSite("provisionOwnerAccount", data.ownerUserId, options?.bootstrapSite);
74016
74420
  },
74017
74421
  async completeSiteSetup(data, opts, deps) {
74018
- const settings = createSettingsService(db, existingSiteId("completeSiteSetup"), databaseSchema, dialect);
74019
- const siteName = data.siteName?.trim();
74020
- if (siteName) {
74021
- await settings.set("SITE_NAME", siteName);
74022
- await deps?.updateCurrentUserName?.(siteName);
74023
- }
74024
- if (data.timeZone !== void 0) await settings.set("TIME_ZONE", data.timeZone ?? "UTC");
74025
- await settings.confirmFirstRunLanguage({
74026
- siteLanguage: data.siteLanguage ?? "",
74027
- browserLanguage: data.browserLanguage
74028
- }, opts);
74422
+ await recordSiteAnswers(existingSiteId("completeSiteSetup"), data, opts, deps);
74423
+ },
74424
+ async setUpInstance(data) {
74425
+ assertSelfHosted("setUpInstance");
74426
+ const existingSite = await createSiteService(db, databaseSchema).getOnlySite();
74427
+ if (existingSite && data.siteId !== void 0 && existingSite.id !== data.siteId) throw new Error(`This install's site is ${existingSite.id}, not ${data.siteId}. Check that the database is the one you meant.`);
74428
+ if (existingSite && await settingsFor(existingSite.id).isOnboardingComplete()) return {
74429
+ outcome: "already-set-up",
74430
+ siteId: existingSite.id
74431
+ };
74432
+ const owner = await claimOwnerAccount(data.email, data.password);
74433
+ const setUpSiteId = await provisionSite("setUpInstance", owner.userId, {
74434
+ ...options?.bootstrapSite,
74435
+ id: data.siteId
74436
+ });
74437
+ const { user } = databaseSchema;
74438
+ await recordSiteAnswers(setUpSiteId, {
74439
+ siteName: data.siteName,
74440
+ siteLanguage: data.siteLanguage,
74441
+ timeZone: data.timeZone ?? null
74442
+ }, { oldLanguage: await settingsFor(setUpSiteId).get("SITE_LANGUAGE") ?? "" }, { async updateCurrentUserName(displayName) {
74443
+ await db.update(user).set({
74444
+ name: displayName,
74445
+ updatedAt: /* @__PURE__ */ new Date(now() * 1e3)
74446
+ }).where(eq(user.id, owner.userId));
74447
+ } });
74448
+ return {
74449
+ outcome: owner.created ? "created" : "resumed",
74450
+ siteId: setUpSiteId
74451
+ };
74029
74452
  }
74030
74453
  };
74031
74454
  }
@@ -74299,7 +74722,6 @@ function createSiteAdminService(db, databaseSchema = sqliteSchemaBundle, databas
74299
74722
  const themeCss = buildThemeStyle(activeTheme, appConfig.themeMode, fontOverrides);
74300
74723
  const navItemList = await navItems.list();
74301
74724
  const appleTouchKey = allSettings[SETTINGS_KEYS.SITE_FAVICON_APPLE_TOUCH];
74302
- const { createExportService } = await import("./github-sync-BPAvT999.js").then((n) => n.c);
74303
74725
  const exportService = createExportService({
74304
74726
  collections,
74305
74727
  media: mediaService,
@@ -74852,8 +75274,48 @@ function createUploadSessionService(db, siteId, media, databaseSchema = sqliteSc
74852
75274
  * Routes call these methods instead of touching the settings table to
74853
75275
  * keep the model relational (one entry per (installation_id, site_id)
74854
75276
  * pair) instead of serialising a JSON list into a single settings row.
75277
+ *
75278
+ * Two scopes matter and they are not interchangeable. A *site* scope
75279
+ * answers "which accounts is this blog syncing through". A *user* scope
75280
+ * answers "which accounts has this person already authorized anywhere",
75281
+ * which is what the connect flow needs: a GitHub App installs once per
75282
+ * GitHub account, so someone who connected their first site can never
75283
+ * install it a second time — GitHub sends them to the installation's
75284
+ * Configure page and never calls back. Their other sites have to be able
75285
+ * to reuse that installation without leaving Jant.
74855
75286
  */ function createGitHubAppInstallationsService(db, databaseSchema = sqliteSchemaBundle) {
74856
- const { githubAppInstallation, settings } = databaseSchema;
75287
+ const { githubAppInstallation, settings, siteMembers } = databaseSchema;
75288
+ /**
75289
+ * Rows for every binding on a site the user belongs to. One join, not
75290
+ * a per-site fan-out — a user has few sites but the query shape is
75291
+ * what gets copied.
75292
+ */ function selectUserBindings(userId, installationId) {
75293
+ const scope = eq(siteMembers.userId, userId);
75294
+ return db.select({
75295
+ installationId: githubAppInstallation.installationId,
75296
+ accountLogin: githubAppInstallation.accountLogin,
75297
+ accountType: githubAppInstallation.accountType,
75298
+ accountAvatarUrl: githubAppInstallation.accountAvatarUrl,
75299
+ addedAt: githubAppInstallation.addedAt
75300
+ }).from(githubAppInstallation).innerJoin(siteMembers, eq(siteMembers.siteId, githubAppInstallation.siteId)).where(installationId ? and(scope, eq(githubAppInstallation.installationId, installationId)) : scope);
75301
+ }
75302
+ /** Collapse per-site bindings into one entry per installation. */ function toVisible(rows) {
75303
+ const byInstallation = /* @__PURE__ */ new Map();
75304
+ for (const row of rows) {
75305
+ const entry = {
75306
+ installationId: row.installationId,
75307
+ account: {
75308
+ login: row.accountLogin,
75309
+ type: toAccountType(row.accountType),
75310
+ avatarUrl: row.accountAvatarUrl
75311
+ },
75312
+ addedAt: row.addedAt
75313
+ };
75314
+ const existing = byInstallation.get(row.installationId);
75315
+ if (!existing || existing.addedAt < entry.addedAt) byInstallation.set(row.installationId, entry);
75316
+ }
75317
+ return [...byInstallation.values()].sort((a, b) => b.addedAt - a.addedAt);
75318
+ }
74857
75319
  async function writeSetting(siteId, key, value) {
74858
75320
  const timestamp = now();
74859
75321
  await db.insert(settings).values({
@@ -74888,6 +75350,18 @@ function createUploadSessionService(db, siteId, media, databaseSchema = sqliteSc
74888
75350
  async listInstallationsForSite(siteId) {
74889
75351
  return (await db.select().from(githubAppInstallation).where(eq(githubAppInstallation.siteId, siteId))).map(toStored).sort((a, b) => b.addedAt - a.addedAt);
74890
75352
  },
75353
+ async listInstallationsForUser(userId) {
75354
+ return toVisible(await selectUserBindings(userId));
75355
+ },
75356
+ async findInstallationForUser(installationId, userId) {
75357
+ return toVisible(await selectUserBindings(userId, installationId))[0] ?? null;
75358
+ },
75359
+ async listSyncingInstallationsForUser(userId) {
75360
+ return (await db.select({
75361
+ siteId: settings.siteId,
75362
+ installationId: settings.value
75363
+ }).from(settings).innerJoin(siteMembers, eq(siteMembers.siteId, settings.siteId)).where(and(eq(siteMembers.userId, userId), eq(settings.key, "GITHUB_SYNC_APP_INSTALLATION_ID")))).filter((row) => row.installationId.trim().length > 0);
75364
+ },
74891
75365
  async listSitesForInstallation(installationId) {
74892
75366
  return (await db.select({ siteId: githubAppInstallation.siteId }).from(githubAppInstallation).where(eq(githubAppInstallation.installationId, installationId))).map((row) => row.siteId);
74893
75367
  },
@@ -74912,6 +75386,10 @@ function createUploadSessionService(db, siteId, media, databaseSchema = sqliteSc
74912
75386
  async removeInstallation(installationId, siteId) {
74913
75387
  await db.delete(githubAppInstallation).where(and(eq(githubAppInstallation.installationId, installationId), eq(githubAppInstallation.siteId, siteId)));
74914
75388
  },
75389
+ async removeInstallationForUser(installationId, userId) {
75390
+ const memberSites = db.select({ siteId: siteMembers.siteId }).from(siteMembers).where(eq(siteMembers.userId, userId));
75391
+ await db.delete(githubAppInstallation).where(and(eq(githubAppInstallation.installationId, installationId), inArray(githubAppInstallation.siteId, memberSites)));
75392
+ },
74915
75393
  async removeInstallationEverywhere(installationId) {
74916
75394
  const siteIds = (await db.select({ siteId: githubAppInstallation.siteId }).from(githubAppInstallation).where(eq(githubAppInstallation.installationId, installationId))).map((row) => row.siteId);
74917
75395
  if (siteIds.length === 0) return [];
@@ -74954,14 +75432,16 @@ function createUploadSessionService(db, siteId, media, databaseSchema = sqliteSc
74954
75432
  }
74955
75433
  };
74956
75434
  }
75435
+ /** The column is a checked enum; narrow it without trusting the read. */ function toAccountType(value) {
75436
+ return value === "Organization" ? "Organization" : "User";
75437
+ }
74957
75438
  function toStored(row) {
74958
- const type = row.accountType === "Organization" ? "Organization" : "User";
74959
75439
  return {
74960
75440
  installationId: row.installationId,
74961
75441
  siteId: row.siteId,
74962
75442
  account: {
74963
75443
  login: row.accountLogin,
74964
- type,
75444
+ type: toAccountType(row.accountType),
74965
75445
  avatarUrl: row.accountAvatarUrl
74966
75446
  },
74967
75447
  addedAt: row.addedAt
@@ -75618,6 +76098,34 @@ function createBetterSqliteRawQuery(sqlite) {
75618
76098
  storage: createStorageDriver(env)
75619
76099
  };
75620
76100
  }
76101
+ /**
76102
+ * Set up a self-hosted Node install without a browser, the way the two setup
76103
+ * screens would. Backs `jant setup`.
76104
+ *
76105
+ * @param env - Bindings with a resolved `NODE_DATABASE`
76106
+ * @param input - The owner's credentials and the site's answers, unvalidated
76107
+ * @returns What setup found, and the site
76108
+ * @throws {ValidationError} When the input fails `InstanceSetupSchema`
76109
+ * @throws {Error} Whenever `BootstrapService.setUpInstance` refuses
76110
+ * @example
76111
+ * ```ts
76112
+ * await setUpNodeInstance(bindings, {
76113
+ * email: "owner@example.com",
76114
+ * password: "correct horse battery",
76115
+ * });
76116
+ * ```
76117
+ */ async function setUpNodeInstance(env, input) {
76118
+ const nodeDatabase = env.NODE_DATABASE;
76119
+ if (!nodeDatabase) throw new Error("Node setup requires a resolved database binding.");
76120
+ const parsed = InstanceSetupSchema.safeParse(input);
76121
+ if (!parsed.success) throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid setup input.");
76122
+ return createBootstrapService(nodeDatabase.db, TRANSIENT_SINGLE_SITE_ID, {
76123
+ schema: nodeDatabase.schema,
76124
+ databaseDialect: nodeDatabase.dialect,
76125
+ bootstrapSite: getSingleSiteBootstrapOptions(env),
76126
+ siteResolutionMode: getSiteResolutionMode(env)
76127
+ }).setUpInstance(parsed.data);
76128
+ }
75621
76129
  //#endregion
75622
76130
  //#region src/runtime/index.ts
75623
76131
  /**
@@ -75691,6 +76199,8 @@ async function getDatabaseReadiness(env) {
75691
76199
  const database = await getDatabaseReadiness(env);
75692
76200
  return {
75693
76201
  status: startupConfig.ok && database.ok ? "ok" : "error",
76202
+ version: CORE_VERSION,
76203
+ ...env.NODE_STARTED_AT === void 0 ? {} : { startedAt: env.NODE_STARTED_AT },
75694
76204
  checks: {
75695
76205
  startupConfig,
75696
76206
  database
@@ -75986,11 +76496,16 @@ async function servePublicStorage(c) {
75986
76496
  app.use("*", async (c, next) => {
75987
76497
  const path = new URL(c.req.url).pathname;
75988
76498
  if (path.startsWith("/api/") || path === "/skill.md" || isAssetPath(path)) return next();
75989
- const customUrl = await c.var.services.customUrls.getByPath(path.slice(1));
75990
- if (customUrl?.targetType === "redirect" && customUrl.toPath) return c.redirect(toPublicHref(customUrl.toPath, getRuntimeSitePathPrefix({
76499
+ const storedPath = normalizePath(path);
76500
+ const record = await c.var.services.paths.resolve(storedPath);
76501
+ c.set("pathLookup", {
76502
+ path: storedPath,
76503
+ record
76504
+ });
76505
+ if (record?.kind === "redirect" && record.redirectToPath) return c.redirect(toPublicHref(`/${record.redirectToPath}`, getRuntimeSitePathPrefix({
75991
76506
  env: c.env,
75992
76507
  currentSiteDomain: c.var.currentSiteDomain
75993
- })), customUrl.redirectType ?? 301);
76508
+ })), record.redirectType ?? 301);
75994
76509
  await next();
75995
76510
  });
75996
76511
  app.use("*", withConfig());
@@ -76044,4 +76559,4 @@ async function servePublicStorage(c) {
76044
76559
  return app;
76045
76560
  }
76046
76561
  //#endregion
76047
- export { resolveCjkFontProfile as A, toNavItemViews as C, BUILTIN_FONT_THEMES as D, toSearchResultView as E, BUILTIN_COLOR_THEMES as M, getPublicAssetBasePath as N, getCjkFontCssVariables as O, isAssetPath as P, toNavItemView as S, toPostViews as T, defaultFeedRenderer as _, createSiteService as a, toArchiveGroupsWithMedia as b, resolveConfig as c, setWebhook as d, createStorageDriver as f, schema_exports$1 as g, createNodeDatabase as h, createNodeRequestRuntime as i, buildThemeStyle as j, getFontThemeCssVariables as k, getWebhookUrl as l, sqliteSchemaBundle as m, createApp as n, getHostBasedStartupConfigurationIssues as o, pgSchemaBundle as p, createNodeCliRuntime as r, resolveDatabaseDialect as s, app_exports as t, setMyCommands as u, createMediaContext as v, toPostView as w, toMediaView as x, toArchiveGroups as y };
76562
+ export { getFontThemeCssVariables as A, toNavItemView as C, toSearchResultView as D, toPostViews as E, isAssetPath as F, buildThemeStyle as M, BUILTIN_COLOR_THEMES as N, BUILTIN_FONT_THEMES as O, getPublicAssetBasePath as P, toMediaView as S, toPostView as T, schema_exports$1 as _, setUpNodeInstance as a, toArchiveGroups as b, resolveDatabaseDialect as c, setMyCommands as d, setWebhook as f, createNodeDatabase as g, sqliteSchemaBundle as h, createNodeRequestRuntime as i, resolveCjkFontProfile as j, getCjkFontCssVariables as k, resolveConfig as l, pgSchemaBundle as m, createApp as n, createSiteService as o, createStorageDriver as p, createNodeCliRuntime as r, getHostBasedStartupConfigurationIssues as s, app_exports as t, getWebhookUrl as u, defaultFeedRenderer as v, toNavItemViews as w, toArchiveGroupsWithMedia as x, createMediaContext as y };