create-starlight-cms 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -0
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/bin/index.js +147 -0
- package/package.json +46 -0
- package/template/.nvmrc +1 -0
- package/template/LICENSE +21 -0
- package/template/README.md +68 -0
- package/template/THIRD_PARTY_NOTICES.md +12 -0
- package/template/_gitignore +14 -0
- package/template/astro.config.mjs +26 -0
- package/template/migrations/0001_schema.sql +73 -0
- package/template/migrations/0002_publish_delivery.sql +16 -0
- package/template/package-lock.json.template +9123 -0
- package/template/package.json +89 -0
- package/template/scripts/build-admin.mjs +38 -0
- package/template/scripts/check-linux-bindings.mjs +18 -0
- package/template/scripts/dev.mjs +94 -0
- package/template/src/admin/admin.scss +182 -0
- package/template/src/admin/app.ts +130 -0
- package/template/src/admin/client.tsx +684 -0
- package/template/src/admin/components/tiptap-extension/node-background-extension.ts +150 -0
- package/template/src/admin/components/tiptap-icons/align-center-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/align-justify-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/align-left-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/align-right-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/arrow-left-icon.tsx +24 -0
- package/template/src/admin/components/tiptap-icons/arrow-right-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/ban-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/blockquote-icon.tsx +44 -0
- package/template/src/admin/components/tiptap-icons/bold-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/case-sensitive-icon.tsx +32 -0
- package/template/src/admin/components/tiptap-icons/check-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/chevron-down-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/chevron-up-icon.tsx +22 -0
- package/template/src/admin/components/tiptap-icons/close-icon.tsx +24 -0
- package/template/src/admin/components/tiptap-icons/code-block-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/code2-icon.tsx +32 -0
- package/template/src/admin/components/tiptap-icons/corner-down-left-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/external-link-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/heading-five-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/heading-four-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/heading-icon.tsx +24 -0
- package/template/src/admin/components/tiptap-icons/heading-one-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/heading-six-icon.tsx +30 -0
- package/template/src/admin/components/tiptap-icons/heading-three-icon.tsx +36 -0
- package/template/src/admin/components/tiptap-icons/heading-two-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/highlighter-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/image-plus-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/italic-icon.tsx +24 -0
- package/template/src/admin/components/tiptap-icons/link-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/list-icon.tsx +56 -0
- package/template/src/admin/components/tiptap-icons/list-ordered-icon.tsx +56 -0
- package/template/src/admin/components/tiptap-icons/list-todo-icon.tsx +50 -0
- package/template/src/admin/components/tiptap-icons/moon-star-icon.tsx +30 -0
- package/template/src/admin/components/tiptap-icons/redo2-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/search-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/strike-icon.tsx +28 -0
- package/template/src/admin/components/tiptap-icons/subscript-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/sun-icon.tsx +58 -0
- package/template/src/admin/components/tiptap-icons/superscript-icon.tsx +38 -0
- package/template/src/admin/components/tiptap-icons/trash-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/underline-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/undo2-icon.tsx +26 -0
- package/template/src/admin/components/tiptap-icons/whole-word-icon.tsx +36 -0
- package/template/src/admin/components/tiptap-node/blockquote-node/blockquote-node.scss +37 -0
- package/template/src/admin/components/tiptap-node/code-block-node/code-block-node.scss +54 -0
- package/template/src/admin/components/tiptap-node/heading-node/heading-node.scss +45 -0
- package/template/src/admin/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension.ts +14 -0
- package/template/src/admin/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss +25 -0
- package/template/src/admin/components/tiptap-node/image-node/image-node.scss +35 -0
- package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node-extension.ts +161 -0
- package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node.scss +249 -0
- package/template/src/admin/components/tiptap-node/image-upload-node/image-upload-node.tsx +554 -0
- package/template/src/admin/components/tiptap-node/image-upload-node/index.tsx +1 -0
- package/template/src/admin/components/tiptap-node/list-node/list-node.scss +208 -0
- package/template/src/admin/components/tiptap-node/paragraph-node/paragraph-node.scss +217 -0
- package/template/src/admin/components/tiptap-templates/simple/NOTICE.md +14 -0
- package/template/src/admin/components/tiptap-templates/simple/data/content.json +477 -0
- package/template/src/admin/components/tiptap-templates/simple/simple-editor.scss +97 -0
- package/template/src/admin/components/tiptap-templates/simple/simple-editor.tsx +348 -0
- package/template/src/admin/components/tiptap-templates/simple/theme-toggle.tsx +44 -0
- package/template/src/admin/components/tiptap-ui/blockquote-button/blockquote-button.tsx +122 -0
- package/template/src/admin/components/tiptap-ui/blockquote-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/blockquote-button/use-blockquote.ts +273 -0
- package/template/src/admin/components/tiptap-ui/code-block-button/code-block-button.tsx +122 -0
- package/template/src/admin/components/tiptap-ui/code-block-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/code-block-button/use-code-block.ts +283 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-button/color-highlight-button.scss +49 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-button/color-highlight-button.tsx +170 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-button/use-color-highlight.ts +380 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-popover/color-highlight-popover.tsx +231 -0
- package/template/src/admin/components/tiptap-ui/color-highlight-popover/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui/heading-button/heading-button.tsx +124 -0
- package/template/src/admin/components/tiptap-ui/heading-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/heading-button/use-heading.ts +348 -0
- package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +128 -0
- package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/heading-dropdown-menu/use-heading-dropdown-menu.ts +132 -0
- package/template/src/admin/components/tiptap-ui/image-upload-button/image-upload-button.tsx +130 -0
- package/template/src/admin/components/tiptap-ui/image-upload-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/image-upload-button/use-image-upload.ts +197 -0
- package/template/src/admin/components/tiptap-ui/link-popover/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/link-popover/link-popover.scss +16 -0
- package/template/src/admin/components/tiptap-ui/link-popover/link-popover.tsx +315 -0
- package/template/src/admin/components/tiptap-ui/link-popover/use-link-popover.ts +297 -0
- package/template/src/admin/components/tiptap-ui/list-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/list-button/list-button.tsx +120 -0
- package/template/src/admin/components/tiptap-ui/list-button/use-list.ts +353 -0
- package/template/src/admin/components/tiptap-ui/list-dropdown-menu/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +126 -0
- package/template/src/admin/components/tiptap-ui/list-dropdown-menu/use-list-dropdown-menu.ts +220 -0
- package/template/src/admin/components/tiptap-ui/mark-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/mark-button/mark-button.tsx +122 -0
- package/template/src/admin/components/tiptap-ui/mark-button/use-mark.ts +219 -0
- package/template/src/admin/components/tiptap-ui/search-and-replace/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/search-and-replace/search-and-replace.scss +300 -0
- package/template/src/admin/components/tiptap-ui/search-and-replace/search-and-replace.tsx +718 -0
- package/template/src/admin/components/tiptap-ui/search-and-replace/use-search-and-replace.ts +528 -0
- package/template/src/admin/components/tiptap-ui/text-align-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/text-align-button/text-align-button.tsx +144 -0
- package/template/src/admin/components/tiptap-ui/text-align-button/use-text-align.ts +237 -0
- package/template/src/admin/components/tiptap-ui/undo-redo-button/index.tsx +2 -0
- package/template/src/admin/components/tiptap-ui/undo-redo-button/undo-redo-button.tsx +125 -0
- package/template/src/admin/components/tiptap-ui/undo-redo-button/use-undo-redo.ts +200 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/badge-colors.scss +395 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/badge-group.scss +16 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/badge.scss +99 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/badge.tsx +44 -0
- package/template/src/admin/components/tiptap-ui-primitive/badge/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/button/button-colors.scss +613 -0
- package/template/src/admin/components/tiptap-ui-primitive/button/button.scss +390 -0
- package/template/src/admin/components/tiptap-ui-primitive/button/button.tsx +158 -0
- package/template/src/admin/components/tiptap-ui-primitive/button/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/button-group/button-group.scss +89 -0
- package/template/src/admin/components/tiptap-ui-primitive/button-group/button-group.tsx +72 -0
- package/template/src/admin/components/tiptap-ui-primitive/button-group/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/card/card.scss +74 -0
- package/template/src/admin/components/tiptap-ui-primitive/card/card.tsx +79 -0
- package/template/src/admin/components/tiptap-ui-primitive/card/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.scss +152 -0
- package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/dropdown-menu.tsx +262 -0
- package/template/src/admin/components/tiptap-ui-primitive/dropdown-menu/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/input/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/input/input.scss +41 -0
- package/template/src/admin/components/tiptap-ui-primitive/input/input.tsx +22 -0
- package/template/src/admin/components/tiptap-ui-primitive/input-group/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/input-group/input-group.scss +225 -0
- package/template/src/admin/components/tiptap-ui-primitive/input-group/input-group.tsx +135 -0
- package/template/src/admin/components/tiptap-ui-primitive/popover/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/popover/popover.scss +64 -0
- package/template/src/admin/components/tiptap-ui-primitive/popover/popover.tsx +35 -0
- package/template/src/admin/components/tiptap-ui-primitive/separator/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/separator/separator.scss +23 -0
- package/template/src/admin/components/tiptap-ui-primitive/separator/separator.tsx +28 -0
- package/template/src/admin/components/tiptap-ui-primitive/spacer/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/spacer/spacer.tsx +24 -0
- package/template/src/admin/components/tiptap-ui-primitive/switch/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/switch/switch.scss +86 -0
- package/template/src/admin/components/tiptap-ui-primitive/switch/switch.tsx +29 -0
- package/template/src/admin/components/tiptap-ui-primitive/textarea/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/textarea/textarea.scss +42 -0
- package/template/src/admin/components/tiptap-ui-primitive/textarea/textarea.tsx +16 -0
- package/template/src/admin/components/tiptap-ui-primitive/toolbar/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/toolbar/toolbar.scss +98 -0
- package/template/src/admin/components/tiptap-ui-primitive/toolbar/toolbar.tsx +203 -0
- package/template/src/admin/components/tiptap-ui-primitive/tooltip/index.tsx +1 -0
- package/template/src/admin/components/tiptap-ui-primitive/tooltip/tooltip.scss +43 -0
- package/template/src/admin/components/tiptap-ui-primitive/tooltip/tooltip.tsx +241 -0
- package/template/src/admin/hooks/use-composed-ref.ts +46 -0
- package/template/src/admin/hooks/use-cursor-visibility.ts +69 -0
- package/template/src/admin/hooks/use-element-rect.ts +166 -0
- package/template/src/admin/hooks/use-is-breakpoint.ts +35 -0
- package/template/src/admin/hooks/use-menu-navigation.ts +204 -0
- package/template/src/admin/hooks/use-scrolling.ts +75 -0
- package/template/src/admin/hooks/use-throttled-callback.ts +48 -0
- package/template/src/admin/hooks/use-tiptap-editor.ts +69 -0
- package/template/src/admin/hooks/use-unmount.ts +21 -0
- package/template/src/admin/hooks/use-window-size.ts +93 -0
- package/template/src/admin/html.ts +17 -0
- package/template/src/admin/lib/tiptap-utils.ts +652 -0
- package/template/src/admin/navigation-tree.tsx +82 -0
- package/template/src/admin/preview.ts +73 -0
- package/template/src/admin/styles/_cms-theme.scss +58 -0
- package/template/src/admin/styles/_keyframe-animations.scss +91 -0
- package/template/src/admin/styles/_variables.scss +298 -0
- package/template/src/assets/favicon.svg +1 -0
- package/template/src/assets/logo.svg +1 -0
- package/template/src/astro-env.d.ts +1 -0
- package/template/src/content.config.ts +12 -0
- package/template/src/db/bootstrap.ts +36 -0
- package/template/src/db/client.ts +7 -0
- package/template/src/db/schema.ts +45 -0
- package/template/src/documents/service.ts +178 -0
- package/template/src/documents/validation.ts +9 -0
- package/template/src/env.ts +9 -0
- package/template/src/index.ts +10 -0
- package/template/src/locales.ts +10 -0
- package/template/src/media/service.ts +120 -0
- package/template/src/navigation/service.ts +171 -0
- package/template/src/pages/[locale]/cms-preview-shell.astro +12 -0
- package/template/src/pages/cms-preview-shell.astro +5 -0
- package/template/src/publish/deploy-hook.ts +42 -0
- package/template/src/publish/record.ts +20 -0
- package/template/src/publish/service.ts +135 -0
- package/template/src/scss.d.ts +5 -0
- package/template/src/site.config.ts +7 -0
- package/template/src/sql.d.ts +4 -0
- package/template/src/starlight/cms-origin.ts +4 -0
- package/template/src/starlight/loader.ts +21 -0
- package/template/src/starlight/preview-render.ts +162 -0
- package/template/src/starlight/preview-shell.astro +14 -0
- package/template/src/starlight/render.ts +59 -0
- package/template/src/starlight/schema.ts +34 -0
- package/template/src/starlight/sidebar.ts +35 -0
- package/template/src/starlight/snapshot.ts +47 -0
- package/template/src/starlight/source.ts +26 -0
- package/template/src/starlight/tiptap.ts +29 -0
- package/template/src/styles/starlight.css +12 -0
- package/template/test-files/content.test.template.ts +82 -0
- package/template/test-files/deploy-hook.test.template.ts +32 -0
- package/template/test-files/media.test.template.ts +39 -0
- package/template/test-files/publishing.integration.test.template.ts +156 -0
- package/template/tsconfig.admin.json +18 -0
- package/template/tsconfig.json +23 -0
- package/template/worker-configuration.d.ts +15389 -0
- package/template/wrangler.jsonc +26 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cloudflare-starlight-cms",
|
|
3
|
+
"description": "A self-hosted documentation CMS for Astro Starlight, built on Cloudflare Workers, D1, R2, and Access.",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.19.0"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "astro build && node scripts/build-admin.mjs",
|
|
12
|
+
"build:empty": "CMS_INITIAL_EMPTY=1 npm run build",
|
|
13
|
+
"dev": "node scripts/dev.mjs",
|
|
14
|
+
"check": "CMS_INITIAL_EMPTY=1 astro check && tsc --noEmit && npm run check:admin && npm run check:linux-lock",
|
|
15
|
+
"check:admin": "tsc -p tsconfig.admin.json --noEmit",
|
|
16
|
+
"check:linux-lock": "node scripts/check-linux-bindings.mjs",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"migrate:local": "wrangler d1 migrations apply DB --local",
|
|
19
|
+
"migrate:remote": "wrangler d1 migrations apply DB --remote",
|
|
20
|
+
"deploy": "wrangler deploy",
|
|
21
|
+
"typegen": "wrangler types worker-configuration.d.ts --env-interface CloudflareBindings",
|
|
22
|
+
"dry-run": "wrangler deploy --dry-run"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@astrojs/check": "0.9.10",
|
|
26
|
+
"@astrojs/starlight": "0.42.0",
|
|
27
|
+
"@floating-ui/react": "0.27.20",
|
|
28
|
+
"@headless-tree/core": "1.7.0",
|
|
29
|
+
"@headless-tree/react": "1.7.0",
|
|
30
|
+
"@radix-ui/react-dropdown-menu": "2.1.24",
|
|
31
|
+
"@radix-ui/react-popover": "1.1.23",
|
|
32
|
+
"@tiptap/core": "3.31.3",
|
|
33
|
+
"@tiptap/extension-find-and-replace": "3.31.3",
|
|
34
|
+
"@tiptap/extension-highlight": "3.31.3",
|
|
35
|
+
"@tiptap/extension-horizontal-rule": "3.31.3",
|
|
36
|
+
"@tiptap/extension-image": "3.31.3",
|
|
37
|
+
"@tiptap/extension-link": "3.31.3",
|
|
38
|
+
"@tiptap/extension-list": "3.31.3",
|
|
39
|
+
"@tiptap/extension-subscript": "3.31.3",
|
|
40
|
+
"@tiptap/extension-superscript": "3.31.3",
|
|
41
|
+
"@tiptap/extension-table": "3.31.3",
|
|
42
|
+
"@tiptap/extension-table-cell": "3.31.3",
|
|
43
|
+
"@tiptap/extension-table-header": "3.31.3",
|
|
44
|
+
"@tiptap/extension-table-row": "3.31.3",
|
|
45
|
+
"@tiptap/extension-text-align": "3.31.3",
|
|
46
|
+
"@tiptap/extension-typography": "3.31.3",
|
|
47
|
+
"@tiptap/extensions": "3.31.3",
|
|
48
|
+
"@tiptap/pm": "3.31.3",
|
|
49
|
+
"@tiptap/react": "3.31.3",
|
|
50
|
+
"@tiptap/starter-kit": "3.31.3",
|
|
51
|
+
"astro": "7.3.2",
|
|
52
|
+
"clsx": "2.1.1",
|
|
53
|
+
"drizzle-orm": "0.45.2",
|
|
54
|
+
"github-slugger": "2.0.0",
|
|
55
|
+
"hono": "4.13.7",
|
|
56
|
+
"lodash.throttle": "4.1.1",
|
|
57
|
+
"react": "18.3.1",
|
|
58
|
+
"react-dom": "18.3.1",
|
|
59
|
+
"react-hotkeys-hook": "5.3.3",
|
|
60
|
+
"zod": "4.6.4"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@base-ui/react": "1.8.0",
|
|
64
|
+
"@types/lodash.throttle": "4.1.9",
|
|
65
|
+
"@types/node": "22.20.1",
|
|
66
|
+
"@types/react": "18.3.12",
|
|
67
|
+
"@types/react-dom": "18.3.1",
|
|
68
|
+
"class-variance-authority": "0.7.1",
|
|
69
|
+
"esbuild": "0.28.2",
|
|
70
|
+
"miniflare": "5.20260908.0-alpha",
|
|
71
|
+
"sass-embedded": "1.104.0",
|
|
72
|
+
"typescript": "5.9.3",
|
|
73
|
+
"vitest": "5.0.0",
|
|
74
|
+
"wrangler": "4.130.0"
|
|
75
|
+
},
|
|
76
|
+
"optionalDependencies": {
|
|
77
|
+
"@astrojs/compiler-binding-linux-x64-gnu": "0.4.0",
|
|
78
|
+
"@bruits/satteri-linux-x64-gnu": "0.10.5",
|
|
79
|
+
"@esbuild/linux-x64": "0.28.1",
|
|
80
|
+
"@img/sharp-libvips-linux-x64": "1.3.1",
|
|
81
|
+
"@img/sharp-linux-x64": "0.35.2",
|
|
82
|
+
"@pagefind/linux-x64": "1.5.2",
|
|
83
|
+
"@rolldown/binding-linux-x64-gnu": "1.2.7",
|
|
84
|
+
"lightningcss-linux-x64-gnu": "1.33.0"
|
|
85
|
+
},
|
|
86
|
+
"cloudflareStarlightCms": {
|
|
87
|
+
"templateVersion": "0.9.0"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { build, context } from 'esbuild';
|
|
2
|
+
import { compile } from 'sass-embedded';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { copyFile, mkdir } from 'node:fs/promises';
|
|
5
|
+
|
|
6
|
+
const assetDirectory = process.env.ADMIN_ASSETS_DIR ?? 'dist';
|
|
7
|
+
await mkdir(`${assetDirectory}/admin`, { recursive: true });
|
|
8
|
+
await copyFile('src/assets/favicon.svg', `${assetDirectory}/favicon.svg`);
|
|
9
|
+
|
|
10
|
+
const sassPlugin = {
|
|
11
|
+
name: 'sass',
|
|
12
|
+
setup(buildContext) {
|
|
13
|
+
buildContext.onLoad({ filter: /\.scss$/ }, (args) => {
|
|
14
|
+
const result = compile(args.path, { style: 'compressed', loadPaths: ['src/admin'] });
|
|
15
|
+
return { contents: result.css, loader: 'css', watchFiles: result.loadedUrls.filter((url) => url.protocol === 'file:').map(fileURLToPath) };
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const options = {
|
|
21
|
+
entryPoints: ['src/admin/client.tsx'],
|
|
22
|
+
bundle: true,
|
|
23
|
+
format: 'esm',
|
|
24
|
+
jsx: 'automatic',
|
|
25
|
+
loader: { '.svg': 'dataurl' },
|
|
26
|
+
outfile: `${assetDirectory}/admin/app.js`,
|
|
27
|
+
alias: { '@': './src/admin' },
|
|
28
|
+
plugins: [sassPlugin],
|
|
29
|
+
};
|
|
30
|
+
if (process.argv.includes('--watch')) {
|
|
31
|
+
const buildContext = await context(options);
|
|
32
|
+
await buildContext.watch();
|
|
33
|
+
const stop = async () => { await buildContext.dispose(); };
|
|
34
|
+
process.once('SIGINT', stop);
|
|
35
|
+
process.once('SIGTERM', stop);
|
|
36
|
+
} else {
|
|
37
|
+
await build(options);
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
const lock = JSON.parse(await readFile(new URL('../package-lock.json', import.meta.url), 'utf8'));
|
|
4
|
+
const packages = lock.packages ?? {};
|
|
5
|
+
const linuxX64 = /linux-x64(?:-(?:gnu|glibc))?$/;
|
|
6
|
+
const missing = new Set();
|
|
7
|
+
|
|
8
|
+
for (const pkg of Object.values(packages)) {
|
|
9
|
+
for (const name of Object.keys(pkg.optionalDependencies ?? {})) {
|
|
10
|
+
if (linuxX64.test(name) && !packages[`node_modules/${name}`]) missing.add(name);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (missing.size > 0) {
|
|
15
|
+
throw new Error(`package-lock.json is missing Linux x64 native bindings: ${[...missing].sort().join(', ')}. Add the matching package as an exact optionalDependency and regenerate the lockfile.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log('Linux x64 native bindings are present in package-lock.json.');
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const cmsPort = Number(process.env.CMS_PORT ?? 8787);
|
|
4
|
+
const docsPort = Number(process.env.DOCS_PORT ?? 4321);
|
|
5
|
+
const cmsUrl = `http://127.0.0.1:${cmsPort}/admin/export/snapshot`;
|
|
6
|
+
const children = new Set();
|
|
7
|
+
let stopping = false;
|
|
8
|
+
let timer;
|
|
9
|
+
|
|
10
|
+
function stop() {
|
|
11
|
+
stopping = true;
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
for (const child of children) child.kill('SIGTERM');
|
|
14
|
+
}
|
|
15
|
+
function start(command, args, env = process.env) {
|
|
16
|
+
const child = spawn(command, args, { stdio: 'inherit', env });
|
|
17
|
+
children.add(child);
|
|
18
|
+
child.once('exit', () => children.delete(child));
|
|
19
|
+
return child;
|
|
20
|
+
}
|
|
21
|
+
function run(command, args, env) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const child = start(command, args, env);
|
|
24
|
+
child.once('error', reject);
|
|
25
|
+
child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`${command} exited with ${code}`)));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function server(command, args, env) {
|
|
29
|
+
const child = start(command, args, env);
|
|
30
|
+
const failed = (message) => {
|
|
31
|
+
if (stopping) return;
|
|
32
|
+
console.error(message);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
stop();
|
|
35
|
+
};
|
|
36
|
+
child.once('error', (error) => failed(error.message));
|
|
37
|
+
child.once('exit', () => failed(`${command} stopped unexpectedly`));
|
|
38
|
+
}
|
|
39
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
+
async function snapshot() {
|
|
41
|
+
const response = await fetch(cmsUrl, { signal: AbortSignal.timeout(5000) });
|
|
42
|
+
if (!response.ok) throw new Error(`CMS export returned ${response.status}`);
|
|
43
|
+
return response.text();
|
|
44
|
+
}
|
|
45
|
+
async function ready() {
|
|
46
|
+
for (let attempt = 0; attempt < 60 && !stopping; attempt++) {
|
|
47
|
+
try { return await snapshot(); } catch { await wait(500); }
|
|
48
|
+
}
|
|
49
|
+
throw new Error('Local CMS did not become ready');
|
|
50
|
+
}
|
|
51
|
+
const cli = (name) => `node_modules/${name}/bin/${name === 'wrangler' ? 'wrangler.js' : 'astro.mjs'}`;
|
|
52
|
+
async function build() {
|
|
53
|
+
await run(process.execPath, [cli('astro'), 'build'], { ...process.env, CMS_EXPORT_URL: cmsUrl });
|
|
54
|
+
// Match production: both public and Admin assets are served from the configured
|
|
55
|
+
// Static Assets directory. Astro clears dist, so recreate the Admin bundle after it.
|
|
56
|
+
await run(process.execPath, ['scripts/build-admin.mjs']);
|
|
57
|
+
}
|
|
58
|
+
process.once('SIGINT', stop);
|
|
59
|
+
process.once('SIGTERM', stop);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
if (![cmsPort, docsPort].every((port) => Number.isInteger(port) && port > 0 && port < 65536) || cmsPort === docsPort) throw new Error('CMS_PORT and DOCS_PORT must be distinct valid ports');
|
|
63
|
+
// Local development always uses Miniflare state. Applying migrations is idempotent
|
|
64
|
+
// and avoids a separate first-run command without touching a remote D1 database.
|
|
65
|
+
await run(process.execPath, [cli('wrangler'), 'd1', 'migrations', 'apply', 'DB', '--local'], { ...process.env, CI: '1' });
|
|
66
|
+
await run(process.execPath, ['scripts/build-admin.mjs']);
|
|
67
|
+
server(process.execPath, ['scripts/build-admin.mjs', '--watch']);
|
|
68
|
+
server(process.execPath, [cli('wrangler'), 'dev', '--local', '--log-level', 'warn', '--port', String(cmsPort), '--var', 'LOCAL_DEV_BUILD:true']);
|
|
69
|
+
let completed = await ready();
|
|
70
|
+
if (stopping) throw new Error('Development server stopped');
|
|
71
|
+
await build();
|
|
72
|
+
if (stopping) throw new Error('Development server stopped');
|
|
73
|
+
server(process.execPath, [cli('astro'), 'preview', '--ignore-lock', '--host', '127.0.0.1', '--port', String(docsPort)], { ...process.env, ASTRO_PREVIEW_BACKGROUND: 'false' });
|
|
74
|
+
console.log(`Admin: http://127.0.0.1:${cmsPort}/admin/\nPublished docs: http://127.0.0.1:${docsPort}/`);
|
|
75
|
+
let failed;
|
|
76
|
+
async function poll() {
|
|
77
|
+
try {
|
|
78
|
+
const next = await snapshot();
|
|
79
|
+
if (next !== completed && next !== failed && !stopping) {
|
|
80
|
+
console.log('Published content changed. Building docs…');
|
|
81
|
+
try { await build(); completed = next; failed = undefined; console.log('Docs rebuilt. Refresh the public page.'); }
|
|
82
|
+
catch (error) { failed = next; throw error; }
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!stopping) console.error('Local build/export failed. Fix the error and restart dev, or publish a new change.', error.message);
|
|
86
|
+
} finally {
|
|
87
|
+
if (!stopping) timer = setTimeout(poll, 1000);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!stopping) timer = setTimeout(poll, 1000);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (!stopping) { console.error(error.message); process.exitCode = 1; }
|
|
93
|
+
stop();
|
|
94
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
:root { color: var(--cms-text); background: var(--cms-bg); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
html, body, #admin-root { min-width: 320px; min-height: 100%; margin: 0; background: var(--cms-bg); }
|
|
4
|
+
button, input, textarea { font: inherit; }
|
|
5
|
+
button { cursor: pointer; }
|
|
6
|
+
button:disabled { cursor: not-allowed; opacity: .55; }
|
|
7
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
8
|
+
|
|
9
|
+
.cms-shell { min-height: 100vh; color: var(--cms-text); background: var(--cms-bg); }
|
|
10
|
+
.cms-topbar { position: sticky; top: 0; z-index: 30; display: flex; min-height: 64px; align-items: center; justify-content: space-between; gap: 1rem; padding: 0 1.5rem; background: color-mix(in srgb, var(--cms-surface) 94%, transparent); border-bottom: 1px solid var(--cms-border); backdrop-filter: blur(16px); }
|
|
11
|
+
.cms-brand { display: inline-flex; align-items: center; gap: .65rem; color: var(--cms-text); font-size: .98rem; font-weight: 750; letter-spacing: -.02em; text-decoration: none; }
|
|
12
|
+
.cms-brand-logo { display: block; width: auto; height: 2.25rem; max-width: min(15rem, 52vw); }
|
|
13
|
+
.cms-brand-mark, .cms-empty-icon { display: grid; width: 30px; height: 30px; place-items: center; color: var(--cms-accent-foreground); background: var(--cms-accent); border-radius: 9px; box-shadow: 0 4px 10px var(--cms-focus-ring); }
|
|
14
|
+
.cms-topbar-controls, .cms-actions { display: flex; align-items: center; justify-content: flex-end; }
|
|
15
|
+
.cms-topbar-controls { gap: .7rem; }
|
|
16
|
+
.cms-actions { min-width: 0; gap: .5rem; }
|
|
17
|
+
.cms-theme-trigger { display: grid; width: 32px; height: 32px; place-items: center; padding: 0; color: var(--cms-text-muted); background: transparent; border: 1px solid var(--cms-border); border-radius: 7px; }
|
|
18
|
+
.cms-theme-trigger:hover { color: var(--cms-text); background: var(--cms-hover); }
|
|
19
|
+
.cms-theme-trigger svg { width: 1rem; height: 1rem; }
|
|
20
|
+
.cms-status { padding: .25rem .55rem; border-radius: 999px; font-size: .72rem; font-weight: 700; line-height: 1.2; text-transform: capitalize; }
|
|
21
|
+
.cms-status-draft, .cms-status-unsaved, .cms-sidebar .tree .treeitem-status.draft, .cms-sidebar .tree .treeitem-status.changes { color: var(--cms-warning-text); background: var(--cms-warning-subtle); }
|
|
22
|
+
.cms-status-published { color: var(--cms-success-text); background: var(--cms-success-subtle); }
|
|
23
|
+
.cms-notice { max-width: 19rem; overflow: hidden; color: var(--cms-text-muted); font-size: .78rem; text-overflow: ellipsis; white-space: nowrap; }
|
|
24
|
+
.cms-notice.is-error { color: var(--cms-danger-text); }
|
|
25
|
+
.cms-button, .cms-history-button { min-height: 34px; padding: .45rem .72rem; color: var(--cms-text-muted); background: var(--cms-elevated); border: 1px solid var(--cms-border); border-radius: 7px; box-shadow: 0 1px 2px var(--cms-shadow); font-size: .8rem; font-weight: 650; }
|
|
26
|
+
.cms-button:hover:not(:disabled), .cms-history-button:hover { color: var(--cms-text); background: var(--cms-hover); }
|
|
27
|
+
.cms-button-primary { color: var(--cms-accent-foreground); background: var(--cms-accent); border-color: var(--cms-accent); }
|
|
28
|
+
.cms-button-primary:hover:not(:disabled) { color: var(--cms-accent-foreground); background: var(--cms-accent-hover); border-color: var(--cms-accent-hover); }
|
|
29
|
+
.cms-button-quiet { background: transparent; box-shadow: none; }
|
|
30
|
+
.cms-button-danger, .cms-button-danger-quiet { color: var(--cms-danger-text); background: var(--cms-danger-subtle); border-color: var(--cms-danger); }
|
|
31
|
+
.cms-button-danger-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
32
|
+
.cms-button-danger:hover:not(:disabled), .cms-button-danger-quiet:hover:not(:disabled) { color: var(--cms-danger-text); background: var(--cms-danger-subtle); border-color: var(--cms-danger); }
|
|
33
|
+
|
|
34
|
+
.cms-workspace { display: grid; grid-template-columns: 250px minmax(0, 1fr); min-height: calc(100vh - 64px); }
|
|
35
|
+
.cms-sidebar { position: sticky; top: 64px; display: flex; height: calc(100vh - 64px); flex-direction: column; gap: .85rem; padding: 1.1rem .8rem; overflow: hidden; background: var(--cms-surface); border-right: 1px solid var(--cms-border); }
|
|
36
|
+
.cms-sidebar-heading { display: flex; align-items: center; justify-content: space-between; padding: 0 .35rem; color: var(--cms-text-muted); font-size: .82rem; font-weight: 750; }
|
|
37
|
+
.cms-count { display: grid; min-width: 22px; height: 22px; place-items: center; color: var(--cms-accent); background: var(--cms-accent-subtle); border-radius: 999px; font-size: .72rem; }
|
|
38
|
+
.cms-new-document, .cms-upload { color: var(--cms-accent-foreground); background: var(--cms-accent); border: 1px solid var(--cms-accent); border-radius: 7px; font-weight: 700; }
|
|
39
|
+
.cms-new-document { width: 100%; padding: .62rem .72rem; box-shadow: 0 1px 2px var(--cms-shadow); font-size: .82rem; text-align: left; }
|
|
40
|
+
.cms-new-document:hover { background: var(--cms-accent-hover); border-color: var(--cms-accent-hover); }
|
|
41
|
+
.cms-new-document::before { margin-right: .45rem; content: "+"; font-size: 1rem; }
|
|
42
|
+
.cms-navigation-actions { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .45rem; }
|
|
43
|
+
.cms-new-folder { padding: .62rem .72rem; color: var(--cms-text-muted); background: var(--cms-elevated); border: 1px solid var(--cms-border); border-radius: 7px; font-size: .78rem; font-weight: 700; }
|
|
44
|
+
.cms-new-folder:hover { color: var(--cms-text); background: var(--cms-hover); }
|
|
45
|
+
.cms-search input, .cms-title-input, .cms-description-input, .cms-slug-input, .cms-folder-fields input, .cms-folder-dialog input { width: 100%; color: var(--cms-text); background: var(--cms-input-bg); border: 1px solid var(--cms-border); outline: 0; }
|
|
46
|
+
.cms-search input { padding: .56rem .65rem; border-radius: 7px; font-size: .78rem; }
|
|
47
|
+
.cms-search input:focus, .cms-title-input:focus, .cms-description-input:focus, .cms-slug-input:focus, .cms-folder-fields input:focus, .cms-folder-dialog input:focus, .cms-content-locale select:focus { border-color: var(--cms-accent); box-shadow: 0 0 0 3px var(--cms-focus-ring); }
|
|
48
|
+
.cms-document-list { display: grid; gap: .95rem; overflow: auto; padding: .15rem .15rem 1.5rem; }
|
|
49
|
+
.cms-document-group { display: grid; gap: .15rem; }
|
|
50
|
+
.cms-document-group h2 { margin: 0 0 .22rem; padding: 0 .35rem; color: var(--cms-text-muted); font-size: .68rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; }
|
|
51
|
+
.cms-document-item { display: grid; gap: .15rem; width: 100%; padding: .55rem .6rem; color: var(--cms-text-muted); background: transparent; border: 1px solid transparent; border-radius: 7px; text-align: left; }
|
|
52
|
+
.cms-document-item:hover { background: var(--cms-hover); }
|
|
53
|
+
.cms-document-item.is-active { color: var(--cms-accent); background: var(--cms-accent-subtle); border-color: var(--cms-accent); }
|
|
54
|
+
.cms-document-item strong { overflow: hidden; color: inherit; font-size: .8rem; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
|
|
55
|
+
.cms-document-item span, .cms-empty-search { color: var(--cms-text-subtle); font-size: .68rem; }
|
|
56
|
+
.cms-document-item.is-active span { color: var(--cms-accent); }
|
|
57
|
+
.cms-empty-search { margin: 0; padding: .35rem; font-size: .78rem; }
|
|
58
|
+
|
|
59
|
+
/* Adapted from Headless Tree's Comprehensive DnD story (MIT). */
|
|
60
|
+
.navigation-tree-scroll { flex: 1 1 0; min-height: 12rem; overflow: auto; padding: 8px; }
|
|
61
|
+
.cms-sidebar .tree { position: relative; display: block; min-height: 100%; max-width: none; }
|
|
62
|
+
.cms-sidebar .tree button[role="treeitem"] { display: flex; width: 100%; padding: 0 0 2px; background: transparent; border: none; }
|
|
63
|
+
.cms-sidebar .tree .treeitem { position: relative; display: flex; width: 100%; align-items: center; gap: .4rem; padding: 6px 10px; color: var(--cms-text-muted); background-color: var(--cms-surface); border-radius: 8px; font-size: .8rem; text-align: left; transition: background-color .2s ease; cursor: pointer; }
|
|
64
|
+
.cms-sidebar .tree .treeitem-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
65
|
+
.cms-sidebar .tree .treeitem-status { margin-left: auto; padding: .12rem .34rem; border-radius: 999px; font-size: .6rem; font-weight: 750; line-height: 1.2; }
|
|
66
|
+
.cms-sidebar .tree .treeitem:hover { background-color: var(--cms-hover); }
|
|
67
|
+
.cms-sidebar .tree button[role="treeitem"]:focus { outline: none; }
|
|
68
|
+
.cms-sidebar .tree .treeitem.selected, .cms-sidebar .tree .treeitem.drop { color: var(--cms-accent); background-color: var(--cms-accent-subtle); }
|
|
69
|
+
.cms-sidebar .tree button:focus-visible .treeitem.focused { outline: 2px solid var(--cms-accent); outline-offset: 1px; }
|
|
70
|
+
.cms-sidebar .tree .treeitem.folder::before { display: inline-block; z-index: 1; width: 10px; margin-right: 4px; content: "›"; color: var(--cms-text-subtle); font-size: .95rem; transition: transform .1s ease-in-out; }
|
|
71
|
+
.cms-sidebar .tree .treeitem.folder.expanded::before { transform: rotate(90deg); }
|
|
72
|
+
.cms-sidebar .tree .treeitem:not(.folder) { padding-left: 24px; }
|
|
73
|
+
.cms-sidebar .tree .treeitem.selected::after { position: absolute; top: 5px; left: -2px; width: 4px; height: 16px; content: " "; background-color: var(--cms-accent); border-radius: 99px; }
|
|
74
|
+
.cms-sidebar .tree .dragline { height: 2px; margin-top: -1px; background-color: var(--cms-accent); }
|
|
75
|
+
.cms-sidebar .tree .dragline::before { box-sizing: content-box; position: absolute; top: -3px; left: 0; width: 4px; height: 4px; content: ""; background: var(--cms-surface); border: 2px solid var(--cms-accent); border-radius: 99px; }
|
|
76
|
+
|
|
77
|
+
.cms-main { min-width: 0; padding: 1.35rem clamp(1rem, 3vw, 3.5rem) 3rem; }
|
|
78
|
+
.cms-breadcrumb { display: flex; align-items: center; flex-wrap: wrap; gap: .35rem; margin: 0 0 1rem; color: var(--cms-text-muted); font-size: .76rem; }
|
|
79
|
+
.cms-breadcrumb-item { display: inline-flex; align-items: center; min-width: 0; gap: .35rem; }
|
|
80
|
+
.cms-breadcrumb-item svg { flex: 0 0 auto; width: .85rem; height: .85rem; color: var(--cms-text-subtle); }
|
|
81
|
+
.cms-breadcrumb button { padding: 0; color: var(--cms-text-muted); background: transparent; border: 0; font: inherit; }
|
|
82
|
+
.cms-breadcrumb button:hover { color: var(--cms-accent); text-decoration: underline; }
|
|
83
|
+
.cms-breadcrumb strong { overflow: hidden; color: var(--cms-text); font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
|
|
84
|
+
.cms-document-actions { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: 0 0 1.1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--cms-border); }
|
|
85
|
+
.cms-document-state, .cms-document-action-buttons { display: flex; align-items: center; gap: .65rem; min-width: 0; }
|
|
86
|
+
.cms-document-state > span:not(.cms-status) { overflow: hidden; color: var(--cms-text-muted); font-size: .76rem; text-overflow: ellipsis; white-space: nowrap; }
|
|
87
|
+
.cms-history-button { display: inline-flex; align-items: center; gap: .35rem; }
|
|
88
|
+
.cms-history-button svg { width: .9rem; height: .9rem; }
|
|
89
|
+
.cms-history-button.is-active { color: var(--cms-accent); background: var(--cms-accent-subtle); border-color: var(--cms-accent); box-shadow: 0 0 0 3px var(--cms-focus-ring); }
|
|
90
|
+
.cms-danger-zone { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--cms-danger); }
|
|
91
|
+
.cms-danger-zone h2 { margin: 0; color: var(--cms-danger-text); font-size: .8rem; }
|
|
92
|
+
.cms-danger-zone p { margin: .25rem 0 0; color: var(--cms-text-muted); font-size: .75rem; }
|
|
93
|
+
.cms-editor-surface { width: 100%; min-width: 0; overflow: visible; }
|
|
94
|
+
.cms-document-fields, .cms-folder-fields { display: grid; gap: .85rem; }
|
|
95
|
+
.cms-content-locale { display: flex; width: fit-content; align-items: center; gap: .5rem; color: var(--cms-text-muted); font-size: .72rem; font-weight: 700; }
|
|
96
|
+
.cms-content-locale select { height: 30px; padding: 0 .45rem; color: var(--cms-text); background: var(--cms-input-bg); border: 1px solid var(--cms-border); border-radius: 6px; font-size: .74rem; font-weight: 650; }
|
|
97
|
+
.cms-meta-field { display: grid; gap: .55rem; }
|
|
98
|
+
.cms-meta-field > span, .cms-editor-field-header > span { color: var(--cms-text-muted); font-size: .72rem; font-weight: 700; letter-spacing: .02em; }
|
|
99
|
+
.cms-title-input, .cms-description-input, .cms-slug-input { border-radius: 8px; box-shadow: 0 1px 2px var(--cms-shadow); }
|
|
100
|
+
.cms-title-input { padding: .85rem .95rem; font-size: clamp(1.45rem, 3vw, 2rem); font-weight: 750; line-height: 1.2; }
|
|
101
|
+
.cms-title-input::placeholder, .cms-description-input::placeholder { color: var(--cms-text-subtle); }
|
|
102
|
+
.cms-description-input { display: block; min-height: 5rem; padding: .75rem .95rem; resize: vertical; font: inherit; font-size: .95rem; line-height: 1.5; }
|
|
103
|
+
.cms-slug-input { padding: .65rem .8rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88rem; }
|
|
104
|
+
.cms-field-help { color: var(--cms-text-subtle); font-size: .72rem; line-height: 1.4; }
|
|
105
|
+
.cms-editor-field { display: grid; gap: .55rem; margin-top: 1.35rem; }
|
|
106
|
+
.cms-editor-field-header { display: flex; align-items: center; justify-content: space-between; }
|
|
107
|
+
|
|
108
|
+
/* Tiptap's official template supplies its editor UI. This wrapper only fits it into the CMS. */
|
|
109
|
+
.cms-simple-editor { overflow: hidden; background: var(--cms-surface); border: 1px solid var(--cms-border); border-radius: 10px; }
|
|
110
|
+
.cms-simple-editor .simple-editor-wrapper { position: relative; width: 100%; height: min(720px, calc(100vh - 220px)); min-height: 560px; overflow: auto; }
|
|
111
|
+
.cms-simple-editor .simple-editor-content { max-width: 54rem; }
|
|
112
|
+
.cms-simple-editor .simple-editor-search-and-replace { position: absolute; top: .5rem; right: .5rem; }
|
|
113
|
+
.cms-simple-editor .tiptap-toolbar { top: 0; }
|
|
114
|
+
.cms-simple-editor .tiptap.ProseMirror.simple-editor img, .cms-simple-editor .tiptap.ProseMirror.simple-editor video { display: block; max-width: 100%; height: auto; margin: 1.25rem 0; border-radius: 8px; }
|
|
115
|
+
.cms-simple-editor .tiptap.ProseMirror.simple-editor aside[data-callout] { margin: 1.25rem 0; padding: 1rem 1.1rem; background: var(--cms-accent-subtle); border-left: 3px solid var(--cms-accent); border-radius: 0 8px 8px 0; }
|
|
116
|
+
.cms-simple-editor .tiptap.ProseMirror.simple-editor ol[data-steps] { padding: 1rem 1rem 1rem 2.25rem; background: var(--cms-hover); border-radius: 8px; }
|
|
117
|
+
.cms-simple-editor .tiptap.ProseMirror.simple-editor section[data-tabs] { padding: 1rem; border: 1px solid var(--cms-border); border-radius: 8px; }
|
|
118
|
+
.cms-simple-editor .tiptap.ProseMirror.simple-editor section[data-tab] + section[data-tab] { margin-top: .75rem; padding-top: .75rem; border-top: 1px solid var(--cms-border); }
|
|
119
|
+
|
|
120
|
+
.cms-close-settings { display: grid; width: 28px; height: 28px; place-items: center; padding: 0; color: var(--cms-text-muted); background: transparent; border: 0; border-radius: 5px; font-size: 1.2rem; }
|
|
121
|
+
.cms-close-settings:hover { background: var(--cms-hover); }
|
|
122
|
+
.cms-revision-backdrop { position: fixed; z-index: 100; inset: 0; display: flex; align-items: flex-start; justify-content: flex-end; padding: 5rem 1.5rem 1.5rem; background: var(--cms-backdrop); }
|
|
123
|
+
.cms-revision-drawer { display: flex; flex-direction: column; gap: 1rem; width: min(25rem, 100%); max-height: min(34rem, calc(100vh - 6.5rem)); padding: 1.25rem; overflow: hidden; color: var(--cms-text); background: var(--cms-elevated); border: 1px solid var(--cms-border); border-radius: .75rem; box-shadow: -16px 0 36px var(--cms-shadow); }
|
|
124
|
+
.cms-revision-drawer > header, .cms-folder-panel > header, .cms-folder-dialog header, .cms-media-dialog header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
|
|
125
|
+
.cms-revision-drawer h2 { margin: 0; color: var(--cms-text); font-size: 1rem; }
|
|
126
|
+
.cms-revision-drawer header p, .cms-revisions-empty { margin: .3rem 0 0; color: var(--cms-text-muted); font-size: .78rem; line-height: 1.45; }
|
|
127
|
+
.cms-revisions { display: grid; gap: .45rem; }
|
|
128
|
+
.cms-revision-drawer .cms-revisions { overflow: auto; padding-right: .2rem; }
|
|
129
|
+
.cms-revisions > p { margin: 0; color: var(--cms-text-subtle); font-size: .75rem; }
|
|
130
|
+
.cms-revision { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .5rem; background: var(--cms-bg); border: 1px solid var(--cms-border); border-radius: 6px; }
|
|
131
|
+
.cms-revision span { display: grid; gap: .12rem; min-width: 0; color: var(--cms-text-muted); font-size: .7rem; }
|
|
132
|
+
.cms-revision time { overflow: hidden; color: var(--cms-text-subtle); font-size: .65rem; text-overflow: ellipsis; white-space: nowrap; }
|
|
133
|
+
.cms-revision button { padding: .25rem .35rem; color: var(--cms-accent); background: var(--cms-surface); border: 1px solid var(--cms-accent); border-radius: 5px; font-size: .68rem; font-weight: 650; }
|
|
134
|
+
|
|
135
|
+
.cms-empty-state { display: grid; justify-items: center; gap: .75rem; max-width: 440px; margin: 18vh auto; color: var(--cms-text-muted); text-align: center; }
|
|
136
|
+
.cms-empty-state .cms-empty-icon { width: 42px; height: 42px; font-size: 1.1rem; }
|
|
137
|
+
.cms-empty-state h1 { margin: .35rem 0 0; color: var(--cms-text); font-size: 1.35rem; }
|
|
138
|
+
.cms-empty-state p { margin: 0; font-size: .9rem; }
|
|
139
|
+
.cms-empty-actions, .cms-folder-panel-actions { display: flex; flex-wrap: wrap; gap: .55rem; }
|
|
140
|
+
.cms-folder-panel { display: grid; gap: 1.35rem; width: min(100%, 54rem); margin: 1.25rem auto; }
|
|
141
|
+
.cms-folder-panel header p { margin: 0 0 .35rem; color: var(--cms-text-muted); font-size: .72rem; font-weight: 700; text-transform: uppercase; }
|
|
142
|
+
.cms-folder-panel h1 { margin: 0; color: var(--cms-text); font-size: 1.75rem; letter-spacing: -.03em; }
|
|
143
|
+
.cms-folder-panel header span { display: block; margin-top: .45rem; color: var(--cms-text-muted); font-size: .86rem; }
|
|
144
|
+
.cms-folder-fields input, .cms-folder-dialog input { padding: .72rem .82rem; border-radius: 8px; }
|
|
145
|
+
.cms-folder-panel > footer, .cms-folder-dialog footer { display: flex; align-items: center; justify-content: space-between; gap: .6rem; padding-top: 1rem; border-top: 1px solid var(--cms-border); }
|
|
146
|
+
.cms-folder-dialog { display: grid; gap: 1rem; width: min(440px, 100%); padding: 1.25rem; color: var(--cms-text); background: var(--cms-elevated); border: 1px solid var(--cms-border); border-radius: 12px; box-shadow: 0 24px 48px var(--cms-shadow); }
|
|
147
|
+
.cms-folder-dialog h1, .cms-media-dialog h1 { margin: 0; color: var(--cms-text); font-size: 1rem; }
|
|
148
|
+
.cms-folder-dialog p, .cms-media-dialog p { margin: .3rem 0 0; color: var(--cms-text-muted); font-size: .78rem; }
|
|
149
|
+
.cms-media-backdrop { position: fixed; z-index: 100; inset: 0; display: grid; place-items: center; padding: 1rem; background: var(--cms-backdrop); }
|
|
150
|
+
.cms-media-dialog { width: min(760px, 100%); max-height: min(700px, 90vh); overflow: auto; padding: 1.25rem; background: var(--cms-elevated); border: 1px solid var(--cms-border); border-radius: 12px; box-shadow: 0 24px 48px var(--cms-shadow); }
|
|
151
|
+
.cms-media-dialog-actions { display: flex; justify-content: flex-end; margin: 1rem 0; }
|
|
152
|
+
.cms-upload { position: relative; display: inline-flex; overflow: hidden; padding: .48rem .64rem; }
|
|
153
|
+
.cms-upload input { position: absolute; inset: 0; cursor: pointer; opacity: 0; }
|
|
154
|
+
.cms-media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: .75rem; }
|
|
155
|
+
.cms-media-empty { grid-column: 1 / -1; padding: 2rem 0; text-align: center; }
|
|
156
|
+
.cms-media-card { display: grid; gap: .4rem; min-width: 0; padding: .45rem; color: var(--cms-text-muted); background: var(--cms-surface); border: 1px solid var(--cms-border); border-radius: 8px; }
|
|
157
|
+
.cms-media-insert { display: grid; gap: .35rem; min-width: 0; padding: 0; color: inherit; background: transparent; border: 0; text-align: left; }
|
|
158
|
+
.cms-media-insert:hover .cms-media-preview { outline: 1px solid var(--cms-accent); box-shadow: 0 2px 7px var(--cms-focus-ring); }
|
|
159
|
+
.cms-media-preview { display: grid; height: 96px; overflow: hidden; place-items: center; background: var(--cms-hover); border-radius: 5px; }
|
|
160
|
+
.cms-media-preview img, .cms-media-preview video { width: 100%; height: 100%; object-fit: cover; }
|
|
161
|
+
.cms-media-card strong { overflow: hidden; color: var(--cms-text); font-size: .72rem; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
|
162
|
+
.cms-media-insert > span:last-child { color: var(--cms-text-subtle); font-size: .65rem; }
|
|
163
|
+
.cms-media-card footer { display: flex; align-items: center; justify-content: space-between; gap: .35rem; }
|
|
164
|
+
.cms-media-usage { color: var(--cms-text-subtle); font-size: .65rem; }
|
|
165
|
+
.cms-media-usage-used { color: var(--cms-success); }
|
|
166
|
+
.cms-media-delete { padding: .18rem .35rem; color: var(--cms-danger); background: transparent; border: 1px solid var(--cms-border); border-radius: 4px; font-size: .65rem; }
|
|
167
|
+
.cms-media-delete:hover:not(:disabled) { border-color: var(--cms-danger); background: var(--cms-danger-subtle); }
|
|
168
|
+
|
|
169
|
+
@media (max-width: 720px) {
|
|
170
|
+
.cms-topbar { min-height: 58px; padding: 0 .85rem; }
|
|
171
|
+
.cms-actions { gap: .35rem; }
|
|
172
|
+
.cms-notice, .cms-button-quiet, .cms-status { display: none; }
|
|
173
|
+
.cms-workspace { grid-template-columns: 1fr; }
|
|
174
|
+
.cms-sidebar { position: static; height: auto; max-height: 255px; padding: .85rem; border-right: 0; border-bottom: 1px solid var(--cms-border); }
|
|
175
|
+
.cms-document-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .65rem; }
|
|
176
|
+
.cms-main { padding: 1rem .85rem 2rem; }
|
|
177
|
+
.cms-document-actions, .cms-danger-zone { align-items: flex-start; flex-direction: column; }
|
|
178
|
+
.cms-document-action-buttons { width: 100%; justify-content: flex-end; }
|
|
179
|
+
.cms-revision-backdrop { padding: 4rem .75rem .75rem; }
|
|
180
|
+
.cms-revision-drawer { max-height: calc(100vh - 4.75rem); }
|
|
181
|
+
.cms-simple-editor .simple-editor-wrapper { min-height: 450px; }
|
|
182
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Hono, type Context, type MiddlewareHandler } from 'hono';
|
|
2
|
+
import { ensureSchema } from '../db/bootstrap.ts';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { DocumentConflictError, DocumentNotFoundError, createDocument, createDocumentTranslation, deleteDocument, getDocument, listDocuments, listRevisions, restoreRevision, updateDocument } from '../documents/service.ts';
|
|
5
|
+
import { InvalidMediaError, MediaInUseError, MediaNotFoundError, deleteUnusedMedia, listMedia, uploadMedia } from '../media/service.ts';
|
|
6
|
+
import type { RuntimeEnv } from '../env.ts';
|
|
7
|
+
import { defaultLocale, isSupportedLocale, type SupportedLocale } from '../locales.ts';
|
|
8
|
+
import { publishedSnapshot } from '../starlight/snapshot.ts';
|
|
9
|
+
import { adminHtml } from './html.ts';
|
|
10
|
+
import { previewDocument } from './preview.ts';
|
|
11
|
+
import { createFolder, createFolderTranslation, deleteFolder, FolderNotFoundError, listTree, NavigationConflictError, replaceTreeChildren, updateFolder } from '../navigation/service.ts';
|
|
12
|
+
import { PublishDeliveryConflictError, PublishDeliveryNotFoundError, deliverPendingChanges, listPublishDeliveries, publishDocumentAndRequest, requestPublish, retryPublish } from '../publish/service.ts';
|
|
13
|
+
|
|
14
|
+
type AdminEnv = { Bindings: RuntimeEnv };
|
|
15
|
+
const app = new Hono<AdminEnv>();
|
|
16
|
+
const jsonHeaders = { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' };
|
|
17
|
+
const noStore = { 'Cache-Control': 'no-store' };
|
|
18
|
+
|
|
19
|
+
function locale(c: Context<AdminEnv>): SupportedLocale {
|
|
20
|
+
const value = c.req.query('locale') ?? defaultLocale;
|
|
21
|
+
if (!isSupportedLocale(value)) throw new NavigationConflictError('Unsupported language');
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function csrfAllowed(request: Request): boolean {
|
|
26
|
+
if (!['POST', 'PUT', 'DELETE'].includes(request.method)) return true;
|
|
27
|
+
const origin = request.headers.get('origin');
|
|
28
|
+
return request.headers.get('x-requested-with') === 'cloudflare-starlight-cms' &&
|
|
29
|
+
(!origin || origin === new URL(request.url).origin);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const csrf: MiddlewareHandler<AdminEnv> = async (c, next) => {
|
|
33
|
+
if (!csrfAllowed(c.req.raw)) return c.json({ error: 'Cross-site request rejected' }, 403, jsonHeaders);
|
|
34
|
+
await next();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
app.onError((error, c) => {
|
|
39
|
+
if (error instanceof DocumentNotFoundError) return c.json({ error: 'Not found' }, 404, jsonHeaders);
|
|
40
|
+
if (error instanceof DocumentConflictError) return c.json({ error: error.message || 'The document changed. Reload it and try again.' }, 409, jsonHeaders);
|
|
41
|
+
if (error instanceof InvalidMediaError) return c.json({ error: error.message }, 400, jsonHeaders);
|
|
42
|
+
if (error instanceof MediaNotFoundError) return c.json({ error: 'Media not found' }, 404, jsonHeaders);
|
|
43
|
+
if (error instanceof MediaInUseError) return c.json({ error: error.message }, 409, jsonHeaders);
|
|
44
|
+
if (error instanceof FolderNotFoundError) return c.json({ error: 'Folder not found' }, 404, jsonHeaders);
|
|
45
|
+
if (error instanceof NavigationConflictError) return c.json({ error: error.message }, 409, jsonHeaders);
|
|
46
|
+
if (error instanceof PublishDeliveryNotFoundError) return c.json({ error: 'Build request not found' }, 404, jsonHeaders);
|
|
47
|
+
if (error instanceof PublishDeliveryConflictError) return c.json({ error: error.message }, 409, jsonHeaders);
|
|
48
|
+
if (error instanceof z.ZodError) return c.json({ error: 'Invalid request', details: error.issues }, 400, jsonHeaders);
|
|
49
|
+
console.error('Admin API error', error);
|
|
50
|
+
return c.json({ error: 'Request failed' }, 500, jsonHeaders);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
app.use('/admin/*', async (c, next) => {
|
|
54
|
+
if (c.req.path.startsWith('/admin/api/') || c.req.path.startsWith('/admin/preview/') || c.req.path === '/admin/export/snapshot') {
|
|
55
|
+
try {
|
|
56
|
+
await ensureSchema(c.env.DB);
|
|
57
|
+
} catch {
|
|
58
|
+
return c.json({ error: 'Database initialization failed. Retry the request.' }, 503, { ...noStore, 'Retry-After': '5' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await next();
|
|
62
|
+
});
|
|
63
|
+
app.get('/admin/export/snapshot', async (c) => c.json(await publishedSnapshot(c.env), 200, jsonHeaders));
|
|
64
|
+
const adminHome = (c: Context<AdminEnv>) => {
|
|
65
|
+
return c.html(adminHtml(), 200, { 'Cache-Control': 'no-store' });
|
|
66
|
+
};
|
|
67
|
+
app.get('/admin', (c) => c.redirect('/admin/', 302));
|
|
68
|
+
app.get('/admin/', adminHome);
|
|
69
|
+
app.get('/admin/preview/:id', async (c) => previewDocument(c.env, c.req.raw, c.req.param('id'), locale(c)));
|
|
70
|
+
app.get('/admin/app.js', (c) => c.env.ASSETS.fetch(c.req.raw));
|
|
71
|
+
|
|
72
|
+
app.use('/admin/api/*', csrf);
|
|
73
|
+
app.use('/admin/api/*', async (c, next) => {
|
|
74
|
+
await next();
|
|
75
|
+
if (!c.req.path.startsWith('/admin/api/media') && ['POST', 'PUT', 'DELETE'].includes(c.req.method) && c.res.status < 400) {
|
|
76
|
+
c.executionCtx.waitUntil(deliverPendingChanges(c.env));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
app.get('/admin/api/tree', async (c) => c.json(await listTree(c.env, locale(c)), 200, jsonHeaders));
|
|
80
|
+
app.post('/admin/api/folders', async (c) => c.json(await createFolder(c.env, await c.req.json(), locale(c)), 201, jsonHeaders));
|
|
81
|
+
app.put('/admin/api/folders/:id', async (c) => c.json(await updateFolder(c.env, c.req.param('id'), await c.req.json(), locale(c)), 200, jsonHeaders));
|
|
82
|
+
app.post('/admin/api/folders/:id/translations', async (c) => c.json(await createFolderTranslation(c.env, c.req.param('id'), locale(c), (await c.req.json<{ sourceLocale?: SupportedLocale }>()).sourceLocale ?? defaultLocale), 201, jsonHeaders));
|
|
83
|
+
app.delete('/admin/api/folders/:id', async (c) => { await deleteFolder(c.env, c.req.param('id')); return new Response(null, { status: 204, headers: noStore }); });
|
|
84
|
+
app.put('/admin/api/tree/children', async (c) => c.json(await replaceTreeChildren(c.env, await c.req.json()), 200, jsonHeaders));
|
|
85
|
+
app.get('/admin/api/documents', async (c) => c.json(await listDocuments(c.env, locale(c)), 200, jsonHeaders));
|
|
86
|
+
app.post('/admin/api/documents', async (c) => c.json(await createDocument(c.env, await c.req.json(), locale(c)), 201, jsonHeaders));
|
|
87
|
+
app.post('/admin/api/documents/:id/translations', async (c) => c.json(await createDocumentTranslation(c.env, c.req.param('id'), locale(c), (await c.req.json<{ sourceLocale?: SupportedLocale }>()).sourceLocale ?? defaultLocale), 201, jsonHeaders));
|
|
88
|
+
app.get('/admin/api/documents/:id', async (c) => c.json(await getDocument(c.env, c.req.param('id'), locale(c)), 200, jsonHeaders));
|
|
89
|
+
app.put('/admin/api/documents/:id', async (c) => c.json(await updateDocument(c.env, c.req.param('id'), await c.req.json(), locale(c)), 200, jsonHeaders));
|
|
90
|
+
app.delete('/admin/api/documents/:id', async (c) => {
|
|
91
|
+
const { version } = await c.req.json<{ version?: number }>();
|
|
92
|
+
if (!Number.isInteger(version)) return c.json({ error: 'version is required' }, 400, jsonHeaders);
|
|
93
|
+
await deleteDocument(c.env, c.req.param('id'), version!, locale(c));
|
|
94
|
+
return new Response(null, { status: 204, headers: noStore });
|
|
95
|
+
});
|
|
96
|
+
app.post('/admin/api/documents/:id/publish', async (c) => {
|
|
97
|
+
const { version } = await c.req.json<{ version?: number }>();
|
|
98
|
+
if (!Number.isInteger(version)) return c.json({ error: 'version is required' }, 400, jsonHeaders);
|
|
99
|
+
const result = await publishDocumentAndRequest(c.env, c.req.param('id'), version!, locale(c));
|
|
100
|
+
return c.json(result, 200, jsonHeaders);
|
|
101
|
+
});
|
|
102
|
+
app.post('/admin/api/publish/site', async (c) => c.json({ delivery: await requestPublish(c.env, 'site') }, 200, jsonHeaders));
|
|
103
|
+
app.get('/admin/api/publish/deliveries', async (c) => c.json(await listPublishDeliveries(c.env), 200, jsonHeaders));
|
|
104
|
+
app.post('/admin/api/publish/deliveries/:id/retry', async (c) => c.json({ delivery: await retryPublish(c.env, c.req.param('id')) }, 200, jsonHeaders));
|
|
105
|
+
app.get('/admin/api/documents/:id/revisions', async (c) => c.json(await listRevisions(c.env, c.req.param('id'), locale(c)), 200, jsonHeaders));
|
|
106
|
+
app.post('/admin/api/documents/:id/revisions/:revisionId/restore', async (c) => {
|
|
107
|
+
const { version } = await c.req.json<{ version?: number }>();
|
|
108
|
+
if (!Number.isInteger(version)) return c.json({ error: 'version is required' }, 400, jsonHeaders);
|
|
109
|
+
return c.json(await restoreRevision(c.env, c.req.param('id'), c.req.param('revisionId'), version!, locale(c)), 200, jsonHeaders);
|
|
110
|
+
});
|
|
111
|
+
app.get('/admin/api/media', async (c) => c.json(await listMedia(c.env), 200, jsonHeaders));
|
|
112
|
+
app.post('/admin/api/media', async (c) => c.json(await uploadMedia(c.env, c.req.raw), 201, jsonHeaders));
|
|
113
|
+
app.delete('/admin/api/media/:id', async (c) => {
|
|
114
|
+
await deleteUnusedMedia(c.env, c.req.param('id'));
|
|
115
|
+
return new Response(null, { status: 204, headers: noStore });
|
|
116
|
+
});
|
|
117
|
+
app.get('/admin/api/media/object/:key{.+}', async (c) => {
|
|
118
|
+
const key = c.req.param('key');
|
|
119
|
+
if (!/^media\/[a-z0-9-]+\.(png|jpg|webp|avif|mp4|webm)$/.test(key)) return c.json({ error: 'Not found' }, 404, jsonHeaders);
|
|
120
|
+
const object = await c.env.MEDIA.get(key);
|
|
121
|
+
if (!object) return c.json({ error: 'Not found' }, 404, jsonHeaders);
|
|
122
|
+
const headers = new Headers(noStore);
|
|
123
|
+
object.writeHttpMetadata(headers);
|
|
124
|
+
headers.set('ETag', object.httpEtag);
|
|
125
|
+
return new Response(object.body, { headers });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
app.all('/admin/*', (c) => c.env.ASSETS.fetch(c.req.raw));
|
|
129
|
+
|
|
130
|
+
export { app as adminApp };
|