Use JavaScript to allow dynamic content in a page cache. In this episode I show you how to insert the user-specific content into a page through JavaScript.
First I thought that it is not a good Idea to only hide the links, because "FireBug"-Users will find them, but as you can restrict the execution of these (often restful) actions it is not that big problem, if someone knows them.
Have you thought about using the session cookie do determine whether a user is signed in? This way a static javascript file could optionally enable the admin links and a request to users/show.js would be eliminated. I'm currently trying to implement this on one of my projects, but there are two issues:
1. The rails session cookie is difficult to decode and parse using javascript.
2. The application could set a cookie on login for this purpose, however I'm still figuring out how to integrate this with Authlogic.
If you store username in cookie on login, and clear it on logout, you can avoid hitting Rails. Javascript would then use this cookie to hide or display links on the page. This way all can be done entirely client side.
Then, in my site javascript file, I use the last part of this jQuery cookie plugin (the first two-thirds are for writing cookies with jQuery, which we won't need):
Why not use fragment caching for the central part (parts) and generate layout (with header) each time the page is accessed? Not much difference in speed, but it might be cleaner. The only exceptions are Edit / Destroy links, which can be added through JS. This way is also unobtrusive (except admin links, which are not so important for unobtrusiveness).
Andy: Search engines? Other than that I don't care about it either.
Meanwhile, if you don't care about customer not having JS why do you need page caching in the first place? You can make all links as JS calls and reload just required part(s) of the page (http://vh-daf.ru/service -- it is in Russian, but try to click on the top submenu and you get the idea). Just because of 0.5% (or less) weirdos who still want to press reload button? :)
In fact on this site you can disable JS and still get (almost) the same result, but I would not say it is easy for more complicated cases and I have done non-JS links for Google, not for humans.
A bit confusing topic -- hard to realize where the border line is between traditional and JS design.
@Pawel, Storing the username in a cookie and parsing it in JavaScript is a great idea, although it is quite specific to this solution. Here I try to give a more generic solution that will work for any kind of dynamic content (such as something that may not be dependent on the current user's actions).
I'll play around with this a bit more and possibly cover the cookie solution in a future episode.
Thanks Ryan...great screencast. I'm curious how this looks in production. Is there a slight "jump" or flicker to the page as it appears to have loaded, and then a second update is made? Or is not noticeable. Thanks!
Brian
@Duncan - to "1": you cannot use session cookies at all from Javascript, since for security purposes they are sent as HTTP-only cookies (not just by Rails but by nearly all servers these days). If you want to access something from JS, you must set a "regular" cookie.
I used this technique two years ago for different reasons:
When you log out and go back in the browser using the page cache you go back to pages where you are logged in. The problem was I had pages loading dynamic content only accessible while logged in in the session, and going back to such a page would bring up the server-generated code for "logged in" from the browser's page cache, but the AJAX would produce a failure.
So I turned to the store-login-info-in-extra-cookie solution proposed in the comments above.
However, I removed the code again, among other things (like complexity) because of this...
The disadvantage of any cookies used as a replacement for browser-side storage is that they are uselessly sent back to thee server with each and every HTTP request. Apart from the bytes going over the wire, I believe I saw a presentation (from the YUI guys in the collection of great tech. videos in the YUI theater) where this was identified as having a significant (meaning noticeable overall) performance impact. One of the 14 main pieces of advice for website performance is to reduce the number of cookies as much as possible.
I suggest not to use cookies for client-side storage, as tempting as it is. There already are tools (e.g. a module in Dojo, persist-js, a module planned for by and for YUI in the near future) trying to give a consistent API for the many client-side storage implementations (ranging from Flash to HTML5-type global and local storage in Firefox 3(.5)).
The problem with hacks is they tend to persist and byte you later, when you forgot you had implemented them. So let's use *real* client-side storage if you must, especially since the problem you are trying to solve is not THAT pressing.
(sorry for posting sooo muuuch, but I think it's valuable content???)
...and another excellent performance presentation, this one also from a YUI guy:
http://www.slideshare.net/natekoechley/high-performance-web-sites-2008 (Cookies starting on slide 57)
@Brian: There won't be any flicker with THIS example in (even mildly) modern browsers.
Main reason: the Javascript is loaded from the HEAD part of the page, and that means it is fully loaded and executed BEFORE the page is loaded and rendered. Of course, because of the wait for the DOM-ready event in the Javascript the actual work done by the script happens at about the time the good old <body onLoad="..."> would execute, but there is so little to do for the script execution will be instantaneous for all practical purposes.
I guess you still could get a flicker, but only if the layout change was major, i.e. the page has a complicated layout with lots of elements in it, and a lot of areas on the page would be impacted. In THIS example only thus far empty areas are affected, no other elements already on the page change in any way.
Perhaps this is my unfamiliarity with JavaScript showing, but I'd be concerned with the security of this -- what's to prevent somebody from using a client-side override to enable the hidden admin links? Or do you not care if the links become active because other checking will prevent them from working if the user isn't an admin?
(responding to the concerns of some people, again longer since several commentators voiced concerns)
There seems to be a BIG misunderstanding about security in a web application.
Hiding (JS) code or links does not provide any security. Why?
Well, let's look at an analogy in the real world. This argument needs two steps.
1) Is your house more secure because you don't tell anyone you hid the key under the second big stone from the left?
2) If you just said yes, would you still say so if I change the scenario by saying "if there are hundreds of people searching around your house for a way in 24 hours a day, 7 days a week?
My point is, in a world of a village hiding links on the webpage is like hiding the key under the stone and may be "secure" enough in that environment.
On the Internet, where hundreds of thousands of automated (often hijacked) computers probe all reachable servers and services all the time (which does not mean they are all on *your* server at the same time, but some of them always are probing your systems!), this is NOT secure in ANY way.
On the Internet, the only way to achieve security is by securing the services themselves, not by hiding that those services exist.
In this context it means you add a before_filter where you check if there is a user with the appropriate rights logged in in the current session. If you do that you don't need to care if anyone finds your links. Finding admin-links in a Rails application is VERY easy - unless you deviate from standards such as common naming conventions and REST significantly - which in turn makes your life so much more difficult in the long run.
Thanks for the snippets everyone. I've forked Ryan's code and implemented the cookie solution: http://github.com/lemonbbq/railscasts-dynamic-page-caching/
It seems to work quite well as rails doesn't even get hit on the cached requests. Like the session cookie, the caching cookie doesn't have an expiry date set. This means when the user quits their browser, both cookies are cleared and the user is signed out normally.
Currently the user's name and admin status are stored in separate cookies. I'd like to combine these (possibly along with other attributes) into a single cookie using JSON. Everything works fine on the rails side, but I'm having problems getting JavaScript to unescape the JSON string - the spaces get replaced by plus signs. I'll have to work on this.
Integrating with Authlogic should be a breeze: one would simply need to add callbacks for after_save, after_destroy and after_persisting to set and destroy the caching cookie.
@Michael: You raise an interesting point about the performance hit from using cookies. From YUI's results it seems that only cookies larger 500 bytes really slow down the response time. Surely the increase in performance from page caching would outweigh the effect of slightly larger cookies. Though I agree that the increased complexity would be nice to avoid.
Glad to see more people paying attention to this technique! I've been presenting on this (which I've been calling progressive caching) for a few months now, and have a couple of blog posts:
The above issues notwithstanding, you don't actually need to use JS to search for the DIVs you want to unhide.
Instead of setting "display: none" on every element, just add a class to it like "adminContent".
The main CSS file should specify that *.adminContent should have display: none.
It should also specify that a common parent, say body, when assigned an isAdmin class, should affect its children:
body.isAdmin *.adminContent {
display: block;
}
The dynamic JS would then need to simply add the "isAdmin" class to the body tag and the browser will take care of the rest due to CSS cascading and a higher specifity score of the above rule that unhides than the generic one that hides. It's also significantly faster than doing it manually with JS dom searching if you have a large DOM tree.
Please do an episode on the new nested forms patch, introduced in Rails 2.3. All examples I have seen so far are for 1-M associations. Please demonstrate for both 1-1 and 1-M. I guess it is really no difference apart from a hasOne instead of hasMany in the model, but just for completions sake, would be nice with both examples included ;)
Thanx for some great screencasts...
The main drawbacks some of the above suggestions are:
1) Link to pages that shouldn't be indexed will have a negative impact for you when google does it's link matrix magic - PageRank (and no, rel="nofollow" does not help you!).
2) Stuff like hidden links and javascript inserts might cause problem for programs used by the blind.
3) Cluttering your html with a lot of stuff you'll instantly hide and keep hidden during the entire stay on the page just isn't clean - KISS, Broadband, Rendering, (and some would even argue, Security through obscurity).
Keep up the nice work Ryan, you make Mondays a bit nicer!
Hey Ryan, I owe you so much! Concerning page caching and dynamic content I read about ESI which sounds like a cool method. I will mix your way of hiding the admin links with some ESI partials with Easy-esi: http://github.com/grosser/easy_esi. Maybe ESI (though not really new) is worth an episode?
I consent with the above comments. In my views you can exploit a cookie to accumulate the user name and the role, to avoid to hit the users controller.
Thanks for the post.I really pleased to read it.I do agree that one can utilize a cookie to accrue the user name and the role, to evade to smack the users controller.
You raise an interesting point about the performance hit from using cookies. From YUI's results it seems that only cookies larger 500 bytes really slow down the response time. Surely the increase in performance from page caching would outweigh the effect of slightly larger cookies. Though I agree that the increased complexity would be nice to avoid.
Great idea and great screencast.
First I thought that it is not a good Idea to only hide the links, because "FireBug"-Users will find them, but as you can restrict the execution of these (often restful) actions it is not that big problem, if someone knows them.
Really great idea .
Thanks for another great screencast, Ryan!
Have you thought about using the session cookie do determine whether a user is signed in? This way a static javascript file could optionally enable the admin links and a request to users/show.js would be eliminated. I'm currently trying to implement this on one of my projects, but there are two issues:
1. The rails session cookie is difficult to decode and parse using javascript.
2. The application could set a cookie on login for this purpose, however I'm still figuring out how to integrate this with Authlogic.
If I make any progress, I'll share it here.
If you store username in cookie on login, and clear it on logout, you can avoid hitting Rails. Javascript would then use this cookie to hide or display links on the page. This way all can be done entirely client side.
Duncan - I got this working in my Rails app (using Authlogic). It basically looks like this:
(trying out Pastie, let's see if it works...)
In application_controller.rb:
<script src='http://pastie.org/535397.js'></script>
Then, in my site javascript file, I use the last part of this jQuery cookie plugin (the first two-thirds are for writing cookies with jQuery, which we won't need):
http://plugins.jquery.com/project/Cookie
And then:
<script src='http://pastie.org/535398.js'></script>
Now, whenever you're logged in, jQuery seeks out the p tag with an id of "account" and fills it with the user's information.
Aaah, Pastie didn't work like I'd thought. Well, I'll post it inline. It goes:
(in application_controller)
before_filter :set_user_info_cookie
private
def set_user_info_cookie
cookies[:user_info] = { :value => current_user.email, :expires => 1.hour.from_now } if current_user
cookies.delete :user_info unless current_user
end
jQuery snippet:
if ($.cookie('user_info')) {
$("p#account").html($.cookie('user_info') + " | <a href='/myaccount'>My Account</a> | <a href='/logout'>Logout</a>");
};
Pawel: My thoughts exactly. People, wake up. Put your presentation logic on the client-side -- yes, that means JavaScript.
I wonder how much longer it will take the majority of Rails programmers to realize this.
Chris: Looks like a good example. But you can set the cookie through JavaScript/jQuery as well. Even less work for the server.
Why not use fragment caching for the central part (parts) and generate layout (with header) each time the page is accessed? Not much difference in speed, but it might be cleaner. The only exceptions are Edit / Destroy links, which can be added through JS. This way is also unobtrusive (except admin links, which are not so important for unobtrusiveness).
Hi Ryan, I agree with the other comments above. You can use a cookie to store the username and the role, to avoid to hit the users controller.
Of course this technique doesn't expose the app to any security hole, because of the server-side check when an administrative link will be clicked.
@Duncan here a snippet (and a related blog post too) for parse cookies with Prototype: http://bit.ly/yPStP
Victor Moroz: I'm probably going to be crucified for saying this, but I no longer care about the 0.5% (or less) weirdos who have JavaScript disabled.
It's a technique. You may need this in situations where cookies/JS is not a viable or elegant solution. Calm down.
Andy: Search engines? Other than that I don't care about it either.
Meanwhile, if you don't care about customer not having JS why do you need page caching in the first place? You can make all links as JS calls and reload just required part(s) of the page (http://vh-daf.ru/service -- it is in Russian, but try to click on the top submenu and you get the idea). Just because of 0.5% (or less) weirdos who still want to press reload button? :)
In fact on this site you can disable JS and still get (almost) the same result, but I would not say it is easy for more complicated cases and I have done non-JS links for Google, not for humans.
A bit confusing topic -- hard to realize where the border line is between traditional and JS design.
@gUI, yes.
@Pawel, Storing the username in a cookie and parsing it in JavaScript is a great idea, although it is quite specific to this solution. Here I try to give a more generic solution that will work for any kind of dynamic content (such as something that may not be dependent on the current user's actions).
I'll play around with this a bit more and possibly cover the cookie solution in a future episode.
Thanks Ryan...great screencast. I'm curious how this looks in production. Is there a slight "jump" or flicker to the page as it appears to have loaded, and then a second update is made? Or is not noticeable. Thanks!
Brian
@Duncan - to "1": you cannot use session cookies at all from Javascript, since for security purposes they are sent as HTTP-only cookies (not just by Rails but by nearly all servers these days). If you want to access something from JS, you must set a "regular" cookie.
I used this technique two years ago for different reasons:
When you log out and go back in the browser using the page cache you go back to pages where you are logged in. The problem was I had pages loading dynamic content only accessible while logged in in the session, and going back to such a page would bring up the server-generated code for "logged in" from the browser's page cache, but the AJAX would produce a failure.
So I turned to the store-login-info-in-extra-cookie solution proposed in the comments above.
However, I removed the code again, among other things (like complexity) because of this...
The disadvantage of any cookies used as a replacement for browser-side storage is that they are uselessly sent back to thee server with each and every HTTP request. Apart from the bytes going over the wire, I believe I saw a presentation (from the YUI guys in the collection of great tech. videos in the YUI theater) where this was identified as having a significant (meaning noticeable overall) performance impact. One of the 14 main pieces of advice for website performance is to reduce the number of cookies as much as possible.
I suggest not to use cookies for client-side storage, as tempting as it is. There already are tools (e.g. a module in Dojo, persist-js, a module planned for by and for YUI in the near future) trying to give a consistent API for the many client-side storage implementations (ranging from Flash to HTML5-type global and local storage in Firefox 3(.5)).
The problem with hacks is they tend to persist and byte you later, when you forgot you had implemented them. So let's use *real* client-side storage if you must, especially since the problem you are trying to solve is not THAT pressing.
I found the presentation about the impact of cookies on website performance:
http://yuiblog.com/blog/2007/03/01/performance-research-part-3/
(sorry for posting sooo muuuch, but I think it's valuable content???)
...and another excellent performance presentation, this one also from a YUI guy:
http://www.slideshare.net/natekoechley/high-performance-web-sites-2008 (Cookies starting on slide 57)
@Brian: There won't be any flicker with THIS example in (even mildly) modern browsers.
Main reason: the Javascript is loaded from the HEAD part of the page, and that means it is fully loaded and executed BEFORE the page is loaded and rendered. Of course, because of the wait for the DOM-ready event in the Javascript the actual work done by the script happens at about the time the good old <body onLoad="..."> would execute, but there is so little to do for the script execution will be instantaneous for all practical purposes.
I guess you still could get a flicker, but only if the layout change was major, i.e. the page has a complicated layout with lots of elements in it, and a lot of areas on the page would be impacted. In THIS example only thus far empty areas are affected, no other elements already on the page change in any way.
Perhaps this is my unfamiliarity with JavaScript showing, but I'd be concerned with the security of this -- what's to prevent somebody from using a client-side override to enable the hidden admin links? Or do you not care if the links become active because other checking will prevent them from working if the user isn't an admin?
About Security
(responding to the concerns of some people, again longer since several commentators voiced concerns)
There seems to be a BIG misunderstanding about security in a web application.
Hiding (JS) code or links does not provide any security. Why?
Well, let's look at an analogy in the real world. This argument needs two steps.
1) Is your house more secure because you don't tell anyone you hid the key under the second big stone from the left?
2) If you just said yes, would you still say so if I change the scenario by saying "if there are hundreds of people searching around your house for a way in 24 hours a day, 7 days a week?
My point is, in a world of a village hiding links on the webpage is like hiding the key under the stone and may be "secure" enough in that environment.
On the Internet, where hundreds of thousands of automated (often hijacked) computers probe all reachable servers and services all the time (which does not mean they are all on *your* server at the same time, but some of them always are probing your systems!), this is NOT secure in ANY way.
On the Internet, the only way to achieve security is by securing the services themselves, not by hiding that those services exist.
In this context it means you add a before_filter where you check if there is a user with the appropriate rights logged in in the current session. If you do that you don't need to care if anyone finds your links. Finding admin-links in a Rails application is VERY easy - unless you deviate from standards such as common naming conventions and REST significantly - which in turn makes your life so much more difficult in the long run.
Thanks for the snippets everyone. I've forked Ryan's code and implemented the cookie solution: http://github.com/lemonbbq/railscasts-dynamic-page-caching/
It seems to work quite well as rails doesn't even get hit on the cached requests. Like the session cookie, the caching cookie doesn't have an expiry date set. This means when the user quits their browser, both cookies are cleared and the user is signed out normally.
Currently the user's name and admin status are stored in separate cookies. I'd like to combine these (possibly along with other attributes) into a single cookie using JSON. Everything works fine on the rails side, but I'm having problems getting JavaScript to unescape the JSON string - the spaces get replaced by plus signs. I'll have to work on this.
Integrating with Authlogic should be a breeze: one would simply need to add callbacks for after_save, after_destroy and after_persisting to set and destroy the caching cookie.
@Michael: You raise an interesting point about the performance hit from using cookies. From YUI's results it seems that only cookies larger 500 bytes really slow down the response time. Surely the increase in performance from page caching would outweigh the effect of slightly larger cookies. Though I agree that the increased complexity would be nice to avoid.
Glad to see more people paying attention to this technique! I've been presenting on this (which I've been calling progressive caching) for a few months now, and have a couple of blog posts:
http://www.culann.com/2009/04/progressive-caching
and
http://www.viget.com/extend/progressive-caching-in-depth/
The above issues notwithstanding, you don't actually need to use JS to search for the DIVs you want to unhide.
Instead of setting "display: none" on every element, just add a class to it like "adminContent".
The main CSS file should specify that *.adminContent should have display: none.
It should also specify that a common parent, say body, when assigned an isAdmin class, should affect its children:
body.isAdmin *.adminContent {
display: block;
}
The dynamic JS would then need to simply add the "isAdmin" class to the body tag and the browser will take care of the rest due to CSS cascading and a higher specifity score of the above rule that unhides than the generic one that hides. It's also significantly faster than doing it manually with JS dom searching if you have a large DOM tree.
Hey Ryan,
Great episode, as usual.
I was wondering how much of this you can achieve with the recently added support for caches_action ..., :layout => false.
Regards
Hi Ryan,
Please do an episode on the new nested forms patch, introduced in Rails 2.3. All examples I have seen so far are for 1-M associations. Please demonstrate for both 1-1 and 1-M. I guess it is really no difference apart from a hasOne instead of hasMany in the model, but just for completions sake, would be nice with both examples included ;)
Thanx for some great screencasts...
Hey! Great episode Ryan.
I've also translated it to Prototype for those who don't want to install the JQuery library:
http://handyrailstips.com/tips/18-dynamic-page-caching-with-prototype
Cheers
The main drawbacks some of the above suggestions are:
1) Link to pages that shouldn't be indexed will have a negative impact for you when google does it's link matrix magic - PageRank (and no, rel="nofollow" does not help you!).
2) Stuff like hidden links and javascript inserts might cause problem for programs used by the blind.
3) Cluttering your html with a lot of stuff you'll instantly hide and keep hidden during the entire stay on the page just isn't clean - KISS, Broadband, Rendering, (and some would even argue, Security through obscurity).
Keep up the nice work Ryan, you make Mondays a bit nicer!
tast bud...
Super post!
Hey Ryan, I owe you so much! Concerning page caching and dynamic content I read about ESI which sounds like a cool method. I will mix your way of hiding the admin links with some ESI partials with Easy-esi: http://github.com/grosser/easy_esi. Maybe ESI (though not really new) is worth an episode?
Cheers, Val
Had to add :locals => { :flash => flash } to the render partial as it was barfing whiny nils if I didn't.
oh, nice service.
what does this mean?
thank youuu
thank youuu:)
thank you admin thanksss
thanksss admin
thank youuu
. I hope I can improve through learning this respect. Bu
Gerek teknik altyapımız gerekse isabetli ekspertiz analizlerimizle sektörümüzde fark yaratmak ve
koşulsuz memnuniyet sağlamak için çalışmaktayız.
evdne ev duvar kagidi
Great article, hey I stumbled on to this post
<div id="forums">
<% for forum in @forums %>
<div class="forum">
<h2><%= link_to h(forum.name), forum %></h2>
<p><%=h forum.description "%></p>
<p class="admin" style="display:none">
<%= link_to "Edit", edit_forum_path(forum) %> |
<%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %>
</p>
</div>
<% end %>
</div>"
I consent with the above comments. In my views you can exploit a cookie to accumulate the user name and the role, to avoid to hit the users controller.
Thanks for the post.I really pleased to read it.I do agree that one can utilize a cookie to accrue the user name and the role, to evade to smack the users controller.
This one is enlightened blog post. Thanks a lot for sharing your valuable views through this blog.
Hi all,
Is it possible to use page caching, but then pull in an entire form via ajax so that it has a fresh authenticity token?
Is that inadvisable for any particular reason?
Thanks,
Raviv
Hi Ryan,
I'm trying to get your example to work, but having no luck with content_for. There is never any content added in the layout where you have
<%= yield :head %>
I pulled the latest code off github and having the same issue.
Forget my comment above. It's working. Not sure what i was doing wrong. I updated the project to use bundler and it works fine. Thanks.
You raise an interesting point about the performance hit from using cookies. From YUI's results it seems that only cookies larger 500 bytes really slow down the response time. Surely the increase in performance from page caching would outweigh the effect of slightly larger cookies. Though I agree that the increased complexity would be nice to avoid.
Why just put the link stuff in the admin? Condition inside the js.erb partial ? Would be better than the display none :/