- ExtensionlessURLs
Available since OmniFaces 1.0
FacesViews is a mechanism to use SEO-friendly extensionless URLs in a Faces application without the need to enlist individual Facelet source files in some configuration file.
By default, all URLs generated by ViewHandler.getActionURL(FacesContext, String)
, which is used by among others <h:form>
, <h:link>
, <h:button>
and all extended tags, will also be extensionless. And, URLs with an extension will be 301-redirected to the extensionless one.
Usage
Zero configuration
Put Facelets source files into /WEB-INF/faces-views
directory. All Facelets files in this special directory will be automatically scanned as extensionless URLs.
Minimal configuration
Below is the minimal web.xml
configuration to make all Facelets source files found in the root folder and all subdirectories of the public web content (excluding /WEB-INF
, /META-INF
and /resources
) available as extensionless URLs:
<context-param>
<param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
<param-value>/*.xhtml</param-value>
</context-param>
The path pattern /*.xhtml
basically means that all files with the .xhtml
extension from the directory /
must be scanned, including all sub directories. In case you want to scan only .xhtml
files in the directory /foo
, then use path pattern of /foo/*.xhtml
instead. In case you want to scan all files in the directory /foo
, then use path pattern of /foo
. You can specify multiple values separated by a comma.
MultiViews configuration
Enabling MultiViews is a matter of suffixing the path pattern with /*
. The support was added in OmniFaces 2.5. Below is the web.xml
configuration which extends the above minimal configuration with MultiViews support:
<context-param>
<param-name>org.omnifaces.FACES_VIEWS_SCAN_PATHS</param-name>
<param-value>/*.xhtml/*</param-value>
</context-param>
On an example URL of https://example.com/context/foo/bar/baz
when neither /foo/bar/baz.xhtml
nor /foo/bar.xhtml
exist, but /foo.xhtml
does exist, then the request will forward to /foo.xhtml
and make the values bar
and baz
available as injectable path parameters via @
Param
in the managed bean associated with /foo.xhtml
.
@Inject @Param(pathIndex=0)
private String bar;
@Inject @Param(pathIndex=1)
private String baz;
Advanced configuration
See package documentation for configuration settings as to mapping, filtering and forwarding behavior.
PrettyFaces
Note that there is some overlap between this feature and PrettyFaces. The difference is that FacesViews has a focus on zero- or very minimal config, where PrettyFaces has a focus on very powerful mapping mechanisms, which of course need some level of configuration. As such FacesViews will only focus on auto discovering views and mapping them to both .xhtml
and to no-extension without needing to explicitly declare the FacesServlet
in web.xml
.
Specifically, FacesViews will thus not become a general URL rewriting tool (e.g. one that maps path segments to parameters, or that totally changes the name of the URL). For this the user is advised to look at the aforementioned PrettyFaces.
The showcase application runs on FacesViews and is thus an implicit demo. An extra demo is given below:
Navigate to FacesViews pages without an extension: viewsdemo
<p>
The showcase application runs on FacesViews and is thus an implicit demo. An extra demo
is given below:
</p>
<p>
Navigate to FacesViews pages without an extension:
<a href="#{request.contextPath}/viewsdemo">viewsdemo</a>
</p>
package org.omnifaces.showcase.facesviews;
import java.io.Serializable;
import java.util.Date;
import jakarta.enterprise.context.Conversation;
import jakarta.enterprise.context.ConversationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named
@ConversationScoped
public class PostRedirectBean implements Serializable {
private static final long serialVersionUID = -1209535053351028792L;
private Date date = new Date();
@Inject
private Conversation conversation;
public String toViewsDemo() {
if (conversation.isTransient()) {
conversation.begin();
}
return "/viewsdemo?faces-redirect=true";
}
public String toSecond() {
if (conversation.isTransient()) {
conversation.begin();
}
return "/other/second?faces-redirect=true";
}
public Date getDate() {
return date;
}
}
<ui:composition template="/WEB-INF/templates/layout.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="jakarta.faces.core"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:o="http://omnifaces.org/ui"
>
<ui:define name="contentTitle">Viewsdemo</ui:define>
<ui:define name="content">
<p>
This page resides in the <code>/WEB-INF/faces-views</code> folder as
<code>viewsdemo.xhtml</code>
and has been automatically scanned by FacesViews. It can be requested with both its
original extension (<code>viewsdemo.xhtml</code>) as well as without its extension
(<code>viewsdemo</code>). When <a href="#{request.contextPath}/viewsdemo.xhtml">requesting with an extension</a>
a redirect will take place to its extensionless variant.
</p>
<p>
To demo that extensionless URLs can be used in combination with other URL rewriters (like the CDI conversation
scope), the PostRedirectGet variant starts a conversation. When going back and forth between the demo pages with
PRG, the created time should stay the same after the first navigation.
</p>
<p>
Bean created at: #{postRedirectBean.date}
</p>
<p>
<h:link value="Link to other FacesViews scanned page" outcome="/other/second" />
</p>
<p>
Command button that invokes an action and afterwards redirects to the "second" page:
</p>
<h:form id="form">
<h:commandButton value="PostRedirectGet to other FacesViews page with conversation" action="#{postRedirectBean.toSecond}" />
</h:form>
<hr />
<p>
<h:link value="Back to entry demo page" outcome="/facesviews/ExtensionlessURLs" />
</p>
</ui:define>
</ui:composition>
<ui:composition template="/WEB-INF/templates/layout.xhtml"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="jakarta.faces.core"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:o="http://omnifaces.org/ui"
>
<ui:define name="contentTitle">Second</ui:define>
<ui:define name="content">
<p>
This page resides in the <code>/WEB-INF/faces-views</code> folder as
<code>other/second.xhtml</code>
and has been automatically scanned by FacesViews as well.
</p>
<p>
Bean created at: #{postRedirectBean.date}
</p>
<p>
<h:link value="Back via link" outcome="/viewsdemo" />
</p>
<p>
Command button that invokes an action and afterwards redirects to the "viewsdemo" page:
</p>
<h:form>
<h:commandButton value="Back via PRG with conversation" action="#{postRedirectBean.toViewsDemo}" />
</h:form>
<hr />
<p>
<h:link value="Back to entry demo page" outcome="/facesviews/ExtensionlessURLs.xhtml" />
</p>
</ui:define>
</ui:composition>
API documentation
Java source code
org.omnifaces.facesviews.FacesViewsForwardingFilter
org.omnifaces.facesviews.ExtensionAction
org.omnifaces.facesviews.FacesViewsResourceHandler
org.omnifaces.facesviews.FacesViews
org.omnifaces.component.output.PathParam
org.omnifaces.ApplicationProcessor
org.omnifaces.facesviews.FacesViewsViewHandler
org.omnifaces.facesviews.UriExtensionRequestWrapper
org.omnifaces.facesviews.PathAction