Crownpeak Digital Experience Management (DXM) Software Development Kit (SDK) for React has been constructed to assist the Single Page App developer in developing client-side applications that leverage DXM for content management purposes.
- If you're upgrading from a version before 2.1.0, please run the following before installing the new patch.
yarn crownpeak upgrade # or npx crownpeak upgrade
- If you're upgrading to version 3.0.0 or higher, please note the change to asynchronous data loading, and include a check for
this.state.isLoaded
in yourrender()
method. See the examples below.
-
Runtime libraries to handle communication with either Dynamic (DXM Dynamic Content API) or Static (On-disk JSON payload) Data Sources
As a development team runs their build process, the underlying React Application will be minified and likely packed into a set of browser-compatible libraries (e.g., ES5). We expect any DXM NPM Packages also to be compressed in this manner. To facilitate communication between the React Application and content managed within DXM, a runtime NPM Package is provided. The purpose of this package is:
- Read application configuration detail from a global environment file (e.g., Dynamic Content API endpoint and credentials, static content disk location, etc.);
- Making data models available to the React Application, which a developer can map against
- Dynamic Data - Asynchronously processing data from the DXM Dynamic Content API, using the Search G2 Raw JSON endpoint; and
- Static Data - Loading JSON payload data directly from local storage.
-
DXM Content-Type Scaffolding
Developers will continue to work with their Continuous Integration / Delivery and source control tooling to create a React application. However, the purpose of the DXM Content-Type Scaffolding build step is to convert the React Components in a single direction (React > DXM), into the necessary configuration to support CMS operations. At present, the DXM Component Library includes the capability to auto-generate Templates (input.aspx, output.aspx, post_input.aspx) based upon a moustache-style syntax (decorating of editable properties). It is not intended that we re-design this process, as it is fully supported within DXM, and customer-battle-tested - therefore, in order to create Template configuration, the build step:
- Converts React Components into Crownpeak Components by using the existing Component Builder Process, via the CMS Access API (DXM's RESTful Content Manipulation API), and then existing "post_save" process;
- Creates Templates for each React Page (One of the DXM React Component Types) by using the existing Template Builder Process, again via the CMS Access API and existing "post_save" process; and
- Creates a new Model for the React Page Content-Type, via the CMS Access API, so that authors can create multiple versions of a structured Page or Component, without needing to run an entire development/test cycle.
yarn add crownpeak-dxm-react-sdk
# or
npm install crownpeak-dxm-react-sdk
Review example project at https://github.com/Crownpeak/DXM-SDK-Examples/tree/master/React for complete usage options. The example project includes the following capabilities:
- Routing using
React-Router
and JSON payload, delivered from DXM to map AssetId to requested path. Although not part of the SDK itself, the example can be used if desired. For routes.json structure, see example at the foot of this README. CmsStaticPage
type to load payload data from JSON file on filesystem, delivered by DXM;CmsDynamicPage
type to load payload data from DXM Dynamic Content API.
Loads payload data from JSON file on filesystem - expects knowledge of DXM AssetId in order to find file with corresponding name (e.g., 12345.json). CmsStaticPage is the data equivalent of a DXM Asset when used as a page. Example at /examples/bootstrap-blog/pages/blogPage.js:
import React from 'react'
import Header from "../components/header";
import TopicList from "../components/topicList";
import FeaturedPost from "../components/featuredPost";
import SecondaryPost from "../components/secondaryPost";
import BlogPost from "../components/blogPost";
import PostArchives from "../components/postArchives";
import Footer from "../components/footer";
import { CmsStaticPage, CmsDynamicPage } from 'crownpeak-dxm-react-sdk';
import Routing from "../js/routing";
export default class BlogPage extends CmsStaticPage
{
constructor(props)
{
super(props);
this.cmsWrapper = ""; //insert Wrapper Name from data-cms-wrapper-name in HTML, or don't include property to accept defaults.
this.cmsUseTmf = false; //set to true to create templates that use the Translation Model Framework.
this.cmsUseMetadata = false; //set to true to create templates that include the standard MetaData component.
this.cmsSuppressModel = false; //set to true to suppress model and content folder creation when scaffolding.
this.cmsSuppressFolder = false; //set to true to suppress content folder creation when scaffolding.
if(this.props && this.props.location) this.cmsAssetId = Routing.getCmsAssetId(this.props.location.pathname);
}
render() {
super.render();
return (
this.state.isLoaded &&
<div>
<div className="container">
<Header month={this.props.match.params.month}/>
<TopicList/>
<div className="jumbotron p-3 p-md-5 text-white rounded bg-dark">
<FeaturedPost/>
</div>
<div className="row mb-2">
<div className="col-md-6">
<SecondaryPost/>
</div>
<div className="col-md-6">
<SecondaryPost/>
</div>
</div>
</div>
<main role="main" className="container">
<div className="row">
<div className="col-md-8 blog-main">
<h3 className="pb-3 mb-4 font-italic border-bottom">
From the Firehose
</h3>
<BlogPost/>
</div>
<aside className="col-md-4 blog-sidebar">
<PostArchives/>
</aside>
</div>
</main>
<Footer/>
</div>
)
}
}
Loads payload data from DXM Dynamic Content API upon request - expects knowledge of DXM AssetId.
import React from 'react'
import Header from "../components/header";
import TopicList from "../components/topicList";
import FeaturedPost from "../components/featuredPost";
import SecondaryPost from "../components/secondaryPost";
import BlogPost from "../components/blogPost";
import PostArchives from "../components/postArchives";
import Footer from "../components/footer";
import { CmsStaticPage, CmsDynamicPage } from 'crownpeak-dxm-react-sdk';
import Routing from "../js/routing";
export default class BlogPage extends CmsDynamicPage
{
constructor(props)
{
super(props);
this.cmsWrapper = ""; //insert Wrapper Name from data-cms-wrapper-name in HTML, or don't include property to accept defaults.
this.cmsUseTmf = false; //set to true to create templates that use the Translation Model Framework.
this.cmsUseMetadata = false; //set to true to create templates that include the standard MetaData component.
this.cmsSuppressModel = false; //set to true to suppress model and content folder creation when scaffolding.
this.cmsSuppressFolder = false; //set to true to suppress content folder creation when scaffolding.
if(this.props && this.props.location) this.cmsAssetId = Routing.getCmsAssetId(this.props.location.pathname);
}
render() {
super.render();
return (
this.state.isLoaded &&
<div>
<div className="container">
<Header month={this.props.match.params.month}/>
<TopicList/>
<div className="jumbotron p-3 p-md-5 text-white rounded bg-dark">
<FeaturedPost/>
</div>
<div className="row mb-2">
<div className="col-md-6">
<SecondaryPost/>
</div>
<div className="col-md-6">
<SecondaryPost/>
</div>
</div>
</div>
<main role="main" className="container">
<div className="row">
<div className="col-md-8 blog-main">
<h3 className="pb-3 mb-4 font-italic border-bottom">
From the Firehose
</h3>
<BlogPost/>
</div>
<aside className="col-md-4 blog-sidebar">
<PostArchives/>
</aside>
</div>
</main>
<Footer/>
</div>
)
}
}
If you are using React function components, you must call out to a load() method on the page type, or
include a comment containing exactly CmsPage
for your page to be recognised and scaffolded correctly.
import React, { useState, useEffect } from 'react';
import { CmsStaticPage } from 'crownpeak-dxm-react-sdk';
// [ ... ]
export default function BlogPage()
{
let isLoaded = CmsStaticPage.load(12345, useState, useEffect); // Or use CmsDynamicPage
const cmsWrapper = ""; //insert Wrapper Name from data-cms-wrapper-name in HTML, or don't include property to accept defaults.
const cmsUseTmf = false; //set to true to create templates that use the Translation Model Framework.
const cmsUseMetadata = false; //set to true to create templates that include the standard MetaData component.
const cmsSuppressModel = false; //set to true to suppress model and content folder creation when scaffolding.
const cmsSuppressFolder = false; //set to true to suppress content folder creation when scaffolding.
return (
isLoaded &&
<div>
<!-- [...] -->
</div>
)
}
export function PageLoadingItsOwnData() {
// The comment below is required for your page to be recognised by the scaffolding process.
// CmsPage
return (
<etc />
)
}
If you wish to limit the time available for your page to load its data, you can specify a timeout in milliseconds like this:.
this.cmsLoadDataTimeout = 3000; // Timeout after 3 seconds
For React function components, the timeout is an optional fourth argument to the load
method on the CmsStaticPage
and CmsDynamicPage
classes. For example:
let isLoaded = CmsStaticPage.load(12345, useState, useEffect, 3000);
Note that if a timeout occurs when loading dynamic data, it will automatically attempt to fall back to loading static data, with the same timeout value. If this succeeds, you will see a warning in the browser console. If it fails, the cmsDataError event will be triggered as described below, or you will see an error in the browser console.
If you want to know when your data has loaded, or whether an error occurred during load, or you wish to modify properties of a request before it is sent, you can use the following:
this.cmsDataLoaded = (data, assetId) => {
alert(`Loaded ${assetId}, ${JSON.stringify(data)}`);
};
this.cmsDataError = (exception, assetId) => {
alert(`Error ${assetId}, ${exception}`);
};
this.cmsBeforeLoadingData = (options) => {
// options will be either:
// * an XmlHttpRequest for synchronous data requests; or
// * a RequestInit object for asynchronous data requests, which will be passed to the fetch call.
};
Inside cmsDataLoaded
it is also possible to modify or replace the loaded data. Return an object from your function to do this.
Inside cmsBeforeLoadingData
any modifications you make to the options
object will be passed to the data request.
For React function components, these events can be passed to the load
method on the CmsStaticPage
and CmsDynamicPage
classes. For example:
const cmsDataLoaded = (data, assetId) => {
alert(`Loaded ${assetId}, ${JSON.stringify(data)}`);
};
const cmsDataError = (exception, assetId) => {
alert(`Error ${assetId}, ${exception}`);
};
const cmsBeforeLoadingData = (options) => {
alert("Before loading data");
};
let isLoaded = CmsStaticPage.load(12345, useState, useEffect, 3000, cmsDataLoaded, cmsDataError, cmsBeforeLoadingData);
As with class-based pages, you can also modify or replace the data inside the cmsDataLoaded event by returning a new object.
Also as with class-based pages, you can modify the options
object and these modifications will be passed to the data request.
If you have global changes that you wish to make for all data requests, these can be set directly on the CmsStaticDataProvider
and CmsDynamicDataProvider
objects via static properties.
CmsStaticDataProvider.beforeLoadingData = (options) => alert("Before loading static data");
CmsDynamicDataProvider.beforeLoadingData = (options) => alert("Before loading dynamic data");
Includes CmsField references for content rendering from DXM within a React Component.:
import React from 'react';
import { CmsComponent, CmsField, CmsFieldTypes } from 'crownpeak-dxm-react-sdk';
import ReactHtmlParser from 'react-html-parser';
export default class BlogPost extends CmsComponent
{
constructor(props)
{
super(props);
this.post_title = new CmsField("Post_Title", CmsFieldTypes.TEXT);
this.post_date = new CmsField("Post_Date", CmsFieldTypes.DATE);
this.post_content = new CmsField("Post_Content", CmsFieldTypes.WYSIWYG);
this.post_category = new CmsField("Post_Category", CmsFieldTypes.DOCUMENT);
this.cmsFolder = ""; //set the subfolder in which the component will be created when scaffolding.
this.cmsZones = []; //set the zones into which the component is permitted to be dropped.
this.cmsDisableDragDrop = false; // set to true to hide this component from drag and drop.
}
render() {
return (
<div className="blog-post">
<h2 className="blog-post-title">{ this.post_title }</h2>
<p className="blog-post-meta">Date: { new Date(this.post_date).toLocaleDateString() }</p>
{ ReactHtmlParser(this.post_content) }
{ /*this.post_category*/ }
</div>
)
}
}
If you are using React function components, you must call out to the CmsDataCache.setComponent() method for your component to be recognised and scaffolded correctly.
import React from 'react';
import { CmsField, CmsFieldTypes, CmsDataCache } from 'crownpeak-dxm-react-sdk';
import ReactHtmlParser from 'react-html-parser';
export default function BlogPost(props)
{
CmsDataCache.setComponent("BlogPost");
const cmsFolder = ""; //set the subfolder in which the component will be created when scaffolding.
const cmsZones = []; //set the zones into which the component is permitted to be dropped.
const cmsDisableDragDrop = false; // set to true to hide this component from drag and drop.
var heading = new CmsField("Heading", CmsFieldTypes.TEXT);
var description = new CmsField("Description", CmsFieldTypes.WYSIWYG);
return (
<div className="container">
<h1>{ heading }</h1>
{ ReactHtmlParser(description) }
</div>
)
}
Enables implementation of draggable components via DXM. Example usage below:
import { CmsDropZoneComponent } from 'crownpeak-dxm-react-sdk';
import PrimaryCTA from "../components/primaryCta";
import SecondaryCTA from "../components/secondaryCta";
export default class DropZone extends CmsDropZoneComponent {
constructor(props)
{
super(props);
this.components = {
"PrimaryCTA": PrimaryCTA,
"SecondaryCTA": SecondaryCTA
};
}
}
Example implementation upon a CmsStaticPage
or CmsDynamicPage
:
<DropZone name="Test"/>
For further details, see examples/bootstrap-homepage project.
Enables implementation of list items within DXM. Example usage below (note comment, which is requirement for DXM scaffolding):
import React from 'react';
import {CmsComponent, CmsField, CmsFieldTypes, CmsDataCache } from 'crownpeak-dxm-react-sdk';
import SecondaryContainer from './secondaryContainer';
export default class SecondaryList extends CmsComponent
{
constructor(props)
{
super(props);
this.SecondaryContainers = new CmsField("SecondaryContainer", "Widget", CmsDataCache.get(CmsDataCache.cmsAssetId).SecondaryList);
}
render () {
let i = 0;
return (
<div className="row">
{/* <List name="SecondaryContainers" type="Widget" itemName="_Widget"> */}
{this.SecondaryContainers.value.map(sc => {
return <SecondaryContainer data={sc.SecondaryContainer} key={i++}/>
})}
{/* </List> */}
</div>
)
}
}
If your application code is too complex for the parser to be able to extract your fields, it is possible to provide your own markup for the Component Library to use instead of your component, page and wrapper code:
{/* cp-scaffold
<h2>{Heading:Text}</h2>
else */}
<h2>{{ heading.value.length > MAX_LENGTH ? heading.value.substr(0, MAX_LENGTH) + "..." : heading }}</h2>
{/* /cp-scaffold */}
It is also possible to add extra markup that is not used directly in your application, for example to support extra data capture:
{/* cp-scaffold
{SupplementaryField:Text}
/cp-scaffold */}
When using cp-scaffold on a wrapper, you should use normal HTML comments rather than React comments, for example:
<!-- cp-scaffold {metadata} /cp-scaffold -->
If your application code uses components that are not directly contained with your application, the markup will not be detected during the parsing process. For this (and other) use-cases, you can now supply a list of string replacements that will be made at the end of the parsing process.
To do this, create a .cpscaffold.json
file in the root of the project, as follows:
{
"replacements": {
"source": "replacement",
"second_source": "second_replacement"
}
}
Each key inside the replacements
item contains the item to be replaced, and its value contains the replacement.
The source string will be turned into a regular expression, so characters that are meaningful in RegExps should be escaped using \\
. This support allows for more complex replacements such as:
"<Col ([a-z]{2})=\\{([0-9]+)\\}>": "<div class=\"col-$1-$2\">"
If the source string contains a well-formed tag declaration, for example <div>
, any corresponding element in the source string will match, including those with attributes. The replacement operation also combines duplicate attributes, if present after replacement, using a space to separate the values. So, for example, if the .cpscaffold.json
replacements contains:
"<Container>": "<div class=\"container\">",
A source string containing <Container class="medium">
would be replaced with <div class="container medium">
.
Enumeration containing field types supported within the SDK.
CmsFieldType | DXM Mapping |
---|---|
TEXT | Text |
WYSIWYG | Wysiwyg |
DATE | Date |
DOCUMENT | Document |
IMAGE | Src |
HREF | Href |
Enables fields to be extracted from content and published as separate fields into Search G2, to support easier sorting and filtering:
this.title = new CmsField("Title", CmsFieldTypes.TEXT, null, CmsIndexedField.STRING);
A number of different values are available in the CmsIndexedField
enumerated type to support different data types:
Enum value | Search G2 Prefix | Comment |
---|---|---|
STRING | custom_s_ | String, exact match only. |
TEXT | custom_t_ | Text, substring matches allowed, tokenised so not usefully sortable. |
DATETIME | custom_dt_ | DateTime, must be ISO-8601. |
INTEGER | custom_i_ | 32-bit signed integer, must be valid. |
LONG | custom_l_ | 64-bit signed integer, must be valid. |
FLOAT | custom_f_ | 32-bit IEEE floading point, must be valid. |
DOUBLE | custom_d_ | 64-bit IEEE floating point, must be valid. |
BOOLEAN | custom_b_ | Boolean, must be true or false. |
LOCATION | custom_p_ | Point, must be valid lat,lon. |
CURRENCY | custom_c_ | Currency, supporting exchange rates. |
If an invalid value for the specific data type is sent to Search G2, the entire statement is liable to fail silently. |
Used to run a one-time dynamic query from DXM's Dynamic Content API.
import React from 'react';
import { Link } from 'react-router-dom';
import { CmsComponent, CmsDynamicDataProvider } from 'crownpeak-dxm-react-sdk';
export default class PostArchives extends CmsComponent
{
constructor(props)
{
super (props);
const data = new CmsDynamicDataProvider().getDynamicQuery("q=*:*&fq=custom_s_type:\"Blog%20Page\"&rows=0&facet=true&facet.mincount=1&facet.range=custom_dt_created&f.custom_dt_created.facet.range.start=NOW/YEAR-1YEAR&f.custom_dt_created.facet.range.end=NOW/YEAR%2B1YEAR&f.custom_dt_created.facet.range.gap=%2B1MONTH");
this.months = data.facet_counts.facet_ranges.custom_dt_created.counts.filter((_c, i) => i%2 === 0);
}
render() {
return (
<div className="p-3">
<h4 className="font-italic">Archives</h4>
<ol className="list-unstyled mb-0">
{this.months.map((month) => {
return <li key={month.substr(0,7)}><Link to={`/posts/months/${month.substr(0,7)}`}>{ [new Date(month).toLocaleString('default', { month: 'long', year: 'numeric' })] }</Link></li>
})}
</ol>
</div>
)
}
}
Used to load content from a JSON Object on Filesystem and populate fields in CmsComponent.
import React from 'react';
import { CmsComponent, CmsStaticDataProvider } from 'crownpeak-dxm-react-sdk';
export default class TopicList extends CmsComponent
{
constructor(props)
{
super (props);
this.topics = new CmsStaticDataProvider().getCustomData("topics.json");
}
render() {
return (
<div className="nav-scroller py-1 mb-2">
<nav className="nav d-flex justify-content-between">
{this.topics.map((topic) => {
return <a key={topic.toString()} className="p-2 text-muted" href="#">{ topic }</a>
})}
</nav>
</div>
)
}
}
There are a number of options that can be specified on the constructor of an item that extends CmsPage. These are set as properties on the extending class. For example:
this.cmsUseTmf = true;
Property | Description |
---|---|
cmsUseTmf | If set, the resulting template will use the Translation Model Framework (TMF). Defaults to false. |
cmsUseMetadata | If set, the resulting template will include the standard MetaData component. Defaults to false. |
cmsSuppressModel | If set, no model will be created for the resulting template. Defaults to false. |
cmsSuppressFolder | If set (or if cmsSuppressModel is set), no content folder will be created for the resulting model. Defaults to false. |
-
Requires update to DXM Component Library, by installing dxm-cl-patch-for-sdk-latest.xml.
-
Requires .env file located in root of the React project to be scaffolded. Values required within .env file are:
Key | Description |
---|---|
CMS_INSTANCE | DXM Instance Name. |
CMS_USERNAME | DXM Username with access to create Assets, Models, Templates, etc. |
CMS_PASSWORD | Pretty obvious. |
CMS_API_KEY | DXM Developer API Key - Can be obtained by contacting Crownpeak Support. |
CMS_SITE_ROOT | DXM Site Root Asset Id. |
CMS_PROJECT | DXM Project Asset Id. |
CMS_WORKFLOW | DXM Workflow Id (to be applied to created Models). |
CMS_SERVER | (Optional) Allows base Crownpeak DXM URL to be overridden. |
CMS_SCAFFOLD_IGNORE | (Optional) One or more paths to ignore during scaffolding, separated by commas. Paths are resolved relative to the application root. |
# Crownpeak DXM Configuration
CMS_INSTANCE={Replace with CMS Instance Name}
CMS_USERNAME={Replace with CMS Username}
CMS_PASSWORD={Replace with CMS Password}
CMS_API_KEY={Replace with CMS Developer API Key}
CMS_SITE_ROOT={Replace with Asset Id of Site Root}
CMS_PROJECT={Replace with Asset Id of Project}
CMS_WORKFLOW={Replace with Workflow Id}
CMS_STATIC_CONTENT_LOCATION=/content/json
CMS_DYNAMIC_CONTENT_LOCATION=//searchg2.crownpeak.net/{Replace with Search G2 Collection Name}/select/?wt=json
CMS_SCAFFOLD_IGNORE=build,.cache
Installation instructions:
- Create new DXM Site Root and check "Install Component Project using Component Library 2.2"; or
$ yarn crownpeak init --folder <parent-folder-id> --name "New Site Name"
-
After site creation, set the values for
CMS_SITE_ROOT
andCMS_PROJECT
in your.env
file to be the relevant asset IDs from DXM; -
Install the manifest (detailed above);
$ yarn crownpeak patch
- Verify that all your settings are correct.
$ yarn crownpeak scaffold --verify
From the root of the project to be React scaffolded:
$ yarn crownpeak scaffold
yarn run v1.22.4
$ ../../sdk/crownpeak scaffold
Uploaded [holder.min.js] as [/Skunks Works/React SDK/_Assets/js/holder.min.js] (261402)
Unable to find source file [/Users/paul.taylor/Documents/Repos/Crownpeak/DXM-React-SDK/examples/bootstrap/js/bundle.js] for upload
Uploaded [blog.css] as [/Skunks Works/React SDK/_Assets/css/blog.css] (261400)
Saved wrapper [Blog] as [/Skunks Works/React SDK/Component Project/Component Library/Nav Wrapper Definitions/Blog Wrapper] (261771)
Saved component [BlogPost] as [/Skunks Works/React SDK/Component Project/Component Library/Component Definitions/Blog Post] (261776)
Saved component [FeaturedPost] as [/Skunks Works/React SDK/Component Project/Component Library/Component Definitions/Featured Post] (261777)
Saved component [Footer] as [/Skunks Works/React SDK/Component Project/Component Library/Component Definitions/Footer] (261778)
Saved component [Header] as [/Skunks Works/React SDK/Component Project/Component Library/Component Definitions/Header] (261779)
Saved component [PostArchives] as [/Skunks Works/React SDK/Component Project/Component Library/Component Definitions/Post Archives] (261780)
Saved component [SecondaryPost] as [/Skunks Works/React SDK/Component Project/Component Library/Component Definitions/Secondary Post] (261781)
Saved component [TopicList] as [/Skunks Works/React SDK/Component Project/Component Library/Component Definitions/Topic List] (261782)
Saved template [BlogPage] as [/Skunks Works/React SDK/Component Project/Component Library/Template Definitions/Blog Page Template] (261370)
Saved model [BlogPage] as [/Skunks Works/React SDK/Component Project/Models/Blog Page Folder/Blog Page] (261784)
Saved content folder [Blog Pages] as [/Skunks Works/React SDK/Blog Pages/] (261376)
✨ Done in 62.61s.
The scaffolding can be run multiple times as additional capabilities are added to the React project. Asset data within DXM will not be destroyed by future runs.
The crownpeak scaffold
script supports a number of optional command-line parameters:
Parameter | Effect |
---|---|
--dry-run |
Report on the items that would be imported into the CMS, but do not import them. |
--verbose |
Show verbose output where applicable. |
--verify |
Verify that the Crownpeak DXM environment is configured correctly. |
--no-components |
Do not import any components. |
--no-pages |
Do not import any pages, templates, or models. |
--no-uploads |
Do not import any uploads; for example CSS, JavaScript or images. |
--no-wrappers |
Do not import any wrappers. |
--only <name> |
Only import items matching the specified name. Can be used multiple times. |
--ignore <name> |
Ignore a single unmet dependency with the specified name. Can be used multiple times. |
These are intended to improve performance for multiple runs, and you should expect to see errors if the items being skipped have not already been created within the CMS; for example, if you provide the --no--components
parameter where the components have not previously been imported.
[
{
"path": "/",
"exact": true,
"component": "BlogPage",
"cmsassetid": "261377"
},
{
"path": "/posts/months/:month",
"component": "BlogPage",
"cmsassetid": "23456"
}
]
Walk through of creating Crownpeak's Demo Site (Procam) from scratch, starting with an empty folder.
Thanks to:
- Richard Lund for the refactoring;
- Paul Taylor for a few edits ;)
- Brad Thompson for more contributions
MIT License
Copyright (c) 2022 Crownpeak Technology, inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.