code

Its all just ones and zeroes isn't it? 
Filed under

lift

 

Creating a custom 404 error page using Lift

(Update: my previously posted solution opened up a bit of security hole with the catch-all sitemap menu item, so I've revised this solution based on advice from David Pollak)

If you're building a website, its nice to have a custom 404 page.  That way, if users type in an incorrect url, or, they follow an old link, you can show them a friendly page and hopefully give them something to help find what they might be looking for.

Stackoverflow has a great 404 page, for example, try browsing to: http://stackoverflow.com/foobar

Notice two important details:

  • The url in your browser is still the incorrect one you click on, this lets you see what page you tried to go to, and possibly fix it
  • Using Firebug or something similar, you can see that the status code for this page is 404, not 200 OK.  This helps search engines determine that this page is no longer there.

I'd found articles on the web indicating how to add a custom 404 handler to Lift, but they involved a redirect, which breaks both the details above: you get sent to a new URL, the client receives a 200 status code instead of a 404.  This has been called the soft 404 issue.  I highly recommend you do return a 404 status code so that search engines are not confused, and that you don't redirect the user so they can see the incorrect url.  See my post on my other blog about developing applications that are of the internet.

Here's a solution that works with Lift 1.1-M8.

First, a method to generate the page to show to users when the specified url cannot be found, it is merely some boiler plate code to handle rendering a standard lift template page that you've designed, in this case called 404.html:



  def generate404 = {
    import scala.xml.Node
    
    val resp: Box[Node] =  S.setVars("expandAll" -> "true")  {
      for {
        rendered <- S.runTemplate("404" :: Nil)
      } yield mergeToHtmlHead(rendered)(0)
    }

    XhtmlResponse(resp openOr <html><body>Got a 404</body></html>,
                  Full(DocType.xhtmlStrict), List("Content-Type" -> "text/html; charset=utf-8"),Nil, 404, S.ieMode)     
  }

Next, add this code to your Boot.scala file:



// Catch 404s 
 LiftRules.passNotFoundToChain = false 
 LiftRules.uriNotFound.prepend { 
       case (req, _) => XhtmlTemplateResponse(notFoundNode, 404) 
 }


Next, create a template to show users when when Lift cannot find a template or handler for a given Url, e.g. 404.html:



<lift:surround with="default" at="content">
    <head>
        <title>Page Not Found </title>
    </head>
        <h1>Page Not Found</h1>
        <p>We couldn't find the page you were looking for.</p>
</lift:surround>

Finally, the mergeToHead method is not currently exposed by Lift, so as a work-around I've copied its source into my Boot class. Obviously this is a horrible solution, but it works well for now.  It sounds like this method will be exposed in future Lift versions, and/or there might even be a more elegant solution available.

If your 404 template doesn't require head merging (e.g. it doesn't have a <head> section in the HTML to specify a title) then you won't need this.



   /**
   * TODO: I copied this out of http://scala-tools.org/scaladocs/liftweb/1.0/net/liftweb/util/HeadHelper.scala.html
   * 
   * DPP says we won't need to do this in a future release of Lift
   * 
   * This method finds all <head> tags that are descendants of
   * <body> tags in the specified NodeSequence and merges
   * the contents of those tags into the <head> tag closest
   * to the root of the XML tree.
   */
  def mergeToHtmlHead(xhtml: NodeSeq) : NodeSeq = {
    def trimText(in: NodeSeq): NodeSeq = in flatMap {
      case e: Elem =>
        Elem(e.prefix, e.label, e.attributes, e.scope, trimText(e.child) :_*)

      case g: Group =>
        trimText(g.child)

      case t: Text =>
        val s = t.text.trim
        if (s.length == 0) NodeSeq.Empty
        Text(s)

      case x => x
    }

    val headInBody: NodeSeq =
    (for (body <- xhtml \ "body";
          head <- findElems(body)(_.label == "head")) yield trimText(head.child)).
    toList.removeDuplicates.flatMap(a => a)

    if (headInBody.isEmpty) xhtml
    else {
      def xform(in: NodeSeq, inBody: Boolean): NodeSeq = in flatMap {
        case e: Elem if !inBody && e.label == "body" =>
          Elem(e.prefix, e.label, e.attributes, e.scope, xform(e.child, true) :_*)

        case e: Elem if inBody && e.label == "head" => NodeSeq.Empty

        case e: Elem if e.label == "head" =>
          Elem(e.prefix, e.label, e.attributes,
               e.scope, e.child ++ headInBody :_*)

        case e: Elem =>
          Elem(e.prefix, e.label, e.attributes, e.scope, xform(e.child, inBody) :_*)

        case g: Group =>
          xform(g.child, inBody)

        case x => x
      }

      xform(xhtml, false)
    }
  }


Thats it! Now your web application will serve up a friendly page when users arrive at not found URLs, and search engines won't get confused.

Filed under  //   lift   scala  

Comments [0]