Dashboard > Hippo CMS > ... > 05. Hippo CMS Best Practices > Decoupling repository and website URI spaces using the Sitemenu
Decoupling repository and website URI spaces using the Sitemenu
Added by Niels van Kampenhout, last edited by Niels van Kampenhout on Feb 01, 2007  (view change) show comment
Labels: 


Also read about the Sitemenu Input Module before you start implementing the sitemenu.

Theory

Before starting the development of a website, an important design decision to make is how the URI spaces of the repository and the website will relate. For very simple websites, it may be preferable to maintain a one-on-one relationship, in which the website URIs are determined by the repository URIs of the documents they (re)present. For example: suppose we have a document that lives in the repository at http://myrepo/tutorial/files/tutorial.preview/content/articles/howto/doc.xml. Our webpage could have a URI like http://www.mywebsite.com/articles/howto/doc.html. Notice the corresponding part: /articles/howto/doc..

Now imagine what happens if we decide to restructure our repository, and move our document to http://myrepo/tutorial/files/tutorial.preview/content/tutorials/hippocms/howtos/doc.xml? The website URI will have to change too! This is not a good thing, as people might have bookmarked the page. Or we might have to rewrite the Cocoon Sitemap of the website, to map the new repository URIs. We really don't want to do this.

So we need to decouple the two URI spaces, and add a mapping between the two. And we should make this mapping configurable, because changes like the one described above a very likely to happen. The easiest way to do this, is create an XML document that contains the mapping, and make it available for editing in Hippo CMS. The frontend application (the website) can then use this XML file to determine which queries it should do, and how it should present the results, for each request it receives.

Practice

Backend (CMS)

Adding the sitemenu backend template

We can invent any XML structure for the sitemenu we like. The general idea however is that it should look like this:

<root>
  <menu1>
    <level1>
      <name>intro</name>
      <title>Introduction</title>
      <datasource>/content/tutorial/intro.xml</datasource>
      <template>document</template>
    </level1>
    <level1>
      <name>articles</name>
      <title>Articles</title>
      <datasource>/content/tutorial/articles</datasource>
      <template>10-most-recent-documents</template>
      <level2>
        <name>archive</name>
        <title>Old articles</title>
        <datasource>/content/tutorial/archive</datasource>
        <template>10-most-recent-documents</template>
      </level2>
    </level1>
  </menu1>
</root>

Each menu item element (level1, level2, etc.) has four child elements:

  • name: the name of this item as it will appear in the URI of the corresponding page
  • title: the label of the item as it will be displayed in the navigation on the page
  • datasource: a (relative) repository URI to be used as source for a DASL query. It can point to a folder as well as a single document.
  • template: the type of page this is: a text page, a news overview, a list of recent changes, etc. This determines what information is to be retrieved from the repository, and how it is to be presented to the user.

The example sitemenu above defines the following URIs:

See the documentation on backend template development for instruction on how to make an editable document type.

Frontend (website)

Defining the sitemenu input module

See Sitemenu Input Module.

DASL query

For each page template defined in the sitemenu backend template, we need to write a DASL query to fetch the desired content and/or metadata. Since we have defined two possible page templates (document and 10-most-recent-documents), we write two corresponding DASL queries, and put them in the src/site/dasl/queries folder.

document.xml

<request xmlns="http://hippo.nl/webdav/1.0"
  xmlns:d="DAV:"
  xmlns:h="http://hippo.nl/cms/1.0"
  xmlns:jx="http://apache.org/cocoon/templates/jx/1.0"
  xmlns:slide="http://jakarta.apache.org/slide/"
  target="${target}"
  method="SEARCH"
  jx:cache-key="${ck}"
  jx:cache-validity="${cacheValidity}">
  <header name="Depth" value="3"/>
  <body>
    <d:searchrequest>
      <d:basicsearch>
        <d:select>
          <d:prop>
            <d:displayname/>
            <h:caption/>
          </d:prop>
        </d:select>
        <d:from>
          <d:scope>
            <d:href>${path}</d:href>
            <d:depth>0</d:depth>
          </d:scope>
        </d:from>
      </d:basicsearch>
    </d:searchrequest>
  </body>
</request>

10-most-recent-documents.xml

<request xmlns="http://hippo.nl/webdav/1.0"
  xmlns:d="DAV:"
  xmlns:h="http://hippo.nl/cms/1.0"
  xmlns:jx="http://apache.org/cocoon/templates/jx/1.0"
  xmlns:slide="http://jakarta.apache.org/slide/"
  target="${target}"
  method="SEARCH"
  jx:cache-key="${ck}"
  jx:cache-validity="${cacheValidity}">
  <header name="Depth" value="1"/>
  <body>
    <d:searchrequest>
      <d:basicsearch>
        <d:select>
          <d:prop>
            <d:displayname/>
            <h:caption/>
            <d:creationdate/>
          </d:prop>
        </d:select>
        <d:from>
          <d:scope>
            <d:href>${path}</d:href>
            <d:depth>1</d:depth>
          </d:scope>
        </d:from>
        <d:where>
           <d:and>
             <d:not>
               <d:is-collection/>
             </d:not>
           </d:and>
        </d:where>
        <d:orderby>
          <d:order>
            <d:prop><d:creationdate/></d:prop>
            <d:descending/>
          </d:order>
        </d:orderby>
        <d:limit>
          <d:nresults>10</d:nresults>
        </d:limit>
      </d:basicsearch>
    </d:searchrequest>
  </body>
</request>
Page templates (XSL stylesheets)

To present the results that we get back from the DASL queries, to the visitors of the website, we also need to write an XSL stylesheet for each page template: document.xsl and 10-most-recent-documents.xsl. These are saved in the folder src/site/transformers/pagetypes.

Pipelines

In the main sitemap (src/sitemap.xmap), we can now define one pipeline for each sitemenu level. The pipeline below is for level 1.

<map:match pattern="*">
  <map:aggregate element="sitecontent" label="mycontent">
    <map:part element="sitemenu" src="cocoon:/sitemenu"/>
    <map:part element="maincontent"
      src="cocoon:/dasl/query/{sitemenu:template}{sitemenu:datasource}"/>
  </map:aggregate>
  <map:transform src="transformers/pagetypes/{sitemenu:template}.xsl"/>
  <map:serialize type="xhtml"/>
</map:match>

Using the sitemenu input module, the 'name' and 'template' values of the corresponding item in the sitemenu.xml file are retrieved, and passed as arguments to the pipeline performing the DASL query. The XSL stylesheet used for transformation to XHTML is also selected based on the 'template' value of the corresponding menu item.

We can make a generic DASL pipeline in a sub sitemap mounted under prefix 'dasl':

<!--
  caching dasl
  {1} = dasl id
  {2} = path relative to handbook location
  -->
<map:match pattern="query/*/**">
  <map:call function="jxcache">
    <map:parameter name="target" value="{repository:files}{global:handbooksLocation}"/>
    <map:parameter name="key:path" value="{repository:rootPath}{repository:filesPath}{global:handbooksLocation}/{2}"/>
    <map:parameter name="depth" value="2"/>
    <map:parameter name="key:dasl" value="{1}"/>
    <map:parameter name="pipeline" value="execute-dasl/{1}"/>
  </map:call>
</map:match>

<!--
  the actual dasl query execution pipeline called from flowscript
  {1} = dasl id
  -->
<map:match pattern="execute-dasl/*">
  <map:generate src="queries/{1}.xml" type="jx"/>
  <map:transform type="webdav"/>
  <map:transform src="transformers/dasl-result.xsl">
    <map:parameter name="prefix" value="{repository:filesPath}"/>
  </map:transform>
  <map:transform type="include"/>
  <map:serialize type="xml"/>
</map:match>

See the documentation on DASL queries for details.

Internal links: the tricky stuff

Disconnecting the repository layout from the URI space of the frontend has many advantages. However, there is one drawback: it is not straightforward anymore to make internal links (links in one document in the repository to another). The frontend needs extra functionality to do a reverse lookup through the sitemenu, in order to determine the frontend URI to link to.

Luckily, Cocoon provides just the tools to do this. The sitemenu can be transformed to a locationmap using a Cocoon pipeline. The LinkRewriterTransformer can use this dynamically generated locationmap to rewrite all internal links on a page.

So first we need a pipeline to transform the sitemenu into a locationmap. Let's call it "linkmap".

<map:match pattern="linkmap">
   <map:aggregate element="linkmap">
     <map:part element="menu" src="repository://content/nova/common/nav/sitemenu.xml"/>
   </map:aggregate> 
   <map:transform src="transformers/libs/util/linkmap.xsl" label="mycontent"/>
   <map:transform src="transformers/libs/util/linkmap2locationmap.xsl"/>
   <map:serialize type="xml"/>
</map:match>

We generate the sitemenu XML document. We transform it into a locationmap using an XSL file "sitemenu2linkmap.xsl".

To use the generated linkmap as a locationmap (to be used by the linkrewriter), we need to define it in cocoon.xconf:

<component-instance name="menulm"
                    class="org.apache.cocoon.components.modules.input.LocationMapModule" 
                    logger="core.modules.linkmap">
  <file src="cocoon://linkmap"/>
  <cacheable>true</cacheable>
  <reloadable>true</reloadable>
</component-instance>

In our sitemap we need to declare the LinkRewriterTransformer, and configure it to use the linkmap locationmap:

<map:transformer name="linkrewriter" src="org.apache.cocoon.transformation.LinkRewriterTransformer">
  <schemes>menulm</schemes>
  <link-attrs>href action src</link-attrs>
</map:transformer>

Now we can prefix every repository URI in our documents with "menulm:", and use the LinkRewriterTransformer to rewrite it as a website URI!

<map:match pattern="*">
  <map:aggregate element="sitecontent" label="mycontent">
    <map:part element="sitemenu" src="cocoon:/sitemenu"/>
    <map:part element="maincontent"
      src="cocoon:/dasl/query/{sitemenu:/root/menu1/level1[name='{1}']/template}{sitemenu:/root/menu1/level1[name='{1}']/datasource}"/>
  </map:aggregate>
  <map:transform src="transformers/pagetypes/{sitemenu:/root/menu1/level1[name='{1}']/template}.xsl"/>
  <map:transform type="linkrewriter"/>
  <map:serialize type="xhtml"/>
</map:match>

References

Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.5.7 Build:#813 Aug 28, 2007) - Bug/feature request - Contact Administrators