Home Overview Demos/Webs News Download/Install Docs/Support Investors/Partners Commercial
Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat (Stateful version)
Introduction/Setup
How can a web site be SPI and page based at the same time?
Web application set-up
Creating the ItsNat servlet
Main page processing
Infrastructure of fundamental states
Support of Google Analytics to monitor fundamental states
Fundamental states: overview and overview.showpopup
Fundamental state detail and secondary state "More detail"
Fundamental state not_found
Conclusion
Download and Online Demo

Last update: 2013, Jul. 1  

Introduction


ItsNat is a web framework strongly focused on Single Page Interface applications.

The Single Page Interface (SPI) paradigm is not new, these days SPI is very popular on "web applications" because modern web frameworks with AJAX support make this kind of applications easier than ever.

However this tutorial shows how to build SPI web sites, web sites based on pages can be evolved to SPI without losing the benefits of page based web sites like bookmarking, Search Engine Optimization (SEO) or accessibility (JavaScript disabled) according to The Single Page Interface Manifesto.

To understand this tutorial some basic knowledge of ItsNat is required.


How can a web site be SPI and page based at the same time?


The typical behavior of ItsNat is the following: when the DOM in server changes JavaScript code is automatically generated and sent to the client to update the client DOM accordingly. Usually in web sites many elements are shared like the header, footer etc and the content area is almost the unique zone being fully changed, in SPI this "content area" can be changed with new HTML fragments, a HTML fragment is dynamically inserted into the DOM document in server using W3C DOM Java API, in the same time this new HTML code is also automatically inserted by JavaScript code in client (usually using innerHTML), unfortunately this approach is not SEO friendly because web crawlers absolutely ignore JavaScript.

To provide page simulation one ItsNat feature is the key: fast-load mode.

If ItsNat is configured in fast-load mode (the default mode), when the initial page (based on a pure HTML template) is being loaded, any DOM change performed in server is not sent as JavaScript, DOM rendering to generate the initial HTML page being sent to the client, is done after the user code in server is executed. Hence the same user code manipulating DOM can generate JavaScript or plain HTML depending on the phase is executed, when an event is received (JavaScript) or on load time of the initial page (HTML).

When fast-load mode is disabled, the template being used is the markup being sent to the client and DOM changes in server are sent as JavaScript DOM operations.

In fast-load mode we can use a parameter in the query part of URL or in your preferred pretty URL to specify what "page" ("fundamental state" following the SPI Manifesto terminology) is going to be loaded, executing the same code being used to insert the required HTML fragment into the content zone. The final DOM tree is the tree being serialized as HTML and loaded by the client.

These are the basics, more things are required to provide in SPI the same capabilities as page based web sites, such as dual (AJAX/normal) links, URL rewriting for bookmarking, support of page visit counters (in this case "state visit counters"), JavaScript disabled and support of Back/Forward buttons (history navigation). These will be explained with code.


Web application set-up


ItsNat does not require special set up or application servers, any servlet container supporting Java 1.5 or upper is enough. Use your preferred IDE to create an empty web application, this tutorial uses spitut as the name of the web application. Add to the WEB-INF/lib directory all required JAR files by ItsNat (you can find these jars in /fw_dist/lib folder of the ItsNat distribution).


Creating the ItsNat servlet


ItsNat does not impose a bootstrap, there is no "default" ItsNat servlet, in fact you can use several servlets using ItsNat in your web application. Using your IDE add a new servlet class with name servlet (this name is not mandatory) in the default package (again not mandatory).

Default registration in web.xml is valid, ItsNat does not require special initialization parameters or filters in web.xml.

According to this setup the URL accessing our servlet is (8080 is supposed):

http://localhost:8080/spitut/servlet

Because our web site is SPI we would like a prettier URL like

http://localhost:8080/spitut/

We have two options:

  1. Add servlet as the welcome file
        <welcome-file-list>
            <welcome-file>servlet</welcome-file>
        </welcome-file-list>
    

  2. Add a simple index.jsp (this file is usually by default the "welcome file" and lazy people use this JSP to start the web application in their IDE) with this content:
        <jsp:forward page="/servlet" />
    

Now replace the generated code of the servlet class with this code:

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.itsnat.core.tmpl.ItsNatDocumentTemplate;
import org.itsnat.core.ItsNatServletConfig;
import org.itsnat.core.ItsNatServletContext;
import org.itsnat.core.http.HttpServletWrapper;
import org.itsnat.core.http.ItsNatHttpServlet;
import org.itsnat.spitut.SPITutGlobalEventListener;
import org.itsnat.spitut.SPITutGlobalLoadRequestListener;
import org.itsnat.spitut.SPITutMainLoadRequestListener;

public class servlet extends HttpServletWrapper
{

    public void init(ServletConfig config) throws ServletException
    {
        super.init(config);

        ItsNatHttpServlet itsNatServlet = getItsNatHttpServlet();

        ItsNatServletContext itsNatCtx = itsNatServlet.getItsNatServletContext();
        itsNatCtx.setMaxOpenDocumentsBySession(4); // To avoid abusive users

        ItsNatServletConfig itsNatConfig = itsNatServlet.getItsNatServletConfig();
        itsNatConfig.setFastLoadMode(true); // Not really needed, is the same as default

        String pathBase = getServletContext().getRealPath("/");
        String pathPages =     pathBase + "/WEB-INF/pages/";
        String pathFragments = pathBase + "/WEB-INF/fragments/";

        itsNatServlet.addEventListener(new SPITutGlobalEventListener());
        itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());

        ItsNatDocumentTemplate docTemplate;
        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",
                    pathPages + "main.xhtml");
        docTemplate.addItsNatServletRequestListener(new SPITutMainLoadRequestListener());

        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("google_analytics","text/html",
                    pathPages + "google_analytics.xhtml");
        docTemplate.setScriptingEnabled(false);

        // Fragments
        itsNatServlet.registerItsNatDocFragmentTemplate("not_found","text/html",
                    pathFragments + "not_found.xhtml");
        itsNatServlet.registerItsNatDocFragmentTemplate("overview","text/html",
                    pathFragments + "overview.xhtml");
        itsNatServlet.registerItsNatDocFragmentTemplate("overview.popup","text/html",
                    pathFragments + "overview_popup.xhtml");
        itsNatServlet.registerItsNatDocFragmentTemplate("detail","text/html",
                    pathFragments + "detail.xhtml");
        itsNatServlet.registerItsNatDocFragmentTemplate("detail.more","text/html",
                    pathFragments + "detail_more.xhtml");
    }
}

As you can see our servlet now inherits from HttpServletWrapper:

public class servlet extends HttpServletWrapper

This ItsNat class redirects any request to the ItsNatHttpServlet object wrapping the servlet instance.

Web application configuration is done in the standard init(ServletConfig) method, configuration is ItsNat is "classic", imperative, calling configuration methods.

        itsNatCtx.setMaxOpenDocumentsBySession(4); // To avoid abusive users

This call sets 4 documents with server state by user session, this value tries to avoid abusive users of opening too many browser windows with the same page because these pages are isolated in server (no shared data between pages).

        itsNatConfig.setFastLoadMode(true); // Not really needed, is the same as default

This call is not really necessary because fast-load mode is the default, this method is called to clearly state fast-load mode is mandatory in SPI web sites with page simulation.

        String pathBase = getServletContext().getRealPath("/");
        String pathPages =     pathBase + "/WEB-INF/pages/";
        String pathFragments = pathBase + "/WEB-INF/fragments/";

The folder WEB-INF/pages is going to be used to save "ItsNat page templates", because our web application is SPI only one page template is need, later another very simple page will be added to monitor page visits. Into the folder WEB-INF/fragments will be saved "page fragments", page fragments are pure HTML pages where only the content of <body> (or <head>) is used.

        itsNatServlet.addEventListener(new SPITutGlobalEventListener());

SPITutGlobalEventListener is a global org.w3c.dom.events.EventListener, all AJAX requests received by this servlet (for any document loaded by this servlet) are first dispatched to this listener. This is the code:

package org.itsnat.spitut;

import org.itsnat.core.ClientDocument;
import org.itsnat.core.event.ItsNatEvent;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;

public class SPITutGlobalEventListener implements EventListener
{
    public SPITutGlobalEventListener()
    {
    }

    public void handleEvent(Event evt)
    {
        ItsNatEvent itsNatEvt = (ItsNatEvent)evt;
        if (itsNatEvt.getItsNatDocument() == null)
        {
            StringBuilder code = new StringBuilder();
            code.append("if (confirm('Expired session. Reload?'))");
            code.append("  window.location.reload(true);");
            ClientDocument clientDoc = itsNatEvt.getClientDocument();
            clientDoc.addCodeToSend(code.toString());
            itsNatEvt.getItsNatEventListenerChain().stop();
        }
    }
}

When the web browser of an end user sends an event from a client document lost in server (usually because the user session is expired), the method ItsNatEvent.getItsNatDocument() returns null because this event is "orphan", there is no document in server attached to the client document. In this case we ask to the end user to reload the page.

The call:

            itsNatEvt.getItsNatEventListenerChain().stop();

is not really needed, it prevents other global event listeners of being called (not in this case).

This SPITutGlobalEventListener class is not actually needed because the default behavior of ItsNat processing an orphan event is to automatically reload the page (the only difference is that end user is not asked).

Following in the servlet:

        itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());

Registers a global document (page) listener. This listener is called when a page (=document) load is requested or any other unknown page request (not AJAX events). Source code:

package org.itsnat.spitut;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.itsnat.core.ItsNatDocument;
import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.event.ItsNatServletRequestListener;

public class SPITutGlobalLoadRequestListener implements ItsNatServletRequestListener
{
    public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
    {
        ItsNatDocument itsNatDoc = request.getItsNatDocument();
        if (itsNatDoc == null)
        {
            // Requested with a custom URL, not ItsNat standard format,
            // for instance servlet without params.
            // Internal redirection specifying the target template page:
            ServletRequest servRequest = request.getServletRequest();
            String gAnalytState = servRequest.getParameter("ganalyt_st");
            if (gAnalytState != null)
                servRequest.setAttribute("itsnat_doc_name","google_analytics");
            else
                servRequest.setAttribute("itsnat_doc_name","main");
            ServletResponse servResponse = response.getServletResponse();

            request.getItsNatServlet().processRequest(servRequest,servResponse);
        }
    }
}

If the request includes the default parameter itsnat_doc_name and the specified template exists the method ItsNatServletRequest.getItsNatDocument() returns a non-null ItsNatDocument object, if the URL requested does not include itsnat_doc_name is a "custom" request and getItsNatDocument() returns null. The later is the case of pretty URLs.

We first check whether the parameter ganalyt_st is present, this parameter is going to be used later for state monitoring using Google Analytics. If this parameter is not present is the expected case of a request like this:

http://localhost:8080/spitut/

Or this:

http://localhost:8080/spitut/servlet

In this case we have to load the main page of our web site, so itsnat_doc_name is specified with value "main" as a request attribute, the value "main" is the name of the main template of our SPI web site, ItsNat checks first if itsnat_doc_name was specified as attribute and then as parameter. Now we are ready to re-send the request to ItsNat calling:

            request.getItsNatServlet().processRequest(servRequest,servResponse);

This new request has the same effect as the URL:

http://localhost:8080/spitut/servlet?itsnat_doc_name=main

Now the target page template is specified and found and request.getItsNatDocument(); returns a non-null value and nothing is done in this case because this global listener is not the typical place to manage normal page load requests.


Main page processing


Returning to the servlet:

        ItsNatDocumentTemplate docTemplate;
        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",
                    pathPages + "main.xhtml");

This call registers with the name "main" the page template file main.xhtml (saved in WEB-INF/pages/):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="expires" content="Wed, 1 Dec 1997 03:01:00 GMT" />
    <meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
    <title id="titleId" itsnat:nocache="true">Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat</title>
    <link rel="stylesheet" type="text/css" href="css/style.css" />
    <script type="text/javascript" src="js/spi_bookmarking.js?timestamp=2011-02-11_01"></script>
    <script type="text/javascript">
    function setState(name)
    {
        if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
        var itsNatDoc = document.getItsNatDoc();
        var evt = itsNatDoc.createUserEvent("setState");
        evt.setExtraParam("name",name);
        itsNatDoc.dispatchUserEvent(null,evt);
    }

    window.spiSite.onBackForward = setState;
    </script>
</head>
<body>

<div class="main">
    <table style="width:100%; height:100%; padding:0; margin:0;" border="0px" cellpadding="0" cellspacing="0">
    <tbody>
    <tr style="height:50px;">
        <td>
            <h2 style="text-align:center;">Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat</h2>
        </td>
    </tr>
    <tr style="height:40px;">
        <td>
            <table style="width:100%; margin:0; padding:0; border: #ED752A solid; border-width: 0 0 2px 0; ">
                <tbody>
                <tr class="mainMenu" itsnat:nocache="true">
                    <td id="menuOpOverviewId">
                        <a href="?st=overview" onclick="setState('overview'); return false;"
                           class="menuLink">Overview</a>
                    </td>
                    <td id="menuOpDetailId">
                        <a href="?st=detail" onclick="setState('detail'); return false;"
                           class="menuLink">Detail</a>
                    </td>
                    <td> </td>
                </tr>
                </tbody>
            </table>
        </td>
    </tr>
    <tr style="height:70%; /* For MSIE */">
        <td id="contentParentId" itsnat:nocache="true" style="padding:20px; vertical-align:super;" >



        </td>
    </tr>
    <tr style="height:50px"">
        <td style="border-top: 1px solid black; text-align:center;">
            SOME FOOTER
        </td>
    </tr>
    </tbody>
    </table>

</div>

<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>

</body>
</html>

As you can see ItsNat templates are pure HTML files because view logic is coded with W3C DOM Java API. By default templates are cached, that is, DOM nodes are internally serialized as plain HTML and shared between users; any cached DOM subtree is replaced in server by a text node containing a special mark, when this subtree is sent to the client ItsNat automatically sends the cached markup. DOM caching is interesting for "static" (server point of view) parts of the template, caching can be fully avoided with a configuration flag.

Because we want to modify some parts, these parts are not static and must be marked as "not cached" with the special attribute itsnat:nocache="true" where itsnat prefix is declared in <html> as xmlns:itsnat="http://itsnat.org/itsnat".

For instance:

    <title id="titleId" itsnat:nocache="true">Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat</title>

The title of the SPI web site will change when a new fundamental state is loaded.

                <tr class="mainMenu" itsnat:nocache="true">

This row contains the web site main menu, is not cached because we need to access menu items in server to change the color of the current selected option.

        <td id="contentParentId" itsnat:nocache="true" style="padding:20px; vertical-align:super;" >

This table cell is the parent of the "content area", when the end user clicks some menu option, this zone will change accordingly.

Finally:

<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>

The URL of this <iframe> will be changed to keep track of fundamental states.

The following code:

    <script type="text/javascript">
    function setState(name)
    {
        if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded

        var itsNatDoc = document.getItsNatDoc();
        var evt = itsNatDoc.createUserEvent("setState");
        evt.setExtraParam("name",name);
        itsNatDoc.dispatchUserEvent(null,evt);
    }

    window.spiSite.onBackForward = setState;
    </script>

Is being used to send ItsNat "user events", a user event is an extension of W3C DOM Events and is fired calling some public ItsNat methods from JavaScript. User events are used in this tutorial to notify the server about the next fundamental state to be loaded, these user events are received by a user event listener in server previously registered.

An example of how fundamental states are changed is this link (a menu item):

                        <a href="?st=overview" onclick="setState('overview'); return false;"
                           class="menuLink">Overview</a>

When end user clicks this link the call setState('overview'); sends a user event to command server to load the new fundamental state "overview", because onclick returns false the default behavior of the link is aborted hence href="?st=overview" is ignored and the page remains the same (partially changed with the new state). This is not the case of search engine crawlers, these bots ignore JavaScript and follow the link loading a new page with "overview" as the initial state. This is an example of dual link AJAX/normal.

Now is the time of registering a load listener for the main template (back to the servlet):

        ItsNatDocumentTemplate docTemplate;
        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",
                    pathPages + "main.xhtml");
        docTemplate.addItsNatServletRequestListener(new SPITutMainLoadRequestListener());

The SPITutMainLoadRequestListener.processRequest(…) method will be called when the servlet receives a new load request of this template, one call per load request. The source code is really simple:

package org.itsnat.spitut;

import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.event.ItsNatServletRequestListener;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;

public class SPITutMainLoadRequestListener implements ItsNatServletRequestListener
{
    public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
    {
        new SPITutMainDocument((ItsNatHttpServletRequest)request,(ItsNatHttpServletResponse)response);
    }
}

A new SPITutMainDocument instance is created per call (load request), this instance holds an ItsNatHTMLDocument object which represents the client document (page) being loaded.

This object is not going to be garbage collected because some event listener is going to be registered in ItsNatHTMLDocument and this document instance is automatically hold by ItsNat following the life cycle of the client document: when the end user leaves the page the document in server is automatically discarded. If no unload event is received ItsNat automatically discards documents in server when no event is received for a long time (the session expiring time is used) or when the user session expires (in spite of ItsNat developers do not use sessions, in ItsNat servlet sessions are the basic mechanism to identify end users).

Source code of SPITutMainDocument:

package org.itsnat.spitut;

import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.itsnat.core.ItsNatServlet;
import org.itsnat.core.domutil.ItsNatDOMUtil;
import org.itsnat.core.event.ItsNatUserEvent;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.core.tmpl.ItsNatDocFragmentTemplate;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.html.HTMLDocument;
import org.w3c.dom.html.HTMLTitleElement;

public class SPITutMainDocument implements EventListener
{
    protected ItsNatHTMLDocument itsNatDoc;
    protected String title;
    protected HTMLTitleElement titleElem;
    protected Map<String,Element> menuElemMap = new HashMap<String,Element>();
    protected Element currentMenuItemElem;
    protected Element contentParentElem;
    protected SPITutState currentState;
    protected Element googleAnalyticsElem;
    protected String googleAnalyticsIFrameURL;

    public SPITutMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response)
    {
        this.itsNatDoc = (ItsNatHTMLDocument)request.getItsNatDocument();

        HTMLDocument doc = itsNatDoc.getHTMLDocument();

        this.titleElem = (HTMLTitleElement)doc.getElementById("titleId");
        this.title = titleElem.getText(); // Initial value

        menuElemMap.put("overview",doc.getElementById("menuOpOverviewId"));
        menuElemMap.put("detail",doc.getElementById("menuOpDetailId"));
        // More menu options here...

        itsNatDoc.addUserEventListener(null,"setState", this);

        this.contentParentElem = doc.getElementById("contentParentId");
        this.googleAnalyticsElem = doc.getElementById("googleAnalyticsId");
        this.googleAnalyticsIFrameURL = googleAnalyticsElem.getAttribute("src");  // Initial value

        HttpServletRequest servReq = request.getHttpServletRequest();
        String stateName = servReq.getParameter("_escaped_fragment_"); // Google bot, has priority, its value is based on the hash fragment
        if (stateName != null)
        {
            if (stateName.startsWith("st="))  // st means "state"
                stateName = stateName.substring("st=".length(), stateName.length());
            else // Wrong format
                stateName = "overview";
        }
        else
        {
            stateName = servReq.getParameter("st");
            if (stateName == null)
                stateName = "overview";
        }

        changeState(stateName);
    }

    public ItsNatHTMLDocument getItsNatHTMLDocument()
    {
        return itsNatDoc;
    }

    public void setStateTitle(String stateTitle)
    {
        String pageTitle = title + " - " + stateTitle;
        if (itsNatDoc.isLoading())
            titleElem.setText(pageTitle);
        else
            itsNatDoc.addCodeToSend("document.title = \"" + pageTitle + "\";\n");
    }

    public Element getContentParentElement()
    {
        return contentParentElem;
    }

    public ItsNatDocFragmentTemplate getFragmentTemplate(String name)
    {
        ItsNatServlet servlet = itsNatDoc.getItsNatDocumentTemplate().getItsNatServlet();
        return servlet.getItsNatDocFragmentTemplate(name);
    }

    public DocumentFragment loadDocumentFragment(String name)
    {
        ItsNatDocFragmentTemplate template = getFragmentTemplate(name);
        if (template == null) return null;
        return template.loadDocumentFragment(itsNatDoc);
    }

    public String getFragmentName(String stateName)
    {
        String fragmentName = stateName;
        int pos = stateName.indexOf('.');
        if (pos != -1) fragmentName = stateName.substring(0, pos); // Case "overview.showpopup"
        return fragmentName;
    }

    public void changeState(String stateName)
    {
        String fragmentName = getFragmentName(stateName);

        ItsNatDocFragmentTemplate template = getFragmentTemplate(fragmentName);
        if (template == null)
        {
            changeState("not_found");
            return;
        }

        // Cleaning previous state:
        if (currentState != null)
        {
            currentState.dispose();
            this.currentState = null;
        }

        ItsNatDOMUtil.removeAllChildren(contentParentElem);

        // Setting new state:
        changeActiveMenu(stateName);

        DocumentFragment frag = template.loadDocumentFragment(itsNatDoc);
        contentParentElem.appendChild(frag);

        if (stateName.equals("overview")||stateName.equals("overview.showpopup"))
        {
            boolean popup = stateName.equals("overview.showpopup");
            this.currentState = new SPITutStateOverview(this,popup);
        }
        else if (stateName.equals("detail"))
            this.currentState = new SPITutStateDetail(this);

        itsNatDoc.addCodeToSend("try{ window.scroll(0,-5000); }catch(ex){}");
        // try/catch is used to avoid exceptions when some (mobile) browser does not support window.scroll()
    }

    public void registerState(SPITutState state)
    {
        setStateTitle(state.getStateTitle());
        String stateName = state.getStateName();
        itsNatDoc.addCodeToSend("spiSite.setURLReference(\"" + stateName + "\");");
        googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);
    }

    public void handleEvent(Event evt)
    {
        if (evt instanceof ItsNatUserEvent)
        {
            ItsNatUserEvent itsNatEvt = (ItsNatUserEvent)evt;
            String name = (String)itsNatEvt.getExtraParam("name");
            changeState(name);
        }
    }

    public void changeActiveMenu(String stateName)
    {
        String mainMenuItem;
        int pos = stateName.indexOf('.');
        if (pos != -1) mainMenuItem = stateName.substring(0, pos); // Case "overview.showpopup"
        else mainMenuItem = stateName;

        if (currentMenuItemElem != null)
            currentMenuItemElem.removeAttribute("class");

        this.currentMenuItemElem = menuElemMap.get(mainMenuItem);

        if (currentMenuItemElem != null)
            currentMenuItemElem.setAttribute("class","menuOpSelected");
    }
}

This class is the core of the web site, tightly associated to the main template and responsible of (fundamental) state management, states directly dependent on main menu.

The first sentence:

        this.itsNatDoc = (ItsNatHTMLDocument)request.getItsNatDocument();

Saves the ItsNat document object in an attribute because SPITutMainDocument is a wrapper of the ItsNatHTMLDocument.

        this.titleElem = (HTMLTitleElement)doc.getElementById("titleId");
        this.title = titleElem.getText(); // Initial value

We are going to change the page title when a new fundamental state is loaded.

        menuElemMap.put("overview",doc.getElementById("menuOpOverviewId"));
        menuElemMap.put("detail",doc.getElementById("menuOpDetailId"));
        // More menu options here...

The menuElemMap collection maps state names with the DOM elements of the menu items, we need these elements to change their appearance when a menu option is selected, menu selection changes the current fundamental state being shown.

        itsNatDoc.addUserEventListener(null,"setState", this);

Adds this instance as the user event listener listening state changes, usually when end users click a menu option. User events are received by the handleEvent(Event) method of this class.

        this.contentParentElem = doc.getElementById("contentParentId");
        this.googleAnalyticsElem = doc.getElementById("googleAnalyticsId");
        this.googleAnalyticsIFrameURL = googleAnalyticsElem.getAttribute("src");  // Initial value

These DOM elements are useful to insert/remove the markup of the "content area" and for monitoring state visits with Google Analytics.

Finally the constructor ends with:

        HttpServletRequest servReq = request.getHttpServletRequest();
        String stateName = servReq.getParameter("_escaped_fragment_"); // Google bot, has priority, its value is based on the hash fragment
        if (stateName != null)
        {
            if (stateName.startsWith("st="))  // st means "state"
                stateName = stateName.substring("st=".length(), stateName.length());
            else // Wrong format
                stateName = "overview";
        }
        else
        {
            stateName = servReq.getParameter("st");
            if (stateName == null)
                stateName = "overview";
        }

        changeState(stateName);

This demo application is prepared to be crawled by Google Search bots following its AJAX Crawling Specification, links ending with #! are also followed by Google bots, in this case the target site is accessed replacing #! with a parameter _escaped_fragment= followed by the text following #!. For instance this link is crawled by Google bots requesting with this link.

The st parameter is used to load the web site with the specified initial state. When no st parameter is provided, for instance when loading with this URL, http://localhost:8080/spitut, the default state is "overview" as if the "Overview" menu option was selected. This normal parameter is interesting for non-Google crawlers and to provide navigation with JavaScript disabled.

The method changeState(String) is the responsible of change management. This method loads the specified fundamental state inserting the new markup into the content area, changes the appearance of the active menu option and delegates further state processing to the appropriated SPITutState class.

The event listener receiving user events is very simple:

    public void handleEvent(Event evt)
    {
        if (evt instanceof ItsNatUserEvent)
        {
            ItsNatUserEvent itsNatEvt = (ItsNatUserEvent)evt;
            String name = (String)itsNatEvt.getExtraParam("name");
            changeState(name);
        }
    }

It just changes the current fundamental state with the selected state when end user clicks some menu option.


Infrastructure of fundamental states


Now is the time to deep inside the fundamental states being used as examples in this SPI web site.

Back to the servlet:

        // Fragments

        itsNatServlet.registerItsNatDocFragmentTemplate("not_found","text/html",
                    pathFragments + "not_found.xhtml");
        itsNatServlet.registerItsNatDocFragmentTemplate("overview","text/html",
                    pathFragments + "overview.xhtml");
        itsNatServlet.registerItsNatDocFragmentTemplate("overview.popup","text/html",
                    pathFragments + "overview_popup.xhtml");
        itsNatServlet.registerItsNatDocFragmentTemplate("detail","text/html",
                    pathFragments + "detail.xhtml");
        itsNatServlet.registerItsNatDocFragmentTemplate("detail.more","text/html",
                    pathFragments + "detail_more.xhtml");

The fragment "overview" contains the markup in <body> being included in the content area of the main page when user selects the "Overview" menu option or the URL loading our web site specifies "overview" as the initial state. The same for "detail".

The fragment "overview.popup" is going to be inserted into the overview fragment, the same for "detail.more".

The method SPITutMainDocument.changeState(String stateName) instances the specified fundamental state, classes of fundamental states inherits from SPITutState, this class establishes a contract with concrete inherited state classes.

package org.itsnat.spitut;

import org.itsnat.core.html.ItsNatHTMLDocument;

public abstract class SPITutState
{
    protected SPITutMainDocument spiTutDoc;

    public SPITutState(SPITutMainDocument spiTutDoc)
    {
        this.spiTutDoc = spiTutDoc;

        spiTutDoc.registerState(this);
    }

    public SPITutMainDocument getSPITutMainDocument()
    {
        return spiTutDoc;
    }

    public ItsNatHTMLDocument getItsNatHTMLDocument()
    {
        return spiTutDoc.getItsNatHTMLDocument();
    }

    public abstract void dispose();
    public abstract String getStateTitle();
    public abstract String getStateName();
}

The call:

        spiTutDoc.registerState(this);

is specially interesting, this SPITutMainDocument method sets the state being created as the default state.

    public void registerState(SPITutState state)
    {
        setStateTitle(state.getStateTitle());
        String stateName = state.getStateName();
        itsNatDoc.addCodeToSend("spiSite.setURLReference(\"" + stateName + "\");");
        googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);
    }

The call:

        setStateTitle(state.getStateTitle());

changes the page title adding the state name.

        itsNatDoc.addCodeToSend("spiSite.setURLReference(\"" + stateName + "\");");

This code sends to the client some JavaScript code to specify the fundamental state being loaded in client. The JavaScript method setURLReference(stateName) is included into the file spi_bookmarking.js, which is included by the main template and ever present in our SPI web site. This method changes the reference part of the URL to save the current state, this change does not imply a page reload remaining our web site as SPI. When one end user bookmarks the main page, the URL, containing the current state value in the reference part, is also saved. When returning to the web site with this bookmarked URL, a script contained in spi_bookmarking.js is executed on load time adding the appropriated st parameter to the URL reloading again the main page with the required initial fundamental state, the same bookmarked state.

The method setURLReference(stateName) also calls detectBackForward(). This method uses a JavaScript timer to keep track of Back/Forward clicks (or browser history navigation in general) for Back/Forward simulation in SPI. When the Back/Forward buttons are clicked only the reference part of the URL changes hence no reload happens, this is correct if you do not want the typical Back/Forward behavior to remain pure SPI. The method detectBackForward() may be used to provide some Back/Forward simulation, a JavaScript timer detects when the reference part of the URL has been changed (usually by Back/Forward) and if no window.spiSite.onBackForward function is registered, the page automatically is reloaded of course this behavior is not pure SPI but it may be "acceptable" if end users reclaim Back/Forward support in your SPI web site.

In our example Back/Forward buttons are supported with no reload because window.spiSite.onBackForward was registered in main.xhtml with the call:

    <script>
    function setState(name)
    {
        ...
    }

    window.spiSite.onBackForward = setState;
    </script>

When Back/Forward button is detected, the state change in the URL reference is forwarded to be processed in server calling setName with the new state name, this method performs the required change delegating to the server by an AJAX call, and in server state transition is executed accordingly. This way our Single Page Interface remains pure SPI including Back/Forward behavior. If arbitrary change state transitions are too complex without reloading the page, use the default approach (page reload) and leaving window.spiSite.onBackForward undefined.

This is the code of spi_bookmarking.js:

function SPISite()
{
    this.load = load;
    this.detectURLChange = detectURLChange;
    this.detectURLChangeCB = detectURLChangeCB;
    this.setURLReference = setURLReference;
    this.onBackForward = null; // Public, user defined

    this.firstTime = true;
    this.initialURLWithState = null;
    this.href = null;
    this.disabled = false; 

    this.load();

    function load() // page load phase
    {
        if (this.disabled) return;

        var url = window.location.href;
        var posR = url.lastIndexOf("#!st=");
        if (posR == -1) return;
        var state = url.substring(posR + "#!st=".length);
        if (state == "") return;
        this.initialURLWithState = window.location.href;
    }

    function setURLReference(stateName)
    {
        if (this.disabled) return;

        var url = window.location.href;
        var posR = url.lastIndexOf("#");
        if (posR != -1) url = url.substring(0,posR);
        url = url + "#!st=" + stateName;
        if (url != window.location.href)
            window.location.href = url;

        this.href = window.location.href;

        if (!this.firstTime) return;

        this.firstTime = false;
        if (this.initialURLWithState != null)
        {
            // Loads the initial state in URL if different to default
            window.location.href = this.initialURLWithState;
            this.initialURLWithState = null;
            this.detectURLChange(1); // As soon as possible
        }
        else this.detectURLChange(200);
    }

    function detectURLChange(time)
    {
        var func = function()
        {
            arguments.callee.spiSite.detectURLChangeCB();
            window.setTimeout(arguments.callee,200);
        };
        func.spiSite = this;
        window.setTimeout(func,time);
    }

    function detectURLChangeCB()
    {
        // Detecting when only the reference part of the URL changes
        var url = window.location.href;
        if (this.href == url) return;
        var posR = url.lastIndexOf("#!st=");
        if (posR == -1) return;
        var posR2 = this.href.lastIndexOf("#!st=");
        if (posR != posR2) return;
        var url2 = url.substring(0,posR);
        var spiHref2 = this.href.substring(0,posR);
        if (url2 != spiHref2) return;

        // Only changed the reference part
        this.href = url;

        var stateName = url.substring(posR + "#!st=".length);
        if (this.onBackForward) this.onBackForward(stateName);
        else try { window.location.reload(true); }
             catch(ex) { window.location = window.location; }
    }
}

window.spiSite = new SPISite();


Support of Google Analytics to monitor fundamental states


The last call of SPITutMainDocument.registerState(...) is:

        googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);

Sets the src attribute of the <iframe> being used for Google Analytics adding the name of the new fundamental state. For instance:

<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>

Because the src attribute has been changed the page of the <iframe>, only containing Google Analytics’s scripts, is reloaded with the new URL. This way you can monitor how many times (and who and how) end users have been visiting the fundamental states of your SPI web site.

The parameter ganalyt_st is detected in SPITutGlobalLoadRequestListener and redirected the request to load the google_analytics template registered in servlet with the following call:

        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("google_analytics","text/html",
                    pathPages + "google_analytics.xhtml");
        docTemplate.setScriptingEnabled(false);

This template (google_analytics.xhtml) only contains scripts of Google Analytics:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="expires" content="Wed, 1 Dec 1997 03:01:00 GMT" />
    <meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
    <title>Google Analytics</title>
</head>
<body style="margin:0; padding:0;">
     <!-- <script>alert(window.location);</script>    -->
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost +
    "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
var pageTracker = _gat._getTracker("UA-2924757-2");
pageTracker._trackPageview();
</script>

</body>
</html>

This file could be a simple html (or JSP) file outside ItsNat control, but ItsNat ever adds response headers to disable page caching in browsers (a good thing to force and ensure script execution).

Note the call:

        docTemplate.setScriptingEnabled(false);

This configuration call sets this page template as non-scriptable, ItsNat does not add JavaScript code to the page for client-server synchronization purposes hence there is no AJAX, furthermore, the ItsNat document in server only is created and used in load time then is discarded because this page is stateless (server point of view).


Fundamental states: overview and overview.showpopup


Concrete fundamental states (inherited from SPITutState) are SPITutStateOverview, SPITutStateOverviewShowPopup, SPITutStateDetail.

SPITutStateOverview and SPITutStateDetail are directly launched by SPITutMainDocument because both are the main menu options. SPITutStateOverviewShowPopup is very interesting because in spite of it is a sub-state of SPITutStateOverview we want this state ("Overview showing a popup window") to be a fundamental state, that is bookmarkable, content reached by search engine crawlers and monitored by Google Analitycs, so it is inherited from SPITutState because the SPITutMainDocument.registerState(String) method must be called.

Now we are ready to show SPITutStateOverview:

package org.itsnat.spitut;

import org.w3c.dom.Element;

import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateOverview extends SPITutState implements EventListener
{
    protected Element showPopupElem;
    protected SPITutStateOverviewShowPopup popup;

    public SPITutStateOverview(SPITutMainDocument spiTutDoc,boolean showPopup)
    {
        super(spiTutDoc);

        HTMLDocument doc = getItsNatHTMLDocument().getHTMLDocument();
        this.showPopupElem = doc.getElementById("showPopupId");
        ((EventTarget)showPopupElem).addEventListener("click",this,false);

        if (showPopup) showOverviewPopup();
    }

    public void dispose()
    {
        if (popup != null) popup.dispose();
        ((EventTarget)showPopupElem).removeEventListener("click",this,false);
    }

    public void handleEvent(Event evt)
    {
        showOverviewPopup();
    }

    public void showOverviewPopup()
    {
        ((EventTarget)showPopupElem).removeEventListener("click",this,false); // Avoids two consecutive clicks

        this.popup = new SPITutStateOverviewShowPopup(this);
    }

    public void onDisposeOverviewPopup()
    {
        this.popup = null;
        ((EventTarget)showPopupElem).addEventListener("click",this,false); // Restores

        spiTutDoc.registerState(this);
    }

    @Override
    public String getStateTitle()
    {
        return "Overview";
    }

    @Override
    public String getStateName()
    {
        return "overview";
    }
}

To understand what is doing this class we need the overview.xhtml fragment template, the markup contained in <body> is inserted into the "content area" of our web site when overview state is selected (the markup in <head> is not inserted):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Overview</title>
</head>
<body>

    <div >
        <h2>Overview</h2>
        <p>This "fundamental" state is processed by crawlers of search engines (Google, Yahoo, Bing...)</p>
        <p>To understant how this SPI web site is "seen" by search engines, disable
            JavaScript in your browser.
        </p>
        <p>You can load a new text like a pop-up window, the link
           used to open this pop-up contains a URL specifying as fundamental
           state this state with the popup already loaded, this URL is used
           by crawlers because "return false" is not executed.
           By this way text contained in pop-up is also processed by crawlers.
        </p>
        <a itsnat:nocache="true" id="showPopupId" href="?st=overview.showpopup" onclick="return false;">Show popup</a>
    </div>
</body>
</html>

Because "Overview" is the default state the following picture shows this state as the initial state our web application:


Pay attention to the link:

        <a id="showPopupId" href="?st=overview.showpopup" onclick="return false;">Show popup</a>

This link is dual, AJAX and normal, in this case we directly bind an event listener in server with the call:

        ((EventTarget)showPopupElem).addEventListener("click",this,false);

When clicked, the sub-state "overview.showpopup" (also a fundamental state) is instanced showing a popup modal window with some text.

When this link is reached by web crawlers is also processed trying to load the linked web site with the initial state "overview.showpopup" because onclick handler is ignored. In SPITutMainDocument we know this fundamental state is a sub-state of "overview" so one SPITutStateOverview object is created with showPopup parameter set to true:

    public void changeState(String stateName)
    {
        ...
        if (stateName.equals("overview")||stateName.equals("overview.showpopup"))
        {
            boolean popup = stateName.equals("overview.showpopup");
            this.currentState = new SPITutStateOverview(this,popup);
        }
        ...
    }

Then the popup window is automatically shown in load time, and because ItsNat is in fast-load mode the markup in the popup is rendered and sent to the client as markup, hence reached by search engine crawlers.

The class SPITutStateOverviewShowPopup loads the fragment with some markup and insert this markup to the page on top of a "modal layer":

package org.itsnat.spitut;

import org.itsnat.comp.ItsNatComponentManager;
import org.itsnat.comp.layer.ItsNatModalLayer;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLBodyElement;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateOverviewShowPopup extends SPITutState implements EventListener
{
    protected SPITutStateOverview parent;
    protected Element container;
    protected ItsNatModalLayer layer;

    public SPITutStateOverviewShowPopup(SPITutStateOverview parent)
    {
        super(parent.getSPITutMainDocument());
        this.parent = parent;

        SPITutMainDocument spiTutDoc = parent.getSPITutMainDocument();
        ItsNatHTMLDocument itsNatDoc = parent.getItsNatHTMLDocument();
        HTMLDocument doc = itsNatDoc.getHTMLDocument();
        ItsNatComponentManager compMgr = itsNatDoc.getItsNatComponentManager();
        this.layer = compMgr.createItsNatModalLayer(null,false,1,0.5f,"black",null);
        HTMLBodyElement body = (HTMLBodyElement)doc.getBody();

        DocumentFragment frag = spiTutDoc.loadDocumentFragment("overview.popup");
        this.container = ItsNatTreeWalker.getFirstChildElement(frag);
        body.appendChild(container);

        ((EventTarget)container).addEventListener("click", this, false);

        itsNatDoc.addCodeToSend("try{ window.scroll(0,-1000); }catch(ex){}");
        // try/catch is used to prevent some mobile browser does not support it
    }

    @Override
    public String getStateTitle()
    {
        return "Overview Popup";
    }

    @Override
    public String getStateName()
    {
        return "overview.showpopup";
    }

    public void handleEvent(Event evt)
    {
        dispose();
    }

    public void dispose()
    {
        ((EventTarget)container).removeEventListener("click",this, false);
        container.getParentNode().removeChild(container);
        layer.dispose();

        parent.onDisposeOverviewPopup();
    }
}

This is the template file (overview_popup.xhtml) associated to the name "overview.popup":

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Overview Popup</title>
</head>
<body>

    <div style="position:absolute; z-index:1; background:white; width:80%; height:80%; left:10%;  top:5%; padding:30px;">
        <h2>Overview Popup</h2>

        <p>Overview + popup is also a fundamental state so this text is processed by
            search engine crawlers (Google,Yahoo,Bing...), because you can reach this
            state on load time with a URL <a href="?st=overview.showpopup">like this</a>
            (you can find this text in the end of the page loaded).
        </p>

        <p><a href="?st=overview" onclick="setState('overview'); return false;"
              itsnat:nocache="true">Click to exit</a></p>
        <!-- This "useless" link is only needed in Pocket IE because with some ItsNat
          help it sends events to parent nodes, and to exit with JavaScript disabled -->


    </div>

</body>
</html>

Fundamental state detail and secondary state "More detail"


SPITutStateDetail is another fundamental state and also contains a sub-state ("More Detail"), however in this case this sub-state is not fundamental hence there is no new class inherited from SPITutState and there is no call to SPITutMainDocument.registerState(String) when this sub-state is reached.

Source code of SPITutStateDetail:

package org.itsnat.spitut;

import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateDetail extends SPITutState implements EventListener
{
    protected Element detailMoreLink;
    protected Element detailMoreElem;
    protected boolean inserted = false;

    public SPITutStateDetail(SPITutMainDocument spiTutDoc)
    {
        super(spiTutDoc);

        HTMLDocument doc = getItsNatHTMLDocument().getHTMLDocument();
        this.detailMoreLink = doc.getElementById("detailMoreId");
        ((EventTarget)detailMoreLink).addEventListener("click",this,false);
    }

    public void dispose()
    {
        ((EventTarget)detailMoreLink).removeEventListener("click",this,false);
    }

    @Override
    public String getStateTitle()
    {
        return "Detail";
    }

    @Override
    public String getStateName()
    {
        return "detail";
    }

    public void handleEvent(Event evt)
    {
        if (detailMoreElem == null)
        {
            DocumentFragment frag = spiTutDoc.loadDocumentFragment("detail.more");
            this.detailMoreElem = ItsNatTreeWalker.getFirstChildElement(frag);
        }

        if (!inserted)
        {
            Element contentParentElem = spiTutDoc.getContentParentElement();
            contentParentElem.appendChild(detailMoreElem);

            detailMoreLink.setTextContent("Hide");
            this.inserted = true;
        }
        else
        {
            detailMoreElem.getParentNode().removeChild(detailMoreElem);
            detailMoreLink.setTextContent("More Detail");
            this.inserted = false;
        }
    }
}

And the template fragment detail.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Detail</title>
</head>
<body>
    <h2>Detail</h2>
    <p>This "fundamental" state is processed by crawlers of search engines (Google, Yahoo, Bing...).
       The link below is used to load some new text, in this case the new
       state is not fundamental (is secondary) and the new text cannot be
       reached by crawlers of search engines.
    </p>
    <a href="javascript:;" id="detailMoreId" >More Detail</a><br />
</body>
</html>

Now the link pure AJAX based and the markup with more info ("More Detail") only is inserted when end users click the link, therefore new inserted markup is not reached by web crawlers and there is no call to set this state as bookmarkable, following The Single Page Interface Manifesto this state (showing the "More Detail" text) is a "secondary state".

The fragment with name "detail.more" is the file detail_more.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>More Detail</title>
</head>
<body>
    <span>
    <h3>More Detail</h3>
    <p>This text cannot be reached by search engines, because there is no
    fundamental state registered including this fragment on load time.
    </p>
    </span>
</body>
</html>

Fundamental state not_found


Finally we have the state "not_found", this state is provided to show a "state of error" when the main page is being loaded with a state name unknown (for instance an old bookmark saved before a web site redesign changing state names). When this "not_found" state is reached the <body> content of not_found.xhtml is inserted:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>State Not Found</title>
</head>
<body>

    <h2>State Not Found</h2>

</body>
</html>

Conclusion


This tutorial has shown one generic example of how to build SPI web sites with ItsNat similar to page based counterparts without sacrificing the typical features of the page paradigm like bookmarking, SEO, JavaScript disabled, Back/Forward buttons (history navigation), visit counters etc. When a web site is being ported to SPI, pages are usually converted to states, these states can be fundamental and secondary, this tutorial has shown how fundamental states are very similar to pages including templating design with all of benefits of SPI and how we can set as fundamental state virtually any state thanks to the server-centric nature of ItsNat, The Browser Is The Server approach and the fast-load feature.

Most of the code in this tutorial is infrastructure code, it could be generalized and reused in many projects, this is the fixed cost of a SPI web site, any new fundamental state just require a new plain HTML template some minimal registration code and a new class inherited from SPITutState, this class could be empty or be shared (the same class) for all fundamental states with just static content. In summary, the cost of adding a new fundamental state with only static content is almost the same as adding a simple HTML file like in page based development, with the advantage of no care and no repetition of headers, footers and, in general, content outside of the static content being added avoiding the burden of repetition in the case of page based development.


Download, Online Demo and Links


See running online

Download source code and binaries


Discussion about this tutorial at JavaLobby

Discussion about this tutorial at JavaHispano (in Spanish)



Terms of Use Privacy Statement Contributor Agreement delicious icon Add to delicious