{"id":20170235,"url":"https://github.com/fabrix-app/spool-cms","last_synced_at":"2026-05-10T08:39:11.632Z","repository":{"id":95882805,"uuid":"143949417","full_name":"fabrix-app/spool-cms","owner":"fabrix-app","description":"Spool: CMS for Fabrix, flat file and persisted content tree structures with A/B/C testing","archived":false,"fork":false,"pushed_at":"2018-09-17T15:10:26.000Z","size":213,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-13T15:27:10.288Z","etag":null,"topics":["aaa-testing","ab-testing","cms","fabrix","nodejs","spools","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fabrix-app.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-08-08T02:16:40.000Z","updated_at":"2022-04-20T22:32:40.000Z","dependencies_parsed_at":"2023-04-07T01:48:27.461Z","dependency_job_id":null,"html_url":"https://github.com/fabrix-app/spool-cms","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabrix-app%2Fspool-cms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabrix-app%2Fspool-cms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabrix-app%2Fspool-cms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabrix-app%2Fspool-cms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fabrix-app","download_url":"https://codeload.github.com/fabrix-app/spool-cms/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241605820,"owners_count":19989612,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aaa-testing","ab-testing","cms","fabrix","nodejs","spools","typescript"],"created_at":"2024-11-14T01:17:47.584Z","updated_at":"2026-05-10T08:39:11.576Z","avatar_url":"https://github.com/fabrix-app.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# spool-cms\n\n[![Gitter][gitter-image]][gitter-url]\n[![NPM version][npm-image]][npm-url]\n[![Build Status][ci-image]][ci-url]\n[![Test Coverage][coverage-image]][coverage-url]\n[![Dependency Status][daviddm-image]][daviddm-url]\n[![Follow @FabrixApp on Twitter][twitter-image]][twitter-url]\n\n## Content Management built for speed, scalability, testing, and Developer Joy\n\nThe Proxy Engine Router is an Express middleware built to be used on Fabrixjs with Proxy Engine.\nIt's purpose is to allow for easy development, SEO, and AAA (Triple A - Automated Analytical Assessment) testing from the ground up (a concept developed by [Scott Wyatt](https://github.com/scott-wyatt)). This means that you can automate UI testing and can still use your own controllers to handle views and add Proxy Route content to them as needed.\n\nViews are stored in either a Flat File database or joined with a Postgres database, and are cache-able in a document store such as Redis.\nEach view has a series of tests that are displayed based on a weight, threshold, and baseline for a given demographic.\n\nEach time a view is run, the engine will determine which series to display and track the runs for a given view and positive/negative control conversions for the demographic to score it.\nOnce a Series threshold and baseline is met, it becomes the default view for a given demographic.\n\nTo read why this is important, checkout out our [ article on AAA Testing](http://cali-style.com/blog/pioneering-aaa-webpage-testing)\n\nSay good bye to A/B testing as AAA testing can handle hundreds of different series test at once for each view in a web app and it can do it automatically.  This makes UI testing purely iterative and personable.\n - Want to find the best UI for a time of day and show it at the right time?\n - Want to find the best layouts for males or females or trans and show it to the corresponding audience automatically?\n\nAAA Testing isn't about one ~~size~~ site fits all, it's about finding the right layout per audience. Series documents are given a test number, and version. They default to the latest version, but the default can be changed to any version while keeping the run and score.\nLarge changes to any version should be given another test number. The documents are markdown documents with yaml that allow you to use normal markdown, HTML, or even your own embeds.\n\nUse your own mechanisms to track negative and positive interactions and then feed them back to Proxy Engine to adjust the score.\n\nUse your own mechanisms to determine what qualifies as a demographic.\n\n## Principles \nOne of the most difficult feats when dealing with a CMS, from a developer standpoint, is continuity:\n\n- Developing well crafted pages in a CMS requires a local or staging DB and then taking approved changes live in some slow error prone fashion.\n- A/B Testing requires that Marketers and UX specialist have easy access to creating variations of views and running tests. \n- Database driven view states are notoriously slow.\n- Database content with large amounts of HTML is slow to search.\n\nAll of these are issues for the Modern Web, especially for web apps built as single page applications. \n\nProxy Engine's router takes care of these pain points by: \n\n- Giving developers a flat file database for component driven views with a high amount of version control.\n- Giving Marketers/UXs an easy way to create variations and automatically run tests when given an appropriate editor.\n- Making documents cache-able and still retaining tests and version control across millions of pages.\n- Using the Metadata for each page makes using postgres' JSONB keyword searching fast (and already SEO ready), or easily connect postrgres to an Elasticsearch engine to make content searching even better.\n\n### Additional Use Cases\n- Assign demographics to users and display or withhold content based on it.\n- Complete version control for technical manuals and blogs\n- Flatfile CMS for non database driven web apps.\n- Versioned display structure for web apps using Angular2 ngRX or React Flux.\n\n### Sitemap\n- Proxy Router is very fast with an average of just a few millaseconds per non-cached page render and less than a millasecond when cached. In addition to rendering a page, it also sitemaps a page's children and sitemaps all content on server start/edit.\n\n### Gotchas\n- This style of CMS requires a \"Single Source of Truth\" for Frontend Components to bind too. Try using Redux or ngRX for your frontend.\n- Mechanisms to determine/set Score and Demographic are up to you.\n\n## Dependencies\n### Supported ORMs\n| Repo          |  Build Status (edge)                  |\n|---------------|---------------------------------------|\n| [spool-sequelize](https://github.com/fabrixjs/spool-sequelize) | [![Build status][ci-sequelize-image]][ci-sequelize-url] |\n\n### Supported Webserver\n| Repo          |  Build Status (edge)                  |\n|---------------|---------------------------------------|\n| [spool-express](https://github.com/fabrixjs/spool-express) | [![Build status][ci-express-image]][ci-express-url] |\n\n\n## Install\n\n```sh\n$ npm install --save spool-cms\n```\n\n## Configure\n\n```js\n// config/main.ts\nexport const main = {\n  spools: [\n    // ... other spools\n    require('spool-cms')\n  ]\n}\n```\n\n```js\n// config/web.ts\n  middlewares: {\n    order: [\n      // ... other middleware\n      'cms', // cms must be before router\n      'router'\n    ],\n    cms: function(req, res, next){\n      return require('@fabrix/spool-cms').Middleware.cms(req, res, next)\n    }\n  }\n```\n```js\n// config/cms.ts\nexport const cms = {\n  // The Default Extension to use when creating/updating/reading files, falls back to either .md or .html\n  default_extension: '.md',\n  // Default Threshold\n  threshold: 100,\n  // Default Baseline\n  baseline: 0.75,\n  // Default Weight\n  weight: 50,\n  // Default Flat File Folder\n  folder: 'content',\n  // Default name for \"series\"\n  series: 'series',\n  // Force Flat File and ignore DB\n  force_fl: true,\n  // The number of controls to enqueue before flushing to processor.\n  flush_at: 20,\n  // The number of milliseconds to wait before flushing the queue automatically to processor.\n  flush_after: 10000,\n  // Cache\n  cache: {\n    // The redis datastore prefix\n    prefix: 'pxy',\n    // Allow Caching\n    allow: true,\n    // Milliseconds before cache is ejected\n    eject: 10000\n  }\n}\n```\n\n### Content Folder\nBy default the Proxy Route content directory is `content` in the root directory of your application.  However, it can changed to any directory or even a node_module by setting the `folder` value in `config/cms`. Whatever the content folder, the file structure must follow these guidelines:\n\n- Every directory must have a series directory that contains a named test directory eg. `a0` with a SemVer versioned markdown document.\n- Named test directories follow this pattern: `a0`, `b0`, `c0` etc.  Upon exceeding `z0` change to `a1`, `b1`, `c1` etc.\n- Directories that start with wild cards eg. `:world` or `*` will match express routes.\n- Files must end in a `.md` (markdown) or `.html` (HTML) file extension, but a test directory must be of all one file type.\n\n##### Example\n```\n - content\n   - hello\n     - :world\n       - series\n         - a0\n           - 0.0.0.md\n     - earth\n       - series\n         - a0\n           - 0.0.0.md\n     - html\n       - series\n         - a0\n           - 0.0.0.html\n           - 0.0.1.html\n     - series\n       - a0\n         - 0.0.0.md\n   - series\n     - a0\n       - 0.0.0.md\n       - 0.0.1.md\n     - b0\n       - 0.0.0.md\n```\n\n### req.locals\nProxy Route merges the document's id, series, version, and metadata with req.locals so it can be used in any view template engine required.\nTo access it in your template engine use the request's local variable `cms.document` and `cms.meta`\n\n### Ignore Routes and Alternate Routes\nWhen the fabrix app starts, three configurations are added to fabrixApp.config.cms:\n-  `getRoutes`\n- `ignoreRoutes`\n- `alternateRoutes`\n\nIgnored Routes are any routes that do not use the GET method or have an app config with ignore set to true \n```\n  // config/routes.ts\n  ...\n  '/ignore/me': {\n    'GET': {\n      handler: 'IgnoreController.me',\n      config: {\n        app: {\n          cms: {\n            ignore: true\n          }\n        }\n      }\n    }\n  }\n```\nIt's important to ignore routes that you don't want Proxy Route to check as it will speed up the application.\n\nAlternate Routes are any routes that use the GET method and have a wildcard or an express parameter in the url eg `/home/*` or `/hello/:world`.\nThis is useful for when a child route may not have a specific view eg. `/products/1` and the wildcard might eg. `products/:id`.  With this schema, you need not make a view for each product, and instead just define the wildcard templates which the product will inherit.  This does allow you to still have extreme control over any individual page while also having a fallback.\n\n### Add Policies to RouteController Methods\nBy default spool-proxy-route has no policies to prevent anything from hitting the RouteController endpoints. You will need to create policies with your authentication strategy to determine what is allowed to hit them. We recommend using [Proxy Permissions](https://github.com/calistyle/spool-proxy-permissions) which makes this easy and will lock down administrative endpoints automatically.\n\n### Server Clusters with Flat File (TODO)\nFor Proxy Router to work on a server cluster as a Flat File server, Redis is required. \nAfter any route or series is updated as a Flat File, an event is produced to all other servers in the cluster to copy the flat files to their folder structure. This is quick, but expect a few milliseconds of lag.\n\n### Pull Requests to Source (TODO)\nIf you are hosting your repository on GitHub, then great news, when you create/update/destroy a Page or Series on a production web app, Proxy Router can issue a pull request to your repo. This keeps your remote Flat Files in sync with your production application.\n```\nTODO example\n```\n\n## Usage\n\n### Example series document\n\nThis is the default home page located at `/content/series/a0/index.md`\n\n```sh\n---\ntitle: Homepage Hello World\nkeywords: proxy-engine, amazing\nruns: 0\nscore: 0.0\ndemographics: \n - {name: 'unknown'}\nscripts:\n - /i/can/do/arrays/too.js\n - /path/to/special/page/script.js\nog: {'image': '/and/cool/things/like/og-tags.jpg'}\n---\n\u003cheader-component\u003e\n\u003c/header-component\u003e\n# Homepage Hello World\n\u003ch2\u003eI can use Normal HTML\u003c/h2\u003e\n\nI can even use embeds like a youtube video or my own custom ones.\n@[youtube](lJIrF4YjHfQ)\n\nI can even use custom HTML DOM like ones from Angular\n\n\u003clogin\u003e\n\u003c/login\u003e\n\u003cfooter-component [wow]=\"amazing\"\u003e\n\u003c/footer-component\u003e\n```\n\n*** Note: Html components must end on a newline or else they are wrapped by a paragraph tag. \nThis is part of Common Mark Spec.\n\n#### Demographics\nDemographics is a generic term because they can literally be anything. For example Let's say we have a site that's homepage needs some simple A/B testing.  We have no information about our user, so we can classify them as \"Unknown\" which is the default demographic. Now, we can set up two series: a0 and b0 and split visits equally with a weight of 50. When our user visits the home page, Proxy Router will send them a0 or b0 and track the view that was run. Now the user does something on the home page which should issue a Positive or Negative control.  \n\n#### Scores and Positive/Negative Controls\nWhen a user does something we don't like on the page, we want to send a negative control back to Proxy Router. For example, if they leave the website, then we might send a negative control. That said, if the user was visiting the a0 series of the homepage, then a0 would get a reduction in it's overall score. If the user does something that we like on the page, for example clicks \"Buy Now\", then we might want to send a positive control that increases the score of a0. We continue this process until the Baseline and Threshold is met.\n   \n#### Baseline and Threshold\nEvery Page has a baseline. The baseline is the minimal times the page can be viewed before the threshold comes into effect.  Imagine it as a survey, where you want 1000 people to take the survey before you review the results. From the previous example between a0 and b0, let's say a 1000 people visit the home page. After that 1000 people have visited, we should have some decent scores from a0 and b0 for example a0 scored 0.89 and b0 score 0.70. Proxy Route now examines the threshold and will predict that a0 is more productive then b0. If we set the threshold to .90, then we will stop testing between a0 and b0 when a0 reaches 0.90 and begin serving only a0 for the \"unknown\" demographic. \n\n#### Scoring\nA max series score is 1.0 and a min series score is 0.0.  This score is the result of positive/negative scores from some machine learning (nothing too fancy) and user interactions. When a user clicks on let's say a button, we can issue a click event with a score between 1 and 100 based on how important that is too us. For example, a page link maybe get a issue a score of 1, while a \"Buy Now\" may issue a score of 100. Proxy-engine will take that event score and compare it to the previous score, runs of the series, the threshold of the page, and the weight of series distribution.  \n\nTODO\nTruncate tests that are failing a standard deviation from the mean after the baseline is met. \n\n### Markdown-it (required)\n[Markdown-it](https://www.npmjs.com/package/markdown-it) \nis used to parse the document from markdown/html to html.\n\n### Markdown-it Meta Plugin (Default)\nIs used to give the flat file readable meta data as well as give the displayed page meta data.\n\n### Markdown-it Component Plugin (Default)\nIs used to give the flat files the ability to use html components typical of angular, angular2 and react\n\n### Markdown-it Block Embed (optional)\n[Markdown-it Block Embed Embed](https://github.com/rotorz/markdown-it-block-embed) \nis used to grant the parsed document embed-ables.  This could be youtube, vimeo, your own short codes, whatever!\n\n### Controllers\n#### RouteController\n##### RouteController.view\nAn example of using `req.proxyroute`.  RouteController.view can return a view as html or as JSON.\n\n##### RouteController.buildToDB\nBuilds the Flat File structure to the database \n\n##### RouteController.buildToFL\nBuilds the Database to a Flat File Structure\n\n##### RouteController.addPage\nAdds a Route Model (Page)\n\n##### RouteController.editPage\nEdits a Route Model (Page)\n\n##### RouteController.removePage\nRemoves a Route Model (Page)\n\n##### RouteController.addSeries\nAdds a RouteDocument Model (Document)\n\n##### RouteController.editSeries\nEdits a RouteDocument Model (Document)\n\n##### RouteController.removeSeries\nRemoves a RouteDocument Model (Document)\n\n##### RouteController.control\nAdds a Positive or Negative value for a series\n\n### Services\n#### Flat Files\n##### RouterFlService.get(req)\nGets a rendered page from a flatfile given the express request object\n@returns\n```js\n{ \n  id: String, // The id of the Route Model (in this case the ID is null since it's a flat file)\n  path: String, // The original request path\n  series: String, // The Series Test of the Route Document Model\n  version: String, // The Test Version of the Route Document Model\n  meta: Object, // The Meta Data from the Route Document Model\n  document: String  // The Rendered HTML of the Route Doucment Model\n}\n```\n\n##### RouterFLService.renderPage()\nResolves and Renders a Route Document\n\n#### Database Files\n##### RouterDBService.get(req)\nGets a rendered page from the database given the express request object\n\n@returns\n```js\n{ \n  id: String, // The id of the Route Model\n  path: String, // The original request path\n  series: String, // The Series Test of the Route Document Model\n  version: String, // The Test Version of the Route Document Model\n  meta: Object, // The Meta Data from the Route Document Model\n  document: String  // The Rendered HTML of the Route Doucment Model\n}\n```\n\n#### Controls\n##### RouteControlsService.addRun()\nAdds a run score to a series\n\n##### RouteControlsService.positive()\nAdds a positive score to a series\n\n##### RouteControlsService.negative()\nAdds a negative score to a series\n\n#### General\n##### RouteService.addPage()\nAdds a Page (Route Model).\nCalls `RouteService.createPage()`\n\n##### RouteService.createPage()\nAdds a Page (Route Model)\n\n##### RouteService.editPage()\nEdits a Page (Route Model).\nCalls `RouteService.updatePage()`\n\n##### RouteService.updatePage()\nUpdates a Page (Route Model)\n\n##### RouteService.removePage()\nRemoves a Page (Route Model).\nCalls `RouteService.destroyPage()`\n\n##### RouteService.destroyPage()\nRemoves a Page (Route Model)\n\n##### RouteService.addSeries()\nAdds a Document (RouteDocument Model).\nCalls `RouterService.createSeries()`\n\n##### RouteService.createSeries()\nCreates a Document (RouteDocument Model)\n\n##### RouteService.editSeries()\nEdits a Document (RouteDocument Model).\nCalls `RouteService.updateSeries()`\n\n##### RouteService.updateSeries()\nUpdates a Document (RouteDocument Model)\n\n##### RouteService.removeSeries()\nRemoves a Document (RouteDocument Model).\nCalls `RouteService.destroySeries()`\n\n##### RouteService.destroySeries()\nDestroys a Document (RouteDocument Model)\n\n#### Render\n##### RenderGenericService.render()\nRenderGenericService is a Proxy-Generics service. This module has a default render if none is specified.\n\nRenders a markdown document using Markdown-it and all the plugins configured in proxyGeneric.render_service\n\n@returns\n```js\n{ \n  meta: \u003c{Object}\u003e, // The Meta Data from the Document\n  document: \u003c{String}\u003e  // The Rendered HTML of the Document\n}\n```\n\n# ROAD MAP\n## 1.0.0\n- Abstract Render as proxy generic - completed\n- Allow folders to not use a series folder\n- Build to DB\n- Build to FL\n- Ignore Staic Assests\n- Support Cacheing and Cache Busting\n- Support Server Clusters for CMS functions\n\n[npm-image]: https://img.shields.io/npm/v/@fabrix/spool-cms.svg?style=flat-square\n[npm-url]: https://npmjs.org/package/@fabrix/spool-cms\n[ci-image]: https://img.shields.io/circleci/project/github/fabrix-app/spool-cms/master.svg\n[ci-url]: https://circleci.com/gh/fabrix-app/spool-cms/tree/master\n[daviddm-image]: http://img.shields.io/david/fabrix-app/spool-cms.svg?style=flat-square\n[daviddm-url]: https://david-dm.org/fabrix-app/spool-cms\n[gitter-image]: http://img.shields.io/badge/+%20GITTER-JOIN%20CHAT%20%E2%86%92-1DCE73.svg?style=flat-square\n[gitter-url]: https://gitter.im/fabrix-app/Lobby\n[twitter-image]: https://img.shields.io/twitter/follow/FabrixApp.svg?style=social\n[twitter-url]: https://twitter.com/FabrixApp\n[coverage-image]: https://img.shields.io/codeclimate/coverage/github/fabrix-app/spool-cms.svg?style=flat-square\n[coverage-url]: https://codeclimate.com/github/fabrix-app/spool-cms/coverage\n\n\n[ci-sequelize-image]: https://img.shields.io/circleci/project/github/fabrix-app/spool-sequelize/master.svg\n[ci-sequelize-url]: https://circleci.com/gh/fabrix-app/spool-sequelize/tree/master\n\n[ci-express-image]: https://img.shields.io/circleci/project/github/fabrix-app/spool-express/master.svg\n[ci-express-url]: https://circleci.com/gh/fabrix-app/spool-express/tree/master\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffabrix-app%2Fspool-cms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffabrix-app%2Fspool-cms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffabrix-app%2Fspool-cms/lists"}