#117 Semi-Static Pages
Jul 07, 2008 | 14 minutes | Controllers
Static pages can sometimes be a little awkward to add to a Rails app. See a couple different solutions to this problem in this episode.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
Hi Ryan,
Nice episode. I would like to know your opinion on something – when I face this issue, I still use the base show action (find(params[:id])) and create a helper for hyperlinks that append a “clean name/permalink” after the /show/:id.
For example: page title is “About us” and then my helper will change it to /show/1/about_us, or whichever trailing route I specify. I have used this method for a while and believe it would be more efficient since the find would be quicker db-wise and is not dependant on permalink, which can be dangerous in some instances if someone breaches the idea of a permalink (ie; a client not understanding the concept). Do you see any disadvantages besides the fact the ID is still present in the URL and, I guess, the possible length of some URLs?
Cheers,
Adam
Should specify that the appended “nice link” is ignored by my route, and is only present for readability and the google-types
Hi Ryan,
Another great episode. Quick note for the show- your nifty generators gem:
gem install ryanb-nifty-generators --source=http://gems.github.com
Good work Ryan. A few things to add:
I think it would be wise to create a content_html field in your database and apply the RedCloth filter in the model upon saving instead of in the view. This will give faster page loads because we are only applying the filter on write(not reads) and we keep our views dumb, as they should be.
Also, by default RedCloth ignores single line brakes and converts double line brakes to a new paragraph. So, to convert single line brakes to 'br' tags do like so...
RedCloth.new( @page.content, [:hard_breaks] ).to_html
I've used a similar approach to create a really small embedded wiki. However, I was never happy with the internal links between the pages. Any thoughts on how to best include links in the semi-static pages?
I've often wanted Rails to give me bang methods like "find_by_permalink!" to save me the work of manually raising an ActiveRecord::RecordNotFound exception.
sintaxi - maybe will be better to use some sort of caching which could be better and cleaner.
One thing I'm definitely missing here is overriding the to_param method of a model. That's pretty much invaluable when talking about permalinks.
Other than that - solid work as usual.
@Adam, Including the id in the URL is a good solution for many cases because it can make your routes clean and simple. Usually this is implemented with overriding the to_param method in the model. See episode 63 for details (now linked in the show notes).
That said, I think semi-static pages are an exception. I prefer to have the URL very clean here without the ID. Not sure why exactly.
@Gavin, oh yeah, I knew I forgot something! Linked in the show notes now. Thanks.
@sintaxi, right, I should have mentioned the acts_as_textiled plugin for doing this which is now linked in the show notes. Regarding the line breaks, this is the big reason I prefer the latest version of RedCloth (3.301) because it handles single line breaks much better. Even with the option specified in the older version I found it never quite worked right.
@Johannes, with the first approach each page has a named route, so it would be about_path, contact_path, etc. With the second approach you would use the static_path('about'), static_path('contact'), or static_path(page.permalink).
@Wincent, I would like to see this too. Please consider submitting a ticket on rails.lighthouseapp.com if there isn't one already. Link it here if you want afterwards. :)
@Clemens, I left this off on purpose as it's a global way to change how the params are made, and here I prefer to keep the id for the admin interface. However, I should have mentioned episode 63 which covers how to use to_param. That's now in the show notes. Thanks.
I accomplish semi-static pages by having a 'page' boolean attribute in my Post model (which defaults to false). I then have a class method Post.find_page(name) which takes the permalink name as an argument, as well as Post.find_pages(opts = {}). I like my static pages to have URLs like '/identity', '/contact', etc., so in routes.rb:
map.page '/:page', :controller => 'posts', :action => 'page'
Quite simple, really.
Great episode as always.
I have a couple thoughts. First, someone needs to mention that page caching for static pages can be a huge benefit. NginX and Apache can both server cached pages directly without having to even touch the framework. My guess is that others can as well, I just have not used them. (c.f. Episode 115)
@Johannes: I use a similar system fro the help pages at antarestrader.com. I use this code to replace [[wiki links]] with Markdown Links:
txt= txt.gsub(/\[\[(.*?)\]\]/) do |link|
"[#{$1}][/help/" + $1.downcase.gsub(/\s/,'_') + "]"
end
(I'm not sure how this will render in Ryan's page) This code dated from an old version of RedCloth, and it would now be a better to add a rule to the parser. See the RedCloth docs.
Finally, I would be interested in an episode that showed how to use JavaScript to add dynamic content to mostly static pages. For example, adding the edit and destroy links only for logged-in administrators.
Thanks Again,
StarTrader
@StarTrader, excellent point, these static pages are a prime candidate for page caching. Regarding showing the admin links on these pages, I recommend using an "admin" subdomain and showing the links there. With some Apache rewrite rules you can get page caching to not apply to this admin domain.
You may want to look at a plugin called Comatose, which handles what you covered. It's basically a micro CMS that's packaged as a plugin. I've used it on many projects successfully.
Thanks for another great cast Ryan. For sites that have a lot of dynamic content and little static content your approach is great.
I'm wondering though, if a site is mostly static, is rails the right framework to be developing in? Would another Ruby framework like Merb or Sinatra be more appropriate?
@bryce, I'd say use what you know. Rails may not be the perfect tool for the job, but if it is what you're comfortable with and you know how it works, use it.
Certainly there is extra expense for choosing a heavyweight tool (Rails) for something so simple, but that expense may not be as large as learning a new language or framework. You'll have to be the judge of that.
One thing interesting about mostly-static sites is that the meat of the site isn't in the app behavior, it's in the template or HTML code. That part is easy to transition to another framework if you find out Rails doesn't fit the bill as well as you'd like.
Funny but for semi-static pages or fully static page I actually reached the point where I'm not going through Rails at all.
There are a bunch of very good CMS around and after taking my own stab at editing the semi-static page through Rails (and then having to extend it so different people could edit the content and then review it and that little bell and whistle) I realized: "why reinvent the wheel"...
so, on my latest project I'm playing with Apache and mod_rewrite to direct the requests either to Rails for the application or to my CMS for the static/semi-static pages.
it took a little more work from a configuration stand-point (especially the CSS part of it) but the end result is that both aspect of the site are complete without having me rewritting yet another cms.
Same days ago I ran in trouble installing RedCloth. Maybe I solved the problem with creating a symlink. I discribed my problem and the possible solution here: http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/976d2c67405c83c1/d5afc15178b550b6?show_docid=d5afc15178b550b6#
Can anybody say if that's correct? Or do I hae another problem?
Thanks Ryan. I am working on a project that required some static pages and I had already implemented a pages controller/action based approach, but it left me feeling uneasy about the scalability and management of the static content. So in about 30 minutes I converted it over to your more dynamic and RESTFUL solution. Very awesome. I love coming here, learning something new and then revisiting my code to make things better.
How are static pages the way you show them on the first example non-restful?
GET /about => renders the "about us" resource
I don't see how that isn't restful (ok, so you don't allow different mime-types, add to the routes "/about.:format", and you're about done)
But saying "it's not restful" because it's not using a so-called "restful controller" makes no sense =)
Could you do:
links=Static.all.collect {|a| a.permalink}
and then only trigger that route if the permalink is an existing permalink?
Ryan, you are a Rails God.
Could you discuss :include with model finds? Especially more complicated areas of it..having problems finding concrete info on it.
This is a very nice approach, but I've a few concerns on about how it would be used with multiple environments.
Say your legal department requests a change to the wording of the Privacy page. You'd probably want to deploy to a staging site first for them to review it before going live. But if you change the page through the admin web interface, then that change isn't versioned, which goes against known best practice.
So you'd probably need to write a migration but a migration containing a page of text (or Textile) doesn't seem very appealing.
I suppose you could create a Rake task that generates a migration automatically though?
Andy
Andy,
Using some ActiveResource you could also allow the content of your page to be copied from and to an environment using the URL of the resource
Great episode!
On permalinks: there are a number of plugins to transparently handle pretty urls. My own cotribution (http://code.estadobeta.com/plugins/sluggable_finder) turns any field you want into a valid permalink and overwrites AR.to_param and AR.find so you don't need to change anything in your controllers. It also raises AR::RecordNotFound just like with integer IDs. It also handles UTF8 correctly and uniqueness of permalinks (my reason to write the plugin in the first place). Enough with the plug.
Something worth checking out is StaticMatic (http://staticmatic.rubyforge.org/). It's good for personal homepages and documentation.
@Nicolás, very good point, and you're correct. The old approach is RESTful, I should have been specific and said the controller isn't a RESTful style.
@Tal, hmm, I'm not sure I follow. Would that database find go in the routes? I don't know of an easy way to do that.
@Allyn, I mentioned :include back in episode 22, but, if it works out, I plan to go into more detail on it in the next Everyday Active Record episode.
@Andy, you can still do versioning through the database with something similar to the acts_as_versioned plugin.
That said, normally static content is just text. You can toss around a text file to all involved until you get it right, and then put it on the site. I'm not sure a staging environment is entirely necessary for such simple content. But, of course it depends on the site requirements.
Ryan,
Great screencast - I've been wrestling with static pages and how to handle them over the past couple of weeks.
One thing though... Is the nifty-generators gem no longer available from github? I've tried the link in the comments above (the same one as in the readme file) "gem install ryanb-nifty-generators --source=http://gems.github.com" and I keep getting this message back "ERROR: could not find ryanb-nifty-generators locally or in a repository" - is it something with github or user error on my end?
Ryan,
I mean find some way to have the route filter ONLY for the permalinks that exist. Dynamically rather then manually making an array of all of the permalinks you want to go though.
Great stuff!! Thanks.
I think that in more complex cases, it pays to replace the simple view's text filed, with a nice javascript based wysiwyg editor, for example the fantastic FCKeditor (http://www.fckeditor.net/)
This is so much easier for simple users to grasp then RedCloth.
@Nadav: yes, you're right that wysiwyg editors are easier for simple users, but the problem is that they are generally given the ability to do things that they shouldn't be allowed to do, which destroy the look and feel of the site.
Using something like Markdown is a nice way to solve this, making it easy to write semantic markup (indirectly of course, via Markdown's transformation into HTML) and harder to write non-semantic HTML (which you can do if you embed HTML in your Markdown).
And once you get the hang of it, it's much much easier to use than a wysiwyg editor, even though as you point out those are easier at first for simple users.
@Ryan: toward the end of the screen cast the tone of your voice made me think that you might be on the verge of mentioning possible *security* problems with using the catch-all route:
map.static 'static/:permalink', :controller => 'pages', :action => 'show'
Is there any possible security vulnerability created by having such a route? I can't think of any, but the degree of concern in your voice at *anything* going through the PagesController seemed to hint at possible security problems.
Or am I on crack?
@ Matthew Higgins (re: StaticMatic) that looks like a great solution for *generating* static pages, but it doesn't look like it deals with the question of how to integrate them into your Rails app.
Of course maybe you didn't mention it because it's too obvious--just put them in /public. I'd love to hear any downsides anyone can see (especially Ryan) to simply keeping static pages in /public, besides the obvious one of them being more of a pain to edit.
The reason I'm asking is I have a large static site to convert to Rails, and it seems that just dropping it into /public is the logical starting point, while I gradually replace static bits with Rails. Any tips tricks and gotchas are welcome!
Hello,
I am new to rails. I'am using nifty-generators. When i scaffold page, i get in views
<% title "Pages" %>. I am making this at fresh rails project. When i run mongrel, I get error like this: "undefined method `title'". How to fix this?
Brandon: I think it's a performance concern more than anything, since now your pages controller might start handling a bunch of requests that otherwise would have been handled more efficiently higher up the chain. It's also possible you don't want your page editors to be able to carve out chunks of your root URLspace.
Swist: Ryan uses a helper method to create titles without duplication. There's probably some way to make nifty-generators create the helper method, but all it is is:
def title(title)
content_for(:title) { title }
end
and a yield(:title) in the appropriate spot in the site layout.
Hi Ryan, and everybody, i have the following question, i am using the following code:
map.static ':permalink', :controller => 'pages', :action => 'show'
but in my application when i want to link from a different controller for example the session controller to a static page.
It is always add the previous controller to the url, like this.
http://0.0.0.0:3000/session/permalink
How can i fix this, thank you for all your great job.
Hey Ryan,
I was wondering how I can combine nested routing with this. I want static pages, but in sections...so URLs like this:
/issues/issue1
/issues/issue2
I tried looking up nested routing and it all points me in the direction of URLs like this
/category/1/page/34
Any help would be appreciated!
Ryan,
This was a great episode. I've long done this but still strive to make it elegant. The major hangup I have with this is getting an index page to display but not be required. I usually end up defining a map.root with a :action => show and :permalink => index for the page I created. My problem with this is if the page isn't in the database, how do I get the route to fall back to the :action => index?
Thanks for all your ideas and time.
This idea occurred to me awhile ago to handle one client's need for a CMS, it probably could use more work, but I pluginized the thing to use across some other applications that were going to need the same sort of setup. Anyone who is interested can grab it here:
http://github.com/neshmi/super_simple_cms/tree/master
Of course maybe you didn't mention it because it's too obvious--just put them in /public. I'd love to hear any downsides anyone can see (especially Ryan) to simply keeping static pages in /public, besides the obvious one of them being more of a pain to edit.
Another vote here for the Comatose micro-CMS plugin... very easy to integrate with your app and with your existing authentication/authorisation.
http://github.com/darthapo/comatose
Looks cool, but (as has been mentioned in the comments already) there are some pretty decent tools out there that facilitate semi-static pages. I've used Comatose successfully and Sandstone looks interesting (it aims to add a embedded CMS functonality using REST practices).
You must put map.static '*permalink', :controller => 'pages', :action => 'show' to ride this.
For the first method, how do you generate the controller? Is it simply "script/generate controller Info"?
You're then editing about.html.erb. Does this file live in views/info/about.html.erb?
Also, the file starts with the title tag. This fails for me. Does your first method require Textile too?
Very useful screencast - thank you Ryan.
And thank you for the TextMate Theme
too :-) veeery nice
@Ryan please embrace censorship and remove some of the irrelevant (putting it politely) stuff.
@Jay up above has a good point which I am also struggling with.
Also there are a number of folks here that appear to be contributing but post a load of cryptic stuff.
If one means to help they should just help if you feel it impinges on your intellectual property then don't clog things up and force endless scrolling.
Thank you!
Hi Ryan.
Man, you got a lot of span here, I hope you don't erase this comment together with all those.
I like your approach of using a ':permalink' route for semi-static pages, it's almost a blog but without the publication date or the word "article" prepended in the path.
However, I've been using a different way so far, to make both static and semi-static pages. I might call it "Template Routes", which is nothing less than routes generated from a set of templates in the filesystem. Given this set of templates, a special class converts all files in named_routes, using directories as namespaces to avoid any possible duplicated name. It looks like exactly an old "PHP" or even plain HTML webpage, but it is nothing similar, since I use layouts to change design and common elements without editing all pages one by one.
I've made this for very big static web-sites, when the client needs to put a lot of documentation on a webpage without never or almost never changing it.
For example, a directory tree like this:
views/
templates/
about/
harry.html.erb
john.html.erb
about.html.erb
knowledge_base.html.erb
...
layouts/
application.html.erb
about.html.erb
knowledge_base.html.erb
Will generate this routes:
## with template about, which renders application too
about
about_harry
about_john
## with template knowledge_base, which renders application too
knowledge_base
...
This names are then easily accessible from any view with the usual *_path helpers, they don't require extra "tunning" in controller, works great with designers and works great with page caching (and don't charge database with data).
Thanks again ryan. I took a Ruby on Rails class as part of my web design program at Vancouver Island University. Just wanted to let you know that I probably learned more from watching your rails casts than paying attention in class :P One of the classes I remember was to follow your railscast on restful authentication. Keep up the good work.
I installed RedCloth 4.2.3 but when i put <%= textilize h(@interview.body) %> I get this error:
NameError in Interviews#show
uninitialized constant ActionView::Helpers::TextHelper::RedCloth
Does anyone have solution? When I put gem list it shows me that RedCloth is installed.
environment.rb file includes this line:
config.gem "RedCloth", :version => "4.2.3"
This was great!, I made some modifications in the routes.rb (rails 3) to handle a default page for root:
match '/(:permalink)' => 'pages#show', :permalink => 'presentacion'
The default page will be "presentacion" id...
Cheers.
Thanks for the updated routes code for Rails3 Juani, just what I was looking for!
hi Ryan,
confirm "Are you sure?" at destroy not working,
i use rails 3.2.0,
How to fix?
thx for help
make sure you pass "method: :delete" in your view and have jQuery.
This episode has been updated to Rails 5 as a blog post Semi Static Pages in Rails 5