@jant/core 0.3.31 → 0.3.33
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.
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +1442 -989
- package/dist/index.js +1429 -1055
- package/package.json +2 -2
- package/src/__tests__/helpers/app.ts +6 -3
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/client.ts +2 -1
- package/src/db/migrations/0011_add_path_registry.sql +23 -0
- package/src/db/schema.ts +12 -1
- package/src/i18n/locales/en.po +225 -91
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +201 -152
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +201 -152
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/excerpt.test.ts +25 -0
- package/src/lib/__tests__/resolve-config.test.ts +26 -2
- package/src/lib/__tests__/timeline.test.ts +2 -1
- package/src/lib/compose-bridge.ts +30 -1
- package/src/lib/excerpt.ts +16 -7
- package/src/lib/nav-manager-bridge.ts +54 -0
- package/src/lib/navigation.ts +7 -4
- package/src/lib/render.tsx +5 -2
- package/src/lib/resolve-config.ts +7 -0
- package/src/lib/view.ts +42 -10
- package/src/middleware/error-handler.ts +16 -0
- package/src/routes/api/__tests__/posts.test.ts +80 -0
- package/src/routes/api/__tests__/settings.test.ts +1 -1
- package/src/routes/api/posts.ts +6 -29
- package/src/routes/api/upload.ts +2 -14
- package/src/routes/auth/__tests__/setup.test.ts +2 -1
- package/src/routes/compose.tsx +13 -5
- package/src/routes/dash/__tests__/pages.test.ts +2 -1
- package/src/routes/dash/__tests__/settings-avatar.test.ts +151 -33
- package/src/routes/dash/appearance.tsx +71 -4
- package/src/routes/dash/collections.tsx +15 -21
- package/src/routes/dash/media.tsx +1 -13
- package/src/routes/dash/pages.tsx +5 -150
- package/src/routes/dash/posts.tsx +25 -32
- package/src/routes/dash/redirects.tsx +9 -11
- package/src/routes/dash/settings.tsx +29 -111
- package/src/routes/feed/__tests__/rss.test.ts +5 -1
- package/src/routes/pages/__tests__/collections.test.ts +2 -1
- package/src/routes/pages/__tests__/featured.test.ts +2 -1
- package/src/routes/pages/page.tsx +20 -25
- package/src/services/__tests__/collection.test.ts +2 -1
- package/src/services/__tests__/media.test.ts +78 -1
- package/src/services/__tests__/navigation.test.ts +2 -1
- package/src/services/__tests__/page.test.ts +78 -1
- package/src/services/__tests__/path-registry.test.ts +165 -0
- package/src/services/__tests__/post-timeline.test.ts +2 -1
- package/src/services/__tests__/post.test.ts +103 -1
- package/src/services/__tests__/redirect.test.ts +53 -4
- package/src/services/__tests__/search.test.ts +2 -1
- package/src/services/__tests__/settings.test.ts +153 -0
- package/src/services/index.ts +12 -4
- package/src/services/media.ts +72 -4
- package/src/services/page.ts +64 -17
- package/src/services/path-registry.ts +160 -0
- package/src/services/post.ts +119 -24
- package/src/services/redirect.ts +23 -3
- package/src/services/settings.ts +181 -0
- package/src/styles/components.css +135 -0
- package/src/styles/tokens.css +6 -1
- package/src/styles/ui.css +70 -26
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +7 -2
- package/src/types/constants.ts +9 -1
- package/src/types/sortablejs.d.ts +8 -2
- package/src/types/views.ts +1 -1
- package/src/ui/color-themes.ts +31 -31
- package/src/ui/components/__tests__/jant-settings-avatar.test.ts +0 -3
- package/src/ui/components/__tests__/jant-settings-general.test.ts +2 -6
- package/src/ui/components/jant-compose-dialog.ts +3 -2
- package/src/ui/components/jant-compose-editor.ts +17 -2
- package/src/ui/components/jant-nav-manager.ts +1067 -0
- package/src/ui/components/jant-settings-general.ts +2 -35
- package/src/ui/components/nav-manager-types.ts +72 -0
- package/src/ui/components/settings-types.ts +0 -3
- package/src/ui/compose/ComposePrompt.tsx +3 -11
- package/src/ui/dash/appearance/AdvancedContent.tsx +0 -3
- package/src/ui/dash/appearance/AppearanceNav.tsx +12 -8
- package/src/ui/dash/appearance/ColorThemeContent.tsx +1 -4
- package/src/ui/dash/appearance/FontThemeContent.tsx +0 -3
- package/src/ui/dash/appearance/NavigationContent.tsx +302 -0
- package/src/ui/dash/pages/PagesContent.tsx +74 -0
- package/src/ui/dash/settings/AccountContent.tsx +0 -3
- package/src/ui/dash/settings/GeneralContent.tsx +1 -19
- package/src/ui/dash/settings/SettingsNav.tsx +2 -6
- package/src/ui/feed/NoteCard.tsx +2 -2
- package/src/ui/layouts/DashLayout.tsx +83 -86
- package/src/ui/layouts/SiteLayout.tsx +82 -21
- package/src/lib/nav-reorder.ts +0 -26
- package/src/ui/dash/pages/LinkFormContent.tsx +0 -119
- 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: "/collections"
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
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
|
|
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
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
3488
|
-
|
|
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
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
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\":[\"
|
|
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.
|
|
5155
|
+
const CORE_VERSION = "0.3.33";
|
|
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
|
*
|
|
@@ -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
|
|
6358
|
-
* plain-text length exceeds 500 characters
|
|
6359
|
-
*
|
|
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:
|
|
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
|
-
|
|
6556
|
-
|
|
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 (
|
|
7032
|
+
if (url === "/") {
|
|
6560
7033
|
isActive = currentPath === "/";
|
|
6561
7034
|
} else {
|
|
6562
|
-
isActive = currentPath ===
|
|
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
|
|
6569
|
-
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
|
-
|
|
6578
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
d: "
|
|
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
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
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,
|
|
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
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
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
|
-
|
|
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("
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
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("
|
|
7073
|
-
|
|
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
|
-
|
|
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("
|
|
7115
|
-
class: "site-
|
|
7116
|
-
children:
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
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:
|
|
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: "
|
|
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
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
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
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
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
|
|
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: "
|
|
9448
|
+
class: "dash-header",
|
|
8911
9449
|
children: /*#__PURE__*/ jsxDEV("div", {
|
|
8912
|
-
class: "container
|
|
9450
|
+
class: "container dash-header-inner",
|
|
8913
9451
|
children: [
|
|
8914
|
-
/*#__PURE__*/ jsxDEV("
|
|
8915
|
-
|
|
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
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
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: "/
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
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.
|
|
9986
|
-
|
|
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
|
|
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("
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
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("
|
|
10181
|
-
"
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
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
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
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("
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
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-
|
|
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
|
-
|
|
10243
|
-
|
|
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
|
|
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(
|
|
10389
|
-
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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: "
|
|
11312
|
+
class: "dash-subnav",
|
|
11161
11313
|
children: tabs.map((tab)=>/*#__PURE__*/ jsxDEV("a", {
|
|
11162
11314
|
href: tab.href,
|
|
11163
|
-
class:
|
|
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,
|
|
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: "
|
|
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 {
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
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
|
|
11683
|
-
|
|
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
|
|
11689
|
-
|
|
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
|
-
|
|
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
|
|
12743
|
-
|
|
12744
|
-
|
|
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: "
|
|
13399
|
+
class: "dash-subnav",
|
|
13330
13400
|
children: tabs.map((tab)=>/*#__PURE__*/ jsxDEV("a", {
|
|
13331
13401
|
href: tab.href,
|
|
13332
|
-
class:
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
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
|
-
|
|
14382
|
-
|
|
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
|
|
14845
|
+
// Validate media IDs
|
|
14502
14846
|
if (data.mediaIds) {
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
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",
|