To build a SPA application using Tapestry we can use the Zone's system. You have to keep in mind:
public class MyLinkTransformer implements PageRenderLinkTransformer {
	
	...	
	
	@Override
	public PageRenderRequestParameters decodePageRenderRequest(final Request request) {
		...
		final int spaIndex = requestPath.indexOf("/spa/");
		if (spaIndex != -1) {
			final String page = requestPath.substring(spaIndex + 5) + "page";
			String enumPage = null;
			for (final PAGES p : PAGES.values()) {
				if (p.getPageClass().getSimpleName().toLowerCase().equals(page)) {
					enumPage = p.toString();
					break;
				}
			}
			if (enumPage != null) {
				return new PageRenderRequestParameters("specialPages/spa/Index",
				        new URLEventContext(contextValueEncoder, new String[] { enumPage }),
				        false);
			}
		}
		...
	}
}package es.carlosmontero.webapp.t5devutil.pages.specialpages.spa;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.PageRenderLinkSource;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
import org.apache.tapestry5.services.ajax.JavaScriptCallback;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
public class IndexPage {
   public enum PAGES {
      PAGE1(Block1Page.class), PAGE2(Block2Page.class), PAGE3(Block3Page.class);
      private final Class<?> clazz;
      private PAGES(final Class<?> clazz) {
         this.clazz = clazz;
      }
      public Class<?> getPageClass() {
         return clazz;
      }
   }
   @Environmental
   private JavaScriptSupport javaScriptSupport;
   @Inject
   private PageRenderLinkSource pageRenderLinkSource;
   @Inject
   private AjaxResponseRenderer ajaxResponseRenderer;
   @Inject
   private ComponentResources componentResources;
   @Inject
   private Request request;
   @InjectPage
   private Block1Page block1;
   @InjectPage
   private Block2Page block2;
   @InjectPage
   private Block3Page block3;
   @InjectComponent
   private Zone contentZone;
   private PAGES activePage;
   public void onActivate(final PAGES page) {
      activePage = page;
   }
   public Block getContentBlock() {
      if (activePage == null) {
         activePage = PAGES.PAGE1;
      }
      switch (activePage) {
      case PAGE1:
         return block1.getMainBlock();
      case PAGE2:
         return block2.getMainBlock();
      case PAGE3:
         return block3.getMainBlock();
      }
      return null;
   }
   public Object onBack(final PAGES page) {
      if (request.isXHR()) {
         activePage = page;
         ajaxResponseRenderer.addRender(contentZone);
         return null;
      }
      else {
         return page.getPageClass();
      }
   }
   public void onChangePage(final PAGES page) {
      activePage = page;
      ajaxResponseRenderer.addRender(contentZone);
      ajaxResponseRenderer.addCallback(new JavaScriptCallback() {
         @Override
         public void run(final JavaScriptSupport javascriptSupport) {
            final String restoreUrl = componentResources.createEventLink("back", page).toAbsoluteURI();
            final String newUrl = pageRenderLinkSource.createPageRenderLink(page.getPageClass()).toAbsoluteURI();
            javascriptSupport.require("spa").invoke("selectpage")
                    .with(
                            restoreUrl,
                            page + " title",
                            newUrl
                    );
         }
      });
   }
   public void setupRender() {
      javaScriptSupport.require("spa").invoke("init");
   }
}