refactor: Cleanup (#21)
* refactor(routes): remove unused sitemap route * style(comments): enforce consistent comment style * style(exports): enforce consistent export style * chore: Add jsDoc for contentful service * chore: Add jsDoc for contentful service \n \n closes #23 * refactor(app): move query parameter comment to right position and mention comment route middleware * test(npm): add temporary test script * refactor(middlewares): split up bootstrap middleware - fixes #23759 * refactor(cookies): use constances to give context the maxAge cookie setting * refactor(variables): use more descriptive names for variables * space became spaceId when it was just the id not the full space instance * all (access) token variable name variants became [api-type]Token * all clients are now called deliveryClient or previewClient * cpa and cda only remain when they are used as actual API id * env variables names adjusted * perf(helpers): only run marked when content is not empty * refactor(comments): fix typos * refactor(comments): add hint why error is logged to console in settings * chore: Add comments to routes and services * refactor(requires): order and group requires * fix(settings): add validation for wrong preview token * chore: Add comments to routes * chore: fix typo * chore: console.error -> throw * chore: move cookie name to a constant * chore: Fix app.js comment * typo: removing t * chore: typos * chore: removed unnecessary comment line sign * chore: newline for readabillity * chore: remove dangling `t` * chore: remove `t`, add `l` * chore: typos * Fleshed out title * build(npm): remove unused dependencies * build(npm): upgrade to latest stable contentful sdk * chore: Addressing David feedbak * fix(credentials): update to match the new space * chore: Addressing code review comments * chore: Addressing code review comments * chore: res -> response, req-> request * chore: include exactly what we need * chore: Address JPs comments * chore: Address JPs comments * chore: Address Fredericks comments * chore: Address Fredericks comments * fixup! chore: Address Fredericks comments * fixup! fixup! chore: Address Fredericks comments * fixup! fixup! fixup! chore: Address Fredericks comments * fixup! fixup! fixup! fixup! chore: Address Fredericks comments * fixup! fixup! fixup! fixup! fixup! chore: Address Fredericks comments * fixup! fixup! fixup! fixup! fixup! fixup! chore: Address Fredericks comments
This commit is contained in:
14
.eslintrc.js
14
.eslintrc.js
@@ -4,5 +4,15 @@ module.exports = {
|
|||||||
'plugins': [
|
'plugins': [
|
||||||
'standard',
|
'standard',
|
||||||
'promise'
|
'promise'
|
||||||
]
|
],
|
||||||
}
|
'env': {
|
||||||
|
'node': true
|
||||||
|
},
|
||||||
|
'rules': {
|
||||||
|
"capitalized-comments": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"spaced-comment": ["error", "always"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# the-example-app.js
|
# The Node.js example app
|
||||||
The Contentful example app, written in JS
|
The Contentful example app, written in node.js.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -30,6 +30,6 @@ Open http://localhost:3000/?enable_editorial_features in your browser.
|
|||||||
The following deep links are supported:
|
The following deep links are supported:
|
||||||
|
|
||||||
* `?enable_editorial_features` - Shows `Edit in web app` button on every content type plus `Draft` and `Pending Changes` status pills
|
* `?enable_editorial_features` - Shows `Edit in web app` button on every content type plus `Draft` and `Pending Changes` status pills
|
||||||
* `?space_id=xxx&delivery_access_token=xxx&preview_access_token=xxx` - Configure the connected space
|
* `?space_id=xxx&delivery_token=xxx&preview_token=xxx` - Configure the connected space
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
123
app.js
123
app.js
@@ -1,19 +1,25 @@
|
|||||||
require('dotenv').config({ path: 'variables.env' })
|
|
||||||
const express = require('express')
|
|
||||||
const querystring = require('querystring')
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const helpers = require('./helpers')
|
|
||||||
const logger = require('morgan')
|
|
||||||
const cookieParser = require('cookie-parser')
|
|
||||||
const bodyParser = require('body-parser')
|
const bodyParser = require('body-parser')
|
||||||
|
const cookieParser = require('cookie-parser')
|
||||||
|
const express = require('express')
|
||||||
|
const logger = require('morgan')
|
||||||
|
const querystring = require('querystring')
|
||||||
|
|
||||||
const routes = require('./routes/index')
|
// Load environment variables using dotenv
|
||||||
|
require('dotenv').config({ path: 'variables.env' })
|
||||||
|
|
||||||
const { initClient, getSpace } = require('./services/contentful')
|
const helpers = require('./helpers')
|
||||||
const breadcrumb = require('./lib/breadcrumb')
|
const breadcrumb = require('./lib/breadcrumb')
|
||||||
|
const routes = require('./routes/index')
|
||||||
|
const { initClients, getSpace } = require('./services/contentful')
|
||||||
|
const { updateCookie } = require('./lib/cookies')
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
// view engine setup
|
const SETTINGS_NAME = 'theExampleAppSettings'
|
||||||
|
|
||||||
|
// View engine setup
|
||||||
app.set('views', path.join(__dirname, 'views'))
|
app.set('views', path.join(__dirname, 'views'))
|
||||||
app.set('view engine', 'pug')
|
app.set('view engine', 'pug')
|
||||||
|
|
||||||
@@ -24,43 +30,46 @@ app.use(cookieParser())
|
|||||||
app.use(express.static(path.join(__dirname, 'public')))
|
app.use(express.static(path.join(__dirname, 'public')))
|
||||||
app.use(breadcrumb())
|
app.use(breadcrumb())
|
||||||
|
|
||||||
// Pass our application state and custom helpers to all our templates
|
// Set our application state based on environment variables or query parameters
|
||||||
app.use(async function (req, res, next) {
|
app.use(async function (request, response, next) {
|
||||||
// Allow setting of API credentials via query parameters
|
// Set default settings based on environment variables
|
||||||
let settings = {
|
let settings = {
|
||||||
space: process.env.CF_SPACE,
|
spaceId: process.env.CONTENTFUL_SPACE_ID,
|
||||||
cda: process.env.CF_ACCESS_TOKEN,
|
deliveryToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
|
||||||
cpa: process.env.CF_PREVIEW_ACCESS_TOKEN,
|
previewToken: process.env.CONTENTFUL_PREVIEW_TOKEN,
|
||||||
editorialFeatures: false,
|
editorialFeatures: false,
|
||||||
...req.cookies.theExampleAppSettings
|
// Overwrite default settings using those stored in a cookie
|
||||||
|
...request.cookies.theExampleAppSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
const { space_id, preview_access_token, delivery_access_token } = req.query
|
// Allow setting of API credentials via query parameters
|
||||||
if (space_id && preview_access_token && delivery_access_token) { // eslint-disable-line camelcase
|
const { space_id, preview_token, delivery_token } = request.query
|
||||||
|
if (space_id && preview_token && delivery_token) { // eslint-disable-line camelcase
|
||||||
settings = {
|
settings = {
|
||||||
...settings,
|
...settings,
|
||||||
space: space_id,
|
spaceId: space_id,
|
||||||
cda: delivery_access_token,
|
deliveryToken: delivery_token,
|
||||||
cpa: preview_access_token
|
previewToken: preview_token
|
||||||
}
|
}
|
||||||
res.cookie('theExampleAppSettings', settings, { maxAge: 31536000, httpOnly: true })
|
updateCookie(response, SETTINGS_NAME, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow enabling of editorial features via query parameters
|
// Allow enabling of editorial features via query parameters
|
||||||
const { enable_editorial_features } = req.query
|
const { enable_editorial_features } = request.query
|
||||||
if (enable_editorial_features !== undefined) { // eslint-disable-line camelcase
|
if (enable_editorial_features !== undefined) { // eslint-disable-line camelcase
|
||||||
delete req.query.enable_editorial_features
|
delete request.query.enable_editorial_features
|
||||||
settings = {
|
settings.editorialFeatures = true
|
||||||
...settings,
|
updateCookie(response, SETTINGS_NAME, settings)
|
||||||
editorialFeatures: true
|
|
||||||
}
|
|
||||||
res.cookie('theExampleAppSettings', settings, { maxAge: 31536000, httpOnly: true })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initClient(settings)
|
initClients(settings)
|
||||||
res.locals.settings = settings
|
response.locals.settings = settings
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
// Manage language and API type state and make it globally available
|
// Make data available for our views to consume
|
||||||
|
app.use(async function (request, response, next) {
|
||||||
|
// Set active api based on query parameter
|
||||||
const apis = [
|
const apis = [
|
||||||
{
|
{
|
||||||
id: 'cda',
|
id: 'cda',
|
||||||
@@ -72,54 +81,56 @@ app.use(async function (req, res, next) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
res.locals.currentApi = apis
|
response.locals.currentApi = apis
|
||||||
.find((api) => api.id === (req.query.api || 'cda'))
|
.find((api) => api.id === (request.query.api || 'cda'))
|
||||||
|
|
||||||
// Get enabled locales from Contentful
|
// Get enabled locales from Contentful
|
||||||
const space = await getSpace()
|
const space = await getSpace()
|
||||||
res.locals.locales = space.locales
|
response.locals.locales = space.locales
|
||||||
|
|
||||||
const defaultLocale = res.locals.locales
|
const defaultLocale = response.locals.locales
|
||||||
.find((locale) => locale.default)
|
.find((locale) => locale.default)
|
||||||
|
|
||||||
if (req.query.locale) {
|
if (request.query.locale) {
|
||||||
res.locals.currentLocale = space.locales
|
response.locals.currentLocale = space.locales
|
||||||
.find((locale) => locale.code === req.query.locale)
|
.find((locale) => locale.code === request.query.locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.locals.currentLocale) {
|
if (!response.locals.currentLocale) {
|
||||||
res.locals.currentLocale = defaultLocale
|
response.locals.currentLocale = defaultLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject custom helpers
|
// Inject custom helpers
|
||||||
res.locals.helpers = helpers
|
response.locals.helpers = helpers
|
||||||
|
|
||||||
// Make query string available in templates
|
// Make query string available in templates to render links properly
|
||||||
const qs = querystring.stringify(req.query)
|
const qs = querystring.stringify(request.query)
|
||||||
res.locals.queryString = qs ? `?${qs}` : ''
|
response.locals.queryString = qs ? `?${qs}` : ''
|
||||||
res.locals.query = req.query
|
response.locals.query = request.query
|
||||||
res.locals.currentPath = req.path
|
response.locals.currentPath = request.path
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Initialize the route handling
|
||||||
|
// Check ./routes/index.js to get a list of all implemented routes
|
||||||
app.use('/', routes)
|
app.use('/', routes)
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// Catch 404 and forward to error handler
|
||||||
app.use(function (req, res, next) {
|
app.use(function (request, response, next) {
|
||||||
var err = new Error('Not Found')
|
var err = new Error('Not Found')
|
||||||
err.status = 404
|
err.status = 404
|
||||||
next(err)
|
next(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
// error handler
|
// Error handler
|
||||||
app.use(function (err, req, res, next) {
|
app.use(function (err, request, response, next) {
|
||||||
// set locals, only providing error in development
|
// Set locals, only providing error in development
|
||||||
res.locals.error = req.app.get('env') === 'development' ? err : {}
|
response.locals.error = request.app.get('env') === 'development' ? err : {}
|
||||||
|
|
||||||
// render the error page
|
// Render the error page
|
||||||
res.status(err.status || 500)
|
response.status(err.status || 500)
|
||||||
res.render('error')
|
response.render('error')
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = app
|
module.exports = app
|
||||||
|
|||||||
30
bin/www
30
bin/www
@@ -1,46 +1,42 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module dependencies.
|
* Module dependencies
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const app = require('../app')
|
const app = require('../app')
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
/**
|
|
||||||
* Get port from environment and store in Express.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get port from environment and store in Express
|
||||||
|
*/
|
||||||
const port = normalizePort(process.env.PORT || '3000')
|
const port = normalizePort(process.env.PORT || '3000')
|
||||||
app.set('port', port)
|
app.set('port', port)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create HTTP server.
|
* Create HTTP server
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const server = http.createServer(app)
|
const server = http.createServer(app)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen on provided port, on all network interfaces.
|
* Listen on provided port, on all network interfaces
|
||||||
*/
|
*/
|
||||||
|
|
||||||
server.listen(port)
|
server.listen(port)
|
||||||
server.on('error', onError)
|
server.on('error', onError)
|
||||||
server.on('listening', onListening)
|
server.on('listening', onListening)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a port into a number, string, or false.
|
* Normalize a port into a number, string, or false
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function normalizePort (val) {
|
function normalizePort (val) {
|
||||||
const port = parseInt(val, 10)
|
const port = parseInt(val, 10)
|
||||||
|
|
||||||
if (isNaN(port)) {
|
if (isNaN(port)) {
|
||||||
// named pipe
|
// Named pipe
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
if (port >= 0) {
|
if (port >= 0) {
|
||||||
// port number
|
// Port number
|
||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +44,8 @@ function normalizePort (val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event listener for HTTP server "error" event.
|
* Event listener for HTTP server "error" event
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function onError (error) {
|
function onError (error) {
|
||||||
if (error.syscall !== 'listen') {
|
if (error.syscall !== 'listen') {
|
||||||
throw error
|
throw error
|
||||||
@@ -60,7 +55,7 @@ function onError (error) {
|
|||||||
? 'Pipe ' + port
|
? 'Pipe ' + port
|
||||||
: 'Port ' + port
|
: 'Port ' + port
|
||||||
|
|
||||||
// handle specific listen errors with friendly messages
|
// Handle specific listen errors with friendly messages
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case 'EACCES':
|
case 'EACCES':
|
||||||
console.error(bind + ' requires elevated privileges')
|
console.error(bind + ' requires elevated privileges')
|
||||||
@@ -76,9 +71,8 @@ function onError (error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event listener for HTTP server "listening" event.
|
* Event listener for HTTP server "listening" event
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function onListening () {
|
function onListening () {
|
||||||
const addr = server.address()
|
const addr = server.address()
|
||||||
const bind = typeof addr === 'string'
|
const bind = typeof addr === 'string'
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
/*
|
/**
|
||||||
Catch Errors Handler
|
* Catch Errors Handler
|
||||||
With async/await, you need some way to catch errors.
|
* Instead of using try{} catch(e) {} in each controller, we wrap the function in
|
||||||
Instead of using try{} catch(e) {} in each controller, we wrap the function in
|
* catchErrors(), catch any errors they throw, and pass it along to our express middleware with next().
|
||||||
catchErrors(), catch any errors they throw, and pass it along to our express middleware with next().
|
*/
|
||||||
*/
|
|
||||||
|
|
||||||
exports.catchErrors = (fn) => {
|
module.exports.catchErrors = (fn) => {
|
||||||
return function (req, res, next) {
|
return function (request, response, next) {
|
||||||
return fn(req, res, next).catch((e) => {
|
return fn(request, response, next).catch((e) => {
|
||||||
next(e)
|
next(e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
31
helpers.js
31
helpers.js
@@ -1,23 +1,28 @@
|
|||||||
const marked = require('marked')
|
const marked = require('marked')
|
||||||
|
|
||||||
// Parse markdown text
|
// Parse markdown text
|
||||||
exports.markdown = (content) => {
|
module.exports.markdown = (content = '') => {
|
||||||
content = content || ''
|
if (!content.trim()) {
|
||||||
return marked(removeIvalidDataURL(content), {sanitize: true})
|
return ''
|
||||||
|
}
|
||||||
|
return marked(removeInvalidDataURL(content), {sanitize: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump is a handy debugging function we can use to sort of "console.log" our data
|
// A handy debugging function we can use to sort of "console.log" our data
|
||||||
exports.dump = (obj) => JSON.stringify(obj, null, 2)
|
module.exports.dump = (obj) => JSON.stringify(obj, null, 2)
|
||||||
|
|
||||||
// Evil users might try to add base64 url data to execute js
|
module.exports.formatMetaTitle = (title) => {
|
||||||
// so we should take care of that
|
|
||||||
function removeIvalidDataURL (content) {
|
|
||||||
let regex = /data:\S+;base64\S*/gm
|
|
||||||
return content.replace(regex, '#')
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.formatMetaTitle = (title) => {
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return 'The Example App'
|
return 'The Example App'
|
||||||
}
|
}
|
||||||
return `${title.charAt(0).toUpperCase()}${title.slice(1)} — The Example App`
|
return `${title.charAt(0).toUpperCase()}${title.slice(1)} — The Example App`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evil users might try to add base64 url data to execute js code
|
||||||
|
* so we should purge any potentially harmful data to mitigate risk
|
||||||
|
*/
|
||||||
|
function removeInvalidDataURL (content) {
|
||||||
|
let regex = /data:\S+;base64\S*/gm
|
||||||
|
return content.replace(regex, '#')
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
const url = require('url')
|
const url = require('url')
|
||||||
|
|
||||||
module.exports = (modifier) => {
|
module.exports = (modifier) => {
|
||||||
return (req, res, next) => {
|
return (request, response, next) => {
|
||||||
const baseUrl = url.format({ protocol: req.protocol, host: req.get('host') })
|
const baseUrl = url.format({ protocol: request.protocol, host: request.get('host') })
|
||||||
const parts = url.parse(req.url).pathname.split('/').filter(Boolean)
|
const parts = url.parse(request.url).pathname.split('/').filter(Boolean)
|
||||||
let items = []
|
let items = []
|
||||||
|
|
||||||
items.push({ label: 'Home', url: baseUrl })
|
items.push({ label: 'Home', url: baseUrl })
|
||||||
@@ -19,9 +19,8 @@ module.exports = (modifier) => {
|
|||||||
if (modifier) {
|
if (modifier) {
|
||||||
items = items.map(modifier)
|
items = items.map(modifier)
|
||||||
}
|
}
|
||||||
// make it global
|
// Make it global
|
||||||
req.app.locals.breadcrumb = items
|
request.app.locals.breadcrumb = items
|
||||||
// next operation
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
lib/cookies.js
Normal file
4
lib/cookies.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const ONE_YEAR_IN_SECONDS = 31536000
|
||||||
|
module.exports.updateCookie = (response, cookieName, value) => {
|
||||||
|
response.cookie(cookieName, value, { maxAge: ONE_YEAR_IN_SECONDS, httpOnly: true })
|
||||||
|
}
|
||||||
10
package.json
10
package.json
@@ -7,6 +7,7 @@
|
|||||||
"start": "node ./bin/www",
|
"start": "node ./bin/www",
|
||||||
"lint": "eslint ./app.js routes",
|
"lint": "eslint ./app.js routes",
|
||||||
"format": "eslint --fix . bin --ignore public node_modules",
|
"format": "eslint --fix . bin --ignore public node_modules",
|
||||||
|
"test": "echo 'test'",
|
||||||
"test:integration": "jest test/integration",
|
"test:integration": "jest test/integration",
|
||||||
"test:integration:watch": "jest test/integration --watch",
|
"test:integration:watch": "jest test/integration --watch",
|
||||||
"test:unit": "jest test/unit",
|
"test:unit": "jest test/unit",
|
||||||
@@ -17,16 +18,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "~1.15.2",
|
"body-parser": "~1.15.2",
|
||||||
"contentful": "^5.0.0-rc3",
|
"contentful": "^5.0.2",
|
||||||
"cookie-parser": "~1.4.3",
|
"cookie-parser": "~1.4.3",
|
||||||
"debug": "~2.2.0",
|
|
||||||
"dotenv": "^4.0.0",
|
"dotenv": "^4.0.0",
|
||||||
"express": "~4.14.0",
|
"express": "~4.14.0",
|
||||||
"jstransformer-markdown-it": "^2.0.0",
|
|
||||||
"marked": "^0.3.6",
|
"marked": "^0.3.6",
|
||||||
"morgan": "~1.7.0",
|
"morgan": "~1.7.0",
|
||||||
"pug": "~2.0.0-beta6",
|
"pug": "~2.0.0-beta6"
|
||||||
"safe-json-stringify": "^1.0.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cheerio": "^1.0.0-rc.2",
|
"cheerio": "^1.0.0-rc.2",
|
||||||
@@ -37,8 +35,6 @@
|
|||||||
"eslint-plugin-standard": "^2.0.1",
|
"eslint-plugin-standard": "^2.0.1",
|
||||||
"jest": "^21.2.1",
|
"jest": "^21.2.1",
|
||||||
"nodemon": "^1.12.1",
|
"nodemon": "^1.12.1",
|
||||||
"pug-lint": "^2.5.0",
|
|
||||||
"superagent": "^3.6.3",
|
|
||||||
"supertest": "^3.0.0"
|
"supertest": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
/* GET category listing. */
|
/*
|
||||||
exports.getCategories = async (req, res, next) => {
|
* The purpose of this module is to render the category page when the route is requested
|
||||||
res.render('categories', { title: 'Categories' })
|
*/
|
||||||
|
|
||||||
|
// Renders categories page when `/categories` route is requested
|
||||||
|
module.exports.getCategories = async (request, response, next) => {
|
||||||
|
response.render('categories', { title: 'Categories' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +1,124 @@
|
|||||||
const {getCourses, getCourse, getCategories, getCoursesByCategory} = require('./../services/contentful')
|
/*
|
||||||
const attachEntryState = require('./../lib/entry-state')
|
* The purpose of this module is to render the category page when the route is requested
|
||||||
|
*/
|
||||||
|
|
||||||
exports.getCourses = async (req, res, next) => {
|
const {
|
||||||
// We get all the entries with the content type `course`
|
getCourses,
|
||||||
|
getCourse,
|
||||||
|
getCategories,
|
||||||
|
getCoursesByCategory
|
||||||
|
} = require('./../services/contentful')
|
||||||
|
|
||||||
|
const attachEntryState = require('../lib/entry-state')
|
||||||
|
const { updateCookie } = require('../lib/cookies')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders courses list when `/courses` route is requested
|
||||||
|
*
|
||||||
|
* @param request - Object - Express request object
|
||||||
|
* @param response - Object - Express response object
|
||||||
|
* @param next - Function - express callback
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.getCourses = async (request, response, next) => {
|
||||||
|
// Get all the entries of content type course
|
||||||
let courses = []
|
let courses = []
|
||||||
let categories = []
|
let categories = []
|
||||||
courses = await getCourses(res.locals.currentLocale.code, res.locals.currentApi.id)
|
courses = await getCourses(response.locals.currentLocale.code, response.locals.currentApi.id)
|
||||||
|
|
||||||
// Attach entry state flags when using preview API
|
// Attach entry state flags when using preview API
|
||||||
if (res.locals.settings.editorialFeatures && res.locals.currentApi.id === 'cpa') {
|
if (response.locals.settings.editorialFeatures && response.locals.currentApi.id === 'cpa') {
|
||||||
courses = await Promise.all(courses.map(attachEntryState))
|
courses = await Promise.all(courses.map(attachEntryState))
|
||||||
}
|
}
|
||||||
|
|
||||||
categories = await getCategories(res.locals.currentLocale.code, res.locals.currentApi.id)
|
categories = await getCategories(response.locals.currentLocale.code, response.locals.currentApi.id)
|
||||||
res.render('courses', { title: `All Courses (${courses.length})`, categories, courses })
|
response.render('courses', { title: `All Courses (${courses.length})`, categories, courses })
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCourse = async (req, res, next) => {
|
/**
|
||||||
let course = await getCourse(req.params.slug, res.locals.currentLocale.code, res.locals.currentApi.id)
|
* Renders a course when `/courses/:slug` route is requested
|
||||||
|
*
|
||||||
|
* @param request - Object - Express request object
|
||||||
|
* @param response - Object - Express response object
|
||||||
|
* @param next - Function - express callback
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.getCourse = async (request, response, next) => {
|
||||||
|
let course = await getCourse(request.params.slug, response.locals.currentLocale.code, response.locals.currentApi.id)
|
||||||
|
|
||||||
// Get lessons
|
// Get lessons
|
||||||
const lessons = course.fields.lessons
|
const lessons = course.fields.lessons
|
||||||
const lessonIndex = lessons.findIndex((lesson) => lesson.fields.slug === req.params.lslug)
|
let {lesson, lessonIndex} = getNextLesson(lessons, request.params.lslug)
|
||||||
const lesson = lessons[lessonIndex]
|
|
||||||
|
|
||||||
// Save visited lessons
|
// Manage state of viewed lessons
|
||||||
const cookie = req.cookies.visitedLessons
|
const cookie = request.cookies.visitedLessons
|
||||||
let visitedLessons = cookie || []
|
let visitedLessons = cookie || []
|
||||||
visitedLessons.push(course.sys.id)
|
visitedLessons.push(course.sys.id)
|
||||||
visitedLessons = [...new Set(visitedLessons)]
|
visitedLessons = [...new Set(visitedLessons)]
|
||||||
res.cookie('visitedLessons', visitedLessons, { maxAge: 900000, httpOnly: true })
|
updateCookie(response, 'visitedLessons', visitedLessons)
|
||||||
|
|
||||||
// Attach entry state flags when using preview API
|
// Attach entry state flags when using preview API
|
||||||
if (res.locals.settings.editorialFeatures && res.locals.currentApi.id === 'cpa') {
|
if (response.locals.settings.editorialFeatures && response.locals.currentApi.id === 'cpa') {
|
||||||
course = await attachEntryState(course)
|
course = await attachEntryState(course)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('course', {title: course.fields.title, course, lesson, lessons, lessonIndex, visitedLessons})
|
response.render('course', {title: course.fields.title, course, lesson, lessons, lessonIndex, visitedLessons})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getCoursesByCategory = async (req, res, next) => {
|
/**
|
||||||
|
* Renders a courses list by a category when `/courses/category/:category` route is requested
|
||||||
|
*
|
||||||
|
* @param request - Object - Express request object
|
||||||
|
* @param response - Object - Express response object
|
||||||
|
* @param next - Function - Express callback
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.getCoursesByCategory = async (request, response, next) => {
|
||||||
// We get all the entries with the content type `course` filtered by a category
|
// We get all the entries with the content type `course` filtered by a category
|
||||||
let courses = []
|
let courses = []
|
||||||
let categories = []
|
let categories = []
|
||||||
let activeCategory = ''
|
let activeCategory = ''
|
||||||
try {
|
try {
|
||||||
categories = await getCategories()
|
categories = await getCategories()
|
||||||
activeCategory = categories.find((category) => category.fields.slug === req.params.category)
|
activeCategory = categories.find((category) => category.fields.slug === request.params.category)
|
||||||
courses = await getCoursesByCategory(activeCategory.sys.id, res.locals.currentLocale.code, res.locals.currentApi.id)
|
courses = await getCoursesByCategory(activeCategory.sys.id, response.locals.currentLocale.code, response.locals.currentApi.id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Error ', e)
|
console.log('Error ', e)
|
||||||
}
|
}
|
||||||
res.render('courses', { title: `${activeCategory.fields.title} (${courses.length})`, categories, courses })
|
response.render('courses', { title: `${activeCategory.fields.title} (${courses.length})`, categories, courses })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GET course lesson detail. */
|
/**
|
||||||
exports.getLesson = async (req, res, next) => {
|
* Renders a lesson details when `/courses/:courseSlug/lessons/:lessonSlug` route is requested
|
||||||
let course = await getCourse(req.params.cslug, res.locals.currentLocale.code, res.locals.currentApi.id)
|
*
|
||||||
|
* @param request - Object - Express request object
|
||||||
|
* @param response - Object - Express response object
|
||||||
|
* @param next - Function - express callback
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.getLesson = async (request, response, next) => {
|
||||||
|
let course = await getCourse(request.params.cslug, response.locals.currentLocale.code, response.locals.currentApi.id)
|
||||||
|
|
||||||
const lessons = course.fields.lessons
|
const lessons = course.fields.lessons
|
||||||
const lessonIndex = lessons.findIndex((lesson) => lesson.fields.slug === req.params.lslug)
|
let {lesson, nextLesson} = getNextLesson(lessons, request.params.lslug)
|
||||||
let lesson = lessons[lessonIndex]
|
|
||||||
const nextLesson = lessons[lessonIndex + 1] || null
|
|
||||||
|
|
||||||
// Save visited lessons
|
// Save visited lessons
|
||||||
const cookie = req.cookies.visitedLessons
|
const cookie = request.cookies.visitedLessons
|
||||||
let visitedLessons = cookie || []
|
let visitedLessons = cookie || []
|
||||||
visitedLessons.push(lesson.sys.id)
|
visitedLessons.push(lesson.sys.id)
|
||||||
visitedLessons = [...new Set(visitedLessons)]
|
visitedLessons = [...new Set(visitedLessons)]
|
||||||
res.cookie('visitedLessons', visitedLessons, { maxAge: 900000, httpOnly: true })
|
updateCookie(response, 'visitedLessons', visitedLessons)
|
||||||
|
|
||||||
// Attach entry state flags when using preview API
|
// Attach entry state flags when using preview API
|
||||||
if (res.locals.settings.editorialFeatures && res.locals.currentApi.id === 'cpa') {
|
if (response.locals.settings.editorialFeatures && response.locals.currentApi.id === 'cpa') {
|
||||||
lesson = await attachEntryState(lesson)
|
lesson = await attachEntryState(lesson)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('course', {
|
response.render('course', {
|
||||||
title: `${course.fields.title} | ${lesson.fields.title}`,
|
title: `${course.fields.title} | ${lesson.fields.title}`,
|
||||||
course,
|
course,
|
||||||
lesson,
|
lesson,
|
||||||
@@ -84,3 +128,14 @@ exports.getLesson = async (req, res, next) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNextLesson (lessons, lslug) {
|
||||||
|
const lessonIndex = lessons.findIndex((lesson) => lesson.fields.slug === lslug)
|
||||||
|
let lesson = lessons[lessonIndex]
|
||||||
|
const nextLesson = lessons[lessonIndex + 1] || null
|
||||||
|
|
||||||
|
return {
|
||||||
|
lessonIndex,
|
||||||
|
lesson,
|
||||||
|
nextLesson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
exports.getImprint = (req, res, next) => {
|
/**
|
||||||
res.render('imprint', { title: 'Imprint' })
|
* Renders imprint page when `/imprint` route is requested
|
||||||
|
* @param request - Object - Express request
|
||||||
|
* @param response - Object - Express response
|
||||||
|
* @param next - Function - Express callback
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.getImprint = (request, response, next) => {
|
||||||
|
response.render('imprint', { title: 'Imprint' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* This module connects rendering modules to routes
|
||||||
|
*/
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const { catchErrors } = require('../handlers/errorHandlers')
|
|
||||||
const { getCourses, getCourse, getLesson, getCoursesByCategory } = require('./courses')
|
|
||||||
const { getSettings, postSettings } = require('./settings')
|
|
||||||
const { getSitemap } = require('./sitemap')
|
|
||||||
const { getLandingPage } = require('./landingPage')
|
|
||||||
const { getImprint } = require('./imprint')
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
/* GET the home landing page. */
|
const { catchErrors } = require('../handlers/errorHandlers')
|
||||||
|
|
||||||
|
const { getCourses, getCourse, getLesson, getCoursesByCategory } = require('./courses')
|
||||||
|
const { getSettings, postSettings } = require('./settings')
|
||||||
|
const { getLandingPage } = require('./landingPage')
|
||||||
|
const { getImprint } = require('./imprint')
|
||||||
|
|
||||||
|
// GET the home landing page
|
||||||
router.get('/', catchErrors(getLandingPage))
|
router.get('/', catchErrors(getLandingPage))
|
||||||
|
|
||||||
/* Courses Routes */
|
// Courses routes
|
||||||
router.get('/courses', catchErrors(getCourses))
|
router.get('/courses', catchErrors(getCourses))
|
||||||
router.get('/courses/categories/:category', catchErrors(getCoursesByCategory))
|
router.get('/courses/categories/:category', catchErrors(getCoursesByCategory))
|
||||||
router.get('/courses/:slug', catchErrors(getCourse))
|
router.get('/courses/:slug', catchErrors(getCourse))
|
||||||
router.get('/courses/:slug/lessons', catchErrors(getCourse))
|
router.get('/courses/:slug/lessons', catchErrors(getCourse))
|
||||||
router.get('/courses/:cslug/lessons/:lslug', catchErrors(getLesson))
|
router.get('/courses/:cslug/lessons/:lslug', catchErrors(getLesson))
|
||||||
|
|
||||||
/* Settings Routes */
|
// Settings routes
|
||||||
router.get('/settings', catchErrors(getSettings))
|
router.get('/settings', catchErrors(getSettings))
|
||||||
router.post('/settings', catchErrors(postSettings))
|
router.post('/settings', catchErrors(postSettings))
|
||||||
|
|
||||||
/* Sitemap Route */
|
// Imprint route
|
||||||
router.get('/sitemap', catchErrors(getSitemap))
|
|
||||||
|
|
||||||
/* Imprint Route */
|
|
||||||
router.get('/imprint', catchErrors(getImprint))
|
router.get('/imprint', catchErrors(getImprint))
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
const { getLandingPage } = require('../services/contentful')
|
/**
|
||||||
const attachEntryState = require('./../lib/entry-state')
|
* This module renders a layout when its route is requested
|
||||||
|
* It is used for pages like home page
|
||||||
|
*/
|
||||||
const url = require('url')
|
const url = require('url')
|
||||||
|
|
||||||
exports.getLandingPage = async (req, res, next) => {
|
const { getLandingPage } = require('../services/contentful')
|
||||||
let pathname = url.parse(req.url).pathname.split('/').filter(Boolean)[0]
|
const attachEntryState = require('./../lib/entry-state')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a landing page when `/` route is requested
|
||||||
|
* based on the pathname an entry is queried from contentful
|
||||||
|
* and a view is rendered from the pulled data
|
||||||
|
*
|
||||||
|
* @param request - Object - Express request
|
||||||
|
* @param response - Object - Express response
|
||||||
|
* @param next - Function - Express callback
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.getLandingPage = async (request, response, next) => {
|
||||||
|
let pathname = url.parse(request.url).pathname.split('/').filter(Boolean)[0]
|
||||||
pathname = pathname || 'home'
|
pathname = pathname || 'home'
|
||||||
let landingPage = await getLandingPage(
|
let landingPage = await getLandingPage(
|
||||||
pathname,
|
pathname,
|
||||||
res.locals.currentLocale.code,
|
response.locals.currentLocale.code,
|
||||||
res.locals.currentApi.id
|
response.locals.currentApi.id
|
||||||
)
|
)
|
||||||
|
|
||||||
// Attach entry state flags when using preview APIgs
|
// Attach entry state flags when using preview API
|
||||||
if (res.locals.settings.editorialFeatures && res.locals.currentApi.id === 'cpa') {
|
if (response.locals.settings.editorialFeatures && response.locals.currentApi.id === 'cpa') {
|
||||||
landingPage = await attachEntryState(landingPage)
|
landingPage = await attachEntryState(landingPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('landingPage', { title: pathname, landingPage })
|
response.render('landingPage', { title: pathname, landingPage })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* This module renders the settings page when `settings` route is requested
|
||||||
|
* it also saves the settings to a cookie
|
||||||
|
*/
|
||||||
const { createClient } = require('contentful')
|
const { createClient } = require('contentful')
|
||||||
const { initClient, getSpace } = require('./../services/contentful')
|
const { initClients, getSpace } = require('./../services/contentful')
|
||||||
|
const { updateCookie } = require('../lib/cookies')
|
||||||
|
|
||||||
async function renderSettings (res, opts) {
|
const SETTINGS_NAME = 'theExampleAppSettings'
|
||||||
// Get connectred space to display the space name on top of the settings
|
|
||||||
|
async function renderSettings (response, opts) {
|
||||||
|
// Get connected space to display the space name on top of the settings
|
||||||
let space = false
|
let space = false
|
||||||
try {
|
try {
|
||||||
space = await getSpace()
|
space = await getSpace()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
// We throw the error here, it will be handled by the error middleware
|
||||||
|
// We keep space false to ensure the "Connected to" box is not shown.
|
||||||
|
throw (error)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('settings', {
|
response.render('settings', {
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
errors: {},
|
errors: {},
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
@@ -20,68 +29,85 @@ async function renderSettings (res, opts) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GET settings page. */
|
/**
|
||||||
exports.getSettings = async (req, res, next) => {
|
* Renders the settings page when `/settings` route is requested
|
||||||
const { settings } = res.locals
|
*
|
||||||
await renderSettings(res, {
|
* @param request - Object - Express request
|
||||||
|
* @param response - Object - Express response
|
||||||
|
* @param next - Function - Express callback
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.getSettings = async (request, response, next) => {
|
||||||
|
const { settings } = response.locals
|
||||||
|
await renderSettings(response, {
|
||||||
settings
|
settings
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* POST settings page. */
|
/**
|
||||||
exports.postSettings = async (req, res, next) => {
|
* Save settings when POST request is triggered to the `/settings` route
|
||||||
|
* and render the settings page
|
||||||
|
*
|
||||||
|
* @param request - Object - Express request
|
||||||
|
* @param response - Object - Express response
|
||||||
|
* @param next - Function - Express callback
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.postSettings = async (request, response, next) => {
|
||||||
const errorList = []
|
const errorList = []
|
||||||
const { space, cda, cpa, editorialFeatures } = req.body
|
const { spaceId, deliveryToken, previewToken, editorialFeatures } = request.body
|
||||||
const settings = {
|
const settings = {
|
||||||
space,
|
spaceId,
|
||||||
cda,
|
deliveryToken,
|
||||||
cpa,
|
previewToken,
|
||||||
editorialFeatures: !!editorialFeatures
|
editorialFeatures: !!editorialFeatures
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields.
|
// Validate required fields.
|
||||||
if (!space) {
|
if (!spaceId) {
|
||||||
errorList.push({
|
errorList.push({
|
||||||
field: 'space',
|
field: 'spaceId',
|
||||||
message: 'This field is required'
|
message: 'This field is required'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cda) {
|
if (!deliveryToken) {
|
||||||
errorList.push({
|
errorList.push({
|
||||||
field: 'cda',
|
field: 'deliveryToken',
|
||||||
message: 'This field is required'
|
message: 'This field is required'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cpa) {
|
if (!previewToken) {
|
||||||
errorList.push({
|
errorList.push({
|
||||||
field: 'cpa',
|
field: 'previewToken',
|
||||||
message: 'This field is required'
|
message: 'This field is required'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate space and CDA access token.
|
// Validate space and delivery access token.
|
||||||
if (space && cda) {
|
if (spaceId && deliveryToken) {
|
||||||
try {
|
try {
|
||||||
await createClient({
|
await createClient({
|
||||||
space,
|
space: spaceId,
|
||||||
accessToken: cda
|
accessToken: deliveryToken
|
||||||
}).getSpace()
|
}).getSpace()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.response.status === 401) {
|
if (err.response.status === 401) {
|
||||||
errorList.push({
|
errorList.push({
|
||||||
field: 'cda',
|
field: 'deliveryToken',
|
||||||
message: 'Your Delivery API key is invalid.'
|
message: 'Your Delivery API key is invalid.'
|
||||||
})
|
})
|
||||||
} else if (err.response.status === 404) {
|
} else if (err.response.status === 404) {
|
||||||
errorList.push({
|
errorList.push({
|
||||||
field: 'space',
|
field: 'spaceId',
|
||||||
message: 'This space does not exist or your access token is not associated with your space.'
|
message: 'This space does not exist or your access token is not associated with your space.'
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
errorList.push({
|
errorList.push({
|
||||||
field: 'cda',
|
field: 'deliveryToken',
|
||||||
message: `Something went wrong: ${err.response.data.message}`
|
message: `Something went wrong: ${err.response.data.message}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -89,24 +115,27 @@ exports.postSettings = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate space and CPA access token.
|
// Validate space and CPA access token.
|
||||||
if (space && cpa) {
|
if (spaceId && previewToken) {
|
||||||
try {
|
try {
|
||||||
await createClient({
|
await createClient({
|
||||||
space,
|
space: spaceId,
|
||||||
accessToken: cpa,
|
accessToken: previewToken,
|
||||||
host: 'preview.contentful.com'
|
host: 'preview.contentful.com'
|
||||||
}).getSpace()
|
}).getSpace()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.response.status === 401) {
|
if (err.response.status === 401) {
|
||||||
errorList.push({
|
errorList.push({
|
||||||
field: 'cpa',
|
field: 'previewToken',
|
||||||
message: 'Your Preview API key is invalid.'
|
message: 'Your Preview API key is invalid.'
|
||||||
})
|
})
|
||||||
} else if (err.response.status === 404) {
|
} else if (err.response.status === 404) {
|
||||||
// Already validated via CDA
|
errorList.push({
|
||||||
|
field: 'spaceId',
|
||||||
|
message: 'This space does not exist or your delivery token is not associated with your space.'
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
errorList.push({
|
errorList.push({
|
||||||
field: 'cpa',
|
field: 'previewToken',
|
||||||
message: `Something went wrong: ${err.response.data.message}`
|
message: `Something went wrong: ${err.response.data.message}`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -116,11 +145,11 @@ exports.postSettings = async (req, res, next) => {
|
|||||||
// When no errors occurred
|
// When no errors occurred
|
||||||
if (!errorList.length) {
|
if (!errorList.length) {
|
||||||
// Store new settings
|
// Store new settings
|
||||||
res.cookie('theExampleAppSettings', settings, { maxAge: 31536000, httpOnly: true })
|
updateCookie(response, SETTINGS_NAME, settings)
|
||||||
res.locals.settings = settings
|
response.locals.settings = settings
|
||||||
|
|
||||||
// Reinit clients
|
// Reinit clients
|
||||||
initClient(settings)
|
initClients(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate error dictionary
|
// Generate error dictionary
|
||||||
@@ -135,7 +164,7 @@ exports.postSettings = async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
await renderSettings(res, {
|
await renderSettings(response, {
|
||||||
settings,
|
settings,
|
||||||
errors,
|
errors,
|
||||||
hasErrors: errorList.length > 0,
|
hasErrors: errorList.length > 0,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
/* GET sitemap page. */
|
|
||||||
exports.getSitemap = async (req, res, next) => {
|
|
||||||
res.render('sitemap', { title: 'Sitemap' })
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,107 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* The purpose of this module is to get data from contentful
|
||||||
|
*/
|
||||||
const { createClient } = require('contentful')
|
const { createClient } = require('contentful')
|
||||||
|
|
||||||
let cdaClient = null
|
let deliveryClient = null
|
||||||
let cpaClient = null
|
let previewClient = null
|
||||||
|
|
||||||
// Initialize our client
|
/**
|
||||||
exports.initClient = (options) => {
|
* Initialize the contentful Client
|
||||||
// Getting the version the app version
|
* @param options {space: string, cda: string, cpa: string}
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.initClients = (options) => {
|
||||||
|
// Getting the app version
|
||||||
const { version } = require('../package.json')
|
const { version } = require('../package.json')
|
||||||
|
|
||||||
const config = options || {
|
const config = options || {
|
||||||
space: process.env.CF_SPACE,
|
spaceId: process.env.CF_SPACE_ID,
|
||||||
cda: process.env.CF_ACCESS_TOKEN,
|
deliveryToken: process.env.CF_DELIVERY_TOKEN,
|
||||||
cpa: process.env.CF_PREVIEW_ACCESS_TOKEN
|
previewToken: process.env.CF_PREVIEW_TOKEN
|
||||||
}
|
}
|
||||||
cdaClient = createClient({
|
deliveryClient = createClient({
|
||||||
application: `contentful.the-example-app.node/${version}`,
|
application: `the-example-app.node/${version}`,
|
||||||
space: config.space,
|
space: config.spaceId,
|
||||||
accessToken: config.cda
|
accessToken: config.deliveryToken
|
||||||
})
|
})
|
||||||
cpaClient = createClient({
|
previewClient = createClient({
|
||||||
application: `contentful.the-example-app.node/${version}`,
|
application: `the-example-app.node/${version}`,
|
||||||
space: config.space,
|
space: config.spaceId,
|
||||||
accessToken: config.cpa,
|
accessToken: config.previewToken,
|
||||||
host: 'preview.contentful.com'
|
host: 'preview.contentful.com'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the Space the app is connected to. Used for the settings form and to get all available locales.
|
/**
|
||||||
exports.getSpace = assert((api = `cda`) => {
|
* Get the Space the app is connected to. Used for the settings form and to get all available locales
|
||||||
const client = api === 'cda' ? cdaClient : cpaClient
|
* @param api - string - the api to use, cda or cap. Default: 'cda'
|
||||||
return client.getSpace()
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
module.exports.getSpace = assert((api = `cda`) => {
|
||||||
|
return getClient(api).getSpace()
|
||||||
}, 'Space')
|
}, 'Space')
|
||||||
|
|
||||||
// Get a single entry. Used to detect the `Draft` or `Pending Changes` state.
|
/**
|
||||||
exports.getEntry = assert((entryId, api = `cda`) => {
|
* Gets an entry. Used to detect the `Draft` or `Pending Changes` state
|
||||||
const client = api === 'cda' ? cdaClient : cpaClient
|
* @param entryId - string - the entry id
|
||||||
return client.getEntry(entryId)
|
* @param api - string - the api to use fetching the entry
|
||||||
|
*
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports.getEntry = assert((entryId, api = `cda`) => {
|
||||||
|
return getClient(api).getEntry(entryId)
|
||||||
}, 'Entry')
|
}, 'Entry')
|
||||||
|
|
||||||
// to get all the courses we request all the entries
|
/**
|
||||||
// with the content_type `course` from Contentful
|
* Get all entries with content_type `course`
|
||||||
exports.getCourses = assert((locale = 'en-US', api = `cda`) => {
|
* @param locale - string - the locale of the entry [default: 'en-US']
|
||||||
const client = api === 'cda' ? cdaClient : cpaClient
|
* @param api - string the api enpoint to use when fetching the data
|
||||||
return client.getEntries({
|
* @returns {Array<Object>}
|
||||||
|
*/
|
||||||
|
module.exports.getCourses = assert((locale = 'en-US', api = `cda`) => {
|
||||||
|
return getClient(api).getEntries({
|
||||||
content_type: 'course',
|
content_type: 'course',
|
||||||
locale,
|
locale,
|
||||||
order: 'sys.createdAt',
|
order: 'sys.createdAt', // Ordering the entries by creation date
|
||||||
include: 10
|
include: 6 // We use include param to increase the link level, the include value goes from 1 to 6
|
||||||
})
|
})
|
||||||
.then((response) => response.items)
|
.then((response) => response.items)
|
||||||
}, 'Course')
|
}, 'Course')
|
||||||
|
|
||||||
// Landing pages like the home or about page are fully controlable via Contentful.
|
/**
|
||||||
exports.getLandingPage = (slug, locale = 'en-US', api = `cda`) => {
|
* Get entries of content_type `layout` e.g. Landing page
|
||||||
const client = api === 'cda' ? cdaClient : cpaClient
|
* @param slug - string - the slug of the entry to use in the query
|
||||||
return client.getEntries({
|
* @param locale - string - locale of the entry to request [default: 'en-US']
|
||||||
|
* @param api - string - the api enpoint to use when fetching the data
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
module.exports.getLandingPage = (slug, locale = 'en-US', api = `cda`) => {
|
||||||
|
// Even though we need a single entry, we request it using the collection endpoint
|
||||||
|
// To get all the linked refs in one go, the SDK will use the data and resolve the links automatically
|
||||||
|
return getClient(api).getEntries({
|
||||||
content_type: 'layout',
|
content_type: 'layout',
|
||||||
locale,
|
locale,
|
||||||
'fields.slug': slug,
|
'fields.slug': slug,
|
||||||
include: 10
|
include: 6
|
||||||
})
|
})
|
||||||
.then((response) => response.items[0])
|
.then((response) => response.items[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// the SDK supports link resolution only when you call the collection endpoints
|
/**
|
||||||
// That's why we are using getEntries with a query instead of getEntry(entryId)
|
* Get an entry with content_type `course`
|
||||||
// make sure to specify the content_type whenever you want to perform a query
|
* @param slug - string - the slug of the entry to use in the query
|
||||||
exports.getCourse = assert((slug, locale = 'en-US', api = `cda`) => {
|
* @param locale - string - locale of the entry to request [default: 'en-US']
|
||||||
const client = api === 'cda' ? cdaClient : cpaClient
|
* @param api - string - the api enpoint to use when fetching the data
|
||||||
return client.getEntries({
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
module.exports.getCourse = assert((slug, locale = 'en-US', api = `cda`) => {
|
||||||
|
// Even though we need a single entry, we request it using the collection endpoint
|
||||||
|
// To get all the linked refs in one go, the SDK will use the data and resolve the links automatically
|
||||||
|
return getClient(api).getEntries({
|
||||||
content_type: 'course',
|
content_type: 'course',
|
||||||
'fields.slug': slug,
|
'fields.slug': slug,
|
||||||
locale,
|
locale,
|
||||||
include: 10
|
include: 6
|
||||||
})
|
})
|
||||||
.then((response) => response.items[0])
|
.then((response) => response.items[0])
|
||||||
}, 'Course')
|
}, 'Course')
|
||||||
|
|
||||||
exports.getCategories = assert((locale = 'en-US', api = `cda`) => {
|
module.exports.getCategories = assert((locale = 'en-US', api = `cda`) => {
|
||||||
const client = api === 'cda' ? cdaClient : cpaClient
|
return getClient(api).getEntries({content_type: 'category', locale})
|
||||||
return client.getEntries({content_type: 'category', locale})
|
|
||||||
.then((response) => response.items)
|
.then((response) => response.items)
|
||||||
}, 'Course')
|
}, 'Course')
|
||||||
|
|
||||||
// Getting a course by Category is simply querying all entries
|
/**
|
||||||
// with a query params `fields.categories.sys.id` equal to the desired category id
|
* Get Courses by Categories
|
||||||
// Note that you need to send the `content_type` param to be able to query the entry
|
* To get a course by category, simply query all entries
|
||||||
exports.getCoursesByCategory = assert((category, locale = 'en-US', api = `cda`) => {
|
* with a query params `fields.categories.sys.id` equal to the desired category id
|
||||||
const client = api === 'cda' ? cdaClient : cpaClient
|
* Note that you need to send the `content_type` param to be able to query the entry
|
||||||
return client.getEntries({
|
* @param category - string - the id of the category
|
||||||
|
* @param locale - string - locale of the entry to request [default: 'en-US']
|
||||||
|
* @param api - string - the api enpoint to use when fetching the data
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
module.exports.getCoursesByCategory = assert((category, locale = 'en-US', api = `cda`) => {
|
||||||
|
return getClient(api).getEntries({
|
||||||
content_type: 'course',
|
content_type: 'course',
|
||||||
'fields.categories.sys.id': category,
|
'fields.categories.sys.id': category,
|
||||||
locale,
|
locale,
|
||||||
order: '-sys.createdAt',
|
order: '-sys.createdAt',
|
||||||
include: 10
|
include: 6
|
||||||
})
|
})
|
||||||
.then((response) => response.items)
|
.then((response) => response.items)
|
||||||
}, 'Category')
|
}, 'Category')
|
||||||
|
|
||||||
// Utitlities functions
|
// Utility function
|
||||||
|
function getClient (api = 'cda') {
|
||||||
|
return api === 'cda' ? deliveryClient : previewClient
|
||||||
|
}
|
||||||
|
|
||||||
function assert (fn, context) {
|
function assert (fn, context) {
|
||||||
return function (req, res, next) {
|
return function (request, response, next) {
|
||||||
return fn(req, res, next)
|
return fn(request, response, next)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
var err = new Error(`${context} Not Found`)
|
var err = new Error(`${context} Not Found`)
|
||||||
|
|||||||
6
test/.eslintrc.js
Normal file
6
test/.eslintrc.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
'env': {
|
||||||
|
'node': true,
|
||||||
|
'jest': true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/* global describe, test, expect */
|
|
||||||
const app = require('../../app')
|
|
||||||
const request = require('supertest')
|
const request = require('supertest')
|
||||||
|
|
||||||
|
const app = require('../../app')
|
||||||
|
|
||||||
describe('courses', () => {
|
describe('courses', () => {
|
||||||
test('it should render a list of courses', () => {
|
test('it should render a list of courses', () => {
|
||||||
return request(app).get('/courses')
|
return request(app).get('/courses')
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* global describe, test */
|
|
||||||
const app = require('../../app')
|
|
||||||
const request = require('supertest')
|
const request = require('supertest')
|
||||||
|
|
||||||
|
const app = require('../../app')
|
||||||
|
|
||||||
describe('Home page', () => {
|
describe('Home page', () => {
|
||||||
test('it should render the landing page', () => {
|
test('it should render the landing page', () => {
|
||||||
return request(app).get('/').expect(200)
|
return request(app).get('/').expect(200)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
const app = require('../../app')
|
|
||||||
const request = require('supertest')
|
|
||||||
const cheerio = require('cheerio')
|
const cheerio = require('cheerio')
|
||||||
const cookie = require('cookie')
|
const cookie = require('cookie')
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
|
const request = require('supertest')
|
||||||
|
|
||||||
function getSettingsCookie (res) {
|
const app = require('../../app')
|
||||||
|
|
||||||
|
function getSettingsCookie (response) {
|
||||||
try {
|
try {
|
||||||
const cookies = res.headers['set-cookie']
|
const cookies = response.headers['set-cookie']
|
||||||
const settingsCookie = cookies.find((cookie) => cookie.startsWith('theExampleAppSettings='))
|
const settingsCookie = cookies.find((cookie) => cookie.startsWith('theExampleAppSettings='))
|
||||||
const parsedCookie = cookie.parse(settingsCookie)
|
const parsedCookie = cookie.parse(settingsCookie)
|
||||||
return cookieParser.JSONCookie(parsedCookie.theExampleAppSettings)
|
return cookieParser.JSONCookie(parsedCookie.theExampleAppSettings)
|
||||||
@@ -38,15 +39,15 @@ describe('settings', () => {
|
|||||||
expect(inputCpa.val()).toBe(process.env.CF_PREVIEW_ACCESS_TOKEN)
|
expect(inputCpa.val()).toBe(process.env.CF_PREVIEW_ACCESS_TOKEN)
|
||||||
|
|
||||||
const inputEditorialFeatures = $('#input-editorial-features')
|
const inputEditorialFeatures = $('#input-editorial-features')
|
||||||
expect(inputEditorialFeatures.prop('checked')).toBeFalsy()
|
expect(inputEditorialFeaturesponse.prop('checked')).toBeFalsy()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should have the editorial features enabled when query parameter is set and set cookie for it', () => {
|
test('should have the editorial features enabled when query parameter is set and set cookie for it', () => {
|
||||||
return request(app).get('/settings?enable_editorial_features')
|
return request(app).get('/settings?enable_editorial_features')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((response) => {
|
||||||
const cookie = getSettingsCookie(res)
|
const cookie = getSettingsCookie(response)
|
||||||
if (!cookie.editorialFeatures) {
|
if (!cookie.editorialFeatures) {
|
||||||
throw new Error('Did not set cookie value for editorial features')
|
throw new Error('Did not set cookie value for editorial features')
|
||||||
}
|
}
|
||||||
@@ -67,7 +68,7 @@ describe('settings', () => {
|
|||||||
const $ = cheerio.load(response.text)
|
const $ = cheerio.load(response.text)
|
||||||
|
|
||||||
const inputEditorialFeatures = $('#input-editorial-features')
|
const inputEditorialFeatures = $('#input-editorial-features')
|
||||||
expect(inputEditorialFeatures.prop('checked')).toBeTruthy()
|
expect(inputEditorialFeaturesponse.prop('checked')).toBeTruthy()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ const { mockCourse, mockCategory } = require('./mocks/index')
|
|||||||
jest.mock('../../services/contentful')
|
jest.mock('../../services/contentful')
|
||||||
const contentful = require('../../services/contentful')
|
const contentful = require('../../services/contentful')
|
||||||
|
|
||||||
const req = {}
|
const request = {}
|
||||||
const res = {
|
const response = {
|
||||||
locals: {
|
locals: {
|
||||||
currentLocale: {
|
currentLocale: {
|
||||||
code: 'en-US'
|
code: 'en-US'
|
||||||
@@ -25,58 +25,58 @@ beforeAll(() => {
|
|||||||
contentful.getCategories.mockImplementation(() => [mockCategory])
|
contentful.getCategories.mockImplementation(() => [mockCategory])
|
||||||
|
|
||||||
contentful.getCoursesByCategory.mockImplementation(() => [])
|
contentful.getCoursesByCategory.mockImplementation(() => [])
|
||||||
res.render = jest.fn()
|
response.render = jest.fn()
|
||||||
res.cookie = jest.fn()
|
response.cookie = jest.fn()
|
||||||
req.cookies = { visitedLessons: [] }
|
request.cookies = { visitedLessons: [] }
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
res.render.mockClear()
|
response.render.mockClear()
|
||||||
res.render.mockReset()
|
response.render.mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Courses', () => {
|
describe('Courses', () => {
|
||||||
test('it should courses list once', async () => {
|
test('it should courses list once', async () => {
|
||||||
await getCourses(req, res)
|
await getCourses(request, response)
|
||||||
expect(res.render.mock.calls[0][0]).toBe('courses')
|
expect(response.render.mock.calls[0][0]).toBe('courses')
|
||||||
expect(res.render.mock.calls[0][1].title).toBe('All Courses (1)')
|
expect(response.render.mock.calls[0][1].title).toBe('All Courses (1)')
|
||||||
expect(res.render.mock.calls[0][1].courses.length).toBe(1)
|
expect(response.render.mock.calls[0][1].courses.length).toBe(1)
|
||||||
expect(res.render.mock.calls.length).toBe(1)
|
expect(response.render.mock.calls.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it should render single course once', async () => {
|
test('it should render single course once', async () => {
|
||||||
req.params = {slug: 'slug', lslug: 'lessonSlug'}
|
request.params = {slug: 'slug', lslug: 'lessonSlug'}
|
||||||
await getCourse(req, res)
|
await getCourse(request, response)
|
||||||
expect(res.render.mock.calls[0][0]).toBe('course')
|
expect(response.render.mock.calls[0][0]).toBe('course')
|
||||||
expect(res.render.mock.calls[0][1].title).toBe(mockCourse.fields.title)
|
expect(response.render.mock.calls[0][1].title).toBe(mockCourse.fields.title)
|
||||||
expect(res.render.mock.calls[0][1].course.sys.id).toBe(mockCourse.sys.id)
|
expect(response.render.mock.calls[0][1].course.sys.id).toBe(mockCourse.sys.id)
|
||||||
expect(res.render.mock.calls[0][1].lesson.sys.id).toBe(mockCourse.fields.lessons[0].sys.id)
|
expect(response.render.mock.calls[0][1].lesson.sys.id).toBe(mockCourse.fields.lessons[0].sys.id)
|
||||||
expect(res.render.mock.calls.length).toBe(1)
|
expect(response.render.mock.calls.length).toBe(1)
|
||||||
})
|
})
|
||||||
test('it should render list of courses by categories', async () => {
|
test('it should render list of courses by categories', async () => {
|
||||||
req.params = {slug: 'slug', lslug: 'lslug', category: 'categorySlug'}
|
request.params = {slug: 'slug', lslug: 'lslug', category: 'categorySlug'}
|
||||||
await getCoursesByCategory(req, res)
|
await getCoursesByCategory(request, response)
|
||||||
expect(res.render.mock.calls[0][0]).toBe('courses')
|
expect(response.render.mock.calls[0][0]).toBe('courses')
|
||||||
expect(res.render.mock.calls[0][1].title).toBe(`${mockCategory.fields.title} (0)`)
|
expect(response.render.mock.calls[0][1].title).toBe(`${mockCategory.fields.title} (0)`)
|
||||||
expect(res.render.mock.calls.length).toBe(1)
|
expect(response.render.mock.calls.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Lessons', () => {
|
describe('Lessons', () => {
|
||||||
test('it should render a lesson', async () => {
|
test('it should render a lesson', async () => {
|
||||||
req.params = { cslug: 'courseSlug', lslug: 'lessonSlug' }
|
request.params = { cslug: 'courseSlug', lslug: 'lessonSlug' }
|
||||||
await getLesson(req, res)
|
await getLesson(request, response)
|
||||||
expect(res.render.mock.calls[0][0]).toBe('course')
|
expect(response.render.mock.calls[0][0]).toBe('course')
|
||||||
expect(res.render.mock.calls[0][1].title).toBe('Course title | Lesson title')
|
expect(response.render.mock.calls[0][1].title).toBe('Course title | Lesson title')
|
||||||
expect(res.render.mock.calls[0][1].course.sys.id).toBe(mockCourse.sys.id)
|
expect(response.render.mock.calls[0][1].course.sys.id).toBe(mockCourse.sys.id)
|
||||||
expect(res.render.mock.calls[0][1].lesson.sys.id).toBe(mockCourse.fields.lessons[0].sys.id)
|
expect(response.render.mock.calls[0][1].lesson.sys.id).toBe(mockCourse.fields.lessons[0].sys.id)
|
||||||
expect(res.render.mock.calls.length).toBe(1)
|
expect(response.render.mock.calls.length).toBe(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Settings', () => {
|
describe('Settings', () => {
|
||||||
test('It should render settings', async () => {
|
test('It should render settings', async () => {
|
||||||
res.locals = {
|
response.locals = {
|
||||||
settings: {
|
settings: {
|
||||||
space: 'spaceId',
|
space: 'spaceId',
|
||||||
cda: 'cda',
|
cda: 'cda',
|
||||||
@@ -84,9 +84,9 @@ describe('Settings', () => {
|
|||||||
editorialFeatures: false
|
editorialFeatures: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await getSettings(req, res)
|
await getSettings(request, response)
|
||||||
expect(res.render.mock.calls[0][0]).toBe('settings')
|
expect(response.render.mock.calls[0][0]).toBe('settings')
|
||||||
expect(res.render.mock.calls[0][1].title).toBe('Settings')
|
expect(response.render.mock.calls[0][1].title).toBe('Settings')
|
||||||
expect(res.render.mock.calls[0][1].settings).toBe(res.locals.settings)
|
expect(response.render.mock.calls[0][1].settings).toBe(response.locals.settings)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
CF_SPACE=8im83r7wdkz2
|
CONTENTFUL_SPACE_ID=ft4tkuv7nwl0
|
||||||
CF_ACCESS_TOKEN=febda697c8d0031e6da7df9c56c97c1b19a8a8f5c400c0deacfdf7c25397d5ae
|
CONTENTFUL_DELIVERY_TOKEN=57459fe48bd2b1bef4855294455af52562dbc0c7f0eb84f8b2cd68692c186417
|
||||||
CF_PREVIEW_ACCESS_TOKEN=7480498b164ce9d580c811ce3ccd1c73518f967ba7afd83384654aff866bd5d2
|
CONTENTFUL_PREVIEW_TOKEN=a9972e3cd83528def2fc9d3428c67cd622eb26d0a24239718c6ac61fe0288f2f
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|||||||
@@ -39,26 +39,26 @@ block content
|
|||||||
|
|
||||||
form(action=`/settings` method="POST" class="form")
|
form(action=`/settings` method="POST" class="form")
|
||||||
.form-item
|
.form-item
|
||||||
label(for="input-space") Space ID
|
label(for="input-space-id") Space ID
|
||||||
input(type="text" name="space" id="input-space" value=settings.space)
|
input(type="text" name="spaceId" id="input-space-id" value=settings.spaceId)
|
||||||
if 'space' in errors
|
if 'spaceId' in errors
|
||||||
+renderErrors(errors.space)
|
+renderErrors(errors.spaceId)
|
||||||
.form-item__help-text The Space ID is a unique identifier for your space.
|
.form-item__help-text The Space ID is a unique identifier for your space.
|
||||||
|
|
||||||
.form-item
|
.form-item
|
||||||
label(for="input-cda") Content Delivery API - access token
|
label(for="input-delivery-token") Content Delivery API - access token
|
||||||
input(type="text" name="cda" id="input-cda" value=settings.cda)
|
input(type="text" name="deliveryToken" id="input-delivery-token" value=settings.deliveryToken)
|
||||||
if 'cda' in errors
|
if 'deliveryToken' in errors
|
||||||
+renderErrors(errors.cda)
|
+renderErrors(errors.deliveryToken)
|
||||||
.form-item__help-text
|
.form-item__help-text
|
||||||
| View published content using this API.
|
| View published content using this API.
|
||||||
a(href='https://www.contentful.com/developers/docs/references/content-delivery-api/' target='_blank' rel='noopener') Content Delivery API.
|
a(href='https://www.contentful.com/developers/docs/references/content-delivery-api/' target='_blank' rel='noopener') Content Delivery API.
|
||||||
|
|
||||||
.form-item
|
.form-item
|
||||||
label(for="input-cpa") Content Preview API - access token
|
label(for="input-preview-token") Content Preview API - access token
|
||||||
input(type="text" name="cpa" id="input-cpa" value=settings.cpa)
|
input(type="text" name="previewToken" id="input-preview-token" value=settings.previewToken)
|
||||||
if 'cpa' in errors
|
if 'previewToken' in errors
|
||||||
+renderErrors(errors.cpa)
|
+renderErrors(errors.previewToken)
|
||||||
.form-item__help-text
|
.form-item__help-text
|
||||||
| Preview unpublished content using this API (i.e. content with “Draft” status).
|
| Preview unpublished content using this API (i.e. content with “Draft” status).
|
||||||
a(href='https://www.contentful.com/developers/docs/references/content-preview-api/' target='_blank' rel='noopener') Content Preview API.
|
a(href='https://www.contentful.com/developers/docs/references/content-preview-api/' target='_blank' rel='noopener') Content Preview API.
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
extends layout
|
|
||||||
|
|
||||||
block content
|
|
||||||
.layout-centered
|
|
||||||
h1= title
|
|
||||||
p Welcome to #{title}
|
|
||||||
Reference in New Issue
Block a user