diff --git a/docs/.meteor/packages b/docs/.meteor/packages index 78d056fca5c..36eb4b5aea2 100644 --- a/docs/.meteor/packages +++ b/docs/.meteor/packages @@ -9,3 +9,4 @@ showdown code-prettify jquery-waypoints less +spiderable diff --git a/docs/client/docs.js b/docs/client/docs.js index 3018e1cd401..9afd559a41a 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -194,6 +194,7 @@ var toc = [ "jquery", "less", "sass", + "spiderable", "stylus", "showdown", "underscore" diff --git a/docs/client/packages.html b/docs/client/packages.html index 65cbde7bd13..1c121570778 100644 --- a/docs/client/packages.html +++ b/docs/client/packages.html @@ -24,6 +24,7 @@

Packages

{{> pkg_jquery}} {{> pkg_less}} {{> pkg_sass}} +{{> pkg_spiderable}} {{> pkg_stylus}} {{> pkg_showdown}} {{> pkg_underscore}} diff --git a/docs/client/packages/spiderable.html b/docs/client/packages/spiderable.html new file mode 100644 index 00000000000..2255f02978d --- /dev/null +++ b/docs/client/packages/spiderable.html @@ -0,0 +1,41 @@ + diff --git a/examples/todos/.meteor/packages b/examples/todos/.meteor/packages index f96ebb32b89..cf1782e3c3d 100644 --- a/examples/todos/.meteor/packages +++ b/examples/todos/.meteor/packages @@ -5,3 +5,4 @@ underscore backbone +spiderable diff --git a/examples/todos/client/todos.css b/examples/todos/client/todos.css index 6657d21e6a8..c21e7c8436a 100644 --- a/examples/todos/client/todos.css +++ b/examples/todos/client/todos.css @@ -131,6 +131,8 @@ h3 { #lists .list-name { cursor: pointer; + color: black; + text-decoration: none; } #createList { diff --git a/examples/todos/client/todos.html b/examples/todos/client/todos.html index f390e82c198..5cf0c45b8c9 100644 --- a/examples/todos/client/todos.html +++ b/examples/todos/client/todos.html @@ -27,9 +27,9 @@

Todo Lists

{{else}}
-
+ {{name}} -
+
{{/if}} diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js index 1dfb23a2208..f703b89fcb4 100644 --- a/examples/todos/client/todos.js +++ b/examples/todos/client/todos.js @@ -88,6 +88,10 @@ Template.lists.events = { 'mousedown .list': function (evt) { // select list Router.setList(this._id); }, + 'click .list': function (evt) { + // prevent clicks on from refreshing the page. + evt.preventDefault(); + }, 'dblclick .list': function (evt) { // start editing list name Session.set('editing_listname', this._id); Meteor.flush(); // force DOM redraw, so we can focus the edit field diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index aa213fad599..dad5f482237 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -575,7 +575,9 @@ _.extend(Meteor, { // "http://subdomain.meteor.com/sockjs" (deprecated), // "/sockjs" (deprecated) connect: function (url, _restartOnUpdate) { - return new Meteor._LivedataConnection(url, _restartOnUpdate); + var ret = new Meteor._LivedataConnection(url, _restartOnUpdate); + Meteor._LivedataConnection._allConnections.push(ret); // hack. see below. + return ret; }, autosubscribe: function (sub_func) { @@ -604,3 +606,14 @@ _.extend(Meteor, { } }); + +// Hack for `spiderable` package: a way to see if the page is done +// loading all the data it needs. +Meteor._LivedataConnection._allConnections = []; +Meteor._LivedataConnection._allSubscriptionsReady = function () { + return _.all(Meteor._LivedataConnection._allConnections, function (conn) { + for (var k in conn.sub_ready_callbacks) + return false; + return true; + }); +}; diff --git a/packages/spiderable/package.js b/packages/spiderable/package.js new file mode 100644 index 00000000000..6951f41fc04 --- /dev/null +++ b/packages/spiderable/package.js @@ -0,0 +1,10 @@ +Package.describe({ + summary: "Makes the application crawlable to web spiders." +}); + +Package.on_use(function (api) { + api.use(['templating'], 'client'); + + api.add_files('spiderable.html', 'client'); + api.add_files('spiderable.js', 'server'); +}); diff --git a/packages/spiderable/spiderable.html b/packages/spiderable/spiderable.html new file mode 100644 index 00000000000..7cbdf71b899 --- /dev/null +++ b/packages/spiderable/spiderable.html @@ -0,0 +1 @@ + diff --git a/packages/spiderable/spiderable.js b/packages/spiderable/spiderable.js new file mode 100644 index 00000000000..153baa4f0ef --- /dev/null +++ b/packages/spiderable/spiderable.js @@ -0,0 +1,90 @@ +(function () { + var fs = __meteor_bootstrap__.require('fs'); + var spawn = __meteor_bootstrap__.require('child_process').spawn; + var querystring = __meteor_bootstrap__.require('querystring'); + var app = __meteor_bootstrap__.app; + + // how long to let phantomjs run before we kill it + var REQUEST_TIMEOUT = 15*1000; + + app.use(function (req, res, next) { + if (/\?.*_escaped_fragment_=/.test(req.url)) { + // get escaped fragment out of the url. + var idx = req.url.indexOf('?'); + var preQuery = req.url.substr(0, idx); + var queryStr = req.url.substr(idx + 1); + var parsed = querystring.parse(queryStr); + delete parsed['_escaped_fragment_']; + var newQuery = querystring.stringify(parsed); + var newPath = preQuery + (newQuery ? "?" + newQuery : ""); + var url = "http://" + req.headers.host + newPath; + + // run phantomjs + // + // Use '/dev/stdin' to avoid writing to a temporary file. Can't + // just omit the file, as PhantomJS takes that to mean 'use a + // REPL' and exits as soon as stdin closes. + var cp = spawn('phantomjs', ['--load-images=no', '/dev/stdin']); + + var data = ''; + cp.stdout.setEncoding('utf8'); + cp.stdout.on('data', function (chunk) { + data += chunk; + }); + + cp.on('exit', function (code) { + if (0 === code && //i.test(data)) { + res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'}); + res.end(data); + } else { + // phantomjs failed. Don't send the error, instead send the + // normal page. + if (code === 127) + Meteor._debug("spiderable: phantomjs not installed. Download and install from http://phantomjs.org/"); + else + Meteor._debug("spiderable: phantomjs failed:", code, data); + + next(); + } + }); + + // don't crash w/ EPIPE if phantomjs isn't installed. + cp.stdin.on('error', function () {}); + + cp.stdin.write( + "var url = '" + url + "';" + +"var page = require('webpage').create();" + +"page.open(url);" + + +"setInterval(function() {" + +" var ready = page.evaluate(function () {" + +" if (typeof Meteor !== 'undefined' && Meteor.status().connected) {" + +" Meteor.flush();" + +" return Meteor._LivedataConnection._allSubscriptionsReady();" + +" }" + +" return false;" + +" });" + + +" if (ready) {" + +" var out = page.content;" + +" out = out.replace(/]+>(.|\\n|\\r)*?<\\/script\\s*>/ig, '');" + +" out = out.replace('', '');" + + +" console.log(out);" + +" phantom.exit();" + +" }" + +"}, 100);"); + cp.stdin.end(); + + // Just kill it if it takes too long. + setTimeout(function () { + if (cp && cp.pid) { + cp.kill(); + } + }, REQUEST_TIMEOUT); + + } else { + next(); + } + }); +})();