About Preaction CMS

Preaction CMS is a barebones, extensible Content Management System built on top of simple JavaScript libraries. It is intended to be friendly to users, administrators, and developers.

git repo: https://github.com/duhdugg/preaction-cms

Installation

Requirements

Provided the following software requirements are met, you should be able to run Preaction CMS on just about any Linux environment with Bash, Node, and Yarn installed.

Installation Steps

  1. Clone the repository and "cd" into it
    git clone https://github.com/duhdugg/preaction-cms.git
    cd preaction-cms

  2. Install Node dependencies
    yarn

  3. Build the client
    yarn build

  4. Run the server
    yarn start

Administration

Administrative Scripts

copy the data/db.sqlite database to a timestamped and hashed file in the data/backups directory:
yarn backup-db

delete all sessions from the database:
yarn clear-sessions

generate a new randomly-generated password for the admin user:
yarn randomize-password

set password by prompt:
yarn set-password

the above will also accept whatever is piped into it, so this example is one way to generate and set a random password that is 8,192 alphanumeric characters in length:
dd if=/dev/urandom | strings -e s | sed 's/[^a-za-z0-9]//g' | tr -d '\n' | dd count=1 bs=8192 2> /dev/null | yarn set-password

create a gzipped tarball named preaction-cms.tar.gz containing data/db.sqlite, the build directory, the src/style directory, and any db-referenced files in the uploads directory:
yarn package

Development

Development Scripts

run the server with automatic reloading of server-side code:
yarn dev-server

apply prettier requirements to source:
yarn makeover

run tests:
yarn test

run server with nodejs inspection:
yarn start-inspect

run server with nodejs profiling:
yarn start-profile

start the react development server for automatic client-side code reloading:
yarn dev-client

Contributing

Pull requests are welcome at https://github.com/duhdugg/preaction-cms/pulls

How to Style

After running yarn build, yarn dev-client, or yarn init-client for the first time, a copy of src/style/custom.template.js will be created at src/style/custom.js. You can edit this file to import your own css or scss files.

By default, the file looks like this:

const ssr = typeof window === 'undefined'
if (!ssr) {
  // import("./custom.scss")
}

You can use dynamic imports to any css or scss file. To ensure that server-side rendering works, you should only import when the window global is not undefined.

If you would like to replace the default Bootstrap or Quill themes with your own version, you will need to edit src/style/base.js instead.

Once your edits have been made, rebuild the client with yarn build.

Path-Specific Styles

The body element will always have a class which reflects the current path. For example, navigating to /foobar/one/ will give the body element a class of path-foobar-one-. You can use this to create page-specific styles.

Example

You can use wildcard attribute selectors to apply styles to a path, and all of its subpaths. Let's say you want a grey background for all pages under /foobar/. This may include /foobar/, /foobar/one/, /foobar/one/alpha/, /foobar/two/, etc.

Your src/style/index.js file may look like this:

const ssr = typeof window === 'undefined'
if (!ssr) {
  import('./foobar-pages.scss')
}

In this example, we create a new file at src/style/foobar-pages.scss, which looks like this:

body[className*='path-foobar-'] {
  a {
    font-weight: bold;
  }
}

In the example above, every page under the /foobar/ path will have bold links.

Favicon

To add a favicon, you should put your icon files at public/icon/icon.svg and public/icon/icon.png. If both are present, the svg will be preferred by browsers which support it. You will need to run yarn build after updating these icon files.

Tracking Changes

You may wish to remove or modify src/style/.gitignore and/or public/icon/.gitignore in your fork or local branch after customizing the style or icon.

How to Extend

You can extend Preaction CMS in the following ways:

Server-Side Middleware Extensions

When starting the server, lib/ext scans the ext directory for other directories containing an index.js file. If such a file exists and also exports a middleware function, that will be called ahead of the default middleware in Preaction CMS. The middleware function can either be an express instance, or a function which takes the arguments: (req, res, next)

Client-Side Block Extensions

By default, Preaction CMS only supports 5 block types: Carousel, Content, Iframe, Navigation, and Spacer. You can add more block types by putting them in the blockExtensions object in src/ext/index.js.

A block extension must meet the following requirements in order to work correctly:

  • It must export a React component as an ES Module
  • The component should accept a preaction object prop
  • The component should have an attribute extensionType set to 'block' (note: this isn't enforced now, but will be in the future)
  • The component should have an attribute label set to a string that gives the block a name (note: this is used to populate the "Add Block" menu)
  • The component must have an attribute Settings which is another React component that should meet the following requirements:
    • The Settings component must accept a propsData object prop
    • The Settings component must accept a getPropsDataValueHandler function prop
    • The Settings component should also render inputs which use the propsData and getPropsDataValueHandler props to provide a UI for customizing your component.

Take a look at the following examples to see extensions that meet these requirements. These projects are good boilerplates to use for creating your own block extension. It is not strictly necessary to use a project like these to build your extensions, but it should help with providing an environment for quick development iterations.

Example

Let's say you've decided to use the "Social Badge" extension from the link above in your Preaction CMS instance. You've cloned the repo, installed its dependencies, and compiled the extension using the build script from the package. You should have a preactioncms-blockext-socialbadge.esm.js file and a preactioncms-blockext-socialbadge-settings.esm.js file. These are the files you need to import into your src/ext/index.js file, which should now look like this:

import SocialBadge from './preactioncms-blockext-socialbadge.esm.js'
import SocialBadgeSettings from './preactioncms-blockext-socialbadge-settings.esm.js'
const allowedReferrers = []
// Unless the extension is a single module with the Settings component already assigned,
// the settings component needs to be assigned to the extension component.
// The decision to separate your component from its settings counterpart makes
// more sense when you do code-splitting (see next example below).
SocialBadge.Settings = SocialBadgeSettings
const blockExtensions = { SocialBadge }
const menuExtensions = {}
export { allowedReferrers, blockExtensions, menuExtensions }

After your src/ext/index.js file is updated, run yarn build.

You should be mindful of how each client-side extension impacts the bundle size when running yarn build. You can run yarn analyze to visualize this impact. If you notice that modules from the node_modules directory of the extension are being packaged into your client bundle, try removing that node_modules directory from the extension's project folder after building it, and before running yarn build on Preaction CMS.

You may also wish to do code-splitting on your block extensions to keep your core bundle to a minimum, where the extension will be loaded as a separate JS file only when a page is rendered which uses it. In this case, you can use the React.lazy API as follows:

import React, { Suspense } from 'react'
import { Spinner } from '@preaction/bootstrap-clips'
const SocialBadge = React.lazy(() =>
  import('./preactioncms-blockext-socialbadge.esm.js')
)
const SocialBadgeSettings = React.lazy(() =>
  import('./preactioncms-blockext-socialbadge-settings.esm.js')
)
const allowedReferrers = []
const blockExtensions = {
  SocialBadge: function (props) {
    return (
      <Suspense fallback={<Spinner size={3.25} />}>
        <SocialBadge {...props} />
      </Suspense>
    )
  },
}
const blockExtensionSettings = {
  SocialBadgeSettings: function (props) {
    return (
      <Suspense fallback={<Spinner size={3.25} />}>
        <SocialBadgeSettings {...props} />
      </Suspense>
    )
  },
}
// note: this method requires that you also set the extensionType and label attributes here
// otherwise, this information will be blank until the component has loaded
blockExtensions.SocialBadge.extensionType = 'block'
blockExtensions.SocialBadge.label = 'Social Badges'
// the Settings component needs to be tied to the extension component
blockExtensions.SocialBadge.Settings =
  blockExtensionSettings.SocialBadgeSettings
const menuExtensions = {}
export { allowedReferrers, blockExtensions, menuExtensions }

Note: the above example also uses React to render a fallback (in this case, the Spinner component from @preaction/bootstrap-clips), although this is not required.

Client-Side Menu Extensions

You may also add menu extensions to Preaction CMS. A menu extension must meet the following requirements:

  • It exports a function which accepts the preaction object as its only argument.
  • The function returns an object which can be used to render a NavItem from the @preaction/bootstrap-clips library. It may also have an order number property which can be used to set the sorting priority of the menu item.

Take a look at the following example to see an extension that meets these requirements:

Example

Create the following menu extension at src/ext/menuext-example.js:

function example(preaction) {
  return {
    name:
      preaction.page && preaction.page.title
        ? `Search Wikipedia: ${preaction.page.title}`
        : 'Wikipedia',
    href: 'https://en.wikipedia.com',
    onClick: (event) => {
      if (preaction.page && preaction.page.title) {
        event.preventDefault()
        window.open(
          'https://en.wikipedia.org/w/index.php?search=' +
            encodeURIComponent(preaction.page.title),
          '_blank',
          'noopener,noreferrer'
        )
      }
    },
    order: 20,
  }
}

example.extensionType = 'menu'

export { example }

Note: This is a trivial example, but clicking on this menu item launches a Wikipedia search on the current page title.

To use this extension, your src/ext/index.js file should look like the following:

import { example } from './menuext-example.js'
const allowedReferrers = []
const blockExtensions = {}
const menuExtensions = { example }
export { allowedReferrers, blockExtensions, menuExtensions }

Tracking Changes

You may wish to alter or remove the src/.gitignore and/or src/ext/.gitignore files after adding extensions to your local Preaction CMS repo.

Running in Production

You should run Preaction CMS behind a reverse-proxy such as Nginx. See NGINX Configuration for configuration examples.

Take a look at the lib/env API documentation to understand which environment variables should be set when running in a production environment.

Running on Systemd

An example service file for systemd can be found in: examples/preaction-cms.service

Refer to your operating system's documentation for deploying this file.

Reminder
Documentation for Preaction CMS environment variables can be found here: https://duhdugg.github.io/preaction-cms/#libenv

NGINX Configuration

See the following file: examples/nginx.conf.

This file can be used as a starting template for your nginx config. Note that it does not get into all the specifics of Nginx. For that, you would need to consult the Nginx documentation, and/or your operating system's documentation for Nginx.

You may wish to run Preaction CMS within a subpath on your server. You should consult examples/nginx-nonempty-root.conf to see how to rewrite the routes needed. The client for Preaction CMS needs to be re-built if you are serving from a subpath, using the following to work with this example:
PUBLIC_URL=/cms yarn build

Check here for instructions on how to setup HTTPS using Certbot.

Server-Side API

Server-Side API
Static Members
lib/adminRequired(req, res, next)
lib/cache
lib/csrf
lib/db
lib/env
lib/ext
lib/icon()
lib/pages
lib/processImg(path)
lib/randomString(length)
lib/redirects
lib/renameUpload(path)
lib/render
lib/session
lib/settings
lib/slash
lib/ua
lib/uploads
lib/warm