{"id":44866735,"url":"https://github.com/uttori/uttori-wiki","last_synced_at":"2026-02-17T11:35:04.833Z","repository":{"id":34507120,"uuid":"152799109","full_name":"uttori/uttori-wiki","owner":"uttori","description":"Uttori Wiki is a wiki module for the Uttori system.","archived":false,"fork":false,"pushed_at":"2025-10-17T17:10:19.000Z","size":3401,"stargazers_count":9,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-18T19:45:05.379Z","etag":null,"topics":["express","markdown","node","uttori-wiki","wiki"],"latest_commit_sha":null,"homepage":"https://wiki.superfamicom.org/","language":"JavaScript","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/uttori.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2018-10-12T19:34:20.000Z","updated_at":"2025-10-17T17:10:23.000Z","dependencies_parsed_at":"2023-12-25T09:44:05.413Z","dependency_job_id":"cd8ce4b2-bc4d-4d16-90b4-9d1b44729ddc","html_url":"https://github.com/uttori/uttori-wiki","commit_stats":null,"previous_names":[],"tags_count":55,"template":false,"template_full_name":null,"purl":"pkg:github/uttori/uttori-wiki","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uttori%2Futtori-wiki","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uttori%2Futtori-wiki/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uttori%2Futtori-wiki/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uttori%2Futtori-wiki/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/uttori","download_url":"https://codeload.github.com/uttori/uttori-wiki/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/uttori%2Futtori-wiki/sbom","scorecard":{"id":887083,"data":{"date":"2025-08-11","repo":{"name":"github.com/uttori/uttori-wiki","commit":"bc0c5d5f00caf8c7292a75b5a3c64c74168191ce"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.5,"checks":[{"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":"Code-Review","score":0,"reason":"Found 0/26 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":2,"reason":"3 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"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":"Dangerous-Workflow","score":-1,"reason":"no workflows found","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":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"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":"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":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"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":"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 'master'"],"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":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 6 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"}},{"name":"Vulnerabilities","score":4,"reason":"6 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-xffm-g5w8-qvg7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-76c9-3jph-rj3q","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-24T10:25:27.319Z","repository_id":34507120,"created_at":"2025-08-24T10:25:27.319Z","updated_at":"2025-08-24T10:25:27.319Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29542524,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T08:11:05.436Z","status":"ssl_error","status_checked_at":"2026-02-17T08:09:38.860Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6: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":["express","markdown","node","uttori-wiki","wiki"],"created_at":"2026-02-17T11:35:00.747Z","updated_at":"2026-02-17T11:35:04.812Z","avatar_url":"https://github.com/uttori.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![view on npm](https://img.shields.io/npm/v/@uttori/wiki.svg)](https://www.npmjs.com/package/@uttori/wiki)\n[![npm module downloads](https://img.shields.io/npm/dt/@uttori/wiki.svg)](https://www.npmjs.com/package/@uttori/wiki)\n[![Coverage Status](https://coveralls.io/repos/github/uttori/uttori-wiki/badge.svg?branch=master)](https://coveralls.io/github/uttori/uttori-wiki?branch=master)\n\n# Uttori Wiki\n\nUttoriWiki is a fast, simple, blog / wiki / knowledge base / generic website built around Express.js using the [Uttori](https://github.com/uttori) set of components allowing specific chunks of functionality be changed or swapped out to fit specific needs.\n\nWhy yet another knowledge management / note taking app? I wanted to have something that functioned as a wiki or blog or similar small app that I could reuse components for and keep extensible without having to rewrite everything or learn a new framework.\n\nBecause of that, UttoriWiki is plugin based. Search and Storage engines are fully configurable. The format of the data is also up to you: Markdown, Wikitext, Creole, AsciiDoc, Textile, reStructuredText, BBCode, Pendown, etc. Markdown is the default and best supported.\n\nNothing is prescribed. Don't want to write in Markdown? You don't need to! Don't want to store files on disk? Choose a database storage engine! Already running a bunch of external dependencies and want to plug into those? You can _most likely_ do it!\n\nRendering happens in a pipeline making it easy to render to Markdown, then filter content out and manipulate the content like removing tags or replacing text with emojis.\n\n## Configuration\n\nPlease see `src/config.js` or [the config doc](https://github.com/uttori/uttori-wiki/blob/master/docs/config.md) for all options. Below is an example configuration using some plugins:\n\n- [@uttori/storage-provider-json-file](https://github.com/uttori/uttori-storage-provider-json-file)\n- [@uttori/search-provider-lunr](https://github.com/uttori/uttori-search-provider-lunr)\n- [@uttori/plugin-renderer-replacer](https://github.com/uttori/uttori-plugin-renderer-replacer)\n- [@uttori/plugin-renderer-markdown-it](https://github.com/uttori/uttori-plugin-renderer-markdown-it)\n- [@uttori/plugin-upload-multer](https://github.com/uttori/uttori-plugin-upload-multer)\n- [@uttori/plugin-generator-sitemap](https://github.com/uttori/uttori-plugin-generator-sitemap)\n- [@uttori/plugin-analytics-json-file](https://github.com/uttori/uttori-plugin-analytics-json-file)\n\n```javascript\nimport { Plugin: StorageProvider } from '@uttori/storage-provider-json-file';\nimport { Plugin: SearchProvider } from '@uttori/search-provider-lunr';\n\nimport AnalyticsPlugin from '@uttori/plugin-analytics-json-file';\nimport MarkdownItRenderer from '@uttori/plugin-renderer-markdown-it';\nimport ReplacerRenderer from '@uttori/plugin-renderer-replacer';\nimport MulterUpload from '@uttori/plugin-upload-multer';\nimport SitemapGenerator from '@uttori/plugin-generator-sitemap';\nimport { AddQueryOutputToViewModel } from '@uttori/wiki';\n\nconst config = {\n  homePage: 'home-page',\n  ignoreSlugs: ['home-page'],\n  excerptLength: 400,\n  publicUrl: 'http://127.0.0.1:8000/wiki',\n  themePath: path.join(__dirname, 'theme'),\n  publicPath: path.join(__dirname, 'public'),\n  useDeleteKey: false,\n  deleteKey: process.env.DELETE_KEY || '',\n  useEditKey: false,\n  editKey: process.env.EDIT_KEY || '',\n  publicHistory: true,\n  allowedDocumentKeys: [],\n\n  // Plugins\n  plugins: [\n    StorageProvider,\n    SearchProvider,\n    AnalyticsPlugin,\n    MarkdownItRenderer,\n    ReplacerRenderer,\n    MulterUpload,\n    SitemapGenerator,\n  ],\n\n  // Use the JSON to Disk Storage Provider\n  [StorageProvider.configKey]: {\n    // Path in which to store content (markdown files, etc.)\n    contentDirectory: `${__dirname}/content`,\n\n    // Path in which to store content history (markdown files, etc.)\n    historyDirectory: `${__dirname}/content/history`,\n\n    // File Extension\n    extension: 'json',\n  },\n\n  // Use the Lunr Search Provider\n  [SearchProvider.configKey]: {\n    // Optional Lunr locale\n    lunr_locales: [],\n\n    // Ignore Slugs\n    ignoreSlugs: ['home-page'],\n  },\n\n  // Plugin: Analytics with JSON Files\n  [AnalyticsPlugin.configKey]: {\n    events: {\n      getPopularDocuments: ['popular-documents'],\n      updateDocument: ['document-save', 'document-delete'],\n      validateConfig: ['validate-config'],\n    },\n\n    // Directory files will be uploaded to.\n    directory: `${__dirname}/data`,\n\n    // Name of the JSON file.\n    name: 'visits',\n\n    // File extension to use for the JSON file.\n    extension: 'json',\n  },\n\n  // Plugin: Markdown rendering with MarkdownIt\n  [MarkdownItRenderer.configKey]: {\n    events: {\n      renderContent: ['render-content'],\n      renderCollection: ['render-search-results'],\n      validateConfig: ['validate-config'],\n    },\n\n\n    // Uttori Specific Configuration\n    uttori: {\n      // Prefix for relative URLs, useful when the Express app is not at root.\n      baseUrl: '',\n\n      // Safe List, if a domain is not in this list, it is set to 'external nofollow noreferrer'.\n      allowedExternalDomains: [\n        'my-site.org',\n      ],\n\n      // Open external domains in a new window.\n      openNewWindow: true,\n\n      // Table of Contents\n      toc: {\n        // The opening DOM tag for the TOC container.\n        openingTag: '\u003cnav class=\"table-of-contents\"\u003e',\n\n        // The closing DOM tag for the TOC container.\n        closingTag: '\u003c/nav\u003e',\n\n        // Slugify options for convering content to anchor links.\n        slugify: {\n          lower: true,\n        },\n      },\n    },\n  },\n\n  // Plugin: Replace text\n  [ReplacerRenderer.configKey]: {\n    events: {\n      renderContent: ['render-content'],\n      renderCollection: ['render-search-results'],\n      validateConfig: ['validate-config'],\n    },\n\n    // Rules for text replace\n    rules: [\n      {\n        test: /bunny|rabbit/gm,\n        output: '🐰',\n      },\n    ],\n  },\n\n  // Plugin: Multer Upload\n  [MulterUpload.configKey]: {\n    events: {\n      bindRoutes: ['bind-routes'],\n      validateConfig: ['validate-config'],\n    },\n\n    // Directory files will be uploaded to\n    directory: `${__dirname}/uploads`,\n\n    // URL to POST files to\n    route: '/upload',\n\n    // URL to GET uploads from\n    publicRoute: '/uploads',\n  },\n\n  // Plugin: Sitemap Generator\n  [SitemapGenerator.configKey]: {\n    events: {\n      callback: ['document-save', 'document-delete'],\n      validateConfig: ['validate-config'],\n    },\n\n    // Sitemap URL (ie https://wiki.domain.tld)\n    base_url: 'https://wiki.domain.tld',\n\n    // Location where the XML sitemap will be written to.\n    directory: `${__dirname}/themes/default/public`,\n\n    urls: [\n      {\n        url: '/',\n        lastmod: new Date().toISOString(),\n        priority: '1.00',\n      },\n      {\n        url: '/tags',\n        lastmod: new Date().toISOString(),\n        priority: '0.90',\n      },\n      {\n        url: '/new',\n        lastmod: new Date().toISOString(),\n        priority: '0.70',\n      },\n    ],\n  },\n\n  // Plugin: View Model Related Documents\n  [AddQueryOutputToViewModel.configKey]: {\n    events: {\n      callback: [\n        'view-model-home',\n        'view-model-edit',\n        'view-model-new',\n        'view-model-search',\n        'view-model-tag',\n        'view-model-tag-index',\n        'view-model-detail',\n      ],\n    },\n    queries: {\n      'view-model-home' : [\n        {\n          key: 'tags',\n          query: `SELECT tags FROM documents WHERE slug NOT_IN (\"${ignoreSlugs.join('\", \"')}\") ORDER BY id ASC LIMIT -1`,\n          format: (tags) =\u003e [...new Set(tags.flatMap((t) =\u003e t.tags))].filter(Boolean).sort((a, b) =\u003e a.localeCompare(b)),\n          fallback: [],\n        },\n        {\n          key: 'documents',\n          query: `SELECT * FROM documents WHERE slug NOT_IN (\"${ignoreSlugs.join('\", \"')}\") ORDER BY id ASC LIMIT -1`,\n          fallback: [],\n        },\n        {\n          key: 'popularDocuments',\n          fallback: [],\n          format: (results) =\u003e results.map((result) =\u003e result.slug),\n          queryFunction: async (target, context) =\u003e {\n            const ignoreSlugs = ['home-page'];\n            const [popular] = await context.hooks.fetch('popular-documents', { limit: 5 }, context);\n            const slugs = `\"${popular.map(({ slug }) =\u003e slug).join('\", \"')}\"`;\n            const query = `SELECT 'slug', 'title' FROM documents WHERE slug NOT_IN (${ignoreSlugs}) AND slug IN (${slugs}) ORDER BY updateDate DESC LIMIT 5`;\n            const [results] = await context.hooks.fetch('storage-query', query);\n            return [results];\n          },\n        }\n      ],\n    },\n  },\n\n  // Middleware Configuration in the form of ['function', 'param1', 'param2', ...]\n  middleware: [\n    ['disable', 'x-powered-by'],\n    ['enable', 'view cache'],\n    ['set', 'views', path.join(`${__dirname}/themes/`, 'default', 'templates')],\n\n    // EJS Specific Setup\n    ['use', layouts],\n    ['set', 'layout extractScripts', true],\n    ['set', 'layout extractStyles', true],\n    // If you use the `.ejs` extension use the below:\n    // ['set', 'view engine', 'ejs'],\n    // I prefer using `.html` templates:\n    ['set', 'view engine', 'html'],\n    ['engine', 'html', ejs.renderFile],\n  ],\n\n  redirects: [\n    {\n      route: '/:year/:slug',\n      target: '/:slug',\n      status: 301,\n      appendQueryString: true,\n    },\n  ],\n\n  // Override route handlers\n  homeRoute: (request, response, next) =\u003e { ... },\n  tagIndexRoute: (request, response, next) =\u003e { ... },\n  tagRoute: (request, response, next) =\u003e { ... },\n  searchRoute: (request, response, next) =\u003e { ... },\n  editRoute: (request, response, next) =\u003e { ... },\n  deleteRoute: (request, response, next) =\u003e { ... },\n  saveRoute: (request, response, next) =\u003e { ... },\n  saveNewRoute: (request, response, next) =\u003e { ... },\n  newRoute: (request, response, next) =\u003e { ... },\n  detailRoute: (request, response, next) =\u003e { ... },\n  previewRoute: (request, response, next) =\u003e { ... },\n  historyIndexRoute: (request, response, next) =\u003e { ... },\n  historyDetailRoute: (request, response, next) =\u003e { ... },\n  historyRestoreRoute: (request, response, next) =\u003e { ... },\n  notFoundRoute: (request, response, next) =\u003e { ... },\n  saveValidRoute: (request, response, next) =\u003e { ... },\n\n  // Custom per route middleware, in the order they should be used\n  routeMiddleware: {\n    home: [],\n    tagIndex: [],\n    tag: [],\n    search: [],\n    notFound: [],\n    create: [],\n    saveNew: [],\n    preview: [],\n    edit: [],\n    delete: [],\n    historyIndex: [],\n    historyDetail: [],\n    historyRestore: [],\n    save: [],\n    detail: [],\n  },\n};\n\nexport default config;\n```\n\nUse in an example Express.js app:\n\n```javascript\n// Server\nimport express from 'express';\n\n// Reference the Uttori Wiki middleware\nimport { wiki as middleware } from '@uttori/wiki';\n\n// Pull in our custom config, example above\nimport config from './config.js';\n\n// Initilize Your app\nconst app = express();\n\n// Setup the app\napp.set('port', process.env.PORT || 8000);\napp.set('ip', process.env.IP || '127.0.0.1');\n\n// Setup Express\napp.use(express.json({ limit: '50mb' }));\napp.use(express.urlencoded({ limit: '50mb', extended: true }));\n\n// Setup the wiki, could also mount under a sub directory path with other applications\napp.use('/', middleware(config));\n\n// Listen for connections\napp.listen(app.get('port'), app.get('ip'), () =\u003e {\n  console.log('✔ listening at %s:%d', app.get('ip'), app.get('port'));\n});\n```\n\n## Events\n\nThe following events are avaliable to hook into through plugins and are used in the methods below:\n\n| Name                         | Type       | Returns                   | Description |\n|------------------------------|------------|---------------------------|-------------|\n| `bind-routes`                | `dispatch` |                           | Called after the default routes are bound to the server. |\n| `document-delete`            | `dispatch` |                           | Called when a document is about to be deleted. |\n| `document-save`              | `filter`   | Uttori Document           | Called when a document is about to be saved. |\n| `render-content`             | `filter`   | HTML Content              | Called when content is being prepared to be shown. |\n| `render-search-results`      | `filter`   | Array of Uttori Documents | Called when search results have been collected and is being prepared to be shown. |\n| `validate-config`            | `dispatch` |                           | Called after initial configuration validation. |\n| `validate-invalid`           | `dispatch` |                           | Called when a document is found invalid (spam?). |\n| `validate-valid`             | `dispatch` |                           | Called when a document is found to be valid. |\n| `validate-save`              | `validate` | Boolean                   | Called before saving a document to validate the document. |\n| `view-model-detail`          | `filter`   | View Model                | Called when rendering the detail page just before being shown. |\n| `view-model-edit`            | `filter`   | View Model                | Called when rendering the edit page just before being shown. |\n| `view-model-error-404`       | `filter`   | View Model                | Called when rendering a 404 Not Found error page just before being shown. |\n| `view-model-history-detail`  | `filter`   | View Model                | Called when rendering a history detail page just before being shown. |\n| `view-model-history-index`   | `filter`   | View Model                | Called when rendering a history index page just before being shown. |\n| `view-model-history-restore` | `filter`   | View Model                | Called when rendering a history restore page just before being shown. |\n| `view-model-home`            | `filter`   | View Model                | Called when rendering the home page just before being shown. |\n| `view-model-metadata`        | `filter`   | View Model                | Called after the initial view model metadata is setup. |\n| `view-model-new`             | `filter`   | View Model                | Called when rendering the new document page just before being shown. |\n| `view-model-search`          | `filter`   | View Model                | Called when rendering a search result page just before being shown. |\n| `view-model-tag-index`       | `filter`   | View Model                | Called when rendering the tag index page just before being shown. |\n| `view-model-tag`             | `filter`   | View Model                | Called when rendering a tag detail page just before being shown. |\n\n* * *\n\n## Included Plugins\n\n## Form Handler Plugin\n\nA flexible form handling plugin for Uttori Wiki that allows you to easily define multiple forms through configuration objects and handle submissions with configurable handlers.\n\n### Features\n\n- **Multiple Form Support**: Define multiple forms with different configurations\n- **Flexible Field Types**: Support for text, email, textarea, number, url, and custom validation\n- **Configurable Handlers**: Default console logging, email sending, Google Sheets integration\n- **JSON and Form Data**: Accepts both JSON and form-encoded data\n- **Validation**: Built-in validation with custom regex support\n- **Middleware Support**: Add custom middleware for authentication, rate limiting, etc.\n- **Error Handling**: Comprehensive error handling and validation\n\n### Configuration\n\nAdd the plugin to your Uttori Wiki configuration:\n\n```javascript\nimport FormHandler from './src/plugins/form-handler.js';\nimport EmailHandler from './src/plugins/form-handlers/email-handler.js';\nimport GoogleDocsHandler from './src/plugins/form-handlers/google-docs-handler.js';\n\nconst config = {\n  // ... other config\n  plugins: [\n    FormHandler,\n    // ... other plugins\n  ],\n  [FormHandler.configKe]: {\n    baseRoute: '/forms', // Optional: base route for all forms\n    forms: [\n      {\n        name: 'contact',\n        route: '/contact',\n        fields: [\n          {\n            name: 'name',\n            type: 'text',\n            required: true,\n            label: 'Full Name',\n            placeholder: 'Enter your full name'\n          },\n          {\n            name: 'email',\n            type: 'email',\n            required: true,\n            label: 'Email Address',\n            placeholder: 'Enter your email address'\n          },\n          {\n            name: 'message',\n            type: 'textarea',\n            required: true,\n            label: 'Message',\n            placeholder: 'Enter your message'\n          }\n        ],\n        handler: EmailHandler.create({\n          host: 'smtp.gmail.com',\n          port: 587,\n          secure: false,\n          user: 'your-email@gmail.com',\n          pass: 'your-app-password',\n          from: 'your-email@gmail.com',\n          to: 'contact@yoursite.com',\n          subject: 'Contact Form Submission from {name}',\n          template: `\n            \u003ch2\u003eNew Contact Form Submission\u003c/h2\u003e\n            \u003cp\u003e\u003cstrong\u003eName:\u003c/strong\u003e {name}\u003c/p\u003e\n            \u003cp\u003e\u003cstrong\u003eEmail:\u003c/strong\u003e {email}\u003c/p\u003e\n            \u003cp\u003e\u003cstrong\u003eMessage:\u003c/strong\u003e\u003c/p\u003e\n            \u003cp\u003e{message}\u003c/p\u003e\n            \u003chr\u003e\n            \u003cp\u003e\u003cem\u003eSubmitted at: {timestamp}\u003c/em\u003e\u003c/p\u003e\n          `\n        }),\n        successMessage: 'Thank you for your message! We will get back to you soon.',\n        errorMessage: 'There was an error submitting your message. Please try again.'\n      }\n    ]\n  }\n};\n```\n\n### Form Configuration\n\n#### Form Object Properties\n\n- **name** (string, required): Unique identifier for the form\n- **route** (string, required): URL route for form submission (relative to baseRoute)\n- **fields** (array, required): Array of field configurations\n- **handler** (function, optional): Custom handler function for form processing\n- **successMessage** (string, required): Success message to return\n- **errorMessage** (string, required): Error message to return\n- **middleware** (array, optional): Custom Express middleware for the form route\n\n#### Field Object Properties\n\n- **name** (string, required): Field name (used as form data key)\n- **type** (string, required): Field type (text, email, textarea, number, url)\n- **required** (boolean, optional): Whether the field is required\n- **label** (string, optional): Display label for the field\n- **placeholder** (string, optional): Placeholder text for the field\n- **validation** (function, optional): Custom validation function\n- **errorMessage** (string, optional): Custom error message for validation\n\n### Built-in Handlers\n\n#### Default Handler (Console Logging)\n\nIf no custom handler is provided, the form data will be logged to the console:\n\n```javascript\n{\n  name: 'feedback',\n  route: '/feedback',\n  fields: [\n    { name: 'rating', type: 'number', required: true },\n    { name: 'comment', type: 'textarea', required: false }\n  ]\n  // No handler - uses default console.log\n}\n```\n\n#### Email Handler\n\nSend form submissions via email using `nodemailer`:\n\n```javascript\nimport EmailHandler from './src/plugins/form-handlers/email-handler.js';\n\n// In your form configuration\nhandler: EmailHandler.create({\n  transportOptions: { ... },\n  from: 'your-email@gmail.com',\n  to: 'contact@yoursite.com',\n  subject: 'Contact Form Submission from {name}',\n  template: `\n    \u003ch2\u003eNew Contact Form Submission\u003c/h2\u003e\n    \u003cp\u003e\u003cstrong\u003eName:\u003c/strong\u003e {name}\u003c/p\u003e\n    \u003cp\u003e\u003cstrong\u003eEmail:\u003c/strong\u003e {email}\u003c/p\u003e\n    \u003cp\u003e\u003cstrong\u003eMessage:\u003c/strong\u003e\u003c/p\u003e\n    \u003cp\u003e{message}\u003c/p\u003e\n  `\n})\n```\n\n##### Email Handler Configuration\n\n- **transportOptions.host** (string, required): SMTP host\n- **transportOptions.port** (number, required): SMTP port\n- **transportOptions.secure** (boolean, optional): Whether to use SSL/TLS\n- **transportOptions.auth.user** (string, required): SMTP username\n- **transportOptions.auth.pass** (string, required): SMTP password\n- **from** (string, required): Email address to send from\n- **to** (string, required): Email address to send to\n- **subject** (string, required): Email subject template\n- **template** (string, optional): Email body HTML template\n\n##### Email Template Variables\n\n- `{formName}`: The form name\n- `{timestamp}`: Current timestamp\n- `{fieldName}`: Any form field value (replace `fieldName` with actual field name)\n\n#### Google Sheets Handler\n\nWrite form submissions to Google Sheets:\n\n```javascript\nimport GoogleDocsHandler from './src/plugins/form-handlers/google-docs-handler.js';\n\n// In your form configuration\nhandler: GoogleDocsHandler.create({\n  credentialsPath: './google-credentials.json',\n  spreadsheetId: 'your-spreadsheet-id',\n  sheetName: 'Form Submissions',\n  headers: ['name', 'email', 'message'],\n  appendTimestamp: true\n})\n```\n\n##### Google Sheets Handler Configuration\n\n- **credentialsPath** (string, required): Path to Google service account credentials JSON file\n- **spreadsheetId** (string, required): Google Sheets spreadsheet ID\n- **sheetName** (string, required): Name of the sheet to write to\n- **headers** (array, optional): Custom headers for the spreadsheet\n- **appendTimestamp** (boolean, optional): Whether to append timestamp to each row\n\n##### Setting up Google Sheets\n\n1. Create a Google Cloud Project and enable the Google Sheets API\n2. Create a service account and download the credentials JSON file\n3. Share your Google Sheet with the service account email\n4. Use `GoogleDocsHandler.setupHeaders(config)` to initialize the sheet headers\n\n### Custom Handlers\n\nYou can create custom handlers by providing a function that accepts form data, form config, request, and response:\n\n```javascript\n{\n  name: 'custom-form',\n  route: '/custom',\n  fields: [\n    { name: 'data', type: 'text', required: true }\n  ],\n  handler: async (formData, formConfig, req, res) =\u003e {\n    // Custom processing logic\n    console.log('Custom handler processing:', formData);\n\n    // Save to database, send to API, etc.\n    await saveToDatabase(formData);\n\n    return {\n      message: 'Data processed successfully',\n      id: 'some-id'\n    };\n  }\n}\n```\n\n### API Endpoints\n\nForms are accessible at: `{baseRoute}{formRoute}`\n\nFor example, with `baseRoute: '/forms'` and form `route: '/contact'`:\n- POST `/forms/contact`\n\n### Request/Response Format\n\n#### Request\n\nAccepts both JSON and form-encoded data:\n\n```javascript\n// JSON\nfetch('/forms/contact', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    name: 'John Doe',\n    email: 'john@example.com',\n    message: 'Hello world!'\n  })\n});\n\n// Form data\nconst formData = new FormData();\nformData.append('name', 'John Doe');\nformData.append('email', 'john@example.com');\nformData.append('message', 'Hello world!');\n\nfetch('/forms/contact', {\n  method: 'POST',\n  body: formData\n});\n```\n\n#### Response\n\n```javascript\n// Success\n{\n  \"success\": true,\n  \"message\": \"Thank you for your message! We will get back to you soon.\",\n  \"data\": {\n    \"messageId\": \"email-message-id\",\n    \"message\": \"Email sent successfully\"\n  }\n}\n\n// Error\n{\n  \"success\": false,\n  \"message\": \"There was an error submitting your message. Please try again.\",\n  \"errors\": [\n    \"Field \\\"email\\\" must be a valid email address\"\n  ]\n}\n```\n\n### Validation\n\nThe plugin provides built-in validation for:\n\n- **Required fields**: Checks if required fields are present and not empty\n- **Email format**: Validates email addresses using regex\n- **Number format**: Validates numeric values\n- **URL format**: Validates URLs\n- **Custom regex**: Supports custom validation patterns\n\n### Middleware Support\n\nAdd custom middleware for authentication, rate limiting, etc.:\n\n```javascript\n{\n  name: 'admin-form',\n  route: '/admin/feedback',\n  fields: [\n    { name: 'feedback', type: 'textarea', required: true }\n  ],\n  middleware: [\n    // Authentication middleware\n    (req, res, next) =\u003e {\n      if (!req.session || !req.session.user) {\n        return res.status(401).json({ error: 'Authentication required' });\n      }\n      next();\n    },\n    // Rate limiting middleware\n    rateLimit({\n      windowMs: 15 * 60 * 1000, // 15 minutes\n      max: 5 // limit each IP to 5 requests per windowMs\n    })\n  ],\n  handler: customHandler\n}\n```\n\n### Error Handling\n\nThe plugin handles various error scenarios:\n\n- **Validation errors**: Returns 400 with validation details\n- **Handler errors**: Returns 500 with error message\n- **Configuration errors**: Throws during plugin registration\n- **Missing fields**: Validates required fields\n- **Invalid data types**: Validates field types and formats\n\n### Dependencies\n\n- **nodemailer**: For email handler (install with `npm install nodemailer`)\n- **googleapis**: For Google Sheets handler (install with `npm install googleapis`)\n\n### Security Considerations\n\n- Validate all input data\n- Use HTTPS in production\n- Implement rate limiting for public forms\n- Sanitize email templates to prevent injection\n- Secure Google credentials file\n- Use environment variables for sensitive configuration\n\n---\n\n## Tag Routes Plugin\n\nThis plugin provides tag index and individual tag pages.\n\n### Installation\n\nAdd the plugin to your wiki configuration:\n\n```javascript\nimport TagRoutesPlugin from './plugins/tag-routes.js';\n\nconst config = {\n  plugins: [\n    TagRoutesPlugin,\n    // ... other plugins\n  ],\n  'uttori-plugin-tag-routes': {\n    // plugin configuration\n  }\n};\n```\n\n### Configuration\n\nThe plugin accepts the following configuration options:\n\n```javascript\n{\n  'uttori-plugin-tag-routes': {\n    route: 'tags',                    // Route path for tag pages (default: 'tags')\n    title: 'Tags',                    // Default title for tag pages (default: 'Tags')\n    ignoreTags: [],                   // Tags to ignore when generating the tags page (default: [])\n    limit: 1024,                      // Max documents per tag (default: 1024)\n    titles: {},                       // Custom titles for specific tags (default: {})\n    tagIndexRoute: undefined,         // Custom tag index route handler (default: undefined)\n    tagRoute: undefined,              // Custom tag detail route handler (default: undefined)\n    routeMiddleware: {                // Middleware for tag routes\n      tagIndex: [],\n      tag: []\n    }\n  }\n}\n```\n\n### Required Hooks\n\nThe plugin uses the following hooks to maintain existing functionality:\n\n#### Core Hooks Used\n\n1. **`bind-routes`** (dispatch)\n   - **Purpose**: Registers tag routes with the Express server\n   - **Usage**: Plugin listens to this hook to add its routes\n   - **Implementation**: `context.hooks.on('bind-routes', TagRoutesPlugin.bindRoutes(plugin))`\n\n2. **`storage-query`** (fetch)\n   - **Purpose**: Queries the storage system for documents\n   - **Usage**: Used in `getTaggedDocuments()` to find documents with specific tags\n   - **Implementation**: `await this.context.hooks.fetch('storage-query', query, this.context)`\n\n3. **`view-model-tag-index`** (filter)\n   - **Purpose**: Allows modification of the tag index view model\n   - **Usage**: Applied to the view model before rendering the tag index page\n   - **Implementation**: `await this.context.hooks.filter('view-model-tag-index', viewModel, this.context)`\n\n4. **`view-model-tag`** (filter)\n   - **Purpose**: Allows modification of the individual tag view model\n   - **Usage**: Applied to the view model before rendering individual tag pages\n   - **Implementation**: `await this.context.hooks.filter('view-model-tag', viewModel, this.context)`\n\n#### Context Methods Used\n\nThe plugin relies on the following methods from the wiki context:\n\n1. **`buildMetadata(document, path, robots)`**\n   - **Purpose**: Builds metadata for view models\n   - **Usage**: Creates metadata for tag index and tag detail pages\n\n2. **`config.ignoreSlugs`**\n   - **Purpose**: List of slugs to exclude from tag queries\n   - **Usage**: Used in storage queries to filter out ignored documents\n\n### Routes Provided\n\nThe plugin registers the following routes:\n\n1. **`GET /{route}`** (default: `GET /tags`)\n   - **Handler**: `tagIndex`\n   - **Purpose**: Displays the tag index page with all available tags\n   - **Template**: `tags`\n\n2. **`GET /{route}/:tag`** (default: `GET /tags/:tag`)\n   - **Handler**: `tag`\n   - **Purpose**: Displays all documents with a specific tag\n   - **Template**: `tag`\n\n### Templates Required\n\nThe plugin expects the following templates to exist in your theme:\n\n1. **`tags`** - Tag index page template\n   - **Variables**: `title`, `config`, `session`, `taggedDocuments`, `meta`, `basePath`, `flash`\n\n2. **`tag`** - Individual tag page template\n   - **Variables**: `title`, `config`, `session`, `taggedDocuments`, `meta`, `basePath`, `flash`\n\n### Migration from Core\n\nWhen migrating from the core tag functionality:\n\n1. **Remove from config.js**:\n   - `ignoreTags` property\n   - `routes.tags` property\n   - `titles.tags` property\n   - `tagIndexRoute` and `tagRoute` properties\n   - `routeMiddleware.tagIndex` and `routeMiddleware.tag` properties\n\n2. **Remove from wiki.js**:\n   - `tagIndex` method\n   - `tag` method\n   - `getTaggedDocuments` method\n   - Tag route binding in `bindRoutes`\n\n3. **Add plugin to configuration**:\n   - Import `TagRoutesPlugin`\n   - Add to `plugins` array\n   - Configure with `'uttori-plugin-tag-routes'` key\n\n### Backward Compatibility\n\nThe plugin maintains full backward compatibility with existing functionality:\n\n- All existing hooks continue to work\n- Template variables remain the same\n- Route structure is preserved (configurable)\n- Custom route handlers are supported\n- Middleware support is maintained\n\n### Example Usage\n\n```javascript\nimport UttoriWiki from './src/wiki.js';\nimport TagRoutesPlugin from './src/plugins/tag-routes.js';\n\nconst config = {\n  plugins: [TagRoutesPlugin],\n  'uttori-plugin-tag-routes': {\n    route: 'categories',              // Use 'categories' instead of 'tags'\n    title: 'Categories',              // Custom title\n    ignoreTags: ['private', 'draft'], // Ignore these tags\n    limit: 50,                        // Limit to 50 documents per tag\n    titles: {                         // Custom titles for specific tags\n      'javascript': 'JavaScript',\n      'nodejs': 'Node.js'\n    }\n  }\n};\n\nconst wiki = new UttoriWiki(config, server);\n```\n\nThis will create routes at `/categories` and `/categories/:tag` with the specified configuration.\n\n---\n\n## API Reference\n\n## Classes\n\n\u003cdl\u003e\n\u003cdt\u003e\u003ca href=\"#UttoriWiki\"\u003eUttoriWiki\u003c/a\u003e\u003c/dt\u003e\n\u003cdd\u003e\u003cp\u003eUttoriWiki is a fast, simple, wiki knowledge base.\u003c/p\u003e\n\u003c/dd\u003e\n\u003c/dl\u003e\n\n## Typedefs\n\n\u003cdl\u003e\n\u003cdt\u003e\u003ca href=\"#UttoriWikiViewModel\"\u003eUttoriWikiViewModel\u003c/a\u003e : \u003ccode\u003eobject\u003c/code\u003e\u003c/dt\u003e\n\u003cdd\u003e\u003c/dd\u003e\n\u003cdt\u003e\u003ca href=\"#UttoriWikiDocument\"\u003eUttoriWikiDocument\u003c/a\u003e : \u003ccode\u003eobject\u003c/code\u003e\u003c/dt\u003e\n\u003cdd\u003e\u003c/dd\u003e\n\u003cdt\u003e\u003ca href=\"#UttoriWikiDocumentAttachment\"\u003eUttoriWikiDocumentAttachment\u003c/a\u003e : \u003ccode\u003eobject\u003c/code\u003e\u003c/dt\u003e\n\u003cdd\u003e\u003c/dd\u003e\n\u003cdt\u003e\u003ca href=\"#UttoriWikiDocumentMetaData\"\u003eUttoriWikiDocumentMetaData\u003c/a\u003e : \u003ccode\u003eobject\u003c/code\u003e\u003c/dt\u003e\n\u003cdd\u003e\u003c/dd\u003e\n\u003c/dl\u003e\n\n\u003ca name=\"UttoriWiki\"\u003e\u003c/a\u003e\n\n## UttoriWiki\nUttoriWiki is a fast, simple, wiki knowledge base.\n\n**Kind**: global class  \n**Properties**\n\n| Name | Type | Description |\n| --- | --- | --- |\n| config | \u003ccode\u003eUttoriWikiConfig\u003c/code\u003e | The configuration object. |\n| hooks | \u003ccode\u003emodule:@uttori/event-dispatcher~EventDispatcher\u003c/code\u003e | The hook / event dispatching object. |\n\n\n* [UttoriWiki](#UttoriWiki)\n    * [new UttoriWiki(config, server)](#new_UttoriWiki_new)\n    * [.config](#UttoriWiki+config) : \u003ccode\u003eUttoriWikiConfig\u003c/code\u003e\n    * [.hooks](#UttoriWiki+hooks) : \u003ccode\u003emodule:@uttori/event-dispatcher~EventDispatcher\u003c/code\u003e\n    * [.home](#UttoriWiki+home)\n    * [.homepageRedirect](#UttoriWiki+homepageRedirect) : \u003ccode\u003emodule:express~RequestHandler\u003c/code\u003e\n    * [.search](#UttoriWiki+search)\n    * [.edit](#UttoriWiki+edit)\n    * [.delete](#UttoriWiki+delete)\n    * [.save](#UttoriWiki+save)\n    * [.saveNew](#UttoriWiki+saveNew)\n    * [.create](#UttoriWiki+create)\n    * [.detail](#UttoriWiki+detail)\n    * [.preview](#UttoriWiki+preview)\n    * [.historyIndex](#UttoriWiki+historyIndex)\n    * [.historyDetail](#UttoriWiki+historyDetail)\n    * [.historyRestore](#UttoriWiki+historyRestore)\n    * [.notFound](#UttoriWiki+notFound)\n    * [.saveValid](#UttoriWiki+saveValid)\n    * [.registerPlugins(config)](#UttoriWiki+registerPlugins)\n    * [.validateConfig(config)](#UttoriWiki+validateConfig)\n    * [.buildMetadata(document, [path], [robots])](#UttoriWiki+buildMetadata) ⇒ [\u003ccode\u003ePromise.\u0026lt;UttoriWikiDocumentMetaData\u0026gt;\u003c/code\u003e](#UttoriWikiDocumentMetaData)\n    * [.bindRoutes(server)](#UttoriWiki+bindRoutes)\n\n\u003ca name=\"new_UttoriWiki_new\"\u003e\u003c/a\u003e\n\n### new UttoriWiki(config, server)\nCreates an instance of UttoriWiki.\n\n\n| Param | Type | Description |\n| --- | --- | --- |\n| config | \u003ccode\u003eUttoriWikiConfig\u003c/code\u003e | A configuration object. |\n| server | \u003ccode\u003emodule:express~Application\u003c/code\u003e | The Express server instance. |\n\n**Example** *(Init UttoriWiki)*  \n```js\nconst server = express();\nconst wiki = new UttoriWiki(config, server);\nserver.listen(server.get('port'), server.get('ip'), () =\u003e { ... });\n```\n\u003ca name=\"UttoriWiki+config\"\u003e\u003c/a\u003e\n\n### uttoriWiki.config : \u003ccode\u003eUttoriWikiConfig\u003c/code\u003e\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\u003ca name=\"UttoriWiki+hooks\"\u003e\u003c/a\u003e\n\n### uttoriWiki.hooks : \u003ccode\u003emodule:@uttori/event-dispatcher~EventDispatcher\u003c/code\u003e\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\u003ca name=\"UttoriWiki+home\"\u003e\u003c/a\u003e\n\n### uttoriWiki.home\nRenders the homepage with the `home` template.\n\nHooks:\n- `filter` - `render-content` - Passes in the home-page content.\n- `filter` - `view-model-home` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+homepageRedirect\"\u003e\u003c/a\u003e\n\n### uttoriWiki.homepageRedirect : \u003ccode\u003emodule:express~RequestHandler\u003c/code\u003e\nRedirects to the homepage.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\u003ca name=\"UttoriWiki+search\"\u003e\u003c/a\u003e\n\n### uttoriWiki.search\nRenders the search page using the `search` template.\n\nHooks:\n- `filter` - `render-search-results` - Passes in the search results.\n- `filter` - `view-model-search` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request.\u0026lt;{}, {}, {}, {s: string}\u0026gt;\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+edit\"\u003e\u003c/a\u003e\n\n### uttoriWiki.edit\nRenders the edit page using the `edit` template.\n\nHooks:\n- `filter` - `view-model-edit` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+delete\"\u003e\u003c/a\u003e\n\n### uttoriWiki.delete\nAttempts to delete a document and redirect to the homepage.\nIf the config `useDeleteKey` value is true, the key is verified before deleting.\n\nHooks:\n- `dispatch` - `document-delete` - Passes in the document beind deleted.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+save\"\u003e\u003c/a\u003e\n\n### uttoriWiki.save\nAttempts to update an existing document and redirects to the detail view of that document when successful.\n\nHooks:\n- `validate` - `validate-save` - Passes in the request.\n- `dispatch` - `validate-invalid` - Passes in the request.\n- `dispatch` - `validate-valid` - Passes in the request.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request.\u0026lt;SaveParams, {}, UttoriWikiDocument\u0026gt;\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+saveNew\"\u003e\u003c/a\u003e\n\n### uttoriWiki.saveNew\nAttempts to save a new document and redirects to the detail view of that document when successful.\n\nHooks:\n- `validate` - `validate-save` - Passes in the request.\n- `dispatch` - `validate-invalid` - Passes in the request.\n- `dispatch` - `validate-valid` - Passes in the request.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request.\u0026lt;SaveParams, {}, UttoriWikiDocument\u0026gt;\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+create\"\u003e\u003c/a\u003e\n\n### uttoriWiki.create\nRenders the creation page using the `edit` template.\n\nHooks:\n- `filter` - `view-model-new` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+detail\"\u003e\u003c/a\u003e\n\n### uttoriWiki.detail\nRenders the detail page using the `detail` template.\n\nHooks:\n- `fetch` - `storage-get` - Get the requested content from the storage.\n- `filter` - `render-content` - Passes in the document content.\n- `filter` - `view-model-detail` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+preview\"\u003e\u003c/a\u003e\n\n### uttoriWiki.preview\nRenders the a preview of the passed in content.\nSets the `X-Robots-Tag` header to `noindex`.\n\nHooks:\n- `render-content` - `render-content` - Passes in the request body content.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+historyIndex\"\u003e\u003c/a\u003e\n\n### uttoriWiki.historyIndex\nRenders the history index page using the `history_index` template.\nSets the `X-Robots-Tag` header to `noindex`.\n\nHooks:\n- `filter` - `view-model-history-index` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+historyDetail\"\u003e\u003c/a\u003e\n\n### uttoriWiki.historyDetail\nRenders the history detail page using the `detail` template.\nSets the `X-Robots-Tag` header to `noindex`.\n\nHooks:\n- `render-content` - `render-content` - Passes in the document content.\n- `filter` - `view-model-history-index` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+historyRestore\"\u003e\u003c/a\u003e\n\n### uttoriWiki.historyRestore\nRenders the history restore page using the `edit` template.\nSets the `X-Robots-Tag` header to `noindex`.\n\nHooks:\n- `filter` - `view-model-history-restore` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+notFound\"\u003e\u003c/a\u003e\n\n### uttoriWiki.notFound\nRenders the 404 Not Found page using the `404` template.\nSets the `X-Robots-Tag` header to `noindex`.\n\nHooks:\n- `filter` - `view-model-error-404` - Passes in the viewModel.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+saveValid\"\u003e\u003c/a\u003e\n\n### uttoriWiki.saveValid\nHandles saving documents, and changing the slug of documents, then redirecting to the document.\n\n`title`, `excerpt`, and `content` will default to a blank string\n`tags` is expected to be a comma delimited string in the request body, \"tag-1,tag-2\"\n`slug` will be converted to lowercase and will use `request.body.slug` and fall back to `request.params.slug`.\n\nHooks:\n- `filter` - `document-save` - Passes in the document.\n\n**Kind**: instance property of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| request | \u003ccode\u003emodule:express~Request.\u0026lt;SaveParams, {}, UttoriWikiDocument\u0026gt;\u003c/code\u003e | The Express Request object. |\n| response | \u003ccode\u003emodule:express~Response\u003c/code\u003e | The Express Response object. |\n| next | \u003ccode\u003emodule:express~NextFunction\u003c/code\u003e | The Express Next function. |\n\n\u003ca name=\"UttoriWiki+registerPlugins\"\u003e\u003c/a\u003e\n\n### uttoriWiki.registerPlugins(config)\nRegisters plugins with the Event Dispatcher.\n\n**Kind**: instance method of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| config | \u003ccode\u003eUttoriWikiConfig\u003c/code\u003e | A configuration object. |\n\n\u003ca name=\"UttoriWiki+validateConfig\"\u003e\u003c/a\u003e\n\n### uttoriWiki.validateConfig(config)\nValidates the config.\n\nHooks:\n- `dispatch` - `validate-config` - Passes in the config object.\n\n**Kind**: instance method of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| config | \u003ccode\u003eUttoriWikiConfig\u003c/code\u003e | A configuration object. |\n\n\u003ca name=\"UttoriWiki+buildMetadata\"\u003e\u003c/a\u003e\n\n### uttoriWiki.buildMetadata(document, [path], [robots]) ⇒ [\u003ccode\u003ePromise.\u0026lt;UttoriWikiDocumentMetaData\u0026gt;\u003c/code\u003e](#UttoriWikiDocumentMetaData)\nBuilds the metadata for the view model.\n\nHooks:\n- `filter` - `render-content` - Passes in the meta description.\n\n**Kind**: instance method of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n**Returns**: [\u003ccode\u003ePromise.\u0026lt;UttoriWikiDocumentMetaData\u0026gt;\u003c/code\u003e](#UttoriWikiDocumentMetaData) - Metadata object.  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| document | [\u003ccode\u003ePartial.\u0026lt;UttoriWikiDocument\u0026gt;\u003c/code\u003e](#UttoriWikiDocument) | A UttoriWikiDocument. |\n| [path] | \u003ccode\u003estring\u003c/code\u003e | The URL path to build meta data for with leading slash. |\n| [robots] | \u003ccode\u003estring\u003c/code\u003e | A meta robots tag value. |\n\n**Example**  \n```js\nconst metadata = await wiki.buildMetadata(document, '/private-document-path', 'no-index');\n➜ {\n  canonical,   // `${this.config.publicUrl}/private-document-path`\n  robots,      // 'no-index'\n  title,       // document.title\n  description, // document.excerpt || document.content.slice(0, 160)\n  modified,    // new Date(document.updateDate).toISOString()\n  published,   // new Date(document.createDate).toISOString()\n}\n```\n\u003ca name=\"UttoriWiki+bindRoutes\"\u003e\u003c/a\u003e\n\n### uttoriWiki.bindRoutes(server)\nBind the routes to the server.\nRoutes are bound in the order of Home, Tags, Search, Not Found Placeholder, Document, Plugins, Not Found - Catch All\n\nHooks:\n- `dispatch` - `bind-routes` - Passes in the server instance.\n\n**Kind**: instance method of [\u003ccode\u003eUttoriWiki\u003c/code\u003e](#UttoriWiki)  \n\n| Param | Type | Description |\n| --- | --- | --- |\n| server | \u003ccode\u003emodule:express~Application\u003c/code\u003e | The Express server instance. |\n\n\u003ca name=\"UttoriWikiViewModel\"\u003e\u003c/a\u003e\n\n## UttoriWikiViewModel : \u003ccode\u003eobject\u003c/code\u003e\n**Kind**: global typedef  \n**Properties**\n\n| Name | Type | Description |\n| --- | --- | --- |\n| title | \u003ccode\u003estring\u003c/code\u003e | The document title to be used anywhere a title may be needed. |\n| config | \u003ccode\u003eUttoriWikiConfig\u003c/code\u003e | The configuration object. |\n| meta | [\u003ccode\u003eUttoriWikiDocumentMetaData\u003c/code\u003e](#UttoriWikiDocumentMetaData) | The metadata object. |\n| basePath | \u003ccode\u003estring\u003c/code\u003e | The base path of the request. |\n| [document] | [\u003ccode\u003eUttoriWikiDocument\u003c/code\u003e](#UttoriWikiDocument) | The document object. |\n| [session] | \u003ccode\u003emodule:express-session~Session\u003c/code\u003e | The Express session object. |\n| [flash] | \u003ccode\u003eboolean\u003c/code\u003e \\| \u003ccode\u003eobject\u003c/code\u003e \\| \u003ccode\u003eArray.\u0026lt;string\u0026gt;\u003c/code\u003e | The flash object. |\n| [taggedDocuments] | [\u003ccode\u003eArray.\u0026lt;UttoriWikiDocument\u0026gt;\u003c/code\u003e](#UttoriWikiDocument) \\| \u003ccode\u003eRecord.\u0026lt;string, Array.\u0026lt;UttoriWikiDocument\u0026gt;\u0026gt;\u003c/code\u003e | An array of documents that are tagged with the document. |\n| [searchTerm] | \u003ccode\u003estring\u003c/code\u003e | The search term to be used in the search results. |\n| [searchResults] | [\u003ccode\u003eArray.\u0026lt;UttoriWikiDocument\u0026gt;\u003c/code\u003e](#UttoriWikiDocument) | An array of search results. |\n| [slug] | \u003ccode\u003estring\u003c/code\u003e | The slug of the document. |\n| [action] | \u003ccode\u003estring\u003c/code\u003e | The action to be used in the form. |\n| [revision] | \u003ccode\u003estring\u003c/code\u003e | The revision of the document. |\n| [historyByDay] | \u003ccode\u003eRecord.\u0026lt;string, Array.\u0026lt;string\u0026gt;\u0026gt;\u003c/code\u003e | An object of history by day. |\n| [currentDocument] | [\u003ccode\u003eUttoriWikiDocument\u003c/code\u003e](#UttoriWikiDocument) | The current version of the document for comparison. |\n| [diffs] | \u003ccode\u003eRecord.\u0026lt;string, string\u0026gt;\u003c/code\u003e | An object containing HTML table diffs for changed fields. |\n\n\u003ca name=\"UttoriWikiDocument\"\u003e\u003c/a\u003e\n\n## UttoriWikiDocument : \u003ccode\u003eobject\u003c/code\u003e\n**Kind**: global typedef  \n**Properties**\n\n| Name | Type | Description |\n| --- | --- | --- |\n| slug | \u003ccode\u003estring\u003c/code\u003e | The document slug to be used in the URL and as a unique ID. |\n| title | \u003ccode\u003estring\u003c/code\u003e | The document title to be used anywhere a title may be needed. |\n| [image] | \u003ccode\u003estring\u003c/code\u003e | An image to represent the document in Open Graph or elsewhere. |\n| [excerpt] | \u003ccode\u003estring\u003c/code\u003e | A succinct deescription of the document, think meta description. |\n| content | \u003ccode\u003estring\u003c/code\u003e | All text content for the doucment. |\n| [html] | \u003ccode\u003estring\u003c/code\u003e | All rendered HTML content for the doucment that will be presented to the user. |\n| createDate | \u003ccode\u003enumber\u003c/code\u003e | The Unix timestamp of the creation date of the document. |\n| updateDate | \u003ccode\u003enumber\u003c/code\u003e | The Unix timestamp of the last update date to the document. |\n| tags | \u003ccode\u003estring\u003c/code\u003e \\| \u003ccode\u003eArray.\u0026lt;string\u0026gt;\u003c/code\u003e | A collection of tags that represent the document. |\n| [redirects] | \u003ccode\u003estring\u003c/code\u003e \\| \u003ccode\u003eArray.\u0026lt;string\u0026gt;\u003c/code\u003e | An array of slug like strings that will redirect to this document. Useful for renaming and keeping links valid or for short form WikiLinks. |\n| [layout] | \u003ccode\u003estring\u003c/code\u003e | The layout to use when rendering the document. |\n| [attachments] | [\u003ccode\u003eArray.\u0026lt;UttoriWikiDocumentAttachment\u0026gt;\u003c/code\u003e](#UttoriWikiDocumentAttachment) | An array of attachments to the document with name being a display name, path being the path to the file, and type being the MIME type of the file. Useful for storing files like PDFs, images, etc. |\n\n\u003ca name=\"UttoriWikiDocumentAttachment\"\u003e\u003c/a\u003e\n\n## UttoriWikiDocumentAttachment : \u003ccode\u003eobject\u003c/code\u003e\n**Kind**: global typedef  \n**Properties**\n\n| Name | Type | Description |\n| --- | --- | --- |\n| name | \u003ccode\u003estring\u003c/code\u003e | The display name of the attachment. |\n| path | \u003ccode\u003estring\u003c/code\u003e | The path to the attachment. |\n| type | \u003ccode\u003estring\u003c/code\u003e | The MIME type of the attachment. |\n| [skip] | \u003ccode\u003eboolean\u003c/code\u003e | Whether to skip the attachment. Used to control whether to index the attachment. |\n\n\u003ca name=\"UttoriWikiDocumentMetaData\"\u003e\u003c/a\u003e\n\n## UttoriWikiDocumentMetaData : \u003ccode\u003eobject\u003c/code\u003e\n**Kind**: global typedef  \n**Properties**\n\n| Name | Type | Description |\n| --- | --- | --- |\n| canonical | \u003ccode\u003estring\u003c/code\u003e | `${this.config.publicUrl}/private-document-path` |\n| robots | \u003ccode\u003estring\u003c/code\u003e | 'no-index' |\n| title | \u003ccode\u003estring\u003c/code\u003e | document.title |\n| description | \u003ccode\u003estring\u003c/code\u003e | document.excerpt || document.content.slice(0, 160) |\n| modified | \u003ccode\u003estring\u003c/code\u003e | new Date(document.updateDate).toISOString() |\n| published | \u003ccode\u003estring\u003c/code\u003e | new Date(document.createDate).toISOString() |\n| image | \u003ccode\u003estring\u003c/code\u003e | OpenGraph Image |\n\n\n* * *\n\n## Tests\n\nTo run the test suite, first install the dependencies, then run `npm test`:\n\n```bash\nnpm install\nDEBUG=Uttori* npm test\n```\n\n## Contributors\n\n- [Matthew Callis](https://github.com/MatthewCallis) - author of UttoriWiki\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Futtori%2Futtori-wiki","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Futtori%2Futtori-wiki","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Futtori%2Futtori-wiki/lists"}