5.0.0-alpha.16
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
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.
Clone the repository and "cd" into it
git clone https://github.com/duhdugg/preaction-cms.git
cd preaction-cms
Install Node dependencies
yarn
Build the client
yarn build
Run the server
yarn start
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
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
Pull requests are welcome at https://github.com/duhdugg/preaction-cms/pulls
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
.
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.
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.
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.
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.
You can extend Preaction CMS in the following ways:
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)
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:
preaction
object propextensionType
set to 'block'
(note: this isn't enforced now, but will be in the future)label
set to a string that gives the block a name (note: this is used to populate the "Add Block" menu)Settings
which is another React component that should meet the following requirements:
propsData
object propgetPropsDataValueHandler
function proppropsData
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.
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.
You may also add menu extensions to Preaction CMS. A menu extension must meet the following requirements:
preaction
object as its only argument.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:
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 }
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.
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.
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
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.
this is an Express middleware function used to protect routes
env.readOnly
must be false and the session must be an authorized admin.
Otherwise a 403 forbidden error response will be sent.
(express.Request)
(express.Response)
(func)
this clears all items in the store
this is the Express middleware used on cached routes
(express.Request)
(express.Response)
(func)
CSRF protection middleware
(Express.Request)
(Express.Response)
(func)
Type: express
the main database at db/data.sqlite
Type: sequelize
the authorization database at data/auth.sqlite, which houses data for sessions and users
Type: sequelize
more sensitive parts of the express application are configured via runtime environment variables
enables the middleware in cache.js,
true
by default,
set with lack of environment variable: CMS_CACHE_DISABLE
Type: boolean
CMS_CACHE_DISABLE=1 npm start
this sets the name of the session cookie.
defaults to @preaction/cms:session
Type: string
CMS_COOKIE_NAME='session' npm start
trust the reverse proxy (via the "X-Forwarded-Proto" header)
for setting secure cookies.
must be true
if cookieSecure is true
and
running behind a reverse proxy like nginx (recommended for production).
Set with the CMS_COOKIE_PROXY
environment variable
Type: boolean
CMS_COOKIE_PROXY=1 npm start
sets the SAMESITE flag on cookies.
secure
is the ideal setting for production deployments,
and also requires cookieSecure
to be true
.
Set with CMS_COOKIE_SAMESITE environment variable
Type: boolean
CMS_COOKIE_SAMESITE='secure' CMS_COOKIE_SECURE=1 npm start
for signing cookies, defaults to a randomly generated string
which changes every start.
set with CMS_COOKIE_SECRET
environment variable
Type: string
CMS_COOKIE_SECRET="$(dd if=/dev/random status=none | strings | sed 's/[^a-zA-Z0-9]//g' | tr -d '\n' | dd bs=1 count=43 status=none)" npm start
sets the SECURE flag on cookies,
false
by default,
set with CMS_COOKIE_SECURE
environment variable
Type: boolean
CMS_COOKIE_SECURE=1 npm start
enables automatic database backups, defaults to false
,
set with CMS_DB_BACKUP
environment variable
Type: boolean
CMS_DB_BACKUP=1 npm start
enables sequelize logging to output raw SQL commands,
set with CMS_DB_LOGGING
environment variable
Type: boolean
CMS_DB_LOGGING=1 npm start
set tracking ID to enable Google Analytics
CMS_GOOGLE_ANALYTICS='UA-FOOBAR-1' npm start
set with NODE_ENV
environment variable
NODE_ENV='production' CMS_COOKIE_SAMESITE='secure' CMS_COOKIE_SECURE=1 npm start
the TCP port the express application server should listen on.
defaults to 8999
.
set with the CMS_PORT
environment variable
Type: number
CMS_PORT=8080 npm start
disables all admin-required middleware
so that no edits can be made to the database.
Defaults to false
.
Set with the CMS_READONLY
environment variable
Type: boolean
CMS_READONLY=1 npm start
allows removing EXIF data from uploads.
Defaults to true
.
Set with the CMS_DO_NOT_REMOVE_EXIF
environment variable
Type: boolean
CMS_DO_NOT_REMOVE_EXIF=1 npm start
set a path such as '/preaction-cms' to put
all routing behind a specific subpath on your nginx reverse proxy.
defaults to ''
.
Set with the CMS_ROOT
environment variable.
Type: string
CMS_ROOT='/preaction' npm start
enables the /sitemap.xml path
and configures the protocol and domain.
Set with the CMS_SITEMAP_HOSTNAME
environment variable.
Type: string
CMS_SITEMAP_HOSTNAME='https://example.com' npm start
This module will look in the ext
directory for directories
containing an index.js
file which exports a named middleware
function.
Once found, it will push each of these functions to the end of the
middleware Array
. This will allow extension middleware to override most
of the default routes.
Type: Array<func>
table definitions
Type: Object
Type: lib/pages.model/PageBlock
Type: express
create a new page block
(express.Request)
Name | Description |
---|---|
req.params Object
|
|
req.params._id number
|
|
req.body Object
|
as JSON |
req.body.blockType string
|
|
req.body.settings Object
|
(express.Response)
Object
:
block
creates a new content item for a block
(express.Request)
Name | Description |
---|---|
req.params Object
|
|
req.params._id number
|
|
req.body Object
|
as JSON |
req.body.contentType string
|
(express.Response)
Object
:
content
creates a page
(express.Request)
Name | Description |
---|---|
req.params Object
|
|
req.params._id number
|
|
req.body Object
|
as JSON |
req.body.pageType string
|
|
req.body.key string
|
|
req.body.title string
|
|
req.body.parentId number
|
|
req.body.settings Object
|
(express.Response)
Object
:
page
update a pageBlock by its id
(express.Request)
Name | Description |
---|---|
req.params Object
|
|
req.params._id number
|
|
req.body Object
|
as JSON |
req.body.ordering number
|
|
req.body.settings Object
|
(express.Response)
Object
:
pageBlock
depth should be specified in most cases to improve performance.
(number)
(number)
if depth is 1, no child pages will be populated in the tree
if depth is 2, only direct descendents will be populated in the tree
if depth is 3, it will include grandchildren
if depth is less than 1 (default), no limit is applied to child page depth
(boolean
= true
)
if true, the header, footer, and hero pages will be excluded from the tree
Promise
:
Object
"full" page, meaning blocks, content,
tree, and sitemap included
JSON CRUD API
Type: express
middleware to handle matched redirects with a 302 response
(express.Request)
(express.Response)
(func)
render the client (public/index.html), replacing placeholder text with env variables and metadata, where metadata is defined by settings in db, combined with options passed in the third argument
(express.Request)
(express.Response)
(Object
= {}
)
Name | Description |
---|---|
options.siteTitle string
|
|
options.pageTitle string
|
|
options.page Object
|
full page, not required |
options.page.settings Object
|
|
options.page.settings.headerPath string
|
will defer to
options.page.fallbackSettings.headerPath
if
undefined
|
options.page.settings.footerPath string
|
will defer to
options.page.fallbackSettings.footerPath
if
undefined
|
options.page.settings.heroPath string
|
will defer to
options.page.fallbackSettings.heroPath
if
undefined
|
options.page.fallbackSettings Object
|
|
options.page.fallbackSettings.headerPath string
|
will defer to
'/home/header/'
if
undefined
|
options.page.fallbackSettings.footerPath string
|
will defer to
'/home/footer/'
if
undefined
|
options.page.fallbackSettings.heroPath string
|
will defer to
'/home/hero/'
if
undefined
|
middleware function to require that the session is authenticated before proceeding to the next middleware function. Otherwise, a 401 unauthorized response error is returned
(express.Request)
(express.Response)
(func)
handles login, session, and token API
Type: express
handles logout requests to de-authenticate the session
(express.Request)
(express.Response)
(func)
boolean
:
true
(express.Request)
(express.Response)
(func)
string
:
CSRF token
renders the application (which should route to login page)
string
:
text/html
handles login requests to authenticate the session using bcrypt to compare the password
(express.Request)
Name | Description |
---|---|
req.body Object
|
as JSON |
req.body.username string
|
|
req.body.password string
|
(express.Response)
(func)
Object
:
req.session
and status = 200 if valid, otherwise
{}
and 401
Type: express-session
defaults ensure that the application will not fail due to missing settings
Type: Object
{
bodyTheme: '',
bodyGradient: false,
footerPath: '/home/footer/',
headerPath: '/home/header/',
heroPath: '/home/hero/',
init: true, // used to determine that the frontend has loaded settings from server
isNavParent: false, // not used directly. inherited and overridden by pages
headerTheme: '',
headerGradient: false,
heroTheme: '',
heroGradient: false,
mainTheme: '',
mainGradient: false,
footerTheme: '',
footerGradient: false,
maxWidthFooterContainer: false,
maxWidthHeaderContainer: false,
maxWidthHeroContainer: false,
maxWidthMainContainer: false,
maxWidthNav: false,
metaDescription: '',
navbarTheme: 'dark',
navActiveSubmenuTheme: 'primary',
navActiveTabTheme: 'white',
navAlignment: 'left',
navCollapsible: true,
navPosition: 'fixed-top',
navSpacing: 'normal',
navType: 'basic',
absoluteNavBehavior: 'same-window',
showFooter: true,
showHeader: true,
showHero: true,
heroPosition: 'above-header',
siteTitle: 'Preaction CMS',
}
Type: express
handles non-trailing slash redirects
(express.Request)
(express.Response)
(func)
looks for Trident in User-Agent header
(express.Request)
(express.Response)
(func)
string
:
text/html for those pesky IE users
Type: express