{"id":15048814,"url":"https://github.com/jaredwray/writr","last_synced_at":"2026-04-02T14:47:19.119Z","repository":{"id":37484045,"uuid":"131678352","full_name":"jaredwray/writr","owner":"jaredwray","description":"Markdown Rendering Simplified","archived":false,"fork":false,"pushed_at":"2026-03-27T03:05:51.000Z","size":1936,"stargazers_count":14,"open_issues_count":0,"forks_count":8,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-27T15:11:09.919Z","etag":null,"topics":["emoji","github-flavored-markdown","highlighting","html","markdown","markdown-processor","markdown-to-html","remark","renderer","syntax","table-of-contents","toc"],"latest_commit_sha":null,"homepage":"https://writr.org","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jaredwray.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2018-05-01T05:04:51.000Z","updated_at":"2026-03-27T03:05:49.000Z","dependencies_parsed_at":"2023-10-29T00:23:16.758Z","dependency_job_id":"da88b498-7051-419d-9dbe-fd0fbf9d4953","html_url":"https://github.com/jaredwray/writr","commit_stats":null,"previous_names":[],"tags_count":74,"template":false,"template_full_name":null,"purl":"pkg:github/jaredwray/writr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fwritr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fwritr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fwritr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fwritr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jaredwray","download_url":"https://codeload.github.com/jaredwray/writr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredwray%2Fwritr/sbom","scorecard":{"id":506490,"data":{"date":"2025-08-11","repo":{"name":"github.com/jaredwray/writr","commit":"a163e54d6aae70d8ab35c486c624dce101ee9d43"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":5.2,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/17 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":10,"reason":"30 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Security-Policy","score":3,"reason":"security policy file detected","details":["Info: security policy file detected: SECURITY.md:1","Warn: no linked content found","Warn: One or no descriptive hints of disclosure, vulnerability, and/or timelines in security policy","Info: Found text in security policy: SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/code-coverage.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/code-coverage.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/code-coverage.yml:21: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/code-coverage.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/code-coverage.yml:35: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/code-coverage.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/code-ql.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/code-ql.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/code-ql.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/code-ql.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/code-ql.yml:51: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/code-ql.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/code-ql.yml:65: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/code-ql.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy-site.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/deploy-site.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy-site.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/deploy-site.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy-site.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/deploy-site.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/release.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:33: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/tests.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/jaredwray/writr/tests.yml/main?enable=pin","Warn: npmCommand not pinned by hash: .github/workflows/code-coverage.yml:27","Warn: npmCommand not pinned by hash: .github/workflows/deploy-site.yml:28","Warn: npmCommand not pinned by hash: .github/workflows/release.yml:25","Warn: npmCommand not pinned by hash: .github/workflows/tests.yml:31","Info:   0 out of  12 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   3 third-party GitHubAction dependencies pinned","Info:   0 out of   4 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Info: topLevel 'contents' permission set to 'read': .github/workflows/code-coverage.yml:11","Warn: no topLevel permission defined: .github/workflows/code-ql.yml:1","Info: topLevel 'contents' permission set to 'read': .github/workflows/deploy-site.yml:9","Info: topLevel 'contents' permission set to 'read': .github/workflows/release.yml:9","Info: topLevel 'contents' permission set to 'read': .github/workflows/tests.yml:11","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":10,"reason":"SAST tool is run on all commits","details":["Info: SAST configuration detected: CodeQL","Info: all commits (26) are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-19T23:20:47.822Z","repository_id":37484045,"created_at":"2025-08-19T23:20:47.822Z","updated_at":"2025-08-19T23:20:47.822Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31308426,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["emoji","github-flavored-markdown","highlighting","html","markdown","markdown-processor","markdown-to-html","remark","renderer","syntax","table-of-contents","toc"],"created_at":"2024-09-24T21:16:27.706Z","updated_at":"2026-04-02T14:47:19.107Z","avatar_url":"https://github.com/jaredwray.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Writr](site/logo.svg)\n\n# Markdown Rendering Simplified\n[![tests](https://github.com/jaredwray/writr/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/writr/actions/workflows/tests.yml)\n[![ai-integration-tests](https://github.com/jaredwray/writr/actions/workflows/ai-integration-tests.yml/badge.svg)](https://github.com/jaredwray/writr/actions/workflows/ai-integration-tests.yml)\n[![GitHub license](https://img.shields.io/github/license/jaredwray/writr)](https://github.com/jaredwray/writr/blob/master/LICENSE)\n[![codecov](https://codecov.io/gh/jaredwray/writr/branch/master/graph/badge.svg?token=1YdMesM07X)](https://codecov.io/gh/jaredwray/writr)\n[![npm](https://img.shields.io/npm/dm/writr)](https://npmjs.com/package/writr)\n[![npm](https://img.shields.io/npm/v/writr)](https://npmjs.com/package/writr)\n\n# Features\n* Removes the remark / unified complexity and easy to use.\n* Powered by the [unified processor](https://github.com/unifiedjs/unified) for an extensible plugin pipeline.\n* Built in caching 💥 making it render very fast when there isn't a change\n* Frontmatter support built in by default. :tada:\n* Easily Render to `React` or `HTML`.\n* Generates a Table of Contents for your markdown files (remark-toc).\n* Slug generation for your markdown files (rehype-slug).\n* Code Highlighting (rehype-highlight).\n* Math Support (rehype-katex).\n* Markdown to HTML (rehype-stringify).\n* Github Flavor Markdown (remark-gfm).\n* Emoji Support (remark-emoji).\n* MDX Support (remark-mdx).\n* Raw HTML Passthrough (rehype-raw).\n* Built in Hooks for adding code to render pipeline.\n* AI-powered metadata generation, SEO, and translation via the [Vercel AI SDK](https://sdk.vercel.ai).\n\n# Table of Contents\n- [Getting Started](#getting-started)\n- [API](#api)\n  - [`new Writr(arg?: string | WritrOptions, options?: WritrOptions)`](#new-writrarg-string--writroptions-options-writroptions)\n  - [`.ai`](#writrai)\n  - [`.content`](#content)\n  - [`.body`](#body)\n  - [`.options`](#options)\n  - [`.frontmatter`](#frontmatter)\n  - [`.frontMatterRaw`](#frontmatterraw)\n  - [`.cache`](#cache)\n  - [`.engine`](#engine)\n  - [`.render(options?: RenderOptions)`](#renderoptions-renderoptions)\n  - [`.renderSync(options?: RenderOptions)`](#rendersyncoptions-renderoptions)\n  - [`.renderToFile(filePath: string, options?)`](#rendertofilefilepath-string-options-renderoptions)\n  - [`.renderToFileSync(filePath: string, options?)`](#rendertofilesyncfilepath-string-options-renderoptions)\n  - [`.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`](#renderreactoptions-renderoptions-reactoptions-htmlreactparseroptions)\n  - [`.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`](#renderreactsync-options-renderoptions-reactoptions-htmlreactparseroptions)\n  - [`.validate(content?: string, options?: RenderOptions)`](#validatecontent-string-options-renderoptions)\n  - [`.validateSync(content?: string, options?: RenderOptions)`](#validatesynccontent-string-options-renderoptions)\n  - [`.loadFromFile(filePath: string)`](#loadfromfilefilepath-string)\n  - [`.loadFromFileSync(filePath: string)`](#loadfromfilesyncfilepath-string)\n  - [`.saveToFile(filePath: string)`](#savetofilefilepath-string)\n  - [`.saveToFileSync(filePath: string)`](#savetofilesyncfilepath-string)\n- [Caching On Render](#caching-on-render)\n- [GitHub Flavored Markdown (GFM)](#github-flavored-markdown-gfm)\n  - [GFM Features](#gfm-features)\n  - [Using GFM](#using-gfm)\n  - [Disabling GFM](#disabling-gfm)\n- [Hooks](#hooks)\n- [Emitters](#emitters)\n  - [Error Events](#error-events)\n  - [Listening to Error Events](#listening-to-error-events)\n  - [Methods that Emit Errors](#methods-that-emit-errors)\n  - [Error Event Examples](#error-event-examples)\n  - [Event Emitter Methods](#event-emitter-methods)\n- [AI](#ai)\n  - [AI Options](#ai-options)\n  - [AI Provider Configuration](#ai-provider-configuration)\n  - [Metadata](#metadata)\n    - [Generating Metadata](#generating-metadata)\n    - [Applying Metadata to Frontmatter](#applying-metadata-to-frontmatter)\n    - [Overwrite](#overwrite)\n    - [Field Mapping](#field-mapping)\n  - [SEO](#seo)\n  - [Translation](#translation)\n  - [Using WritrAI Directly](#using-writrai-directly)\n- [Migrating to v6](#migrating-to-v6)\n- [Unified Processor Engine](#unified-processor-engine)\n- [Benchmarks](#benchmarks)\n- [ESM and Node Version Support](#esm-and-node-version-support)\n- [Code of Conduct and Contributing](#code-of-conduct-and-contributing)\n- [License](#license)\n\n# Getting Started \n\n```bash\n\u003e npm install writr\n```\n\nThen you can use it like this:\n\n```javascript\nimport { Writr } from 'writr';\n\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\n\nconst html = await writr.render(); // \u003ch1\u003eHello World 🙂\u003c/h1\u003e\u003cp\u003eThis is a test.\u003c/p\u003e\n```\nIts just that simple. Want to add some options? No problem.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nconst options  = {\n\temoji: false\n}\nconst html = await writr.render(options); // \u003ch1\u003eHello World ::-):\u003c/h1\u003e\u003cp\u003eThis is a test.\u003c/p\u003e\n```\n\nAn example passing in the options also via the constructor:\n\n```javascript\nimport { Writr, WritrOptions } from 'writr';\nconst writrOptions = {\n  renderOptions: {\n    emoji: true,\n    toc: true,\n    slug: true,\n    highlight: true,\n    gfm: true,\n    math: true,\n    mdx: true,\n    rawHtml: false,\n    caching: true,\n  }\n};\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`, writrOptions);\nconst html = await writr.render(options); // \u003ch1\u003eHello World ::-):\u003c/h1\u003e\u003cp\u003eThis is a test.\u003c/p\u003e\n```\n\n# API\n\n## `new Writr(arg?: string | WritrOptions, options?: WritrOptions)` \n\nBy default the constructor takes in a markdown `string` or `WritrOptions` in the first parameter. You can also send in nothing and set the markdown via `.content` property. If you want to pass in your markdown and options you can easily do this with `new Writr('## Your Markdown Here', { ...options here})`. You can access the `WritrOptions` from the instance of Writr. Here is an example of WritrOptions.\n\n```javascript\nimport { Writr, WritrOptions } from 'writr';\nconst writrOptions = {\n  renderOptions: {\n    emoji: true,\n    toc: true,\n    slug: true,\n    highlight: true,\n    gfm: true,\n    math: true,\n    mdx: true,\n    rawHtml: false,\n    caching: true,\n  }\n};\nconst writr = new Writr(writrOptions);\n```\n\n## `.content`\n\nSetting the markdown content for the instance of Writr. This can be set via the constructor or directly on the instance and can even handle `frontmatter`.\n\n```javascript\n\nimport { Writr } from 'writr';\nconst writr = new Writr();\nwritr.content = `---\ntitle: Hello World\n---\n# Hello World ::-):\\n\\n This is a test.`;\n```\n\n## `.body`\n\ngets the body of the markdown content. This is the content without the frontmatter.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr();\nwritr.content = `---\ntitle: Hello World\n---\n# Hello World ::-):\\n\\n This is a test.`;\nconsole.log(writr.body); // '# Hello World ::-):\\n\\n This is a test.'\n```\n\n## `.options`\n\nAccessing the default options for this instance of Writr. Here is the default settings for `WritrOptions`. These are the default settings for the `WritrOptions`:\n\n```javascript\n{\n  renderOptions: {\n    emoji: true,\n    toc: true,\n    slug: true,\n    highlight: true,\n    gfm: true,\n    math: true,\n    mdx: false,\n    rawHtml: false,\n    caching: true,\n  }\n}\n```\n\nBy default, raw HTML in markdown (such as `\u0026lt;iframe\u0026gt;`, `\u0026lt;video\u0026gt;`, or `\u0026lt;div\u0026gt;` tags) is stripped during rendering. Set `rawHtml: true` to preserve raw HTML elements and their attributes in the rendered output. This is useful for embedding videos, widgets, or custom HTML in your markdown content.\n\n**Note:** Setting `mdx: true` also enables raw HTML passthrough as part of the MDX specification. The `rawHtml` option is for enabling raw HTML in standard markdown without using MDX.\n\n## `.frontmatter`\n\nAccessing the frontmatter for this instance of Writr. This is a `Record\u0026lt;string, any\u0026gt;` and can be set via the `.content` property.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr();\nwritr.content = `---\ntitle: Hello World\n---\n# Hello World ::-):\\n\\n This is a test.`;\nconsole.log(writr.frontmatter); // { title: 'Hello World' }\n```\n\nyou can also set the front matter directly like this:\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr();\nwritr.frontmatter = { title: 'Hello World' };\n```\n\n## `.frontMatterRaw`\n\nAccessing the raw frontmatter for this instance of Writr. This is a `string` and can be set via the `.content` property.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr();\nwritr.content = `---\ntitle: Hello World\n---\n# Hello World ::-):\\n\\n This is a test.`;\nconsole.log(writr.frontMatterRaw); // '---\\ntitle: Hello World\\n---'\n```\n\n## `.cache`\n\nAccessing the cache for this instance of Writr. By default this is an in memory cache and is disabled (set to false) by default. You can enable this by setting `caching: true` in the `RenderOptions` of the `WritrOptions` or when calling render passing the `RenderOptions` like here:\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nconst options  = {\n  caching: true\n}\nconst html = await writr.render(options); // \u003ch1\u003eHello World ::-):\u003c/h1\u003e\u003cp\u003eThis is a test.\u003c/p\u003e\n```\n\n\n## `.engine`\n\nAccessing the underlying engine for this instance of Writr. This is a `Processor\u0026lt;Root, Root, Root, undefined, undefined\u0026gt;` from the core [`unified`](https://github.com/unifiedjs/unified) project and uses the familiar `.use()` plugin pattern. You can chain additional unified plugins on this processor to customize the render pipeline. Learn more about the unified engine at [unifiedjs.com](https://unifiedjs.com/) and check out the [getting started guide](https://unifiedjs.com/learn/guide/using-unified/) for examples.\n\n\n## `.render(options?: RenderOptions)`\n\nRendering markdown to HTML. the options are based on RenderOptions. Which you can access from the Writr instance.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nconst html = await writr.render(); // \u003ch1\u003eHello World 🙂\u003c/h1\u003e\u003cp\u003eThis is a test.\u003c/p\u003e\n\n//passing in with render options\nconst options  = {\n  emoji: false\n}\n\nconst html = await writr.render(options); // \u003ch1\u003eHello World ::-):\u003c/h1\u003e\u003cp\u003eThis is a test.\u003c/p\u003e\n```\n\n## `.renderSync(options?: RenderOptions)`\n\nRendering markdown to HTML synchronously. the options are based on RenderOptions. Which you can access from the Writr instance. The parameters are the same as the `.render()` function.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nconst html = writr.renderSync(); // \u003ch1\u003eHello World 🙂\u003c/h1\u003e\u003cp\u003eThis is a test.\u003c/p\u003e\n```\n\n## `.renderToFile(filePath: string, options?: RenderOptions)`\n\nRendering markdown to a file. The options are based on RenderOptions.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nawait writr.renderToFile('path/to/file.html');\n```\n\n## `.renderToFileSync(filePath: string, options?: RenderOptions)`\n\nRendering markdown to a file synchronously. The options are based on RenderOptions.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nwritr.renderToFileSync('path/to/file.html');\n```\n\n## `.renderReact(options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`\n\nRendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nconst reactElement = await writr.renderReact(); // Will return a React.JSX.Element\n```\n\n## `.renderReactSync( options?: RenderOptions, reactOptions?: HTMLReactParserOptions)`\n\nRendering markdown to React. The options are based on RenderOptions and now HTMLReactParserOptions from `html-react-parser`.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nconst reactElement = writr.renderReactSync(); // Will return a React.JSX.Element\n```\n\n## `.validate(content?: string, options?: RenderOptions)`\n\nValidate markdown content by attempting to render it. Returns a `WritrValidateResult` object with a `valid` boolean and optional `error` property. Note that this will disable caching on render to ensure accurate validation.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World\\n\\nThis is a test.`);\n\n// Validate current content\nconst result = await writr.validate();\nconsole.log(result.valid); // true\n\n// Validate external content without changing the instance\nconst externalResult = await writr.validate('## Different Content');\nconsole.log(externalResult.valid); // true\nconsole.log(writr.content); // Still \"# Hello World\\n\\nThis is a test.\"\n\n// Handle validation errors\nconst invalidWritr = new Writr('Put invalid markdown here');\nconst errorResult = await invalidWritr.validate();\nconsole.log(errorResult.valid); // false\nconsole.log(errorResult.error?.message); // \"Invalid plugin\"\n```\n\n## `.validateSync(content?: string, options?: RenderOptions)`\n\nSynchronously validate markdown content by attempting to render it. Returns a `WritrValidateResult` object with a `valid` boolean and optional `error` property.\n\nThis is the synchronous version of `.validate()` with the same parameters and behavior.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World\\n\\nThis is a test.`);\n\n// Validate current content synchronously\nconst result = writr.validateSync();\nconsole.log(result.valid); // true\n\n// Validate external content without changing the instance\nconst externalResult = writr.validateSync('## Different Content');\nconsole.log(externalResult.valid); // true\nconsole.log(writr.content); // Still \"# Hello World\\n\\nThis is a test.\"\n```\n\n## `.loadFromFile(filePath: string)`\n\nLoad your markdown content from a file path.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr();\nawait writr.loadFromFile('path/to/file.md');\n```\n\n## `.loadFromFileSync(filePath: string)`\n\nLoad your markdown content from a file path synchronously.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr();\nwritr.loadFromFileSync('path/to/file.md');\n```\n\n## `.saveToFile(filePath: string)`\n\nSave your markdown and frontmatter (if included) content to a file path.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nawait writr.saveToFile('path/to/file.md');\n```\n\n## `.saveToFileSync(filePath: string)`\n\nSave your markdown and frontmatter (if included) content to a file path synchronously.\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nwritr.saveToFileSync('path/to/file.md');\n```\n\n# Caching On Render\n\nCaching is built into Writr and is an in-memory cache using `CacheableMemory` from [Cacheable](https://cacheable.org). It is turned off by default and can be enabled by setting `caching: true` in the `RenderOptions` of the `WritrOptions` or when calling render passing the `RenderOptions` like here:\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`, { renderOptions: { caching: true } });\n```\n\nor via `RenderOptions` such as:\n\n```javascript\nimport { Writr } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nawait writr.render({ caching: true});\n```\n\nIf you want to set the caching options for the instance of Writr you can do so like this:\n\n```javascript\n// we will set the lruSize of the cache and the default ttl\nimport {Writr} from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`, { renderOptions: { caching: true } });\nwritr.cache.store.lruSize = 100;\nwritr.cache.store.ttl = '5m'; // setting it to 5 minutes\n```\n\n# GitHub Flavored Markdown (GFM)\n\nWritr includes full support for [GitHub Flavored Markdown](https://github.github.com/gfm/) (GFM) through the `remark-gfm` and `remark-github-blockquote-alert` plugins. GFM is enabled by default and adds several powerful features to standard Markdown.\n\n## GFM Features\n\nWhen GFM is enabled (which it is by default), you get access to the following features:\n\n### Tables\n\nCreate tables using pipes and hyphens:\n\n```markdown\n| Feature | Supported |\n|---------|-----------|\n| Tables  | Yes       |\n| Alerts  | Yes       |\n```\n\n### Strikethrough\n\nUse `~~` to create strikethrough text:\n\n```markdown\n~~This text is crossed out~~\n```\n\n### Task Lists\n\nCreate interactive checkboxes:\n\n```markdown\n- [x] Completed task\n- [ ] Incomplete task\n- [ ] Another task\n```\n\n### Autolinks\n\nURLs are automatically converted to clickable links:\n\n```markdown\nhttps://github.com\n```\n\n### GitHub Blockquote Alerts\n\nGitHub-style alerts are supported to emphasize critical information. These are blockquote-based admonitions that render with special styling:\n\n```markdown\n\u003e [!NOTE]\n\u003e Useful information that users should know, even when skimming content.\n\n\u003e [!TIP]\n\u003e Helpful advice for doing things better or more easily.\n\n\u003e [!IMPORTANT]\n\u003e Key information users need to know to achieve their goal.\n\n\u003e [!WARNING]\n\u003e Urgent info that needs immediate user attention to avoid problems.\n\n\u003e [!CAUTION]\n\u003e Advises about risks or negative outcomes of certain actions.\n```\n\n## Using GFM\n\nGFM is enabled by default. Here's an example:\n\n```javascript\nimport { Writr } from 'writr';\n\nconst markdown = `\n# Task List Example\n\n- [x] Learn Writr basics\n- [ ] Master GFM features\n\n\u003e [!NOTE]\n\u003e GitHub Flavored Markdown is enabled by default!\n\n| Feature | Status |\n|---------|--------|\n| GFM     | ✓      |\n`;\n\nconst writr = new Writr(markdown);\nconst html = await writr.render(); // Renders with full GFM support\n```\n\n## Disabling GFM\n\nIf you need to disable GFM features, you can set `gfm: false` in the render options:\n\n```javascript\nimport { Writr } from 'writr';\n\nconst writr = new Writr('~~strikethrough~~ text');\n\n// Disable GFM\nconst html = await writr.render({ gfm: false });\n// Output: \u003cp\u003e~~strikethrough~~ text\u003c/p\u003e\n\n// With GFM enabled (default)\nconst htmlWithGfm = await writr.render({ gfm: true });\n// Output: \u003cp\u003e\u003cdel\u003estrikethrough\u003c/del\u003e text\u003c/p\u003e\n```\n\nNote: When GFM is disabled, GitHub blockquote alerts will not be processed and will render as regular blockquotes.\n\n# Hooks\n\nHooks are a way to add additional parsing to the render pipeline. You can add hooks to the the Writr instance. Here is an example of adding a hook to the instance of Writr:\n\n```javascript\nimport { Writr, WritrHooks } from 'writr';\nconst writr = new Writr(`# Hello World ::-):\\n\\n This is a test.`);\nwritr.onHook(WritrHooks.beforeRender, data =\u003e {\n  data.body = 'Hello, Universe!';\n});\nconst result = await writr.render();\nconsole.log(result); // Hello, Universe!\n```\n\nFor `beforeRender` the data object is a `renderData` object. Here is the interface for `renderData`:\n\n```typescript\nexport type renderData = {\n  body: string\n  options: RenderOptions;\n}\n```\n\nFor `afterRender` the data object is a `resultData` object. Here is the interface for `resultData`:\n\n```typescript\nexport type resultData = {\n  result: string;\n}\n```\n\nFor `saveToFile` the data object is an object with the `filePath` and `content`. Here is the interface for `saveToFileData`:\n\n```typescript\nexport type saveToFileData = {\n  filePath: string;\n  content: string;\n}\n```\n\nThis is called when you call `saveToFile`, `saveToFileSync`.\n\nFor `renderToFile` the data object is an object with the `filePath` and `content`. Here is the interface for `renderToFileData`:\n\n```typescript\nexport type renderToFileData = {\n  filePath: string;\n  content: string;\n}\n```\n\nThis is called when you call `renderToFile`, `renderToFileSync`.\n\nFor `loadFromFile` the data object is an object with `content` so you can change before it is set on `writr.content`. Here is the interface for `loadFromFileData`:\n\n```typescript\nexport type loadFromFileData = {\n  content: string;\n}\n```\n\nThis is called when you call `loadFromFile`, `loadFromFileSync`.\n\n# Emitters\n\nWritr extends the [Hookified](https://github.com/jaredwray/hookified) class, which provides event emitter capabilities. This means you can listen to events emitted by Writr during its lifecycle, particularly error events.\n\n## Error Events\n\nWritr emits an `error` event whenever an error occurs in any of its methods. This provides a centralized way to handle errors without wrapping every method call in a try/catch block.\n\n### Listening to Error Events\n\nYou can listen to error events using the `.on()` method:\n\n```javascript\nimport { Writr } from 'writr';\n\nconst writr = new Writr('# Hello World');\n\n// Listen for any errors\nwritr.on('error', (error) =\u003e {\n  console.error('An error occurred:', error.message);\n  // Handle the error appropriately\n  // Log to error tracking service, display to user, etc.\n});\n\n// With a listener registered, errors are emitted to the listener\n// and the method returns its fallback value (e.g. \"\" for render)\nconst html = await writr.render();\n```\n\n### Methods that Emit Errors\n\nAll 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.\n\n**Rendering Methods** — emit error, return `\"\"`:\n- `render()` - Emits error when markdown rendering fails, returns empty string\n- `renderSync()` - Emits error when markdown rendering fails, returns empty string\n- `renderReact()` - Emits error when React rendering fails, returns empty string\n- `renderReactSync()` - Emits error when React rendering fails, returns empty string\n\n**Validation Methods:**\n- `validate()` - Does **not** emit errors. Returns `{ valid: false, error }` on failure\n- `validateSync()` - Emits error and returns `{ valid: false, error }` on failure\n\n**File Operations** — emit error, return void:\n- `renderToFile()` - Emits error when rendering or file writing fails\n- `renderToFileSync()` - Emits error when rendering or file writing fails\n- `loadFromFile()` - Emits error when file reading fails\n- `loadFromFileSync()` - Emits error when file reading fails\n- `saveToFile()` - Emits error when file writing fails\n- `saveToFileSync()` - Emits error when file writing fails\n\n**Front Matter Operations** — emit error, return fallback:\n- `frontMatter` getter - Emits error when YAML parsing fails, returns `{}`\n- `frontMatter` setter - Emits error when YAML serialization fails\n\n### Error Event Examples\n\n**Example 1: Global Error Handler**\n\n```javascript\nimport { Writr } from 'writr';\n\nconst writr = new Writr();\n\n// Set up a global error handler\nwritr.on('error', (error) =\u003e {\n  // Log to your monitoring service\n  console.error('Writr error:', error);\n\n  // Send to error tracking (e.g., Sentry, Rollbar)\n  // errorTracker.captureException(error);\n});\n\n// All errors will be emitted to the listener above\nawait writr.loadFromFile('./content.md');\nconst html = await writr.render();\n```\n\n**Example 2: Validation with Error Listening**\n\n```javascript\nimport { Writr } from 'writr';\n\nconst writr = new Writr('# My Content');\nlet lastError = null;\n\nwritr.on('error', (error) =\u003e {\n  lastError = error;\n});\n\nconst result = await writr.validate();\n\nif (!result.valid) {\n  console.log('Validation failed');\n  console.log('Error details:', lastError);\n  // result.error is also available\n}\n```\n\n**Example 3: File Operations Without Try/Catch**\n\n```javascript\nimport { Writr } from 'writr';\n\nconst writr = new Writr('# Content');\n\nwritr.on('error', (error) =\u003e {\n  console.error('File operation failed:', error.message);\n  // Handle gracefully - maybe use default content\n});\n\n// With a listener registered, errors are emitted and the method returns normally\nawait writr.loadFromFile('./maybe-missing.md');\n// Note: without a listener, this will throw by default (throwOnEmptyListeners is true)\n```\n\n### Event Emitter Methods\n\nSince Writr extends Hookified, you have access to standard event emitter methods:\n\n- `writr.on(event, handler)` - Add an event listener\n- `writr.once(event, handler)` - Add a one-time event listener\n- `writr.off(event, handler)` - Remove an event listener\n- `writr.emit(event, data)` - Emit an event (used internally)\n\nFor more information about event handling capabilities, see the [Hookified documentation](https://github.com/jaredwray/hookified).\n\n# AI\n\nWritr 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.\n\n```typescript\nimport { Writr } from 'writr';\nimport { openai } from '@ai-sdk/openai';\n\nconst writr = new Writr('# My Document\\n\\nSome markdown content here.', {\n  ai: { model: openai('gpt-4.1-mini') },\n});\n\n// Generate metadata\nconst metadata = await writr.ai.getMetadata();\n\n// Generate only specific fields\nconst metadata = await writr.ai.getMetadata({ title: true, description: true });\n\n// Generate SEO metadata\nconst seo = await writr.ai.getSEO();\n\n// Translate to Spanish\nconst translated = await writr.ai.getTranslation({ to: 'es' });\n\n// Apply generated metadata to frontmatter\nconst result = await writr.ai.applyMetadata({\n  generate: { description: true, category: true },\n  overwrite: true,\n});\n```\n\n## AI Options\n\nPass `ai` in the `WritrOptions` to enable AI features:\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `model` | `LanguageModel` | Yes | The AI SDK model instance (e.g. `openai(\"gpt-4.1-mini\")`). |\n| `cache` | `boolean` | No | Enables in-memory caching of AI results. |\n| `prompts` | `WritrAIPrompts` | No | Custom prompt overrides for metadata, SEO, and translation. |\n\n```typescript\nconst writr = new Writr('# My Document', {\n  ai: {\n    model: openai('gpt-4.1-mini'),\n    cache: true,\n  },\n});\n```\n\n## AI Provider Configuration\n\nBy default, the provider imports read API keys from environment variables:\n\n| Provider | Import | Environment Variable |\n|----------|--------|---------------------|\n| OpenAI | `openai` from `@ai-sdk/openai` | `OPENAI_API_KEY` |\n| Anthropic | `anthropic` from `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` |\n| Google | `google` from `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` |\n\n```typescript\n// Uses OPENAI_API_KEY from environment\nimport { openai } from '@ai-sdk/openai';\n\nconst writr = new Writr('# Hello', { ai: { model: openai('gpt-4.1-mini') } });\n```\n\nTo set API keys programmatically, use the provider factory functions instead:\n\n```typescript\nimport { Writr } from 'writr';\nimport { createOpenAI } from '@ai-sdk/openai';\nimport { createAnthropic } from '@ai-sdk/anthropic';\nimport { createGoogleGenerativeAI } from '@ai-sdk/google';\n\n// OpenAI\nconst openai = createOpenAI({ apiKey: 'your-openai-key' });\nconst writr = new Writr('# Hello', { ai: { model: openai('gpt-4.1-mini') } });\n\n// Anthropic\nconst anthropic = createAnthropic({ apiKey: 'your-anthropic-key' });\nconst writr = new Writr('# Hello', { ai: { model: anthropic('claude-sonnet-4-20250514') } });\n\n// Google\nconst google = createGoogleGenerativeAI({ apiKey: 'your-google-key' });\nconst writr = new Writr('# Hello', { ai: { model: google('gemini-2.0-flash') } });\n```\n\n## Metadata\n\nGenerate metadata from your document content using `writr.ai.getMetadata()`, or generate and apply it directly to frontmatter with `writr.ai.applyMetadata()`.\n\n### Generating Metadata\n\n`getMetadata()` analyzes the document and returns a `WritrMetadata` object. By default all fields are generated. Pass options to select specific fields.\n\n```typescript\n// Generate all metadata fields\nconst metadata = await writr.ai.getMetadata();\nconsole.log(metadata.title);       // \"Getting Started with Writr\"\nconsole.log(metadata.tags);        // [\"markdown\", \"rendering\", \"typescript\"]\nconsole.log(metadata.description); // \"A guide to using Writr for markdown processing.\"\nconsole.log(metadata.readingTime); // 3 (minutes)\nconsole.log(metadata.wordCount);   // 450\n\n// Generate only specific fields\nconst partial = await writr.ai.getMetadata({\n  title: true,\n  description: true,\n  tags: true,\n});\n```\n\n**Generated fields:**\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `title` | `string` | The best-fit title for the document. |\n| `description` | `string` | A concise meta-style description of the document. |\n| `tags` | `string[]` | Human-friendly labels for organizing the document. |\n| `keywords` | `string[]` | Search-oriented terms related to the document content. |\n| `preview` | `string` | A short teaser or preview snippet of the content. |\n| `summary` | `string` | A slightly longer overview of the document. |\n| `category` | `string` | A broad grouping such as \"docs\", \"guide\", or \"blog\". |\n| `topic` | `string` | The primary subject the document is about. |\n| `audience` | `string` | The intended audience for the document. |\n| `difficulty` | `\"beginner\" \\| \"intermediate\" \\| \"advanced\"` | The estimated skill level required. |\n| `readingTime` | `number` | Estimated reading time in minutes (computed, not AI-generated). |\n| `wordCount` | `number` | Total word count of the document (computed, not AI-generated). |\n\n### Applying Metadata to Frontmatter\n\n`applyMetadata()` generates metadata and writes it into the document's frontmatter. The result tells you exactly what happened:\n\n- **`applied`** — fields that were newly written because they were missing from frontmatter.\n- **`skipped`** — fields that already existed and were not overwritten.\n- **`overwritten`** — fields that replaced existing frontmatter values.\n\n```typescript\nconst result = await writr.ai.applyMetadata();\nconsole.log(result.applied);     // [\"description\", \"tags\", \"category\"]\nconsole.log(result.skipped);     // [\"title\"] (already existed)\nconsole.log(result.overwritten); // []\n```\n\n### Overwrite\n\nBy default, `applyMetadata()` only fills in missing fields — existing frontmatter values are never touched. The `overwrite` option changes this behavior:\n\n- **Default (no overwrite):** Only missing fields are written. Existing values are preserved.\n- **`overwrite: true`:** All generated fields replace existing frontmatter values.\n- **`overwrite: ['field1', 'field2']`:** Only the listed fields are overwritten. Other existing values are preserved.\n\n```typescript\n// Overwrite all generated fields, even if they already exist\nconst result = await writr.ai.applyMetadata({\n  generate: { title: true, description: true },\n  overwrite: true,\n});\n\n// Only overwrite title, leave description alone if it already exists\nconst result = await writr.ai.applyMetadata({\n  generate: { title: true, description: true, category: true },\n  overwrite: ['title'],\n});\n```\n\n### Field Mapping\n\nThe `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.\n\n```typescript\nconst result = await writr.ai.applyMetadata({\n  generate: { description: true, tags: true },\n  fieldMap: {\n    description: 'meta_description',\n    tags: 'labels',\n  },\n});\n// writr.frontMatter.meta_description === \"A guide to...\"\n// writr.frontMatter.labels === [\"markdown\", \"rendering\"]\n```\n\nThe mapping applies to all behaviors — field existence checks, overwrites, and skips all use the mapped key when checking frontmatter.\n\n## SEO\n\nGenerate SEO metadata using `writr.ai.getSEO()`. By default all fields are generated. Pass options to select specific fields.\n\n```typescript\nconst seo = await writr.ai.getSEO();\nconsole.log(seo.slug);              // \"getting-started-with-writr\"\nconsole.log(seo.openGraph?.title);  // \"Getting Started with Writr\"\n\n// Generate only a slug\nconst seo = await writr.ai.getSEO({ slug: true });\n```\n\n**Available fields:** `slug`, `openGraph` (includes `title`, `description`, `image`).\n\n## Translation\n\nTranslate the document into another language using `writr.ai.getTranslation()`. Returns a new `Writr` instance with the translated content.\n\n```typescript\nconst spanish = await writr.ai.getTranslation({ to: 'es' });\nconsole.log(spanish.body); // Spanish markdown\n\n// With source language and frontmatter translation\nconst french = await writr.ai.getTranslation({\n  to: 'fr',\n  from: 'en',\n  translateFrontMatter: true,\n});\n```\n\n| Option | Type | Required | Description |\n|--------|------|----------|-------------|\n| `to` | `string` | Yes | Target language or locale. |\n| `from` | `string` | No | Source language or locale. |\n| `translateFrontMatter` | `boolean` | No | Also translate frontmatter string values. |\n\n## Using WritrAI Directly\n\n`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.\n\n```typescript\nimport { Writr, WritrAI } from 'writr';\nimport { openai } from '@ai-sdk/openai';\n\nconst writr = new Writr('# My Document\\n\\nSome markdown content here.');\nconst ai = new WritrAI(writr, {\n  model: openai('gpt-4.1-mini'),\n  cache: true,\n});\n\n// Generate metadata\nconst metadata = await ai.getMetadata();\nconsole.log(metadata.title);\nconsole.log(metadata.tags);\n\n// Generate SEO data\nconst seo = await ai.getSEO();\nconsole.log(seo.slug);\n\n// Translate\nconst translated = await ai.getTranslation({ to: 'es' });\nconsole.log(translated.body);\n\n// Apply metadata to frontmatter\nconst result = await ai.applyMetadata({\n  generate: { title: true, description: true, tags: true },\n  overwrite: true,\n});\n```\n\n# Migrating to v6\n\nWritr 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.\n\n## Breaking Changes\n\n### `throwErrors` removed\n\nThe `throwErrors` option has been removed from `WritrOptions`. Use `throwOnEmitError` instead, which is provided by hookified's `HookifiedOptions` (now spread into `WritrOptions`).\n\n**Before (v5):**\n\n```typescript\nconst writr = new Writr('# Hello', { throwErrors: true });\n```\n\n**After (v6):**\n\n```typescript\nconst writr = new Writr('# Hello', { throwOnEmitError: true });\n```\n\n### Error handling redesign\n\nAll 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).\n\n**How errors propagate:**\n\n- **With a listener registered:** Errors are passed to the listener. The method returns its fallback value without throwing.\n- **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.\n- **With `throwOnEmitError: true`:** Every `emit('error')` call throws, even when listeners are registered. This affects all methods that emit errors.\n\n**Other changes:**\n\n- `render()` and `renderSync()` no longer throw wrapped `\"Failed to render markdown: ...\"` errors. They emit the original error and return `\"\"`.\n- `validate()` (async) no longer emits errors — it only returns `{ valid: false, error }`. `validateSync()` still emits.\n\n### hookified v2\n\nWritr now uses hookified v2 which introduces several new options available through `WritrOptions`:\n\n- `throwOnEmitError` — Throw when `emit(\"error\")` is called, even with listeners (default: `false`)\n- `throwOnHookError` — Throw when a hook handler throws (default: `false`)\n- `throwOnEmptyListeners` — Throw when emitting `error` with no listeners (default: `true`)\n- `eventLogger` — Logger instance for event logging\n\nSee the [hookified documentation](https://github.com/jaredwray/hookified) for full details.\n\n# Unified Processor Engine\n\nWritr builds on top of the open source [unified](https://github.com/unifiedjs/unified) processor – the core project that powers\n[remark](https://github.com/remarkjs/remark), [rehype](https://github.com/rehypejs/rehype), and many other content tools. Unified\nprovides a pluggable pipeline where each plugin transforms a syntax tree. Writr configures a default set of plugins to turn\nMarkdown into HTML, but you can access the processor through the `.engine` property to add your own behavior with\n`writr.engine.use(myPlugin)`. The [unified documentation](https://unifiedjs.com/) has more details and guides for building\nplugins and working with the processor directly.\n\n# Benchmarks\n\nThis 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:\n\n|           name            |  summary  |  ops/sec  |  time/op  |  margin  |  samples  |\n|---------------------------|:---------:|----------:|----------:|:--------:|----------:|\n|  Writr (Sync) (Caching)   |    🥇     |      83K  |     14µs  |  ±0.27%  |      74K  |\n|  Writr (Async) (Caching)  |    -3%    |      80K  |     14µs  |  ±0.27%  |      71K  |\n|  markdown-it              |   -30%    |      58K  |     20µs  |  ±0.37%  |      50K  |\n|  marked                   |   -33%    |      56K  |     25µs  |  ±0.50%  |      40K  |\n|  Writr (Sync)             |   -94%    |       5K  |    225µs  |  ±0.88%  |      10K  |\n|  Writr (Async)            |   -94%    |       5K  |    229µs  |  ±0.89%  |      10K  |\n\nAs you can see this module is performant with `caching` enabled but was built to be performant enough but with all the features added in. If you are just wanting performance and not features then `markdown-it` or `marked` is the solution unless you use `Writr` with caching. \n\n|           name            |  summary  |  ops/sec  |  time/op  |  margin  |  samples  |\n|---------------------------|:---------:|----------:|----------:|:--------:|----------:|\n|  Writr (Async) (Caching)  |    🥇     |      26K  |     39µs  |  ±0.12%  |      25K  |\n|  Writr (Sync) (Caching)   |  -0.92%   |      26K  |     40µs  |  ±0.15%  |      25K  |\n|  Writr (Sync)             |   -93%    |       2K  |    630µs  |  ±0.97%  |      10K  |\n|  Writr (Async)            |   -93%    |       2K  |    649µs  |  ±0.96%  |      10K  |\n\nThe benchmark shows rendering performance via Sync and Async methods with caching enabled and disabled and all features.\n\n# ESM and Node Version Support\n\nThis package is ESM only and tested on the current lts version and its previous. Please don't open issues for questions regarding CommonJS / ESM or previous Nodejs versions.\n\n# Code of Conduct and Contributing\nPlease use our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contributing](CONTRIBUTING.md) guidelines for development and testing. We appreciate your contributions!\n\n# License\n\n[MIT](LICENSE) \u0026 © [Jared Wray](https://jaredwray.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaredwray%2Fwritr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjaredwray%2Fwritr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaredwray%2Fwritr/lists"}