6.0.4 ā€¢ Published 4 months ago

@uttori/wiki v6.0.4

Weekly downloads
16
License
MIT
Repository
github
Last release
4 months ago

view on npm npm module downloads Build Status Coverage Status

Uttori Wiki

UttoriWiki is a fast, simple, wiki / knowledge base built around Express.js using the Uttori set of components allowing single 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.

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.

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 words out and replace text with emojis.

If you want to test it out, check out the demo repo to get up and going in a minutes.

Configuration

Please see src/config.js or the config doc for all options. Below is an example configuration using some plugins:

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],
  ],

  // 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'));
});

Events

The following events are avaliable to hook into through plugins and are used in the methods below:

NameTypeReturnsDescription
bind-routesdispatchCalled after the default routes are bound to the server.
document-deletedispatchCalled when a document is about to be deleted.
document-savefilterUttori DocumentCalled when a document is about to be saved.
render-contentfilterHTML ContentCalled when content is being prepared to be shown.
render-search-resultsfilterArray of Uttori DocumentsCalled when search results have been collected and is being prepared to be shown.
validate-configdispatchCalled after initial configuration validation.
validate-invaliddispatchCalled when a document is found invalid (spam?).
validate-validdispatchCalled when a document is found to be valid.
validate-savevalidateBooleanCalled before saving a document to validate the document.
view-model-detailfilterView ModelCalled when rendering the detail page just before being shown.
view-model-editfilterView ModelCalled when rendering the edit page just before being shown.
view-model-error-404filterView ModelCalled when rendering a 404 Not Found error page just before being shown.
view-model-history-detailfilterView ModelCalled when rendering a history detail page just before being shown.
view-model-history-indexfilterView ModelCalled when rendering a history index page just before being shown.
view-model-history-restorefilterView ModelCalled when rendering a history restore page just before being shown.
view-model-homefilterView ModelCalled when rendering the home page just before being shown.
view-model-metadatafilterView ModelCalled after the initial view model metadata is setup.
view-model-newfilterView ModelCalled when rendering the new document page just before being shown.
view-model-searchfilterView ModelCalled when rendering a search result page just before being shown.
view-model-tag-indexfilterView ModelCalled when rendering the tag index page just before being shown.
view-model-tagfilterView ModelCalled when rendering a tag detail page just before being shown.

API Reference

Classes

Functions

Typedefs

UttoriWiki

UttoriWiki is a fast, simple, wiki knowledge base.

Kind: global class
Properties

NameTypeDescription
configUttoriWikiConfigThe configuration object.
hooksEventDispatcherThe hook / event dispatching object.

new UttoriWiki(config, server)

Creates an instance of UttoriWiki.

ParamTypeDescription
configUttoriWikiConfigA configuration object.
servermodule:express~ApplicationThe Express server instance.

Example (Init UttoriWiki)

const server = express();
const wiki = new UttoriWiki(config, server);
server.listen(server.get('port'), server.get('ip'), () => { ... });

uttoriWiki.config : UttoriWikiConfig

Kind: instance property of UttoriWiki

uttoriWiki.hooks : EventDispatcher

Kind: instance property of UttoriWiki

uttoriWiki.registerPlugins(config)

Registers plugins with the Event Dispatcher.

Kind: instance method of UttoriWiki

ParamTypeDescription
configUttoriWikiConfigA configuration object.

uttoriWiki.validateConfig(config)

Validates the config.

Hooks:

  • dispatch - validate-config - Passes in the config object.

Kind: instance method of UttoriWiki

ParamTypeDescription
configUttoriWikiConfigA 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.

ParamTypeDescription
documentUttoriWikiDocument | objectA UttoriWikiDocument.
pathstringThe URL path to build meta data for with leading slash.
robotsstringA 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()
}

uttoriWiki.bindRoutes(server)

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

ParamTypeDescription
servermodule:express~ApplicationThe Express server instance.

uttoriWiki.home(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.homepageRedirect(request, response, _next)

Redirects to the homepage.

Kind: instance method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
_nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.tagIndex(request, response, next)

Renders the tag index page with the tags template.

Hooks:

  • filter - view-model-tag-index - Passes in the viewModel.

Kind: instance method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.tag(request, response, next)

Renders the tag detail page with tag template. Sets the X-Robots-Tag header to noindex. Attempts to pull in the relevant site section for the tag if defined in the config site sections.

Hooks:

  • filter - view-model-tag - Passes in the viewModel.

Kind: instance method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.search(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.edit(request, response, next)

Renders the edit page using the edit template.

Hooks:

  • filter - view-model-edit - Passes in the viewModel.

Kind: instance method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.delete(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.save(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.saveNew(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.create(request, response, next)

Renders the creation page using the edit template.

Hooks:

  • filter - view-model-new - Passes in the viewModel.

Kind: instance method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.detail(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.preview(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.historyIndex(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.historyDetail(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.historyRestore(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.notFound(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.saveValid(request, response, next)

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 method of UttoriWiki

ParamTypeDescription
requestmodule:express~RequestThe Express Request object.
responsemodule:express~ResponseThe Express Response object.
nextmodule:express~NextFunctionThe Express Next function.

uttoriWiki.getTaggedDocuments(tag, limit) ā‡’ Promise.<Array>

Returns the documents with the provided tag, up to the provided limit. This will exclude any documents that have slugs in the config.ignoreSlugs array.

Hooks:

  • fetch - storage-query - Searched for the tagged documents.

Kind: instance method of UttoriWiki
Returns: Promise.<Array> - Promise object that resolves to the array of the documents.

ParamTypeDefaultDescription
tagstringThe tag to look for in documents.
limitnumber1024The maximum number of documents to be returned.

Example

wiki.getTaggedDocuments('example', 10);
āžœ [{ slug: 'example', title: 'Example', content: 'Example content.', tags: ['example'] }]

asyncHandler() : AsyncRequestHandler

Kind: global function

UttoriWikiDocument : object

Kind: global typedef
Properties

NameTypeDescription
slugstringThe document slug to be used in the URL and as a unique ID.
titlestringThe document title to be used anywhere a title may be needed.
imagestringAn image to represent the document in Open Graph or elsewhere.
excerptstringA succinct deescription of the document, think meta description.
contentstringAll text content for the doucment.
htmlstringAll rendered HTML content for the doucment that will be presented to the user.
createDatenumberThe Unix timestamp of the creation date of the document.
updateDatenumberThe Unix timestamp of the last update date to the document.
tagsArray.<string>A collection of tags that represent the document.
redirectsArray.<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.
layoutstringThe layout to use when rendering the document.

UttoriWikiDocumentMetaData : object

Kind: global typedef
Properties

NameTypeDescription
canonicalstring${this.config.publicUrl}/private-document-path
robotsstring'no-index'
titlestringdocument.title
descriptionstringdocument.excerptdocument.content.slice(0, 160)
modifiedstringnew Date(document.updateDate).toISOString()
publishedstringnew Date(document.createDate).toISOString()
imagestringOpenGraph Image

Tests

To run the test suite, first install the dependencies, then run npm test:

npm install
DEBUG=Uttori* npm test

Contributors

License

MIT

6.0.4

4 months ago

6.0.1

4 months ago

6.0.0

5 months ago

6.0.3

4 months ago

6.0.2

4 months ago

5.2.2

1 year ago

5.2.1

1 year ago

5.2.0

1 year ago

5.1.0

1 year ago

5.0.3

1 year ago

5.0.2

2 years ago

5.0.1

2 years ago

5.0.0

2 years ago

4.2.2

2 years ago

4.2.1

2 years ago

4.2.0

2 years ago

4.1.1

3 years ago

4.1.0

3 years ago

4.0.0

3 years ago

3.4.1

3 years ago

3.4.0

4 years ago

3.3.0

4 years ago

3.2.1

4 years ago

3.2.0

4 years ago

3.1.0

4 years ago

3.0.0

4 years ago