writr 5.0.4 → 6.0.1
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 +357 -43
- package/dist/writr.d.ts +357 -24
- package/dist/writr.js +376 -28
- package/package.json +17 -10
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Markdown Rendering Simplified
|
|
4
4
|
[](https://github.com/jaredwray/writr/actions/workflows/tests.yml)
|
|
5
|
+
[](https://github.com/jaredwray/writr/actions/workflows/ai-integration-tests.yml)
|
|
5
6
|
[](https://github.com/jaredwray/writr/blob/master/LICENSE)
|
|
6
7
|
[](https://codecov.io/gh/jaredwray/writr)
|
|
7
8
|
[](https://npmjs.com/package/writr)
|
|
@@ -22,21 +23,13 @@
|
|
|
22
23
|
* Emoji Support (remark-emoji).
|
|
23
24
|
* MDX Support (remark-mdx).
|
|
24
25
|
* Built in Hooks for adding code to render pipeline.
|
|
25
|
-
|
|
26
|
-
# Unified Processor Engine
|
|
27
|
-
|
|
28
|
-
Writr builds on top of the open source [unified](https://github.com/unifiedjs/unified) processor – the core project that powers
|
|
29
|
-
[remark](https://github.com/remarkjs/remark), [rehype](https://github.com/rehypejs/rehype), and many other content tools. Unified
|
|
30
|
-
provides a pluggable pipeline where each plugin transforms a syntax tree. Writr configures a default set of plugins to turn
|
|
31
|
-
Markdown into HTML, but you can access the processor through the `.engine` property to add your own behavior with
|
|
32
|
-
`writr.engine.use(myPlugin)`. The [unified documentation](https://unifiedjs.com/) has more details and guides for building
|
|
33
|
-
plugins and working with the processor directly.
|
|
26
|
+
* AI-powered metadata generation, SEO, and translation via the [Vercel AI SDK](https://sdk.vercel.ai).
|
|
34
27
|
|
|
35
28
|
# Table of Contents
|
|
36
|
-
- [Unified Processor Engine](#unified-processor-engine)
|
|
37
29
|
- [Getting Started](#getting-started)
|
|
38
30
|
- [API](#api)
|
|
39
31
|
- [`new Writr(arg?: string | WritrOptions, options?: WritrOptions)`](#new-writrarg-string--writroptions-options-writroptions)
|
|
32
|
+
- [`.ai`](#writrai)
|
|
40
33
|
- [`.content`](#content)
|
|
41
34
|
- [`.body`](#body)
|
|
42
35
|
- [`.options`](#options)
|
|
@@ -68,6 +61,19 @@ plugins and working with the processor directly.
|
|
|
68
61
|
- [Methods that Emit Errors](#methods-that-emit-errors)
|
|
69
62
|
- [Error Event Examples](#error-event-examples)
|
|
70
63
|
- [Event Emitter Methods](#event-emitter-methods)
|
|
64
|
+
- [AI](#ai)
|
|
65
|
+
- [AI Options](#ai-options)
|
|
66
|
+
- [AI Provider Configuration](#ai-provider-configuration)
|
|
67
|
+
- [Metadata](#metadata)
|
|
68
|
+
- [Generating Metadata](#generating-metadata)
|
|
69
|
+
- [Applying Metadata to Frontmatter](#applying-metadata-to-frontmatter)
|
|
70
|
+
- [Overwrite](#overwrite)
|
|
71
|
+
- [Field Mapping](#field-mapping)
|
|
72
|
+
- [SEO](#seo)
|
|
73
|
+
- [Translation](#translation)
|
|
74
|
+
- [Using WritrAI Directly](#using-writrai-directly)
|
|
75
|
+
- [Migrating to v6](#migrating-to-v6)
|
|
76
|
+
- [Unified Processor Engine](#unified-processor-engine)
|
|
71
77
|
- [Benchmarks](#benchmarks)
|
|
72
78
|
- [ESM and Node Version Support](#esm-and-node-version-support)
|
|
73
79
|
- [Code of Conduct and Contributing](#code-of-conduct-and-contributing)
|
|
@@ -104,7 +110,6 @@ An example passing in the options also via the constructor:
|
|
|
104
110
|
```javascript
|
|
105
111
|
import { Writr, WritrOptions } from 'writr';
|
|
106
112
|
const writrOptions = {
|
|
107
|
-
throwErrors: true,
|
|
108
113
|
renderOptions: {
|
|
109
114
|
emoji: true,
|
|
110
115
|
toc: true,
|
|
@@ -129,7 +134,6 @@ By default the constructor takes in a markdown `string` or `WritrOptions` in the
|
|
|
129
134
|
```javascript
|
|
130
135
|
import { Writr, WritrOptions } from 'writr';
|
|
131
136
|
const writrOptions = {
|
|
132
|
-
throwErrors: true,
|
|
133
137
|
renderOptions: {
|
|
134
138
|
emoji: true,
|
|
135
139
|
toc: true,
|
|
@@ -178,7 +182,6 @@ Accessing the default options for this instance of Writr. Here is the default se
|
|
|
178
182
|
|
|
179
183
|
```javascript
|
|
180
184
|
{
|
|
181
|
-
throwErrors: false,
|
|
182
185
|
renderOptions: {
|
|
183
186
|
emoji: true,
|
|
184
187
|
toc: true,
|
|
@@ -186,8 +189,8 @@ Accessing the default options for this instance of Writr. Here is the default se
|
|
|
186
189
|
highlight: true,
|
|
187
190
|
gfm: true,
|
|
188
191
|
math: true,
|
|
189
|
-
mdx:
|
|
190
|
-
caching:
|
|
192
|
+
mdx: false,
|
|
193
|
+
caching: true,
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
196
|
```
|
|
@@ -335,7 +338,7 @@ console.log(writr.content); // Still "# Hello World\n\nThis is a test."
|
|
|
335
338
|
const invalidWritr = new Writr('Put invalid markdown here');
|
|
336
339
|
const errorResult = await invalidWritr.validate();
|
|
337
340
|
console.log(errorResult.valid); // false
|
|
338
|
-
console.log(errorResult.error?.message); // "
|
|
341
|
+
console.log(errorResult.error?.message); // "Invalid plugin"
|
|
339
342
|
```
|
|
340
343
|
|
|
341
344
|
## `.validateSync(content?: string, options?: RenderOptions)`
|
|
@@ -623,38 +626,35 @@ writr.on('error', (error) => {
|
|
|
623
626
|
// Log to error tracking service, display to user, etc.
|
|
624
627
|
});
|
|
625
628
|
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
} catch (error) {
|
|
630
|
-
// Error is also thrown, so you can handle it here too
|
|
631
|
-
}
|
|
629
|
+
// With a listener registered, errors are emitted to the listener
|
|
630
|
+
// and the method returns its fallback value (e.g. "" for render)
|
|
631
|
+
const html = await writr.render();
|
|
632
632
|
```
|
|
633
633
|
|
|
634
634
|
### Methods that Emit Errors
|
|
635
635
|
|
|
636
|
-
|
|
636
|
+
All methods use an emit-only error pattern — they call `this.emit('error', error)` but never explicitly re-throw. If no error listener is registered and `throwOnEmptyListeners` is `true` (the default), the `emit('error')` call itself will throw, following standard Node.js EventEmitter behavior.
|
|
637
637
|
|
|
638
|
-
**Rendering Methods
|
|
639
|
-
- `render()` - Emits error
|
|
640
|
-
- `renderSync()` - Emits error
|
|
641
|
-
- `renderReact()` - Emits error
|
|
642
|
-
- `renderReactSync()` - Emits error
|
|
638
|
+
**Rendering Methods** — emit error, return `""`:
|
|
639
|
+
- `render()` - Emits error when markdown rendering fails, returns empty string
|
|
640
|
+
- `renderSync()` - Emits error when markdown rendering fails, returns empty string
|
|
641
|
+
- `renderReact()` - Emits error when React rendering fails, returns empty string
|
|
642
|
+
- `renderReactSync()` - Emits error when React rendering fails, returns empty string
|
|
643
643
|
|
|
644
644
|
**Validation Methods:**
|
|
645
|
-
- `validate()` -
|
|
646
|
-
- `validateSync()` - Emits error
|
|
647
|
-
|
|
648
|
-
**File Operations
|
|
649
|
-
- `renderToFile()` - Emits error when file writing fails
|
|
650
|
-
- `renderToFileSync()` - Emits error when file writing fails
|
|
651
|
-
- `loadFromFile()` - Emits error when file reading fails
|
|
652
|
-
- `loadFromFileSync()` - Emits error when file reading fails
|
|
653
|
-
- `saveToFile()` - Emits error when file writing fails
|
|
654
|
-
- `saveToFileSync()` - Emits error when file writing fails
|
|
655
|
-
|
|
656
|
-
**Front Matter Operations
|
|
657
|
-
- `frontMatter` getter - Emits error when YAML parsing fails
|
|
645
|
+
- `validate()` - Does **not** emit errors. Returns `{ valid: false, error }` on failure
|
|
646
|
+
- `validateSync()` - Emits error and returns `{ valid: false, error }` on failure
|
|
647
|
+
|
|
648
|
+
**File Operations** — emit error, return void:
|
|
649
|
+
- `renderToFile()` - Emits error when rendering or file writing fails
|
|
650
|
+
- `renderToFileSync()` - Emits error when rendering or file writing fails
|
|
651
|
+
- `loadFromFile()` - Emits error when file reading fails
|
|
652
|
+
- `loadFromFileSync()` - Emits error when file reading fails
|
|
653
|
+
- `saveToFile()` - Emits error when file writing fails
|
|
654
|
+
- `saveToFileSync()` - Emits error when file writing fails
|
|
655
|
+
|
|
656
|
+
**Front Matter Operations** — emit error, return fallback:
|
|
657
|
+
- `frontMatter` getter - Emits error when YAML parsing fails, returns `{}`
|
|
658
658
|
- `frontMatter` setter - Emits error when YAML serialization fails
|
|
659
659
|
|
|
660
660
|
### Error Event Examples
|
|
@@ -706,15 +706,16 @@ if (!result.valid) {
|
|
|
706
706
|
```javascript
|
|
707
707
|
import { Writr } from 'writr';
|
|
708
708
|
|
|
709
|
-
const writr = new Writr('# Content'
|
|
709
|
+
const writr = new Writr('# Content');
|
|
710
710
|
|
|
711
711
|
writr.on('error', (error) => {
|
|
712
712
|
console.error('File operation failed:', error.message);
|
|
713
713
|
// Handle gracefully - maybe use default content
|
|
714
714
|
});
|
|
715
715
|
|
|
716
|
-
//
|
|
716
|
+
// With a listener registered, errors are emitted and the method returns normally
|
|
717
717
|
await writr.loadFromFile('./maybe-missing.md');
|
|
718
|
+
// Note: without a listener, this will throw by default (throwOnEmptyListeners is true)
|
|
718
719
|
```
|
|
719
720
|
|
|
720
721
|
### Event Emitter Methods
|
|
@@ -728,6 +729,319 @@ Since Writr extends Hookified, you have access to standard event emitter methods
|
|
|
728
729
|
|
|
729
730
|
For more information about event handling capabilities, see the [Hookified documentation](https://github.com/jaredwray/hookified).
|
|
730
731
|
|
|
732
|
+
# AI
|
|
733
|
+
|
|
734
|
+
Writr includes built-in AI capabilities for metadata generation, SEO, and translation powered by the [Vercel AI SDK](https://sdk.vercel.ai). Plug in any supported model provider (OpenAI, Anthropic, Google, etc.) via the `ai` option.
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
import { Writr } from 'writr';
|
|
738
|
+
import { openai } from '@ai-sdk/openai';
|
|
739
|
+
|
|
740
|
+
const writr = new Writr('# My Document\n\nSome markdown content here.', {
|
|
741
|
+
ai: { model: openai('gpt-4.1-mini') },
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// Generate metadata
|
|
745
|
+
const metadata = await writr.ai.getMetadata();
|
|
746
|
+
|
|
747
|
+
// Generate only specific fields
|
|
748
|
+
const metadata = await writr.ai.getMetadata({ title: true, description: true });
|
|
749
|
+
|
|
750
|
+
// Generate SEO metadata
|
|
751
|
+
const seo = await writr.ai.getSEO();
|
|
752
|
+
|
|
753
|
+
// Translate to Spanish
|
|
754
|
+
const translated = await writr.ai.getTranslation({ to: 'es' });
|
|
755
|
+
|
|
756
|
+
// Apply generated metadata to frontmatter
|
|
757
|
+
const result = await writr.ai.applyMetadata({
|
|
758
|
+
generate: { description: true, category: true },
|
|
759
|
+
overwrite: true,
|
|
760
|
+
});
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
## AI Options
|
|
764
|
+
|
|
765
|
+
Pass `ai` in the `WritrOptions` to enable AI features:
|
|
766
|
+
|
|
767
|
+
| Property | Type | Required | Description |
|
|
768
|
+
|----------|------|----------|-------------|
|
|
769
|
+
| `model` | `LanguageModel` | Yes | The AI SDK model instance (e.g. `openai("gpt-4.1-mini")`). |
|
|
770
|
+
| `cache` | `boolean` | No | Enables in-memory caching of AI results. |
|
|
771
|
+
| `prompts` | `WritrAIPrompts` | No | Custom prompt overrides for metadata, SEO, and translation. |
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
774
|
+
const writr = new Writr('# My Document', {
|
|
775
|
+
ai: {
|
|
776
|
+
model: openai('gpt-4.1-mini'),
|
|
777
|
+
cache: true,
|
|
778
|
+
},
|
|
779
|
+
});
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
## AI Provider Configuration
|
|
783
|
+
|
|
784
|
+
By default, the provider imports read API keys from environment variables:
|
|
785
|
+
|
|
786
|
+
| Provider | Import | Environment Variable |
|
|
787
|
+
|----------|--------|---------------------|
|
|
788
|
+
| OpenAI | `openai` from `@ai-sdk/openai` | `OPENAI_API_KEY` |
|
|
789
|
+
| Anthropic | `anthropic` from `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` |
|
|
790
|
+
| Google | `google` from `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` |
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
// Uses OPENAI_API_KEY from environment
|
|
794
|
+
import { openai } from '@ai-sdk/openai';
|
|
795
|
+
|
|
796
|
+
const writr = new Writr('# Hello', { ai: { model: openai('gpt-4.1-mini') } });
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
To set API keys programmatically, use the provider factory functions instead:
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
import { Writr } from 'writr';
|
|
803
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
804
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
805
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
806
|
+
|
|
807
|
+
// OpenAI
|
|
808
|
+
const openai = createOpenAI({ apiKey: 'your-openai-key' });
|
|
809
|
+
const writr = new Writr('# Hello', { ai: { model: openai('gpt-4.1-mini') } });
|
|
810
|
+
|
|
811
|
+
// Anthropic
|
|
812
|
+
const anthropic = createAnthropic({ apiKey: 'your-anthropic-key' });
|
|
813
|
+
const writr = new Writr('# Hello', { ai: { model: anthropic('claude-sonnet-4-20250514') } });
|
|
814
|
+
|
|
815
|
+
// Google
|
|
816
|
+
const google = createGoogleGenerativeAI({ apiKey: 'your-google-key' });
|
|
817
|
+
const writr = new Writr('# Hello', { ai: { model: google('gemini-2.0-flash') } });
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
## Metadata
|
|
821
|
+
|
|
822
|
+
Generate metadata from your document content using `writr.ai.getMetadata()`, or generate and apply it directly to frontmatter with `writr.ai.applyMetadata()`.
|
|
823
|
+
|
|
824
|
+
### Generating Metadata
|
|
825
|
+
|
|
826
|
+
`getMetadata()` analyzes the document and returns a `WritrMetadata` object. By default all fields are generated. Pass options to select specific fields.
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
// Generate all metadata fields
|
|
830
|
+
const metadata = await writr.ai.getMetadata();
|
|
831
|
+
console.log(metadata.title); // "Getting Started with Writr"
|
|
832
|
+
console.log(metadata.tags); // ["markdown", "rendering", "typescript"]
|
|
833
|
+
console.log(metadata.description); // "A guide to using Writr for markdown processing."
|
|
834
|
+
console.log(metadata.readingTime); // 3 (minutes)
|
|
835
|
+
console.log(metadata.wordCount); // 450
|
|
836
|
+
|
|
837
|
+
// Generate only specific fields
|
|
838
|
+
const partial = await writr.ai.getMetadata({
|
|
839
|
+
title: true,
|
|
840
|
+
description: true,
|
|
841
|
+
tags: true,
|
|
842
|
+
});
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**Generated fields:**
|
|
846
|
+
|
|
847
|
+
| Field | Type | Description |
|
|
848
|
+
|-------|------|-------------|
|
|
849
|
+
| `title` | `string` | The best-fit title for the document. |
|
|
850
|
+
| `description` | `string` | A concise meta-style description of the document. |
|
|
851
|
+
| `tags` | `string[]` | Human-friendly labels for organizing the document. |
|
|
852
|
+
| `keywords` | `string[]` | Search-oriented terms related to the document content. |
|
|
853
|
+
| `preview` | `string` | A short teaser or preview snippet of the content. |
|
|
854
|
+
| `summary` | `string` | A slightly longer overview of the document. |
|
|
855
|
+
| `category` | `string` | A broad grouping such as "docs", "guide", or "blog". |
|
|
856
|
+
| `topic` | `string` | The primary subject the document is about. |
|
|
857
|
+
| `audience` | `string` | The intended audience for the document. |
|
|
858
|
+
| `difficulty` | `"beginner" \| "intermediate" \| "advanced"` | The estimated skill level required. |
|
|
859
|
+
| `readingTime` | `number` | Estimated reading time in minutes (computed, not AI-generated). |
|
|
860
|
+
| `wordCount` | `number` | Total word count of the document (computed, not AI-generated). |
|
|
861
|
+
|
|
862
|
+
### Applying Metadata to Frontmatter
|
|
863
|
+
|
|
864
|
+
`applyMetadata()` generates metadata and writes it into the document's frontmatter. The result tells you exactly what happened:
|
|
865
|
+
|
|
866
|
+
- **`applied`** — fields that were newly written because they were missing from frontmatter.
|
|
867
|
+
- **`skipped`** — fields that already existed and were not overwritten.
|
|
868
|
+
- **`overwritten`** — fields that replaced existing frontmatter values.
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
const result = await writr.ai.applyMetadata();
|
|
872
|
+
console.log(result.applied); // ["description", "tags", "category"]
|
|
873
|
+
console.log(result.skipped); // ["title"] (already existed)
|
|
874
|
+
console.log(result.overwritten); // []
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### Overwrite
|
|
878
|
+
|
|
879
|
+
By default, `applyMetadata()` only fills in missing fields — existing frontmatter values are never touched. The `overwrite` option changes this behavior:
|
|
880
|
+
|
|
881
|
+
- **Default (no overwrite):** Only missing fields are written. Existing values are preserved.
|
|
882
|
+
- **`overwrite: true`:** All generated fields replace existing frontmatter values.
|
|
883
|
+
- **`overwrite: ['field1', 'field2']`:** Only the listed fields are overwritten. Other existing values are preserved.
|
|
884
|
+
|
|
885
|
+
```typescript
|
|
886
|
+
// Overwrite all generated fields, even if they already exist
|
|
887
|
+
const result = await writr.ai.applyMetadata({
|
|
888
|
+
generate: { title: true, description: true },
|
|
889
|
+
overwrite: true,
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// Only overwrite title, leave description alone if it already exists
|
|
893
|
+
const result = await writr.ai.applyMetadata({
|
|
894
|
+
generate: { title: true, description: true, category: true },
|
|
895
|
+
overwrite: ['title'],
|
|
896
|
+
});
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
### Field Mapping
|
|
900
|
+
|
|
901
|
+
The `fieldMap` option maps generated metadata keys to different frontmatter field names. This is useful when your frontmatter schema uses different naming conventions than the default metadata keys.
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
const result = await writr.ai.applyMetadata({
|
|
905
|
+
generate: { description: true, tags: true },
|
|
906
|
+
fieldMap: {
|
|
907
|
+
description: 'meta_description',
|
|
908
|
+
tags: 'labels',
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
// writr.frontMatter.meta_description === "A guide to..."
|
|
912
|
+
// writr.frontMatter.labels === ["markdown", "rendering"]
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
The mapping applies to all behaviors — field existence checks, overwrites, and skips all use the mapped key when checking frontmatter.
|
|
916
|
+
|
|
917
|
+
## SEO
|
|
918
|
+
|
|
919
|
+
Generate SEO metadata using `writr.ai.getSEO()`. By default all fields are generated. Pass options to select specific fields.
|
|
920
|
+
|
|
921
|
+
```typescript
|
|
922
|
+
const seo = await writr.ai.getSEO();
|
|
923
|
+
console.log(seo.slug); // "getting-started-with-writr"
|
|
924
|
+
console.log(seo.openGraph?.title); // "Getting Started with Writr"
|
|
925
|
+
|
|
926
|
+
// Generate only a slug
|
|
927
|
+
const seo = await writr.ai.getSEO({ slug: true });
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
**Available fields:** `slug`, `openGraph` (includes `title`, `description`, `image`).
|
|
931
|
+
|
|
932
|
+
## Translation
|
|
933
|
+
|
|
934
|
+
Translate the document into another language using `writr.ai.getTranslation()`. Returns a new `Writr` instance with the translated content.
|
|
935
|
+
|
|
936
|
+
```typescript
|
|
937
|
+
const spanish = await writr.ai.getTranslation({ to: 'es' });
|
|
938
|
+
console.log(spanish.body); // Spanish markdown
|
|
939
|
+
|
|
940
|
+
// With source language and frontmatter translation
|
|
941
|
+
const french = await writr.ai.getTranslation({
|
|
942
|
+
to: 'fr',
|
|
943
|
+
from: 'en',
|
|
944
|
+
translateFrontMatter: true,
|
|
945
|
+
});
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
| Option | Type | Required | Description |
|
|
949
|
+
|--------|------|----------|-------------|
|
|
950
|
+
| `to` | `string` | Yes | Target language or locale. |
|
|
951
|
+
| `from` | `string` | No | Source language or locale. |
|
|
952
|
+
| `translateFrontMatter` | `boolean` | No | Also translate frontmatter string values. |
|
|
953
|
+
|
|
954
|
+
## Using WritrAI Directly
|
|
955
|
+
|
|
956
|
+
`WritrAI` is exported as a named export and can be instantiated independently from the `Writr` constructor. This is useful when you want to configure the AI instance separately or swap models on the fly.
|
|
957
|
+
|
|
958
|
+
```typescript
|
|
959
|
+
import { Writr, WritrAI } from 'writr';
|
|
960
|
+
import { openai } from '@ai-sdk/openai';
|
|
961
|
+
|
|
962
|
+
const writr = new Writr('# My Document\n\nSome markdown content here.');
|
|
963
|
+
const ai = new WritrAI(writr, {
|
|
964
|
+
model: openai('gpt-4.1-mini'),
|
|
965
|
+
cache: true,
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// Generate metadata
|
|
969
|
+
const metadata = await ai.getMetadata();
|
|
970
|
+
console.log(metadata.title);
|
|
971
|
+
console.log(metadata.tags);
|
|
972
|
+
|
|
973
|
+
// Generate SEO data
|
|
974
|
+
const seo = await ai.getSEO();
|
|
975
|
+
console.log(seo.slug);
|
|
976
|
+
|
|
977
|
+
// Translate
|
|
978
|
+
const translated = await ai.getTranslation({ to: 'es' });
|
|
979
|
+
console.log(translated.body);
|
|
980
|
+
|
|
981
|
+
// Apply metadata to frontmatter
|
|
982
|
+
const result = await ai.applyMetadata({
|
|
983
|
+
generate: { title: true, description: true, tags: true },
|
|
984
|
+
overwrite: true,
|
|
985
|
+
});
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
# Migrating to v6
|
|
989
|
+
|
|
990
|
+
Writr v6 upgrades [hookified](https://github.com/jaredwray/hookified) from v1 to v2 and removes `throwErrors` in favor of hookified's built-in error handling options.
|
|
991
|
+
|
|
992
|
+
## Breaking Changes
|
|
993
|
+
|
|
994
|
+
### `throwErrors` removed
|
|
995
|
+
|
|
996
|
+
The `throwErrors` option has been removed from `WritrOptions`. Use `throwOnEmitError` instead, which is provided by hookified's `HookifiedOptions` (now spread into `WritrOptions`).
|
|
997
|
+
|
|
998
|
+
**Before (v5):**
|
|
999
|
+
|
|
1000
|
+
```typescript
|
|
1001
|
+
const writr = new Writr('# Hello', { throwErrors: true });
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
**After (v6):**
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
const writr = new Writr('# Hello', { throwOnEmitError: true });
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
### Error handling redesign
|
|
1011
|
+
|
|
1012
|
+
All methods now use an **emit-only** pattern — errors are emitted via `emit('error', error)` but never explicitly re-thrown. Methods return fallback values on error (`""` for render methods, `{}` for frontMatter getter, `{ valid: false, error }` for validate).
|
|
1013
|
+
|
|
1014
|
+
**How errors propagate:**
|
|
1015
|
+
|
|
1016
|
+
- **With a listener registered:** Errors are passed to the listener. The method returns its fallback value without throwing.
|
|
1017
|
+
- **Without a listener (default behavior):** Since `throwOnEmptyListeners` defaults to `true`, the `emit('error')` call itself throws, following standard Node.js EventEmitter behavior. This means unhandled errors will still surface as exceptions.
|
|
1018
|
+
- **With `throwOnEmitError: true`:** Every `emit('error')` call throws, even when listeners are registered. This affects all methods that emit errors.
|
|
1019
|
+
|
|
1020
|
+
**Other changes:**
|
|
1021
|
+
|
|
1022
|
+
- `render()` and `renderSync()` no longer throw wrapped `"Failed to render markdown: ..."` errors. They emit the original error and return `""`.
|
|
1023
|
+
- `validate()` (async) no longer emits errors — it only returns `{ valid: false, error }`. `validateSync()` still emits.
|
|
1024
|
+
|
|
1025
|
+
### hookified v2
|
|
1026
|
+
|
|
1027
|
+
Writr now uses hookified v2 which introduces several new options available through `WritrOptions`:
|
|
1028
|
+
|
|
1029
|
+
- `throwOnEmitError` — Throw when `emit("error")` is called, even with listeners (default: `false`)
|
|
1030
|
+
- `throwOnHookError` — Throw when a hook handler throws (default: `false`)
|
|
1031
|
+
- `throwOnEmptyListeners` — Throw when emitting `error` with no listeners (default: `true`)
|
|
1032
|
+
- `eventLogger` — Logger instance for event logging
|
|
1033
|
+
|
|
1034
|
+
See the [hookified documentation](https://github.com/jaredwray/hookified) for full details.
|
|
1035
|
+
|
|
1036
|
+
# Unified Processor Engine
|
|
1037
|
+
|
|
1038
|
+
Writr builds on top of the open source [unified](https://github.com/unifiedjs/unified) processor – the core project that powers
|
|
1039
|
+
[remark](https://github.com/remarkjs/remark), [rehype](https://github.com/rehypejs/rehype), and many other content tools. Unified
|
|
1040
|
+
provides a pluggable pipeline where each plugin transforms a syntax tree. Writr configures a default set of plugins to turn
|
|
1041
|
+
Markdown into HTML, but you can access the processor through the `.engine` property to add your own behavior with
|
|
1042
|
+
`writr.engine.use(myPlugin)`. The [unified documentation](https://unifiedjs.com/) has more details and guides for building
|
|
1043
|
+
plugins and working with the processor directly.
|
|
1044
|
+
|
|
731
1045
|
# Benchmarks
|
|
732
1046
|
|
|
733
1047
|
This is a comparison with minimal configuration where we have disabled all rendering pipeline and just did straight caching + rendering to compare it against the fastest:
|