├── Procfile ├── views ├── brand.pug ├── products.pug ├── product.pug ├── categories.pug ├── error.pug ├── index.pug ├── layout.pug └── mixins.pug ├── keys_and_ids.png ├── default_website.png ├── services ├── brands.js ├── contentfulClient.js ├── contentTypes.js ├── categories.js └── products.js ├── app.json ├── routes ├── brands.js ├── index.js ├── products.js └── categories.js ├── .gitignore ├── package.json ├── LICENSE ├── app.js ├── bin └── www ├── public ├── images │ └── contentful.svg └── stylesheets │ └── style.css └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /views/brand.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | +renderBrand(brand) 5 | -------------------------------------------------------------------------------- /views/products.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | +renderProducts(products) 5 | -------------------------------------------------------------------------------- /views/product.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | +renderSingleProduct(product) 5 | -------------------------------------------------------------------------------- /keys_and_ids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful_express_tutorial/master/keys_and_ids.png -------------------------------------------------------------------------------- /views/categories.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | +renderCategories(categories, products) 5 | -------------------------------------------------------------------------------- /default_website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/contentful_express_tutorial/master/default_website.png -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1 Products 5 | .products 6 | +renderProducts(products) 7 | -------------------------------------------------------------------------------- /services/brands.js: -------------------------------------------------------------------------------- 1 | var client = require('./contentfulClient').client 2 | 3 | function getBrand (brandId) { 4 | return client.getEntries({'sys.id': brandId}) 5 | } 6 | 7 | module.exports = { 8 | getBrand 9 | } 10 | -------------------------------------------------------------------------------- /services/contentfulClient.js: -------------------------------------------------------------------------------- 1 | var contentful = require('contentful') 2 | var config = require('../package.json').config || {} 3 | 4 | var client = contentful.createClient({ 5 | accessToken: config.accessToken, 6 | space: config.space 7 | }) 8 | 9 | exports.client = client 10 | 11 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contentful Express.js Example", 3 | "description": "This Example will show how to build a simple Express.js app using Contentful", 4 | "repository": "https://github.com/contentful/contentful_express_tutorial", 5 | "keywords": ["node", "express", "contentful"] 6 | } 7 | -------------------------------------------------------------------------------- /services/contentTypes.js: -------------------------------------------------------------------------------- 1 | var client = require('./contentfulClient').client 2 | 3 | var contentTypes = {} 4 | 5 | function getContentTypes () { 6 | return client.getContentTypes().then(function (collection) { 7 | contentTypes = collection 8 | }) 9 | } 10 | 11 | module.exports = { 12 | contentTypes: contentTypes, 13 | getContentTypes: getContentTypes 14 | } 15 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | include ./mixins.pug 2 | doctype html 3 | html 4 | head 5 | title= title 6 | link(rel='stylesheet', href='/stylesheets/style.css') 7 | body 8 | main 9 | .app-container 10 | nav 11 | .nav-container 12 | img(src='/images/contentful.svg' width="157" height="32") 13 | ul 14 | li 15 | a(href="/" data-nav) Home 16 | li 17 | a(href="/categories" data-nav) Categories 18 | #content.content-container 19 | block content 20 | -------------------------------------------------------------------------------- /routes/brands.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var brands = require('../services/brands') 3 | var router = express.Router() 4 | 5 | /* router params */ 6 | router.param('id', function (req, res, next, id) { 7 | brands.getBrand(id).then(function (brandsCollection) { 8 | req.brand = brandsCollection.items[0] 9 | next() 10 | }).catch(function (err) { 11 | console.log(err) 12 | next() 13 | }) 14 | }) 15 | 16 | router.get('/:id', function (req, res, next) { 17 | res.render('brand', {title: 'Brand', brand: req.brand}) 18 | }) 19 | module.exports = router 20 | -------------------------------------------------------------------------------- /services/categories.js: -------------------------------------------------------------------------------- 1 | var client = require('./contentfulClient').client 2 | 3 | function getCategory (id, query) { 4 | // little trick to get an entry with include 5 | // this way all linked items will be resolved for us 6 | query = query || {} 7 | query['content_type'] = 'category' 8 | query['sys.id'] = id 9 | return client.getEntries(query) 10 | } 11 | 12 | function getCategories (query) { 13 | query = query || {} 14 | query.content_type = 'category' 15 | return client.getEntries(query) 16 | } 17 | module.exports = { 18 | getCategory, 19 | getCategories 20 | } 21 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var products = require('../services/products') 4 | 5 | /* GET home page. */ 6 | router.use(function (req, res, next) { 7 | products.getProducts().then(function (productCollection) { 8 | req.products = productCollection 9 | next() 10 | }).catch(function (err) { 11 | console.log('index.js - getProducts (line 7) error:', JSON.stringify(err,null,2)) 12 | next() 13 | }) 14 | }) 15 | 16 | router.get('/', function (req, res, next) { 17 | res.render('products', { 18 | 'title': 'Products', 19 | 'products': req.products 20 | }) 21 | }) 22 | 23 | module.exports = router 24 | -------------------------------------------------------------------------------- /services/products.js: -------------------------------------------------------------------------------- 1 | var client = require('./contentfulClient').client 2 | 3 | function getProduct (slug, query) { 4 | // little trick to get an entry with include 5 | // this way all linked items will be resolved for us 6 | query = query || {} 7 | query['content_type'] = 'product' 8 | query['fields.slug'] = slug 9 | return client.getEntries(query) 10 | } 11 | 12 | function getProducts (query) { 13 | query = query || {} 14 | query.content_type = 'product' 15 | return client.getEntries(query) 16 | } 17 | 18 | function getProductsInCategory (id) { 19 | return getProducts({'fields.categories.sys.id[in]': id}) 20 | } 21 | module.exports = { 22 | getProduct, 23 | getProducts, 24 | getProductsInCategory 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # vim stuff 40 | *.swp 41 | *.swo 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contentful_express_tutorial", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "npm run production", 7 | "heroku-prebuild": "npm install", 8 | "dev": "cross-env NODE_ENV=development nodemon ./bin/www", 9 | "production": "cross-env NODE_ENV=production node ./bin/www" 10 | }, 11 | "config": { 12 | "accessToken": "0e3ec801b5af550c8a1257e8623b1c77ac9b3d8fcfc1b2b7494e3cb77878f92a", 13 | "space": "wl1z0pal05vy" 14 | }, 15 | "dependencies": { 16 | "body-parser": "^1.18.3", 17 | "compression": "^1.6.2", 18 | "contentful": "^3.6.4", 19 | "cookie-parser": "~1.4.3", 20 | "cross-env": "^5.0.1", 21 | "debug": "~2.2.0", 22 | "express": "^4.16.4", 23 | "helmet": "^2.2.0", 24 | "morgan": "^1.9.1", 25 | "nodemon": "^1.18.7", 26 | "pug": "^2.0.3", 27 | "serve-favicon": "^2.5.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Contentful 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /routes/products.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var products = require('../services/products') 4 | 5 | /* router params */ 6 | router.param('slug', function (req, res, next, slug) { 7 | products.getProduct(slug).then(function (product) { 8 | req.product = product.items[0] 9 | next() 10 | }).catch(function (err) { 11 | console.log('products.js - getProduct (line 7) error:', JSON.stringify(err,null,2)) 12 | next() 13 | }) 14 | }) 15 | 16 | router.use(function (req, res, next) { 17 | products.getProducts().then(function (productCollection) { 18 | req.products = productCollection.items 19 | next() 20 | }).catch(function (err) { 21 | console.log('products.js - getProducts (line 17) error:', JSON.stringify(err,null,2)) 22 | next() 23 | }) 24 | }) 25 | 26 | router.get('/products/:slug', function (req, res, next) { 27 | res.render('product', {title: req.product.fields.productName, product: req.product}) 28 | }) 29 | 30 | router.get('/products', function (req, res, next) { 31 | res.render('products', { 32 | 'title': 'Products', 33 | 'products': req.products 34 | }) 35 | }) 36 | 37 | router.get('/', function (req, res, next) { 38 | res.render('products', { 39 | 'title': 'Products', 40 | 'products': req.products 41 | }) 42 | }) 43 | 44 | module.exports = router 45 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var path = require('path') 3 | var logger = require('morgan') 4 | var cookieParser = require('cookie-parser') 5 | var bodyParser = require('body-parser') 6 | var compression = require('compression') 7 | var helmet = require('helmet') 8 | var products = require('./routes/products') 9 | var categories = require('./routes/categories') 10 | var brands = require('./routes/brands') 11 | 12 | var app = express() 13 | 14 | // view engine setup 15 | app.set('views', path.join(__dirname, 'views')) 16 | app.set('view engine', 'pug') 17 | app.set('view cache', true) 18 | app.use(helmet()) // protect from well known vulnerabilities 19 | app.use(bodyParser.json()) 20 | app.use(bodyParser.urlencoded({ extended: false })) 21 | app.use(cookieParser()) 22 | app.use(compression()) 23 | app.use(express.static(path.join(__dirname, 'public'))) 24 | 25 | app.use('/', products) 26 | app.use('/products', products) 27 | app.use('/categories', categories) 28 | app.use('/brand', brands) 29 | 30 | // catch 404 and forward to error handler 31 | app.use(function (req, res, next) { 32 | var err = new Error('Not Found') 33 | err.status = 404 34 | next(err) 35 | }) 36 | 37 | // error handlers 38 | 39 | // development error handler 40 | // will print stacktrace 41 | if (app.get('env') === 'development') { 42 | app.use(logger('dev')) 43 | app.use(function (err, req, res, next) { 44 | res.status(err.status || 500) 45 | res.render('error', { 46 | message: err.message, 47 | error: err 48 | }) 49 | }) 50 | } 51 | 52 | // production error handler 53 | // no stacktraces leaked to user 54 | app.use(function (err, req, res, next) { 55 | res.status(err.status || 500) 56 | res.render('error', { 57 | message: err.message, 58 | error: {} 59 | }) 60 | }) 61 | 62 | module.exports = app 63 | -------------------------------------------------------------------------------- /routes/categories.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var router = express.Router() 3 | var categories = require('../services/categories.js') 4 | var products = require('../services/products') 5 | /* router params */ 6 | router.param('id', function (req, res, next, id) { 7 | categories.getCategories().then(function (categories) { 8 | req.categories = categories.items 9 | products.getProductsInCategory(id).then(function (productsCollection) { 10 | req.products = productsCollection.items 11 | next() 12 | }).catch(function (err) { 13 | console.log('categories.js - getProductsInCategory (line 9) error:', JSON.stringify(err,null,2)) 14 | next() 15 | }) 16 | }).catch(function (err) { 17 | console.log('categories.js - getCategories (line 7) error:', JSON.stringify(err,null,2)) 18 | next() 19 | }) 20 | }) 21 | 22 | router.use(function (req, res, next) { 23 | categories.getCategories().then(function (categoryCollection) { 24 | req.categories = categoryCollection.items 25 | products.getProducts().then(function (productsCollection) { 26 | req.products = productsCollection.items 27 | next() 28 | }).catch(function (err) { 29 | console.log('categories.js - getProducts (line 25) error:', JSON.stringify(err,null,2)) 30 | next() 31 | }) 32 | }).catch(function (err) { 33 | console.log('categories.js - getCategories (line 23) error:', JSON.stringify(err,null,2)) 34 | next() 35 | }) 36 | }) 37 | 38 | router.get('/:id', function (req, res, next) { 39 | res.render('categories', {title: 'Categories', categories: req.categories, products: req.products}) 40 | }) 41 | router.get('/', function (req, res, next) { 42 | res.render('categories', {title: 'Categories', categories: req.categories, products: req.products}) 43 | }) 44 | module.exports = router 45 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app') 8 | var debug = require('debug')('contentful_express_tutorial:server') 9 | var http = require('http') 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000') 16 | app.set('port', port) 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app) 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port) 29 | server.on('error', onError) 30 | server.on('listening', onListening) 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort (val) { 37 | var port = parseInt(val, 10) 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port 47 | } 48 | 49 | return false 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError (error) { 57 | if (error.syscall !== 'listen') { 58 | throw error 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges') 69 | process.exit(1) 70 | break 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use') 73 | process.exit(1) 74 | break 75 | default: 76 | throw error 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening () { 85 | var addr = server.address() 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port 89 | debug('Listening on ' + bind) 90 | } 91 | -------------------------------------------------------------------------------- /public/images/contentful.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /views/mixins.pug: -------------------------------------------------------------------------------- 1 | mixin renderProducts(products) 2 | h1 Products 3 | .products 4 | for product in products 5 | +renderProduct(product) 6 | 7 | mixin renderProduct(product) 8 | .product-in-list 9 | .product-image 10 | +renderImage(product.fields.image[0], product.fields.slug, 150) 11 | .product-details 12 | +renderProductDetails(product.fields) 13 | 14 | 15 | mixin renderImage(image, slug, w) 16 | if image && image.fields.file 17 | if slug != "" 18 | a(href="/products/"+slug data-nav) 19 | if(w) 20 | img(src=image.fields.file.url+"?w="+w alt=image.fields.title) 21 | else 22 | img(src=image.fields.file.url alt=image.fields.title) 23 | 24 | else if(w) 25 | img(src=image.fields.file.url+"?w="+w alt=image.fields.title width=w) 26 | else 27 | img(src=image.fields.file.url alt=image.fields.title) 28 | else 29 | 30 | 31 | mixin renderProductDetails(fields) 32 | +renderProductHeader(fields) 33 | p.product-categories #{fields.categories.map(function(category){ return category.fields.title }).join(', ')} 34 | p #{fields.productDescription} 35 | p #{fields.price} € 36 | +renderTags(fields.tags) 37 | 38 | mixin renderProductHeader(fields) 39 | .product-header 40 | h2 41 | a(href="/products/"+fields.slug data-nav) #{fields.productName} 42 | span by 43 | a(href="brand/"+fields.brand.sys.id data-env) #{fields.brand.fields.companyName} 44 | 45 | mixin renderSingleProduct(singleProduct) 46 | .single-product 47 | .product-image 48 | +renderImage(singleProduct.fields.image[0], "", 300) 49 | .product-details 50 | .product-header 51 | h2 #{singleProduct.fields.productName} 52 | span by 53 | a(href="/brand/"+singleProduct.fields.brand.sys.id data-nav) #{singleProduct.fields.brand.fields.companyName} 54 | p.product-categories #{singleProduct.fields.categories.map(function(category){ return category.fields.title }).join(', ')} 55 | p #{singleProduct.fields.productDescription} 56 | p Size/Type/Color: #{singleProduct.fields.sizetypecolor} 57 | p #{singleProduct.fields.quantity} in stock 58 | p #{singleProduct.fields.price} € 59 | p SKU: #{singleProduct.fields.sku} 60 | p More details: 61 | a(href=singleProduct.fields.website) #{singleProduct.fields.website} 62 | +renderTags(singleProduct.fields.tags) 63 | 64 | mixin renderTags(tags) 65 | p.product-tags 66 | span Tags: 67 | ul.tags 68 | for tag in tags 69 | li.tag #{tag} 70 | 71 | mixin renderCategories(categories, products) 72 | .categories 73 | ul.categories-list 74 | +renderCategoriesList(categories) 75 | div 76 | +renderProducts(products) 77 | 78 | mixin renderCategoriesList(categories) 79 | li 80 | a(href="/categories") All 81 | for category in categories 82 | li 83 | img(src=category.fields.icon.fields.file.url width="20" height="20" alt=category.fields.categoryDescription title=category.fields.categoryDescription) 84 | a(href="/categories/"+category.sys.id data-nav) #{category.fields.title} 85 | 86 | 87 | mixin renderBrand(brand) 88 | .brand 89 | h2 #{brand.fields.companyName} 90 | div 91 | +renderImage(brand.fields.logo) 92 | p #{brand.fields.companyDescription} 93 | p 94 | a(href=brand.fields.website) #{brand.fields.website} 95 | p 96 | a(href=brand.fields.twitter) #{brand.fields.twitter} 97 | p 98 | a(href=brand.fields.email) #{brand.fields.email} 99 | p 100 | a(href=brand.fields.phone) #{brand.fields.phone} 101 | 102 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | border: 0; 6 | } 7 | 8 | body { 9 | padding: 0; 10 | margin: 0; 11 | border: none; 12 | font-family: 'Avenir', sans-serif; 13 | } 14 | 15 | main { 16 | position: absolute; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | right: 0; 21 | } 22 | 23 | a { 24 | border-bottom: 1px dotted #5B9FEF; 25 | color: #4A90E2; 26 | text-decoration: none; 27 | } 28 | 29 | .app-container { 30 | position: absolute; 31 | top: 0; 32 | bottom: 0; 33 | left: 0; 34 | right: 0; 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: flex-start; 38 | } 39 | 40 | .app-container nav { 41 | background: #192532; 42 | } 43 | 44 | .content-container { 45 | padding-top: 40px; 46 | width: 1000px; 47 | margin: 0 auto; 48 | } 49 | 50 | .nav-container { 51 | width: 1000px; 52 | margin: 0 auto; 53 | display: flex; 54 | justify-content: space-between; 55 | align-items: center; 56 | } 57 | 58 | .app-container nav ul { 59 | display: flex; 60 | justify-content: center; 61 | padding-top: 2px; 62 | padding: 20px 0px; 63 | } 64 | 65 | .app-container nav li { 66 | list-style: none; 67 | margin-left: 20px; 68 | } 69 | 70 | .app-container nav a { 71 | display: inline-block; 72 | color: rgba(255,255,255,0.75); 73 | border-bottom: none; 74 | } 75 | 76 | .app-container nav a:hover { 77 | color: white; 78 | } 79 | 80 | .settings-form, 81 | .form-container { 82 | display: flex; 83 | flex-direction: column; 84 | align-items: center; 85 | } 86 | 87 | .form-container { 88 | margin: 35px 0px 65px; 89 | } 90 | 91 | .settings-form > p, 92 | .form-container > p { 93 | margin-bottom: 25px; 94 | } 95 | 96 | .settings-form p { 97 | text-align: center; 98 | } 99 | 100 | .settings-form input, 101 | .settings-form button { 102 | font-size: 14px; 103 | padding: 0 10px; 104 | height: 38px; 105 | border-radius: 2px; 106 | } 107 | 108 | .settings-form input { 109 | width: 350px; 110 | background-color: #F7F9FA; 111 | border: 1px solid #DBE3E7; 112 | color: #536171; 113 | } 114 | 115 | .settings-form .label-title { 116 | display: block; 117 | margin-bottom: 4px; 118 | font-size: 16px; 119 | font-weight: bold; 120 | } 121 | 122 | .settings-form button { 123 | width: 200px; 124 | border: 1px solid #2b67ad; 125 | background: linear-gradient(0deg, #3C80CF 0%, #5B9FEF 100%) no-repeat; 126 | color: white; 127 | } 128 | 129 | .products { 130 | display: flex; 131 | flex-direction: column; 132 | margin-top: 20px; 133 | } 134 | 135 | .product-in-list { 136 | display: flex; 137 | margin-bottom: 60px; 138 | } 139 | 140 | .product-image a { 141 | border-bottom: none; 142 | } 143 | 144 | .product-header h2 { 145 | display: inline; 146 | } 147 | 148 | .product-header h2 a { 149 | border-bottom: none; 150 | } 151 | 152 | .product-details > * { 153 | margin-bottom: 10px; 154 | } 155 | 156 | .product-categories { 157 | font-weight: bold; 158 | } 159 | 160 | .product-tags span { 161 | font-weight: bold; 162 | } 163 | 164 | .product, .single-product { 165 | display: flex; 166 | flex-direction: column; 167 | } 168 | 169 | .single-product > *, product > *, .brand > * { 170 | margin-bottom: 15px; 171 | } 172 | 173 | .single-product > .product-image { 174 | display: block; 175 | margin: 0 auto; 176 | } 177 | 178 | .single-product > .product-details { 179 | width: 70%; 180 | margin: 0 auto; 181 | } 182 | 183 | .categories { 184 | display: flex; 185 | } 186 | 187 | .categories-list { 188 | list-style: none; 189 | width: 100%; 190 | } 191 | 192 | .categories-list li { 193 | display: flex; 194 | align-items: center; 195 | margin-bottom: 15px; 196 | } 197 | 198 | .categories-list img { 199 | margin-right: 5px; 200 | } 201 | .tags { 202 | list-style: none; 203 | margin: 0; 204 | overflow: hidden; 205 | padding: 0; 206 | } 207 | 208 | .tags li { 209 | float: left; 210 | } 211 | .tags li a { 212 | border-bottom: none; 213 | } 214 | .tag { 215 | background: #eee; 216 | color: #4A90E2; 217 | border-radius: 3px 0 0 3px; 218 | display: inline-block; 219 | height: 26px; 220 | line-height: 26px; 221 | padding: 0 20px 0 23px; 222 | position: relative; 223 | margin: 0 10px 10px 0; 224 | text-decoration: none; 225 | transition: color 0.2s; 226 | } 227 | 228 | .tag::before { 229 | background: #fff; 230 | border-radius: 10px; 231 | box-shadow: inset 0 1px rgba(0, 0, 0, 0.25); 232 | content: ''; 233 | height: 6px; 234 | left: 10px; 235 | position: absolute; 236 | width: 6px; 237 | top: 10px; 238 | } 239 | 240 | .tag::after { 241 | background: #fff; 242 | border-bottom: 13px solid transparent; 243 | border-left: 10px solid #eee; 244 | border-top: 13px solid transparent; 245 | content: ''; 246 | position: absolute; 247 | right: 0; 248 | top: 0; 249 | } 250 | 251 | .tag:hover { 252 | background-color: #dbe3e7; 253 | color: #4A90E2; 254 | } 255 | 256 | .tag:hover::after { 257 | border-left-color: #dbe3e7; 258 | } 259 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This guide will walk you through your first steps using Contentful within an Express Node js application. It will provide a step-by-step guide on how to get your first entries and start using the content you create on Contentful. 2 | 3 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 4 | 5 | ## Requirements 6 | 7 | - A [Heroku][1] account 8 | - [Heroku CLI][14] installed 9 | - [Node.js] [2] 6.2.1 installed 10 | - Npm 3.10.7 which should be installed with Nodejs 11 | - [Git][12] installed 12 | - Basic Command Line Interface knowledge 13 | 14 | ## Start from a demo application 15 | 16 | First we'll start with a [demo project][3], which is a simple Express js application using Contentful's 'Product Catalogue' template, so that you can see it running for yourself. 17 | 18 | Run the following commands to get started. 19 | 20 | - Clone the example repository: 21 | 22 | ~~~bash 23 | git clone https://github.com/contentful/contentful_express_tutorial.git 24 | ~~~ 25 | 26 | - Navigate into the repository's directory: 27 | 28 | ~~~bash 29 | $ cd contentful_express_tutorial 30 | ~~~ 31 | 32 | - Install dependencies: 33 | 34 | ~~~bash 35 | $ npm install 36 | ~~~ 37 | 38 | - Run the server: 39 | 40 | ~~~bash 41 | $ npm run dev 42 | ~~~ 43 | 44 | Everything is now set up. You can view your new data by opening [_http://localhost:3000/_][4] in your browser. 45 | 46 | It should look something like this: 47 | 48 | ![Express js Demo Application](./default_website.png) 49 | 50 | ### Using your own content 51 | 52 | You can create your own custom data by following these steps: 53 | 54 | - Create an account with [Contentful][6] or [Log In][5]. 55 | - Create a new Space with the 'Product Catalogue' template, name it whatever you like. 56 | - Copy the space Id and api key as shown in the screenshot 57 | 58 | ![Keys Page](keys_and_ids.png) 59 | 60 | - Change the product content type by your own, [here](https://github.com/contentful/contentful_express_tutorial/blob/master/services/products.js). 61 | e.g. replacing `2PqfXUJwE8qSYKuM0U6w8M` with `product` 62 | - Change the category content type by your own, [here](https://github.com/contentful/contentful_express_tutorial/blob/master/services/categories.js). 63 | e.g. replacing `6XwpTaSiiI2Ak2Ww0oi6qa` with `category` 64 | 65 | - In the Express js application: 66 | - navigate to the package.json file open it and change the values `accessToken` and `space` in the `config` section and save. 67 | - run `npm run dev` to start the server 68 | - Your space will be now displayed in your application 69 | 70 | 71 | Next in the _[Contentful web app][6] > Content_: 72 | 73 | - Open the product called 'Playsam Streamliner Classic Car, Espresso'. 74 | - Change the value of the _Product name_ field to a new value, 75 | - Click the _Publish changes_ button 76 | - Wait a few seconds for the changes to propagate to the CDN. 77 | - Reload your Express js application and you will see the new product name. 78 | 79 | You can continue to edit your content inside the [Contentful web app][13] and see the content change inside your application. 80 | 81 | ### Deploy the demo to Heroku 82 | 83 | To view the demo application live in your own production environment, follow these steps: 84 | 85 | - Having Heroku CLI Installed, Login to Heroku if you're not logged in already: 86 | 87 | ~~~bash 88 | heroku login 89 | ~~~ 90 | 91 | - Create a new instance: 92 | 93 | ~~~bash 94 | heroku create 95 | ~~~ 96 | 97 | - Commit your change: 98 | 99 | ~~~bash 100 | git add . 101 | git commit -m "Add Website" 102 | ~~~ 103 | 104 | - Deploy to Heroku: 105 | 106 | ~~~bash 107 | git push heroku master 108 | ~~~ 109 | 110 | - Open the application in your browser: 111 | 112 | ~~~bash 113 | heroku open 114 | ~~~ 115 | 116 | ## Next Steps 117 | 118 | After this guide, you should be able to start using Contentful with your Express js applications, but every project has different needs and we want to provide you with the best solutions we can. 119 | 120 | You can read about the Contentful CDA library in more detail on our [contentful.js GitHub][1] or our [Getting Started with CDA SDK tutorial][9]. We also suggest taking a look at our [Product Example Application][10]. 121 | 122 | Do you like building static sites? Check how to build static sites using Contentful with [Metalsmith][11] 123 | 124 | [1]: https://heroku.com 125 | [2]: https://nodejs.org 126 | [3]: https://github.com/contentful/contentful_express_tutorial 127 | [4]: http://localhost:3000 128 | [5]: https://www.contentful.com/sign-up/#starter 129 | [6]: https://app.contentful.com 130 | [7]: /developers/docs/references/content-delivery-api/#/reference/search-parameters 131 | [8]: https://github.com/contentful/contentful.js 132 | [9]: https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/ 133 | [10]: https://github.com/contentful/product-catalogue-js 134 | [11]: https://github.com/contentful-labs/contentful-metalsmith-example 135 | [12]: https://git-scm.com/downloads 136 | [14]: https://devcenter.heroku.com/articles/heroku-command-line#download-and-install 137 | --------------------------------------------------------------------------------