├── .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 | |
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 |
--------------------------------------------------------------------------------