@jant/core 0.3.32 → 0.3.34

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 (97) hide show
  1. package/dist/client/client.css +1 -1
  2. package/dist/client/client.js +1442 -989
  3. package/dist/index.js +1431 -1057
  4. package/package.json +1 -1
  5. package/src/__tests__/helpers/app.ts +6 -3
  6. package/src/__tests__/helpers/db.ts +3 -0
  7. package/src/app.tsx +1 -1
  8. package/src/client.ts +2 -1
  9. package/src/db/migrations/0011_add_path_registry.sql +23 -0
  10. package/src/db/schema.ts +12 -1
  11. package/src/i18n/locales/en.po +225 -91
  12. package/src/i18n/locales/en.ts +1 -1
  13. package/src/i18n/locales/zh-Hans.po +201 -152
  14. package/src/i18n/locales/zh-Hans.ts +1 -1
  15. package/src/i18n/locales/zh-Hant.po +201 -152
  16. package/src/i18n/locales/zh-Hant.ts +1 -1
  17. package/src/lib/__tests__/excerpt.test.ts +25 -0
  18. package/src/lib/__tests__/resolve-config.test.ts +26 -2
  19. package/src/lib/__tests__/timeline.test.ts +2 -1
  20. package/src/lib/compose-bridge.ts +30 -1
  21. package/src/lib/excerpt.ts +16 -7
  22. package/src/lib/nav-manager-bridge.ts +54 -0
  23. package/src/lib/navigation.ts +7 -4
  24. package/src/lib/render.tsx +5 -2
  25. package/src/lib/resolve-config.ts +7 -0
  26. package/src/lib/view.ts +42 -10
  27. package/src/middleware/error-handler.ts +16 -0
  28. package/src/routes/api/__tests__/posts.test.ts +80 -0
  29. package/src/routes/api/__tests__/settings.test.ts +1 -1
  30. package/src/routes/api/posts.ts +6 -29
  31. package/src/routes/api/upload.ts +2 -14
  32. package/src/routes/auth/__tests__/setup.test.ts +3 -2
  33. package/src/routes/auth/setup.tsx +1 -1
  34. package/src/routes/compose.tsx +13 -5
  35. package/src/routes/dash/__tests__/pages.test.ts +2 -1
  36. package/src/routes/dash/__tests__/settings-avatar.test.ts +151 -33
  37. package/src/routes/dash/appearance.tsx +71 -4
  38. package/src/routes/dash/collections.tsx +15 -21
  39. package/src/routes/dash/media.tsx +1 -13
  40. package/src/routes/dash/pages.tsx +5 -150
  41. package/src/routes/dash/posts.tsx +25 -32
  42. package/src/routes/dash/redirects.tsx +9 -11
  43. package/src/routes/dash/settings.tsx +29 -111
  44. package/src/routes/feed/__tests__/rss.test.ts +5 -1
  45. package/src/routes/pages/__tests__/collections.test.ts +2 -1
  46. package/src/routes/pages/__tests__/featured.test.ts +2 -1
  47. package/src/routes/pages/page.tsx +20 -25
  48. package/src/services/__tests__/collection.test.ts +2 -1
  49. package/src/services/__tests__/media.test.ts +78 -1
  50. package/src/services/__tests__/navigation.test.ts +2 -1
  51. package/src/services/__tests__/page.test.ts +78 -1
  52. package/src/services/__tests__/path-registry.test.ts +165 -0
  53. package/src/services/__tests__/post-timeline.test.ts +2 -1
  54. package/src/services/__tests__/post.test.ts +103 -1
  55. package/src/services/__tests__/redirect.test.ts +53 -4
  56. package/src/services/__tests__/search.test.ts +2 -1
  57. package/src/services/__tests__/settings.test.ts +153 -0
  58. package/src/services/index.ts +12 -4
  59. package/src/services/media.ts +72 -4
  60. package/src/services/page.ts +64 -17
  61. package/src/services/path-registry.ts +160 -0
  62. package/src/services/post.ts +119 -24
  63. package/src/services/redirect.ts +23 -3
  64. package/src/services/settings.ts +181 -0
  65. package/src/styles/components.css +135 -0
  66. package/src/styles/tokens.css +6 -1
  67. package/src/styles/ui.css +70 -26
  68. package/src/types/bindings.ts +1 -0
  69. package/src/types/config.ts +7 -2
  70. package/src/types/constants.ts +9 -1
  71. package/src/types/sortablejs.d.ts +8 -2
  72. package/src/types/views.ts +1 -1
  73. package/src/ui/color-themes.ts +31 -31
  74. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +0 -3
  75. package/src/ui/components/__tests__/jant-settings-general.test.ts +2 -6
  76. package/src/ui/components/jant-compose-dialog.ts +3 -2
  77. package/src/ui/components/jant-compose-editor.ts +17 -2
  78. package/src/ui/components/jant-nav-manager.ts +1067 -0
  79. package/src/ui/components/jant-settings-general.ts +2 -35
  80. package/src/ui/components/nav-manager-types.ts +72 -0
  81. package/src/ui/components/settings-types.ts +0 -3
  82. package/src/ui/compose/ComposePrompt.tsx +3 -11
  83. package/src/ui/dash/appearance/AdvancedContent.tsx +0 -3
  84. package/src/ui/dash/appearance/AppearanceNav.tsx +12 -8
  85. package/src/ui/dash/appearance/ColorThemeContent.tsx +1 -4
  86. package/src/ui/dash/appearance/FontThemeContent.tsx +0 -3
  87. package/src/ui/dash/appearance/NavigationContent.tsx +302 -0
  88. package/src/ui/dash/pages/PagesContent.tsx +74 -0
  89. package/src/ui/dash/settings/AccountContent.tsx +0 -3
  90. package/src/ui/dash/settings/GeneralContent.tsx +1 -19
  91. package/src/ui/dash/settings/SettingsNav.tsx +2 -6
  92. package/src/ui/feed/NoteCard.tsx +2 -2
  93. package/src/ui/layouts/DashLayout.tsx +83 -86
  94. package/src/ui/layouts/SiteLayout.tsx +82 -21
  95. package/src/lib/nav-reorder.ts +0 -26
  96. package/src/ui/dash/pages/LinkFormContent.tsx +0 -119
  97. package/src/ui/dash/pages/UnifiedPagesContent.tsx +0 -203
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { drizzle } from 'drizzle-orm/d1';
2
2
  import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
3
3
  import { eq, and, inArray, isNull, sql, or, desc, asc } from 'drizzle-orm';
4
+ import { uuidv7 } from 'uuidv7';
4
5
  import { marked } from 'marked';
5
6
  import { pinyin } from 'pinyin-pro';
6
- import { uuidv7 } from 'uuidv7';
7
7
  import { hashPassword } from 'better-auth/crypto';
8
8
  import { betterAuth } from 'better-auth';
9
9
  import { drizzleAdapter } from 'better-auth/adapters/drizzle';
@@ -2240,7 +2240,8 @@ const navItems = sqliteTable("nav_items", {
2240
2240
  type: text("type", {
2241
2241
  enum: [
2242
2242
  "page",
2243
- "link"
2243
+ "link",
2244
+ "system"
2244
2245
  ]
2245
2246
  }).notNull().default("link"),
2246
2247
  label: text("label").notNull(),
@@ -2267,6 +2268,15 @@ const redirects = sqliteTable("redirects", {
2267
2268
  createdAt: integer("created_at").notNull()
2268
2269
  });
2269
2270
  // =============================================================================
2271
+ // Path Registry (URL path ownership)
2272
+ // =============================================================================
2273
+ const pathRegistry = sqliteTable("path_registry", {
2274
+ path: text("path").primaryKey(),
2275
+ ownerType: text("owner_type").notNull(),
2276
+ ownerId: integer("owner_id").notNull(),
2277
+ createdAt: integer("created_at").notNull()
2278
+ });
2279
+ // =============================================================================
2270
2280
  // Settings (Key-Value)
2271
2281
  // =============================================================================
2272
2282
  const settings = sqliteTable("settings", {
@@ -2356,6 +2366,7 @@ const schema = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
2356
2366
  media,
2357
2367
  navItems,
2358
2368
  pages,
2369
+ pathRegistry,
2359
2370
  postCollections,
2360
2371
  posts,
2361
2372
  redirects,
@@ -2561,8 +2572,27 @@ const SORT_ORDERS = [
2561
2572
  ];
2562
2573
  const NAV_ITEM_TYPES = [
2563
2574
  "page",
2564
- "link"
2575
+ "link",
2576
+ "system"
2565
2577
  ];
2578
+ const SYSTEM_NAV_KEYS = {
2579
+ rss: {
2580
+ defaultLabel: "RSS",
2581
+ url: "/feed"
2582
+ },
2583
+ dashboard: {
2584
+ defaultLabel: "Dashboard",
2585
+ url: "/dash"
2586
+ },
2587
+ collections: {
2588
+ defaultLabel: "Collections",
2589
+ url: "/c"
2590
+ },
2591
+ archive: {
2592
+ defaultLabel: "Archive",
2593
+ url: "/archive"
2594
+ }
2595
+ };
2566
2596
  const MAX_MEDIA_ATTACHMENTS = 20;
2567
2597
  const MAX_PINNED_POSTS = 3;
2568
2598
 
@@ -2586,7 +2616,7 @@ const MAX_PINNED_POSTS = 3;
2586
2616
  envOnly: false
2587
2617
  },
2588
2618
  SITE_DESCRIPTION: {
2589
- defaultValue: "A microblog powered by Jant",
2619
+ defaultValue: "Thoughts, links, and quotes — one post at a time",
2590
2620
  envOnly: false
2591
2621
  },
2592
2622
  SITE_LANGUAGE: {
@@ -2597,9 +2627,13 @@ const MAX_PINNED_POSTS = 3;
2597
2627
  defaultValue: "latest",
2598
2628
  envOnly: false
2599
2629
  },
2630
+ HEADER_NAV_MAX_VISIBLE: {
2631
+ defaultValue: "3",
2632
+ envOnly: false
2633
+ },
2600
2634
  // Environment-only (deployment/infrastructure config)
2601
2635
  DEFAULT_THEME: {
2602
- defaultValue: "halloween",
2636
+ defaultValue: "notepad",
2603
2637
  envOnly: true
2604
2638
  },
2605
2639
  SITE_URL: {
@@ -2723,6 +2757,37 @@ const MAX_PINNED_POSTS = 3;
2723
2757
  }
2724
2758
  };
2725
2759
 
2760
+ /**
2761
+ * Application Constants
2762
+ */ /**
2763
+ * Reserved URL paths that cannot be used for pages
2764
+ */ const RESERVED_PATHS = [
2765
+ "featured",
2766
+ "latest",
2767
+ "collections",
2768
+ "signin",
2769
+ "signout",
2770
+ "setup",
2771
+ "dash",
2772
+ "api",
2773
+ "feed",
2774
+ "search",
2775
+ "archive",
2776
+ "media",
2777
+ "pages",
2778
+ "reset",
2779
+ "p",
2780
+ "c",
2781
+ "static",
2782
+ "assets",
2783
+ "health"
2784
+ ];
2785
+ /**
2786
+ * Check if a path is reserved
2787
+ */ function isReservedPath(path) {
2788
+ const firstSegment = path.split("/")[0]?.toLowerCase();
2789
+ return RESERVED_PATHS.includes(firstSegment);
2790
+ }
2726
2791
  const SETTINGS_KEYS = Object.fromEntries(Object.entries(CONFIG_FIELDS).filter(([, field])=>!field.envOnly || "internal" in field).map(([key])=>[
2727
2792
  key,
2728
2793
  key
@@ -2733,6 +2798,174 @@ const SETTINGS_KEYS = Object.fromEntries(Object.entries(CONFIG_FIELDS).filter(([
2733
2798
  COMPLETED: "completed"
2734
2799
  };
2735
2800
 
2801
+ /** MIME types allowed for upload */ const ALLOWED_UPLOAD_TYPES = [
2802
+ "image/jpeg",
2803
+ "image/png",
2804
+ "image/gif",
2805
+ "image/webp",
2806
+ "image/svg+xml"
2807
+ ];
2808
+ /** Maximum file size in bytes (10MB) */ const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
2809
+ /**
2810
+ * Validates an uploaded file's type and size.
2811
+ *
2812
+ * @param file - The uploaded File object
2813
+ * @returns null if valid, error message string if invalid
2814
+ * @example
2815
+ * ```ts
2816
+ * const error = validateUploadFile(file);
2817
+ * if (error) return dsToast(error, "error");
2818
+ * ```
2819
+ */ function validateUploadFile(file) {
2820
+ if (!ALLOWED_UPLOAD_TYPES.includes(file.type)) {
2821
+ return "File type not allowed.";
2822
+ }
2823
+ if (file.size > MAX_UPLOAD_SIZE) {
2824
+ return "File too large (max 10MB).";
2825
+ }
2826
+ return null;
2827
+ }
2828
+ /**
2829
+ * Generates a unique storage key for an uploaded file.
2830
+ * Format: `media/YYYY/MM/uuid.ext`
2831
+ *
2832
+ * @param originalFilename - Original filename to extract extension from
2833
+ * @returns Object with generated id, filename, and storageKey
2834
+ * @example
2835
+ * ```ts
2836
+ * const { id, filename, storageKey } = generateStorageKey("photo.jpg");
2837
+ * // { id: "0192...", filename: "0192....jpg", storageKey: "media/2025/01/0192....jpg" }
2838
+ * ```
2839
+ */ function generateStorageKey(originalFilename) {
2840
+ const ext = originalFilename.split(".").pop() || "bin";
2841
+ const id = uuidv7();
2842
+ const date = new Date();
2843
+ const year = date.getUTCFullYear();
2844
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
2845
+ const filename = `${id}.${ext}`;
2846
+ const storageKey = `media/${year}/${month}/${filename}`;
2847
+ return {
2848
+ id,
2849
+ filename,
2850
+ storageKey
2851
+ };
2852
+ }
2853
+
2854
+ /**
2855
+ * Convert an ArrayBuffer to a base64 string.
2856
+ *
2857
+ * @param buffer - The ArrayBuffer to encode
2858
+ * @returns base64-encoded string
2859
+ *
2860
+ * @example
2861
+ * ```ts
2862
+ * const b64 = arrayBufferToBase64(await blob.arrayBuffer());
2863
+ * ```
2864
+ */ function arrayBufferToBase64(buffer) {
2865
+ const bytes = new Uint8Array(buffer);
2866
+ let binary = "";
2867
+ for(let i = 0; i < bytes.byteLength; i++){
2868
+ binary += String.fromCharCode(bytes[i]);
2869
+ }
2870
+ return btoa(binary);
2871
+ }
2872
+ /**
2873
+ * Convert a base64 string to a Uint8Array.
2874
+ *
2875
+ * @param base64 - The base64 string to decode
2876
+ * @returns decoded Uint8Array
2877
+ *
2878
+ * @example
2879
+ * ```ts
2880
+ * const bytes = base64ToUint8Array(storedBase64);
2881
+ * ```
2882
+ */ function base64ToUint8Array(base64) {
2883
+ const binary = atob(base64);
2884
+ const bytes = new Uint8Array(binary.length);
2885
+ for(let i = 0; i < binary.length; i++){
2886
+ bytes[i] = binary.charCodeAt(i);
2887
+ }
2888
+ return bytes;
2889
+ }
2890
+
2891
+ /**
2892
+ * Domain Error Classes
2893
+ *
2894
+ * Typed errors per coding-standards.md error taxonomy.
2895
+ * Services throw these; the error handler middleware maps them to HTTP responses.
2896
+ */ /**
2897
+ * Base class for all domain errors.
2898
+ * Each subclass maps to a specific HTTP status code.
2899
+ */ class DomainError extends Error {
2900
+ statusCode;
2901
+ code;
2902
+ constructor(message, statusCode, code){
2903
+ super(message), this.statusCode = statusCode, this.code = code;
2904
+ this.name = this.constructor.name;
2905
+ }
2906
+ }
2907
+ /** Invalid input — 400 */ class ValidationError extends DomainError {
2908
+ details;
2909
+ constructor(message, details){
2910
+ super(message, 400, "VALIDATION_ERROR"), this.details = details;
2911
+ }
2912
+ }
2913
+ /** Not authenticated — 401 */ class UnauthorizedError extends DomainError {
2914
+ constructor(message = "Unauthorized"){
2915
+ super(message, 401, "UNAUTHORIZED");
2916
+ }
2917
+ }
2918
+ /** Resource doesn't exist — 404 */ class NotFoundError extends DomainError {
2919
+ constructor(resource = "Resource"){
2920
+ super(`${resource} not found`, 404, "NOT_FOUND");
2921
+ }
2922
+ }
2923
+ /** State conflict (e.g. duplicate) — 409 */ class ConflictError extends DomainError {
2924
+ constructor(message){
2925
+ super(message, 409, "CONFLICT");
2926
+ }
2927
+ }
2928
+ /** Third-party failure — 500 */ class ExternalServiceError extends DomainError {
2929
+ constructor(message){
2930
+ super(message, 500, "EXTERNAL_SERVICE_ERROR");
2931
+ }
2932
+ }
2933
+ // =============================================================================
2934
+ // Route Helpers
2935
+ // =============================================================================
2936
+ /**
2937
+ * Asserts a value is not null/undefined, throwing NotFoundError if it is.
2938
+ *
2939
+ * @param value - The value to check
2940
+ * @param resource - Resource name for the error message
2941
+ * @returns The non-null value
2942
+ * @example
2943
+ * ```ts
2944
+ * const post = assertFound(await services.posts.getById(id), "Post");
2945
+ * ```
2946
+ */ function assertFound(value, resource) {
2947
+ if (value == null) {
2948
+ throw new NotFoundError(resource);
2949
+ }
2950
+ return value;
2951
+ }
2952
+ /**
2953
+ * Parse a route parameter as a positive integer, throwing ValidationError if invalid.
2954
+ *
2955
+ * @param value - Raw string parameter from the route
2956
+ * @returns Parsed integer
2957
+ * @example
2958
+ * ```ts
2959
+ * const id = parseIntParam(c.req.param("id"));
2960
+ * ```
2961
+ */ function parseIntParam(value) {
2962
+ const id = parseInt(value, 10);
2963
+ if (isNaN(id) || id < 1) {
2964
+ throw new ValidationError("Invalid ID");
2965
+ }
2966
+ return id;
2967
+ }
2968
+
2736
2969
  function createSettingsService(db) {
2737
2970
  return {
2738
2971
  async get (key) {
@@ -2790,6 +3023,102 @@ function createSettingsService(db) {
2790
3023
  },
2791
3024
  async completeOnboarding () {
2792
3025
  await this.set(SETTINGS_KEYS.ONBOARDING_STATUS, ONBOARDING_STATUS.COMPLETED);
3026
+ },
3027
+ async updateGeneral (data, opts) {
3028
+ // Site name: set if non-empty, remove otherwise
3029
+ if (data.siteName.trim()) {
3030
+ await this.set("SITE_NAME", data.siteName.trim());
3031
+ } else {
3032
+ await this.remove("SITE_NAME");
3033
+ }
3034
+ // Site description: set if non-empty, remove otherwise
3035
+ if (data.siteDescription.trim()) {
3036
+ await this.set("SITE_DESCRIPTION", data.siteDescription.trim());
3037
+ } else {
3038
+ await this.remove("SITE_DESCRIPTION");
3039
+ }
3040
+ // Footer: set if non-empty, remove otherwise
3041
+ if (data.siteFooter?.trim()) {
3042
+ await this.set("SITE_FOOTER", data.siteFooter.trim());
3043
+ } else {
3044
+ await this.remove("SITE_FOOTER");
3045
+ }
3046
+ // Language is always stored
3047
+ await this.set("SITE_LANGUAGE", data.siteLanguage);
3048
+ // Homepage default view: only update if provided (may be managed separately)
3049
+ if (data.homeDefaultView !== undefined) {
3050
+ if (data.homeDefaultView === "featured") {
3051
+ await this.set("HOME_DEFAULT_VIEW", data.homeDefaultView);
3052
+ } else {
3053
+ await this.remove("HOME_DEFAULT_VIEW");
3054
+ }
3055
+ }
3056
+ // Header nav max visible: only update if provided (may be managed separately)
3057
+ if (data.headerNavMaxVisible !== undefined) {
3058
+ const navMax = parseInt(String(data.headerNavMaxVisible), 10);
3059
+ if (!isNaN(navMax) && navMax !== 3) {
3060
+ await this.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
3061
+ } else {
3062
+ await this.remove("HEADER_NAV_MAX_VISIBLE");
3063
+ }
3064
+ }
3065
+ // Timezone: only store non-default (default is UTC)
3066
+ if (data.timeZone && data.timeZone !== "UTC") {
3067
+ await this.set("TIME_ZONE", data.timeZone);
3068
+ } else {
3069
+ await this.remove("TIME_ZONE");
3070
+ }
3071
+ return {
3072
+ languageChanged: opts.oldLanguage !== data.siteLanguage,
3073
+ displayName: data.siteName.trim() || opts.fallbackSiteName
3074
+ };
3075
+ },
3076
+ async uploadAvatar (data, deps) {
3077
+ const uploadError = validateUploadFile(data.file);
3078
+ if (uploadError) {
3079
+ throw new ValidationError(uploadError);
3080
+ }
3081
+ const { id, filename, storageKey } = generateStorageKey(data.file.name);
3082
+ await deps.storage.put(storageKey, data.file.stream(), {
3083
+ contentType: data.file.type
3084
+ });
3085
+ await deps.media.create({
3086
+ id,
3087
+ filename,
3088
+ originalName: data.file.name,
3089
+ mimeType: data.file.type,
3090
+ size: data.file.size,
3091
+ storageKey,
3092
+ provider: deps.storageProvider
3093
+ });
3094
+ await this.set("SITE_AVATAR", storageKey);
3095
+ // Store favicon ICO as base64 in settings (tiny file, accessed every page load)
3096
+ if (data.faviconIco) {
3097
+ const b64 = arrayBufferToBase64(data.faviconIco);
3098
+ await this.set("SITE_FAVICON_ICO", b64);
3099
+ }
3100
+ // Store apple-touch-icon in storage (180x180 PNG, not tiny enough for base64)
3101
+ if (data.appleTouchIcon) {
3102
+ const appleTouchKey = "favicon/apple-touch-icon.png";
3103
+ await deps.storage.put(appleTouchKey, new Uint8Array(data.appleTouchIcon), {
3104
+ contentType: "image/png"
3105
+ });
3106
+ await this.set("SITE_FAVICON_APPLE_TOUCH", appleTouchKey);
3107
+ }
3108
+ // Set favicon version for cache-busting
3109
+ const ts = new Date();
3110
+ const version = String(ts.getUTCFullYear()) + String(ts.getUTCMonth() + 1).padStart(2, "0") + String(ts.getUTCDate()).padStart(2, "0") + String(ts.getUTCHours()).padStart(2, "0") + String(ts.getUTCMinutes()).padStart(2, "0");
3111
+ await this.set("SITE_FAVICON_VERSION", version);
3112
+ },
3113
+ async removeAvatar (storage) {
3114
+ const appleTouchKey = await this.get("SITE_FAVICON_APPLE_TOUCH");
3115
+ if (storage && appleTouchKey) {
3116
+ await storage.delete(appleTouchKey);
3117
+ }
3118
+ await this.remove("SITE_AVATAR");
3119
+ await this.remove("SITE_FAVICON_ICO");
3120
+ await this.remove("SITE_FAVICON_APPLE_TOUCH");
3121
+ await this.remove("SITE_FAVICON_VERSION");
2793
3122
  }
2794
3123
  };
2795
3124
  }
@@ -2880,7 +3209,11 @@ const markdown = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
2880
3209
  toPlainText
2881
3210
  }, Symbol.toStringTag, { value: 'Module' }));
2882
3211
 
2883
- function createPostService(db) {
3212
+ /** Check if an error is a SQLite UNIQUE constraint violation (D1 or better-sqlite3) */ function isUniqueConstraintError$1(err) {
3213
+ const msg = String(err);
3214
+ return msg.includes("UNIQUE constraint") || msg.includes("SQLITE_CONSTRAINT");
3215
+ }
3216
+ function createPostService(db, pathRegistry) {
2884
3217
  /** Build WHERE conditions from filters (shared by list and count) */ function buildFilterConditions(filters) {
2885
3218
  const conditions = [];
2886
3219
  if (filters.status) {
@@ -2979,26 +3312,46 @@ function createPostService(db) {
2979
3312
  }
2980
3313
  }
2981
3314
  }
2982
- const result = await db.insert(posts).values({
2983
- format: data.format,
2984
- status,
2985
- featured: featured ? 1 : 0,
2986
- pinned: data.pinned ? 1 : 0,
2987
- path: data.path ?? null,
2988
- title: data.title ?? null,
2989
- url: data.url ?? null,
2990
- body: data.body ?? null,
2991
- bodyHtml,
2992
- quoteText: data.quoteText ?? null,
2993
- rating: data.rating ?? null,
2994
- replyToId: data.replyToId ?? null,
2995
- threadId,
2996
- publishedAt: data.publishedAt ?? timestamp,
2997
- createdAt: timestamp,
2998
- updatedAt: timestamp
2999
- }).returning();
3315
+ // Validate path availability before DB insert — throws friendly
3316
+ // ConflictError/ValidationError instead of a raw UNIQUE constraint error.
3317
+ // Uses placeholder owner ID; corrected to real ID after insert.
3318
+ if (data.path) {
3319
+ await pathRegistry.claim(data.path, "post", 0);
3320
+ }
3321
+ let result;
3322
+ try {
3323
+ result = await db.insert(posts).values({
3324
+ format: data.format,
3325
+ status,
3326
+ featured: featured ? 1 : 0,
3327
+ pinned: data.pinned ? 1 : 0,
3328
+ path: data.path ?? null,
3329
+ title: data.title ?? null,
3330
+ url: data.url ?? null,
3331
+ body: data.body ?? null,
3332
+ bodyHtml,
3333
+ quoteText: data.quoteText ?? null,
3334
+ rating: data.rating ?? null,
3335
+ replyToId: data.replyToId ?? null,
3336
+ threadId,
3337
+ publishedAt: data.publishedAt ?? timestamp,
3338
+ createdAt: timestamp,
3339
+ updatedAt: timestamp
3340
+ }).returning();
3341
+ } catch (err) {
3342
+ if (data.path) await pathRegistry.release(data.path);
3343
+ if (isUniqueConstraintError$1(err)) {
3344
+ throw new ConflictError(`Path "${data.path}" is already in use`);
3345
+ }
3346
+ throw err;
3347
+ }
3000
3348
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
3001
3349
  const post = toPost(result[0]);
3350
+ // Update registry with actual post ID
3351
+ if (post.path) {
3352
+ await pathRegistry.release(post.path);
3353
+ await pathRegistry.claim(post.path, "post", post.id);
3354
+ }
3002
3355
  // Sync collection memberships if provided
3003
3356
  if (data.collectionIds && data.collectionIds.length > 0) {
3004
3357
  await db.insert(postCollections).values(data.collectionIds.map((collectionId)=>({
@@ -3011,6 +3364,18 @@ function createPostService(db) {
3011
3364
  async update (id, data) {
3012
3365
  const existing = await this.getById(id);
3013
3366
  if (!existing) return null;
3367
+ // Handle path changes in the registry before modifying the post
3368
+ const pathChanging = data.path !== undefined && data.path !== existing.path;
3369
+ if (pathChanging) {
3370
+ // Claim new path (if non-null) before releasing old
3371
+ if (data.path) {
3372
+ await pathRegistry.claim(data.path, "post", id);
3373
+ }
3374
+ // Release old path (if it existed)
3375
+ if (existing.path) {
3376
+ await pathRegistry.release(existing.path);
3377
+ }
3378
+ }
3014
3379
  const timestamp = now();
3015
3380
  const updates = {
3016
3381
  updatedAt: timestamp
@@ -3068,9 +3433,40 @@ function createPostService(db) {
3068
3433
  const updateResult = results[updateIdx];
3069
3434
  return updateResult?.[0] ? toPost(updateResult[0]) : null;
3070
3435
  },
3071
- async delete (id) {
3436
+ async delete (id, deps) {
3072
3437
  const existing = await this.getById(id);
3073
3438
  if (!existing) return false;
3439
+ // Clean up media for all affected posts
3440
+ if (deps?.media) {
3441
+ let postIds;
3442
+ if (!existing.threadId) {
3443
+ const thread = await this.getThread(id);
3444
+ postIds = thread.map((p)=>p.id);
3445
+ } else {
3446
+ postIds = [
3447
+ id
3448
+ ];
3449
+ }
3450
+ const mediaMap = await deps.media.getByPostIds(postIds);
3451
+ const allMedia = [
3452
+ ...mediaMap.values()
3453
+ ].flat();
3454
+ if (allMedia.length > 0) {
3455
+ await deps.media.deleteByIds(allMedia.map((m)=>m.id), deps.storage);
3456
+ }
3457
+ }
3458
+ // Release paths from registry
3459
+ if (!existing.threadId) {
3460
+ // Thread root: release paths for all posts in thread
3461
+ const thread = await this.getThread(id);
3462
+ for (const post of thread){
3463
+ if (post.path) {
3464
+ await pathRegistry.release(post.path);
3465
+ }
3466
+ }
3467
+ } else if (existing.path) {
3468
+ await pathRegistry.release(existing.path);
3469
+ }
3074
3470
  const timestamp = now();
3075
3471
  // If this is a thread root, soft delete all posts in the thread
3076
3472
  if (!existing.threadId) {
@@ -3135,7 +3531,11 @@ function createPostService(db) {
3135
3531
  };
3136
3532
  }
3137
3533
 
3138
- function createPageService(db) {
3534
+ /** Check if an error is a SQLite UNIQUE constraint violation (D1 or better-sqlite3) */ function isUniqueConstraintError(err) {
3535
+ const msg = String(err);
3536
+ return msg.includes("UNIQUE constraint") || msg.includes("SQLITE_CONSTRAINT");
3537
+ }
3538
+ function createPageService(db, pathRegistry) {
3139
3539
  function toPage(row) {
3140
3540
  return {
3141
3541
  id: row.id,
@@ -3170,23 +3570,47 @@ function createPageService(db) {
3170
3570
  return rows.map(toPage);
3171
3571
  },
3172
3572
  async create (data) {
3573
+ // Validate and reserve path before DB insert — throws friendly
3574
+ // ConflictError/ValidationError instead of a raw UNIQUE constraint error.
3575
+ // Uses placeholder owner ID; corrected to real ID after insert.
3576
+ await pathRegistry.claim(data.slug, "page", 0);
3173
3577
  const timestamp = now();
3174
3578
  const bodyHtml = data.body ? render(data.body) : null;
3175
- const result = await db.insert(pages).values({
3176
- slug: data.slug,
3177
- title: data.title ?? null,
3178
- body: data.body ?? null,
3179
- bodyHtml,
3180
- status: data.status ?? "published",
3181
- createdAt: timestamp,
3182
- updatedAt: timestamp
3183
- }).returning();
3184
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
3185
- return toPage(result[0]);
3579
+ let page;
3580
+ try {
3581
+ const result = await db.insert(pages).values({
3582
+ slug: data.slug,
3583
+ title: data.title ?? null,
3584
+ body: data.body ?? null,
3585
+ bodyHtml,
3586
+ status: data.status ?? "published",
3587
+ createdAt: timestamp,
3588
+ updatedAt: timestamp
3589
+ }).returning();
3590
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
3591
+ page = toPage(result[0]);
3592
+ } catch (err) {
3593
+ await pathRegistry.release(data.slug);
3594
+ // Surface DB unique constraint failures as a friendly error
3595
+ if (isUniqueConstraintError(err)) {
3596
+ throw new ConflictError(`Slug "${data.slug}" is already in use`);
3597
+ }
3598
+ throw err;
3599
+ }
3600
+ // Update registry with actual page ID
3601
+ await pathRegistry.release(data.slug);
3602
+ await pathRegistry.claim(data.slug, "page", page.id);
3603
+ return page;
3186
3604
  },
3187
3605
  async update (id, data) {
3188
3606
  const existing = await this.getById(id);
3189
3607
  if (!existing) return null;
3608
+ const slugChanging = data.slug !== undefined && data.slug !== existing.slug;
3609
+ // If slug is changing, claim the new path first (validates before modifying)
3610
+ if (slugChanging) {
3611
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by slugChanging check
3612
+ await pathRegistry.claim(data.slug, "page", id);
3613
+ }
3190
3614
  const timestamp = now();
3191
3615
  const updates = {
3192
3616
  updatedAt: timestamp
@@ -3199,7 +3623,7 @@ function createPageService(db) {
3199
3623
  updates.bodyHtml = data.body ? render(data.body) : null;
3200
3624
  }
3201
3625
  // If slug changed, update related nav_items
3202
- if (data.slug !== undefined && data.slug !== existing.slug) {
3626
+ if (slugChanging) {
3203
3627
  await db.update(navItems).set({
3204
3628
  url: `/${data.slug}`,
3205
3629
  updatedAt: timestamp
@@ -3213,9 +3637,15 @@ function createPageService(db) {
3213
3637
  }).where(eq(navItems.pageId, id));
3214
3638
  }
3215
3639
  const result = await db.update(pages).set(updates).where(eq(pages.id, id)).returning();
3640
+ // Release old slug from registry after successful update
3641
+ if (slugChanging) {
3642
+ await pathRegistry.release(existing.slug);
3643
+ }
3216
3644
  return result[0] ? toPage(result[0]) : null;
3217
3645
  },
3218
3646
  async delete (id) {
3647
+ // Release path registry entries for this page
3648
+ await pathRegistry.releaseByOwner("page", id);
3219
3649
  // nav_items with page_id FK have ON DELETE CASCADE, so they auto-delete
3220
3650
  const result = await db.delete(pages).where(eq(pages.id, id)).returning();
3221
3651
  return result.length > 0;
@@ -3324,7 +3754,7 @@ const url = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
3324
3754
  slugify
3325
3755
  }, Symbol.toStringTag, { value: 'Module' }));
3326
3756
 
3327
- function createRedirectService(db) {
3757
+ function createRedirectService(db, pathRegistry) {
3328
3758
  function toRedirect(row) {
3329
3759
  return {
3330
3760
  id: row.id,
@@ -3343,7 +3773,15 @@ function createRedirectService(db) {
3343
3773
  async create (fromPath, toPath, type = 301) {
3344
3774
  const timestamp = now();
3345
3775
  const normalizedFrom = normalizePath(fromPath);
3346
- // Delete existing redirect from this path if any
3776
+ // Check if path is claimed by a non-redirect entity
3777
+ const existingClaim = await pathRegistry.getByPath(normalizedFrom);
3778
+ if (existingClaim && existingClaim.ownerType !== "redirect") {
3779
+ throw new ConflictError(`Path "${normalizedFrom}" is already in use`);
3780
+ }
3781
+ // Delete existing redirect from this path if any (upsert behavior)
3782
+ if (existingClaim?.ownerType === "redirect") {
3783
+ await pathRegistry.release(normalizedFrom);
3784
+ }
3347
3785
  await db.delete(redirects).where(eq(redirects.fromPath, normalizedFrom));
3348
3786
  const result = await db.insert(redirects).values({
3349
3787
  fromPath: normalizedFrom,
@@ -3352,9 +3790,13 @@ function createRedirectService(db) {
3352
3790
  createdAt: timestamp
3353
3791
  }).returning();
3354
3792
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
3355
- return toRedirect(result[0]);
3793
+ const redirect = toRedirect(result[0]);
3794
+ await pathRegistry.claim(normalizedFrom, "redirect", redirect.id);
3795
+ return redirect;
3356
3796
  },
3357
3797
  async delete (id) {
3798
+ // Release path registry entries for this redirect
3799
+ await pathRegistry.releaseByOwner("redirect", id);
3358
3800
  const result = await db.delete(redirects).where(eq(redirects.id, id)).returning();
3359
3801
  return result.length > 0;
3360
3802
  },
@@ -3429,6 +3871,16 @@ function createMediaService(db) {
3429
3871
  const rows = await db.select().from(media).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(media.createdAt)).limit(limit);
3430
3872
  return rows.map(toMedia);
3431
3873
  },
3874
+ async validateIds (ids) {
3875
+ if (ids.length === 0) return;
3876
+ if (ids.length > MAX_MEDIA_ATTACHMENTS) {
3877
+ throw new ValidationError(`Posts allow at most ${MAX_MEDIA_ATTACHMENTS} media attachments`);
3878
+ }
3879
+ const existing = await this.getByIds(ids);
3880
+ if (existing.length !== ids.length) {
3881
+ throw new ValidationError("One or more media IDs are invalid");
3882
+ }
3883
+ },
3432
3884
  async create (data) {
3433
3885
  const id = data.id ?? uuidv7();
3434
3886
  const timestamp = now();
@@ -3483,9 +3935,28 @@ function createMediaService(db) {
3483
3935
  alt
3484
3936
  }).where(eq(media.id, id));
3485
3937
  },
3486
- async delete (id) {
3487
- const result = await db.delete(media).where(eq(media.id, id)).returning();
3488
- return result.length > 0;
3938
+ async delete (id, storage) {
3939
+ const record = await this.getById(id);
3940
+ if (!record) return false;
3941
+ if (storage) {
3942
+ await storage.delete(record.storageKey).catch((err)=>{
3943
+ // eslint-disable-next-line no-console -- Error logging is intentional
3944
+ console.error("Storage delete error:", err);
3945
+ });
3946
+ }
3947
+ await db.delete(media).where(eq(media.id, id));
3948
+ return true;
3949
+ },
3950
+ async deleteByIds (ids, storage) {
3951
+ if (ids.length === 0) return;
3952
+ if (storage) {
3953
+ const records = await this.getByIds(ids);
3954
+ await Promise.all(records.map((r)=>storage.delete(r.storageKey).catch((err)=>{
3955
+ // eslint-disable-next-line no-console -- Error logging is intentional
3956
+ console.error("Storage delete error:", err);
3957
+ })));
3958
+ }
3959
+ await db.delete(media).where(inArray(media.id, ids));
3489
3960
  }
3490
3961
  };
3491
3962
  }
@@ -3818,79 +4289,6 @@ function createNavItemService(db) {
3818
4289
  };
3819
4290
  }
3820
4291
 
3821
- /**
3822
- * Domain Error Classes
3823
- *
3824
- * Typed errors per coding-standards.md error taxonomy.
3825
- * Services throw these; the error handler middleware maps them to HTTP responses.
3826
- */ /**
3827
- * Base class for all domain errors.
3828
- * Each subclass maps to a specific HTTP status code.
3829
- */ class DomainError extends Error {
3830
- statusCode;
3831
- code;
3832
- constructor(message, statusCode, code){
3833
- super(message), this.statusCode = statusCode, this.code = code;
3834
- this.name = this.constructor.name;
3835
- }
3836
- }
3837
- /** Invalid input — 400 */ class ValidationError extends DomainError {
3838
- details;
3839
- constructor(message, details){
3840
- super(message, 400, "VALIDATION_ERROR"), this.details = details;
3841
- }
3842
- }
3843
- /** Not authenticated — 401 */ class UnauthorizedError extends DomainError {
3844
- constructor(message = "Unauthorized"){
3845
- super(message, 401, "UNAUTHORIZED");
3846
- }
3847
- }
3848
- /** Resource doesn't exist — 404 */ class NotFoundError extends DomainError {
3849
- constructor(resource = "Resource"){
3850
- super(`${resource} not found`, 404, "NOT_FOUND");
3851
- }
3852
- }
3853
- /** Third-party failure — 500 */ class ExternalServiceError extends DomainError {
3854
- constructor(message){
3855
- super(message, 500, "EXTERNAL_SERVICE_ERROR");
3856
- }
3857
- }
3858
- // =============================================================================
3859
- // Route Helpers
3860
- // =============================================================================
3861
- /**
3862
- * Asserts a value is not null/undefined, throwing NotFoundError if it is.
3863
- *
3864
- * @param value - The value to check
3865
- * @param resource - Resource name for the error message
3866
- * @returns The non-null value
3867
- * @example
3868
- * ```ts
3869
- * const post = assertFound(await services.posts.getById(id), "Post");
3870
- * ```
3871
- */ function assertFound(value, resource) {
3872
- if (value == null) {
3873
- throw new NotFoundError(resource);
3874
- }
3875
- return value;
3876
- }
3877
- /**
3878
- * Parse a route parameter as a positive integer, throwing ValidationError if invalid.
3879
- *
3880
- * @param value - Raw string parameter from the route
3881
- * @returns Parsed integer
3882
- * @example
3883
- * ```ts
3884
- * const id = parseIntParam(c.req.param("id"));
3885
- * ```
3886
- */ function parseIntParam(value) {
3887
- const id = parseInt(value, 10);
3888
- if (isNaN(id) || id < 1) {
3889
- throw new ValidationError("Invalid ID");
3890
- }
3891
- return id;
3892
- }
3893
-
3894
4292
  function createAuthService(db, settings) {
3895
4293
  async function validateResetToken(token) {
3896
4294
  const stored = await settings.get(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
@@ -3929,13 +4327,75 @@ function createAuthService(db, settings) {
3929
4327
  };
3930
4328
  }
3931
4329
 
4330
+ function createPathRegistryService(db) {
4331
+ function toEntry(row) {
4332
+ return {
4333
+ path: row.path,
4334
+ ownerType: row.ownerType,
4335
+ ownerId: row.ownerId,
4336
+ createdAt: row.createdAt
4337
+ };
4338
+ }
4339
+ return {
4340
+ async claim (path, ownerType, ownerId) {
4341
+ const normalized = normalizePath(path);
4342
+ if (isReservedPath(normalized)) {
4343
+ throw new ValidationError(`Path "${normalized}" is reserved and cannot be used`);
4344
+ }
4345
+ // Check existing claim
4346
+ const existing = await db.select().from(pathRegistry).where(eq(pathRegistry.path, normalized)).limit(1);
4347
+ if (existing[0]) {
4348
+ const entry = toEntry(existing[0]);
4349
+ // Idempotent: same owner re-claiming is a no-op
4350
+ if (entry.ownerType === ownerType && entry.ownerId === ownerId) {
4351
+ return entry;
4352
+ }
4353
+ throw new ConflictError(`Path "${normalized}" is already in use`);
4354
+ }
4355
+ const timestamp = now();
4356
+ await db.insert(pathRegistry).values({
4357
+ path: normalized,
4358
+ ownerType,
4359
+ ownerId,
4360
+ createdAt: timestamp
4361
+ });
4362
+ return {
4363
+ path: normalized,
4364
+ ownerType,
4365
+ ownerId,
4366
+ createdAt: timestamp
4367
+ };
4368
+ },
4369
+ async release (path) {
4370
+ const normalized = normalizePath(path);
4371
+ await db.delete(pathRegistry).where(eq(pathRegistry.path, normalized));
4372
+ },
4373
+ async releaseByOwner (ownerType, ownerId) {
4374
+ await db.delete(pathRegistry).where(and(eq(pathRegistry.ownerType, ownerType), eq(pathRegistry.ownerId, ownerId)));
4375
+ },
4376
+ async getByPath (path) {
4377
+ const normalized = normalizePath(path);
4378
+ const result = await db.select().from(pathRegistry).where(eq(pathRegistry.path, normalized)).limit(1);
4379
+ return result[0] ? toEntry(result[0]) : null;
4380
+ },
4381
+ async isAvailable (path) {
4382
+ const normalized = normalizePath(path);
4383
+ if (isReservedPath(normalized)) return false;
4384
+ const existing = await db.select().from(pathRegistry).where(eq(pathRegistry.path, normalized)).limit(1);
4385
+ return existing.length === 0;
4386
+ }
4387
+ };
4388
+ }
4389
+
3932
4390
  function createServices(db, d1) {
3933
4391
  const settings = createSettingsService(db);
4392
+ const pathRegistry = createPathRegistryService(db);
3934
4393
  return {
3935
4394
  settings,
3936
- posts: createPostService(db),
3937
- pages: createPageService(db),
3938
- redirects: createRedirectService(db),
4395
+ pathRegistry,
4396
+ posts: createPostService(db, pathRegistry),
4397
+ pages: createPageService(db, pathRegistry),
4398
+ redirects: createRedirectService(db, pathRegistry),
3939
4399
  media: createMediaService(db),
3940
4400
  collections: createCollectionService(db),
3941
4401
  search: createSearchService(d1),
@@ -3992,11 +4452,11 @@ const baseLocale = "en";
3992
4452
  return typeof value === "string" && locales.includes(value);
3993
4453
  }
3994
4454
 
3995
- /*eslint-disable*/ const messages$2 = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+AXdXp\":[\"Label and URL are required\"],\"+MACwa\":[\"No collections yet.\"],\"+MH6k9\":[\"Add to nav\"],\"+bHzpy\":[\"Display text for the link\"],\"+owNNn\":[\"Posts\"],\"+zy2Nq\":[\"Type\"],\"/0D1Xp\":[\"Edit Collection\"],\"/DFKdU\":[\"Type the quote...\"],\"/R/sGB\":[\"Password changed successfully.\"],\"/Rj5P4\":[\"Your Name\"],\"/rkqRV\":[\"All pages are in your navigation.\"],\"07Epll\":[\"This will theme both your site and your dashboard. All color themes support dark mode.\"],\"0JkyS7\":[\"Create your first page\"],\"0a6MpL\":[\"New Redirect\"],\"0ieXE7\":[\"Highest rated\"],\"0yIy82\":[\"No featured posts yet.\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"Notes\"],\"1Oj1sI\":[\"Order saved\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"2N0qpv\":[\"Post title...\"],\"2cFU6q\":[\"Site Footer\"],\"2fUwEY\":[\"Select Media\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"Page title...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Siwmw\":[\"More options\"],\"3Yvsaz\":[\"302 (Temporary)\"],\"4/SFQS\":[\"View Site\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4KzVT6\":[\"Delete Page\"],\"4Ml90q\":[\"SEO\"],\"4b3oEV\":[\"Content\"],\"4mDPGp\":[\"The URL path for this page. Use lowercase letters, numbers, and hyphens.\"],\"6C8dEg\":[\"Attached Text\"],\"6WdDG7\":[\"Page\"],\"6YtxFj\":[\"Name\"],\"6tU2jr\":[\"No collections found.\"],\"71Xwww\":[\"Invalid request\"],\"7G4SBz\":[\"Page content (Markdown supported)...\"],\"7Mk+/h\":[\"Update Collection\"],\"7Q1KKN\":[\"From Path\"],\"7aECQB\":[\"Invalid or Expired Link\"],\"7nGhhM\":[\"What's on your mind?\"],\"7p5kLi\":[\"Dashboard\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8HgKQc\":[\"SEO settings saved successfully.\"],\"8WX0J+\":[\"Your thoughts (optional)\"],\"8WtVZw\":[\"Failed to save post. Please try again.\"],\"8ZsakT\":[\"Password\"],\"8qX8Jl\":[\"Choose a font pairing for your site. All options use system fonts for fast loading.\"],\"8tM8+a\":[\"Save as draft\"],\"9+vGLh\":[\"Custom CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"A1taO8\":[\"Search\"],\"AeXO77\":[\"Account\"],\"AyHO4m\":[\"What's this collection about?\"],\"B373X+\":[\"Edit Post\"],\"B495Gs\":[\"Archive\"],\"BjF0Jv\":[\"Lowercase letters, numbers, and hyphens only\"],\"Cl55aD\":[\"Current password is incorrect.\"],\"D3uuEX\":[\"No media selected yet.\"],\"D9Oea+\":[\"Permalink\"],\"DCKkhU\":[\"Current Password\"],\"DHhJ7s\":[\"Previous\"],\"DPfwMq\":[\"Done\"],\"DoJzLz\":[\"Collections\"],\"E80cJw\":[\"Deleting this media will remove it permanently from storage.\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"Complete Setup\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"Update\"],\"Eq6YVV\":[\"Score\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FGrimz\":[\"New Post\"],\"FkMol5\":[\"Featured\"],\"Fxf4jq\":[\"Description (optional)\"],\"GA5A5H\":[\"Delete Collection\"],\"GBDTf5\":[\"Auth not configured\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"Your site navigation\"],\"GX2VMa\":[\"Create your admin account.\"],\"GbVAnd\":[\"This password reset link is invalid or has expired. Please generate a new one.\"],\"GorKul\":[\"Welcome to Jant\"],\"GrZ6fH\":[\"New Page\"],\"GxkJXS\":[\"Uploading...\"],\"H29JXm\":[\"+ ALT\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"No posts found.\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"Save Changes\"],\"IagCbF\":[\"URL\"],\"J4FNfC\":[\"No posts in this collection.\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"Need help? Visit the <0>documentation</0>\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"Use this URL to embed the media in your posts.\"],\"KbS2K9\":[\"Reset Password\"],\"KdSsVl\":[\"Author (optional)\"],\"KiJn9B\":[\"Note\"],\"KmGXnO\":[\"Are you sure you want to delete this post? This cannot be undone.\"],\"KsIZ3c\":[\"Footer saved successfully.\"],\"KuCcWu\":[\"Displayed at the bottom of all posts and pages. Markdown supported.\"],\"L85WcV\":[\"Slug\"],\"LdyooL\":[\"link\"],\"LkA8jz\":[\"Add alt text\"],\"LkvLQe\":[\"No pages yet.\"],\"M1RvTd\":[\"Click image to view full size\"],\"M2kIWU\":[\"Font theme\"],\"M8kJqa\":[\"Drafts\"],\"M9xgHy\":[\"Redirects\"],\"MHrjPM\":[\"Title\"],\"MLSRl9\":[\"Quote Text\"],\"MWBOxm\":[\"Collections (optional)\"],\"MZbQHL\":[\"No results found.\"],\"MdMyne\":[\"Source link (optional)\"],\"Mhf/H/\":[\"Create Redirect\"],\"MnbH31\":[\"page\"],\"MqghUt\":[\"Search posts...\"],\"N0APCr\":[\"This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.\"],\"N40H+G\":[\"All\"],\"NU2Fqi\":[\"Save CSS\"],\"Naqg3G\":[\"No file provided\"],\"O3oNi5\":[\"Email\"],\"OCNZaU\":[\"The path to redirect from\"],\"ODiSoW\":[\"No posts yet.\"],\"ONWvwQ\":[\"Upload\"],\"OVSkIF\":[\"The quick brown fox jumps over the lazy dog.\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pbm2/N\":[\"Create Collection\"],\"QEbNBb\":[\"Path (e.g. /archive) or full URL (e.g. https://example.com)\"],\"QLkhbH\":[\"The text being quoted...\"],\"Qjlym2\":[\"Failed to create account\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"Links\"],\"RwGhWy\":[\"Thread with \",[\"count\"],\" posts\"],\"RxsRD6\":[\"Time Zone\"],\"SGJDS5\":[\"Storage not configured\"],\"SJmfuf\":[\"Site Name\"],\"ST+lN2\":[\"No media uploaded yet.\"],\"T0bsor\":[\"Settings saved successfully.\"],\"TNFigk\":[\"Default Homepage View\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"Settings\"],\"U5v6Gh\":[\"Edit Page\"],\"UDMjsP\":[\"Quick Actions\"],\"UGT5vp\":[\"Save Settings\"],\"UeOiKl\":[\"Invalid input\"],\"Ui5/i3\":[\"It's OK for search engines to index my site\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UxKoFf\":[\"Navigation\"],\"V4WsyL\":[\"Add Link\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VgozPa\":[\"Avatar display setting saved successfully.\"],\"VhMDMg\":[\"Change Password\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"General\"],\"WmZ/rP\":[\"To Path\"],\"XrnWzN\":[\"Published!\"],\"Y+7JGK\":[\"Create Page\"],\"Y75ho6\":[\"Other pages\"],\"YIix5Y\":[\"Search...\"],\"Z2lfX1\":[\"Choose Icon\"],\"Z3FXyt\":[\"Loading...\"],\"Z6NwTi\":[\"Save as Draft\"],\"ZQKLI1\":[\"Danger Zone\"],\"ZUpE9/\":[\"This is displayed at the bottom of all of your posts and pages. Markdown is supported.\"],\"ZhhOwV\":[\"Quote\"],\"aAIQg2\":[\"Appearance\"],\"aHTB7P\":[\"Supplementary content attached to your post\"],\"aT4jc4\":[\"Invalid email or password\"],\"aaGV/9\":[\"New Link\"],\"ajBsih\":[\"Post published successfully.\"],\"alKG0+\":[\"Font Theme\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"About this blog\"],\"b+/jO6\":[\"301 (Permanent)\"],\"b4VwHs\":[\"No file provided.\"],\"bG4pfW\":[\"Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.\"],\"bHYIks\":[\"Sign Out\"],\"bcE7Mx\":[\"Failed to update profile.\"],\"biOepV\":[\"← Back to home\"],\"bzSI52\":[\"Discard\"],\"cTUByn\":[\"Newest first\"],\"cnGeoo\":[\"Delete\"],\"dEgA5A\":[\"Cancel\"],\"dmCcPs\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square image at least 180x180 pixels.\"],\"dtQNkT\":[\"Invalid font theme selected.\"],\"e6Jr7Q\":[\"← Back to Collections\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"eneWvv\":[\"Draft\"],\"er8+x7\":[\"Demo account pre-filled. Just click Sign In.\"],\"f/bxrN\":[\"Name is required.\"],\"f6e0Ry\":[\"Article\"],\"fDGOiR\":[\"Passwords do not match.\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"My Collection\"],\"g3mKmM\":[\"Un-nav\"],\"gDx5MG\":[\"Edit Link\"],\"gJH6Bs\":[\"Alt text improves accessibility\"],\"gOwwEy\":[\"Storage not configured.\"],\"hDRU5q\":[\"Found \",[\"0\"],\" results\"],\"hG89Ed\":[\"Image\"],\"hWOZIv\":[\"Enter your new password.\"],\"hXzOVo\":[\"Next\"],\"he3ygx\":[\"Copy\"],\"heSQoS\":[\"Paste a URL...\"],\"hmXTCY\":[\"Invalid theme selected.\"],\"hrL0Be\":[\"Icon (optional)\"],\"i0vDGK\":[\"Sort Order\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"URL (optional)\"],\"iEqmSU\":[\"Custom CSS saved successfully.\"],\"iH8pgl\":[\"Back\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"Published pages are accessible via their slug. Drafts are not visible.\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jpctdh\":[\"View\"],\"jt/Ow/\":[\"posts\"],\"jvyYZG\":[\"What's on your mind...\"],\"k1ifdL\":[\"Processing...\"],\"kI1qVD\":[\"Format\"],\"kL1h6U\":[\"Remove divider\"],\"kNiQp6\":[\"Pinned\"],\"kPMIr+\":[\"Give it a title...\"],\"kd7eBB\":[\"Create Link\"],\"kj6ppi\":[\"entry\"],\"kyNTQ2\":[\"Emoji or icon name\"],\"l/6JHD\":[\"Search icons...\"],\"l6ANt9\":[\"Lowest rated\"],\"lO1Oow\":[\"Upload successful!\"],\"m16xKo\":[\"Add\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"Sign In\"],\"n6QD94\":[\"Oldest first\"],\"o21Y+P\":[\"entries\"],\"o4dofa\":[\"AUTH_SECRET not configured\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oKOOsY\":[\"Color Theme\"],\"oYPBa0\":[\"Update Page\"],\"p2/GCq\":[\"Confirm Password\"],\"pB0OKE\":[\"New Divider\"],\"pRhYH2\":[\"Posts in Collection (\",[\"count\"],\")\"],\"pZq3aX\":[\"Upload failed. Please try again.\"],\"pnve/d\":[\"Profile saved successfully.\"],\"q+hNag\":[\"Collection\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qdxmd4\":[\"Attached text\"],\"qiXmlF\":[\"Add Media\"],\"qt89I8\":[\"Draft saved.\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"Color theme\"],\"rdUucN\":[\"Preview\"],\"rzNUSl\":[\"Thread with 1 post\"],\"sGajR7\":[\"Thread start\"],\"smzF8S\":[\"Show \",[\"remainingCount\"],\" more \",[\"0\"]],\"ssqvZi\":[\"Save Profile\"],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"tfDRzk\":[\"Save\"],\"tfrt7B\":[\"No redirects configured.\"],\"tiq7kl\":[\"Page \",[\"page\"]],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"Published\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"Status\"],\"vERlcd\":[\"Profile\"],\"vXIe7J\":[\"Language\"],\"vh0C9b\":[\"No navigation links yet. Add pages to navigation or create links.\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vzU4k9\":[\"New Collection\"],\"wEF6Ix\":[\"The destination path or URL\"],\"wK4OTM\":[\"Title (optional)\"],\"wL3cK8\":[\"Latest\"],\"wM5UXj\":[\"Delete Media\"],\"wRR604\":[\"Pages\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"Untitled\"],\"x+doid\":[\"Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.\"],\"x0mzE0\":[\"Create your first post\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"Media\"],\"y0R9F0\":[\"Post updated successfully.\"],\"y28hnO\":[\"Post\"],\"yQ2kGp\":[\"Load more\"],\"yjkELF\":[\"Confirm New Password\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"Found 1 result\"],\"zBFr9G\":[\"Paste a long article, AI response, or any text...\\n\\nMarkdown formatting will be preserved.\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"],\"zennIg\":[\"URL-safe identifier (lowercase, numbers, hyphens). For CJK titles, slug will be auto-generated on the server.\"]}");
4455
+ /*eslint-disable*/ const messages$2 = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+AXdXp\":[\"Label and URL are required\"],\"+MACwa\":[\"No collections yet.\"],\"+MH6k9\":[\"Add to nav\"],\"+bHzpy\":[\"Display text for the link\"],\"+owNNn\":[\"Posts\"],\"+zy2Nq\":[\"Type\"],\"/0D1Xp\":[\"Edit Collection\"],\"/DFKdU\":[\"Type the quote...\"],\"/R/sGB\":[\"Password changed successfully.\"],\"/Rj5P4\":[\"Your Name\"],\"/rkqRV\":[\"All pages are in your navigation.\"],\"07Epll\":[\"This will theme both your site and your dashboard. All color themes support dark mode.\"],\"0JkyS7\":[\"Create your first page\"],\"0a6MpL\":[\"New Redirect\"],\"0ieXE7\":[\"Highest rated\"],\"0yIy82\":[\"No featured posts yet.\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"Notes\"],\"1Oj1sI\":[\"Order saved\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"2N0qpv\":[\"Post title...\"],\"2cFU6q\":[\"Site Footer\"],\"2fUwEY\":[\"Select Media\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"Page title...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Siwmw\":[\"More options\"],\"3Yvsaz\":[\"302 (Temporary)\"],\"3uSoGn\":[\"Header Nav Links\"],\"4/SFQS\":[\"View Site\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4JBD+x\":[\"Failed to save. Please try again.\"],\"4KzVT6\":[\"Delete Page\"],\"4Ml90q\":[\"SEO\"],\"4b3oEV\":[\"Content\"],\"4mDPGp\":[\"The URL path for this page. Use lowercase letters, numbers, and hyphens.\"],\"538Vy5\":[\"No navigation items yet. Add pages, links, or enable system items below.\"],\"6C8dEg\":[\"Attached Text\"],\"6WdDG7\":[\"Page\"],\"6YtxFj\":[\"Name\"],\"6tU2jr\":[\"No collections found.\"],\"71Xwww\":[\"Invalid request\"],\"7G4SBz\":[\"Page content (Markdown supported)...\"],\"7Mk+/h\":[\"Update Collection\"],\"7Q1KKN\":[\"From Path\"],\"7aECQB\":[\"Invalid or Expired Link\"],\"7aYVPs\":[\"All pages are in navigation\"],\"7nGhhM\":[\"What's on your mind?\"],\"7p5kLi\":[\"Dashboard\"],\"7vhWI8\":[\"New Password\"],\"87a/t/\":[\"Label\"],\"8HgKQc\":[\"SEO settings saved successfully.\"],\"8WX0J+\":[\"Your thoughts (optional)\"],\"8WtVZw\":[\"Failed to save post. Please try again.\"],\"8ZsakT\":[\"Password\"],\"8qX8Jl\":[\"Choose a font pairing for your site. All options use system fonts for fast loading.\"],\"8tM8+a\":[\"Save as draft\"],\"8xE385\":[\"Add to navigation\"],\"9+vGLh\":[\"Custom CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"A1taO8\":[\"Search\"],\"AeXO77\":[\"Account\"],\"AyHO4m\":[\"What's this collection about?\"],\"Az4JB1\":[\"Use Featured as default home view\"],\"B373X+\":[\"Edit Post\"],\"B495Gs\":[\"Archive\"],\"BjF0Jv\":[\"Lowercase letters, numbers, and hyphens only\"],\"Cl55aD\":[\"Current password is incorrect.\"],\"D3uuEX\":[\"No media selected yet.\"],\"D9Oea+\":[\"Permalink\"],\"DCKkhU\":[\"Current Password\"],\"DHhJ7s\":[\"Previous\"],\"DPfwMq\":[\"Done\"],\"DVljCN\":[\"Choose a page…\"],\"DoJzLz\":[\"Collections\"],\"E80cJw\":[\"Deleting this media will remove it permanently from storage.\"],\"EEYbdt\":[\"Publish\"],\"EGwzOK\":[\"Complete Setup\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"Update\"],\"Eq6YVV\":[\"Score\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FGrimz\":[\"New Post\"],\"FkMol5\":[\"Featured\"],\"Fxf4jq\":[\"Description (optional)\"],\"GA5A5H\":[\"Delete Collection\"],\"GBDTf5\":[\"Auth not configured\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"Your site navigation\"],\"GX2VMa\":[\"Create your admin account.\"],\"GbVAnd\":[\"This password reset link is invalid or has expired. Please generate a new one.\"],\"GlEzsR\":[\"A short intro for search engines and feed readers. Plain text only.\"],\"GorKul\":[\"Welcome to Jant\"],\"GrZ6fH\":[\"New Page\"],\"GxkJXS\":[\"Uploading...\"],\"H29JXm\":[\"+ ALT\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"No posts found.\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"Save Changes\"],\"IagCbF\":[\"URL\"],\"J4FNfC\":[\"No posts in this collection.\"],\"J6bLeg\":[\"Add a custom link to any URL\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"Need help? Visit the <0>documentation</0>\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"Use this URL to embed the media in your posts.\"],\"KbS2K9\":[\"Reset Password\"],\"KdSsVl\":[\"Author (optional)\"],\"KiJn9B\":[\"Note\"],\"KmGXnO\":[\"Are you sure you want to delete this post? This cannot be undone.\"],\"KsIZ3c\":[\"Footer saved successfully.\"],\"KuCcWu\":[\"Displayed at the bottom of all posts and pages. Markdown supported.\"],\"L85WcV\":[\"Slug\"],\"LdyooL\":[\"link\"],\"LkA8jz\":[\"Add alt text\"],\"LkvLQe\":[\"No pages yet.\"],\"M1RvTd\":[\"Click image to view full size\"],\"M2kIWU\":[\"Font theme\"],\"M6CbAU\":[\"Toggle edit panel\"],\"M8kJqa\":[\"Drafts\"],\"M8lheL\":[\"Max Visible Nav Links\"],\"M9xgHy\":[\"Redirects\"],\"MHrjPM\":[\"Title\"],\"MLSRl9\":[\"Quote Text\"],\"MWBOxm\":[\"Collections (optional)\"],\"MZbQHL\":[\"No results found.\"],\"MdMyne\":[\"Source link (optional)\"],\"Mhf/H/\":[\"Create Redirect\"],\"MnbH31\":[\"page\"],\"MqghUt\":[\"Search posts...\"],\"N0APCr\":[\"This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.\"],\"N40H+G\":[\"All\"],\"NU2Fqi\":[\"Save CSS\"],\"Naqg3G\":[\"No file provided\"],\"O3oNi5\":[\"Email\"],\"OCNZaU\":[\"The path to redirect from\"],\"ODiSoW\":[\"No posts yet.\"],\"ONWvwQ\":[\"Upload\"],\"OVSkIF\":[\"The quick brown fox jumps over the lazy dog.\"],\"OeUWA7\":[\"Add Page\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PJnyHS\":[\"Max visible links saved\"],\"PZ7HJ8\":[\"Blog Avatar\"],\"Pbm2/N\":[\"Create Collection\"],\"QEbNBb\":[\"Path (e.g. /archive) or full URL (e.g. https://example.com)\"],\"QLkhbH\":[\"The text being quoted...\"],\"Qjlym2\":[\"Failed to create account\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"Links\"],\"RwGhWy\":[\"Thread with \",[\"count\"],\" posts\"],\"RxsRD6\":[\"Time Zone\"],\"SGJDS5\":[\"Storage not configured\"],\"SJmfuf\":[\"Site Name\"],\"ST+lN2\":[\"No media uploaded yet.\"],\"T0bsor\":[\"Settings saved successfully.\"],\"TNFigk\":[\"Default Homepage View\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"Settings\"],\"U5v6Gh\":[\"Edit Page\"],\"UDMjsP\":[\"Quick Actions\"],\"UGT5vp\":[\"Save Settings\"],\"UeOiKl\":[\"Invalid input\"],\"Ui5/i3\":[\"It's OK for search engines to index my site\"],\"Uj/btJ\":[\"Display avatar in my site header\"],\"UxKoFf\":[\"Navigation\"],\"UzGRD9\":[\"Home view saved\"],\"V4WsyL\":[\"Add Link\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VgozPa\":[\"Avatar display setting saved successfully.\"],\"VhMDMg\":[\"Change Password\"],\"Vn3jYy\":[\"Navigation items\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"General\"],\"WmZ/rP\":[\"To Path\"],\"XrnWzN\":[\"Published!\"],\"Y+7JGK\":[\"Create Page\"],\"Y75ho6\":[\"Other pages\"],\"YIix5Y\":[\"Search...\"],\"Z2lfX1\":[\"Choose Icon\"],\"Z3FXyt\":[\"Loading...\"],\"Z6NwTi\":[\"Save as Draft\"],\"ZQKLI1\":[\"Danger Zone\"],\"ZUpE9/\":[\"This is displayed at the bottom of all of your posts and pages. Markdown is supported.\"],\"ZhhOwV\":[\"Quote\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"aAIQg2\":[\"Appearance\"],\"aHTB7P\":[\"Supplementary content attached to your post\"],\"aT4jc4\":[\"Invalid email or password\"],\"aaGV/9\":[\"New Link\"],\"ajBsih\":[\"Post published successfully.\"],\"alKG0+\":[\"Font Theme\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"About this blog\"],\"b+/jO6\":[\"301 (Permanent)\"],\"b+FyBD\":[\"Add page to navigation\"],\"b+JhJf\":[\"Max visible links\"],\"b4VwHs\":[\"No file provided.\"],\"bDqhXY\":[\"Failed to delete. Please try again.\"],\"bG4pfW\":[\"Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.\"],\"bHYIks\":[\"Sign Out\"],\"bcE7Mx\":[\"Failed to update profile.\"],\"biOepV\":[\"← Back to home\"],\"bzSI52\":[\"Discard\"],\"cTUByn\":[\"Newest first\"],\"ccaIM9\":[\"More links\"],\"cnGeoo\":[\"Delete\"],\"dEgA5A\":[\"Cancel\"],\"dStw5E\":[\"Add an existing page to your navigation\"],\"dmCcPs\":[\"This is used for your favicon and apple-touch-icon. For best results, upload a square image at least 180x180 pixels.\"],\"dtQNkT\":[\"Invalid font theme selected.\"],\"e6Jr7Q\":[\"← Back to Collections\"],\"ePK91l\":[\"Edit\"],\"eWLklq\":[\"Quotes\"],\"eneWvv\":[\"Draft\"],\"er8+x7\":[\"Demo account pre-filled. Just click Sign In.\"],\"f/bxrN\":[\"Name is required.\"],\"f6e0Ry\":[\"Article\"],\"fDGOiR\":[\"Passwords do not match.\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"My Collection\"],\"g3mKmM\":[\"Un-nav\"],\"gDx5MG\":[\"Edit Link\"],\"gJH6Bs\":[\"Alt text improves accessibility\"],\"gOwwEy\":[\"Storage not configured.\"],\"hDRU5q\":[\"Found \",[\"0\"],\" results\"],\"hG89Ed\":[\"Image\"],\"hQAbqI\":[\"No pages yet. Create your first page to get started.\"],\"hWOZIv\":[\"Enter your new password.\"],\"hXzOVo\":[\"Next\"],\"he3ygx\":[\"Copy\"],\"heSQoS\":[\"Paste a URL...\"],\"hmXTCY\":[\"Invalid theme selected.\"],\"hrL0Be\":[\"Icon (optional)\"],\"i0vDGK\":[\"Sort Order\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"URL (optional)\"],\"iEUzMn\":[\"system\"],\"iEqmSU\":[\"Custom CSS saved successfully.\"],\"iH8pgl\":[\"Back\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"Published pages are accessible via their slug. Drafts are not visible.\"],\"jUV7CU\":[\"Upload Avatar\"],\"jVUmOK\":[\"Markdown supported\"],\"jpctdh\":[\"View\"],\"jt/Ow/\":[\"posts\"],\"jvyYZG\":[\"What's on your mind...\"],\"k1ifdL\":[\"Processing...\"],\"kI1qVD\":[\"Format\"],\"kL1h6U\":[\"Remove divider\"],\"kNiQp6\":[\"Pinned\"],\"kPMIr+\":[\"Give it a title...\"],\"kd7eBB\":[\"Create Link\"],\"kj6ppi\":[\"entry\"],\"kyNTQ2\":[\"Emoji or icon name\"],\"l/6JHD\":[\"Search icons...\"],\"l6ANt9\":[\"Lowest rated\"],\"lO1Oow\":[\"Upload successful!\"],\"m16xKo\":[\"Add\"],\"mO5HMZ\":[\"All pages are already in navigation.\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"Sign In\"],\"n6QD94\":[\"Oldest first\"],\"o21Y+P\":[\"entries\"],\"o4dofa\":[\"AUTH_SECRET not configured\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oKOOsY\":[\"Color Theme\"],\"oSiRP0\":[\"System links\"],\"oYPBa0\":[\"Update Page\"],\"p2/GCq\":[\"Confirm Password\"],\"pB0OKE\":[\"New Divider\"],\"pI2MWS\":[\"Search pages…\"],\"pRhYH2\":[\"Posts in Collection (\",[\"count\"],\")\"],\"pZq3aX\":[\"Upload failed. Please try again.\"],\"pnve/d\":[\"Profile saved successfully.\"],\"q+hNag\":[\"Collection\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qdxmd4\":[\"Attached text\"],\"qiXmlF\":[\"Add Media\"],\"qt89I8\":[\"Draft saved.\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"Color theme\"],\"rdUucN\":[\"Preview\"],\"rzNUSl\":[\"Thread with 1 post\"],\"sDGoxy\":[\"Toggle built-in navigation items. Enabled items appear in your navigation alongside pages and links.\"],\"sGajR7\":[\"Thread start\"],\"smzF8S\":[\"Show \",[\"remainingCount\"],\" more \",[\"0\"]],\"ssqvZi\":[\"Save Profile\"],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"Remove\"],\"tfDRzk\":[\"Save\"],\"tfQNeI\":[\"No pages found.\"],\"tfrt7B\":[\"No redirects configured.\"],\"tiq7kl\":[\"Page \",[\"page\"]],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"Published\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"Status\"],\"vERlcd\":[\"Profile\"],\"vXIe7J\":[\"Language\"],\"vh0C9b\":[\"No navigation links yet. Add pages to navigation or create links.\"],\"vmQmHx\":[\"Add custom CSS to override any styles. Use data attributes like [data-page], [data-post], [data-format] to target specific elements.\"],\"vzU4k9\":[\"New Collection\"],\"w8Rv8T\":[\"Label is required\"],\"wEF6Ix\":[\"The destination path or URL\"],\"wK4OTM\":[\"Title (optional)\"],\"wL3cK8\":[\"Latest\"],\"wM5UXj\":[\"Delete Media\"],\"wRR604\":[\"Pages\"],\"wc+17X\":[\"/* Your custom CSS here */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"Untitled\"],\"x+doid\":[\"Images are automatically optimized: resized to max 1920px, converted to WebP, and metadata stripped.\"],\"x0mzE0\":[\"Create your first post\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"Media\"],\"y0R9F0\":[\"Post updated successfully.\"],\"y28hnO\":[\"Post\"],\"yQ2kGp\":[\"Load more\"],\"yjkELF\":[\"Confirm New Password\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"Found 1 result\"],\"zBFr9G\":[\"Paste a long article, AI response, or any text...\\n\\nMarkdown formatting will be preserved.\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"],\"zennIg\":[\"URL-safe identifier (lowercase, numbers, hyphens). For CJK titles, slug will be auto-generated on the server.\"],\"zucql+\":[\"Menu\"]}");
3996
4456
 
3997
- /*eslint-disable*/ const messages$1 = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+AXdXp\":[\"标签和 URL 是必填项\"],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"添加到导航\"],\"+bHzpy\":[\"链接的显示文本\"],\"+owNNn\":[\"帖子\"],\"+zy2Nq\":[\"类型\"],\"/0D1Xp\":[\"编辑集合\"],\"/DFKdU\":[\"输入引用...\"],\"/R/sGB\":[\"密码更改成功。\"],\"/Rj5P4\":[\"您的姓名\"],\"/rkqRV\":[\"所有页面都在您的导航中。\"],\"07Epll\":[\"这将为您的网站和仪表板设置主题。所有颜色主题都支持暗黑模式。\"],\"0JkyS7\":[\"Create your first page\"],\"0a6MpL\":[\"新重定向\"],\"0ieXE7\":[\"最高评分\"],\"0yIy82\":[\"尚无精选帖子。\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"笔记\"],\"1Oj1sI\":[\"已保存顺序\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"2N0qpv\":[\"帖子标题...\"],\"2cFU6q\":[\"网站页脚\"],\"2fUwEY\":[\"选择媒体\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"页面标题...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Siwmw\":[\"More options\"],\"3Yvsaz\":[\"302(临时)\"],\"4/SFQS\":[\"查看网站\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4KzVT6\":[\"删除页面\"],\"4Ml90q\":[\"搜索引擎优化\"],\"4b3oEV\":[\"内容\"],\"4mDPGp\":[\"此页面的 URL 路径。使用小写字母、数字和连字符。\"],\"6C8dEg\":[\"附加文本\"],\"6WdDG7\":[\"页面\"],\"6YtxFj\":[\"姓名\"],\"6tU2jr\":[\"No collections found.\"],\"71Xwww\":[\"无效的请求\"],\"7G4SBz\":[\"页面内容(支持Markdown)...\"],\"7Mk+/h\":[\"更新收藏夹\"],\"7Q1KKN\":[\"来源路径\"],\"7aECQB\":[\"无效或过期的链接\"],\"7nGhhM\":[\"你在想什么?\"],\"7p5kLi\":[\"仪表板\"],\"7vhWI8\":[\"新密码\"],\"87a/t/\":[\"标签\"],\"8HgKQc\":[\"SEO设置已成功保存。\"],\"8WX0J+\":[\"您的想法(可选)\"],\"8WtVZw\":[\"保存帖子失败。请再试一次。\"],\"8ZsakT\":[\"密码\"],\"8qX8Jl\":[\"选择一个字体搭配用于您的网站。所有选项都使用系统字体以实现快速加载。\"],\"8tM8+a\":[\"保存为草稿\"],\"9+vGLh\":[\"自定义 CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"A1taO8\":[\"搜索\"],\"AeXO77\":[\"账户\"],\"AyHO4m\":[\"这个系列是关于什么的?\"],\"B373X+\":[\"编辑帖子\"],\"B495Gs\":[\"档案馆\"],\"BjF0Jv\":[\"仅允许小写字母、数字和连字符\"],\"Cl55aD\":[\"当前密码不正确。\"],\"D3uuEX\":[\"尚未选择任何媒体。\"],\"D9Oea+\":[\"永久链接\"],\"DCKkhU\":[\"当前密码\"],\"DHhJ7s\":[\"上一页\"],\"DPfwMq\":[\"完成\"],\"DoJzLz\":[\"收藏夹\"],\"E80cJw\":[\"删除此媒体将永久从存储中移除。\"],\"EEYbdt\":[\"发布\"],\"EGwzOK\":[\"完成设置\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"更新\"],\"Eq6YVV\":[\"得分\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FGrimz\":[\"新帖子\"],\"FkMol5\":[\"精选\"],\"Fxf4jq\":[\"描述(可选)\"],\"GA5A5H\":[\"Delete Collection\"],\"GBDTf5\":[\"身份验证未配置\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"您的网站导航\"],\"GX2VMa\":[\"创建您的管理员账户。\"],\"GbVAnd\":[\"此密码重置链接无效或已过期。请生成一个新的链接。\"],\"GorKul\":[\"欢迎来到Jant\"],\"GrZ6fH\":[\"新页面\"],\"GxkJXS\":[\"上传中...\"],\"H29JXm\":[\"+ ALT\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"未找到帖子。\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"保存更改\"],\"IagCbF\":[\"网址\"],\"J4FNfC\":[\"此集合中没有帖子。\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"需要帮助吗?访问<0>文档</0>。\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"使用此 URL 将媒体嵌入到您的帖子中。\"],\"KbS2K9\":[\"重置密码\"],\"KdSsVl\":[\"作者(可选)\"],\"KiJn9B\":[\"注意\"],\"KmGXnO\":[\"您确定要删除此帖子吗?此操作无法撤销。\"],\"KsIZ3c\":[\"页脚保存成功。\"],\"KuCcWu\":[\"Displayed at the bottom of all posts and pages. Markdown supported.\"],\"L85WcV\":[\"缩略名\"],\"LdyooL\":[\"链接\"],\"LkA8jz\":[\"Add alt text\"],\"LkvLQe\":[\"No pages yet.\"],\"M1RvTd\":[\"点击图片查看完整尺寸\"],\"M2kIWU\":[\"字体主题\"],\"M8kJqa\":[\"草稿\"],\"M9xgHy\":[\"重定向\"],\"MHrjPM\":[\"标题\"],\"MLSRl9\":[\"引用文本\"],\"MWBOxm\":[\"集合(可选)\"],\"MZbQHL\":[\"未找到结果。\"],\"MdMyne\":[\"来源链接(可选)\"],\"Mhf/H/\":[\"创建重定向\"],\"MnbH31\":[\"页面\"],\"MqghUt\":[\"搜索帖子...\"],\"N0APCr\":[\"这将在您的默认主页上显示在博客文章上方。这也用于您主页上的元描述。\"],\"N40H+G\":[\"所有\"],\"NU2Fqi\":[\"保存 CSS\"],\"Naqg3G\":[\"未提供文件\"],\"O3oNi5\":[\"电子邮件\"],\"OCNZaU\":[\"重定向的路径\"],\"ODiSoW\":[\"还没有帖子。\"],\"ONWvwQ\":[\"上传\"],\"OVSkIF\":[\"敏捷的棕色狐狸跳过懒狗。\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PZ7HJ8\":[\"博客头像\"],\"Pbm2/N\":[\"创建集合\"],\"QEbNBb\":[\"路径(例如 /archive)或完整 URL(例如 https://example.com)\"],\"QLkhbH\":[\"被引用的文本...\"],\"Qjlym2\":[\"创建账户失败\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"链接\"],\"RwGhWy\":[\"包含 \",[\"count\"],\" 条帖子的话题\"],\"RxsRD6\":[\"时区\"],\"SGJDS5\":[\"存储未配置\"],\"SJmfuf\":[\"网站名称\"],\"ST+lN2\":[\"尚未上传任何媒体。\"],\"T0bsor\":[\"设置已成功保存。\"],\"TNFigk\":[\"默认主页视图\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"设置\"],\"U5v6Gh\":[\"编辑页面\"],\"UDMjsP\":[\"快速操作\"],\"UGT5vp\":[\"Save Settings\"],\"UeOiKl\":[\"无效输入\"],\"Ui5/i3\":[\"允许搜索引擎索引我的网站是可以的\"],\"Uj/btJ\":[\"在我的网站头部显示头像\"],\"UxKoFf\":[\"Navigation\"],\"V4WsyL\":[\"添加链接\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VgozPa\":[\"头像显示设置已成功保存。\"],\"VhMDMg\":[\"更改密码\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"常规\"],\"WmZ/rP\":[\"到路径\"],\"XrnWzN\":[\"Published!\"],\"Y+7JGK\":[\"创建页面\"],\"Y75ho6\":[\"其他页面\"],\"YIix5Y\":[\"Search...\"],\"Z2lfX1\":[\"选择图标\"],\"Z3FXyt\":[\"加载中...\"],\"Z6NwTi\":[\"保存为草稿\"],\"ZQKLI1\":[\"危险区域\"],\"ZUpE9/\":[\"这将在您所有的帖子和页面底部显示。支持Markdown。\"],\"ZhhOwV\":[\"引用\"],\"aAIQg2\":[\"外观\"],\"aHTB7P\":[\"附加在您帖子上的补充内容\"],\"aT4jc4\":[\"无效的电子邮件或密码\"],\"aaGV/9\":[\"新链接\"],\"ajBsih\":[\"帖子发布成功。\"],\"alKG0+\":[\"Font Theme\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"关于这个博客\"],\"b+/jO6\":[\"301(永久)\"],\"b4VwHs\":[\"未提供文件。\"],\"bG4pfW\":[\"Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.\"],\"bHYIks\":[\"登出\"],\"bcE7Mx\":[\"更新个人资料失败。\"],\"biOepV\":[\"← Back to home\"],\"bzSI52\":[\"丢弃\"],\"cTUByn\":[\"最新优先\"],\"cnGeoo\":[\"删除\"],\"dEgA5A\":[\"取消\"],\"dmCcPs\":[\"这用于您的网站图标和苹果触控图标。为了获得最佳效果,请上传至少 180x180 像素的正方形图像。\"],\"dtQNkT\":[\"所选字体主题无效。\"],\"e6Jr7Q\":[\"← 返回收藏夹\"],\"ePK91l\":[\"编辑\"],\"eWLklq\":[\"引用\"],\"eneWvv\":[\"草稿\"],\"er8+x7\":[\"演示账户已预填。只需点击登录。\"],\"f/bxrN\":[\"名称是必需的。\"],\"f6e0Ry\":[\"Article\"],\"fDGOiR\":[\"密码不匹配。\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"我的收藏\"],\"g3mKmM\":[\"取消导航\"],\"gDx5MG\":[\"编辑链接\"],\"gJH6Bs\":[\"Alt text improves accessibility\"],\"gOwwEy\":[\"存储未配置。\"],\"hDRU5q\":[\"找到 \",[\"0\"],\" 个结果\"],\"hG89Ed\":[\"Image\"],\"hWOZIv\":[\"输入您的新密码。\"],\"hXzOVo\":[\"下一页\"],\"he3ygx\":[\"复制\"],\"heSQoS\":[\"粘贴一个网址...\"],\"hmXTCY\":[\"所选主题无效。\"],\"hrL0Be\":[\"图标(可选)\"],\"i0vDGK\":[\"排序顺序\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"网址(可选)\"],\"iEqmSU\":[\"自定义 CSS 保存成功。\"],\"iH8pgl\":[\"返回\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"已发布的页面可以通过其别名访问。草稿不可见。\"],\"jUV7CU\":[\"上传头像\"],\"jVUmOK\":[\"支持Markdown\"],\"jpctdh\":[\"查看\"],\"jt/Ow/\":[\"posts\"],\"jvyYZG\":[\"你在想什么...\"],\"k1ifdL\":[\"处理中...\"],\"kI1qVD\":[\"格式\"],\"kL1h6U\":[\"移除分隔符\"],\"kNiQp6\":[\"已固定\"],\"kPMIr+\":[\"给它一个标题...\"],\"kd7eBB\":[\"创建链接\"],\"kj6ppi\":[\"条目\"],\"kyNTQ2\":[\"Emoji or icon name\"],\"l/6JHD\":[\"搜索图标...\"],\"l6ANt9\":[\"最低评分\"],\"lO1Oow\":[\"上传成功!\"],\"m16xKo\":[\"Add\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"登录\"],\"n6QD94\":[\"最旧的在前\"],\"o21Y+P\":[\"条目\"],\"o4dofa\":[\"AUTH_SECRET 未配置\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oKOOsY\":[\"Color Theme\"],\"oYPBa0\":[\"更新页面\"],\"p2/GCq\":[\"确认密码\"],\"pB0OKE\":[\"新分隔符\"],\"pRhYH2\":[\"集合中的帖子 (\",[\"count\"],\")\"],\"pZq3aX\":[\"上传失败。请再试一次。\"],\"pnve/d\":[\"个人资料保存成功。\"],\"q+hNag\":[\"集合\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qdxmd4\":[\"Attached text\"],\"qiXmlF\":[\"添加媒体\"],\"qt89I8\":[\"草稿已保存。\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"颜色主题\"],\"rdUucN\":[\"预览\"],\"rzNUSl\":[\"包含 1 条帖子的话题\"],\"sGajR7\":[\"线程开始\"],\"smzF8S\":[\"显示 \",[\"remainingCount\"],\" 个更多 \",[\"0\"]],\"ssqvZi\":[\"保存个人资料\"],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"移除\"],\"tfDRzk\":[\"保存\"],\"tfrt7B\":[\"未配置重定向。\"],\"tiq7kl\":[\"页面 \",[\"page\"]],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"已发布\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"状态\"],\"vERlcd\":[\"个人资料\"],\"vXIe7J\":[\"语言\"],\"vh0C9b\":[\"尚未有导航链接。请将页面添加到导航或创建链接。\"],\"vmQmHx\":[\"添加自定义 CSS 以覆盖任何样式。使用数据属性,如 [data-page]、[data-post]、[data-format] 来定位特定元素。\"],\"vzU4k9\":[\"新收藏\"],\"wEF6Ix\":[\"目标路径或 URL\"],\"wK4OTM\":[\"标题(可选)\"],\"wL3cK8\":[\"最新\"],\"wM5UXj\":[\"删除媒体\"],\"wRR604\":[\"页面\"],\"wc+17X\":[\"/* 您的自定义 CSS 在这里 */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"无标题\"],\"x+doid\":[\"图像会自动优化:调整大小至最大 1920px,转换为 WebP,并去除元数据。\"],\"x0mzE0\":[\"创建你的第一篇帖子\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"媒体\"],\"y0R9F0\":[\"帖子更新成功。\"],\"y28hnO\":[\"帖子\"],\"yQ2kGp\":[\"加载更多\"],\"yjkELF\":[\"确认新密码\"],\"yz7wBu\":[\"关闭\"],\"yzF66j\":[\"链接\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"找到 1 个结果\"],\"zBFr9G\":[\"粘贴一篇长文章、AI 响应或任何文本...\\n\\nMarkdown 格式将被保留。\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"],\"zennIg\":[\"URL安全标识符(小写字母、数字、连字符)。对于CJK标题,slug将在服务器上自动生成。\"]}");
4457
+ /*eslint-disable*/ const messages$1 = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+AXdXp\":[\"标签和 URL 是必填项\"],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"Add to nav\"],\"+bHzpy\":[\"Display text for the link\"],\"+owNNn\":[\"帖子\"],\"+zy2Nq\":[\"类型\"],\"/0D1Xp\":[\"编辑集合\"],\"/DFKdU\":[\"输入引用...\"],\"/R/sGB\":[\"密码更改成功。\"],\"/Rj5P4\":[\"您的姓名\"],\"/rkqRV\":[\"All pages are in your navigation.\"],\"07Epll\":[\"这将为您的网站和仪表板设置主题。所有颜色主题都支持暗黑模式。\"],\"0JkyS7\":[\"Create your first page\"],\"0a6MpL\":[\"新重定向\"],\"0ieXE7\":[\"最高评分\"],\"0yIy82\":[\"尚无精选帖子。\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"笔记\"],\"1Oj1sI\":[\"已保存顺序\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"2N0qpv\":[\"帖子标题...\"],\"2cFU6q\":[\"网站页脚\"],\"2fUwEY\":[\"选择媒体\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"页面标题...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Siwmw\":[\"More options\"],\"3Yvsaz\":[\"302(临时)\"],\"3uSoGn\":[\"Header Nav Links\"],\"4/SFQS\":[\"查看网站\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4JBD+x\":[\"保存失败。请再试一次。\"],\"4KzVT6\":[\"删除页面\"],\"4Ml90q\":[\"搜索引擎优化\"],\"4b3oEV\":[\"内容\"],\"4mDPGp\":[\"此页面的 URL 路径。使用小写字母、数字和连字符。\"],\"538Vy5\":[\"尚未有导航项。请在下面添加页面、链接或启用系统项。\"],\"6C8dEg\":[\"附加文本\"],\"6WdDG7\":[\"页面\"],\"6YtxFj\":[\"姓名\"],\"6tU2jr\":[\"未找到任何收藏。\"],\"71Xwww\":[\"Invalid request\"],\"7G4SBz\":[\"页面内容(支持Markdown)...\"],\"7Mk+/h\":[\"更新收藏夹\"],\"7Q1KKN\":[\"来源路径\"],\"7aECQB\":[\"无效或过期的链接\"],\"7aYVPs\":[\"所有页面都在导航中\"],\"7nGhhM\":[\"你在想什么?\"],\"7p5kLi\":[\"仪表板\"],\"7vhWI8\":[\"新密码\"],\"87a/t/\":[\"标签\"],\"8HgKQc\":[\"SEO设置已成功保存。\"],\"8WX0J+\":[\"您的想法(可选)\"],\"8WtVZw\":[\"保存帖子失败。请再试一次。\"],\"8ZsakT\":[\"密码\"],\"8qX8Jl\":[\"选择一个字体搭配用于您的网站。所有选项都使用系统字体以实现快速加载。\"],\"8tM8+a\":[\"保存为草稿\"],\"8xE385\":[\"添加到导航\"],\"9+vGLh\":[\"自定义 CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"A1taO8\":[\"搜索\"],\"AeXO77\":[\"账户\"],\"AyHO4m\":[\"这个系列是关于什么的?\"],\"Az4JB1\":[\"Use Featured as default home view\"],\"B373X+\":[\"编辑帖子\"],\"B495Gs\":[\"档案馆\"],\"BjF0Jv\":[\"仅允许小写字母、数字和连字符\"],\"Cl55aD\":[\"当前密码不正确。\"],\"D3uuEX\":[\"尚未选择任何媒体。\"],\"D9Oea+\":[\"永久链接\"],\"DCKkhU\":[\"当前密码\"],\"DHhJ7s\":[\"上一页\"],\"DPfwMq\":[\"完成\"],\"DVljCN\":[\"Choose a page…\"],\"DoJzLz\":[\"收藏夹\"],\"E80cJw\":[\"删除此媒体将永久从存储中移除。\"],\"EEYbdt\":[\"发布\"],\"EGwzOK\":[\"完成设置\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"更新\"],\"Eq6YVV\":[\"得分\"],\"FESYvt\":[\"为视力障碍人士描述这一内容...\"],\"FGrimz\":[\"新帖子\"],\"FkMol5\":[\"精选\"],\"Fxf4jq\":[\"描述(可选)\"],\"GA5A5H\":[\"Delete Collection\"],\"GBDTf5\":[\"身份验证未配置\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"Your site navigation\"],\"GX2VMa\":[\"创建您的管理员账户。\"],\"GbVAnd\":[\"此密码重置链接无效或已过期。请生成一个新的链接。\"],\"GlEzsR\":[\"搜索引擎和订阅阅读器的简短介绍。仅限纯文本。\"],\"GorKul\":[\"欢迎来到Jant\"],\"GrZ6fH\":[\"新页面\"],\"GxkJXS\":[\"上传中...\"],\"H29JXm\":[\"+ ALT\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"未找到帖子。\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"Save Changes\"],\"IagCbF\":[\"网址\"],\"J4FNfC\":[\"此集合中没有帖子。\"],\"J6bLeg\":[\"添加自定义链接到任何 URL\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"需要帮助吗?访问<0>文档</0>。\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"使用此 URL 将媒体嵌入到您的帖子中。\"],\"KbS2K9\":[\"重置密码\"],\"KdSsVl\":[\"作者(可选)\"],\"KiJn9B\":[\"注意\"],\"KmGXnO\":[\"您确定要删除此帖子吗?此操作无法撤销。\"],\"KsIZ3c\":[\"Footer saved successfully.\"],\"KuCcWu\":[\"显示在所有帖子和页面的底部。支持Markdown。\"],\"L85WcV\":[\"缩略名\"],\"LdyooL\":[\"链接\"],\"LkA8jz\":[\"添加替代文本\"],\"LkvLQe\":[\"No pages yet.\"],\"M1RvTd\":[\"点击图片查看完整尺寸\"],\"M2kIWU\":[\"字体主题\"],\"M6CbAU\":[\"切换编辑面板\"],\"M8kJqa\":[\"草稿\"],\"M8lheL\":[\"最大可见导航链接\"],\"M9xgHy\":[\"重定向\"],\"MHrjPM\":[\"标题\"],\"MLSRl9\":[\"引用文本\"],\"MWBOxm\":[\"集合(可选)\"],\"MZbQHL\":[\"未找到结果。\"],\"MdMyne\":[\"来源链接(可选)\"],\"Mhf/H/\":[\"创建重定向\"],\"MnbH31\":[\"页面\"],\"MqghUt\":[\"搜索帖子...\"],\"N0APCr\":[\"This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.\"],\"N40H+G\":[\"所有\"],\"NU2Fqi\":[\"保存 CSS\"],\"Naqg3G\":[\"未提供文件\"],\"O3oNi5\":[\"电子邮件\"],\"OCNZaU\":[\"重定向的路径\"],\"ODiSoW\":[\"还没有帖子。\"],\"ONWvwQ\":[\"上传\"],\"OVSkIF\":[\"敏捷的棕色狐狸跳过懒狗。\"],\"OeUWA7\":[\"添加页面\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PJnyHS\":[\"Max visible links saved\"],\"PZ7HJ8\":[\"博客头像\"],\"Pbm2/N\":[\"创建集合\"],\"QEbNBb\":[\"Path (e.g. /archive) or full URL (e.g. https://example.com)\"],\"QLkhbH\":[\"被引用的文本...\"],\"Qjlym2\":[\"创建账户失败\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"链接\"],\"RwGhWy\":[\"包含 \",[\"count\"],\" 条帖子的话题\"],\"RxsRD6\":[\"时区\"],\"SGJDS5\":[\"存储未配置\"],\"SJmfuf\":[\"网站名称\"],\"ST+lN2\":[\"尚未上传任何媒体。\"],\"T0bsor\":[\"设置已成功保存。\"],\"TNFigk\":[\"默认主页视图\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"设置\"],\"U5v6Gh\":[\"编辑页面\"],\"UDMjsP\":[\"快速操作\"],\"UGT5vp\":[\"Save Settings\"],\"UeOiKl\":[\"无效输入\"],\"Ui5/i3\":[\"允许搜索引擎索引我的网站是可以的\"],\"Uj/btJ\":[\"在我的网站头部显示头像\"],\"UxKoFf\":[\"导航\"],\"UzGRD9\":[\"Home view saved\"],\"V4WsyL\":[\"添加链接\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VgozPa\":[\"头像显示设置已成功保存。\"],\"VhMDMg\":[\"更改密码\"],\"Vn3jYy\":[\"导航项\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"常规\"],\"WmZ/rP\":[\"到路径\"],\"XrnWzN\":[\"已发布!\"],\"Y+7JGK\":[\"创建页面\"],\"Y75ho6\":[\"Other pages\"],\"YIix5Y\":[\"搜索...\"],\"Z2lfX1\":[\"选择图标\"],\"Z3FXyt\":[\"加载中...\"],\"Z6NwTi\":[\"保存为草稿\"],\"ZQKLI1\":[\"危险区域\"],\"ZUpE9/\":[\"This is displayed at the bottom of all of your posts and pages. Markdown is supported.\"],\"ZhhOwV\":[\"引用\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"aAIQg2\":[\"外观\"],\"aHTB7P\":[\"附加在您帖子上的补充内容\"],\"aT4jc4\":[\"无效的电子邮件或密码\"],\"aaGV/9\":[\"New Link\"],\"ajBsih\":[\"帖子发布成功。\"],\"alKG0+\":[\"字体主题\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"关于这个博客\"],\"b+/jO6\":[\"301(永久)\"],\"b+FyBD\":[\"Add page to navigation\"],\"b+JhJf\":[\"Max visible links\"],\"b4VwHs\":[\"未提供文件。\"],\"bDqhXY\":[\"删除失败。请再试一次。\"],\"bG4pfW\":[\"Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.\"],\"bHYIks\":[\"登出\"],\"bcE7Mx\":[\"更新个人资料失败。\"],\"biOepV\":[\"← Back to home\"],\"bzSI52\":[\"丢弃\"],\"cTUByn\":[\"最新优先\"],\"ccaIM9\":[\"更多链接\"],\"cnGeoo\":[\"删除\"],\"dEgA5A\":[\"取消\"],\"dStw5E\":[\"将现有页面添加到您的导航中\"],\"dmCcPs\":[\"这用于您的网站图标和苹果触控图标。为了获得最佳效果,请上传至少 180x180 像素的正方形图像。\"],\"dtQNkT\":[\"所选字体主题无效。\"],\"e6Jr7Q\":[\"← 返回收藏夹\"],\"ePK91l\":[\"编辑\"],\"eWLklq\":[\"引用\"],\"eneWvv\":[\"草稿\"],\"er8+x7\":[\"演示账户已预填。只需点击登录。\"],\"f/bxrN\":[\"名称是必需的。\"],\"f6e0Ry\":[\"Article\"],\"fDGOiR\":[\"密码不匹配。\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"我的收藏\"],\"g3mKmM\":[\"Un-nav\"],\"gDx5MG\":[\"Edit Link\"],\"gJH6Bs\":[\"替代文本提高可访问性\"],\"gOwwEy\":[\"存储未配置。\"],\"hDRU5q\":[\"找到 \",[\"0\"],\" 个结果\"],\"hG89Ed\":[\"Image\"],\"hQAbqI\":[\"还没有页面。创建您的第一个页面以开始。\"],\"hWOZIv\":[\"输入您的新密码。\"],\"hXzOVo\":[\"下一页\"],\"he3ygx\":[\"复制\"],\"heSQoS\":[\"粘贴一个网址...\"],\"hmXTCY\":[\"所选主题无效。\"],\"hrL0Be\":[\"图标(可选)\"],\"i0vDGK\":[\"排序顺序\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"网址(可选)\"],\"iEUzMn\":[\"系统\"],\"iEqmSU\":[\"自定义 CSS 保存成功。\"],\"iH8pgl\":[\"返回\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"已发布的页面可以通过其别名访问。草稿不可见。\"],\"jUV7CU\":[\"上传头像\"],\"jVUmOK\":[\"支持Markdown\"],\"jpctdh\":[\"查看\"],\"jt/Ow/\":[\"posts\"],\"jvyYZG\":[\"你在想什么...\"],\"k1ifdL\":[\"处理中...\"],\"kI1qVD\":[\"格式\"],\"kL1h6U\":[\"移除分隔符\"],\"kNiQp6\":[\"已固定\"],\"kPMIr+\":[\"给它一个标题...\"],\"kd7eBB\":[\"Create Link\"],\"kj6ppi\":[\"条目\"],\"kyNTQ2\":[\"Emoji or icon name\"],\"l/6JHD\":[\"搜索图标...\"],\"l6ANt9\":[\"最低评分\"],\"lO1Oow\":[\"上传成功!\"],\"m16xKo\":[\"添加\"],\"mO5HMZ\":[\"All pages are already in navigation.\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"登录\"],\"n6QD94\":[\"最旧的在前\"],\"o21Y+P\":[\"条目\"],\"o4dofa\":[\"AUTH_SECRET 未配置\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oKOOsY\":[\"颜色主题\"],\"oSiRP0\":[\"系统链接\"],\"oYPBa0\":[\"更新页面\"],\"p2/GCq\":[\"确认密码\"],\"pB0OKE\":[\"新分隔符\"],\"pI2MWS\":[\"Search pages…\"],\"pRhYH2\":[\"集合中的帖子 (\",[\"count\"],\")\"],\"pZq3aX\":[\"上传失败。请再试一次。\"],\"pnve/d\":[\"个人资料保存成功。\"],\"q+hNag\":[\"集合\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qdxmd4\":[\"Attached text\"],\"qiXmlF\":[\"添加媒体\"],\"qt89I8\":[\"草稿已保存。\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"颜色主题\"],\"rdUucN\":[\"预览\"],\"rzNUSl\":[\"包含 1 条帖子的话题\"],\"sDGoxy\":[\"切换内置导航项。启用的项会与页面和链接一起出现在您的导航中。\"],\"sGajR7\":[\"线程开始\"],\"smzF8S\":[\"显示 \",[\"remainingCount\"],\" 个更多 \",[\"0\"]],\"ssqvZi\":[\"保存个人资料\"],\"sxkWRg\":[\"高级\"],\"t/YqKh\":[\"移除\"],\"tfDRzk\":[\"保存\"],\"tfQNeI\":[\"No pages found.\"],\"tfrt7B\":[\"未配置重定向。\"],\"tiq7kl\":[\"页面 \",[\"page\"]],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"已发布\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"状态\"],\"vERlcd\":[\"个人资料\"],\"vXIe7J\":[\"语言\"],\"vh0C9b\":[\"No navigation links yet. Add pages to navigation or create links.\"],\"vmQmHx\":[\"添加自定义 CSS 以覆盖任何样式。使用数据属性,如 [data-page]、[data-post]、[data-format] 来定位特定元素。\"],\"vzU4k9\":[\"新收藏\"],\"w8Rv8T\":[\"标签是必需的\"],\"wEF6Ix\":[\"目标路径或 URL\"],\"wK4OTM\":[\"标题(可选)\"],\"wL3cK8\":[\"最新\"],\"wM5UXj\":[\"删除媒体\"],\"wRR604\":[\"页面\"],\"wc+17X\":[\"/* 您的自定义 CSS 在这里 */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"无标题\"],\"x+doid\":[\"图像会自动优化:调整大小至最大 1920px,转换为 WebP,并去除元数据。\"],\"x0mzE0\":[\"创建你的第一篇帖子\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"媒体\"],\"y0R9F0\":[\"帖子更新成功。\"],\"y28hnO\":[\"帖子\"],\"yQ2kGp\":[\"加载更多\"],\"yjkELF\":[\"确认新密码\"],\"yz7wBu\":[\"关闭\"],\"yzF66j\":[\"链接\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"找到 1 个结果\"],\"zBFr9G\":[\"粘贴一篇长文章、AI 响应或任何文本...\\n\\nMarkdown 格式将被保留。\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"],\"zennIg\":[\"URL安全标识符(小写字母、数字、连字符)。对于CJK标题,slug将在服务器上自动生成。\"],\"zucql+\":[\"菜单\"]}");
3998
4458
 
3999
- /*eslint-disable*/ const messages = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+AXdXp\":[\"標籤和網址是必填的\"],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"加入導航\"],\"+bHzpy\":[\"顯示連結的文字\"],\"+owNNn\":[\"文章\"],\"+zy2Nq\":[\"類型\"],\"/0D1Xp\":[\"編輯收藏集\"],\"/DFKdU\":[\"輸入引用...\"],\"/R/sGB\":[\"密碼已成功更改。\"],\"/Rj5P4\":[\"您的姓名\"],\"/rkqRV\":[\"所有頁面都在您的導航中。\"],\"07Epll\":[\"這將為您的網站和儀表板設置主題。所有顏色主題都支持深色模式。\"],\"0JkyS7\":[\"Create your first page\"],\"0a6MpL\":[\"新重定向\"],\"0ieXE7\":[\"最高評價\"],\"0yIy82\":[\"尚未有精選文章。\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"筆記\"],\"1Oj1sI\":[\"已保存順序\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"2N0qpv\":[\"文章標題...\"],\"2cFU6q\":[\"網站頁腳\"],\"2fUwEY\":[\"選擇媒體\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"頁面標題...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Siwmw\":[\"More options\"],\"3Yvsaz\":[\"302(臨時)\"],\"4/SFQS\":[\"查看網站\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4KzVT6\":[\"刪除頁面\"],\"4Ml90q\":[\"SEO\"],\"4b3oEV\":[\"內容\"],\"4mDPGp\":[\"此頁面的 URL 路徑。使用小寫字母、數字和連字符。\"],\"6C8dEg\":[\"附加文本\"],\"6WdDG7\":[\"頁面\"],\"6YtxFj\":[\"名稱\"],\"6tU2jr\":[\"No collections found.\"],\"71Xwww\":[\"無效的請求\"],\"7G4SBz\":[\"頁面內容(支持Markdown)...\"],\"7Mk+/h\":[\"更新收藏集\"],\"7Q1KKN\":[\"來源路徑\"],\"7aECQB\":[\"無效或已過期的連結\"],\"7nGhhM\":[\"你在想什麼?\"],\"7p5kLi\":[\"儀表板\"],\"7vhWI8\":[\"新密碼\"],\"87a/t/\":[\"標籤\"],\"8HgKQc\":[\"SEO 設定已成功儲存。\"],\"8WX0J+\":[\"您的想法(可選)\"],\"8WtVZw\":[\"無法保存帖子。請再試一次。\"],\"8ZsakT\":[\"密碼\"],\"8qX8Jl\":[\"選擇一個字體搭配以供您的網站使用。所有選項均使用系統字體以加快加載速度。\"],\"8tM8+a\":[\"儲存為草稿\"],\"9+vGLh\":[\"自訂 CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AyHO4m\":[\"這個收藏是關於什麼的?\"],\"B373X+\":[\"編輯文章\"],\"B495Gs\":[\"檔案館\"],\"BjF0Jv\":[\"僅限小寫字母、數字和連字符\"],\"Cl55aD\":[\"當前密碼不正確。\"],\"D3uuEX\":[\"尚未選擇任何媒體。\"],\"D9Oea+\":[\"永久鏈接\"],\"DCKkhU\":[\"當前密碼\"],\"DHhJ7s\":[\"上一頁\"],\"DPfwMq\":[\"完成\"],\"DoJzLz\":[\"收藏夾\"],\"E80cJw\":[\"刪除此媒體將永久從存儲中移除它。\"],\"EEYbdt\":[\"發佈\"],\"EGwzOK\":[\"完成設置\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"更新\"],\"Eq6YVV\":[\"分數\"],\"FESYvt\":[\"Describe this for people with visual impairments...\"],\"FGrimz\":[\"新帖子\"],\"FkMol5\":[\"精選\"],\"Fxf4jq\":[\"描述(可選)\"],\"GA5A5H\":[\"Delete Collection\"],\"GBDTf5\":[\"身份驗證未配置\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"您的網站導航\"],\"GX2VMa\":[\"建立您的管理員帳戶。\"],\"GbVAnd\":[\"此密碼重設連結無效或已過期。請生成一個新的連結。\"],\"GorKul\":[\"歡迎來到 Jant\"],\"GrZ6fH\":[\"新頁面\"],\"GxkJXS\":[\"上傳中...\"],\"H29JXm\":[\"+ ALT\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"未找到任何帖子。\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"保存更改\"],\"IagCbF\":[\"網址\"],\"J4FNfC\":[\"此集合中沒有帖子。\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"需要幫助嗎?請訪問<0>文檔</0>。\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"使用此 URL 將媒體嵌入到您的帖子中。\"],\"KbS2K9\":[\"重設密碼\"],\"KdSsVl\":[\"作者(可選)\"],\"KiJn9B\":[\"備註\"],\"KmGXnO\":[\"您確定要刪除這篇文章嗎?這個操作無法撤銷。\"],\"KsIZ3c\":[\"頁腳已成功保存。\"],\"KuCcWu\":[\"Displayed at the bottom of all posts and pages. Markdown supported.\"],\"L85WcV\":[\"縮略名\"],\"LdyooL\":[\"鏈接\"],\"LkA8jz\":[\"Add alt text\"],\"LkvLQe\":[\"No pages yet.\"],\"M1RvTd\":[\"點擊圖片以查看完整大小\"],\"M2kIWU\":[\"字型主題\"],\"M8kJqa\":[\"草稿\"],\"M9xgHy\":[\"重定向\"],\"MHrjPM\":[\"標題\"],\"MLSRl9\":[\"引用文本\"],\"MWBOxm\":[\"收藏(可選)\"],\"MZbQHL\":[\"未找到結果。\"],\"MdMyne\":[\"來源連結(選填)\"],\"Mhf/H/\":[\"建立重定向\"],\"MnbH31\":[\"頁面\"],\"MqghUt\":[\"搜尋帖子...\"],\"N0APCr\":[\"這會顯示在您預設首頁的部落格文章上方。這也用於您首頁的 meta 描述。\"],\"N40H+G\":[\"所有\"],\"NU2Fqi\":[\"儲存 CSS\"],\"Naqg3G\":[\"未提供檔案\"],\"O3oNi5\":[\"電子郵件\"],\"OCNZaU\":[\"重定向來源的路徑\"],\"ODiSoW\":[\"尚未有帖子。\"],\"ONWvwQ\":[\"上傳\"],\"OVSkIF\":[\"敏捷的棕色狐狸跳過懶惰的狗。\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PZ7HJ8\":[\"部落格頭像\"],\"Pbm2/N\":[\"創建收藏夾\"],\"QEbNBb\":[\"路徑(例如 /archive)或完整網址(例如 https://example.com)\"],\"QLkhbH\":[\"被引用的文本...\"],\"Qjlym2\":[\"無法創建帳戶\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"連結\"],\"RwGhWy\":[\"包含 \",[\"count\"],\" 則帖子的主題\"],\"RxsRD6\":[\"時區\"],\"SGJDS5\":[\"儲存未配置\"],\"SJmfuf\":[\"網站名稱\"],\"ST+lN2\":[\"尚未上傳任何媒體。\"],\"T0bsor\":[\"設置已成功保存。\"],\"TNFigk\":[\"預設首頁視圖\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"設定\"],\"U5v6Gh\":[\"編輯頁面\"],\"UDMjsP\":[\"快速操作\"],\"UGT5vp\":[\"Save Settings\"],\"UeOiKl\":[\"無效的輸入\"],\"Ui5/i3\":[\"允許搜尋引擎索引我的網站是可以的\"],\"Uj/btJ\":[\"在我的網站標頭中顯示頭像\"],\"UxKoFf\":[\"Navigation\"],\"V4WsyL\":[\"新增連結\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VgozPa\":[\"頭像顯示設置已成功保存。\"],\"VhMDMg\":[\"更改密碼\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"一般設定\"],\"WmZ/rP\":[\"到路徑\"],\"XrnWzN\":[\"Published!\"],\"Y+7JGK\":[\"創建頁面\"],\"Y75ho6\":[\"其他頁面\"],\"YIix5Y\":[\"Search...\"],\"Z2lfX1\":[\"選擇圖示\"],\"Z3FXyt\":[\"載入中...\"],\"Z6NwTi\":[\"儲存為草稿\"],\"ZQKLI1\":[\"危險區域\"],\"ZUpE9/\":[\"這會顯示在您所有的文章和頁面的底部。支持 Markdown。\"],\"ZhhOwV\":[\"引用\"],\"aAIQg2\":[\"外觀\"],\"aHTB7P\":[\"附加在您帖子上的補充內容\"],\"aT4jc4\":[\"無效的電子郵件或密碼\"],\"aaGV/9\":[\"新連結\"],\"ajBsih\":[\"發佈文章成功。\"],\"alKG0+\":[\"Font Theme\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"關於這個部落格\"],\"b+/jO6\":[\"301(永久)\"],\"b4VwHs\":[\"未提供檔案。\"],\"bG4pfW\":[\"Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.\"],\"bHYIks\":[\"登出\"],\"bcE7Mx\":[\"無法更新個人資料。\"],\"biOepV\":[\"← Back to home\"],\"bzSI52\":[\"丟棄\"],\"cTUByn\":[\"最新的在前\"],\"cnGeoo\":[\"刪除\"],\"dEgA5A\":[\"取消\"],\"dmCcPs\":[\"這是用於您的網站圖標和蘋果觸控圖標。為了獲得最佳效果,請上傳至少 180x180 像素的正方形圖片。\"],\"dtQNkT\":[\"選擇的字體主題無效。\"],\"e6Jr7Q\":[\"← 返回收藏夾\"],\"ePK91l\":[\"編輯\"],\"eWLklq\":[\"引用\"],\"eneWvv\":[\"草稿\"],\"er8+x7\":[\"示範帳戶已預填。只需點擊登入。\"],\"f/bxrN\":[\"名稱是必填的。\"],\"f6e0Ry\":[\"Article\"],\"fDGOiR\":[\"密碼不匹配。\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"我的收藏\"],\"g3mKmM\":[\"取消導航\"],\"gDx5MG\":[\"編輯連結\"],\"gJH6Bs\":[\"Alt text improves accessibility\"],\"gOwwEy\":[\"儲存空間未配置。\"],\"hDRU5q\":[\"找到 \",[\"0\"],\" 個結果\"],\"hG89Ed\":[\"Image\"],\"hWOZIv\":[\"請輸入您的新密碼。\"],\"hXzOVo\":[\"下一頁\"],\"he3ygx\":[\"複製\"],\"heSQoS\":[\"粘貼一個網址...\"],\"hmXTCY\":[\"選擇的主題無效。\"],\"hrL0Be\":[\"圖示(可選)\"],\"i0vDGK\":[\"排序順序\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"網址(可選)\"],\"iEqmSU\":[\"自訂 CSS 已成功儲存。\"],\"iH8pgl\":[\"返回\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"已發佈的頁面可以通過其標識符訪問。草稿不可見。\"],\"jUV7CU\":[\"上傳頭像\"],\"jVUmOK\":[\"支援Markdown\"],\"jpctdh\":[\"查看\"],\"jt/Ow/\":[\"posts\"],\"jvyYZG\":[\"你在想什麼...\"],\"k1ifdL\":[\"處理中...\"],\"kI1qVD\":[\"格式\"],\"kL1h6U\":[\"移除分隔線\"],\"kNiQp6\":[\"置頂\"],\"kPMIr+\":[\"給它一個標題...\"],\"kd7eBB\":[\"建立連結\"],\"kj6ppi\":[\"條目\"],\"kyNTQ2\":[\"Emoji or icon name\"],\"l/6JHD\":[\"搜尋圖示...\"],\"l6ANt9\":[\"最低評價\"],\"lO1Oow\":[\"上傳成功!\"],\"m16xKo\":[\"Add\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"登入\"],\"n6QD94\":[\"最舊的在前\"],\"o21Y+P\":[\"條目\"],\"o4dofa\":[\"AUTH_SECRET 未配置\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oKOOsY\":[\"Color Theme\"],\"oYPBa0\":[\"更新頁面\"],\"p2/GCq\":[\"確認密碼\"],\"pB0OKE\":[\"新分隔線\"],\"pRhYH2\":[\"收藏中的帖子 (\",[\"count\"],\")\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pnve/d\":[\"個人資料已成功儲存。\"],\"q+hNag\":[\"集合\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qdxmd4\":[\"Attached text\"],\"qiXmlF\":[\"添加媒體\"],\"qt89I8\":[\"草稿已保存。\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"顏色主題\"],\"rdUucN\":[\"預覽\"],\"rzNUSl\":[\"包含 1 則貼文的主題\"],\"sGajR7\":[\"線程開始\"],\"smzF8S\":[\"顯示 \",[\"remainingCount\"],\" 個更多 \",[\"0\"]],\"ssqvZi\":[\"保存個人資料\"],\"sxkWRg\":[\"Advanced\"],\"t/YqKh\":[\"移除\"],\"tfDRzk\":[\"保存\"],\"tfrt7B\":[\"未配置任何重定向。\"],\"tiq7kl\":[\"頁面 \",[\"page\"]],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"已發佈\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"狀態\"],\"vERlcd\":[\"個人資料\"],\"vXIe7J\":[\"語言\"],\"vh0C9b\":[\"尚未有導航連結。請將頁面添加到導航或創建連結。\"],\"vmQmHx\":[\"添加自定義 CSS 以覆蓋任何樣式。使用數據屬性,如 [data-page]、[data-post]、[data-format] 來針對特定元素。\"],\"vzU4k9\":[\"新收藏集\"],\"wEF6Ix\":[\"目的地路徑或 URL\"],\"wK4OTM\":[\"標題(選填)\"],\"wL3cK8\":[\"最新\"],\"wM5UXj\":[\"刪除媒體\"],\"wRR604\":[\"頁面\"],\"wc+17X\":[\"/* 您的自訂 CSS 在這裡 */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"無標題\"],\"x+doid\":[\"圖片會自動優化:調整大小至最大 1920 像素,轉換為 WebP 格式,並去除元數據。\"],\"x0mzE0\":[\"創建你的第一篇帖子\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"媒體\"],\"y0R9F0\":[\"帖子已成功更新。\"],\"y28hnO\":[\"文章\"],\"yQ2kGp\":[\"載入更多\"],\"yjkELF\":[\"確認新密碼\"],\"yz7wBu\":[\"關閉\"],\"yzF66j\":[\"連結\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"找到 1 個結果\"],\"zBFr9G\":[\"粘貼一篇長文章、AI 回應或任何文本...\\n\\nMarkdown 格式將被保留。\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"],\"zennIg\":[\"URL安全識別碼(小寫、數字、連字符)。對於CJK標題,slug將在伺服器上自動生成。\"]}");
4459
+ /*eslint-disable*/ const messages = JSON.parse("{\"+7Wr2a\":[\"Edit: \",[\"title\"]],\"+AXdXp\":[\"標籤和網址是必填的\"],\"+MACwa\":[\"尚未有任何收藏。\"],\"+MH6k9\":[\"Add to nav\"],\"+bHzpy\":[\"Display text for the link\"],\"+owNNn\":[\"文章\"],\"+zy2Nq\":[\"類型\"],\"/0D1Xp\":[\"編輯收藏集\"],\"/DFKdU\":[\"輸入引用...\"],\"/R/sGB\":[\"密碼已成功更改。\"],\"/Rj5P4\":[\"您的姓名\"],\"/rkqRV\":[\"All pages are in your navigation.\"],\"07Epll\":[\"這將為您的網站和儀表板設置主題。所有顏色主題都支持深色模式。\"],\"0JkyS7\":[\"Create your first page\"],\"0a6MpL\":[\"新重定向\"],\"0ieXE7\":[\"最高評價\"],\"0yIy82\":[\"尚未有精選文章。\"],\"1CU1Td\":[\"URL-safe identifier (lowercase, numbers, hyphens)\"],\"1DBGsz\":[\"筆記\"],\"1Oj1sI\":[\"已保存順序\"],\"1o+wgo\":[\"e.g. The Verge, John Doe\"],\"2N0qpv\":[\"文章標題...\"],\"2cFU6q\":[\"網站頁腳\"],\"2fUwEY\":[\"選擇媒體\"],\"2q/Q7x\":[\"Visibility\"],\"2rJGtU\":[\"頁面標題...\"],\"3SAro+\":[\"Choose a font for your site. All options use system fonts for fast loading.\"],\"3Siwmw\":[\"More options\"],\"3Yvsaz\":[\"302(臨時)\"],\"3uSoGn\":[\"Header Nav Links\"],\"4/SFQS\":[\"查看網站\"],\"40TVQj\":[\"Custom Path (optional)\"],\"4JBD+x\":[\"保存失敗。請再試一次。\"],\"4KzVT6\":[\"刪除頁面\"],\"4Ml90q\":[\"SEO\"],\"4b3oEV\":[\"內容\"],\"4mDPGp\":[\"此頁面的 URL 路徑。使用小寫字母、數字和連字符。\"],\"538Vy5\":[\"尚未有導航項目。請在下方添加頁面、鏈接或啟用系統項目。\"],\"6C8dEg\":[\"附加文本\"],\"6WdDG7\":[\"頁面\"],\"6YtxFj\":[\"名稱\"],\"6tU2jr\":[\"找不到任何收藏。\"],\"71Xwww\":[\"Invalid request\"],\"7G4SBz\":[\"頁面內容(支持Markdown)...\"],\"7Mk+/h\":[\"更新收藏集\"],\"7Q1KKN\":[\"來源路徑\"],\"7aECQB\":[\"無效或已過期的連結\"],\"7aYVPs\":[\"所有頁面都在導航中\"],\"7nGhhM\":[\"你在想什麼?\"],\"7p5kLi\":[\"儀表板\"],\"7vhWI8\":[\"新密碼\"],\"87a/t/\":[\"標籤\"],\"8HgKQc\":[\"SEO 設定已成功儲存。\"],\"8WX0J+\":[\"您的想法(可選)\"],\"8WtVZw\":[\"無法保存帖子。請再試一次。\"],\"8ZsakT\":[\"密碼\"],\"8qX8Jl\":[\"選擇一個字體搭配以供您的網站使用。所有選項均使用系統字體以加快加載速度。\"],\"8tM8+a\":[\"儲存為草稿\"],\"8xE385\":[\"添加到導航\"],\"9+vGLh\":[\"自訂 CSS\"],\"90Luob\":[[\"count\"],\" replies\"],\"A1taO8\":[\"搜尋\"],\"AeXO77\":[\"帳戶\"],\"AyHO4m\":[\"這個收藏是關於什麼的?\"],\"Az4JB1\":[\"Use Featured as default home view\"],\"B373X+\":[\"編輯文章\"],\"B495Gs\":[\"檔案館\"],\"BjF0Jv\":[\"僅限小寫字母、數字和連字符\"],\"Cl55aD\":[\"當前密碼不正確。\"],\"D3uuEX\":[\"尚未選擇任何媒體。\"],\"D9Oea+\":[\"永久鏈接\"],\"DCKkhU\":[\"當前密碼\"],\"DHhJ7s\":[\"上一頁\"],\"DPfwMq\":[\"完成\"],\"DVljCN\":[\"Choose a page…\"],\"DoJzLz\":[\"收藏夾\"],\"E80cJw\":[\"刪除此媒體將永久從存儲中移除它。\"],\"EEYbdt\":[\"發佈\"],\"EGwzOK\":[\"完成設置\"],\"EdQY6l\":[\"None\"],\"EkH9pt\":[\"更新\"],\"Eq6YVV\":[\"分數\"],\"FESYvt\":[\"為視障人士描述這個...\"],\"FGrimz\":[\"新帖子\"],\"FkMol5\":[\"精選\"],\"Fxf4jq\":[\"描述(可選)\"],\"GA5A5H\":[\"Delete Collection\"],\"GBDTf5\":[\"身份驗證未配置\"],\"GHg6h/\":[\"post\"],\"GTPbOX\":[\"Your site navigation\"],\"GX2VMa\":[\"建立您的管理員帳戶。\"],\"GbVAnd\":[\"此密碼重設連結無效或已過期。請生成一個新的連結。\"],\"GlEzsR\":[\"為搜尋引擎和訂閱閱讀器提供的簡短介紹。僅限純文字。\"],\"GorKul\":[\"歡迎來到 Jant\"],\"GrZ6fH\":[\"新頁面\"],\"GxkJXS\":[\"上傳中...\"],\"H29JXm\":[\"+ ALT\"],\"HfyyXl\":[\"My Blog\"],\"HiETwV\":[\"Quiet (normal)\"],\"Hzi9AA\":[\"未找到任何帖子。\"],\"I6gXOa\":[\"Path\"],\"I8hDlV\":[\"At least 1 image required for image posts.\"],\"IUwGEM\":[\"Save Changes\"],\"IagCbF\":[\"網址\"],\"J4FNfC\":[\"此集合中沒有帖子。\"],\"J6bLeg\":[\"添加自定義連結到任何 URL\"],\"JIBC/T\":[\"Supported formats: JPEG, PNG, GIF, WebP, SVG. Max size: 10MB.\"],\"Jed1wB\":[\"需要幫助嗎?請訪問<0>文檔</0>。\"],\"JiP4aa\":[\"Published pages are accessible via their path. Drafts are not visible.\"],\"K0r7TC\":[\"What's new?\"],\"K9NcLu\":[\"使用此 URL 將媒體嵌入到您的帖子中。\"],\"KbS2K9\":[\"重設密碼\"],\"KdSsVl\":[\"作者(可選)\"],\"KiJn9B\":[\"備註\"],\"KmGXnO\":[\"您確定要刪除這篇文章嗎?這個操作無法撤銷。\"],\"KsIZ3c\":[\"Footer saved successfully.\"],\"KuCcWu\":[\"顯示在所有文章和頁面的底部。支持Markdown。\"],\"L85WcV\":[\"縮略名\"],\"LdyooL\":[\"鏈接\"],\"LkA8jz\":[\"添加替代文字\"],\"LkvLQe\":[\"No pages yet.\"],\"M1RvTd\":[\"點擊圖片以查看完整大小\"],\"M2kIWU\":[\"字型主題\"],\"M6CbAU\":[\"切換編輯面板\"],\"M8kJqa\":[\"草稿\"],\"M8lheL\":[\"最大可見導航連結\"],\"M9xgHy\":[\"重定向\"],\"MHrjPM\":[\"標題\"],\"MLSRl9\":[\"引用文本\"],\"MWBOxm\":[\"收藏(可選)\"],\"MZbQHL\":[\"未找到結果。\"],\"MdMyne\":[\"來源連結(選填)\"],\"Mhf/H/\":[\"建立重定向\"],\"MnbH31\":[\"頁面\"],\"MqghUt\":[\"搜尋帖子...\"],\"N0APCr\":[\"This is displayed above your blog posts on your default home page. This is also used for the meta description on your home page.\"],\"N40H+G\":[\"所有\"],\"NU2Fqi\":[\"儲存 CSS\"],\"Naqg3G\":[\"未提供檔案\"],\"O3oNi5\":[\"電子郵件\"],\"OCNZaU\":[\"重定向來源的路徑\"],\"ODiSoW\":[\"尚未有帖子。\"],\"ONWvwQ\":[\"上傳\"],\"OVSkIF\":[\"敏捷的棕色狐狸跳過懶惰的狗。\"],\"OeUWA7\":[\"新增頁面\"],\"P/XNX0\":[\"This is used for your favicon.\"],\"PJnyHS\":[\"Max visible links saved\"],\"PZ7HJ8\":[\"部落格頭像\"],\"Pbm2/N\":[\"創建收藏夾\"],\"QEbNBb\":[\"Path (e.g. /archive) or full URL (e.g. https://example.com)\"],\"QLkhbH\":[\"被引用的文本...\"],\"Qjlym2\":[\"無法創建帳戶\"],\"RDjuBN\":[\"Setup\"],\"Rj01Fz\":[\"連結\"],\"RwGhWy\":[\"包含 \",[\"count\"],\" 則帖子的主題\"],\"RxsRD6\":[\"時區\"],\"SGJDS5\":[\"儲存未配置\"],\"SJmfuf\":[\"網站名稱\"],\"ST+lN2\":[\"尚未上傳任何媒體。\"],\"T0bsor\":[\"設置已成功保存。\"],\"TNFigk\":[\"預設首頁視圖\"],\"Tt5T6+\":[\"Articles\"],\"TxE+Mj\":[\"1 reply\"],\"Tz0i8g\":[\"設定\"],\"U5v6Gh\":[\"編輯頁面\"],\"UDMjsP\":[\"快速操作\"],\"UGT5vp\":[\"Save Settings\"],\"UeOiKl\":[\"無效的輸入\"],\"Ui5/i3\":[\"允許搜尋引擎索引我的網站是可以的\"],\"Uj/btJ\":[\"在我的網站標頭中顯示頭像\"],\"UxKoFf\":[\"導航\"],\"UzGRD9\":[\"Home view saved\"],\"V4WsyL\":[\"新增連結\"],\"VUSy8D\":[\"Search failed. Please try again.\"],\"VgozPa\":[\"頭像顯示設置已成功保存。\"],\"VhMDMg\":[\"更改密碼\"],\"Vn3jYy\":[\"導航項目\"],\"WDcQq9\":[\"Unlisted\"],\"Weq9zb\":[\"一般設定\"],\"WmZ/rP\":[\"到路徑\"],\"XrnWzN\":[\"已發佈!\"],\"Y+7JGK\":[\"創建頁面\"],\"Y75ho6\":[\"Other pages\"],\"YIix5Y\":[\"搜尋...\"],\"Z2lfX1\":[\"選擇圖示\"],\"Z3FXyt\":[\"載入中...\"],\"Z6NwTi\":[\"儲存為草稿\"],\"ZQKLI1\":[\"危險區域\"],\"ZUpE9/\":[\"This is displayed at the bottom of all of your posts and pages. Markdown is supported.\"],\"ZhhOwV\":[\"引用\"],\"ZmUkwN\":[\"Add custom link to navigation\"],\"aAIQg2\":[\"外觀\"],\"aHTB7P\":[\"附加在您帖子上的補充內容\"],\"aT4jc4\":[\"無效的電子郵件或密碼\"],\"aaGV/9\":[\"New Link\"],\"ajBsih\":[\"發佈文章成功。\"],\"alKG0+\":[\"字型主題\"],\"an5hVd\":[\"Images\"],\"anibOb\":[\"關於這個部落格\"],\"b+/jO6\":[\"301(永久)\"],\"b+FyBD\":[\"Add page to navigation\"],\"b+JhJf\":[\"Max visible links\"],\"b4VwHs\":[\"未提供檔案。\"],\"bDqhXY\":[\"刪除失敗。請再試一次。\"],\"bG4pfW\":[\"Displayed above your blog posts on the home page. Also used as the meta description. Markdown supported.\"],\"bHYIks\":[\"登出\"],\"bcE7Mx\":[\"無法更新個人資料。\"],\"biOepV\":[\"← Back to home\"],\"bzSI52\":[\"丟棄\"],\"cTUByn\":[\"最新的在前\"],\"ccaIM9\":[\"更多連結\"],\"cnGeoo\":[\"刪除\"],\"dEgA5A\":[\"取消\"],\"dStw5E\":[\"將現有頁面添加到您的導航中\"],\"dmCcPs\":[\"這是用於您的網站圖標和蘋果觸控圖標。為了獲得最佳效果,請上傳至少 180x180 像素的正方形圖片。\"],\"dtQNkT\":[\"選擇的字體主題無效。\"],\"e6Jr7Q\":[\"← 返回收藏夾\"],\"ePK91l\":[\"編輯\"],\"eWLklq\":[\"引用\"],\"eneWvv\":[\"草稿\"],\"er8+x7\":[\"示範帳戶已預填。只需點擊登入。\"],\"f/bxrN\":[\"名稱是必填的。\"],\"f6e0Ry\":[\"Article\"],\"fDGOiR\":[\"密碼不匹配。\"],\"fG7BxZ\":[\"Upload images via the API: POST /api/upload with a file form field.\"],\"fttd2R\":[\"我的收藏\"],\"g3mKmM\":[\"Un-nav\"],\"gDx5MG\":[\"Edit Link\"],\"gJH6Bs\":[\"替代文字改善可及性\"],\"gOwwEy\":[\"儲存空間未配置。\"],\"hDRU5q\":[\"找到 \",[\"0\"],\" 個結果\"],\"hG89Ed\":[\"Image\"],\"hQAbqI\":[\"尚未有頁面。創建您的第一個頁面以開始使用。\"],\"hWOZIv\":[\"請輸入您的新密碼。\"],\"hXzOVo\":[\"下一頁\"],\"he3ygx\":[\"複製\"],\"heSQoS\":[\"粘貼一個網址...\"],\"hmXTCY\":[\"選擇的主題無效。\"],\"hrL0Be\":[\"圖示(可選)\"],\"i0vDGK\":[\"排序順序\"],\"iBc+/N\":[\"Custom URL path. Leave empty to use default /p/ID format.\"],\"iDAqU6\":[\"網址(可選)\"],\"iEUzMn\":[\"系統\"],\"iEqmSU\":[\"自訂 CSS 已成功儲存。\"],\"iH8pgl\":[\"返回\"],\"iPHeYN\":[\"Posting...\"],\"ig4hg2\":[\"Let's set up your site.\"],\"jSRrXo\":[\"已發佈的頁面可以通過其標識符訪問。草稿不可見。\"],\"jUV7CU\":[\"上傳頭像\"],\"jVUmOK\":[\"支援Markdown\"],\"jpctdh\":[\"查看\"],\"jt/Ow/\":[\"posts\"],\"jvyYZG\":[\"你在想什麼...\"],\"k1ifdL\":[\"處理中...\"],\"kI1qVD\":[\"格式\"],\"kL1h6U\":[\"移除分隔線\"],\"kNiQp6\":[\"置頂\"],\"kPMIr+\":[\"給它一個標題...\"],\"kd7eBB\":[\"Create Link\"],\"kj6ppi\":[\"條目\"],\"kyNTQ2\":[\"Emoji or icon name\"],\"l/6JHD\":[\"搜尋圖示...\"],\"l6ANt9\":[\"最低評價\"],\"lO1Oow\":[\"上傳成功!\"],\"m16xKo\":[\"新增\"],\"mO5HMZ\":[\"All pages are already in navigation.\"],\"mTOYla\":[\"View all posts →\"],\"mnkknn\":[\"Collection (optional)\"],\"n1ekoW\":[\"登入\"],\"n6QD94\":[\"最舊的在前\"],\"o21Y+P\":[\"條目\"],\"o4dofa\":[\"AUTH_SECRET 未配置\"],\"oJFOZk\":[\"Source Name (optional)\"],\"oKOOsY\":[\"顏色主題\"],\"oSiRP0\":[\"系統連結\"],\"oYPBa0\":[\"更新頁面\"],\"p2/GCq\":[\"確認密碼\"],\"pB0OKE\":[\"新分隔線\"],\"pI2MWS\":[\"Search pages…\"],\"pRhYH2\":[\"收藏中的帖子 (\",[\"count\"],\")\"],\"pZq3aX\":[\"上傳失敗。請再試一次。\"],\"pnve/d\":[\"個人資料已成功儲存。\"],\"q+hNag\":[\"集合\"],\"qMyM2u\":[\"Source URL (optional)\"],\"qdxmd4\":[\"Attached text\"],\"qiXmlF\":[\"添加媒體\"],\"qt89I8\":[\"草稿已保存。\"],\"quFPTj\":[\"Custom Slug (optional)\"],\"r1MpXi\":[\"Quiet\"],\"rFmBG3\":[\"顏色主題\"],\"rdUucN\":[\"預覽\"],\"rzNUSl\":[\"包含 1 則貼文的主題\"],\"sDGoxy\":[\"切換內建導航項目。啟用的項目會與頁面和連結一起顯示在您的導航中。\"],\"sGajR7\":[\"線程開始\"],\"smzF8S\":[\"顯示 \",[\"remainingCount\"],\" 個更多 \",[\"0\"]],\"ssqvZi\":[\"保存個人資料\"],\"sxkWRg\":[\"進階\"],\"t/YqKh\":[\"移除\"],\"tfDRzk\":[\"保存\"],\"tfQNeI\":[\"No pages found.\"],\"tfrt7B\":[\"未配置任何重定向。\"],\"tiq7kl\":[\"頁面 \",[\"page\"]],\"u2f7vd\":[\"Site Description\"],\"u3wRF+\":[\"已發佈\"],\"u6Hp4N\":[\"Markdown\"],\"uAQUqI\":[\"狀態\"],\"vERlcd\":[\"個人資料\"],\"vXIe7J\":[\"語言\"],\"vh0C9b\":[\"No navigation links yet. Add pages to navigation or create links.\"],\"vmQmHx\":[\"添加自定義 CSS 以覆蓋任何樣式。使用數據屬性,如 [data-page]、[data-post]、[data-format] 來針對特定元素。\"],\"vzU4k9\":[\"新收藏集\"],\"w8Rv8T\":[\"標籤是必需的\"],\"wEF6Ix\":[\"目的地路徑或 URL\"],\"wK4OTM\":[\"標題(選填)\"],\"wL3cK8\":[\"最新\"],\"wM5UXj\":[\"刪除媒體\"],\"wRR604\":[\"頁面\"],\"wc+17X\":[\"/* 您的自訂 CSS 在這裡 */\"],\"wdGjkd\":[\"No navigation links configured.\"],\"wja8aL\":[\"無標題\"],\"x+doid\":[\"圖片會自動優化:調整大小至最大 1920 像素,轉換為 WebP 格式,並去除元數據。\"],\"x0mzE0\":[\"創建你的第一篇帖子\"],\"x4RuFo\":[\"Back to home\"],\"xYilR2\":[\"媒體\"],\"y0R9F0\":[\"帖子已成功更新。\"],\"y28hnO\":[\"文章\"],\"yQ2kGp\":[\"載入更多\"],\"yjkELF\":[\"確認新密碼\"],\"yz7wBu\":[\"關閉\"],\"yzF66j\":[\"連結\"],\"z1U/Fh\":[\"Rating\"],\"z8ajIE\":[\"找到 1 個結果\"],\"zBFr9G\":[\"粘貼一篇長文章、AI 回應或任何文本...\\n\\nMarkdown 格式將被保留。\"],\"zH6KqE\":[\"Found \",[\"count\"],\" results\"],\"zennIg\":[\"URL安全識別碼(小寫、數字、連字符)。對於CJK標題,slug將在伺服器上自動生成。\"],\"zucql+\":[\"菜單\"]}");
4000
4460
 
4001
4461
  // Pre-compute merged catalogs at module load time (done once, not per request)
4002
4462
  // For non-English locales, merge English as fallback so missing translations
@@ -4692,7 +5152,7 @@ const I18nProvider = ({ c, children })=>{
4692
5152
  }
4693
5153
 
4694
5154
  const IS_VITE_DEV = typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
4695
- const CORE_VERSION = "0.3.32";
5155
+ const CORE_VERSION = "0.3.34";
4696
5156
 
4697
5157
  const BaseLayout = ({ title, description, lang, c, toast, faviconUrl, faviconVersion, noindex, isAuthenticated = false, children })=>{
4698
5158
  // Read lang from Hono context if available, otherwise use prop or default
@@ -5061,7 +5521,7 @@ const BaseLayout = ({ title, description, lang, c, toast, faviconUrl, faviconVer
5061
5521
  /**
5062
5522
  * Redirect type enum schema
5063
5523
  * Form input validation for redirect type (stored as number in DB)
5064
- */ z.enum([
5524
+ */ const RedirectTypeSchema = z.enum([
5065
5525
  "301",
5066
5526
  "302"
5067
5527
  ]);
@@ -5131,7 +5591,7 @@ const BaseLayout = ({ title, description, lang, c, toast, faviconUrl, faviconVer
5131
5591
  });
5132
5592
  /**
5133
5593
  * API request body schema for updating a collection
5134
- */ CreateCollectionSchema.partial();
5594
+ */ const UpdateCollectionSchema$1 = CreateCollectionSchema.partial();
5135
5595
  // =============================================================================
5136
5596
  // Auth Schemas
5137
5597
  // =============================================================================
@@ -5186,18 +5646,6 @@ const BaseLayout = ({ title, description, lang, c, toast, faviconUrl, faviconVer
5186
5646
  */ const ReorderSchema = z.object({
5187
5647
  ids: z.array(z.coerce.number().int().positive())
5188
5648
  });
5189
- /**
5190
- * Validates media attachment count for a post.
5191
- * All formats allow 0-20 media attachments.
5192
- *
5193
- * @param mediaIds - Array of media IDs to attach
5194
- * @returns null if valid, error string if invalid
5195
- */ function validateMediaCount(mediaIds) {
5196
- if (mediaIds.length > MAX_MEDIA_ATTACHMENTS) {
5197
- return `Posts allow at most ${MAX_MEDIA_ATTACHMENTS} media attachments`;
5198
- }
5199
- return null;
5200
- }
5201
5649
  /**
5202
5650
  * Parse and validate data against a Zod schema, throwing ValidationError on failure.
5203
5651
  *
@@ -5775,7 +6223,7 @@ setupRoutes.post("/setup", async (c)=>{
5775
6223
  await c.var.services.navItems.create({
5776
6224
  type: "link",
5777
6225
  label: "Collections",
5778
- url: "/collections"
6226
+ url: "/c"
5779
6227
  });
5780
6228
  // Seed default navigation items
5781
6229
  await c.var.services.navItems.create({
@@ -6354,9 +6802,10 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6354
6802
  /**
6355
6803
  * Extracts a paragraph-aware HTML excerpt from body HTML.
6356
6804
  *
6357
- * Uses a greedy algorithm: accumulates paragraphs until the total
6358
- * plain-text length exceeds 500 characters, then stops. At least
6359
- * one paragraph is always included.
6805
+ * Uses a greedy algorithm: accumulates paragraphs until either
6806
+ * the total plain-text length exceeds 500 characters or 5 paragraphs
6807
+ * have been collected, whichever comes first. At least one paragraph
6808
+ * is always included.
6360
6809
  *
6361
6810
  * If the content contains a `<!--more-->` marker, the content before
6362
6811
  * the marker is used as the excerpt instead.
@@ -6384,7 +6833,8 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6384
6833
  const excerpt = bodyHtml.split("<!--more-->")[0] ?? "";
6385
6834
  return {
6386
6835
  excerpt,
6387
- hasMore: true
6836
+ hasMore: true,
6837
+ excerptEnd: excerpt.length + "<!--more-->".length
6388
6838
  };
6389
6839
  }
6390
6840
  const paragraphs = bodyHtml.match(/<p>[\s\S]*?<\/p>/g) || [];
@@ -6392,21 +6842,25 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6392
6842
  if (paragraphs.length === 0) {
6393
6843
  return {
6394
6844
  excerpt: bodyHtml,
6395
- hasMore: false
6845
+ hasMore: false,
6846
+ excerptEnd: bodyHtml.length
6396
6847
  };
6397
6848
  }
6398
6849
  let excerpt = "";
6399
6850
  let charCount = 0;
6851
+ let paraCount = 0;
6400
6852
  for (const p of paragraphs){
6401
6853
  const textLen = stripHtml(p).length;
6402
- if (charCount + textLen > 500 && excerpt) break;
6854
+ if ((charCount + textLen > 500 || paraCount >= 5) && excerpt) break;
6403
6855
  excerpt += p;
6404
6856
  charCount += textLen;
6857
+ paraCount++;
6405
6858
  }
6406
6859
  const hasMore = excerpt.length < bodyHtml.length;
6407
6860
  return {
6408
6861
  excerpt,
6409
- hasMore
6862
+ hasMore,
6863
+ excerptEnd: excerpt.length
6410
6864
  };
6411
6865
  }
6412
6866
 
@@ -6471,10 +6925,16 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6471
6925
  // Pre-compute HTML summary for article-style posts (with title)
6472
6926
  let summaryHtml;
6473
6927
  let summaryHasMore;
6928
+ let bodyHtmlWithAnchor = post.bodyHtml;
6474
6929
  if (post.title && post.bodyHtml) {
6475
6930
  const result = getHtmlExcerpt(post.bodyHtml);
6476
6931
  summaryHtml = result.excerpt;
6477
6932
  summaryHasMore = result.hasMore;
6933
+ // Inject #continue anchor at the excerpt boundary for scroll targeting
6934
+ if (result.hasMore) {
6935
+ const pos = result.excerptEnd;
6936
+ bodyHtmlWithAnchor = post.bodyHtml.slice(0, pos) + '<span id="continue"></span>' + post.bodyHtml.slice(pos);
6937
+ }
6478
6938
  }
6479
6939
  // Convert media attachments
6480
6940
  const media = post.mediaAttachments.map((m)=>({
@@ -6491,7 +6951,7 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6491
6951
  permalink,
6492
6952
  path: post.path ?? undefined,
6493
6953
  title: post.title ?? undefined,
6494
- bodyHtml: post.bodyHtml ?? undefined,
6954
+ bodyHtml: bodyHtmlWithAnchor ?? undefined,
6495
6955
  excerpt,
6496
6956
  summaryHtml,
6497
6957
  summaryHasMore,
@@ -6552,21 +7012,34 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6552
7012
  // =============================================================================
6553
7013
  /**
6554
7014
  * Converts a NavItem to a NavItemView with pre-computed state.
6555
- */ function toNavItemView(item, currentPath) {
6556
- const isExternal = item.url.startsWith("http://") || item.url.startsWith("https://");
7015
+ *
7016
+ * @param item - Raw nav item from database
7017
+ * @param currentPath - Current URL path for active state
7018
+ * @param isAuthenticated - Whether the user is logged in (affects system dashboard item)
7019
+ */ function toNavItemView(item, currentPath, isAuthenticated = false) {
7020
+ let url = item.url;
7021
+ let label = item.label;
7022
+ // System dashboard item: resolve URL and label based on auth
7023
+ if (item.type === "system" && item.url === "/dash") {
7024
+ url = isAuthenticated ? "/dash" : "/signin";
7025
+ if (!isAuthenticated) {
7026
+ label = "Sign in";
7027
+ }
7028
+ }
7029
+ const isExternal = url.startsWith("http://") || url.startsWith("https://");
6557
7030
  let isActive = false;
6558
7031
  if (!isExternal) {
6559
- if (item.url === "/") {
7032
+ if (url === "/") {
6560
7033
  isActive = currentPath === "/";
6561
7034
  } else {
6562
- isActive = currentPath === item.url || currentPath.startsWith(item.url + "/");
7035
+ isActive = currentPath === url || currentPath.startsWith(url + "/");
6563
7036
  }
6564
7037
  }
6565
7038
  return {
6566
7039
  id: item.id,
6567
7040
  type: item.type,
6568
- label: item.label,
6569
- url: item.url,
7041
+ label,
7042
+ url,
6570
7043
  pageId: item.pageId ?? undefined,
6571
7044
  isActive,
6572
7045
  isExternal
@@ -6574,8 +7047,12 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6574
7047
  }
6575
7048
  /**
6576
7049
  * Batch converts NavItem[] to NavItemView[].
6577
- */ function toNavItemViews(items, currentPath) {
6578
- return items.map((item)=>toNavItemView(item, currentPath));
7050
+ *
7051
+ * @param items - Raw nav items from database
7052
+ * @param currentPath - Current URL path for active state
7053
+ * @param isAuthenticated - Whether the user is logged in
7054
+ */ function toNavItemViews(items, currentPath, isAuthenticated = false) {
7055
+ return items.map((item)=>toNavItemView(item, currentPath, isAuthenticated));
6579
7056
  }
6580
7057
  // =============================================================================
6581
7058
  // Search Result Conversions
@@ -6651,8 +7128,7 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6651
7128
  const showHeaderAvatar = appConfig.showHeaderAvatar;
6652
7129
  // Render footer markdown
6653
7130
  const siteFooterHtml = siteFooter ? render(siteFooter) : undefined;
6654
- const links = toNavItemViews(items, currentPath);
6655
- // Check auth status for compose button
7131
+ // Check auth status (needed for compose button and system nav items)
6656
7132
  let isAuthenticated = false;
6657
7133
  let collections = [];
6658
7134
  try {
@@ -6663,6 +7139,7 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6663
7139
  } catch {
6664
7140
  // Not authenticated
6665
7141
  }
7142
+ const links = toNavItemViews(items, currentPath, isAuthenticated);
6666
7143
  // Only load collections when authenticated (for compose dialog)
6667
7144
  if (isAuthenticated) {
6668
7145
  collections = await c.var.services.collections.list();
@@ -6675,6 +7152,7 @@ const sqid = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
6675
7152
  isAuthenticated,
6676
7153
  collections,
6677
7154
  homeDefaultView,
7155
+ headerNavMaxVisible: appConfig.headerNavMaxVisible,
6678
7156
  siteAvatarUrl,
6679
7157
  showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
6680
7158
  siteFooterHtml
@@ -6934,46 +7412,50 @@ const ComposePrompt = ()=>{
6934
7412
  const { i18n: $__i18n} = useLingui();
6935
7413
  return /*#__PURE__*/ jsxDEV("div", {
6936
7414
  class: "compose-prompt",
6937
- children: [
6938
- /*#__PURE__*/ jsxDEV("button", {
6939
- type: "button",
6940
- class: "compose-prompt-trigger",
6941
- onclick: "const d=document.getElementById('compose-dialog');d.showModal();d.querySelector('jant-compose-editor')?.focusInput()",
6942
- children: [
6943
- /*#__PURE__*/ jsxDEV("span", {
6944
- class: "compose-prompt-avatar",
6945
- children: /*#__PURE__*/ jsxDEV("svg", {
6946
- xmlns: "http://www.w3.org/2000/svg",
6947
- width: "16",
6948
- height: "16",
6949
- viewBox: "0 0 24 24",
6950
- fill: "none",
6951
- stroke: "currentColor",
6952
- "stroke-width": "2",
6953
- "stroke-linecap": "round",
6954
- "stroke-linejoin": "round",
6955
- children: /*#__PURE__*/ jsxDEV("path", {
6956
- d: "M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 1 1 3.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
7415
+ children: /*#__PURE__*/ jsxDEV("button", {
7416
+ type: "button",
7417
+ class: "compose-prompt-trigger",
7418
+ onclick: "const d=document.getElementById('compose-dialog');d.showModal();d.querySelector('jant-compose-editor')?.focusInput()",
7419
+ children: [
7420
+ /*#__PURE__*/ jsxDEV("span", {
7421
+ class: "compose-prompt-avatar",
7422
+ children: /*#__PURE__*/ jsxDEV("svg", {
7423
+ xmlns: "http://www.w3.org/2000/svg",
7424
+ width: "16",
7425
+ height: "16",
7426
+ viewBox: "0 0 24 24",
7427
+ fill: "none",
7428
+ stroke: "currentColor",
7429
+ "stroke-width": "2",
7430
+ "stroke-linecap": "round",
7431
+ "stroke-linejoin": "round",
7432
+ children: [
7433
+ /*#__PURE__*/ jsxDEV("path", {
7434
+ d: "M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"
7435
+ }),
7436
+ /*#__PURE__*/ jsxDEV("line", {
7437
+ x1: "16",
7438
+ y1: "8",
7439
+ x2: "2",
7440
+ y2: "22"
7441
+ }),
7442
+ /*#__PURE__*/ jsxDEV("line", {
7443
+ x1: "17.5",
7444
+ y1: "15",
7445
+ x2: "9",
7446
+ y2: "15"
6957
7447
  })
6958
- })
6959
- }),
6960
- /*#__PURE__*/ jsxDEV("span", {
6961
- class: "compose-prompt-text",
6962
- children: $__i18n._({
6963
- id: "7nGhhM"
6964
- })
7448
+ ]
7449
+ })
7450
+ }),
7451
+ /*#__PURE__*/ jsxDEV("span", {
7452
+ class: "compose-prompt-text",
7453
+ children: $__i18n._({
7454
+ id: "7nGhhM"
6965
7455
  })
6966
- ]
6967
- }),
6968
- /*#__PURE__*/ jsxDEV("button", {
6969
- type: "button",
6970
- class: "compose-prompt-post-btn",
6971
- onclick: "const d=document.getElementById('compose-dialog');d.showModal();d.querySelector('jant-compose-editor')?.focusInput()",
6972
- children: $__i18n._({
6973
- id: "y28hnO"
6974
7456
  })
6975
- })
6976
- ]
7457
+ ]
7458
+ })
6977
7459
  });
6978
7460
  };
6979
7461
 
@@ -6988,8 +7470,9 @@ function HeaderLink({ link }) {
6988
7470
  children: link.label
6989
7471
  });
6990
7472
  }
6991
- const SiteLayout = ({ siteName, siteDescription, links, currentPath, isAuthenticated, collections, homeDefaultView, siteAvatarUrl, showHeaderAvatar, siteFooterHtml, sidebar, children })=>{
7473
+ const SiteLayout = ({ siteName, links, currentPath, isAuthenticated, collections, homeDefaultView, headerNavMaxVisible, siteAvatarUrl, showHeaderAvatar, siteFooterHtml, sidebar, children })=>{
6992
7474
  const { i18n: $__i18n} = useLingui();
7475
+ const maxVisible = headerNavMaxVisible ?? 3;
6993
7476
  const latestHref = homeDefaultView === "featured" ? "/latest" : "/";
6994
7477
  const featuredHref = homeDefaultView === "featured" ? "/" : "/featured";
6995
7478
  const latestLink = {
@@ -7023,67 +7506,123 @@ const SiteLayout = ({ siteName, siteDescription, links, currentPath, isAuthentic
7023
7506
  class: `site-header ${sidebar ? "site-header-sidebar" : ""}`,
7024
7507
  children: /*#__PURE__*/ jsxDEV("div", {
7025
7508
  class: "site-header-inner",
7026
- children: [
7027
- /*#__PURE__*/ jsxDEV("div", {
7028
- class: "site-header-top site-header-top-bordered",
7029
- children: [
7030
- /*#__PURE__*/ jsxDEV("a", {
7031
- href: "/",
7032
- class: "site-logo",
7033
- children: [
7034
- showHeaderAvatar && siteAvatarUrl && /*#__PURE__*/ jsxDEV("img", {
7035
- src: siteAvatarUrl,
7036
- class: "site-logo-avatar",
7037
- alt: ""
7038
- }),
7039
- siteName
7040
- ]
7041
- }),
7042
- /*#__PURE__*/ jsxDEV("div", {
7043
- class: "site-header-right",
7044
- children: [
7045
- links.length > 0 && /*#__PURE__*/ jsxDEV("nav", {
7046
- class: "site-header-nav",
7047
- children: links.map((link)=>/*#__PURE__*/ jsxDEV(HeaderLink, {
7509
+ children: /*#__PURE__*/ jsxDEV("div", {
7510
+ class: "site-header-top site-header-top-bordered",
7511
+ children: [
7512
+ /*#__PURE__*/ jsxDEV("a", {
7513
+ href: "/",
7514
+ class: "site-logo",
7515
+ children: [
7516
+ showHeaderAvatar && siteAvatarUrl && /*#__PURE__*/ jsxDEV("img", {
7517
+ src: siteAvatarUrl,
7518
+ class: "site-logo-avatar",
7519
+ alt: ""
7520
+ }),
7521
+ siteName
7522
+ ]
7523
+ }),
7524
+ /*#__PURE__*/ jsxDEV("div", {
7525
+ class: "site-header-right",
7526
+ children: [
7527
+ links.length > 0 && /*#__PURE__*/ jsxDEV("nav", {
7528
+ class: "site-header-nav",
7529
+ children: [
7530
+ links.slice(0, maxVisible).map((link)=>/*#__PURE__*/ jsxDEV(HeaderLink, {
7048
7531
  link: link
7049
- }, link.id))
7050
- }),
7051
- /*#__PURE__*/ jsxDEV("a", {
7052
- href: "/search",
7053
- class: `site-header-search ${currentPath === "/search" ? "site-header-search-active" : ""}`,
7054
- "aria-label": searchLabel,
7055
- title: searchLabel,
7056
- children: /*#__PURE__*/ jsxDEV("svg", {
7057
- xmlns: "http://www.w3.org/2000/svg",
7058
- width: "16",
7059
- height: "16",
7060
- viewBox: "0 0 24 24",
7061
- fill: "none",
7062
- stroke: "currentColor",
7063
- "stroke-width": "2",
7064
- "stroke-linecap": "round",
7065
- "stroke-linejoin": "round",
7532
+ }, link.id)),
7533
+ links.length > maxVisible && /*#__PURE__*/ jsxDEV("div", {
7534
+ class: "dropdown-menu site-header-more",
7066
7535
  children: [
7067
- /*#__PURE__*/ jsxDEV("circle", {
7068
- cx: "11",
7069
- cy: "11",
7070
- r: "8"
7536
+ /*#__PURE__*/ jsxDEV("button", {
7537
+ type: "button",
7538
+ id: "site-nav-more-trigger",
7539
+ class: "site-header-more-btn",
7540
+ "aria-haspopup": "menu",
7541
+ "aria-controls": "site-nav-more-menu",
7542
+ "aria-expanded": "false",
7543
+ "aria-label": $__i18n._({
7544
+ id: "ccaIM9"
7545
+ }),
7546
+ children: /*#__PURE__*/ jsxDEV("svg", {
7547
+ xmlns: "http://www.w3.org/2000/svg",
7548
+ width: "16",
7549
+ height: "16",
7550
+ viewBox: "0 0 24 24",
7551
+ fill: "currentColor",
7552
+ children: [
7553
+ /*#__PURE__*/ jsxDEV("circle", {
7554
+ cx: "5",
7555
+ cy: "12",
7556
+ r: "2"
7557
+ }),
7558
+ /*#__PURE__*/ jsxDEV("circle", {
7559
+ cx: "12",
7560
+ cy: "12",
7561
+ r: "2"
7562
+ }),
7563
+ /*#__PURE__*/ jsxDEV("circle", {
7564
+ cx: "19",
7565
+ cy: "12",
7566
+ r: "2"
7567
+ })
7568
+ ]
7569
+ })
7071
7570
  }),
7072
- /*#__PURE__*/ jsxDEV("path", {
7073
- d: "m21 21-4.35-4.35"
7571
+ /*#__PURE__*/ jsxDEV("div", {
7572
+ id: "site-nav-more-popover",
7573
+ "data-popover": true,
7574
+ "data-align": "end",
7575
+ "aria-hidden": "true",
7576
+ children: /*#__PURE__*/ jsxDEV("div", {
7577
+ role: "menu",
7578
+ id: "site-nav-more-menu",
7579
+ "aria-labelledby": "site-nav-more-trigger",
7580
+ children: links.slice(maxVisible).map((link)=>/*#__PURE__*/ jsxDEV("a", {
7581
+ href: link.url,
7582
+ role: "menuitem",
7583
+ ...link.isExternal ? {
7584
+ target: "_blank",
7585
+ rel: "noopener noreferrer"
7586
+ } : {},
7587
+ children: link.label
7588
+ }, link.id))
7589
+ })
7074
7590
  })
7075
7591
  ]
7076
7592
  })
7593
+ ]
7594
+ }),
7595
+ /*#__PURE__*/ jsxDEV("a", {
7596
+ href: "/search",
7597
+ class: `site-header-search ${currentPath === "/search" ? "site-header-search-active" : ""}`,
7598
+ "aria-label": searchLabel,
7599
+ title: searchLabel,
7600
+ children: /*#__PURE__*/ jsxDEV("svg", {
7601
+ xmlns: "http://www.w3.org/2000/svg",
7602
+ width: "16",
7603
+ height: "16",
7604
+ viewBox: "0 0 24 24",
7605
+ fill: "none",
7606
+ stroke: "currentColor",
7607
+ "stroke-width": "2",
7608
+ "stroke-linecap": "round",
7609
+ "stroke-linejoin": "round",
7610
+ children: [
7611
+ /*#__PURE__*/ jsxDEV("circle", {
7612
+ cx: "11",
7613
+ cy: "11",
7614
+ r: "8"
7615
+ }),
7616
+ /*#__PURE__*/ jsxDEV("path", {
7617
+ d: "m21 21-4.35-4.35"
7618
+ })
7619
+ ]
7077
7620
  })
7078
- ]
7079
- })
7080
- ]
7081
- }),
7082
- isHomePage && siteDescription && /*#__PURE__*/ jsxDEV("p", {
7083
- class: "site-description",
7084
- children: siteDescription
7085
- })
7086
- ]
7621
+ })
7622
+ ]
7623
+ })
7624
+ ]
7625
+ })
7087
7626
  })
7088
7627
  }),
7089
7628
  /*#__PURE__*/ jsxDEV("main", {
@@ -7111,23 +7650,29 @@ const SiteLayout = ({ siteName, siteDescription, links, currentPath, isAuthentic
7111
7650
  children: /*#__PURE__*/ jsxDEV("div", {
7112
7651
  class: "site-content",
7113
7652
  children: [
7114
- isHomePage && /*#__PURE__*/ jsxDEV("nav", {
7115
- class: "site-browse-nav",
7116
- children: browseLinks.map((link, i)=>/*#__PURE__*/ jsxDEV(Fragment, {
7117
- children: [
7118
- i > 0 && /*#__PURE__*/ jsxDEV("span", {
7119
- class: "site-browse-sep",
7120
- children: "/"
7121
- }),
7122
- /*#__PURE__*/ jsxDEV("a", {
7123
- href: link.href,
7124
- class: `site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`,
7125
- children: link.label
7126
- }, link.href)
7127
- ]
7128
- }))
7653
+ isHomePage && /*#__PURE__*/ jsxDEV("div", {
7654
+ class: "site-home-header",
7655
+ children: [
7656
+ isAuthenticated && /*#__PURE__*/ jsxDEV(ComposePrompt, {}),
7657
+ /*#__PURE__*/ jsxDEV("nav", {
7658
+ class: "site-browse-nav",
7659
+ children: browseLinks.map((link, i)=>/*#__PURE__*/ jsxDEV(Fragment, {
7660
+ children: [
7661
+ i > 0 && /*#__PURE__*/ jsxDEV("span", {
7662
+ class: "site-browse-sep",
7663
+ "aria-hidden": "true",
7664
+ children: "/"
7665
+ }),
7666
+ /*#__PURE__*/ jsxDEV("a", {
7667
+ href: link.href,
7668
+ class: `site-browse-link ${currentPath === link.href ? "site-browse-link-active" : ""}`,
7669
+ children: link.label
7670
+ }, link.href)
7671
+ ]
7672
+ }))
7673
+ })
7674
+ ]
7129
7675
  }),
7130
- isHomePage && isAuthenticated && /*#__PURE__*/ jsxDEV(ComposePrompt, {}),
7131
7676
  children
7132
7677
  ]
7133
7678
  })
@@ -7171,14 +7716,16 @@ const SiteLayout = ({ siteName, siteDescription, links, currentPath, isAuthentic
7171
7716
  * ```
7172
7717
  */ function renderPublicPage(c, options) {
7173
7718
  const { title, description, navData, content, sidebar } = options;
7719
+ // Use siteDescription as meta description fallback when not explicitly provided
7720
+ const metaDescription = description || navData.siteDescription || undefined;
7174
7721
  const layoutProps = {
7175
7722
  siteName: navData.siteName,
7176
- siteDescription: navData.siteDescription,
7177
7723
  links: navData.links,
7178
7724
  currentPath: navData.currentPath,
7179
7725
  isAuthenticated: navData.isAuthenticated,
7180
7726
  collections: navData.collections,
7181
7727
  homeDefaultView: navData.homeDefaultView,
7728
+ headerNavMaxVisible: navData.headerNavMaxVisible,
7182
7729
  siteAvatarUrl: navData.siteAvatarUrl,
7183
7730
  showHeaderAvatar: navData.showHeaderAvatar,
7184
7731
  siteFooterHtml: navData.siteFooterHtml,
@@ -7191,7 +7738,7 @@ const SiteLayout = ({ siteName, siteDescription, links, currentPath, isAuthentic
7191
7738
  const noindex = appConfig.noindex;
7192
7739
  return c.html(/*#__PURE__*/ jsxDEV(BaseLayout, {
7193
7740
  title: title,
7194
- description: description,
7741
+ description: metaDescription,
7195
7742
  c: c,
7196
7743
  faviconUrl: faviconUrl,
7197
7744
  faviconVersion: faviconVersion,
@@ -7399,9 +7946,9 @@ const NoteCard = ({ post, compact })=>{
7399
7946
  })
7400
7947
  }),
7401
7948
  !compact && isArticle && post.summaryHasMore && /*#__PURE__*/ jsxDEV("a", {
7402
- href: post.permalink,
7949
+ href: `${post.permalink}#continue`,
7403
7950
  class: "text-sm text-muted-foreground hover:underline mt-1 inline-block",
7404
- children: "Read more →"
7951
+ children: "Continue →"
7405
7952
  }),
7406
7953
  /*#__PURE__*/ jsxDEV("footer", {
7407
7954
  class: "mt-2",
@@ -8052,30 +8599,26 @@ const pageRoutes = new Hono();
8052
8599
  pageRoutes.get("/*", async (c)=>{
8053
8600
  const fullPath = c.req.path.slice(1); // Remove leading /
8054
8601
  if (!fullPath) return c.notFound();
8055
- const isMultiSegment = fullPath.includes("/");
8056
- // Pages only have single-level slugs; skip page lookup for multi-segment paths
8057
- if (!isMultiSegment) {
8058
- const page = await c.var.services.pages.getBySlug(fullPath);
8059
- if (page) {
8060
- if (page.status === "draft") {
8061
- return c.notFound();
8062
- }
8063
- const navData = await getNavigationData(c);
8064
- const pageView = toPageView(page);
8065
- return renderPublicPage(c, {
8066
- title: `${page.title || fullPath} - ${navData.siteName}`,
8067
- description: page.body?.slice(0, 160),
8068
- navData,
8069
- content: /*#__PURE__*/ jsxDEV(SinglePage, {
8070
- page: pageView
8071
- })
8072
- });
8602
+ const entry = await c.var.services.pathRegistry.getByPath(fullPath);
8603
+ if (entry?.ownerType === "page") {
8604
+ const page = await c.var.services.pages.getById(entry.ownerId);
8605
+ if (!page || page.status === "draft") {
8606
+ return c.notFound();
8073
8607
  }
8608
+ const navData = await getNavigationData(c);
8609
+ const pageView = toPageView(page);
8610
+ return renderPublicPage(c, {
8611
+ title: `${page.title || fullPath} - ${navData.siteName}`,
8612
+ description: page.body?.slice(0, 160),
8613
+ navData,
8614
+ content: /*#__PURE__*/ jsxDEV(SinglePage, {
8615
+ page: pageView
8616
+ })
8617
+ });
8074
8618
  }
8075
- // Posts support multi-level paths
8076
- const post = await c.var.services.posts.getByPath(fullPath);
8077
- if (post) {
8078
- if (post.status === "draft") {
8619
+ if (entry?.ownerType === "post") {
8620
+ const post = await c.var.services.posts.getById(entry.ownerId);
8621
+ if (!post || post.status === "draft") {
8079
8622
  return c.notFound();
8080
8623
  }
8081
8624
  // Load media attachments
@@ -8897,113 +9440,149 @@ collectionsPageRoutes.get("/", async (c)=>{
8897
9440
 
8898
9441
  function DashLayoutContent({ siteName, currentPath, children }) {
8899
9442
  const { i18n: $__i18n} = useLingui();
8900
- const isActive = (path, match)=>{
8901
- if (!currentPath) return false;
8902
- if (match) return match.test(currentPath);
8903
- return currentPath === path;
8904
- };
8905
- const navClass = (path, match)=>`justify-start px-3 py-2 text-sm rounded-md ${isActive(path, match) ? "bg-accent text-accent-foreground font-medium" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"}`;
9443
+ const navClass = (match)=>`dash-header-link ${currentPath && match.test(currentPath) ? "dash-header-link-active" : ""}`;
8906
9444
  return /*#__PURE__*/ jsxDEV("div", {
8907
9445
  class: "min-h-screen",
8908
9446
  children: [
8909
9447
  /*#__PURE__*/ jsxDEV("header", {
8910
- class: "border-b bg-card",
9448
+ class: "dash-header",
8911
9449
  children: /*#__PURE__*/ jsxDEV("div", {
8912
- class: "container-sidebar flex h-14 items-center justify-between",
9450
+ class: "container dash-header-inner",
8913
9451
  children: [
8914
- /*#__PURE__*/ jsxDEV("a", {
8915
- id: "site-name",
8916
- href: "/dash",
8917
- class: "font-semibold",
8918
- children: siteName
8919
- }),
8920
- /*#__PURE__*/ jsxDEV("nav", {
8921
- class: "flex items-center gap-4",
9452
+ /*#__PURE__*/ jsxDEV("div", {
9453
+ class: "dash-header-left",
8922
9454
  children: [
8923
9455
  /*#__PURE__*/ jsxDEV("a", {
8924
- href: "/",
8925
- class: "text-sm text-muted-foreground hover:text-foreground",
8926
- children: $__i18n._({
8927
- id: "4/SFQS"
8928
- })
9456
+ id: "site-name",
9457
+ href: "/dash",
9458
+ class: "dash-header-logo",
9459
+ children: siteName
8929
9460
  }),
8930
9461
  /*#__PURE__*/ jsxDEV("a", {
8931
- href: "/signout",
8932
- class: "text-sm text-muted-foreground hover:text-foreground",
8933
- children: $__i18n._({
8934
- id: "bHYIks"
9462
+ href: "/",
9463
+ target: "_blank",
9464
+ rel: "noopener noreferrer",
9465
+ class: "dash-header-site-link",
9466
+ "aria-label": $__i18n._({
9467
+ id: "4/SFQS"
9468
+ }),
9469
+ children: /*#__PURE__*/ jsxDEV("svg", {
9470
+ xmlns: "http://www.w3.org/2000/svg",
9471
+ width: "14",
9472
+ height: "14",
9473
+ viewBox: "0 0 24 24",
9474
+ fill: "none",
9475
+ stroke: "currentColor",
9476
+ "stroke-width": "2",
9477
+ "stroke-linecap": "round",
9478
+ "stroke-linejoin": "round",
9479
+ children: [
9480
+ /*#__PURE__*/ jsxDEV("path", {
9481
+ d: "M15 3h6v6"
9482
+ }),
9483
+ /*#__PURE__*/ jsxDEV("path", {
9484
+ d: "M10 14 21 3"
9485
+ }),
9486
+ /*#__PURE__*/ jsxDEV("path", {
9487
+ d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
9488
+ })
9489
+ ]
8935
9490
  })
8936
9491
  })
8937
9492
  ]
8938
- })
8939
- ]
8940
- })
8941
- }),
8942
- /*#__PURE__*/ jsxDEV("div", {
8943
- class: "container-sidebar sidebar-layout py-8",
8944
- children: [
8945
- /*#__PURE__*/ jsxDEV("aside", {
8946
- class: "sidebar-nav",
8947
- children: /*#__PURE__*/ jsxDEV("nav", {
8948
- class: "flex flex-col gap-1",
9493
+ }),
9494
+ /*#__PURE__*/ jsxDEV("nav", {
9495
+ class: "dash-header-nav",
8949
9496
  children: [
8950
- /*#__PURE__*/ jsxDEV("a", {
8951
- href: "/dash",
8952
- class: navClass("/dash", /^\/dash$/),
8953
- children: $__i18n._({
8954
- id: "7p5kLi"
8955
- })
8956
- }),
8957
- /*#__PURE__*/ jsxDEV("a", {
8958
- href: "/dash/posts",
8959
- class: navClass("/dash/posts", /^\/dash\/posts/),
8960
- children: $__i18n._({
8961
- id: "+owNNn"
8962
- })
8963
- }),
8964
9497
  /*#__PURE__*/ jsxDEV("a", {
8965
9498
  href: "/dash/pages",
8966
- class: navClass("/dash/pages", /^\/dash\/pages/),
9499
+ class: navClass(/^\/dash\/pages/),
8967
9500
  children: $__i18n._({
8968
9501
  id: "wRR604"
8969
9502
  })
8970
9503
  }),
8971
- /*#__PURE__*/ jsxDEV("a", {
8972
- href: "/dash/media",
8973
- class: navClass("/dash/media", /^\/dash\/media/),
8974
- children: $__i18n._({
8975
- id: "xYilR2"
8976
- })
8977
- }),
8978
- /*#__PURE__*/ jsxDEV("a", {
8979
- href: "/dash/collections",
8980
- class: navClass("/dash/collections", /^\/dash\/collections/),
8981
- children: $__i18n._({
8982
- id: "DoJzLz"
8983
- })
8984
- }),
8985
9504
  /*#__PURE__*/ jsxDEV("a", {
8986
9505
  href: "/dash/appearance",
8987
- class: navClass("/dash/appearance", /^\/dash\/appearance/),
9506
+ class: navClass(/^\/dash\/appearance/),
8988
9507
  children: $__i18n._({
8989
9508
  id: "aAIQg2"
8990
9509
  })
8991
9510
  }),
8992
9511
  /*#__PURE__*/ jsxDEV("a", {
8993
9512
  href: "/dash/settings",
8994
- class: navClass("/dash/settings", /^\/dash\/settings/),
9513
+ class: navClass(/^\/dash\/settings/),
8995
9514
  children: $__i18n._({
8996
9515
  id: "Tz0i8g"
8997
9516
  })
8998
9517
  })
8999
9518
  ]
9519
+ }),
9520
+ /*#__PURE__*/ jsxDEV("div", {
9521
+ class: "dropdown-menu",
9522
+ children: [
9523
+ /*#__PURE__*/ jsxDEV("button", {
9524
+ type: "button",
9525
+ id: "dash-menu-trigger",
9526
+ class: "dash-header-menu-btn",
9527
+ "aria-haspopup": "menu",
9528
+ "aria-controls": "dash-menu",
9529
+ "aria-expanded": "false",
9530
+ "aria-label": $__i18n._({
9531
+ id: "zucql+"
9532
+ }),
9533
+ children: /*#__PURE__*/ jsxDEV("svg", {
9534
+ xmlns: "http://www.w3.org/2000/svg",
9535
+ width: "16",
9536
+ height: "16",
9537
+ viewBox: "0 0 24 24",
9538
+ fill: "currentColor",
9539
+ children: [
9540
+ /*#__PURE__*/ jsxDEV("circle", {
9541
+ cx: "5",
9542
+ cy: "12",
9543
+ r: "2"
9544
+ }),
9545
+ /*#__PURE__*/ jsxDEV("circle", {
9546
+ cx: "12",
9547
+ cy: "12",
9548
+ r: "2"
9549
+ }),
9550
+ /*#__PURE__*/ jsxDEV("circle", {
9551
+ cx: "19",
9552
+ cy: "12",
9553
+ r: "2"
9554
+ })
9555
+ ]
9556
+ })
9557
+ }),
9558
+ /*#__PURE__*/ jsxDEV("div", {
9559
+ id: "dash-menu-popover",
9560
+ "data-popover": true,
9561
+ "data-align": "end",
9562
+ "aria-hidden": "true",
9563
+ children: /*#__PURE__*/ jsxDEV("div", {
9564
+ role: "menu",
9565
+ id: "dash-menu",
9566
+ "aria-labelledby": "dash-menu-trigger",
9567
+ children: /*#__PURE__*/ jsxDEV("a", {
9568
+ href: "/signout",
9569
+ role: "menuitem",
9570
+ children: $__i18n._({
9571
+ id: "bHYIks"
9572
+ })
9573
+ })
9574
+ })
9575
+ })
9576
+ ]
9000
9577
  })
9001
- }),
9002
- /*#__PURE__*/ jsxDEV("main", {
9003
- class: "sidebar-main",
9004
- children: children
9005
- })
9006
- ]
9578
+ ]
9579
+ })
9580
+ }),
9581
+ /*#__PURE__*/ jsxDEV("div", {
9582
+ class: "container py-8",
9583
+ children: /*#__PURE__*/ jsxDEV("main", {
9584
+ children: children
9585
+ })
9007
9586
  })
9008
9587
  ]
9009
9588
  });
@@ -9808,7 +10387,11 @@ postsRoutes.get("/new", async (c)=>{
9808
10387
  // Create post
9809
10388
  postsRoutes.post("/", async (c)=>{
9810
10389
  const wantsJson = c.req.header("Accept")?.includes("application/json");
9811
- const body = await c.req.json();
10390
+ const body = parseValidated(CreatePostSchema, await c.req.json());
10391
+ // Validate media IDs before creating post
10392
+ if (body.mediaIds && body.mediaIds.length > 0) {
10393
+ await c.var.services.media.validateIds(body.mediaIds);
10394
+ }
9812
10395
  const post = await c.var.services.posts.create({
9813
10396
  format: body.format,
9814
10397
  title: body.title || undefined,
@@ -9952,7 +10535,11 @@ postsRoutes.post("/:id", async (c)=>{
9952
10535
  const id = decode(c.req.param("id"));
9953
10536
  if (!id) return c.notFound();
9954
10537
  const wantsJson = c.req.header("Accept")?.includes("application/json");
9955
- const body = await c.req.json();
10538
+ const body = parseValidated(UpdatePostSchema, await c.req.json());
10539
+ // Validate media IDs if provided
10540
+ if (body.mediaIds !== undefined) {
10541
+ await c.var.services.media.validateIds(body.mediaIds);
10542
+ }
9956
10543
  await c.var.services.posts.update(id, {
9957
10544
  format: body.format,
9958
10545
  title: body.title || null,
@@ -9982,12 +10569,14 @@ postsRoutes.post("/:id", async (c)=>{
9982
10569
  postsRoutes.post("/:id/delete", async (c)=>{
9983
10570
  const id = decode(c.req.param("id"));
9984
10571
  if (!id) return c.notFound();
9985
- await c.var.services.media.detachFromPost(id);
9986
- await c.var.services.posts.delete(id);
10572
+ await c.var.services.posts.delete(id, {
10573
+ media: c.var.services.media,
10574
+ storage: c.var.storage
10575
+ });
9987
10576
  return dsRedirect("/dash/posts");
9988
10577
  });
9989
10578
 
9990
- function UnifiedPagesContent({ navItems, otherPages }) {
10579
+ function PagesContent({ pages }) {
9991
10580
  const { i18n: $__i18n} = useLingui();
9992
10581
  return /*#__PURE__*/ jsxDEV(Fragment, {
9993
10582
  children: [
@@ -9995,283 +10584,49 @@ function UnifiedPagesContent({ navItems, otherPages }) {
9995
10584
  title: $__i18n._({
9996
10585
  id: "wRR604"
9997
10586
  }),
9998
- children: /*#__PURE__*/ jsxDEV("div", {
9999
- class: "flex gap-2",
10000
- children: [
10001
- /*#__PURE__*/ jsxDEV("a", {
10002
- href: "/dash/pages/links/new",
10003
- class: "btn-outline",
10004
- children: $__i18n._({
10005
- id: "V4WsyL"
10006
- })
10007
- }),
10008
- /*#__PURE__*/ jsxDEV("a", {
10009
- href: "/dash/pages/new",
10010
- class: "btn",
10011
- children: $__i18n._({
10012
- id: "GrZ6fH"
10013
- })
10014
- })
10015
- ]
10016
- })
10017
- }),
10018
- /*#__PURE__*/ jsxDEV("section", {
10019
- class: "mb-8",
10020
- children: [
10021
- /*#__PURE__*/ jsxDEV("h2", {
10022
- class: "text-lg font-medium mb-3",
10023
- children: $__i18n._({
10024
- id: "GTPbOX"
10025
- })
10026
- }),
10027
- navItems.length === 0 ? /*#__PURE__*/ jsxDEV("p", {
10028
- class: "text-sm text-muted-foreground py-4",
10029
- children: $__i18n._({
10030
- id: "vh0C9b"
10031
- })
10032
- }) : /*#__PURE__*/ jsxDEV("div", {
10033
- id: "nav-links-list",
10034
- class: "flex flex-col divide-y",
10035
- children: navItems.map((item)=>/*#__PURE__*/ jsxDEV(ListItemRow, {
10036
- actions: item.type === "page" ? /*#__PURE__*/ jsxDEV(Fragment, {
10037
- children: [
10038
- /*#__PURE__*/ jsxDEV(ActionButtons, {
10039
- editHref: item.pageId ? `/dash/pages/${item.pageId}/edit` : undefined,
10040
- editLabel: $__i18n._({
10041
- id: "ePK91l"
10042
- })
10043
- }),
10044
- /*#__PURE__*/ jsxDEV("button", {
10045
- type: "button",
10046
- class: "btn-sm-ghost",
10047
- "data-on:click__prevent": `@post('/dash/pages/${item.pageId}/remove-from-nav')`,
10048
- children: $__i18n._({
10049
- id: "g3mKmM"
10050
- })
10051
- })
10052
- ]
10053
- }) : /*#__PURE__*/ jsxDEV(Fragment, {
10054
- children: /*#__PURE__*/ jsxDEV(ActionButtons, {
10055
- editHref: `/dash/pages/links/${item.id}/edit`,
10056
- editLabel: $__i18n._({
10057
- id: "ePK91l"
10058
- }),
10059
- deleteAction: `/dash/pages/links/${item.id}/delete`,
10060
- deleteLabel: $__i18n._({
10061
- id: "cnGeoo"
10062
- })
10063
- })
10064
- }),
10065
- children: /*#__PURE__*/ jsxDEV("div", {
10066
- class: "flex items-center gap-3 cursor-grab",
10067
- "data-id": item.id,
10068
- children: [
10069
- /*#__PURE__*/ jsxDEV("span", {
10070
- class: "text-muted-foreground select-none",
10071
- children: "⠿"
10072
- }),
10073
- /*#__PURE__*/ jsxDEV("div", {
10074
- class: "flex items-center gap-2",
10075
- children: [
10076
- /*#__PURE__*/ jsxDEV("span", {
10077
- class: "font-medium",
10078
- children: item.label
10079
- }),
10080
- /*#__PURE__*/ jsxDEV("code", {
10081
- class: "text-sm text-muted-foreground bg-muted px-1 rounded",
10082
- children: item.url
10083
- }),
10084
- /*#__PURE__*/ jsxDEV("span", {
10085
- class: "badge-secondary",
10086
- children: item.type === "page" ? $__i18n._({
10087
- id: "MnbH31"
10088
- }) : $__i18n._({
10089
- id: "LdyooL"
10090
- })
10091
- })
10092
- ]
10093
- })
10094
- ]
10095
- })
10096
- }, item.id))
10097
- })
10098
- ]
10099
- }),
10100
- /*#__PURE__*/ jsxDEV("section", {
10101
- children: [
10102
- /*#__PURE__*/ jsxDEV("h2", {
10103
- class: "text-lg font-medium mb-3",
10104
- children: $__i18n._({
10105
- id: "Y75ho6"
10106
- })
10107
- }),
10108
- otherPages.length === 0 ? /*#__PURE__*/ jsxDEV("p", {
10109
- class: "text-sm text-muted-foreground py-4",
10110
- children: $__i18n._({
10111
- id: "/rkqRV"
10112
- })
10113
- }) : /*#__PURE__*/ jsxDEV("div", {
10114
- class: "flex flex-col divide-y",
10115
- children: otherPages.map((page)=>/*#__PURE__*/ jsxDEV(ListItemRow, {
10116
- actions: /*#__PURE__*/ jsxDEV(Fragment, {
10117
- children: [
10118
- /*#__PURE__*/ jsxDEV("button", {
10119
- type: "button",
10120
- class: "btn-sm-outline",
10121
- "data-on:click__prevent": `@post('/dash/pages/${page.id}/add-to-nav')`,
10122
- children: $__i18n._({
10123
- id: "+MH6k9"
10124
- })
10125
- }),
10126
- /*#__PURE__*/ jsxDEV(ActionButtons, {
10127
- editHref: `/dash/pages/${page.id}/edit`,
10128
- editLabel: $__i18n._({
10129
- id: "ePK91l"
10130
- }),
10131
- viewHref: page.status !== "draft" ? `/${page.slug}` : undefined,
10132
- viewLabel: $__i18n._({
10133
- id: "jpctdh"
10134
- })
10135
- })
10136
- ]
10137
- }),
10138
- children: [
10139
- /*#__PURE__*/ jsxDEV("a", {
10140
- href: `/dash/pages/${page.id}`,
10141
- class: "font-medium hover:underline",
10142
- children: page.title || $__i18n._({
10143
- id: "wja8aL"
10144
- })
10145
- }),
10146
- /*#__PURE__*/ jsxDEV("p", {
10147
- class: "text-sm text-muted-foreground mt-1",
10148
- children: [
10149
- "/",
10150
- page.slug
10151
- ]
10152
- })
10153
- ]
10154
- }, page.id))
10587
+ children: /*#__PURE__*/ jsxDEV("a", {
10588
+ href: "/dash/pages/new",
10589
+ class: "btn",
10590
+ children: $__i18n._({
10591
+ id: "GrZ6fH"
10155
10592
  })
10156
- ]
10157
- })
10158
- ]
10159
- });
10160
- }
10161
-
10162
- function LinkFormContent({ item, isEdit }) {
10163
- const { i18n: $__i18n} = useLingui();
10164
- const title = isEdit ? $__i18n._({
10165
- id: "gDx5MG"
10166
- }) : $__i18n._({
10167
- id: "aaGV/9"
10168
- });
10169
- const signals = JSON.stringify({
10170
- label: item?.label ?? "",
10171
- url: item?.url ?? ""
10172
- }).replace(/</g, "\\u003c");
10173
- const action = isEdit ? `/dash/pages/links/${item?.id}` : "/dash/pages/links";
10174
- return /*#__PURE__*/ jsxDEV(Fragment, {
10175
- children: [
10176
- /*#__PURE__*/ jsxDEV("h1", {
10177
- class: "text-2xl font-semibold mb-6",
10178
- children: title
10593
+ })
10179
10594
  }),
10180
- /*#__PURE__*/ jsxDEV("form", {
10181
- "data-signals": signals,
10182
- "data-on:submit__prevent": `@post('${action}')`,
10183
- "data-indicator": "_loading",
10184
- class: "flex flex-col gap-4 max-w-lg",
10185
- children: [
10186
- /*#__PURE__*/ jsxDEV("div", {
10187
- class: "field",
10188
- children: [
10189
- /*#__PURE__*/ jsxDEV("label", {
10190
- class: "label",
10191
- children: $__i18n._({
10192
- id: "87a/t/"
10193
- })
10194
- }),
10195
- /*#__PURE__*/ jsxDEV("input", {
10196
- type: "text",
10197
- "data-bind": "label",
10198
- class: "input",
10199
- placeholder: "Home",
10200
- required: true
10595
+ pages.length === 0 ? /*#__PURE__*/ jsxDEV("p", {
10596
+ class: "text-sm text-muted-foreground py-4",
10597
+ children: $__i18n._({
10598
+ id: "hQAbqI"
10599
+ })
10600
+ }) : /*#__PURE__*/ jsxDEV("div", {
10601
+ class: "flex flex-col divide-y",
10602
+ children: pages.map((page)=>/*#__PURE__*/ jsxDEV(ListItemRow, {
10603
+ actions: /*#__PURE__*/ jsxDEV(ActionButtons, {
10604
+ editHref: `/dash/pages/${page.id}/edit`,
10605
+ editLabel: $__i18n._({
10606
+ id: "ePK91l"
10201
10607
  }),
10202
- /*#__PURE__*/ jsxDEV("p", {
10203
- class: "text-xs text-muted-foreground mt-1",
10204
- children: $__i18n._({
10205
- id: "+bHzpy"
10206
- })
10608
+ viewHref: page.status !== "draft" ? `/${page.slug}` : undefined,
10609
+ viewLabel: $__i18n._({
10610
+ id: "jpctdh"
10207
10611
  })
10208
- ]
10209
- }),
10210
- /*#__PURE__*/ jsxDEV("div", {
10211
- class: "field",
10612
+ }),
10212
10613
  children: [
10213
- /*#__PURE__*/ jsxDEV("label", {
10214
- class: "label",
10215
- children: $__i18n._({
10216
- id: "IagCbF"
10614
+ /*#__PURE__*/ jsxDEV("a", {
10615
+ href: `/dash/pages/${page.id}`,
10616
+ class: "font-medium hover:underline",
10617
+ children: page.title || $__i18n._({
10618
+ id: "wja8aL"
10217
10619
  })
10218
10620
  }),
10219
- /*#__PURE__*/ jsxDEV("input", {
10220
- type: "text",
10221
- "data-bind": "url",
10222
- class: "input",
10223
- placeholder: "/archive or https://...",
10224
- required: true
10225
- }),
10226
10621
  /*#__PURE__*/ jsxDEV("p", {
10227
- class: "text-xs text-muted-foreground mt-1",
10228
- children: $__i18n._({
10229
- id: "QEbNBb"
10230
- })
10231
- })
10232
- ]
10233
- }),
10234
- /*#__PURE__*/ jsxDEV("div", {
10235
- class: "flex gap-2",
10236
- children: [
10237
- /*#__PURE__*/ jsxDEV("button", {
10238
- type: "submit",
10239
- class: "btn",
10240
- "data-attr:disabled": "$_loading",
10622
+ class: "text-sm text-muted-foreground mt-1",
10241
10623
  children: [
10242
- /*#__PURE__*/ jsxDEV("svg", {
10243
- "data-show": "$_loading",
10244
- style: "display:none",
10245
- class: "animate-spin size-4",
10246
- xmlns: "http://www.w3.org/2000/svg",
10247
- viewBox: "0 0 24 24",
10248
- fill: "none",
10249
- stroke: "currentColor",
10250
- "stroke-width": "2",
10251
- "stroke-linecap": "round",
10252
- "stroke-linejoin": "round",
10253
- role: "status",
10254
- children: /*#__PURE__*/ jsxDEV("path", {
10255
- d: "M21 12a9 9 0 1 1-6.219-8.56"
10256
- })
10257
- }),
10258
- isEdit ? $__i18n._({
10259
- id: "IUwGEM"
10260
- }) : $__i18n._({
10261
- id: "kd7eBB"
10262
- })
10624
+ "/",
10625
+ page.slug
10263
10626
  ]
10264
- }),
10265
- /*#__PURE__*/ jsxDEV("a", {
10266
- href: "/dash/pages",
10267
- class: "btn-outline",
10268
- children: $__i18n._({
10269
- id: "dEgA5A"
10270
- })
10271
10627
  })
10272
10628
  ]
10273
- })
10274
- ]
10629
+ }, page.id))
10275
10630
  })
10276
10631
  ]
10277
10632
  });
@@ -10375,19 +10730,15 @@ function EditPageContent({ page }) {
10375
10730
  // Route handlers
10376
10731
  // =============================================================================
10377
10732
  pagesRoutes.get("/", async (c)=>{
10378
- const [navItems, otherPages] = await Promise.all([
10379
- c.var.services.navItems.list(),
10380
- c.var.services.pages.listNotInNav()
10381
- ]);
10733
+ const pages = await c.var.services.pages.list();
10382
10734
  const siteName = c.var.appConfig.siteName;
10383
10735
  return c.html(/*#__PURE__*/ jsxDEV(DashLayout, {
10384
10736
  c: c,
10385
10737
  title: "Pages",
10386
10738
  siteName: siteName,
10387
10739
  currentPath: "/dash/pages",
10388
- children: /*#__PURE__*/ jsxDEV(UnifiedPagesContent, {
10389
- navItems: navItems,
10390
- otherPages: otherPages
10740
+ children: /*#__PURE__*/ jsxDEV(PagesContent, {
10741
+ pages: pages
10391
10742
  })
10392
10743
  }));
10393
10744
  });
@@ -10401,85 +10752,6 @@ pagesRoutes.get("/new", async (c)=>{
10401
10752
  children: /*#__PURE__*/ jsxDEV(NewPageContent, {})
10402
10753
  }));
10403
10754
  });
10404
- pagesRoutes.get("/links/new", async (c)=>{
10405
- const siteName = c.var.appConfig.siteName;
10406
- return c.html(/*#__PURE__*/ jsxDEV(DashLayout, {
10407
- c: c,
10408
- title: "New Link",
10409
- siteName: siteName,
10410
- currentPath: "/dash/pages",
10411
- children: /*#__PURE__*/ jsxDEV(LinkFormContent, {})
10412
- }));
10413
- });
10414
- pagesRoutes.post("/links", async (c)=>{
10415
- const i18n = getI18n(c);
10416
- const body = await c.req.json();
10417
- if (!body.label || !body.url) {
10418
- return dsToast(i18n._({
10419
- id: "+AXdXp"
10420
- }), "error");
10421
- }
10422
- await c.var.services.navItems.create({
10423
- type: "link",
10424
- label: body.label,
10425
- url: body.url
10426
- });
10427
- return dsRedirect("/dash/pages");
10428
- });
10429
- pagesRoutes.post("/reorder", async (c)=>{
10430
- const i18n = getI18n(c);
10431
- const body = await c.req.json();
10432
- if (!Array.isArray(body.ids)) {
10433
- return dsToast(i18n._({
10434
- id: "71Xwww"
10435
- }), "error");
10436
- }
10437
- await c.var.services.navItems.reorder(body.ids);
10438
- return dsToast(i18n._({
10439
- id: "1Oj1sI"
10440
- }));
10441
- });
10442
- pagesRoutes.get("/links/:id/edit", async (c)=>{
10443
- const id = parseInt(c.req.param("id"), 10);
10444
- if (isNaN(id)) return c.notFound();
10445
- const item = await c.var.services.navItems.getById(id);
10446
- if (!item) return c.notFound();
10447
- const siteName = c.var.appConfig.siteName;
10448
- return c.html(/*#__PURE__*/ jsxDEV(DashLayout, {
10449
- c: c,
10450
- title: "Edit Link",
10451
- siteName: siteName,
10452
- currentPath: "/dash/pages",
10453
- children: /*#__PURE__*/ jsxDEV(LinkFormContent, {
10454
- item: item,
10455
- isEdit: true
10456
- })
10457
- }));
10458
- });
10459
- pagesRoutes.post("/links/:id", async (c)=>{
10460
- const i18n = getI18n(c);
10461
- const id = parseInt(c.req.param("id"), 10);
10462
- if (isNaN(id)) return c.notFound();
10463
- const body = await c.req.json();
10464
- if (!body.label || !body.url) {
10465
- return dsToast(i18n._({
10466
- id: "+AXdXp"
10467
- }), "error");
10468
- }
10469
- const updated = await c.var.services.navItems.update(id, {
10470
- label: body.label,
10471
- url: body.url
10472
- });
10473
- if (!updated) return c.notFound();
10474
- return dsRedirect("/dash/pages");
10475
- });
10476
- pagesRoutes.post("/links/:id/delete", async (c)=>{
10477
- const id = parseInt(c.req.param("id"), 10);
10478
- if (!isNaN(id)) {
10479
- await c.var.services.navItems.delete(id);
10480
- }
10481
- return dsRedirect("/dash/pages");
10482
- });
10483
10755
  pagesRoutes.post("/", async (c)=>{
10484
10756
  const i18n = getI18n(c);
10485
10757
  const raw = await c.req.json();
@@ -10498,25 +10770,6 @@ pagesRoutes.post("/", async (c)=>{
10498
10770
  });
10499
10771
  return dsRedirect(`/dash/pages/${page.id}`);
10500
10772
  });
10501
- pagesRoutes.post("/:id/add-to-nav", async (c)=>{
10502
- const id = parseInt(c.req.param("id"), 10);
10503
- if (isNaN(id)) return c.notFound();
10504
- const page = await c.var.services.pages.getById(id);
10505
- if (!page) return c.notFound();
10506
- await c.var.services.navItems.create({
10507
- type: "page",
10508
- label: page.title || page.slug,
10509
- url: `/${page.slug}`,
10510
- pageId: page.id
10511
- });
10512
- return dsRedirect("/dash/pages");
10513
- });
10514
- pagesRoutes.post("/:id/remove-from-nav", async (c)=>{
10515
- const pageId = parseInt(c.req.param("id"), 10);
10516
- if (isNaN(pageId)) return c.notFound();
10517
- await c.var.services.navItems.deleteByPageId(pageId);
10518
- return dsRedirect("/dash/pages");
10519
- });
10520
10773
  pagesRoutes.get("/:id", async (c)=>{
10521
10774
  const id = parseInt(c.req.param("id"), 10);
10522
10775
  if (isNaN(id)) return c.notFound();
@@ -11010,58 +11263,10 @@ mediaRoutes.post("/:id/delete", async (c)=>{
11010
11263
  const id = c.req.param("id");
11011
11264
  const media = await c.var.services.media.getById(id);
11012
11265
  if (!media) return c.notFound();
11013
- // Delete from storage
11014
- const storage = c.var.storage;
11015
- if (storage) {
11016
- try {
11017
- await storage.delete(media.storageKey);
11018
- } catch (err) {
11019
- // eslint-disable-next-line no-console -- Error logging is intentional
11020
- console.error("Storage delete error:", err);
11021
- }
11022
- }
11023
- // Delete from database
11024
- await c.var.services.media.delete(id);
11266
+ await c.var.services.media.delete(id, c.var.storage);
11025
11267
  return dsRedirect("/dash/media");
11026
11268
  });
11027
11269
 
11028
- /**
11029
- * Convert an ArrayBuffer to a base64 string.
11030
- *
11031
- * @param buffer - The ArrayBuffer to encode
11032
- * @returns base64-encoded string
11033
- *
11034
- * @example
11035
- * ```ts
11036
- * const b64 = arrayBufferToBase64(await blob.arrayBuffer());
11037
- * ```
11038
- */ function arrayBufferToBase64(buffer) {
11039
- const bytes = new Uint8Array(buffer);
11040
- let binary = "";
11041
- for(let i = 0; i < bytes.byteLength; i++){
11042
- binary += String.fromCharCode(bytes[i]);
11043
- }
11044
- return btoa(binary);
11045
- }
11046
- /**
11047
- * Convert a base64 string to a Uint8Array.
11048
- *
11049
- * @param base64 - The base64 string to decode
11050
- * @returns decoded Uint8Array
11051
- *
11052
- * @example
11053
- * ```ts
11054
- * const bytes = base64ToUint8Array(storedBase64);
11055
- * ```
11056
- */ function base64ToUint8Array(base64) {
11057
- const binary = atob(base64);
11058
- const bytes = new Uint8Array(binary.length);
11059
- for(let i = 0; i < binary.length; i++){
11060
- bytes[i] = binary.charCodeAt(i);
11061
- }
11062
- return bytes;
11063
- }
11064
-
11065
11270
  /**
11066
11271
  * HTML Utilities
11067
11272
  */ /**
@@ -11078,59 +11283,6 @@ mediaRoutes.post("/:id/delete", async (c)=>{
11078
11283
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11079
11284
  }
11080
11285
 
11081
- /** MIME types allowed for upload */ const ALLOWED_UPLOAD_TYPES = [
11082
- "image/jpeg",
11083
- "image/png",
11084
- "image/gif",
11085
- "image/webp",
11086
- "image/svg+xml"
11087
- ];
11088
- /** Maximum file size in bytes (10MB) */ const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
11089
- /**
11090
- * Validates an uploaded file's type and size.
11091
- *
11092
- * @param file - The uploaded File object
11093
- * @returns null if valid, error message string if invalid
11094
- * @example
11095
- * ```ts
11096
- * const error = validateUploadFile(file);
11097
- * if (error) return dsToast(error, "error");
11098
- * ```
11099
- */ function validateUploadFile(file) {
11100
- if (!ALLOWED_UPLOAD_TYPES.includes(file.type)) {
11101
- return "File type not allowed.";
11102
- }
11103
- if (file.size > MAX_UPLOAD_SIZE) {
11104
- return "File too large (max 10MB).";
11105
- }
11106
- return null;
11107
- }
11108
- /**
11109
- * Generates a unique storage key for an uploaded file.
11110
- * Format: `media/YYYY/MM/uuid.ext`
11111
- *
11112
- * @param originalFilename - Original filename to extract extension from
11113
- * @returns Object with generated id, filename, and storageKey
11114
- * @example
11115
- * ```ts
11116
- * const { id, filename, storageKey } = generateStorageKey("photo.jpg");
11117
- * // { id: "0192...", filename: "0192....jpg", storageKey: "media/2025/01/0192....jpg" }
11118
- * ```
11119
- */ function generateStorageKey(originalFilename) {
11120
- const ext = originalFilename.split(".").pop() || "bin";
11121
- const id = uuidv7();
11122
- const date = new Date();
11123
- const year = date.getUTCFullYear();
11124
- const month = String(date.getUTCMonth() + 1).padStart(2, "0");
11125
- const filename = `${id}.${ext}`;
11126
- const storageKey = `media/${year}/${month}/${filename}`;
11127
- return {
11128
- id,
11129
- filename,
11130
- storageKey
11131
- };
11132
- }
11133
-
11134
11286
  function SettingsNav({ currentTab }) {
11135
11287
  const { i18n: $__i18n} = useLingui();
11136
11288
  const tabs = [
@@ -11157,16 +11309,16 @@ function SettingsNav({ currentTab }) {
11157
11309
  }
11158
11310
  ];
11159
11311
  return /*#__PURE__*/ jsxDEV("nav", {
11160
- class: "flex gap-1 mb-6",
11312
+ class: "dash-subnav",
11161
11313
  children: tabs.map((tab)=>/*#__PURE__*/ jsxDEV("a", {
11162
11314
  href: tab.href,
11163
- class: `px-3 py-2 text-sm rounded-md ${tab.id === currentTab ? "bg-accent text-accent-foreground font-medium" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"}`,
11315
+ class: tab.id === currentTab ? "active" : "",
11164
11316
  children: tab.label
11165
11317
  }, tab.id))
11166
11318
  });
11167
11319
  }
11168
11320
 
11169
- function GeneralContent({ siteName, siteDescription, siteLanguage, homeDefaultView, siteNameFallback, siteDescriptionFallback, siteAvatarUrl, showHeaderAvatar, timeZone, siteFooter, noindex, timezones }) {
11321
+ function GeneralContent({ siteName, siteDescription, siteLanguage, siteNameFallback, siteDescriptionFallback, siteAvatarUrl, showHeaderAvatar, timeZone, siteFooter, noindex, timezones }) {
11170
11322
  const { i18n: $__i18n} = useLingui();
11171
11323
  const labels = JSON.stringify({
11172
11324
  blogAvatar: $__i18n._({
@@ -11203,20 +11355,11 @@ function GeneralContent({ siteName, siteDescription, siteLanguage, homeDefaultVi
11203
11355
  id: "anibOb"
11204
11356
  }),
11205
11357
  aboutBlogHelp: $__i18n._({
11206
- id: "bG4pfW"
11358
+ id: "GlEzsR"
11207
11359
  }),
11208
11360
  language: $__i18n._({
11209
11361
  id: "vXIe7J"
11210
11362
  }),
11211
- defaultHomepageView: $__i18n._({
11212
- id: "TNFigk"
11213
- }),
11214
- latest: $__i18n._({
11215
- id: "wL3cK8"
11216
- }),
11217
- featured: $__i18n._({
11218
- id: "FkMol5"
11219
- }),
11220
11363
  timeZone: $__i18n._({
11221
11364
  id: "RxsRD6"
11222
11365
  }),
@@ -11264,19 +11407,12 @@ function GeneralContent({ siteName, siteDescription, siteLanguage, homeDefaultVi
11264
11407
  siteName,
11265
11408
  siteDescription,
11266
11409
  siteLanguage,
11267
- homeDefaultView,
11268
11410
  timeZone,
11269
11411
  siteFooter,
11270
11412
  noindex
11271
11413
  }).replace(/</g, "\\u003c");
11272
11414
  return /*#__PURE__*/ jsxDEV(Fragment, {
11273
11415
  children: [
11274
- /*#__PURE__*/ jsxDEV("h1", {
11275
- class: "text-2xl font-semibold mb-2",
11276
- children: $__i18n._({
11277
- id: "Tz0i8g"
11278
- })
11279
- }),
11280
11416
  /*#__PURE__*/ jsxDEV(SettingsNav, {
11281
11417
  currentTab: "general"
11282
11418
  }),
@@ -11338,12 +11474,6 @@ function AccountContent({ userName }) {
11338
11474
  }).replace(/</g, "\\u003c");
11339
11475
  return /*#__PURE__*/ jsxDEV(Fragment, {
11340
11476
  children: [
11341
- /*#__PURE__*/ jsxDEV("h1", {
11342
- class: "text-2xl font-semibold mb-2",
11343
- children: $__i18n._({
11344
- id: "Tz0i8g"
11345
- })
11346
- }),
11347
11477
  /*#__PURE__*/ jsxDEV(SettingsNav, {
11348
11478
  currentTab: "account"
11349
11479
  }),
@@ -11539,7 +11669,6 @@ settingsRoutes.get("/", async (c)=>{
11539
11669
  siteName: dbSiteName || "",
11540
11670
  siteDescription: dbSiteDescription || "",
11541
11671
  siteLanguage: appConfig.siteLanguage,
11542
- homeDefaultView: appConfig.homeDefaultView,
11543
11672
  siteNameFallback: appConfig.fallbacks.siteName,
11544
11673
  siteDescriptionFallback: appConfig.fallbacks.siteDescription,
11545
11674
  siteAvatarUrl: appConfig.siteAvatarUrl,
@@ -11554,39 +11683,10 @@ settingsRoutes.get("/", async (c)=>{
11554
11683
  settingsRoutes.post("/", async (c)=>{
11555
11684
  const i18n = getI18n(c);
11556
11685
  const body = await c.req.json();
11557
- const { settings } = c.var.services;
11558
- const oldLanguage = c.var.allSettings["SITE_LANGUAGE"] ?? "en";
11559
- if (body.siteName.trim()) {
11560
- await settings.set("SITE_NAME", body.siteName.trim());
11561
- } else {
11562
- await settings.remove("SITE_NAME");
11563
- }
11564
- if (body.siteDescription.trim()) {
11565
- await settings.set("SITE_DESCRIPTION", body.siteDescription.trim());
11566
- } else {
11567
- await settings.remove("SITE_DESCRIPTION");
11568
- }
11569
- // Footer
11570
- if (body.siteFooter?.trim()) {
11571
- await settings.set("SITE_FOOTER", body.siteFooter.trim());
11572
- } else {
11573
- await settings.remove("SITE_FOOTER");
11574
- }
11575
- await settings.set("SITE_LANGUAGE", body.siteLanguage);
11576
- // Save homepage default view (only store if non-default)
11577
- if (body.homeDefaultView === "featured") {
11578
- await settings.set("HOME_DEFAULT_VIEW", body.homeDefaultView);
11579
- } else {
11580
- await settings.remove("HOME_DEFAULT_VIEW");
11581
- }
11582
- // Timezone
11583
- if (body.timeZone && body.timeZone !== "UTC") {
11584
- await settings.set("TIME_ZONE", body.timeZone);
11585
- } else {
11586
- await settings.remove("TIME_ZONE");
11587
- }
11588
- const languageChanged = oldLanguage !== body.siteLanguage;
11589
- const displayName = body.siteName.trim() || c.var.appConfig.fallbacks.siteName;
11686
+ const { languageChanged, displayName } = await c.var.services.settings.updateGeneral(body, {
11687
+ oldLanguage: c.var.allSettings["SITE_LANGUAGE"] ?? "en",
11688
+ fallbackSiteName: c.var.appConfig.fallbacks.siteName
11689
+ });
11590
11690
  // ── JSON response mode (used by Lit settings bridge) ──────────────
11591
11691
  const wantsJson = c.req.header("accept")?.includes("application/json");
11592
11692
  if (wantsJson) {
@@ -11622,7 +11722,6 @@ settingsRoutes.post("/", async (c)=>{
11622
11722
  _orig_siteDescription: body.siteDescription,
11623
11723
  _orig_siteFooter: body.siteFooter,
11624
11724
  _orig_siteLanguage: body.siteLanguage,
11625
- _orig_homeDefaultView: body.homeDefaultView,
11626
11725
  _orig_timeZone: body.timeZone,
11627
11726
  _generalDirty: false
11628
11727
  });
@@ -11679,61 +11778,30 @@ settingsRoutes.post("/avatar", async (c)=>{
11679
11778
  id: "b4VwHs"
11680
11779
  }), "error");
11681
11780
  }
11682
- const uploadError = validateUploadFile(file);
11683
- if (uploadError) {
11684
- return dsToast(uploadError, "error");
11685
- }
11686
- const { id, filename, storageKey } = generateStorageKey(file.name);
11781
+ const faviconFile = formData.get("favicon");
11782
+ const appleTouchFile = formData.get("appleTouch");
11687
11783
  try {
11688
- await storage.put(storageKey, file.stream(), {
11689
- contentType: file.type
11784
+ await c.var.services.settings.uploadAvatar({
11785
+ file,
11786
+ faviconIco: faviconFile ? await faviconFile.arrayBuffer() : undefined,
11787
+ appleTouchIcon: appleTouchFile ? await appleTouchFile.arrayBuffer() : undefined
11788
+ }, {
11789
+ media: c.var.services.media,
11790
+ storage,
11791
+ storageProvider: c.var.appConfig.storageDriver
11690
11792
  });
11691
- await c.var.services.media.create({
11692
- id,
11693
- filename,
11694
- originalName: file.name,
11695
- mimeType: file.type,
11696
- size: file.size,
11697
- storageKey,
11698
- provider: c.var.appConfig.storageDriver
11699
- });
11700
- await c.var.services.settings.set("SITE_AVATAR", storageKey);
11701
- // Store favicon ICO as base64 in settings (tiny file, accessed every page load)
11702
- const faviconFile = formData.get("favicon");
11703
- if (faviconFile) {
11704
- const b64 = arrayBufferToBase64(await faviconFile.arrayBuffer());
11705
- await c.var.services.settings.set("SITE_FAVICON_ICO", b64);
11706
- }
11707
- // Store apple-touch-icon in R2 (180x180 PNG, not tiny enough for base64)
11708
- const appleTouchFile = formData.get("appleTouch");
11709
- if (appleTouchFile) {
11710
- const appleTouchKey = "favicon/apple-touch-icon.png";
11711
- await storage.put(appleTouchKey, new Uint8Array(await appleTouchFile.arrayBuffer()), {
11712
- contentType: "image/png"
11713
- });
11714
- await c.var.services.settings.set("SITE_FAVICON_APPLE_TOUCH", appleTouchKey);
11715
- }
11716
- // Set favicon version for cache-busting
11717
- const now = new Date();
11718
- const version = String(now.getUTCFullYear()) + String(now.getUTCMonth() + 1).padStart(2, "0") + String(now.getUTCDate()).padStart(2, "0") + String(now.getUTCHours()).padStart(2, "0") + String(now.getUTCMinutes()).padStart(2, "0");
11719
- await c.var.services.settings.set("SITE_FAVICON_VERSION", version);
11720
11793
  return dsRedirect("/dash/settings?saved");
11721
- } catch {
11794
+ } catch (e) {
11795
+ if (e instanceof ValidationError) {
11796
+ return dsToast(e.message, "error");
11797
+ }
11722
11798
  return dsToast(i18n._({
11723
11799
  id: "pZq3aX"
11724
11800
  }), "error");
11725
11801
  }
11726
11802
  });
11727
11803
  settingsRoutes.post("/avatar/remove", async (c)=>{
11728
- const storage = c.var.storage;
11729
- const appleTouchKey = c.var.allSettings["SITE_FAVICON_APPLE_TOUCH"];
11730
- if (storage && appleTouchKey) {
11731
- await storage.delete(appleTouchKey);
11732
- }
11733
- await c.var.services.settings.remove("SITE_AVATAR");
11734
- await c.var.services.settings.remove("SITE_FAVICON_ICO");
11735
- await c.var.services.settings.remove("SITE_FAVICON_APPLE_TOUCH");
11736
- await c.var.services.settings.remove("SITE_FAVICON_VERSION");
11804
+ await c.var.services.settings.removeAvatar(c.var.storage);
11737
11805
  // ── JSON response mode (used by Lit settings bridge) ──────────────
11738
11806
  const wantsJson = c.req.header("accept")?.includes("application/json");
11739
11807
  if (wantsJson) {
@@ -11855,17 +11923,16 @@ settingsRoutes.post("/password", async (c)=>{
11855
11923
  });
11856
11924
  });
11857
11925
 
11926
+ const CreateRedirectBody = z.object({
11927
+ fromPath: z.string().min(1),
11928
+ toPath: z.string().min(1),
11929
+ type: RedirectTypeSchema
11930
+ });
11858
11931
  const redirectsRoutes = new Hono();
11859
11932
  function RedirectsListContent({ redirects }) {
11860
11933
  const { i18n: $__i18n} = useLingui();
11861
11934
  return /*#__PURE__*/ jsxDEV(Fragment, {
11862
- children: [
11863
- /*#__PURE__*/ jsxDEV("h1", {
11864
- class: "text-2xl font-semibold mb-2",
11865
- children: $__i18n._({
11866
- id: "Tz0i8g"
11867
- })
11868
- }),
11935
+ children: [
11869
11936
  /*#__PURE__*/ jsxDEV(SettingsNav, {
11870
11937
  currentTab: "redirects"
11871
11938
  }),
@@ -11934,12 +12001,6 @@ function NewRedirectContent() {
11934
12001
  const { i18n: $__i18n} = useLingui();
11935
12002
  return /*#__PURE__*/ jsxDEV(Fragment, {
11936
12003
  children: [
11937
- /*#__PURE__*/ jsxDEV("h1", {
11938
- class: "text-2xl font-semibold mb-2",
11939
- children: $__i18n._({
11940
- id: "Tz0i8g"
11941
- })
11942
- }),
11943
12004
  /*#__PURE__*/ jsxDEV(SettingsNav, {
11944
12005
  currentTab: "redirects"
11945
12006
  }),
@@ -12102,7 +12163,7 @@ redirectsRoutes.get("/new", async (c)=>{
12102
12163
  });
12103
12164
  // Create redirect
12104
12165
  redirectsRoutes.post("/", async (c)=>{
12105
- const body = await c.req.json();
12166
+ const body = parseValidated(CreateRedirectBody, await c.req.json());
12106
12167
  const type = parseInt(body.type, 10);
12107
12168
  await c.var.services.redirects.create(body.fromPath, body.toPath, type);
12108
12169
  return dsRedirect("/dash/settings/redirects");
@@ -12739,12 +12800,14 @@ collectionsRoutes.get("/new", async (c)=>{
12739
12800
  // Create collection
12740
12801
  collectionsRoutes.post("/", async (c)=>{
12741
12802
  const wantsJson = c.req.header("Accept")?.includes("application/json");
12742
- const body = await c.req.json();
12743
- // Auto-generate slug from title if empty
12744
- const slug = body.slug || slugify(body.title);
12803
+ const raw = await c.req.json();
12804
+ const body = parseValidated(CreateCollectionSchema, {
12805
+ ...raw,
12806
+ slug: raw.slug || slugify(raw.title ?? "")
12807
+ });
12745
12808
  const collection = await c.var.services.collections.create({
12746
12809
  title: body.title,
12747
- slug,
12810
+ slug: body.slug,
12748
12811
  description: body.description || undefined,
12749
12812
  icon: body.icon || undefined,
12750
12813
  sortOrder: body.sortOrder || undefined
@@ -12841,7 +12904,7 @@ collectionsRoutes.post("/:id", async (c)=>{
12841
12904
  const id = parseInt(c.req.param("id"), 10);
12842
12905
  if (isNaN(id)) return c.notFound();
12843
12906
  const wantsJson = c.req.header("Accept")?.includes("application/json");
12844
- const body = await c.req.json();
12907
+ const body = parseValidated(UpdateCollectionSchema$1, await c.req.json());
12845
12908
  await c.var.services.collections.update(id, {
12846
12909
  title: body.title,
12847
12910
  slug: body.slug,
@@ -12939,6 +13002,36 @@ collectionsRoutes.post("/:id/delete", async (c)=>{
12939
13002
  };
12940
13003
  }
12941
13004
  const BUILTIN_COLOR_THEMES = [
13005
+ defineTheme({
13006
+ id: "notepad",
13007
+ name: "Notepad",
13008
+ preview: {
13009
+ lightBg: "#fdfce8",
13010
+ lightText: "#333333",
13011
+ lightLink: "#2060b8",
13012
+ darkBg: "#2a291a",
13013
+ darkText: "#d2d2b8",
13014
+ darkLink: "#6695cc"
13015
+ },
13016
+ light: {
13017
+ bg: "oklch(0.985 0.018 95)",
13018
+ fg: "oklch(0.27 0 0)",
13019
+ primary: "oklch(0.5 0.17 260)",
13020
+ primaryFg: "oklch(0.985 0.01 95)",
13021
+ muted: "oklch(0.94 0.022 95)",
13022
+ mutedFg: "oklch(0.52 0 0)",
13023
+ border: "oklch(0.88 0.025 95)"
13024
+ },
13025
+ dark: {
13026
+ bg: "oklch(0.2 0.02 90)",
13027
+ fg: "oklch(0.87 0.015 95)",
13028
+ primary: "oklch(0.65 0.14 260)",
13029
+ primaryFg: "oklch(0.98 0.01 95)",
13030
+ muted: "oklch(0.26 0.018 90)",
13031
+ mutedFg: "oklch(0.62 0.012 95)",
13032
+ border: "oklch(0.32 0.018 90)"
13033
+ }
13034
+ }),
12942
13035
  defineTheme({
12943
13036
  id: "halloween",
12944
13037
  name: "Halloween",
@@ -13073,36 +13166,6 @@ const BUILTIN_COLOR_THEMES = [
13073
13166
  border: "oklch(0.3 0 0)"
13074
13167
  }
13075
13168
  }),
13076
- defineTheme({
13077
- id: "notepad",
13078
- name: "Notepad",
13079
- preview: {
13080
- lightBg: "#fdfce8",
13081
- lightText: "#333333",
13082
- lightLink: "#2060b8",
13083
- darkBg: "#2a291a",
13084
- darkText: "#d2d2b8",
13085
- darkLink: "#6695cc"
13086
- },
13087
- light: {
13088
- bg: "oklch(0.985 0.018 95)",
13089
- fg: "oklch(0.27 0 0)",
13090
- primary: "oklch(0.5 0.17 260)",
13091
- primaryFg: "oklch(0.985 0.01 95)",
13092
- muted: "oklch(0.94 0.022 95)",
13093
- mutedFg: "oklch(0.52 0 0)",
13094
- border: "oklch(0.88 0.025 95)"
13095
- },
13096
- dark: {
13097
- bg: "oklch(0.2 0.02 90)",
13098
- fg: "oklch(0.87 0.015 95)",
13099
- primary: "oklch(0.65 0.14 260)",
13100
- primaryFg: "oklch(0.98 0.01 95)",
13101
- muted: "oklch(0.26 0.018 90)",
13102
- mutedFg: "oklch(0.62 0.012 95)",
13103
- border: "oklch(0.32 0.018 90)"
13104
- }
13105
- }),
13106
13169
  defineTheme({
13107
13170
  id: "sonnet",
13108
13171
  name: "Sonnet",
@@ -13303,12 +13366,19 @@ const BUILTIN_FONT_THEMES = [
13303
13366
  function AppearanceNav({ currentTab }) {
13304
13367
  const { i18n: $__i18n} = useLingui();
13305
13368
  const tabs = [
13369
+ {
13370
+ id: "navigation",
13371
+ label: $__i18n._({
13372
+ id: "UxKoFf"
13373
+ }),
13374
+ href: "/dash/appearance"
13375
+ },
13306
13376
  {
13307
13377
  id: "color",
13308
13378
  label: $__i18n._({
13309
13379
  id: "oKOOsY"
13310
13380
  }),
13311
- href: "/dash/appearance"
13381
+ href: "/dash/appearance/color"
13312
13382
  },
13313
13383
  {
13314
13384
  id: "fonts",
@@ -13326,10 +13396,10 @@ function AppearanceNav({ currentTab }) {
13326
13396
  }
13327
13397
  ];
13328
13398
  return /*#__PURE__*/ jsxDEV("nav", {
13329
- class: "flex gap-1 mb-6",
13399
+ class: "dash-subnav",
13330
13400
  children: tabs.map((tab)=>/*#__PURE__*/ jsxDEV("a", {
13331
13401
  href: tab.href,
13332
- class: `px-3 py-2 text-sm rounded-md ${tab.id === currentTab ? "bg-accent text-accent-foreground font-medium" : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"}`,
13402
+ class: tab.id === currentTab ? "active" : "",
13333
13403
  children: tab.label
13334
13404
  }, tab.id))
13335
13405
  });
@@ -13416,18 +13486,12 @@ function ColorThemeContent({ themes, currentThemeId }) {
13416
13486
  }).replace(/</g, "\\u003c");
13417
13487
  return /*#__PURE__*/ jsxDEV(Fragment, {
13418
13488
  children: [
13419
- /*#__PURE__*/ jsxDEV("h1", {
13420
- class: "text-2xl font-semibold mb-2",
13421
- children: $__i18n._({
13422
- id: "aAIQg2"
13423
- })
13424
- }),
13425
13489
  /*#__PURE__*/ jsxDEV(AppearanceNav, {
13426
13490
  currentTab: "color"
13427
13491
  }),
13428
13492
  /*#__PURE__*/ jsxDEV("div", {
13429
13493
  "data-signals": themeSignals,
13430
- "data-on:change": "@post('/dash/appearance')",
13494
+ "data-on:change": "@post('/dash/appearance/color')",
13431
13495
  class: "max-w-3xl",
13432
13496
  children: /*#__PURE__*/ jsxDEV("fieldset", {
13433
13497
  children: [
@@ -13461,12 +13525,6 @@ function FontThemeContent({ fontThemes, currentFontThemeId }) {
13461
13525
  const { i18n: $__i18n} = useLingui();
13462
13526
  return /*#__PURE__*/ jsxDEV(Fragment, {
13463
13527
  children: [
13464
- /*#__PURE__*/ jsxDEV("h1", {
13465
- class: "text-2xl font-semibold mb-2",
13466
- children: $__i18n._({
13467
- id: "aAIQg2"
13468
- })
13469
- }),
13470
13528
  /*#__PURE__*/ jsxDEV(AppearanceNav, {
13471
13529
  currentTab: "fonts"
13472
13530
  }),
@@ -13546,6 +13604,264 @@ function FontThemeContent({ fontThemes, currentFontThemeId }) {
13546
13604
  });
13547
13605
  }
13548
13606
 
13607
+ // =============================================================================
13608
+ // System descriptions (used to build the config passed to the Lit component)
13609
+ // =============================================================================
13610
+ const SYSTEM_DESCRIPTIONS = {
13611
+ rss: "Add a link to your RSS feed",
13612
+ dashboard: "Shows 'Dashboard' when logged in, 'Sign in' when logged out",
13613
+ collections: "Link to your collections page",
13614
+ archive: "Link to the post archive"
13615
+ };
13616
+ // =============================================================================
13617
+ // Main component
13618
+ // =============================================================================
13619
+ function NavigationContent({ navItems, availablePages, headerNavMaxVisible, homeDefaultView, siteName }) {
13620
+ const { i18n: $__i18n} = useLingui();
13621
+ // Serialize nav items for the Lit component
13622
+ const itemsData = navItems.map((item)=>({
13623
+ id: item.id,
13624
+ type: item.type,
13625
+ label: item.label,
13626
+ url: item.url,
13627
+ pageId: item.pageId
13628
+ }));
13629
+ // Build system nav config array for the Lit component
13630
+ const systemNavData = Object.keys(SYSTEM_NAV_KEYS).map((key)=>({
13631
+ key,
13632
+ defaultLabel: SYSTEM_NAV_KEYS[key].defaultLabel,
13633
+ url: SYSTEM_NAV_KEYS[key].url,
13634
+ description: SYSTEM_DESCRIPTIONS[key]
13635
+ }));
13636
+ // Serialize available pages for the Lit component
13637
+ const pagesData = availablePages.map((page)=>({
13638
+ id: page.id,
13639
+ title: page.title,
13640
+ slug: page.slug
13641
+ }));
13642
+ const labels = {
13643
+ preview: $__i18n._({
13644
+ id: "rdUucN"
13645
+ }),
13646
+ navigationItems: $__i18n._({
13647
+ id: "Vn3jYy"
13648
+ }),
13649
+ emptyState: $__i18n._({
13650
+ id: "538Vy5"
13651
+ }),
13652
+ page: $__i18n._({
13653
+ id: "MnbH31"
13654
+ }),
13655
+ link: $__i18n._({
13656
+ id: "LdyooL"
13657
+ }),
13658
+ system: $__i18n._({
13659
+ id: "iEUzMn"
13660
+ }),
13661
+ toggleEdit: $__i18n._({
13662
+ id: "M6CbAU"
13663
+ }),
13664
+ label: $__i18n._({
13665
+ id: "87a/t/"
13666
+ }),
13667
+ url: $__i18n._({
13668
+ id: "IagCbF"
13669
+ }),
13670
+ save: $__i18n._({
13671
+ id: "tfDRzk"
13672
+ }),
13673
+ delete: $__i18n._({
13674
+ id: "cnGeoo"
13675
+ }),
13676
+ editPage: $__i18n._({
13677
+ id: "U5v6Gh"
13678
+ }),
13679
+ remove: $__i18n._({
13680
+ id: "t/YqKh"
13681
+ }),
13682
+ orderSaved: $__i18n._({
13683
+ id: "1Oj1sI"
13684
+ }),
13685
+ labelRequired: $__i18n._({
13686
+ id: "w8Rv8T"
13687
+ }),
13688
+ saveFailed: $__i18n._({
13689
+ id: "4JBD+x"
13690
+ }),
13691
+ deleteFailed: $__i18n._({
13692
+ id: "bDqhXY"
13693
+ }),
13694
+ systemLinks: $__i18n._({
13695
+ id: "oSiRP0"
13696
+ }),
13697
+ systemLinksDescription: $__i18n._({
13698
+ id: "sDGoxy"
13699
+ }),
13700
+ addPageToNavigation: $__i18n._({
13701
+ id: "b+FyBD"
13702
+ }),
13703
+ addCustomLinkToNavigation: $__i18n._({
13704
+ id: "ZmUkwN"
13705
+ }),
13706
+ choosePage: $__i18n._({
13707
+ id: "DVljCN"
13708
+ }),
13709
+ searchPages: $__i18n._({
13710
+ id: "pI2MWS"
13711
+ }),
13712
+ noPagesFound: $__i18n._({
13713
+ id: "tfQNeI"
13714
+ }),
13715
+ addLink: $__i18n._({
13716
+ id: "V4WsyL"
13717
+ }),
13718
+ addLinkDescription: $__i18n._({
13719
+ id: "J6bLeg"
13720
+ }),
13721
+ allPagesInNav: $__i18n._({
13722
+ id: "mO5HMZ"
13723
+ }),
13724
+ urlPlaceholder: "/archive or https://...",
13725
+ maxVisibleLinks: $__i18n._({
13726
+ id: "b+JhJf"
13727
+ }),
13728
+ maxVisibleSaved: $__i18n._({
13729
+ id: "PJnyHS"
13730
+ }),
13731
+ useFeaturedAsDefault: $__i18n._({
13732
+ id: "Az4JB1"
13733
+ }),
13734
+ homeViewSaved: $__i18n._({
13735
+ id: "UzGRD9"
13736
+ }),
13737
+ latest: $__i18n._({
13738
+ id: "wL3cK8"
13739
+ }),
13740
+ featured: $__i18n._({
13741
+ id: "FkMol5"
13742
+ }),
13743
+ labelAndUrlRequired: $__i18n._({
13744
+ id: "+AXdXp"
13745
+ })
13746
+ };
13747
+ const escapeJson = (data)=>JSON.stringify(data).replace(/</g, "\\u003c");
13748
+ return /*#__PURE__*/ jsxDEV(Fragment, {
13749
+ children: [
13750
+ /*#__PURE__*/ jsxDEV(AppearanceNav, {
13751
+ currentTab: "navigation"
13752
+ }),
13753
+ /*#__PURE__*/ jsxDEV("div", {
13754
+ class: "max-w-3xl flex flex-col gap-8",
13755
+ children: /*#__PURE__*/ jsxDEV("jant-nav-manager", {
13756
+ items: escapeJson(itemsData),
13757
+ labels: escapeJson(labels),
13758
+ "system-nav-items": escapeJson(systemNavData),
13759
+ "available-pages": escapeJson(pagesData),
13760
+ "site-name": siteName,
13761
+ "max-visible": headerNavMaxVisible,
13762
+ "home-default-view": homeDefaultView,
13763
+ children: /*#__PURE__*/ jsxDEV("div", {
13764
+ class: "border rounded-lg",
13765
+ children: [
13766
+ /*#__PURE__*/ jsxDEV("p", {
13767
+ class: "text-xs text-muted-foreground px-4 pt-3",
13768
+ children: $__i18n._({
13769
+ id: "rdUucN"
13770
+ })
13771
+ }),
13772
+ /*#__PURE__*/ jsxDEV("div", {
13773
+ class: "px-5 py-3",
13774
+ children: [
13775
+ /*#__PURE__*/ jsxDEV("div", {
13776
+ class: "site-header-top",
13777
+ children: [
13778
+ /*#__PURE__*/ jsxDEV("a", {
13779
+ href: "/",
13780
+ class: "site-logo",
13781
+ children: siteName
13782
+ }),
13783
+ /*#__PURE__*/ jsxDEV("div", {
13784
+ class: "site-header-right",
13785
+ children: [
13786
+ navItems.length > 0 && /*#__PURE__*/ jsxDEV("nav", {
13787
+ class: "site-header-nav",
13788
+ children: [
13789
+ navItems.slice(0, headerNavMaxVisible).map((item)=>/*#__PURE__*/ jsxDEV("a", {
13790
+ href: item.url,
13791
+ class: "site-header-link",
13792
+ children: item.label
13793
+ }, item.id)),
13794
+ navItems.length > headerNavMaxVisible && /*#__PURE__*/ jsxDEV("span", {
13795
+ class: "text-muted-foreground",
13796
+ children: "…"
13797
+ })
13798
+ ]
13799
+ }),
13800
+ /*#__PURE__*/ jsxDEV("span", {
13801
+ class: "site-header-search",
13802
+ "aria-hidden": "true",
13803
+ children: /*#__PURE__*/ jsxDEV("svg", {
13804
+ xmlns: "http://www.w3.org/2000/svg",
13805
+ width: "16",
13806
+ height: "16",
13807
+ viewBox: "0 0 24 24",
13808
+ fill: "none",
13809
+ stroke: "currentColor",
13810
+ "stroke-width": "2",
13811
+ "stroke-linecap": "round",
13812
+ "stroke-linejoin": "round",
13813
+ children: [
13814
+ /*#__PURE__*/ jsxDEV("circle", {
13815
+ cx: "11",
13816
+ cy: "11",
13817
+ r: "8"
13818
+ }),
13819
+ /*#__PURE__*/ jsxDEV("path", {
13820
+ d: "m21 21-4.35-4.35"
13821
+ })
13822
+ ]
13823
+ })
13824
+ })
13825
+ ]
13826
+ })
13827
+ ]
13828
+ }),
13829
+ /*#__PURE__*/ jsxDEV("nav", {
13830
+ class: "site-browse-nav",
13831
+ children: [
13832
+ /*#__PURE__*/ jsxDEV("span", {
13833
+ class: "site-browse-link site-browse-link-active",
13834
+ children: homeDefaultView === "featured" ? $__i18n._({
13835
+ id: "FkMol5"
13836
+ }) : $__i18n._({
13837
+ id: "wL3cK8"
13838
+ })
13839
+ }),
13840
+ /*#__PURE__*/ jsxDEV("span", {
13841
+ class: "site-browse-sep",
13842
+ "aria-hidden": "true",
13843
+ children: "/"
13844
+ }),
13845
+ /*#__PURE__*/ jsxDEV("span", {
13846
+ class: "site-browse-link",
13847
+ children: homeDefaultView === "featured" ? $__i18n._({
13848
+ id: "wL3cK8"
13849
+ }) : $__i18n._({
13850
+ id: "FkMol5"
13851
+ })
13852
+ })
13853
+ ]
13854
+ })
13855
+ ]
13856
+ })
13857
+ ]
13858
+ })
13859
+ })
13860
+ })
13861
+ ]
13862
+ });
13863
+ }
13864
+
13549
13865
  function AdvancedContent({ customCSS }) {
13550
13866
  const { i18n: $__i18n} = useLingui();
13551
13867
  const cssSignals = JSON.stringify({
@@ -13553,12 +13869,6 @@ function AdvancedContent({ customCSS }) {
13553
13869
  }).replace(/</g, "\\u003c");
13554
13870
  return /*#__PURE__*/ jsxDEV(Fragment, {
13555
13871
  children: [
13556
- /*#__PURE__*/ jsxDEV("h1", {
13557
- class: "text-2xl font-semibold mb-2",
13558
- children: $__i18n._({
13559
- id: "aAIQg2"
13560
- })
13561
- }),
13562
13872
  /*#__PURE__*/ jsxDEV(AppearanceNav, {
13563
13873
  currentTab: "advanced"
13564
13874
  }),
@@ -13627,9 +13937,65 @@ function AdvancedContent({ customCSS }) {
13627
13937
 
13628
13938
  const appearanceRoutes = new Hono();
13629
13939
  // ===========================================================================
13630
- // Color Theme
13940
+ // Navigation (default tab)
13631
13941
  // ===========================================================================
13632
13942
  appearanceRoutes.get("/", async (c)=>{
13943
+ const [navItems, availablePages] = await Promise.all([
13944
+ c.var.services.navItems.list(),
13945
+ c.var.services.pages.listNotInNav()
13946
+ ]);
13947
+ const siteName = c.var.appConfig.siteName;
13948
+ const headerNavMaxVisible = c.var.appConfig.headerNavMaxVisible;
13949
+ const homeDefaultView = c.var.appConfig.homeDefaultView;
13950
+ return c.html(/*#__PURE__*/ jsxDEV(DashLayout, {
13951
+ c: c,
13952
+ title: "Appearance",
13953
+ siteName: siteName,
13954
+ currentPath: "/dash/appearance",
13955
+ children: /*#__PURE__*/ jsxDEV(NavigationContent, {
13956
+ navItems: navItems,
13957
+ availablePages: availablePages,
13958
+ headerNavMaxVisible: headerNavMaxVisible,
13959
+ homeDefaultView: homeDefaultView,
13960
+ siteName: siteName
13961
+ })
13962
+ }));
13963
+ });
13964
+ // ===========================================================================
13965
+ // Nav max visible links
13966
+ // ===========================================================================
13967
+ appearanceRoutes.post("/nav-max-visible", async (c)=>{
13968
+ const body = await c.req.json();
13969
+ const { settings } = c.var.services;
13970
+ const navMax = Math.max(0, Math.min(5, body.value ?? 3));
13971
+ if (navMax !== 3) {
13972
+ await settings.set("HEADER_NAV_MAX_VISIBLE", String(navMax));
13973
+ } else {
13974
+ await settings.remove("HEADER_NAV_MAX_VISIBLE");
13975
+ }
13976
+ return c.json({
13977
+ ok: true
13978
+ });
13979
+ });
13980
+ // ===========================================================================
13981
+ // Home default view
13982
+ // ===========================================================================
13983
+ appearanceRoutes.post("/home-default-view", async (c)=>{
13984
+ const body = await c.req.json();
13985
+ const { settings } = c.var.services;
13986
+ if (body.value === "featured") {
13987
+ await settings.set("HOME_DEFAULT_VIEW", "featured");
13988
+ } else {
13989
+ await settings.remove("HOME_DEFAULT_VIEW");
13990
+ }
13991
+ return c.json({
13992
+ ok: true
13993
+ });
13994
+ });
13995
+ // ===========================================================================
13996
+ // Color Theme
13997
+ // ===========================================================================
13998
+ appearanceRoutes.get("/color", async (c)=>{
13633
13999
  const siteName = c.var.appConfig.siteName;
13634
14000
  const defaultThemeId = c.var.appConfig.fallbacks.defaultTheme;
13635
14001
  const currentThemeId = c.var.allSettings[SETTINGS_KEYS.THEME] ?? defaultThemeId;
@@ -13649,7 +14015,7 @@ appearanceRoutes.get("/", async (c)=>{
13649
14015
  })
13650
14016
  }));
13651
14017
  });
13652
- appearanceRoutes.post("/", async (c)=>{
14018
+ appearanceRoutes.post("/color", async (c)=>{
13653
14019
  const i18n = getI18n(c);
13654
14020
  const body = await c.req.json();
13655
14021
  const { settings } = c.var.services;
@@ -13666,7 +14032,7 @@ appearanceRoutes.post("/", async (c)=>{
13666
14032
  } else {
13667
14033
  await settings.set(SETTINGS_KEYS.THEME, validTheme.id);
13668
14034
  }
13669
- return dsRedirect("/dash/appearance?saved");
14035
+ return dsRedirect("/dash/appearance/color?saved");
13670
14036
  });
13671
14037
  // ===========================================================================
13672
14038
  // Font Theme
@@ -13800,18 +14166,6 @@ const postsApiRoutes = new Hono();
13800
14166
  mimeType: m.mimeType
13801
14167
  };
13802
14168
  }
13803
- /**
13804
- * Validates media IDs: checks count limit and verifies all IDs exist.
13805
- */ async function validateMediaIds(mediaIds, getByIds) {
13806
- const countError = validateMediaCount(mediaIds);
13807
- if (countError) throw new ValidationError(countError);
13808
- if (mediaIds.length > 0) {
13809
- const existing = await getByIds(mediaIds);
13810
- if (existing.length !== mediaIds.length) {
13811
- throw new ValidationError("One or more media IDs are invalid");
13812
- }
13813
- }
13814
- }
13815
14169
  // List posts
13816
14170
  postsApiRoutes.get("/", async (c)=>{
13817
14171
  const format = c.req.query("format");
@@ -13855,7 +14209,7 @@ postsApiRoutes.post("/", requireAuthApi(), async (c)=>{
13855
14209
  const body = parseValidated(CreatePostSchema, await c.req.json());
13856
14210
  // Validate media IDs
13857
14211
  if (body.mediaIds) {
13858
- await validateMediaIds(body.mediaIds, (ids)=>c.var.services.media.getByIds(ids));
14212
+ await c.var.services.media.validateIds(body.mediaIds);
13859
14213
  }
13860
14214
  const post = await c.var.services.posts.create({
13861
14215
  format: body.format,
@@ -13891,7 +14245,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
13891
14245
  const body = parseValidated(UpdatePostSchema, await c.req.json());
13892
14246
  // Validate media IDs if provided
13893
14247
  if (body.mediaIds !== undefined) {
13894
- await validateMediaIds(body.mediaIds, (ids)=>c.var.services.media.getByIds(ids));
14248
+ await c.var.services.media.validateIds(body.mediaIds);
13895
14249
  }
13896
14250
  const post = assertFound(await c.var.services.posts.update(id, {
13897
14251
  format: body.format,
@@ -13923,9 +14277,10 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c)=>{
13923
14277
  postsApiRoutes.delete("/:id", requireAuthApi(), async (c)=>{
13924
14278
  const id = decode(c.req.param("id"));
13925
14279
  if (!id) throw new ValidationError("Invalid ID");
13926
- // Detach media before deleting
13927
- await c.var.services.media.detachFromPost(id);
13928
- const success = await c.var.services.posts.delete(id);
14280
+ const success = await c.var.services.posts.delete(id, {
14281
+ media: c.var.services.media,
14282
+ storage: c.var.storage
14283
+ });
13929
14284
  if (!success) throw new NotFoundError("Post");
13930
14285
  return c.json({
13931
14286
  success: true
@@ -14378,19 +14733,8 @@ uploadApiRoutes.get("/", async (c)=>{
14378
14733
  // Delete a file
14379
14734
  uploadApiRoutes.delete("/:id", async (c)=>{
14380
14735
  const id = c.req.param("id");
14381
- const media = assertFound(await c.var.services.media.getById(id), "Media");
14382
- // Delete from storage
14383
- const storage = c.var.storage;
14384
- if (storage) {
14385
- try {
14386
- await storage.delete(media.storageKey);
14387
- } catch (err) {
14388
- // eslint-disable-next-line no-console -- Error logging is intentional
14389
- console.error("Storage delete error:", err);
14390
- }
14391
- }
14392
- // Delete from database
14393
- await c.var.services.media.delete(id);
14736
+ assertFound(await c.var.services.media.getById(id), "Media");
14737
+ await c.var.services.media.delete(id, c.var.storage);
14394
14738
  return c.json({
14395
14739
  success: true
14396
14740
  });
@@ -14498,11 +14842,21 @@ composeRoutes.post("/", async (c)=>{
14498
14842
  return dsToast(firstError, "error");
14499
14843
  }
14500
14844
  const data = result.data;
14501
- // Validate media count
14845
+ // Validate media IDs
14502
14846
  if (data.mediaIds) {
14503
- const mediaError = validateMediaCount(data.mediaIds);
14504
- if (mediaError) {
14505
- return dsToast(mediaError, "error");
14847
+ try {
14848
+ await c.var.services.media.validateIds(data.mediaIds);
14849
+ } catch (e) {
14850
+ if (e instanceof ValidationError) {
14851
+ if (wantsJson) {
14852
+ return c.json({
14853
+ status: "error",
14854
+ error: e.message
14855
+ }, 422);
14856
+ }
14857
+ return dsToast(e.message, "error");
14858
+ }
14859
+ throw e;
14506
14860
  }
14507
14861
  }
14508
14862
  const post = await c.var.services.posts.create({
@@ -14734,6 +15088,22 @@ const errorHandler = (err, c)=>{
14734
15088
  console.error("[Jant] Unhandled error:", err);
14735
15089
  return dsToast("An unexpected error occurred", "error");
14736
15090
  }
15091
+ // JSON-accepting requests (Lit bridges)
15092
+ if (c.req.header("accept")?.includes("application/json")) {
15093
+ if (err instanceof DomainError) {
15094
+ const body = {
15095
+ error: err.message,
15096
+ code: err.code
15097
+ };
15098
+ if (err instanceof ValidationError && err.details) body.details = err.details;
15099
+ return c.json(body, err.statusCode);
15100
+ }
15101
+ // eslint-disable-next-line no-console -- Server error logging is intentional
15102
+ console.error("[Jant] Unhandled error:", err);
15103
+ return c.json({
15104
+ error: "Internal server error"
15105
+ }, 500);
15106
+ }
14737
15107
  // Non-API routes: map NotFoundError to Hono's built-in 404
14738
15108
  if (err instanceof NotFoundError) {
14739
15109
  return c.notFound();
@@ -14813,6 +15183,10 @@ const errorHandler = (err, c)=>{
14813
15183
  siteDescriptionExplicit,
14814
15184
  siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
14815
15185
  homeDefaultView: resolve("HOME_DEFAULT_VIEW", allSettings, env),
15186
+ headerNavMaxVisible: (()=>{
15187
+ const parsed = parseInt(resolve("HEADER_NAV_MAX_VISIBLE", allSettings, env), 10);
15188
+ return Math.max(0, Math.min(5, isNaN(parsed) ? 3 : parsed));
15189
+ })(),
14816
15190
  timeZone: resolve("TIME_ZONE", allSettings, env),
14817
15191
  siteFooter: resolve("SITE_FOOTER", allSettings, env),
14818
15192
  noindex: resolve("NOINDEX", allSettings, env) === "true",
@@ -15201,7 +15575,7 @@ const errorHandler = (err, c)=>{
15201
15575
  app.route("/archive", archiveRoutes);
15202
15576
  app.route("/featured", featuredRoutes);
15203
15577
  app.route("/latest", latestRoutes);
15204
- app.route("/collections", collectionsPageRoutes);
15578
+ app.route("/c", collectionsPageRoutes);
15205
15579
  app.route("/c", collectionRoutes);
15206
15580
  app.route("/p", postRoutes);
15207
15581
  app.route("/", homeRoutes);