├── .gitignore ├── LICENSE ├── README.md ├── config-debug ├── config-release ├── debug.js ├── package.json ├── packages.js ├── release.js └── views ├── layouts ├── default.html ├── default.html.js └── default.html.json ├── pages ├── test.html ├── test.html.js └── test.html.json └── widgets ├── image.html ├── image.html.js ├── image.html.json ├── text.html ├── text.html.js └── text.html.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # temp folder 30 | tmp 31 | 32 | # databases folder 33 | databases 34 | 35 | # vscode project 36 | .vscode/* 37 | 38 | # downloaded packages 39 | packages 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Enterprise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![MIT License][license-image] 3 | 4 | # Full-featured nodejs CMS 5 | 6 | ## Installation 7 | 8 | 1. install and run [mongodb](https://www.mongodb.org/downloads) 9 | 2. install graphicsmagick `brew install ghostscript` and `brew install graphicsmagick`, or [graphicsmagick on windows](http://www.graphicsmagick.org/download.html#download-sites) 10 | 3. download, unzip cms 11 | 4. cd inside folder, then `npm install` to download modules 12 | 5. run cms with `node debug.js` ,or `node release.js` in production 13 | 14 | 15 | ## Requirements 16 | - nodejs >= 4.0.0 17 | - running mongodb > 2 18 | 19 | ## Next Steps 20 | 1. goto http://localhost:8080/cmsadmin 21 | 2. click "sign up" and enter admin email/pass - this will create first admin user 22 | 3. log in 23 | 4. check config-debug, config-release to setup mailer, or app name (it is used as database name, if there is no database specified) 24 | 5. enjoy + check website [nodee.io](https://nodee.io) for more information and documentation 25 | 26 | ![nodee CMS concept](https://nodee.io/images/page1_jpg.jpg) 27 | 28 | [Learn more here](https://nodee.io/docs/cms/concept) 29 | 30 | # Nodee CMS – the Concept 31 | 32 | Nodee CMS was born to handle almost any data viewing and the editing scenario. Yes, many good content management systems can do this, but it often means developing new plugins, data structures, types of data editors in the administration area, etc… So, here comes new CMS concept, build with modern technologies for modern websites. 33 | 34 | ## Always see what you are editing 35 | Editing content has to be easy for all content managers. Therefore, onsite (or inline) editing is like a must. We don’t want to go to content pages lists or menu settings forms located somewhere in the admin area, dedicated from the page design. We want to see changes immediately. 36 | 37 | ## Widgets everywhere 38 | Every template is a widget. Some are used like layouts, some like pages, but can be reused like widgets. If inside template HTML is defined widgets container like `
`, widgets can be added by content managers. Or using `` as fixed page partials. Every widget can have attributes. Attributes represent widget settings and can be edited by content managers. 39 | 40 | ## Templates – “2-way data binding” 41 | Same way as data are rendered, they are parsed and stored. Entire flow has 3 steps: render HTML --> manipulate HTML by content manager (send it back) --> parse, extract data and store in database. 42 | 1. Rendering - Let’s take a look at rendering closer (controller --> mapping --> html): 43 | - Widget (or template) controller – can manipulate model (model is by default CmsDocument), or directly send response 44 | - Data mapping – easy mapping via CSS selectors defines how data have to be rendered or injected to HTML template 45 | - HTML template – plain html, only very few special html tag are used (e.g. layout, widget, ...) 46 | 47 | 2. Editing content: ok, now we have rendered HTML page. So we can go to the content administration area and apply all widget editors on this HTML. As a result, we can change texts, images, lists, etc… Editors are defined by CSS selectors, and, of course, can be extended. 48 | 49 | 3. Parsing and Storing data (mapping --> parser --> storing): 50 | - Mapping - thanks to mapping and HTML separation, we can parse data back (by default, same mapping for rendering and parsing is used, but you can different if parse mapping is defined) 51 | - Parser – parser is like controller, it can modify data before storing, but cannot change response 52 | - Storing – after successful parsing, data from “content” property of result object is stored as content of cms document 53 | 54 | ## Front-end developers love it 55 | We don’t like auto-generated components like forms, menus, or carousels, because every front-end is unique, has own styles, layouts, javascript, etc... We want freedom when choosing what will be component looks like, what javascript framework will be used, and how will be data loaded (AJAX, or rendered on the server). Instead of build your own widgets directly in the administration area, and reuse them. 56 | 57 | ## Strong REST API 58 | Of course, API is very useful when we need to migrate content, run some scheduled tasks, or do something from outside. Every user has auto-generated API key. If you want to activate it, just define IPs from which requests are allowed, or simple type “*” to allow any IP. Then you can request all cms resources used by admin area path (/admin/…), of course, have to be allowed by the role of a user. 59 | 60 | ## Forms like you always wish 61 | Forms are something like simple data models. Instead of generating HTML forms, they are publishing API, which can be used as rest endpoint when calling from AJAX (e.g. POST JSON /cmsforms/formId), in old fashioned HTML form way with query parameters or hidden inputs, or from internal API as CmsForm model. If you want pair multiple forms data on some key, just decide which property will be pairing key. You can ask visitors questions in multiple steps, or later, and send different emails. Sending emails can be triggered when some form property is defined or updated, etc… 62 | 63 | ## Customize anything – thanks to total.js 64 | Total.js is full-featured, well designed, and very fast nodejs framework. You can extend or replace anything: serving static files, generating e-tags, authentication, authorization, controller behaviors, etc… It brings nice modularity approach. You can use modules, or whole packages to extend cms functionality. 65 | 66 | ## Business ready 67 | NodeJS is a serious platform. It really can replace some technologies used today. So, why stop with something like basic cms. Let’s make something ready for real business needs. 68 | Planned features: 69 | - Content editors roles, permissions, and workflow 70 | - Content versioning 71 | - Member areas (define non-public areas on website, which are enabled only for registered members) 72 | - Full-text search - sync with Elastic 73 | 74 | 75 | [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat 76 | [license-url]: license.txt 77 | -------------------------------------------------------------------------------- /config-debug: -------------------------------------------------------------------------------- 1 | name : nodee-cms 2 | version : 1.0.0 3 | 4 | default-ip : 127.0.0.1 5 | default-port : 8080 6 | 7 | // static files max-age set to 2 minutes 8 | default-response-maxage : 120 9 | 10 | // session cookie name, expiration in hours 11 | session-cookie-name : __user 12 | session-cookie-secure : false 13 | session-cookie-httpOnly : true 14 | session-cookie-expires : 24 15 | 16 | // auth settings 17 | // auth-datasource-database : nodee-cms-db 18 | auth-datasource-collection : users 19 | auth-mailer-use : mailer-primary 20 | auth-mailer-subject : Password changed 21 | auth-mailer-forgotpass : emails/forgotpass 22 | 23 | // main datasource setings (if database missing app name will be used) 24 | // datasource-primary-host : localhost 25 | // datasource-primary-database : nodee-cms-db 26 | // datasource-primary-username : dbuser 27 | // datasource-primary-password : dbpass 28 | 29 | // primary mailer conf 30 | mailer-primary-from : your@email.com 31 | mailer-primary-name : Demo 32 | mailer-primary-host : smtp.gmail.com 33 | mailer-primary-port : 465 34 | mailer-primary-secure : true 35 | mailer-primary-user : your@email.com 36 | mailer-primary-password : supersecretpassword 37 | 38 | // admin base path, "/admin/" by default 39 | admin-base-path : /cmsadmin -------------------------------------------------------------------------------- /config-release: -------------------------------------------------------------------------------- 1 | name : nodee-cms 2 | version : 1.0.0 3 | 4 | default-ip : 127.0.0.1 5 | default-port : 8080 6 | 7 | // static files max-age set to 2 minutes 8 | default-response-maxage : 120 9 | 10 | // session cookie name, expiration in hours 11 | session-cookie-name : __user 12 | session-cookie-secure : true 13 | session-cookie-httpOnly : true 14 | session-cookie-expires : 24 15 | 16 | // auth settings 17 | // auth-datasource-database : nodee-cms-db 18 | auth-datasource-collection : users 19 | auth-mailer-use : mailer-primary 20 | auth-mailer-subject : Password changed 21 | auth-mailer-forgotpass : emails/forgotpass 22 | 23 | // main datasource setings (if database missing app name will be used) 24 | // datasource-primary-host : localhost 25 | // datasource-primary-database : nodee-cms-db 26 | // datasource-primary-username : dbuser 27 | // datasource-primary-password : dbpass 28 | 29 | // primary mailer conf 30 | mailer-primary-from : your@email.com 31 | mailer-primary-name : Demo 32 | mailer-primary-host : smtp.gmail.com 33 | mailer-primary-port : 465 34 | mailer-primary-secure : true 35 | mailer-primary-user : your@email.com 36 | mailer-primary-password : supersecretpassword 37 | 38 | // admin base path, "/admin/" by default 39 | admin-base-path : /cmsadmin -------------------------------------------------------------------------------- /debug.js: -------------------------------------------------------------------------------- 1 | var packages = [ 2 | 'nodee-total', 3 | 'nodee-admin', 4 | 'nodee-cms' 5 | ]; 6 | 7 | /* 8 | * Debug configs 9 | */ 10 | 11 | var childProcessDebuggerPort = 5859, 12 | watchDirectories = [ '/controllers', '/definitions', '/modules', '/resources', '/components', '/models', '/source' ], 13 | watchExtensions = ['.js', '.resource', '.html'], 14 | watchFiles = ['config', 'config-debug', 'config-release', 'versions']; 15 | 16 | /* 17 | * DEBUG setup: 18 | */ 19 | var isDebugging = process.argv[process.argv.length - 1] === 'debugging'; 20 | var directory = process.cwd(); 21 | var path = require('path'); 22 | var fs = require('fs'); 23 | 24 | // download all packages and start app 25 | if(!isDebugging) require('./packages.js').downloadPackages(packages, function(){ 26 | require('total.js'); // init total.js 27 | run(); // run app in debug mode with file watchers 28 | console.warn('DOWNLOADING'); 29 | }); 30 | else { 31 | require('total.js'); // init total.js 32 | run(); // run app in debug mode with file watchers 33 | } 34 | 35 | function debug() { 36 | var options = {}; 37 | var framework = require('total.js'); 38 | var port = parseInt(process.argv[2]); 39 | if (options.https) return framework.https('debug', options); 40 | framework.http('debug', options); 41 | } 42 | 43 | function app() { 44 | var fork = require('child_process').fork; 45 | var directories = []; 46 | var files = {}; 47 | var force = false; 48 | var changes = []; 49 | var app = null; 50 | var status = 0; 51 | var async = new utils.Async; 52 | var pid = ''; 53 | var pidInterval = null; 54 | var prefix = '------------> '; 55 | var isLoaded = false; 56 | 57 | for(var i=0;i 0) { 181 | console.log(prefix + 'PID: ' + process.pid); 182 | pid = path.join(directory, 'debug.pid'); 183 | fs.writeFileSync(pid, process.pid); 184 | pidInterval = setInterval(function() { 185 | fs.exists(pid, function(exist) { 186 | if (exist) return; 187 | fs.unlink(pid, noop); 188 | if (app !== null) process.kill(app.pid); 189 | process.exit(0); 190 | }) 191 | }, 2e3); 192 | } 193 | restart(); 194 | refresh_directory(); 195 | } 196 | 197 | function run(){ 198 | if(isDebugging) return debug(); 199 | var filename = path.join(directory, 'debug.pid'); 200 | if(!fs.existsSync(filename)) return app(); 201 | fs.unlinkSync(filename); 202 | setTimeout(app, 3e3); 203 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodee-cms", 3 | "preferGlobal": false, 4 | "version": "1.0.1", 5 | "author": "NODEE CMS - Matus Szabo ", 6 | "description": "cms - content management system", 7 | "dependencies":{ 8 | "total.js":"~2.7.0", 9 | "uglify-js":"~3.0.24", 10 | "nodee-utils":"*", 11 | "nodee-model":"*", 12 | "nodee-view":"*" 13 | }, 14 | "analyze": false, 15 | "devDependencies": { }, 16 | "bundledDependencies":[], 17 | "license": "MIT", 18 | "engines": { 19 | "node": ">=4.0.0" 20 | } 21 | } -------------------------------------------------------------------------------- /packages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'), 4 | https = require('https'), 5 | nodeUrl = require('url'), 6 | fs = require('fs'); 7 | 8 | // clear packages folder 9 | deleteFolderRecursiveSync('packages'); 10 | 11 | // create packages folder 12 | fs.mkdirSync('packages'); 13 | 14 | module.exports.downloadPackages = downloadPackages; 15 | module.exports.downloadFile = downloadFile; 16 | module.exports.deleteFolderRecursiveSync = deleteFolderRecursiveSync; 17 | 18 | 19 | function downloadPackages(packages, cb, i){ 20 | i = i || 0; 21 | 22 | var baseUrl = 'https://packages.nodee.io/' +(packages.accessKey ? packages.accessKey+'/' : '')+ 'latest/'; 23 | var fileStream = fs.createWriteStream('packages/' + packages[i] + '.package'); 24 | var url = baseUrl + packages[i] + '.package'; 25 | 26 | downloadFile(url, fileStream, function(err){ 27 | if(err) throw err; 28 | 29 | var fileStream = fs.createWriteStream('packages/' + packages[i] + '.package.json'); 30 | var url = baseUrl + packages[i] + '.package.json'; 31 | downloadFile(url, fileStream, function(err){ 32 | if(err) throw err; 33 | i++; 34 | if(i < packages.length) downloadPackages(packages, cb, i); 35 | else cb(); 36 | }); 37 | }); 38 | } 39 | 40 | function downloadFile(url, fileStream, cb){ // cb(err) 41 | var agent = http; 42 | if(url.substring(0,5)==='https') agent = https; 43 | 44 | var requestOpts = nodeUrl.parse(url); 45 | 46 | // fake browser headers 47 | requestOpts.headers = {}; 48 | 49 | agent.get(requestOpts, function(res){ 50 | if([301,302].indexOf(res.statusCode) > -1 && res.headers.location){ 51 | // follow redirect 52 | return downloadFile(res.headers.location, fileStream, cb); 53 | } 54 | 55 | res.pipe(fileStream); 56 | res.on('end', cb); 57 | }); 58 | } 59 | 60 | function deleteFolderRecursiveSync(path) { 61 | if( fs.existsSync(path) ) { 62 | fs.readdirSync(path).forEach(function(file, index){ 63 | var curPath = path + "/" + file; 64 | if(fs.lstatSync(curPath).isDirectory()) { // recurse 65 | deleteFolderRecursiveSync(curPath); 66 | } else { // delete file 67 | fs.unlinkSync(curPath); 68 | } 69 | }); 70 | fs.rmdirSync(path); 71 | } 72 | } -------------------------------------------------------------------------------- /release.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var packages = [ 4 | 'nodee-total', 5 | 'nodee-admin', 6 | 'nodee-cms' 7 | ]; 8 | 9 | // download all packages and start app 10 | require('./packages.js').downloadPackages(packages, function(){ 11 | require('total.js').http('release'); // start app 12 | }); -------------------------------------------------------------------------------- /views/layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nodee CMS 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 |
13 | Menu Item 14 |
15 |
16 | 17 |
18 |
19 |

Hello World ! You can edit me.

20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /views/layouts/default.html.js: -------------------------------------------------------------------------------- 1 | /*DEFAULT DEPENDENCIES*/ 2 | var Model = require('nodee-model'),Document = Model('CmsDocument'); 3 | module.exports.controller = function(document, done){ 4 | /*CONTROLLER*//* 5 | * available variables: 6 | * Model - base model constructor, use Model('MyConstructorName') to get model constructors 7 | * Document - reference to CmsDocument, same as Model('CmsDocument') 8 | * document - document model instance, it has all props including content 9 | * this - controller or parser 10 | * data - parsed data, only if parser 11 | * done - allways call done([optional error object]), because all controllers and parsers are async 12 | */ 13 | 14 | // get root document id, or this document id if it is root 15 | var rootDocId = document.ancestors[0] || document.id; 16 | 17 | Document 18 | .collection() 19 | 20 | // find all published descendants with "showInMenu" attribute 21 | .find({ ancestors:rootDocId, 'attributes.showInMenu':true, published:true }) 22 | 23 | // get only required fields 24 | .fields({ url:true, title:true }) 25 | 26 | // you can cache repeated queries 27 | //.cache('1m') 28 | 29 | // get results 30 | .all(function(err, menuItems){ 31 | 32 | // callback error, cms will handle it 33 | if(err) return done(err); 34 | 35 | // fill document model property menuItems 36 | document.menuItems = menuItems; 37 | 38 | // have to execute callback, controllers are async 39 | done(); 40 | }); 41 | 42 | /*CONTROLLER*/ 43 | } 44 | -------------------------------------------------------------------------------- /views/layouts/default.html.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseProp": "layouts_default", 3 | "name": "", 4 | "icon": "", 5 | "description": "", 6 | "attributes": [], 7 | "view": { 8 | "h2": { 9 | "html": "content.headline" 10 | }, 11 | ".menu-item": { 12 | "repeat": "menuItems", 13 | "inside": { 14 | "a": { 15 | "html": "title", 16 | "attrs": { 17 | "href": "url" 18 | } 19 | } 20 | } 21 | } 22 | }, 23 | "editors": { 24 | "h2": { 25 | "text": { 26 | "placeholder": "your text here" 27 | } 28 | } 29 | }, 30 | "parse": "" 31 | } -------------------------------------------------------------------------------- /views/pages/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 |
15 | -------------------------------------------------------------------------------- /views/pages/test.html.js: -------------------------------------------------------------------------------- 1 | /*DEFAULT DEPENDENCIES*/ 2 | var Model = require('nodee-model'),Document = Model('CmsDocument'); 3 | -------------------------------------------------------------------------------- /views/pages/test.html.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseProp": "pages_test", 3 | "name": "Test Page", 4 | "icon": "fa-file-text", 5 | "description": "", 6 | "attributes": [ 7 | { 8 | "propName": "showInMenu", 9 | "listProp": "", 10 | "editor": "checkbox", 11 | "settings": { 12 | "value": false 13 | }, 14 | "name": "Show in menu" 15 | } 16 | ], 17 | "view": {}, 18 | "editors": {}, 19 | "parse": "" 20 | } -------------------------------------------------------------------------------- /views/widgets/image.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /views/widgets/image.html.js: -------------------------------------------------------------------------------- 1 | /*DEFAULT DEPENDENCIES*/ 2 | var Model = require('nodee-model'),Document = Model('CmsDocument'); 3 | -------------------------------------------------------------------------------- /views/widgets/image.html.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseProp": "image", 3 | "name": "Image", 4 | "icon": "fa-image", 5 | "description": "", 6 | "attributes": [], 7 | "view": { 8 | "img": { 9 | "attrs": { 10 | "src": "content.{image}" 11 | } 12 | } 13 | }, 14 | "editors": { 15 | "img": { 16 | "image": { 17 | "width": 0, 18 | "widthAuto": false, 19 | "height": 0, 20 | "heightAuto": false, 21 | "crop": false, 22 | "bg_color": "", 23 | "attr": "src", 24 | "valueAsId": false 25 | } 26 | } 27 | }, 28 | "parse": "" 29 | } -------------------------------------------------------------------------------- /views/widgets/text.html: -------------------------------------------------------------------------------- 1 |
2 |

Subtitle

3 |

Some text...

4 |
-------------------------------------------------------------------------------- /views/widgets/text.html.js: -------------------------------------------------------------------------------- 1 | /*DEFAULT DEPENDENCIES*/ 2 | var Model = require('nodee-model'),Document = Model('CmsDocument'); 3 | -------------------------------------------------------------------------------- /views/widgets/text.html.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseProp": "text", 3 | "name": "Rich Text", 4 | "icon": "fa-pencil", 5 | "description": "", 6 | "attributes": [], 7 | "view": { 8 | "div": { 9 | "html": "content.{text}" 10 | } 11 | }, 12 | "editors": { 13 | "div": { 14 | "richtext": { 15 | "wysiwyg": true, 16 | "markdown": true, 17 | "html": true, 18 | "defaultMode": "" 19 | } 20 | } 21 | }, 22 | "parse": "" 23 | } --------------------------------------------------------------------------------