├── src-public ├── assets │ ├── fonts │ └── img │ │ └── blur-background.jpg ├── models │ └── task.coffee ├── styles │ └── app.less ├── controllers │ └── task.coffee ├── app.coffee ├── views │ └── task.jade └── index.jade ├── .gitignore ├── tasks ├── sitemap.yml ├── assets.yml ├── jade-index.yml ├── compile.yml ├── build.coffee ├── javascript-cloud.coffee ├── less.coffee ├── clean.coffee ├── deploy.coffee ├── coffee-cloud.coffee ├── coffee-public.coffee ├── jade.coffee ├── watch.coffee └── vendor.coffee ├── gulpfile.js ├── doc └── angular-parse-boilerplate.png ├── src-cloud ├── main.coffee ├── prerender-parse.js ├── app.coffee └── prerenderio.js ├── bower.json ├── config └── global.json ├── LICENSE ├── package.json └── README.md /src-public/assets/fonts: -------------------------------------------------------------------------------- 1 | ../../bower_components/bootstrap/fonts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | public/ 4 | cloud/ 5 | -------------------------------------------------------------------------------- /tasks/sitemap.yml: -------------------------------------------------------------------------------- 1 | src: src/sitemap.xml 2 | dest: public 3 | 4 | cache: false 5 | -------------------------------------------------------------------------------- /tasks/assets.yml: -------------------------------------------------------------------------------- 1 | src: src-public/assets/**/* 2 | dest: public/ 3 | 4 | cache: true 5 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | gulp = require('gulp'); 2 | plug = require('gulp-plug')(gulp, __dirname + '/tasks'); 3 | -------------------------------------------------------------------------------- /tasks/jade-index.yml: -------------------------------------------------------------------------------- 1 | src: src-public/index.jade 2 | dest: public/ 3 | 4 | cache: true 5 | 6 | pipeline: 7 | jade: 8 | -------------------------------------------------------------------------------- /doc/angular-parse-boilerplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbeurel/angular-parse-boilerplate/HEAD/doc/angular-parse-boilerplate.png -------------------------------------------------------------------------------- /src-public/models/task.coffee: -------------------------------------------------------------------------------- 1 | app.factory 'Task', (Parse) -> 2 | class Task extends Parse.Model 3 | @configure "Task", "title" 4 | -------------------------------------------------------------------------------- /src-public/assets/img/blur-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbeurel/angular-parse-boilerplate/HEAD/src-public/assets/img/blur-background.jpg -------------------------------------------------------------------------------- /tasks/compile.yml: -------------------------------------------------------------------------------- 1 | - assets 2 | - coffee-public 3 | - coffee-cloud 4 | - javascript-cloud 5 | - jade 6 | - jade-index 7 | - less 8 | - vendor 9 | - sitemap 10 | -------------------------------------------------------------------------------- /tasks/build.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | runSequence = require 'run-sequence' 3 | 4 | gulp.task 'default', ['build'] 5 | 6 | gulp.task 'build', (done) -> 7 | runSequence 'clean', 'compile', done 8 | -------------------------------------------------------------------------------- /src-cloud/main.coffee: -------------------------------------------------------------------------------- 1 | require 'cloud/app.js' 2 | 3 | Parse.Cloud.beforeSave "Task", (request, response) -> 4 | request.object.set "random", Math.floor((Math.random() * 100) + 1) 5 | response.success() 6 | return 7 | -------------------------------------------------------------------------------- /src-public/styles/app.less: -------------------------------------------------------------------------------- 1 | @import "../../bower_components/bootstrap/less/bootstrap.less"; 2 | 3 | body { 4 | background-image: url('../img/blur-background.jpg'); 5 | } 6 | 7 | .navbar-brand, h1 { 8 | color: white; 9 | } 10 | -------------------------------------------------------------------------------- /tasks/javascript-cloud.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | gutil = require 'gulp-util' 3 | 4 | gulp.task 'javascript-cloud', (done) -> 5 | gulp.src 'src-cloud/*.js' 6 | .on 'error', gutil.log 7 | .pipe gulp.dest 'cloud' 8 | .on 'end', done 9 | return 10 | -------------------------------------------------------------------------------- /tasks/less.coffee: -------------------------------------------------------------------------------- 1 | less = require('gulp-less'); 2 | concat = require 'gulp-concat' 3 | 4 | gulp.task 'less', (done) -> 5 | gulp.src 'src-public/styles/*.less' 6 | .pipe less() 7 | .pipe concat 'app.css' 8 | .pipe gulp.dest 'public/css' 9 | .on 'end', done 10 | return 11 | -------------------------------------------------------------------------------- /tasks/clean.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | gutil = require 'gulp-util' 3 | clean = require 'gulp-clean' 4 | 5 | gulp.task 'clean', (done) -> 6 | gulp.src ['public', 'cloud'], read: false 7 | .pipe clean() 8 | .on 'error', gutil.log 9 | .on 'end', done 10 | return 11 | -------------------------------------------------------------------------------- /tasks/deploy.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | deploy = require 'gulp-gh-pages' 3 | 4 | gulp.task 'deploy-github-pages', (done) -> 5 | gulp.src './public/**/*' 6 | .pipe deploy 7 | message: 'Update ' + new Date().toISOString() + ' --skip-ci' 8 | .on 'end', done 9 | return 10 | -------------------------------------------------------------------------------- /tasks/coffee-cloud.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | gutil = require 'gulp-util' 3 | coffee = require 'gulp-coffee' 4 | 5 | gulp.task 'coffee-cloud', (done) -> 6 | gulp.src 'src-cloud/*.coffee' 7 | .pipe coffee 8 | bare: true 9 | .on 'error', gutil.log 10 | .pipe gulp.dest 'cloud' 11 | .on 'end', done 12 | return 13 | -------------------------------------------------------------------------------- /tasks/coffee-public.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | gutil = require 'gulp-util' 3 | concat = require 'gulp-concat' 4 | coffee = require 'gulp-coffee' 5 | 6 | gulp.task 'coffee-public', (done) -> 7 | gulp.src 'src-public/**/*.coffee' 8 | .pipe coffee 9 | bare: true 10 | .pipe concat 'app.js' 11 | .on 'error', gutil.log 12 | .pipe gulp.dest 'public/js/' 13 | .on 'end', done 14 | return 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-github-pages", 3 | "version": "", 4 | "dependencies": { 5 | "angular": "1.2.x", 6 | "angular-ui-router": "0.2.x", 7 | "angular-ui-bootstrap-bower": "0.11.x", 8 | "angular-resource": "1.2.x", 9 | "angular-parse": "*", 10 | "angulartics": "~0.17.1", 11 | "bootstrap": "3.2.x", 12 | "moment": "2.6.x", 13 | "lodash": "~2.4.1" 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "applications": { 3 | "_default": { 4 | "link": "angular-parse-boilerplate" 5 | }, 6 | "angular-parse-boilerplate": { 7 | "applicationId": "N2xyMRbsrFcBuzq7TXLwieDGM9FzwODEY44LLFOP", 8 | "masterKey": "8QSCge7dswIvZJbVEWlnpAIjxROaykclkABTtuqw" 9 | } 10 | }, 11 | "global": { 12 | "parseVersion": "1.3.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tasks/jade.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | gutil = require 'gulp-util' 3 | jade = require 'gulp-jade' 4 | templateCache = require 'gulp-angular-templatecache' 5 | 6 | gulp.task 'jade', (done) -> 7 | gulp.src(['src-public/views/**/*.jade', 'src-public/directives/**/*.jade']) 8 | .pipe(jade(doctype: 'html')) 9 | .on 'error', gutil.log 10 | .pipe(templateCache( 11 | filename: 'templates.js' 12 | module: 'app.templates' 13 | standalone: true)) 14 | .on 'error', gutil.log 15 | .pipe gulp.dest('public/js') 16 | .on 'end', done 17 | return 18 | -------------------------------------------------------------------------------- /src-cloud/prerender-parse.js: -------------------------------------------------------------------------------- 1 | 2 | var parseAdaptor = module.exports = function(Parse) { 3 | return function(options, callback) { 4 | Parse.Cloud.httpRequest({ 5 | url: options.href, 6 | headers: options.headers, 7 | success: function(res) { 8 | res.body = res.text; 9 | res.statusCode = res.status; 10 | 11 | if(res.headers['Content-Encoding'] && res.headers['Content-Encoding'] === 'gzip') { 12 | delete res.headers['Content-Encoding']; 13 | delete res.headers['Content-Length']; 14 | } 15 | 16 | callback(res); 17 | }, 18 | error: function(res) { 19 | console.error('Request failed with code ' + res.status); 20 | console.error(res); 21 | callback(null); 22 | } 23 | }); 24 | }; 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /src-public/controllers/task.coffee: -------------------------------------------------------------------------------- 1 | app.controller 'TaskCtrl', ($scope, Task) -> 2 | 3 | $scope.addTask = -> 4 | $scope.newTask.save().then (task) -> 5 | $scope.fetchTasks() 6 | $scope.newTask = new Task 7 | 8 | $scope.removeTask = (task) -> 9 | task.destroy().then () -> 10 | _.remove $scope.tasks, (task) -> 11 | task.objectId is null 12 | 13 | $scope.editingTask = (task) -> 14 | task.editing = true 15 | 16 | $scope.editTask = (task) -> 17 | task.save() 18 | task.editing = false 19 | 20 | $scope.cancelEditing = (task) -> 21 | task.title = task._cache.title 22 | task.editing = false 23 | 24 | $scope.fetchTasks = -> 25 | Task.query() 26 | .then (tasks) -> 27 | $scope.tasks = tasks 28 | 29 | $scope.fetchTasks() 30 | $scope.newTask = new Task 31 | -------------------------------------------------------------------------------- /tasks/watch.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | webserver = require 'gulp-webserver' 3 | runSequence = require 'run-sequence' 4 | 5 | gulp.task 'webserver', ['build'], -> 6 | gulp.src 'public' 7 | .pipe webserver 8 | livereload: true 9 | fallback: 'index.html' 10 | host: '127.0.0.1' 11 | port: 8008 12 | 13 | gulp.task 'watch', -> 14 | runSequence 'webserver', -> 15 | gulp.watch 'src-public/assets/**/*', ['assets'] 16 | gulp.watch 'src-public/**/*.coffee', ['coffee-public'] 17 | gulp.watch 'src-cloud/**/*.coffee', ['coffee-cloud'] 18 | gulp.watch 'src-public/index.jade', ['jade-index'] 19 | gulp.watch 'src-public/**/*.jade', ['jade'] 20 | gulp.watch 'src-public/styles/*.less', ['less'] 21 | gulp.watch 'src-public/translations/*.yml', ['translations'] 22 | gulp.watch 'vendor', ['vendor'] 23 | -------------------------------------------------------------------------------- /src-public/app.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | app = angular.module 'angularParseBoilerplate', [ 4 | 'ng' 5 | 'ngResource' 6 | 'ui.router' 7 | 'ui.bootstrap' 8 | 'app.templates' 9 | 'Parse' 10 | 'angulartics' 11 | 'angulartics.google.analytics' 12 | ] 13 | 14 | app.config ( 15 | $locationProvider 16 | $stateProvider 17 | $urlRouterProvider 18 | ParseProvider 19 | ) -> 20 | 21 | $locationProvider.hashPrefix '!' 22 | 23 | $stateProvider 24 | .state 'task', 25 | url: '/:locale' 26 | controller: 'TaskCtrl' 27 | templateUrl: 'task.html' 28 | 29 | $urlRouterProvider.otherwise '/fr' 30 | 31 | ParseProvider.initialize( 32 | "N2xyMRbsrFcBuzq7TXLwieDGM9FzwODEY44LLFOP", # Application ID 33 | "zTAHO7HKWvbV1awq5wQlexRc368lOQtSbmycOi0O" # REST API Key 34 | ) 35 | 36 | app.run ($rootScope, $state) -> 37 | $rootScope.$state = $state 38 | -------------------------------------------------------------------------------- /tasks/vendor.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | gutil = require 'gulp-util' 3 | concat = require 'gulp-concat' 4 | 5 | gulp.task 'vendor', (done) -> 6 | gulp.src [ 7 | 'bower_components/angular/angular.min.js' 8 | 'bower_components/angular-resource/angular-resource.min.js' 9 | 'bower_components/angular-ui-router/release/angular-ui-router.min.js' 10 | 'bower_components/angular-translate/angular-translate.min.js' 11 | 'bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.min.js' 12 | 'bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js' 13 | 'bower_components/angular-parse/angular-parse.js' 14 | 'bower_components/angulartics/src/angulartics.js' 15 | 'bower_components/angulartics/src/angulartics-ga.js' 16 | 'bower_components/moment/moment.js' 17 | 'bower_components/lodash/dist/lodash.js' 18 | ] 19 | .pipe(concat('vendor.js')) 20 | .on 'error', gutil.log 21 | .pipe gulp.dest('public/js') 22 | .on 'end', done 23 | return 24 | -------------------------------------------------------------------------------- /src-public/views/task.jade: -------------------------------------------------------------------------------- 1 | h1 Todo List \o/ 2 | 3 | form(role="form", ng-submit="addTask()") 4 | .input-group 5 | input.form-control(type="text", ng-model="newTask.title", autofocus) 6 | span.input-group-btn 7 | button.btn.btn-default(type="submit") Add 8 | 9 | div(ng-repeat="task in tasks") 10 | form(ng-hide="task.editing") 11 | .input-group 12 | span.input-group-addon {{ task.random }} 13 | input.form-control(ng-model="task._cache.title", ng-dblclick="editingTask(task)", readonly) 14 | span.input-group-btn 15 | button.btn.btn-default(ng-click="editingTask(task)") Edit 16 | button.btn.btn-default(ng-click="removeTask(task)") Remove 17 | form(ng-submit="editTask(task)", ng-show="task.editing") 18 | .input-group 19 | span.input-group-addon {{ task.random }} 20 | input.form-control(ng-model="task.title") 21 | span.input-group-btn 22 | .btn.btn-default(ng-click="cancelEditing(task)") Cancel 23 | .btn.btn-default(ng-click="removeTask(task)") Remove 24 | -------------------------------------------------------------------------------- /src-cloud/app.coffee: -------------------------------------------------------------------------------- 1 | 2 | # These two lines are required to initialize Express in Cloud Code. 3 | express = require("express") 4 | app = express() 5 | parseAdaptor = require("cloud/prerender-parse.js") 6 | app.use require("cloud/prerenderio.js").setAdaptor(parseAdaptor(Parse)).set("prerenderToken", "OnBuGOnWjpPCOA2oC91v") 7 | 8 | # Global app configuration section 9 | app.set "views", "cloud/views" # Specify the folder to find templates 10 | app.set "view engine", "ejs" # Set the template engine 11 | app.use express.bodyParser() # Middleware for reading request body 12 | 13 | # // Example reading from the request query string of an HTTP get request. 14 | # app.get('/test', function(req, res) { 15 | # // GET http://example.parseapp.com/test?message=hello 16 | # res.send(req.query.message); 17 | # }); 18 | 19 | # // Example reading from the request body of an HTTP post request. 20 | # app.post('/test', function(req, res) { 21 | # // POST http://example.parseapp.com/test (with request body "message=hello") 22 | # res.send(req.body.message); 23 | # }); 24 | 25 | # Attach the Express app to Cloud Code. 26 | app.listen() 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Jonathan Beurel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-github-pages", 3 | "description": "Angular Github Pages", 4 | "homepage": "https://github.com/jbeurel/angular-github-pages", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:jbeurel/angular-github-pages.git" 8 | }, 9 | "engines": { 10 | "node": "0.10.x" 11 | }, 12 | "scripts": { 13 | "postinstall": "node_modules/.bin/bower install | xargs echo && gulp build", 14 | "watch": "node_modules/.bin/gulp watch", 15 | "deploy-github": "node_modules/.bin/gulp deploy-github-pages", 16 | "deploy-parse": "parse deploy" 17 | }, 18 | "dependencies": { 19 | "bower": "1.3.x", 20 | "coffee-script": "1.7.x", 21 | "connect-history-api-fallback": "0.0.4", 22 | "jade": "1.3.x", 23 | "lodash": "2.4.x", 24 | "run-sequence": "0.3.x" 25 | }, 26 | "devDependencies": { 27 | "gulp": "3.6.x", 28 | "gulp-angular-templatecache": "1.1.x", 29 | "gulp-clean": "0.2.x", 30 | "gulp-coffee": "1.4.x", 31 | "gulp-concat": "2.1.x", 32 | "gulp-gh-pages": "^0.3.3", 33 | "gulp-jade": "0.4.x", 34 | "gulp-less": "1.2.x", 35 | "gulp-plug": "^0.2.4", 36 | "gulp-util": "2.2.x", 37 | "gulp-webserver": "^0.3.4", 38 | "webdriver-manager": "0.0.4" 39 | }, 40 | "version": "0.0.0", 41 | "main": "gulpfile.js" 42 | } 43 | -------------------------------------------------------------------------------- /src-public/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en", ng-app="angularParseBoilerplate") 3 | head 4 | title AngularJS Parse Boilerplate 5 | meta(charset="utf-8") 6 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 7 | meta(name="viewport", content="width=device-width, initial-scale=1") 8 | link(rel="stylesheet", href="css/app.css") 9 | 10 | body 11 | a(href='https://github.com/jbeurel/angular-parse-boilerplate') 12 | img(style='position: absolute; top: 0; right: 0; border: 0;', src='https://camo.githubusercontent.com/38ef81f8aca64bb9a64448d0d70f1308ef5341ab/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6461726b626c75655f3132313632312e706e67', alt='Fork me on GitHub', data-canonical-src='https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png') 13 | header#top.navbar.navbar-static-top.bs-docs-nav(role='banner') 14 | .container 15 | .navbar-header 16 | a.navbar-brand 17 | i.glyphicon.glyphicon-thumbs-up 18 | | AngularJS Parse Boilerplate 19 | 20 | main.container(ui-view) 21 | 22 | script(src="js/vendor.js") 23 | script(src="js/templates.js") 24 | script(src="js/app.js") 25 | script (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-38757134-3', 'auto'); 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Angularjs Parse.com Boilerplate [ ![Codeship Status for jbeurel/rider-ranking](https://codeship.com/projects/353a5570-49bb-0132-890d-62ad83b9cfff/status?branch=master)](https://codeship.com/projects/55198) 2 | =============================== 3 | 4 | [Demo :-)](http://ng-parse-boilerplate.parseapp.com) 5 | 6 | ![Demo Screenshot](./doc/angular-parse-boilerplate.png) 7 | 8 | # Technologies 9 | 10 | ## Languages 11 | - [Coffeescript](http://coffeescript.org/) 12 | - [Jade](http://jade-lang.com/) 13 | - [Less](http://www.lesscss.org/) 14 | 15 | ## Framework|Tools 16 | - [AngularJS](http://angularjs.org/) 17 | - [Bootstrap3](http://getbootstrap.com/) 18 | - [Gulp](http://gulpjs.com/) 19 | - Livereload 20 | 21 | # Requirements 22 | 23 | - [NodeJS](http://nodejs.org/) 24 | 25 | # Installation 26 | 27 | `npm install` 28 | 29 | # Configuration 30 | 31 | Edit the [config/global.json](./config/global.json) file to write the configuration of your Parse.com project in order to use the Parse's CLI. 32 | Edit the [src-public/app.coffee](./src-public/app.coffee) file to replace the ParseProvider keys. 33 | 34 | Enjoy! 35 | 36 | # Development server 37 | 38 | `npm run-script watch` 39 | 40 | Access to the application at this address: http://127.0.0.1:8008 41 | The livereload update your browser each time you change source files. 42 | 43 | The Frontend source files are into the [src-public](./src-public) directory and compile to the public directory. 44 | The Backend source files are into the [src-cloud](./src-cloud) directory and compile to the cloud directory. 45 | 46 | # Deploy on Parse Cloud 47 | 48 | `npm run-script deploy-parse` 49 | 50 | # Deploy on Github Pages (alternative) 51 | 52 | You can also deploy the frontend on the Github Pages of your repo by launching this command: 53 | 54 | `npm run-script deploy-github` 55 | 56 | ## Licence 57 | 58 | Licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 59 | -------------------------------------------------------------------------------- /src-cloud/prerenderio.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | url = require('url'); 3 | 4 | 5 | var prerender = module.exports = function(req, res, next) { 6 | 7 | if(!prerender.shouldShowPrerenderedPage(req)) return next(); 8 | 9 | prerender.beforeRenderFn(req, function(err, cachedRender) { 10 | 11 | if (!err && cachedRender && typeof cachedRender == 'string') { 12 | return res.send(200, cachedRender); 13 | } 14 | 15 | prerender.getPrerenderedPageResponse(req, function(prerenderedResponse){ 16 | 17 | if(prerenderedResponse) { 18 | prerender.afterRenderFn(req, prerenderedResponse); 19 | res.set(prerenderedResponse.headers); 20 | return res.send(prerenderedResponse.statusCode, prerenderedResponse.body); 21 | } 22 | 23 | next(); 24 | }); 25 | }); 26 | }; 27 | 28 | 29 | prerender.getEnvServiceUrl = function() { 30 | if(typeof process !== 'undefined' && typeof process.env !== 'undefined') { 31 | return process.env.PRERENDER_SERVICE_URL; 32 | } 33 | return undefined; 34 | }; 35 | 36 | 37 | if(typeof process !== 'undefined' && typeof process.env !== 'undefined') { 38 | prerender.prerenderToken = process.env.PRERENDER_TOKEN; 39 | } 40 | 41 | 42 | 43 | // googlebot, yahoo, and bingbot are not in this list because 44 | // we support _escaped_fragment_ and want to ensure people aren't 45 | // penalized for cloaking. 46 | prerender.crawlerUserAgents = [ 47 | // 'googlebot', 48 | // 'yahoo', 49 | // 'bingbot', 50 | 'baiduspider', 51 | 'facebookexternalhit', 52 | 'twitterbot', 53 | 'rogerbot', 54 | 'linkedinbot', 55 | 'embedly' 56 | ]; 57 | 58 | 59 | prerender.extensionsToIgnore = [ 60 | '.js', 61 | '.css', 62 | '.xml', 63 | '.less', 64 | '.png', 65 | '.jpg', 66 | '.jpeg', 67 | '.gif', 68 | '.pdf', 69 | '.doc', 70 | '.txt', 71 | '.ico', 72 | '.rss', 73 | '.zip', 74 | '.mp3', 75 | '.rar', 76 | '.exe', 77 | '.wmv', 78 | '.doc', 79 | '.avi', 80 | '.ppt', 81 | '.mpg', 82 | '.mpeg', 83 | '.tif', 84 | '.wav', 85 | '.mov', 86 | '.psd', 87 | '.ai', 88 | '.xls', 89 | '.mp4', 90 | '.m4a', 91 | '.swf', 92 | '.dat', 93 | '.dmg', 94 | '.iso', 95 | '.flv', 96 | '.m4v', 97 | '.torrent' 98 | ]; 99 | 100 | 101 | prerender.whitelisted = function(whitelist) { 102 | prerender.whitelist = typeof whitelist === 'string' ? [whitelist] : whitelist; 103 | return this; 104 | }; 105 | 106 | 107 | prerender.blacklisted = function(blacklist) { 108 | prerender.blacklist = typeof blacklist === 'string' ? [blacklist] : blacklist; 109 | return this; 110 | }; 111 | 112 | 113 | prerender.shouldShowPrerenderedPage = function(req) { 114 | var userAgent = req.headers['user-agent'] 115 | , isRequestingPrerenderedPage = false; 116 | 117 | if(!userAgent) return false; 118 | if(req.method != 'GET') return false; 119 | 120 | //if it contains _escaped_fragment_, show prerendered page 121 | if(url.parse(req.url, true).query.hasOwnProperty('_escaped_fragment_')) isRequestingPrerenderedPage = true; 122 | 123 | //if it is a bot...show prerendered page 124 | if(prerender.crawlerUserAgents.some(function(crawlerUserAgent){ return userAgent.toLowerCase().indexOf(crawlerUserAgent.toLowerCase()) !== -1;})) isRequestingPrerenderedPage = true; 125 | 126 | //if it is a bot and is requesting a resource...dont prerender 127 | if(prerender.extensionsToIgnore.some(function(extension){return req.url.indexOf(extension) !== -1;})) return false; 128 | 129 | //if it is a bot and not requesting a resource and is not whitelisted...dont prerender 130 | if(Array.isArray(this.whitelist) && this.whitelist.every(function(whitelisted){return (new RegExp(whitelisted)).test(req.url) === false;})) return false; 131 | 132 | //if it is a bot and not requesting a resource and is not blacklisted(url or referer)...dont prerender 133 | if(Array.isArray(this.blacklist) && this.blacklist.some(function(blacklisted){ 134 | var blacklistedUrl = false 135 | , blacklistedReferer = false 136 | , regex = new RegExp(blacklisted); 137 | 138 | blacklistedUrl = regex.test(req.url) === true; 139 | if(req.headers['referer']) blacklistedReferer = regex.test(req.headers['referer']) === true; 140 | 141 | return blacklistedUrl || blacklistedReferer; 142 | })) return false; 143 | 144 | return isRequestingPrerenderedPage; 145 | }; 146 | 147 | // Default node http adaptor 148 | prerender.adaptor = function(options, callback) { 149 | http.get(options, function(res) { 150 | var pageData = ""; 151 | res.on('data', function (chunk) { 152 | pageData += chunk; 153 | }); 154 | 155 | res.on('end', function(){ 156 | res.body = pageData; 157 | callback(res); 158 | }); 159 | }).on('error', function(e) { 160 | callback(null); 161 | }); 162 | }; 163 | 164 | prerender.setAdaptor = function(adaptor) { 165 | if(adaptor) { 166 | this.adaptor = adaptor; 167 | } 168 | return this; 169 | }; 170 | 171 | prerender.getPrerenderedPageResponse = function(req, callback) { 172 | var options = url.parse(prerender.buildApiUrl(req)); 173 | if(this.prerenderToken) { 174 | options.headers = { 175 | 'X-Prerender-Token': this.prerenderToken, 176 | 'User-Agent': req.headers['user-agent'] 177 | }; 178 | } 179 | 180 | this.adaptor(options, callback); 181 | 182 | }; 183 | 184 | 185 | prerender.buildApiUrl = function(req) { 186 | var prerenderUrl = prerender.getPrerenderServiceUrl(); 187 | var forwardSlash = prerenderUrl.indexOf('/', prerenderUrl.length - 1) !== -1 ? '' : '/'; 188 | 189 | var protocol = req.protocol; 190 | if (req.get('CF-Visitor')) { 191 | var match = req.get('CF-Visitor').match(/"scheme":"(http|https)"/); 192 | if (match) protocol = match[1]; 193 | } 194 | var fullUrl = protocol + "://" + req.get('host') + req.url; 195 | return prerenderUrl + forwardSlash + fullUrl 196 | }; 197 | 198 | 199 | prerender.getPrerenderServiceUrl = function() { 200 | return this.prerenderServiceUrl || this.getEnvServiceUrl() || 'http://service.prerender.io/'; 201 | }; 202 | 203 | prerender.beforeRenderFn = function(req, done) { 204 | if (!this.beforeRender) return done(); 205 | 206 | return this.beforeRender(req, done); 207 | }; 208 | 209 | 210 | prerender.afterRenderFn = function(req, prerender_res) { 211 | if (!this.afterRender) return; 212 | 213 | this.afterRender(req, prerender_res); 214 | }; 215 | 216 | 217 | prerender.set = function(name, value) { 218 | this[name] = value; 219 | return this; 220 | }; 221 | --------------------------------------------------------------------------------