该存储库包含 BCMS 的开源版本,这是一种现代无头 CMS,可让您以灵活的结构轻松管理内容。用户友好的界面、快速的部署选项 - 使其成为寻求可定制 CMS 解决方案的开发人员和团队的完美选择。
git clone https://github.com/bcms/cms
npm i
docker compose up
http://localhost:8080
拥有基于 Debian 的服务器后,您可以通过 SSH 连接到该服务器并按照以下步骤操作。
如果服务器上尚未安装依赖项,请安装它们:
sudo apt update && sudo apt install docker.io git nodejs npm
npm i -g n && n 20
由于我们使用的是 GitHub Packages,因此您需要将配置添加到~/.npmrc
来拉取包。添加以下两行:
//npm.pkg.github.com/:_authToken=<GITHUB_TOKEN>
@bcms:registry=https://npm.pkg.github.com
要生成GITHUB_TOKEN
,请按照本教程进行操作。此令牌所需的唯一权限是read:packages
。
npm i -g @bcms/selfhosted-cli
selfbcms --deploy debian
拥有基于 Debian 的服务器后,您可以通过 SSH 连接到该服务器并按照以下步骤操作。
sudo apt update && sudo apt install docker.io git
mkdir ~ /bcms
mkdir ~ /bcms/db ~ /bcms/uploads ~ /bcms/backups
git clone https://github.com/bcms/cms
docker build . -t my-bcms
docker network create -d bridge --subnet 10.20.0.0/16 --ip-range 10.20.30.0/24 --gateway 10.20.30.1 bcms-net
如果您没有 MongoDB,您可以在同一服务器上的 Docker 容器内运行它:
docker run -d --name my-bcms-db -v ~ /bcms/db:/data/db -e MONGO_INITDB_ROOT_USERNAME= < DB_ADMIN_USERNAME > -e MONGO_INITDB_ROOT_PASSWORD= < DB_ADMIN_PASSWORD > --network bcms-net mongo:7
通过此设置,MongoDB 数据库将存储在~/bcms/db
中,并可通过端口 27017 上的bcms-net
进行访问。
docker run -d --name my-bcms -v ~ /bcms/uploads:/app/backend/uploads -v ~ /bcms/backups:/app/backend/backups -e " DB_URL=<MONGODB_CONNECTION_URL> " --network bcms-net my-bcms
如果 MongoDB 设置在同一服务器上,则DB_URL
将为mongodb://<DB_ADMIN_USERNAME>:<DB_ADMIN_PASSWORD>@my-bcms-db:27017/admin
。
要处理传入请求,您需要设置 Nginx 反向代理。
# File location: ~/bcms/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768 ;
}
http {
sendfile on ;
tcp_nopush on ;
tcp_nodelay on ;
keepalive_timeout 65 ;
types_hash_max_size 2048 ;
server_tokens off ;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on ;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' blob: data:" ;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "no-referrer" ;
server {
listen 80 default_server ;
listen [::]:80 default_server ;
server_name _;
client_max_body_size 105G ;
location /api/v4/socket {
proxy_http_version 1.1 ;
proxy_set_header Upgrade $http_upgrade ;
proxy_set_header Connection "upgrade" ;
proxy_pass http://my-bcms:8080/api/v4/socket;
}
location /__plugin {
proxy_read_timeout 60 ;
proxy_connect_timeout 60 ;
proxy_send_timeout 60 ;
proxy_pass http://my-bcms:8080/__plugin;
}
location / {
proxy_read_timeout 60 ;
proxy_connect_timeout 60 ;
proxy_send_timeout 60 ;
proxy_pass http://my-bcms:8080;
}
}
}
此配置使用默认的 Nginx 虚拟主机。要使用自定义域,请根据需要调整配置。
# File location: ~/bcms/proxy.Dockerfile
FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf
docker build . -f proxy.Dockerfile -t my-bcms-proxy
docker run -d -p 80:80 --name my-bcms-proxy --network bcms-net my-bcms-proxy
我们欢迎贡献!
2024 年 9 月,我们做出了重大转变:BCMS Cloud 变为闭源(作为 BCMS Pro),开源版本变得完全独立。
我们为什么这样做?
当我们第一次构建 BCMS 时,我们将身份验证系统集中在 BCMS 云中。即使您是自托管,您仍然必须通过我们的系统登录。我们认为这会简化邀请用户、发送电子邮件和管理入职等工作。但后来 Reddit 上的人们却因此而拆散了我们。他们是对的。所以,我们听了。
另一个问题是保持 BCMS 保持最新状态。我们不断改进它,但确保您的自托管版本可以轻松更新始终是一个技术难题。
我们最初设置 BCMS 云的方式也产生了问题。每个实例都必须在自己独立的 VPS 上运行,这会减慢速度并使基础设施成本飙升。
我们的目标始终是创造快速、个性化的内容编辑体验。但试图同时保持开源和云版本的发展开始让人感觉不可持续。因此,我们呼吁将他们分开。开源社区现在完全满足了他们的要求:完全独立、可自托管、完全免费的 BCMS。
与此同时,我们将继续推进所有新功能 - BCMS Pro,现在是闭源的,从头开始重新开发,并针对那些需要优质托管体验的人进行了优化。
核心 BCMS 团队非常小 - 只有三名工程师、一名设计师、一名项目经理和一名内容作家。因此,我们将大部分精力集中在 BCMS Pro 上,但我们很高兴看到社区对开源版本的态度。
快乐编码!
如果您有任何疑问或需要帮助,请随时提出问题或通过@Discord 联系我们。
关注 X(推特)
在 LinkedIn 上关注
加入我们的 Discord
您可以通过 4 种主要方式来扩展 BCMS,即事件、功能、作业和插件。前 3 个仅用于扩展后端功能和执行自定义后端任务,而插件是在 BCMS 核心之上拥有自己的后端和 UI 的应用程序。
BCMS 事件是自定义 JavaScript 文件(JS 函数),当触发内部 BCMS 事件时执行,例如创建、更新或删除 Entry 时或创建、更新或删除 Widget 时。所有事件类型的列表
创建事件处理程序就像添加文件根/backend/events
目录一样简单。该目录位于 BCMS 容器内。您可以在运行容器时将其挂载为卷,如下所示: -v <path_to_my_events_dir>:/app/backend/events
。
如果您从此存储库本地运行 BCMS,则可以将事件文件添加到/backend/events/<my_event>.{js|ts}
。
这是一个简单的事件示例:
// Is used locally in /backend/events/test.js
const { createEvent } = require ( '@bcms/selfhosted-backend/event' ) ;
module . exports = createEvent ( async ( ) => {
return {
config : {
// Trigger Event handler only for
// Entry changes
scope : 'entry' ,
// Listen for all Event types:
// (create, update, delete)
type : 'all' ,
} ,
// This method will be executed when
// Event is triggered
async handler ( type , scope , data ) {
// Do something with the event
console . log ( { type , scope , data } ) ;
} ,
} ;
} ) ;
该文件将由 BCMS 后端加载并执行,这意味着它在相同的范围内运行,您可以使用Repo.*
等内部实用程序来访问数据库。该文件的执行不受限制,BCMS后端拥有的所有权限,您的Event也将拥有。这不是很危险吗?是的,也不是,权力越大,责任越大。这意味着您永远不应该在事件内运行不受信任的代码。
现在,如果您想导入一些自定义模块(例如@paralleldrive/cuid2)怎么办?您可以通过将其添加到/backend/custom-package.json
来做到这一点,该文件的结构应如下所示:
{
"dependencies" : {
// Your custom packages
"@paralleldrive/cuid2" : " ^2.2.2 "
}
}
自定义包将在 BCMS 后端启动时初始化。
还有一件事,如果您的事件、作业和函数之间有一些共享逻辑怎么办?我们正在调用这些附加文件,但您可以将它们视为您的自定义实用程序。在/backend/additional
中,您可以添加任何 JS 文件并将其导入到事件中。
与 BCMS 事件类似,BCMS 函数是 JavaScript 文件(JS 函数),在调用函数端点时执行: POST: /api/v4/function/<my_function_name>
。您可以将它们视为自定义代码,当向函数执行端点发出 HTTP 请求时,自定义代码将在 BCMS 后端运行。
创建函数就像将文件添加到/backend/functions
一样简单。该目录位于 BCMS 容器内。您可以在运行容器时将其安装为卷,如下所示: -v <path_to_my_functions_dir>:/app/backend/functions
。
如果您从此存储库本地运行 BCMS,则可以将函数文件添加到/backend/functions/<my_function>.{js|ts}
。
这是一个简单的示例函数,它将回显请求:
const { createFunction } = require ( '@bcms/selfhosted-backend/function' ) ;
module . exports = createFunction ( async ( ) => {
return {
config : {
name : 'echo' ,
} ,
async handler ( { request } ) {
return {
message : 'Function echo' ,
data : request . body ,
} ;
} ,
} ;
} ) ;
要成功调用此函数,您需要创建一个 Api 密钥(管理/密钥管理器)并允许其调用该函数。之后您可以创建一个 HTTP 请求:
POST http://localhost:8080/api/v4/function/echo
Content-Type: application/json
Authorization: ApiKey <key_id>.<key_secret>
{
"fromClient" : " Hey from client "
}
// Response
// {
// "success": true,
// "result": {
// "message": "Function echo",
// "data": {
// "fromClient": "Hey from client"
// }
// }
// }
该文件将由 BCMS 后端加载并执行,这意味着它在相同的范围内运行,您可以使用Repo.*
等内部实用程序来访问数据库。该文件的执行不受限制,BCMS后端拥有的所有权限,您的Function也将拥有。这不是很危险吗?是的,也不是,权力越大,责任越大。这意味着您永远不应该在函数内运行不受信任的代码。
现在,如果您想导入一些自定义模块(例如@paralleldrive/cuid2)怎么办?您可以通过将其添加到/backend/custom-package.json
来做到这一点,该文件的结构应如下所示:
{
"dependencies" : {
// Your custom packages
"@paralleldrive/cuid2" : " ^2.2.2 "
}
}
自定义包将在 BCMS 后端启动时初始化。
还有一件事,如果您的事件、作业和函数之间有一些共享逻辑怎么办?我们正在调用这些附加文件,但您可以将它们视为您的自定义实用程序。在/backend/additional
中,您可以添加任何 JS 文件并将其导入到 Function 中。
与 BCMS 事件和函数类似,BCMS 作业是在指定 CRON 间隔内执行的 JavaScript 文件(JS 函数)。您可以将它们视为自定义代码,它将按指定的时间间隔在 BCMS 后端运行。
创建作业就像将文件添加到/backend/jobs
一样简单。该目录位于 BCMS 容器内。您可以在运行容器时将其挂载为卷,如下所示: -v <path_to_my_jobs_dir>:/app/backend/jobs
。
如果您从此存储库本地运行 BCMS,则可以将作业文件添加到/backend/jobs/<my_job>.{js|ts}
。
这是一个简单的示例函数,它将每分钟控制台记录一次当前时间:
const { createJob } = require ( '@bcms/selfhosted-backend/job' ) ;
module . exports = createJob ( async ( ) => {
return {
cronTime : '* * * * *' , // You can use: https://crontab.guru/
async handler ( ) {
console . log ( new Date ( ) . toISOString ( ) ) ;
} ,
} ;
} ) ;
该文件将由 BCMS 后端加载并执行,这意味着它在相同的范围内运行,您可以使用Repo.*
等内部实用程序来访问数据库。该文件的执行不受限制,BCMS后端拥有的所有权限,您的作业也将拥有。这不是很危险吗?是的,也不是,权力越大,责任越大。这意味着您永远不应该在作业中运行不受信任的代码。
现在,如果您想导入一些自定义模块(例如@paralleldrive/cuid2)怎么办?您可以通过将其添加到/backend/custom-package.json
来做到这一点,该文件的结构应如下所示:
{
"dependencies" : {
// Your custom packages
"@paralleldrive/cuid2" : " ^2.2.2 "
}
}
自定义包将在 BCMS 后端启动时初始化。
还有一件事,如果您的事件、作业和函数之间有一些共享逻辑怎么办?我们正在调用这些附加文件,但您可以将它们视为您的自定义实用程序。在/backend/additional
中,您可以添加任何 JS 文件并将其导入作业中。
您可以将 BCMS 插件视为具有自己的后端和前端的应用程序,该应用程序由 BCMS 后端提供服务,并且可以访问所有后端和前端功能。对于插件后端,您需要遵循一些模式,但对于插件 UI,您可以构建任何 SPA 应用程序(您可以使用 React 或 VanillaJS,但我们建议使用 VueJS,因为您将能够使用 BCMS UI 组件)。
解释这一点的最佳方法是给您一个简单的例子。这将是插件的结构:
/backend/plugins
- /hello-world
- /_ui
- index.html
- config.json
- controller.js
- main.js
为了使这个示例尽可能简单,整个前端代码将包含在_ui/index.html
中,而后端将包含 1 个用于迎接用户的控制器。
配置.json
{
"version" : " 1 " ,
"dependencies" : {}
}
控制器.js
const { createPluginController } = require ( '@bcms/selfhosted-backend/plugin' ) ;
const { createControllerMethod } = require ( '@bcms/selfhosted-backend/_server' ) ;
const {
RP ,
} = require ( '@bcms/selfhosted-backend/security/route-protection/main' ) ;
exports . HelloWorldController = createPluginController ( {
name : 'NameGreet' ,
path : '/greet' ,
methods ( ) {
return {
greet : createControllerMethod ( {
path : '/:name' ,
type : 'get' ,
preRequestHandler : RP . createJwtCheck ( ) ,
async handler ( { request } ) {
const params = request . params ;
return {
greet : `Hello ${ params . name } !` ,
} ;
} ,
} ) ,
} ;
} ,
} ) ;
main.js
const { createPlugin } = require ( '@bcms/selfhosted-backend/plugin' ) ;
const { HelloWorldController } = require ( './controller' ) ;
module . exports = createPlugin ( async ( ) => {
return {
id : 'hello-world' ,
name : 'Hello World' ,
controllers : [ HelloWorldController ] ,
middleware : [ ] ,
async policy ( ) {
return [
{
name : 'Full access' ,
} ,
] ;
} ,
} ;
} ) ;
_ui/index.html
<!DOCTYPE html >
< html lang =" en " >
< head >
< meta charset =" UTF-8 " />
< title > Hello world </ title >
< style >
body {
color: white;
}
</ style >
</ head >
< body >
< h1 > Hello world </ h1 >
< div >
< input id =" greet-input " placeholder =" Greet " type =" text " />
< button onclick =" greet(this) " > Send </ button >
< div id =" greet-result " > </ div >
</ div >
< h2 > List of templates </ h2 >
< div id =" templates " > </ div >
< script >
async function onLoad ( ) {
const templates =
await window . parent . bcms . sdk . template . getAll ( ) ;
const el = document . getElementById ( 'templates' ) ;
if ( el ) {
el . innerHTML = `<pre> ${ JSON . stringify (
templates ,
null ,
4 ,
) } </pre>` ;
}
window . removeEventListener ( 'load' , onLoad ) ;
}
window . addEventListener ( 'load' , onLoad ) ;
async function greet ( ) {
const inputEl = document . getElementById ( 'greet-input' ) ;
const value = inputEl . value ;
const result = await window . parent . bcms . sdk . send ( {
url : `/api/v4/plugin/hello-world/greet/ ${ value } ` ,
} ) ;
const el = document . getElementById ( 'greet-result' ) ;
el . innerHTML = `<pre> ${ JSON . stringify ( result , null , 4 ) } </pre>` ;
}
</ script >
</ body >
</ html >
TODO:更详细地解释插件并创建示例插件