@imjp/writenex-astro 0.1.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.
Files changed (141) hide show
  1. package/README.md +539 -0
  2. package/dist/chunk-5PM6EQE5.js +151 -0
  3. package/dist/chunk-5PM6EQE5.js.map +1 -0
  4. package/dist/chunk-7XU5X6CW.js +1331 -0
  5. package/dist/chunk-7XU5X6CW.js.map +1 -0
  6. package/dist/chunk-AAOQHQPU.js +574 -0
  7. package/dist/chunk-AAOQHQPU.js.map +1 -0
  8. package/dist/chunk-CF2XXJFF.js +1410 -0
  9. package/dist/chunk-CF2XXJFF.js.map +1 -0
  10. package/dist/chunk-CRPZUUDU.js +52 -0
  11. package/dist/chunk-CRPZUUDU.js.map +1 -0
  12. package/dist/chunk-CYLDJ3HZ.js +310 -0
  13. package/dist/chunk-CYLDJ3HZ.js.map +1 -0
  14. package/dist/chunk-KIKIPIFA.js +1 -0
  15. package/dist/chunk-KIKIPIFA.js.map +1 -0
  16. package/dist/chunk-XNTQTTJU.js +145 -0
  17. package/dist/chunk-XNTQTTJU.js.map +1 -0
  18. package/dist/client/index.css +2 -0
  19. package/dist/client/index.css.map +1 -0
  20. package/dist/client/index.js +375 -0
  21. package/dist/client/index.js.map +1 -0
  22. package/dist/client/styles.css +584 -0
  23. package/dist/client/variables.css +304 -0
  24. package/dist/config/index.d.ts +54 -0
  25. package/dist/config/index.js +38 -0
  26. package/dist/config/index.js.map +1 -0
  27. package/dist/config-BmEdBDo_.d.ts +220 -0
  28. package/dist/content-BWR52vD-.d.ts +64 -0
  29. package/dist/discovery/index.d.ts +310 -0
  30. package/dist/discovery/index.js +38 -0
  31. package/dist/discovery/index.js.map +1 -0
  32. package/dist/errors-C0iYiDTv.d.ts +107 -0
  33. package/dist/filesystem/index.d.ts +1292 -0
  34. package/dist/filesystem/index.js +203 -0
  35. package/dist/filesystem/index.js.map +1 -0
  36. package/dist/image-FP7w5ZIs.d.ts +47 -0
  37. package/dist/index.d.ts +64 -0
  38. package/dist/index.js +151 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/loader-55LWCXHA.js +12 -0
  41. package/dist/loader-55LWCXHA.js.map +1 -0
  42. package/dist/loader-CrdnaAWR.d.ts +327 -0
  43. package/dist/server/index.d.ts +357 -0
  44. package/dist/server/index.js +37 -0
  45. package/dist/server/index.js.map +1 -0
  46. package/package.json +94 -0
  47. package/src/client/App.tsx +900 -0
  48. package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
  49. package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
  50. package/src/client/components/ConfigPanel/index.ts +6 -0
  51. package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
  52. package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
  53. package/src/client/components/CreateContentModal/index.ts +7 -0
  54. package/src/client/components/Editor/Editor.css +885 -0
  55. package/src/client/components/Editor/Editor.tsx +484 -0
  56. package/src/client/components/Editor/ImageDialog.css +344 -0
  57. package/src/client/components/Editor/ImageDialog.tsx +367 -0
  58. package/src/client/components/Editor/LinkDialog.css +326 -0
  59. package/src/client/components/Editor/LinkDialog.tsx +332 -0
  60. package/src/client/components/Editor/index.ts +6 -0
  61. package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
  62. package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
  63. package/src/client/components/FrontmatterForm/index.ts +7 -0
  64. package/src/client/components/Header/Header.css +300 -0
  65. package/src/client/components/Header/Header.tsx +300 -0
  66. package/src/client/components/Header/index.ts +7 -0
  67. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
  68. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
  69. package/src/client/components/KeyboardShortcuts/index.ts +6 -0
  70. package/src/client/components/LazyEditor.tsx +75 -0
  71. package/src/client/components/LiveRegion/LiveRegion.css +19 -0
  72. package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
  73. package/src/client/components/LiveRegion/index.ts +7 -0
  74. package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
  75. package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
  76. package/src/client/components/SearchReplace/index.ts +7 -0
  77. package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
  78. package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
  79. package/src/client/components/SelectCollectionModal/index.ts +7 -0
  80. package/src/client/components/Sidebar/Sidebar.css +570 -0
  81. package/src/client/components/Sidebar/Sidebar.tsx +617 -0
  82. package/src/client/components/Sidebar/index.ts +7 -0
  83. package/src/client/components/SkipLink/SkipLink.css +51 -0
  84. package/src/client/components/SkipLink/SkipLink.tsx +67 -0
  85. package/src/client/components/SkipLink/index.ts +7 -0
  86. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
  87. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
  88. package/src/client/components/UnsavedChangesModal/index.ts +1 -0
  89. package/src/client/components/VersionHistory/DiffViewer.css +430 -0
  90. package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
  91. package/src/client/components/VersionHistory/VersionActions.css +318 -0
  92. package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
  93. package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
  94. package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
  95. package/src/client/components/VersionHistory/index.ts +9 -0
  96. package/src/client/context/ApiContext.tsx +154 -0
  97. package/src/client/context/ThemeContext.tsx +172 -0
  98. package/src/client/hooks/useAnnounce.ts +201 -0
  99. package/src/client/hooks/useApi.ts +374 -0
  100. package/src/client/hooks/useArrowNavigation.ts +286 -0
  101. package/src/client/hooks/useAutosave.ts +241 -0
  102. package/src/client/hooks/useFocusTrap.ts +178 -0
  103. package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
  104. package/src/client/hooks/useSearch.ts +206 -0
  105. package/src/client/hooks/useVersionHistory.ts +451 -0
  106. package/src/client/index.tsx +70 -0
  107. package/src/client/styles.css +584 -0
  108. package/src/client/utils/focus.ts +57 -0
  109. package/src/client/utils/openInEditor.ts +130 -0
  110. package/src/client/variables.css +304 -0
  111. package/src/config/defaults.ts +109 -0
  112. package/src/config/index.ts +32 -0
  113. package/src/config/loader.ts +174 -0
  114. package/src/config/schema.ts +161 -0
  115. package/src/core/constants.ts +39 -0
  116. package/src/core/errors.ts +739 -0
  117. package/src/core/index.ts +11 -0
  118. package/src/discovery/collections.ts +216 -0
  119. package/src/discovery/index.ts +33 -0
  120. package/src/discovery/patterns.ts +702 -0
  121. package/src/discovery/schema.ts +453 -0
  122. package/src/filesystem/images.ts +798 -0
  123. package/src/filesystem/index.ts +107 -0
  124. package/src/filesystem/reader.ts +452 -0
  125. package/src/filesystem/version-config.ts +390 -0
  126. package/src/filesystem/versions.ts +1339 -0
  127. package/src/filesystem/watcher.ts +226 -0
  128. package/src/filesystem/writer.ts +540 -0
  129. package/src/index.ts +61 -0
  130. package/src/integration.ts +228 -0
  131. package/src/server/assets.ts +254 -0
  132. package/src/server/cache.ts +355 -0
  133. package/src/server/index.ts +33 -0
  134. package/src/server/middleware.ts +209 -0
  135. package/src/server/routes.ts +1428 -0
  136. package/src/types/api.ts +61 -0
  137. package/src/types/config.ts +134 -0
  138. package/src/types/content.ts +64 -0
  139. package/src/types/image.ts +48 -0
  140. package/src/types/index.ts +58 -0
  141. package/src/types/version.ts +117 -0
package/README.md ADDED
@@ -0,0 +1,539 @@
1
+ # @writenex/astro
2
+
3
+ Visual editor for Astro content collections - WYSIWYG editing for your Astro site.
4
+
5
+ ## Overview
6
+
7
+ **@writenex/astro** is an Astro integration that provides a WYSIWYG editor interface for managing your content collections. It runs alongside your Astro dev server and provides direct filesystem access to your content.
8
+
9
+ ### Key Features
10
+
11
+ - **Zero Config** - Auto-discovers your content collections from `src/content/`
12
+ - **WYSIWYG Editor** - MDXEditor-powered markdown editing with live preview
13
+ - **Smart Schema Detection** - Automatically infers frontmatter schema from existing content
14
+ - **Dynamic Forms** - Auto-generated forms based on detected or configured schema
15
+ - **Image Upload** - Drag-and-drop image upload with colocated or public storage
16
+ - **Version History** - Creates automatic shadow copies on save
17
+ - **Autosave** - Automatic saving with configurable interval
18
+ - **Keyboard Shortcuts** - Familiar shortcuts for common actions
19
+ - **Draft Management** - Toggle draft/published status with visual indicators
20
+ - **Search & Filter** - Find content quickly with search and draft filters
21
+ - **Preview Links** - Quick access to preview your content in the browser
22
+ - **Production Safe** - Disabled by default in production builds
23
+ - **Version History** - Automatic shadow copies with restore capability
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Install the integration
28
+
29
+ ```bash
30
+ npx astro add @writenex/astro
31
+ ```
32
+
33
+ This will install the package and automatically configure your `astro.config.mjs`.
34
+
35
+ ### 2. Start your dev server
36
+
37
+ ```bash
38
+ astro dev
39
+ ```
40
+
41
+ ### 3. Open the editor
42
+
43
+ Visit `http://localhost:4321/_writenex` in your browser.
44
+
45
+ That's it! Writenex will auto-discover your content collections and you can start editing.
46
+
47
+ ### Manual Installation
48
+
49
+ If you prefer to install manually:
50
+
51
+ ```bash
52
+ # npm
53
+ npm install @writenex/astro
54
+
55
+ # pnpm
56
+ pnpm add @writenex/astro
57
+
58
+ # yarn
59
+ yarn add @writenex/astro
60
+ ```
61
+
62
+ Then add the integration to your config:
63
+
64
+ ```typescript
65
+ // astro.config.mjs
66
+ import { defineConfig } from "astro/config";
67
+ import writenex from "@writenex/astro";
68
+
69
+ export default defineConfig({
70
+ integrations: [writenex()],
71
+ });
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ ### Zero Config (Recommended)
77
+
78
+ By default, Writenex auto-discovers your content collections from `src/content/` and infers the frontmatter schema from existing files. No configuration needed for most projects.
79
+
80
+ ### Custom Configuration
81
+
82
+ Create `writenex.config.ts` in your project root for full control:
83
+
84
+ ```typescript
85
+ // writenex.config.ts
86
+ import { defineConfig } from "@writenex/astro";
87
+
88
+ export default defineConfig({
89
+ // Define collections explicitly
90
+ collections: [
91
+ {
92
+ name: "blog",
93
+ path: "src/content/blog",
94
+ filePattern: "{slug}.md",
95
+ previewUrl: "/blog/{slug}",
96
+ schema: {
97
+ title: { type: "string", required: true },
98
+ description: { type: "string" },
99
+ pubDate: { type: "date", required: true },
100
+ updatedDate: { type: "date" },
101
+ heroImage: { type: "image" },
102
+ tags: { type: "array", items: "string" },
103
+ draft: { type: "boolean", default: false },
104
+ },
105
+ },
106
+ {
107
+ name: "docs",
108
+ path: "src/content/docs",
109
+ filePattern: "{slug}.md",
110
+ previewUrl: "/docs/{slug}",
111
+ },
112
+ ],
113
+
114
+ // Image upload settings
115
+ images: {
116
+ strategy: "colocated", // 'colocated' | 'public'
117
+ publicPath: "/images", // For 'public' strategy
118
+ storagePath: "public/images", // For 'public' strategy
119
+ },
120
+
121
+ // Editor settings
122
+ editor: {
123
+ autosave: true,
124
+ autosaveInterval: 3000, // milliseconds
125
+ },
126
+ });
127
+ ```
128
+
129
+ ## Integration Options
130
+
131
+ | Option | Type | Default | Description |
132
+ | ----------------- | --------- | ------- | ---------------------------------------------- |
133
+ | `allowProduction` | `boolean` | `false` | Enable in production builds (use with caution) |
134
+
135
+ ```typescript
136
+ // astro.config.mjs
137
+ writenex({
138
+ allowProduction: false, // Keep false for security
139
+ });
140
+ ```
141
+
142
+ The editor is always available at `/_writenex` during development.
143
+
144
+ ## Collection Configuration
145
+
146
+ | Option | Type | Description |
147
+ | ------------- | -------- | ------------------------------------------- |
148
+ | `name` | `string` | Collection identifier (matches folder name) |
149
+ | `path` | `string` | Path to collection directory |
150
+ | `filePattern` | `string` | File naming pattern (e.g., `{slug}.md`) |
151
+ | `previewUrl` | `string` | URL pattern for preview links |
152
+ | `schema` | `object` | Frontmatter schema definition |
153
+ | `images` | `object` | Override image settings for this collection |
154
+
155
+ ### Schema Field Types
156
+
157
+ | Type | Form Component | Example Value |
158
+ | --------- | -------------- | ----------------------- |
159
+ | `string` | Text input | `"Hello World"` |
160
+ | `number` | Number input | `42` |
161
+ | `boolean` | Toggle switch | `true` |
162
+ | `date` | Date picker | `"2024-01-15"` |
163
+ | `array` | Tag input | `["astro", "tutorial"]` |
164
+ | `image` | Image uploader | `"./my-post/hero.jpg"` |
165
+
166
+ ```typescript
167
+ schema: {
168
+ title: { type: "string", required: true },
169
+ description: { type: "string" },
170
+ pubDate: { type: "date", required: true },
171
+ tags: { type: "array", items: "string" },
172
+ draft: { type: "boolean", default: false },
173
+ heroImage: { type: "image" },
174
+ }
175
+ ```
176
+
177
+ ## Image Strategies
178
+
179
+ ### Colocated (Default)
180
+
181
+ Images are stored alongside content files in a folder with the same name:
182
+
183
+ ```
184
+ src/content/blog/
185
+ ├── my-post.md
186
+ └── my-post/
187
+ ├── hero.jpg
188
+ └── diagram.png
189
+ ```
190
+
191
+ Reference in markdown: `![Alt](./my-post/hero.jpg)`
192
+
193
+ ### Public
194
+
195
+ Images are stored in the `public/` directory:
196
+
197
+ ```
198
+ public/
199
+ └── images/
200
+ └── blog/
201
+ └── my-post-hero.jpg
202
+ ```
203
+
204
+ Reference in markdown: `![Alt](/images/blog/my-post-hero.jpg)`
205
+
206
+ Configure in `writenex.config.ts`:
207
+
208
+ ```typescript
209
+ images: {
210
+ strategy: "public",
211
+ publicPath: "/images",
212
+ storagePath: "public/images",
213
+ }
214
+ ```
215
+
216
+ ## Version History
217
+
218
+ Writenex automatically creates shadow copies of your content before each save, providing a safety net for content editors.
219
+
220
+ ### How It Works
221
+
222
+ 1. Before saving content, Writenex creates a snapshot of the current file
223
+ 2. Snapshots are stored in `.writenex/versions/` (excluded from Git by default)
224
+ 3. Old versions are automatically pruned to maintain the configured limit
225
+ 4. Labeled versions (manual snapshots) are preserved during pruning
226
+
227
+ ### Storage Structure
228
+
229
+ ```
230
+ .writenex/versions/
231
+ ├── .gitignore # Excludes version files from Git
232
+ └── blog/
233
+ └── my-post/
234
+ ├── manifest.json # Version metadata
235
+ ├── 2024-12-11T10-30-00-000Z.md
236
+ └── 2024-12-11T11-45-00-000Z.md
237
+ ```
238
+
239
+ ### Configuration
240
+
241
+ ```typescript
242
+ // writenex.config.ts
243
+ import { defineConfig } from "@writenex/astro";
244
+
245
+ export default defineConfig({
246
+ versionHistory: {
247
+ enabled: true, // Enable/disable version history (default: true)
248
+ maxVersions: 20, // Max versions per content item (default: 20)
249
+ storagePath: ".writenex/versions", // Storage path (default)
250
+ },
251
+ });
252
+ ```
253
+
254
+ | Option | Type | Default | Description |
255
+ | ------------- | --------- | -------------------- | ------------------------------------- |
256
+ | `enabled` | `boolean` | `true` | Enable/disable version history |
257
+ | `maxVersions` | `number` | `20` | Maximum unlabeled versions to keep |
258
+ | `storagePath` | `string` | `.writenex/versions` | Storage path relative to project root |
259
+
260
+ ### Version History API
261
+
262
+ | Method | Endpoint | Description |
263
+ | ------ | ------------------------------------------------------ | --------------------- |
264
+ | GET | `/_writenex/api/versions/:collection/:id` | List all versions |
265
+ | GET | `/_writenex/api/versions/:collection/:id/:versionId` | Get specific version |
266
+ | POST | `/_writenex/api/versions/:collection/:id` | Create manual version |
267
+ | POST | `/_writenex/api/versions/:collection/:id/:vid/restore` | Restore version |
268
+ | GET | `/_writenex/api/versions/:collection/:id/:vid/diff` | Get diff data |
269
+ | DELETE | `/_writenex/api/versions/:collection/:id/:versionId` | Delete version |
270
+ | DELETE | `/_writenex/api/versions/:collection/:id` | Clear all versions |
271
+
272
+ ### Example: List Versions
273
+
274
+ ```bash
275
+ curl http://localhost:4321/_writenex/api/versions/blog/my-post
276
+ ```
277
+
278
+ ```json
279
+ {
280
+ "versions": [
281
+ {
282
+ "id": "2024-12-11T12-00-00-000Z",
283
+ "timestamp": "2024-12-11T12:00:00.000Z",
284
+ "preview": "# My Post\n\nThis is the introduction...",
285
+ "size": 2048
286
+ },
287
+ {
288
+ "id": "2024-12-11T11-45-00-000Z",
289
+ "timestamp": "2024-12-11T11:45:00.000Z",
290
+ "preview": "# My Post\n\nEarlier version...",
291
+ "size": 1856,
292
+ "label": "Before major rewrite"
293
+ }
294
+ ]
295
+ }
296
+ ```
297
+
298
+ ### Example: Restore Version
299
+
300
+ ```bash
301
+ curl -X POST http://localhost:4321/_writenex/api/versions/blog/my-post/2024-12-11T11-45-00-000Z/restore
302
+ ```
303
+
304
+ ```json
305
+ {
306
+ "success": true,
307
+ "version": {
308
+ "id": "2024-12-11T11-45-00-000Z",
309
+ "timestamp": "2024-12-11T11:45:00.000Z",
310
+ "preview": "# My Post\n\nEarlier version...",
311
+ "size": 1856
312
+ },
313
+ "safetySnapshot": {
314
+ "id": "2024-12-11T12-05-00-000Z",
315
+ "timestamp": "2024-12-11T12:05:00.000Z",
316
+ "preview": "# My Post\n\nThis is the introduction...",
317
+ "size": 2048,
318
+ "label": "Before restore"
319
+ }
320
+ }
321
+ ```
322
+
323
+ ### Programmatic Usage
324
+
325
+ ```typescript
326
+ import {
327
+ saveVersionWithConfig,
328
+ getVersionsWithConfig,
329
+ restoreVersionWithConfig,
330
+ } from "@writenex/astro";
331
+
332
+ // Save a version with label
333
+ await saveVersionWithConfig(
334
+ "/project",
335
+ "blog",
336
+ "my-post",
337
+ "---\ntitle: My Post\n---\n\nContent...",
338
+ { maxVersions: 50 },
339
+ { label: "Before major changes" }
340
+ );
341
+
342
+ // List versions
343
+ const versions = await getVersionsWithConfig("/project", "blog", "my-post");
344
+
345
+ // Restore a version
346
+ const result = await restoreVersionWithConfig(
347
+ "/project",
348
+ "blog",
349
+ "my-post",
350
+ "2024-12-11T10-30-00-000Z",
351
+ "/project/src/content/blog/my-post.md"
352
+ );
353
+ ```
354
+
355
+ ## File Patterns
356
+
357
+ Writenex supports various file naming patterns with automatic token resolution:
358
+
359
+ | Pattern | Example Output | Use Case |
360
+ | -------------------------------- | ---------------------------- | ---------------------- |
361
+ | `{slug}.md` | `my-post.md` | Simple (default) |
362
+ | `{slug}/index.md` | `my-post/index.md` | Folder-based |
363
+ | `{date}-{slug}.md` | `2024-01-15-my-post.md` | Date-prefixed |
364
+ | `{year}/{slug}.md` | `2024/my-post.md` | Year folders |
365
+ | `{year}/{month}/{slug}.md` | `2024/06/my-post.md` | Year/month folders |
366
+ | `{year}/{month}/{day}/{slug}.md` | `2024/06/15/my-post.md` | Full date folders |
367
+ | `{lang}/{slug}.md` | `en/my-post.md` | i18n/multi-language |
368
+ | `{lang}/{slug}/index.md` | `id/my-post/index.md` | i18n with folder-based |
369
+ | `{category}/{slug}.md` | `tutorials/my-post.md` | Category folders |
370
+ | `{category}/{slug}/index.md` | `tutorials/my-post/index.md` | Category folder-based |
371
+
372
+ Patterns are auto-detected from existing content or can be configured explicitly.
373
+
374
+ ### Supported Tokens
375
+
376
+ | Token | Source | Default Value |
377
+ | ------------ | ------------------------------------------- | --------------- |
378
+ | `{slug}` | Generated from title | Required |
379
+ | `{date}` | `pubDate` from frontmatter | Current date |
380
+ | `{year}` | Year from `pubDate` | Current year |
381
+ | `{month}` | Month from `pubDate` (zero-padded) | Current month |
382
+ | `{day}` | Day from `pubDate` (zero-padded) | Current day |
383
+ | `{lang}` | `lang`/`language`/`locale` from frontmatter | `en` |
384
+ | `{category}` | `category`/`categories[0]` from frontmatter | `uncategorized` |
385
+ | `{author}` | `author` from frontmatter | `anonymous` |
386
+ | `{type}` | `type`/`contentType` from frontmatter | `post` |
387
+ | `{status}` | `status`/`draft` from frontmatter | `published` |
388
+ | `{series}` | `series` from frontmatter | Empty string |
389
+
390
+ ### Custom Tokens
391
+
392
+ Any token in your pattern that is not in the supported list will be resolved from frontmatter. For example, if you use `{project}/{slug}.md`, the `{project}` value will be taken from `frontmatter.project`.
393
+
394
+ ```typescript
395
+ // writenex.config.ts
396
+ collections: [
397
+ {
398
+ name: "docs",
399
+ path: "src/content/docs",
400
+ filePattern: "{project}/{slug}.md", // Custom token
401
+ },
402
+ ];
403
+ ```
404
+
405
+ When creating content with frontmatter `{ project: "my-app", title: "Getting Started" }`, the file will be created at `src/content/docs/my-app/getting-started.md`.
406
+
407
+ ## Keyboard Shortcuts
408
+
409
+ | Shortcut | Action |
410
+ | ---------------------- | ------------------- |
411
+ | `Alt + N` | New Content |
412
+ | `Ctrl/Cmd + S` | Save |
413
+ | `Ctrl/Cmd + P` | Open preview |
414
+ | `Ctrl/Cmd + /` | Show shortcuts help |
415
+ | `Ctrl/Cmd + Shift + R` | Refresh content |
416
+ | `Escape` | Close modal |
417
+
418
+ Press `Ctrl/Cmd + /` in the editor to see all available shortcuts.
419
+
420
+ ## API Endpoints
421
+
422
+ The integration provides REST API endpoints for programmatic access:
423
+
424
+ | Method | Endpoint | Description |
425
+ | ------ | ---------------------------------------- | -------------------------- |
426
+ | GET | `/_writenex/api/collections` | List all collections |
427
+ | GET | `/_writenex/api/config` | Get current configuration |
428
+ | GET | `/_writenex/api/content/:collection` | List content in collection |
429
+ | GET | `/_writenex/api/content/:collection/:id` | Get single content item |
430
+ | POST | `/_writenex/api/content/:collection` | Create new content |
431
+ | PUT | `/_writenex/api/content/:collection/:id` | Update content |
432
+ | DELETE | `/_writenex/api/content/:collection/:id` | Delete content |
433
+ | POST | `/_writenex/api/images` | Upload image |
434
+
435
+ ### Example: List Collections
436
+
437
+ ```bash
438
+ curl http://localhost:4321/_writenex/api/collections
439
+ ```
440
+
441
+ ```json
442
+ {
443
+ "collections": [
444
+ {
445
+ "name": "blog",
446
+ "path": "src/content/blog",
447
+ "filePattern": "{slug}.md",
448
+ "count": 12,
449
+ "schema": { ... }
450
+ }
451
+ ]
452
+ }
453
+ ```
454
+
455
+ ### Example: Get Content
456
+
457
+ ```bash
458
+ curl http://localhost:4321/_writenex/api/content/blog/my-post
459
+ ```
460
+
461
+ ```json
462
+ {
463
+ "id": "my-post",
464
+ "path": "src/content/blog/my-post.md",
465
+ "frontmatter": {
466
+ "title": "My Post",
467
+ "pubDate": "2024-01-15",
468
+ "draft": false
469
+ },
470
+ "body": "# My Post\n\nContent here..."
471
+ }
472
+ ```
473
+
474
+ ## Security
475
+
476
+ ### Production Guard
477
+
478
+ The integration is **disabled by default in production** to prevent accidental exposure. When you run `astro build`, Writenex will not be included.
479
+
480
+ ### Enabling in Production
481
+
482
+ Only enable for staging/preview environments with proper authentication:
483
+
484
+ ```typescript
485
+ // astro.config.mjs - USE WITH CAUTION
486
+ writenex({
487
+ allowProduction: true,
488
+ });
489
+ ```
490
+
491
+ **Warning:** Enabling in production exposes filesystem write access. Only use behind authentication or in trusted environments.
492
+
493
+ ## Troubleshooting
494
+
495
+ ### Editor not loading
496
+
497
+ 1. Ensure you're running `astro dev` (not `astro build`)
498
+ 2. Check the console for errors
499
+ 3. Verify the integration is added to `astro.config.mjs`
500
+
501
+ ### Collections not discovered
502
+
503
+ 1. Ensure content is in `src/content/` directory
504
+ 2. Check that files have `.md` extension
505
+ 3. Verify frontmatter is valid YAML
506
+
507
+ ### Images not uploading
508
+
509
+ 1. Check file permissions on the target directory
510
+ 2. Ensure the image strategy is configured correctly
511
+ 3. For colocated strategy, the content folder must be writable
512
+
513
+ ### Autosave not working
514
+
515
+ 1. Check if autosave is enabled in config
516
+ 2. Verify there are actual changes to save
517
+ 3. Look for errors in the browser console
518
+
519
+ ## Requirements
520
+
521
+ - Astro 4.x or 5.x
522
+ - React 18.x or 19.x
523
+ - Node.js 18+
524
+
525
+ ### Future Plans
526
+
527
+ - MDX full support (components, imports)
528
+ - CLI wrapper (`npx @writenex/astro`)
529
+ - Git integration (auto-commit on save)
530
+ - Media library management
531
+
532
+ ## License
533
+
534
+ MIT - see [LICENSE](../../LICENSE) for details.
535
+
536
+ ## Related
537
+
538
+ - [Writenex](https://writenex.com) - Standalone markdown editor
539
+ - [Writenex Monorepo](../../README.md) - Project overview
@@ -0,0 +1,151 @@
1
+ import {
2
+ applyConfigDefaults
3
+ } from "./chunk-CRPZUUDU.js";
4
+
5
+ // src/config/loader.ts
6
+ import { existsSync } from "fs";
7
+ import { pathToFileURL } from "url";
8
+ import { join, resolve } from "path";
9
+
10
+ // src/config/schema.ts
11
+ import { z } from "zod";
12
+ var fieldTypeSchema = z.enum([
13
+ "string",
14
+ "number",
15
+ "boolean",
16
+ "date",
17
+ "array",
18
+ "image",
19
+ "object"
20
+ ]);
21
+ var schemaFieldSchema = z.object({
22
+ type: fieldTypeSchema,
23
+ required: z.boolean().optional(),
24
+ default: z.unknown().optional(),
25
+ items: z.string().optional(),
26
+ description: z.string().optional()
27
+ });
28
+ var collectionSchemaSchema = z.record(z.string(), schemaFieldSchema);
29
+ var imageStrategySchema = z.enum(["colocated", "public", "custom"]);
30
+ var imageConfigSchema = z.object({
31
+ strategy: imageStrategySchema,
32
+ publicPath: z.string().optional(),
33
+ storagePath: z.string().optional()
34
+ });
35
+ var collectionConfigSchema = z.object({
36
+ name: z.string().min(1, "Collection name is required"),
37
+ path: z.string().min(1, "Collection path is required"),
38
+ filePattern: z.string().optional(),
39
+ previewUrl: z.string().optional(),
40
+ schema: collectionSchemaSchema.optional(),
41
+ images: imageConfigSchema.optional()
42
+ });
43
+ var discoveryConfigSchema = z.object({
44
+ enabled: z.boolean(),
45
+ ignore: z.array(z.string()).optional()
46
+ });
47
+ var editorConfigSchema = z.object({
48
+ autosave: z.boolean().optional(),
49
+ autosaveInterval: z.number().positive().optional()
50
+ });
51
+ var versionHistoryConfigSchema = z.object({
52
+ enabled: z.boolean().optional(),
53
+ maxVersions: z.number().int().positive().optional(),
54
+ storagePath: z.string().optional()
55
+ });
56
+ var writenexConfigSchema = z.object({
57
+ collections: z.array(collectionConfigSchema).optional(),
58
+ images: imageConfigSchema.optional(),
59
+ editor: editorConfigSchema.optional(),
60
+ discovery: discoveryConfigSchema.optional(),
61
+ versionHistory: versionHistoryConfigSchema.optional()
62
+ });
63
+ var writenexOptionsSchema = z.object({
64
+ allowProduction: z.boolean().optional()
65
+ });
66
+ function defineConfig(config) {
67
+ const result = writenexConfigSchema.safeParse(config);
68
+ if (!result.success) {
69
+ const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
70
+ console.warn(`[writenex] Invalid configuration:
71
+ ${errors}`);
72
+ }
73
+ return config;
74
+ }
75
+ function validateConfig(config) {
76
+ return writenexConfigSchema.safeParse(config);
77
+ }
78
+
79
+ // src/config/loader.ts
80
+ var CONFIG_FILE_NAMES = [
81
+ "writenex.config.ts",
82
+ "writenex.config.mts",
83
+ "writenex.config.js",
84
+ "writenex.config.mjs"
85
+ ];
86
+ function findConfigFile(projectRoot) {
87
+ for (const fileName of CONFIG_FILE_NAMES) {
88
+ const filePath = join(projectRoot, fileName);
89
+ if (existsSync(filePath)) {
90
+ return filePath;
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ async function loadConfigFile(configPath) {
96
+ try {
97
+ const fileUrl = pathToFileURL(resolve(configPath)).href;
98
+ const module = await import(fileUrl);
99
+ const config = module.default ?? module.config ?? module;
100
+ return config;
101
+ } catch (error) {
102
+ const message = error instanceof Error ? error.message : String(error);
103
+ throw new Error(
104
+ `Failed to load configuration from ${configPath}: ${message}`
105
+ );
106
+ }
107
+ }
108
+ async function loadConfig(projectRoot) {
109
+ const warnings = [];
110
+ let userConfig = {};
111
+ let configPath = null;
112
+ let hasConfigFile = false;
113
+ configPath = findConfigFile(projectRoot);
114
+ if (configPath) {
115
+ hasConfigFile = true;
116
+ try {
117
+ userConfig = await loadConfigFile(configPath);
118
+ const validationResult = validateConfig(userConfig);
119
+ if (!validationResult.success) {
120
+ const errors = validationResult.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
121
+ warnings.push(`Configuration validation warnings: ${errors}`);
122
+ }
123
+ } catch (error) {
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ warnings.push(`Failed to load config file: ${message}. Using defaults.`);
126
+ userConfig = {};
127
+ }
128
+ }
129
+ const config = applyConfigDefaults(userConfig);
130
+ return {
131
+ config,
132
+ configPath,
133
+ hasConfigFile,
134
+ warnings
135
+ };
136
+ }
137
+ function contentDirectoryExists(projectRoot, contentPath = "src/content") {
138
+ const fullPath = join(projectRoot, contentPath);
139
+ return existsSync(fullPath);
140
+ }
141
+
142
+ export {
143
+ writenexConfigSchema,
144
+ writenexOptionsSchema,
145
+ defineConfig,
146
+ validateConfig,
147
+ findConfigFile,
148
+ loadConfig,
149
+ contentDirectoryExists
150
+ };
151
+ //# sourceMappingURL=chunk-5PM6EQE5.js.map