UttoriWiki is a fast, simple, blog / wiki / knowledge base / generic website built around Express.js using the Uttori set of components allowing specific chunks of functionality be changed or swapped out to fit specific needs.
Why 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.
Because 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.
Nothing 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!
Rendering 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.
Please see src/config.js or the config doc for all options. Below is an example configuration using some plugins:
- @uttori/storage-provider-json-file
- @uttori/search-provider-lunr
- @uttori/plugin-renderer-replacer
- @uttori/plugin-renderer-markdown-it
- @uttori/plugin-upload-multer
- @uttori/plugin-generator-sitemap
- @uttori/plugin-analytics-json-file
import { Plugin: StorageProvider } from '@uttori/storage-provider-json-file';
import { Plugin: SearchProvider } from '@uttori/search-provider-lunr';
import AnalyticsPlugin from '@uttori/plugin-analytics-json-file';
import MarkdownItRenderer from '@uttori/plugin-renderer-markdown-it';
import ReplacerRenderer from '@uttori/plugin-renderer-replacer';
import MulterUpload from '@uttori/plugin-upload-multer';
import SitemapGenerator from '@uttori/plugin-generator-sitemap';
import { AddQueryOutputToViewModel } from '@uttori/wiki';
const config = {
  homePage: 'home-page',
  ignoreSlugs: ['home-page'],
  excerptLength: 400,
  publicUrl: 'http://127.0.0.1:8000/wiki',
  themePath: path.join(__dirname, 'theme'),
  publicPath: path.join(__dirname, 'public'),
  useDeleteKey: false,
  deleteKey: process.env.DELETE_KEY || '',
  useEditKey: false,
  editKey: process.env.EDIT_KEY || '',
  publicHistory: true,
  allowedDocumentKeys: [],
  // Plugins
  plugins: [
    StorageProvider,
    SearchProvider,
    AnalyticsPlugin,
    MarkdownItRenderer,
    ReplacerRenderer,
    MulterUpload,
    SitemapGenerator,
  ],
  // Use the JSON to Disk Storage Provider
  [StorageProvider.configKey]: {
    // Path in which to store content (markdown files, etc.)
    contentDirectory: `${__dirname}/content`,
    // Path in which to store content history (markdown files, etc.)
    historyDirectory: `${__dirname}/content/history`,
    // File Extension
    extension: 'json',
  },
  // Use the Lunr Search Provider
  [SearchProvider.configKey]: {
    // Optional Lunr locale
    lunr_locales: [],
    // Ignore Slugs
    ignoreSlugs: ['home-page'],
  },
  // Plugin: Analytics with JSON Files
  [AnalyticsPlugin.configKey]: {
    events: {
      getPopularDocuments: ['popular-documents'],
      updateDocument: ['document-save', 'document-delete'],
      validateConfig: ['validate-config'],
    },
    // Directory files will be uploaded to.
    directory: `${__dirname}/data`,
    // Name of the JSON file.
    name: 'visits',
    // File extension to use for the JSON file.
    extension: 'json',
  },
  // Plugin: Markdown rendering with MarkdownIt
  [MarkdownItRenderer.configKey]: {
    events: {
      renderContent: ['render-content'],
      renderCollection: ['render-search-results'],
      validateConfig: ['validate-config'],
    },
    // Uttori Specific Configuration
    uttori: {
      // Prefix for relative URLs, useful when the Express app is not at root.
      baseUrl: '',
      // Safe List, if a domain is not in this list, it is set to 'external nofollow noreferrer'.
      allowedExternalDomains: [
        'my-site.org',
      ],
      // Open external domains in a new window.
      openNewWindow: true,
      // Table of Contents
      toc: {
        // The opening DOM tag for the TOC container.
        openingTag: '<nav class="table-of-contents">',
        // The closing DOM tag for the TOC container.
        closingTag: '</nav>',
        // Slugify options for convering content to anchor links.
        slugify: {
          lower: true,
        },
      },
    },
  },
  // Plugin: Replace text
  [ReplacerRenderer.configKey]: {
    events: {
      renderContent: ['render-content'],
      renderCollection: ['render-search-results'],
      validateConfig: ['validate-config'],
    },
    // Rules for text replace
    rules: [
      {
        test: /bunny|rabbit/gm,
        output: '🐰',
      },
    ],
  },
  // Plugin: Multer Upload
  [MulterUpload.configKey]: {
    events: {
      bindRoutes: ['bind-routes'],
      validateConfig: ['validate-config'],
    },
    // Directory files will be uploaded to
    directory: `${__dirname}/uploads`,
    // URL to POST files to
    route: '/upload',
    // URL to GET uploads from
    publicRoute: '/uploads',
  },
  // Plugin: Sitemap Generator
  [SitemapGenerator.configKey]: {
    events: {
      callback: ['document-save', 'document-delete'],
      validateConfig: ['validate-config'],
    },
    // Sitemap URL (ie https://wiki.domain.tld)
    base_url: 'https://wiki.domain.tld',
    // Location where the XML sitemap will be written to.
    directory: `${__dirname}/themes/default/public`,
    urls: [
      {
        url: '/',
        lastmod: new Date().toISOString(),
        priority: '1.00',
      },
      {
        url: '/tags',
        lastmod: new Date().toISOString(),
        priority: '0.90',
      },
      {
        url: '/new',
        lastmod: new Date().toISOString(),
        priority: '0.70',
      },
    ],
  },
  // Plugin: View Model Related Documents
  [AddQueryOutputToViewModel.configKey]: {
    events: {
      callback: [
        'view-model-home',
        'view-model-edit',
        'view-model-new',
        'view-model-search',
        'view-model-tag',
        'view-model-tag-index',
        'view-model-detail',
      ],
    },
    queries: {
      'view-model-home' : [
        {
          key: 'tags',
          query: `SELECT tags FROM documents WHERE slug NOT_IN ("${ignoreSlugs.join('", "')}") ORDER BY id ASC LIMIT -1`,
          format: (tags) => [...new Set(tags.flatMap((t) => t.tags))].filter(Boolean).sort((a, b) => a.localeCompare(b)),
          fallback: [],
        },
        {
          key: 'documents',
          query: `SELECT * FROM documents WHERE slug NOT_IN ("${ignoreSlugs.join('", "')}") ORDER BY id ASC LIMIT -1`,
          fallback: [],
        },
        {
          key: 'popularDocuments',
          fallback: [],
          format: (results) => results.map((result) => result.slug),
          queryFunction: async (target, context) => {
            const ignoreSlugs = ['home-page'];
            const [popular] = await context.hooks.fetch('popular-documents', { limit: 5 }, context);
            const slugs = `"${popular.map(({ slug }) => slug).join('", "')}"`;
            const query = `SELECT 'slug', 'title' FROM documents WHERE slug NOT_IN (${ignoreSlugs}) AND slug IN (${slugs}) ORDER BY updateDate DESC LIMIT 5`;
            const [results] = await context.hooks.fetch('storage-query', query);
            return [results];
          },
        }
      ],
    },
  },
  // Middleware Configuration in the form of ['function', 'param1', 'param2', ...]
  middleware: [
    ['disable', 'x-powered-by'],
    ['enable', 'view cache'],
    ['set', 'views', path.join(`${__dirname}/themes/`, 'default', 'templates')],
    // EJS Specific Setup
    ['use', layouts],
    ['set', 'layout extractScripts', true],
    ['set', 'layout extractStyles', true],
    // If you use the `.ejs` extension use the below:
    // ['set', 'view engine', 'ejs'],
    // I prefer using `.html` templates:
    ['set', 'view engine', 'html'],
    ['engine', 'html', ejs.renderFile],
  ],
  redirects: [
    {
      route: '/:year/:slug',
      target: '/:slug',
      status: 301,
      appendQueryString: true,
    },
  ],
  // Override route handlers
  homeRoute: (request, response, next) => { ... },
  tagIndexRoute: (request, response, next) => { ... },
  tagRoute: (request, response, next) => { ... },
  searchRoute: (request, response, next) => { ... },
  editRoute: (request, response, next) => { ... },
  deleteRoute: (request, response, next) => { ... },
  saveRoute: (request, response, next) => { ... },
  saveNewRoute: (request, response, next) => { ... },
  newRoute: (request, response, next) => { ... },
  detailRoute: (request, response, next) => { ... },
  previewRoute: (request, response, next) => { ... },
  historyIndexRoute: (request, response, next) => { ... },
  historyDetailRoute: (request, response, next) => { ... },
  historyRestoreRoute: (request, response, next) => { ... },
  notFoundRoute: (request, response, next) => { ... },
  saveValidRoute: (request, response, next) => { ... },
  // Custom per route middleware, in the order they should be used
  routeMiddleware: {
    home: [],
    tagIndex: [],
    tag: [],
    search: [],
    notFound: [],
    create: [],
    saveNew: [],
    preview: [],
    edit: [],
    delete: [],
    historyIndex: [],
    historyDetail: [],
    historyRestore: [],
    save: [],
    detail: [],
  },
};
export default config;Use in an example Express.js app:
// Server
import express from 'express';
// Reference the Uttori Wiki middleware
import { wiki as middleware } from '@uttori/wiki';
// Pull in our custom config, example above
import config from './config.js';
// Initilize Your app
const app = express();
// Setup the app
app.set('port', process.env.PORT || 8000);
app.set('ip', process.env.IP || '127.0.0.1');
// Setup Express
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// Setup the wiki, could also mount under a sub directory path with other applications
app.use('/', middleware(config));
// Listen for connections
app.listen(app.get('port'), app.get('ip'), () => {
  console.log('✔ listening at %s:%d', app.get('ip'), app.get('port'));
});The following events are avaliable to hook into through plugins and are used in the methods below:
| Name | Type | Returns | Description | 
|---|---|---|---|
| bind-routes | dispatch | Called after the default routes are bound to the server. | |
| document-delete | dispatch | Called when a document is about to be deleted. | |
| document-save | filter | Uttori Document | Called when a document is about to be saved. | 
| render-content | filter | HTML Content | Called when content is being prepared to be shown. | 
| render-search-results | filter | Array of Uttori Documents | Called when search results have been collected and is being prepared to be shown. | 
| validate-config | dispatch | Called after initial configuration validation. | |
| validate-invalid | dispatch | Called when a document is found invalid (spam?). | |
| validate-valid | dispatch | Called when a document is found to be valid. | |
| validate-save | validate | Boolean | Called before saving a document to validate the document. | 
| view-model-detail | filter | View Model | Called when rendering the detail page just before being shown. | 
| view-model-edit | filter | View Model | Called when rendering the edit page just before being shown. | 
| view-model-error-404 | filter | View Model | Called when rendering a 404 Not Found error page just before being shown. | 
| view-model-history-detail | filter | View Model | Called when rendering a history detail page just before being shown. | 
| view-model-history-index | filter | View Model | Called when rendering a history index page just before being shown. | 
| view-model-history-restore | filter | View Model | Called when rendering a history restore page just before being shown. | 
| view-model-home | filter | View Model | Called when rendering the home page just before being shown. | 
| view-model-metadata | filter | View Model | Called after the initial view model metadata is setup. | 
| view-model-new | filter | View Model | Called when rendering the new document page just before being shown. | 
| view-model-search | filter | View Model | Called when rendering a search result page just before being shown. | 
| view-model-tag-index | filter | View Model | Called when rendering the tag index page just before being shown. | 
| view-model-tag | filter | View Model | Called when rendering a tag detail page just before being shown. | 
A flexible form handling plugin for Uttori Wiki that allows you to easily define multiple forms through configuration objects and handle submissions with configurable handlers.
- Multiple Form Support: Define multiple forms with different configurations
- Flexible Field Types: Support for text, email, textarea, number, url, and custom validation
- Configurable Handlers: Default console logging, email sending, Google Sheets integration
- JSON and Form Data: Accepts both JSON and form-encoded data
- Validation: Built-in validation with custom regex support
- Middleware Support: Add custom middleware for authentication, rate limiting, etc.
- Error Handling: Comprehensive error handling and validation
Add the plugin to your Uttori Wiki configuration:
import FormHandler from './src/plugins/form-handler.js';
import EmailHandler from './src/plugins/form-handlers/email-handler.js';
import GoogleDocsHandler from './src/plugins/form-handlers/google-docs-handler.js';
const config = {
  // ... other config
  plugins: [
    FormHandler,
    // ... other plugins
  ],
  [FormHandler.configKe]: {
    baseRoute: '/forms', // Optional: base route for all forms
    forms: [
      {
        name: 'contact',
        route: '/contact',
        fields: [
          {
            name: 'name',
            type: 'text',
            required: true,
            label: 'Full Name',
            placeholder: 'Enter your full name'
          },
          {
            name: 'email',
            type: 'email',
            required: true,
            label: 'Email Address',
            placeholder: 'Enter your email address'
          },
          {
            name: 'message',
            type: 'textarea',
            required: true,
            label: 'Message',
            placeholder: 'Enter your message'
          }
        ],
        handler: EmailHandler.create({
          host: 'smtp.gmail.com',
          port: 587,
          secure: false,
          user: 'your-email@gmail.com',
          pass: 'your-app-password',
          from: 'your-email@gmail.com',
          to: 'contact@yoursite.com',
          subject: 'Contact Form Submission from {name}',
          template: `
            <h2>New Contact Form Submission</h2>
            <p><strong>Name:</strong> {name}</p>
            <p><strong>Email:</strong> {email}</p>
            <p><strong>Message:</strong></p>
            <p>{message}</p>
            <hr>
            <p><em>Submitted at: {timestamp}</em></p>
          `
        }),
        successMessage: 'Thank you for your message! We will get back to you soon.',
        errorMessage: 'There was an error submitting your message. Please try again.'
      }
    ]
  }
};- name (string, required): Unique identifier for the form
- route (string, required): URL route for form submission (relative to baseRoute)
- fields (array, required): Array of field configurations
- handler (function, optional): Custom handler function for form processing
- successMessage (string, required): Success message to return
- errorMessage (string, required): Error message to return
- middleware (array, optional): Custom Express middleware for the form route
- name (string, required): Field name (used as form data key)
- type (string, required): Field type (text, email, textarea, number, url)
- required (boolean, optional): Whether the field is required
- label (string, optional): Display label for the field
- placeholder (string, optional): Placeholder text for the field
- validation (function, optional): Custom validation function
- errorMessage (string, optional): Custom error message for validation
If no custom handler is provided, the form data will be logged to the console:
{
  name: 'feedback',
  route: '/feedback',
  fields: [
    { name: 'rating', type: 'number', required: true },
    { name: 'comment', type: 'textarea', required: false }
  ]
  // No handler - uses default console.log
}Send form submissions via email using nodemailer:
import EmailHandler from './src/plugins/form-handlers/email-handler.js';
// In your form configuration
handler: EmailHandler.create({
  transportOptions: { ... },
  from: 'your-email@gmail.com',
  to: 'contact@yoursite.com',
  subject: 'Contact Form Submission from {name}',
  template: `
    <h2>New Contact Form Submission</h2>
    <p><strong>Name:</strong> {name}</p>
    <p><strong>Email:</strong> {email}</p>
    <p><strong>Message:</strong></p>
    <p>{message}</p>
  `
})- transportOptions.host (string, required): SMTP host
- transportOptions.port (number, required): SMTP port
- transportOptions.secure (boolean, optional): Whether to use SSL/TLS
- transportOptions.auth.user (string, required): SMTP username
- transportOptions.auth.pass (string, required): SMTP password
- from (string, required): Email address to send from
- to (string, required): Email address to send to
- subject (string, required): Email subject template
- template (string, optional): Email body HTML template
- {formName}: The form name
- {timestamp}: Current timestamp
- {fieldName}: Any form field value (replace- fieldNamewith actual field name)
Write form submissions to Google Sheets:
import GoogleDocsHandler from './src/plugins/form-handlers/google-docs-handler.js';
// In your form configuration
handler: GoogleDocsHandler.create({
  credentialsPath: './google-credentials.json',
  spreadsheetId: 'your-spreadsheet-id',
  sheetName: 'Form Submissions',
  headers: ['name', 'email', 'message'],
  appendTimestamp: true
})- credentialsPath (string, required): Path to Google service account credentials JSON file
- spreadsheetId (string, required): Google Sheets spreadsheet ID
- sheetName (string, required): Name of the sheet to write to
- headers (array, optional): Custom headers for the spreadsheet
- appendTimestamp (boolean, optional): Whether to append timestamp to each row
- Create a Google Cloud Project and enable the Google Sheets API
- Create a service account and download the credentials JSON file
- Share your Google Sheet with the service account email
- Use GoogleDocsHandler.setupHeaders(config)to initialize the sheet headers
You can create custom handlers by providing a function that accepts form data, form config, request, and response:
{
  name: 'custom-form',
  route: '/custom',
  fields: [
    { name: 'data', type: 'text', required: true }
  ],
  handler: async (formData, formConfig, req, res) => {
    // Custom processing logic
    console.log('Custom handler processing:', formData);
    // Save to database, send to API, etc.
    await saveToDatabase(formData);
    return {
      message: 'Data processed successfully',
      id: 'some-id'
    };
  }
}Forms are accessible at: {baseRoute}{formRoute}
For example, with baseRoute: '/forms' and form route: '/contact':
- POST /forms/contact
Accepts both JSON and form-encoded data:
// JSON
fetch('/forms/contact', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'John Doe',
    email: 'john@example.com',
    message: 'Hello world!'
  })
});
// Form data
const formData = new FormData();
formData.append('name', 'John Doe');
formData.append('email', 'john@example.com');
formData.append('message', 'Hello world!');
fetch('/forms/contact', {
  method: 'POST',
  body: formData
});// Success
{
  "success": true,
  "message": "Thank you for your message! We will get back to you soon.",
  "data": {
    "messageId": "email-message-id",
    "message": "Email sent successfully"
  }
}
// Error
{
  "success": false,
  "message": "There was an error submitting your message. Please try again.",
  "errors": [
    "Field \"email\" must be a valid email address"
  ]
}The plugin provides built-in validation for:
- Required fields: Checks if required fields are present and not empty
- Email format: Validates email addresses using regex
- Number format: Validates numeric values
- URL format: Validates URLs
- Custom regex: Supports custom validation patterns
Add custom middleware for authentication, rate limiting, etc.:
{
  name: 'admin-form',
  route: '/admin/feedback',
  fields: [
    { name: 'feedback', type: 'textarea', required: true }
  ],
  middleware: [
    // Authentication middleware
    (req, res, next) => {
      if (!req.session || !req.session.user) {
        return res.status(401).json({ error: 'Authentication required' });
      }
      next();
    },
    // Rate limiting middleware
    rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 5 // limit each IP to 5 requests per windowMs
    })
  ],
  handler: customHandler
}The plugin handles various error scenarios:
- Validation errors: Returns 400 with validation details
- Handler errors: Returns 500 with error message
- Configuration errors: Throws during plugin registration
- Missing fields: Validates required fields
- Invalid data types: Validates field types and formats
- nodemailer: For email handler (install with npm install nodemailer)
- googleapis: For Google Sheets handler (install with npm install googleapis)
- Validate all input data
- Use HTTPS in production
- Implement rate limiting for public forms
- Sanitize email templates to prevent injection
- Secure Google credentials file
- Use environment variables for sensitive configuration
This plugin provides tag index and individual tag pages.
Add the plugin to your wiki configuration:
import TagRoutesPlugin from './plugins/tag-routes.js';
const config = {
  plugins: [
    TagRoutesPlugin,
    // ... other plugins
  ],
  'uttori-plugin-tag-routes': {
    // plugin configuration
  }
};The plugin accepts the following configuration options:
{
  'uttori-plugin-tag-routes': {
    route: 'tags',                    // Route path for tag pages (default: 'tags')
    title: 'Tags',                    // Default title for tag pages (default: 'Tags')
    ignoreTags: [],                   // Tags to ignore when generating the tags page (default: [])
    limit: 1024,                      // Max documents per tag (default: 1024)
    titles: {},                       // Custom titles for specific tags (default: {})
    tagIndexRoute: undefined,         // Custom tag index route handler (default: undefined)
    tagRoute: undefined,              // Custom tag detail route handler (default: undefined)
    routeMiddleware: {                // Middleware for tag routes
      tagIndex: [],
      tag: []
    }
  }
}The plugin uses the following hooks to maintain existing functionality:
- 
bind-routes(dispatch)- Purpose: Registers tag routes with the Express server
- Usage: Plugin listens to this hook to add its routes
- Implementation: context.hooks.on('bind-routes', TagRoutesPlugin.bindRoutes(plugin))
 
- 
storage-query(fetch)- Purpose: Queries the storage system for documents
- Usage: Used in getTaggedDocuments()to find documents with specific tags
- Implementation: await this.context.hooks.fetch('storage-query', query, this.context)
 
- 
view-model-tag-index(filter)- Purpose: Allows modification of the tag index view model
- Usage: Applied to the view model before rendering the tag index page
- Implementation: await this.context.hooks.filter('view-model-tag-index', viewModel, this.context)
 
- 
view-model-tag(filter)- Purpose: Allows modification of the individual tag view model
- Usage: Applied to the view model before rendering individual tag pages
- Implementation: await this.context.hooks.filter('view-model-tag', viewModel, this.context)
 
The plugin relies on the following methods from the wiki context:
- 
buildMetadata(document, path, robots)- Purpose: Builds metadata for view models
- Usage: Creates metadata for tag index and tag detail pages
 
- 
config.ignoreSlugs- Purpose: List of slugs to exclude from tag queries
- Usage: Used in storage queries to filter out ignored documents
 
The plugin registers the following routes:
- 
GET /{route}(default:GET /tags)- Handler: tagIndex
- Purpose: Displays the tag index page with all available tags
- Template: tags
 
- Handler: 
- 
GET /{route}/:tag(default:GET /tags/:tag)- Handler: tag
- Purpose: Displays all documents with a specific tag
- Template: tag
 
- Handler: 
The plugin expects the following templates to exist in your theme:
- 
tags- Tag index page template- Variables: title,config,session,taggedDocuments,meta,basePath,flash
 
- Variables: 
- 
tag- Individual tag page template- Variables: title,config,session,taggedDocuments,meta,basePath,flash
 
- Variables: 
When migrating from the core tag functionality:
- 
Remove from config.js: - ignoreTagsproperty
- routes.tagsproperty
- titles.tagsproperty
- tagIndexRouteand- tagRouteproperties
- routeMiddleware.tagIndexand- routeMiddleware.tagproperties
 
- 
Remove from wiki.js: - tagIndexmethod
- tagmethod
- getTaggedDocumentsmethod
- Tag route binding in bindRoutes
 
- 
Add plugin to configuration: - Import TagRoutesPlugin
- Add to pluginsarray
- Configure with 'uttori-plugin-tag-routes'key
 
- Import 
The plugin maintains full backward compatibility with existing functionality:
- All existing hooks continue to work
- Template variables remain the same
- Route structure is preserved (configurable)
- Custom route handlers are supported
- Middleware support is maintained
import UttoriWiki from './src/wiki.js';
import TagRoutesPlugin from './src/plugins/tag-routes.js';
const config = {
  plugins: [TagRoutesPlugin],
  'uttori-plugin-tag-routes': {
    route: 'categories',              // Use 'categories' instead of 'tags'
    title: 'Categories',              // Custom title
    ignoreTags: ['private', 'draft'], // Ignore these tags
    limit: 50,                        // Limit to 50 documents per tag
    titles: {                         // Custom titles for specific tags
      'javascript': 'JavaScript',
      'nodejs': 'Node.js'
    }
  }
};
const wiki = new UttoriWiki(config, server);This will create routes at /categories and /categories/:tag with the specified configuration.
- UttoriWiki
- UttoriWiki is a fast, simple, wiki knowledge base. 
- UttoriWikiViewModel : object
- UttoriWikiDocument : object
- UttoriWikiDocumentAttachment : object
- UttoriWikiDocumentMetaData : object
UttoriWiki is a fast, simple, wiki knowledge base.
Kind: global class
Properties
| Name | Type | Description | 
|---|---|---|
| config | UttoriWikiConfig | The configuration object. | 
| hooks | module:@uttori/event-dispatcher~EventDispatcher | The hook / event dispatching object. | 
- UttoriWiki
- new UttoriWiki(config, server)
- .config : UttoriWikiConfig
- .hooks : module:@uttori/event-dispatcher~EventDispatcher
- .home
- .homepageRedirect : module:express~RequestHandler
- .search
- .edit
- .delete
- .save
- .saveNew
- .create
- .detail
- .preview
- .historyIndex
- .historyDetail
- .historyRestore
- .notFound
- .saveValid
- .registerPlugins(config)
- .validateConfig(config)
- .buildMetadata(document, [path], [robots]) ⇒ Promise.<UttoriWikiDocumentMetaData>
- .bindRoutes(server)
 
Creates an instance of UttoriWiki.
| Param | Type | Description | 
|---|---|---|
| config | UttoriWikiConfig | A configuration object. | 
| server | module:express~Application | The Express server instance. | 
Example (Init UttoriWiki)
const server = express();
const wiki = new UttoriWiki(config, server);
server.listen(server.get('port'), server.get('ip'), () => { ... });Kind: instance property of UttoriWiki
Kind: instance property of UttoriWiki
Renders the homepage with the home template.
Hooks:
- filter-- render-content- Passes in the home-page content.
- filter-- view-model-home- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Redirects to the homepage.
Kind: instance property of UttoriWiki
Renders the search page using the search template.
Hooks:
- filter-- render-search-results- Passes in the search results.
- filter-- view-model-search- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request.<{}, {}, {}, {s: string}> | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Renders the edit page using the edit template.
Hooks:
- filter-- view-model-edit- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Attempts to delete a document and redirect to the homepage.
If the config useDeleteKey value is true, the key is verified before deleting.
Hooks:
- dispatch-- document-delete- Passes in the document beind deleted.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Attempts to update an existing document and redirects to the detail view of that document when successful.
Hooks:
- validate-- validate-save- Passes in the request.
- dispatch-- validate-invalid- Passes in the request.
- dispatch-- validate-valid- Passes in the request.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request.<SaveParams, {}, UttoriWikiDocument> | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Attempts to save a new document and redirects to the detail view of that document when successful.
Hooks:
- validate-- validate-save- Passes in the request.
- dispatch-- validate-invalid- Passes in the request.
- dispatch-- validate-valid- Passes in the request.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request.<SaveParams, {}, UttoriWikiDocument> | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Renders the creation page using the edit template.
Hooks:
- filter-- view-model-new- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Renders the detail page using the detail template.
Hooks:
- fetch-- storage-get- Get the requested content from the storage.
- filter-- render-content- Passes in the document content.
- filter-- view-model-detail- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Renders the a preview of the passed in content.
Sets the X-Robots-Tag header to noindex.
Hooks:
- render-content-- render-content- Passes in the request body content.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Renders the history index page using the history_index template.
Sets the X-Robots-Tag header to noindex.
Hooks:
- filter-- view-model-history-index- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Renders the history detail page using the detail template.
Sets the X-Robots-Tag header to noindex.
Hooks:
- render-content-- render-content- Passes in the document content.
- filter-- view-model-history-index- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Renders the history restore page using the edit template.
Sets the X-Robots-Tag header to noindex.
Hooks:
- filter-- view-model-history-restore- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Renders the 404 Not Found page using the 404 template.
Sets the X-Robots-Tag header to noindex.
Hooks:
- filter-- view-model-error-404- Passes in the viewModel.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Handles saving documents, and changing the slug of documents, then redirecting to the document.
title, excerpt, and content will default to a blank string
tags is expected to be a comma delimited string in the request body, "tag-1,tag-2"
slug will be converted to lowercase and will use request.body.slug and fall back to request.params.slug.
Hooks:
- filter-- document-save- Passes in the document.
Kind: instance property of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| request | module:express~Request.<SaveParams, {}, UttoriWikiDocument> | The Express Request object. | 
| response | module:express~Response | The Express Response object. | 
| next | module:express~NextFunction | The Express Next function. | 
Registers plugins with the Event Dispatcher.
Kind: instance method of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| config | UttoriWikiConfig | A configuration object. | 
Validates the config.
Hooks:
- dispatch-- validate-config- Passes in the config object.
Kind: instance method of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| config | UttoriWikiConfig | A configuration object. | 
uttoriWiki.buildMetadata(document, [path], [robots]) ⇒ Promise.<UttoriWikiDocumentMetaData>
Builds the metadata for the view model.
Hooks:
- filter-- render-content- Passes in the meta description.
Kind: instance method of UttoriWiki
Returns: Promise.<UttoriWikiDocumentMetaData> - Metadata object.
| Param | Type | Description | 
|---|---|---|
| document | Partial.<UttoriWikiDocument> | A UttoriWikiDocument. | 
| [path] | string | The URL path to build meta data for with leading slash. | 
| [robots] | string | A meta robots tag value. | 
Example
const metadata = await wiki.buildMetadata(document, '/private-document-path', 'no-index');
➜ {
  canonical,   // `${this.config.publicUrl}/private-document-path`
  robots,      // 'no-index'
  title,       // document.title
  description, // document.excerpt || document.content.slice(0, 160)
  modified,    // new Date(document.updateDate).toISOString()
  published,   // new Date(document.createDate).toISOString()
}Bind the routes to the server. Routes are bound in the order of Home, Tags, Search, Not Found Placeholder, Document, Plugins, Not Found - Catch All
Hooks:
- dispatch-- bind-routes- Passes in the server instance.
Kind: instance method of UttoriWiki
| Param | Type | Description | 
|---|---|---|
| server | module:express~Application | The Express server instance. | 
Kind: global typedef
Properties
| Name | Type | Description | 
|---|---|---|
| title | string | The document title to be used anywhere a title may be needed. | 
| config | UttoriWikiConfig | The configuration object. | 
| meta | UttoriWikiDocumentMetaData | The metadata object. | 
| basePath | string | The base path of the request. | 
| [document] | UttoriWikiDocument | The document object. | 
| [session] | module:express-session~Session | The Express session object. | 
| [flash] | boolean|object|Array.<string> | The flash object. | 
| [taggedDocuments] | Array.<UttoriWikiDocument>|Record.<string, Array.<UttoriWikiDocument>> | An array of documents that are tagged with the document. | 
| [searchTerm] | string | The search term to be used in the search results. | 
| [searchResults] | Array.<UttoriWikiDocument> | An array of search results. | 
| [slug] | string | The slug of the document. | 
| [action] | string | The action to be used in the form. | 
| [revision] | string | The revision of the document. | 
| [historyByDay] | Record.<string, Array.<string>> | An object of history by day. | 
| [currentDocument] | UttoriWikiDocument | The current version of the document for comparison. | 
| [diffs] | Record.<string, string> | An object containing HTML table diffs for changed fields. | 
Kind: global typedef
Properties
| Name | Type | Description | 
|---|---|---|
| slug | string | The document slug to be used in the URL and as a unique ID. | 
| title | string | The document title to be used anywhere a title may be needed. | 
| [image] | string | An image to represent the document in Open Graph or elsewhere. | 
| [excerpt] | string | A succinct deescription of the document, think meta description. | 
| content | string | All text content for the doucment. | 
| [html] | string | All rendered HTML content for the doucment that will be presented to the user. | 
| createDate | number | The Unix timestamp of the creation date of the document. | 
| updateDate | number | The Unix timestamp of the last update date to the document. | 
| tags | string|Array.<string> | A collection of tags that represent the document. | 
| [redirects] | string|Array.<string> | An array of slug like strings that will redirect to this document. Useful for renaming and keeping links valid or for short form WikiLinks. | 
| [layout] | string | The layout to use when rendering the document. | 
| [attachments] | Array.<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. | 
Kind: global typedef
Properties
| Name | Type | Description | 
|---|---|---|
| name | string | The display name of the attachment. | 
| path | string | The path to the attachment. | 
| type | string | The MIME type of the attachment. | 
| [skip] | boolean | Whether to skip the attachment. Used to control whether to index the attachment. | 
Kind: global typedef
Properties
| Name | Type | Description | 
|---|---|---|
| canonical | string | ${this.config.publicUrl}/private-document-path | 
| robots | string | 'no-index' | 
| title | string | document.title | 
| description | string | document.excerpt | 
| modified | string | new Date(document.updateDate).toISOString() | 
| published | string | new Date(document.createDate).toISOString() | 
| image | string | OpenGraph Image | 
To run the test suite, first install the dependencies, then run npm test:
npm install
DEBUG=Uttori* npm test- Matthew Callis - author of UttoriWiki