CPlusPatch CMS

A fast, sleek and beautiful website + full content management system developed with NuxtJS.

It is designed to be minimal but efficient, secure and lightweight.

CPlusPatch CMS is a customizable system for making blogs and personal pages with a dynamic Vue-based templating engine. This is a project in beta, and as such should be treated as unstable.

## Requirements

  • A system that can run Node.js (Linux is HEAVILY recommended), or Docker on Linux
  • An S3 or S3 compatible bucket (for example, Cloudflare R2)

Downloading and running

You may use the docker-compose.yml file included in this repository to bootstrap a working environment very fast.

Alternatively, download and install manually:

git clone https://codeberg.org/CPlusPatch/web-business.git
cd web-business
pnpm install

Then, fill out .env with credentials for the S3 or S3-compatible bucket (such as Cloudflare R2, which I am using) and for PostgreSQL.

cp .env.example .env

For the CDN URL, do not include a trailing slash at the end.

Building manually

pnpm build # Bundles project into JS files

# EITHER
pnpm preview # For testing
pm2 start ecosystem.config.js # For production deployment with PM2

Administration

Adding new users (for now)

NOTE: These instructions are temporary, an admin panel for adding users is coming soon

You may use the following JS scripts to generate a hashed password with Node’s crypto library:

import { randomBytes, pbkdf2 } from "crypto";

export function createSalt(length = 128) {
	return randomBytes(length).toString("base64");
}

export function createPasswordHash(
	password,
	salt = "",
	iterations = 10000
) {
	return new Promise((resolve, reject) => {
		pbkdf2(password, salt, iterations, 100, "sha256", (err, derivedKey) => {
			if (err) return reject(err);
			return resolve(`${derivedKey.toString("hex")}:${salt}`);
		});
	});
}

To use it, run createSalt to generate a random salt, then run createPasswordHash("your password here", salt) to generate a new salted password.

Passwords are stored as hash:salt in the database, and createPasswordHash outputs this format.

To continue, you will need some kind of PostgreSQL browser and a working instance of the web app: more administration tools are coming soon.

Open to the user table, and insert the following row:

┌────────────┬───────────────────┬───────────────┬──────────────────────┐
│  username  │   display_name    │ oauthAccounts │       password       │
├────────────┼───────────────────┼───────────────┼──────────────────────┤
│ A username │ New display name  │ []            │ <password hash here> │
└────────────┴───────────────────┴───────────────┴──────────────────────┘

All the other fields may be left blank or will be generated automatically. If the user needs to be an admin, that is a creator of posts, insert this row instead:

┌────────────┬───────────────────┬───────────────┬──────────────────────┬───────┐
│  username  │   display_name    │ oauthAccounts │       password       │ role  │
├────────────┼───────────────────┼───────────────┼──────────────────────┼───────┤
│ A username │ New display name  │ []            │ <password hash here> │ admin │
└────────────┴───────────────────┴───────────────┴──────────────────────┴───────┘

You can save and login with your new credentials now.

Scripts for development

Images

Convert all JPEG images to WebP (uses ImageMagick)

magick mogrify -format webp *.jpeg

Convert all JPEG images to WebP (uses ImageMagick)

Resize all images to 250×250 (when adding languages to /public/static/languages/)

mogrify -path . -auto-orient -thumbnail 250x *.png

Writing custom templates

To add custom templates to the CMS block system, open the templates/ directory, then create two files inside a category folder: TemplateName.vue and TemplateName.json. You may also create new folders, the location doesn’t matter as long as it’s inside templates/.

Inside the .vue file you may write a new Vue component. You will need to add this block of code:

<script setup lang="ts">
defineProps<{
	editable: boolean;
	// You may add arbitrary props here
}>();
</script>

You may use TypeScript and all classes provided by https://unocss.dev/, as well as NuxtJS 3.6 utilities and any package that is included in package.json (such as nuxt-img).

Here is an example for a big hero header:

<script setup lang="ts">
defineProps<{
	imageMain?: string;
	editable: boolean;
}>();
</script>

<template>
	<!-- Main hero -->
	<div class="relative px-4 mx-auto max-w-7xl w-full sm:px-6">
		<nuxt-img
			preload
			:width="1920"
			:height="1080"
			sizes="lg:1920px md:700px sm:400px"
			class="rounded-lg aspect-video shadow-lg w-full duration-150"
			:src="
				imageMain === '' ? 'https://placehold.co/1920x1080' : imageMain
			"
			alt="VSCode screenshot" />
	</div>
</template>

It is recommended that you look at other files inside templates to get a hang of the syntax.

Syntax for the metadata

Once your component is written, you may add your template metadata to TemplateName.json, like this:

{
	"category": "heroes",
	"name": "BigHero",
	"displayName": "Big Banner",
	"description": "Big banner for showing off an image",
	"inputs": {
		"image-main": "string"
	},
	"defaults": {
		"image-main": ""
	}
}

This will allow you to auto-generate the configuration panel for your component.

The syntax for inputs goes as follow:

Input types

"inputs": {
	"header-image": "image",
	"large-text": "paragraph",
	"text-input": "string",
	"invert-order": "boolean",
}

Arrays

Arrays can be declared with an array of a single object, which will be the type of all objects in the array. You can nest arrays inside arrays at will.

"inputs": {
	"grid": [
		{
			"id": "string",
			"tags": [
				{
					"id": "string",
					"name": "string",
					"color": "string",
					"text-color": "string"
				}
			],
			"description": "string",
			"name": "string",
			"href": "string"
		}
	]
},

Note: These input attributes will be passed to the Vue component as props, so their name will be converted to camelCase (e.g. image-main will be converted to imageMain)

Attributions

I will add more attributions when I find the image sources (mostly logos)

Evan You, https://github.com/yyx990803, CC BY 4.0 https://creativecommons.org/licenses/by/4.0, via Wikimedia Commons, for the VueJS logo

Internals

Data is stored inside a PostgreSQL database.

When uploading images, they are converted to WebP inside the browser itself before upload.


Licensed under GPLv3. Made with ?

GitHub

View Github