├── .gitignore ├── Makefile ├── README.md ├── app.js ├── lib ├── crawler.js ├── job.js └── model.js ├── package.json ├── public ├── css │ ├── iphone.css │ └── screen.css ├── hn.js ├── hn.unpacked.js └── images │ ├── bg.png │ └── light.png └── views ├── index.jade ├── layout.jade ├── news.jade └── partials ├── ga.jade └── webfont.jade /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pids 3 | logs 4 | nohup.out 5 | dump 6 | .sass-cache 7 | node_modules 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | jsmin: 2 | @jsmin < public/hn.unpacked.js > public/hn.js 3 | 4 | .PHONY: jsmin 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Say Hello to HackBack 2 | === 3 | this bookmarklet leads you back to HackerNews 4 | 5 | What Is This For? 6 | --- 7 | Here's the usecase: You opened a HackerNews link from Twitter, Facebook, your favorite RSS reader, or etc. But there's no easy way to check the original post and comments on HN, which might be more valuable that the link itself. 8 | 9 | This bookmarklet does one simple job: it brings you back to the comment page on HN. 10 | 11 | Install The Bookmarket 12 | --- 13 | Goto [HackBack](http://hackback.cloudfoundry.com) on Cloud Foundry and drag the bookmarklet to your browsers' bookmark bar. 14 | 15 | Install On Cloud Foundry 16 | --- 17 | Here is the how to install it on Cloud Foundry: 18 | 19 | gem install vmc 20 | vmc target api.cloudfoundry.com 21 | vmc login # if you've got the invitation, it's in your mail 22 | vmc push your_app 23 | vmc env-add your_app NODE_ENV=production 24 | 25 | # update your app 26 | vmc update your_app 27 | 28 | # check status/logs 29 | vmc stats your_app 30 | vmc logs your_app 31 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | require.paths.unshift('./node_modules'); // for CloudFoundry.com 5 | 6 | var express = require('express') 7 | , sys = require('sys') 8 | , crypto = require('crypto') 9 | , app = module.exports = express.createServer() 10 | , models = require('./lib/model') 11 | , Log = require('log') 12 | , log = new Log(Log.INFO) 13 | , crawler = null 14 | , News = models.News 15 | , AccessCounter = models.AccessCounter; 16 | 17 | var HNHost = "http://news.ycombinator.com"; // Configuration 18 | 19 | app.configure(function() { 20 | app.set('views', __dirname + '/views'); 21 | app.set('view engine', 'jade'); 22 | app.use(express.bodyParser()); 23 | app.use(express.methodOverride()); 24 | app.use(express.cookieParser()); 25 | app.use(app.router); 26 | app.use(express.static(__dirname + '/public')); 27 | app.use(express.logger('[:date] INFO :method :url :response-timems')); 28 | }); 29 | 30 | app.configure('development', function() { 31 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 32 | }); 33 | 34 | app.configure('production', function() { 35 | app.use(express.errorHandler()); 36 | }); 37 | 38 | app.get('/', function(req, res) { 39 | log.info('GET /'); 40 | res.render('index', { 41 | title: 'Say Hello to HackBack' 42 | }); 43 | }); 44 | 45 | function sendJSONP(res, cb, json) { 46 | var jsonp = cb + '(' + JSON.stringify(json) + ')'; 47 | res.header('Content-Type', 'application/javascript'); 48 | res.send(jsonp); 49 | } 50 | 51 | app.get('/api/comment/:url/:cb?', function(req, res) { 52 | log.info('GET /comment'); 53 | var url = req.params.url; 54 | var cb = req.params.cb; 55 | 56 | 57 | News.findOne({href: url}, function(err, doc) { 58 | var result; 59 | 60 | if (err || doc === null) { 61 | AccessCounter.incr('miss'); 62 | log.info('Can not found any news with url = ' + url); 63 | result = { errcode: -1 }; 64 | } else { 65 | AccessCounter.incr('hit'); 66 | log.info('Found news with url = ' + url); 67 | result = { 68 | title: doc.title, 69 | href: doc.href, 70 | comment: HNHost + '/' + doc.comment, 71 | count: doc.c_count 72 | }; 73 | } 74 | 75 | if (cb) { 76 | sendJSONP(res, cb, result); 77 | } else { 78 | res.send(result); 79 | } 80 | }); 81 | }); 82 | 83 | app.get('/api/statistics', function(req, res) { 84 | AccessCounter.stats(function (err, doc) { 85 | res.send(doc); 86 | }); 87 | }); 88 | 89 | app.get('/api/summary', function(req, res) { 90 | log.info('GET /summary'); 91 | 92 | News.count({}, function(err, count) { 93 | if (err) { 94 | log.error(err); 95 | res.send(500); 96 | } else { 97 | res.send({count: count, last_run: crawler.last_run()}); 98 | } 99 | }); 100 | }); 101 | 102 | app.get('/api/gc', function(req, res, next) { 103 | log.info('GET /gc'); 104 | 105 | var d = Date.now(); 106 | d = d - 14 * 24 * 60 * 60 * 1000; // remove links 2 weeks ago 107 | var valid_date = new Date(d); 108 | 109 | News.count({}, function(err, count) { 110 | var before = count; 111 | 112 | News.remove({'updated_at': { $lt : valid_date}}, function(err) { 113 | if (err) { 114 | next(err); 115 | return; 116 | } 117 | 118 | News.count({}, function(err, count) { 119 | res.send({before: before, after: count}); 120 | }); 121 | }); 122 | }); 123 | }); 124 | 125 | // Only listen on $ node app.js 126 | if (!module.parent) { 127 | app.listen(process.env.VMC_APP_PORT || 3000); 128 | log.info("Express server listening on port " + app.address().port); 129 | 130 | crawler = require('./lib/job'); // start crawler job 131 | } 132 | 133 | -------------------------------------------------------------------------------- /lib/crawler.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom') 2 | , request = require('request') 3 | , Log = require('log') 4 | , log = new Log(Log.INFO) 5 | , util = require('util') 6 | , models = require('./model') 7 | , News = models.News; 8 | 9 | var host = "http://news.ycombinator.com"; 10 | 11 | function HNCrawler(pages) { 12 | this.pages = pages || 3; 13 | this.onCompleted = undefined; 14 | this.page = 1; 15 | } 16 | 17 | HNCrawler.prototype.done = function() { 18 | log.info("All done!"); 19 | this.page = 1; 20 | if (this.onCompleted) { 21 | this.onCompleted(); 22 | } 23 | }; 24 | 25 | HNCrawler.prototype.run = function(url, cb) { 26 | log.info("digging " + host + url); 27 | 28 | var self = this; 29 | this.onCompleted = cb; 30 | 31 | // use request as jsdom can't set user-agent 32 | var target = {}; 33 | target.uri = host + url; 34 | target.method = "GET"; 35 | target.headers = {"user-agent": "hackback-crawler-1.0"}; 36 | 37 | try { 38 | request(target, function(error, response, body) { 39 | jsdom.env(body, [], function(errors, window) { 40 | if (errors) { 41 | log.error(errors); 42 | log.info("Failed in crawling HN page: " + self.page); 43 | } 44 | 45 | var links = window.document.getElementsByClassName("title"); 46 | 47 | log.info("Digged page " + self.page + ", found "+ links.length + " links ..."); 48 | 49 | if (links.length === 0) { 50 | log.info(window.document.innerHTML); 51 | self.done(); 52 | log.info("Not all pages crawled, ending"); 53 | return; 54 | } 55 | 56 | var nextHref; 57 | 58 | for (var i = 0; i < links.length; i++) { 59 | var row = {}; 60 | var href = links[i].getElementsByTagName('a'); 61 | if (href.length) { 62 | row.href = href[0].getAttribute('href'); 63 | row.title = href[0].innerHTML; 64 | 65 | var nextTr = links[i].parentNode.nextSibling; 66 | if (nextTr) { 67 | var group2 = nextTr.getElementsByTagName('a'); 68 | var l2 = group2[group2.length - 1]; 69 | if (!l2) continue; 70 | 71 | row.comment = l2.getAttribute('href'); 72 | var c_count = l2.innerHTML.match(/(\d+) comments?/); 73 | row.c_count = c_count ? c_count[1] : 0; 74 | } else { 75 | nextHref = row.href; 76 | break; 77 | } 78 | 79 | News.saveNews(row.title, row.href, row.comment, row.c_count); 80 | } 81 | }; 82 | 83 | self.page++; 84 | if (self.page <= self.pages) { 85 | var sleepTime = 15 + Math.random() * 15; 86 | log.info("Sleep for " + sleepTime + " secs"); 87 | setTimeout(function() { 88 | self.run(nextHref, self.onCompleted); 89 | }, sleepTime * 1000); 90 | } else { 91 | self.done(); 92 | } 93 | }); 94 | }); 95 | } catch (e) { 96 | log.error('failed to crawl'); 97 | log.error(e); 98 | } 99 | }; 100 | 101 | exports.HNCrawler = HNCrawler; 102 | -------------------------------------------------------------------------------- /lib/job.js: -------------------------------------------------------------------------------- 1 | var Log = require('log') 2 | , log = new Log(Log.INFO) 3 | , EventEmitter = require('node-evented').EventEmitter 4 | , emitter = new EventEmitter() 5 | , HNCrawler = require('./crawler').HNCrawler 6 | , crawler = new HNCrawler(1) 7 | , last_run = new Date(); 8 | 9 | // Start crawler job 10 | 11 | emitter.on('digging_pop', function() { 12 | crawler.run('/news', function() { 13 | last_run = new Date(); 14 | var timeout = randomDelay(); 15 | log.info("Will dig HN Newest News in " + Math.floor(timeout / 1000)+ " secs"); 16 | setTimeout(function() { 17 | emitter.emit('digging_new'); 18 | }, timeout); 19 | }); 20 | }); 21 | 22 | emitter.on('digging_new', function() { 23 | crawler.run('/newest', function() { 24 | last_run = new Date(); 25 | var timeout = randomDelay(); 26 | log.info("Will dig HN Popular News in " + Math.floor(timeout / 1000) + " secs"); 27 | setTimeout(function() { 28 | emitter.emit('digging_pop'); 29 | }, timeout); 30 | }); 31 | }); 32 | 33 | var randomDelay = function() { 34 | return (30 + Math.random() * 5) * 1000; 35 | }; 36 | 37 | emitter.emit('digging_new'); 38 | 39 | exports.last_run = function() { 40 | return last_run; 41 | }; 42 | -------------------------------------------------------------------------------- /lib/model.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var boundServices = process.env.VCAP_SERVICES ? JSON.parse(process.env.VCAP_SERVICES) : null; 3 | var credentials = null; 4 | var db = null; 5 | 6 | if (boundServices === null) { 7 | db = mongoose.connect('mongodb://localhost/hackback'); 8 | } else { 9 |   credentials = boundServices['mongodb-1.8'][0]['credentials']; 10 |   db = mongoose.createConnection("mongodb://" 11 |                            + credentials["username"] 12 |                            + ":" + credentials["password"] 13 |                            + "@" + credentials["hostname"] 14 |                            + ":" + credentials["port"] 15 |                            + "/" + credentials["db"]); 16 | } 17 | 18 | Schema = mongoose.Schema; 19 | 20 | // News schema 21 | var News = new Schema({ 22 | title: { type: String, default: '', required: true, index: false }, 23 | href: { type: String, default: '', required: true, index: true }, 24 | comment: { type: String, default: '', required: true }, 25 | c_count: { type: Number, default: 0 }, 26 | created_at: { type: Date, default: Date.now }, 27 | updated_at: { type: Date, default: Date.now } 28 | }); 29 | 30 | News.static('saveNews', function (title, href, comment, c_count, fn) { 31 | fn = fn ? fn : function() {}; 32 | 33 | var doc = {}; 34 | doc.title = title; 35 | doc.href = href; 36 | doc.comment = comment; 37 | doc.c_count = c_count; 38 | doc.updated_at = new Date(); 39 | 40 | News.collection.findAndModify({ href: doc.href}, [], 41 | {$set: doc}, {'new': false, upsert: true}, function(err) { 42 | if (err) { 43 | console.log(err); 44 | } 45 | fn(err, doc); 46 | }); 47 | }); 48 | 49 | mongoose.model('News', News); 50 | 51 | // AccessCounter model 52 | var AccessCounter = new Schema({ 53 | date: { type: Number, required: true, index: true }, 54 | hit_counter: { type: Number, required: true, default: 0 }, 55 | miss_counter: { type: Number, required: true, default: 0 } 56 | }); 57 | 58 | AccessCounter.static('incr', function(type) { 59 | var today = Date.now(); 60 | today = (today - today % 86400000); 61 | 62 | // $inc is an atomic op in mongodb 63 | // read more at http://www.mongodb.org/display/DOCS/Atomic+Operations 64 | var data = { $inc: {} }; 65 | data.$inc[type + '_counter'] = 1; 66 | var mode = { 67 | upsert: true // create object if it doesn't exist. 68 | }; 69 | 70 | AccessCounter.collection.findAndModify({ date: today }, [], data, mode , function(err) { 71 | if (err) { 72 | console.log(err); 73 | } 74 | }); 75 | }); 76 | 77 | AccessCounter.static('stats', function(fn) { 78 | var today = Date.now(); 79 | today = (today - today % 86400000); 80 | 81 | AccessCounter.find({ date: today }, function(err, docs) { 82 | if (err) { 83 | console.log(err); 84 | fn(err, null); 85 | } else { 86 | if (docs) { 87 | var d = {}; 88 | d.hit = docs[0].hit_counter; 89 | d.miss = docs[0].miss_counter; 90 | d.date = new Date(today); 91 | fn(null, d); 92 | } 93 | else fn(null, null); 94 | } 95 | }); 96 | }); 97 | 98 | mongoose.model('AccessCounter', AccessCounter); 99 | 100 | // as we attained db variable, db.model not mongoose.model 101 | var News = exports.News = db.model('News'); 102 | var AccessCounter = exports.AccessCounter = db.model('AccessCounter'); 103 | 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hncrawler", 3 | "description": "Back to HackNews, and a Crawler", 4 | "version": "0.0.1", 5 | "homepage": "hackback.jyorr.com", 6 | "author": "Rakuraku Jyo (http://twitter.com/xu_lele)", 7 | "directories": { 8 | "public": "./public" 9 | }, 10 | "private": true, 11 | "engines": { 12 | "node": ">= 0.4.6" 13 | }, 14 | "dependencies": { 15 | "express": "", 16 | "mongoose": "", 17 | "jade": "", 18 | "jsdom": "", 19 | "request": "", 20 | "node-evented": "", 21 | "log": "" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/css/iphone.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Open+Sans+Condensed:light); 2 | #by { 3 | opacity: 1.0; 4 | } 5 | 6 | #main { 7 | width: 86%; 8 | } 9 | 10 | p { 11 | font-family: 'Open Sans Condensed', sans-serif; 12 | } 13 | -------------------------------------------------------------------------------- /public/css/screen.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-image: url(/images/bg.png); 3 | background-color: #242424; 4 | background-attachment: fixed; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | body { 10 | background: url(/images/light.png) top center no-repeat; 11 | background-attachment: scroll; 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | #main { 17 | max-width: 680px; 18 | width: 76%; 19 | margin: 0 auto; 20 | color: #e2e2e2; 21 | padding-top: 80px; 22 | } 23 | 24 | .highlight { 25 | color: #ff5d28; 26 | } 27 | 28 | h1 { 29 | font-family: 'Expletus Sans', sans-serif; 30 | font-size: 46px; 31 | margin: 0; 32 | padding: 0; 33 | text-shadow: 1px 3px 0px #111; 34 | } 35 | 36 | h3 { 37 | font-family: 'Expletus Sans', sans-serif; 38 | font-size: 30px; 39 | margin: 50px 0 0 0; 40 | /* line-height: 20px;*/ 41 | color: #e6ea50; 42 | text-shadow: 1px 2px 0px #111; 43 | } 44 | 45 | p { 46 | font-family: 'Terminal Dosis Light', sans-serif; 47 | font-size: 20px; 48 | text-shadow: 0px 1px 0px #111; 49 | } 50 | 51 | p em { 52 | text-decoration: underline; 53 | font-style: normal; 54 | } 55 | 56 | #header p { 57 | font-size: 28px; 58 | } 59 | 60 | a { 61 | text-decoration: none; 62 | color: #B1D631; 63 | } 64 | 65 | a:hover { 66 | color: #ff5d28; 67 | } 68 | 69 | .hackback_button a { 70 | border: 3px solid #333; 71 | background-color: #EEE; 72 | border-radius: 5px; 73 | padding: 5px; 74 | background-color: black; 75 | } 76 | 77 | .hackback_button a:hover { 78 | background: #222; 79 | } 80 | 81 | #social_buttons div{ 82 | float: left; 83 | } 84 | 85 | #social_buttons .buttons { 86 | clear: both; 87 | margin-top: 20px; 88 | } 89 | 90 | #social_buttons { 91 | clear: both; 92 | } 93 | 94 | #by { 95 | margin: 60px 0 40px; 96 | padding: 10px; 97 | background: #333; 98 | border-radius: 5px; 99 | opacity: 0.5; 100 | -webkit-transition: opacity 0.2s linear; 101 | } 102 | 103 | #by:hover { 104 | opacity: 1.0; 105 | -webkit-transition: opacity 0.2s linear; 106 | } 107 | 108 | #by p { 109 | font-size: 18px; 110 | padding: 0; 111 | margin: 10px; 112 | } 113 | 114 | #by p.alignright { 115 | text-align: right; 116 | } 117 | 118 | /* For modern browsers */ 119 | .cf:before, 120 | .cf:after { 121 | content:""; 122 | display:table; 123 | } 124 | 125 | .cf:after { 126 | clear:both; 127 | } 128 | 129 | /* For IE 6/7 (trigger hasLayout) */ 130 | .cf { 131 | zoom:1; 132 | } 133 | 134 | -------------------------------------------------------------------------------- /public/hn.js: -------------------------------------------------------------------------------- 1 | 2 | var l=window.location;var el=encodeURIComponent(l.href);var apiUrl="http://hackback.cloudfoundry.com/api/comment/";function jsonp(src){var s=document.createElement('script');old=document.getElementById('srvCall');old&&document.body.removeChild(old);s.charset='UTF-8';s.id='srvCall';document.body.insertBefore(s,document.body.firstChild);s.src=src+'?'+Date.now();} 3 | function srvCallback(doc){if(!doc.errcode){window.location=doc.comment;}else{var answer=confirm("Can't find this article on HackerNews recently. Do you want to post it?");if(answer){window.location="http://news.ycombinator.com/submitlink?u="+ 4 | encodeURIComponent(document.location)+"&t="+ 5 | encodeURIComponent(document.title);}}} 6 | if(/^news\.ycombinator\.com$/.test(l.host)){console.log('Already on HN');}else{jsonp(apiUrl+el+"/srvCallback");} -------------------------------------------------------------------------------- /public/hn.unpacked.js: -------------------------------------------------------------------------------- 1 | var l = window.location; 2 | var el = encodeURIComponent(l.href); 3 | var apiUrl = "http://hackback.cloudfoundry.com/api/comment/"; 4 | 5 | function jsonp(src){ 6 | var s = document.createElement('script'); 7 | old = document.getElementById('srvCall'); 8 | old && document.body.removeChild(old); 9 | s.charset = 'UTF-8'; 10 | s.id = 'srvCall'; 11 | document.body.insertBefore(s, document.body.firstChild); 12 | s.src = src + '?' + Date.now(); 13 | } 14 | 15 | function srvCallback (doc) { 16 | if (!doc.errcode) { 17 | window.location = doc.comment; 18 | } else { 19 | var answer = confirm("Can't find this article on HackerNews recently. Do you want to post it?"); 20 | if (answer) { 21 | window.location = "http://news.ycombinator.com/submitlink?u=" + 22 | encodeURIComponent(document.location) + "&t=" + 23 | encodeURIComponent(document.title); 24 | } 25 | } 26 | } 27 | 28 | if (/^news\.ycombinator\.com$/.test(l.host)) { 29 | console.log('Already on HN'); 30 | } else { 31 | jsonp(apiUrl + el + "/srvCallback"); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /public/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjyo/hackback/53a7a1675ca274d0229e8b8b22621303fcce0bd0/public/images/bg.png -------------------------------------------------------------------------------- /public/images/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rjyo/hackback/53a7a1675ca274d0229e8b8b22621303fcce0bd0/public/images/light.png -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | #header 2 | h1 Say Hello to 3 | span.highlight HackBack 4 | p this bookmarklet leads you back to HackerNews 5 | 6 | #content 7 | h3 What Is This For? 8 | p Here's the usecase: You opened a HackerNews link from Twitter, Facebook, your favorite RSS reader, or etc. But there's no easy way to check the original post and comments on HN, which might be more valuable than the link itself. 9 | p This bookmarklet does one simple job: it brings you back to the comment page on HN. 10 | h3 Install 11 | p Just drag this 12 | span.hackback_button B2HN 13 | | to your bookmark bar. Or you can click it to see what people say about it on HackerNews. 14 | h3 How It Works? 15 | p HackBack crawls HN's popular and newest page every 3 minutes, it saves the article's link and its comment page's url. 16 | p When you clicked the bookmarklet, the url is send to HackBack, and HackBack sends you to the comment page. That's it. 17 | p What if HackBack can't bring you back? Chance! You'll be asked if you want to post it on HN. 18 | 19 | #social_buttons.cf 20 | h3 Share With Your Fellow Hackers 21 | .buttons 22 | .twitter_button 23 | | Tweet 24 | .facebook_button 25 | | 26 | 27 | #by 28 | p HackBack runs on Cloud Foundry, a PaaS service by VMWare, which is really easy to deploy. Without it you may not see it comes online. 29 | p This bookmark is written in node.js using express framework. Data is stored in 10gen's MongoDB. You can fork the source on GitHub. 30 | p.alignright - Brought you by 31 | a(href='http://jyorr.com') Jyo 32 | | , with love. 33 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | link(href='http://fonts.googleapis.com/css?family=Expletus+Sans:600', rel='stylesheet', type='text/css') 5 | link(href='http://fonts.googleapis.com/css?family=Terminal+Dosis+Light', rel='stylesheet', type='text/css') 6 | link(rel='stylesheet', href='/css/screen.css', media="screen, projection") 7 | // for iphone 8 | meta(name="viewport", content="width=320, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") 9 | link(media="only screen and (max-device-width:480px)", href="/css/iphone.css", type="text/css", rel="stylesheet") 10 | title= title 11 | body 12 | #main!= body 13 | 14 | != partial('partials/ga') 15 | -------------------------------------------------------------------------------- /views/news.jade: -------------------------------------------------------------------------------- 1 | h1 2 | a(href= href)= title 3 | a(href= comment) View Comments on HN 4 | 5 | script 6 | window.location = "#{comment}"; 7 | -------------------------------------------------------------------------------- /views/partials/ga.jade: -------------------------------------------------------------------------------- 1 | // for google analytics 2 | script 3 | var _gaq = _gaq || []; 4 | _gaq.push(['_setAccount', 'UA-2362355-14']); 5 | _gaq.push(['_trackPageview']); 6 | 7 | (function() { 8 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 9 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 10 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 11 | })(); 12 | -------------------------------------------------------------------------------- /views/partials/webfont.jade: -------------------------------------------------------------------------------- 1 | script 2 | (function() { 3 | var wf = document.createElement('script'); 4 | wf.src = ('https:' == document.location.protocol ? 'https' : 'http') + 5 | '://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js'; 6 | wf.type = 'text/javascript'; 7 | wf.async = 'true'; 8 | var s = document.getElementsByTagName('script')[0]; 9 | s.parentNode.insertBefore(wf, s); 10 | })(); 11 | --------------------------------------------------------------------------------