@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.
- package/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- 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: ``
|
|
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: ``
|
|
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
|