SEO URL supporting l10n/i18n with PrettyFaces

Splash Forums PrettyFaces Users SEO URL supporting l10n/i18n with PrettyFaces

This topic contains 21 replies, has 4 voices, and was last updated by  Stephan Rudolph 3 years, 3 months ago.

Viewing 15 posts - 1 through 15 (of 22 total)
  • Author
    Posts
  • #19549

    Anonymous

    Re-posted below.

    #23441

    Anonymous

    Hello Lincoln, hello Christian,

    with great interest I read your articles about PrettyFaces and a lot of entries in the support forum regarding URL rewriting and l10n/i18n support.

    I’m in process of creating a complete new website with JSF2.1. Structure and content are almost ready now. The l10n/i18n implementation is based on standard mechanism of HTTP, Java and JSF (Locale, ResourceBundle, etc.). Currently I provide two languages (german and english) but in the future there could be some more (french and italian) eventually.

    How it works today:

    There is no langugae information provided with the URL, the language becomes selected based on the Locale in the HTTP header, provided by the browser.

    If the user prefers another language, he/she could choose it with a selector provided on the website (english | deutsch). This functionallity is implemented by page fragment global_common.xhtml (used by the main template for the whole website) and the managed bean LocaleHandler.java. It works very well.

    Now I like to create a SEO URL supporting l10n/i18n as follows:

    Always the welcome page should be accessible by the URL as follows:

    • http://<tld>

    Always during processing of the request, a two character language information (e.g. en, de, here marked with <language>) should be inserted into the URL, after the top level domain (here marked with <tld>) immediately, as follows:

    • for the welcome page:
      • http://<tld><language>
    • for any other arbitrary page:
      • http://<tld><language>/pagename
      • http://<tld><language>/levelname/pagename
      • http://<tld><language>/levelname/.../levelname/pagename

    The environment is as follows:

    • GlassFish 3.1.2.2 (build 5)
    • JSF 2.1.14
    • PrettyFaces 3.3.3

    Attached please find the files as follows:

    • web.xml
    • sun-web.xml
    • faces-config.xml
    • pretty-config.xml
    • global_common.xhtml
    • LocaleHandler.java

    In my opinion all seems to be straight forward. But unfortunately after reading and trying a lot of suggested approaches I couldn’t get it working yet.

    I would be very happy if you could give me some advice.

    I could imagine that would also be very helpful for many others who also struggle with this challange.

    Kind regards

    Stephan Rudolph

    #23442

    Anonymous

    The first post contains unrecognized <p> tags. The second post is the same than the first post, just without the unrecognized <p> tags. The first post could be deleded.

    Trying to upload some attachements results in the message:

    “You need to actually submit some content!

    Back to OCPsoft Support Forums.”

    What has to be done to submit some attachements (pretty-config.xml, web.xml, etc.)?

    Kind regards

    Stephan Rudolph

    #23443

    Hey,

    I think there are a few ways to achieve what you want. The first idea is to introduce a parent mapping for all your existing mappings that includes the language code. Something like this:

    <url-mapping id="base">
    <pattern value="/#{ lang : localeBean.lang }" />
    </url-mapping>

    <url-mapping id="welcome" parentId="base">
    <pattern value="/welcome" />
    <view-id value="/faces/welcome.xhtml" />
    </url-mapping>

    and a bean like this:

    @Named
    @RequestScoped
    public class LocaleBean {

    private String languageCode;

    /* getters / setters */

    public Locale getLocale() {
    return new Locale(languageCode);
    }

    }

    And a view configuration like this:

    <f:view locale="#{localeBean.locale}">
    ...
    </f:view>

    I think think this setup will work fine. The only problem will be, that you will have to adjust all your <pretty:link> and <h:link> components to include the value of the lang parameter. Something like this:

    <pretty:link mappingId="welcome">
    <f:param value="#{localBean.lang}">
    </pretty:link>

    or:

    <h:link outcome="/welcome.xhtml">
    <f:param name="lang" value="#{localBean.lang}" />
    </h:link>

    If you are looking for a simpler solution, you should have a look at Rewrite, which is the successor of PrettyFaces. I think it could be possible to implement something like this so that you won’t have to adjust much code. But I’m not completely sure how this would look like. But with Rewrite you can do EVERYTHING. :)

    Have a look here:

    http://ocpsoft.org/rewrite/

    and especially:

    http://ocpsoft.org/rewrite/examples/

    #23444

    A thought about how to achieve this with Rewrite. I think something like this can work:

    .addRule(
    Join.path("/{lang}/{path}")
    .where("lang").bindsTo(El.property("localeBean.lang"))
    .where("path").matches(".*")
    .to("/{path}")
    )

    Rewrite 2.0.0.Alpha3 ships with a compatibility module for PrettyFaces. This means you will be able to remove PrettyFaces 3.x and replace it with Rewrite without having to change much of your code.

    #23445

    Anonymous

    Meanwhile I could test the version using PrettyFaces and in principle it works as it should with the configuration as follows:

    <!-- lang -->

    <url-mapping id="lang">

    <pattern value="/#{languageSelected : localeHandler.languageSelected}" />

    </url-mapping>

    <!-- welcome -->

    <url-mapping id="welcome">

    <pattern value="/"/>

    <view-id value="/pages/index.xhtml"/>

    </url-mapping>

    <!-- home -->

    <url-mapping parentId="lang" id="home">

    <pattern value="/"/>

    <view-id value="/pages/index.xhtml"/>

    </url-mapping>

    <!-- company -->

    <url-mapping parentId="lang" id="company">

    <pattern value="/company"/>

    <view-id value="/pages/company.xhtml"/>

    </url-mapping>

    <h:link value="Home"

    outcome="/pages/index">

    <f:param name="languageSelected" value="#{localeHandler.languageSelected}"/>

    </h:link>

    <h:link value="Company"

    outcome="/pages/company">

    <f:param name="languageSelected" value="#{localeHandler.languageSelected}"/>

    </h:link>

    Examples of the resulting URL are:

    http://localhost:8080

    http://localhost:8080/en/

    http://localhost:8080/en/company

    There are two little drawbacks.

    1. The mapping “lang” becomes highlighted by the IDE (IntelliJ IDEA) and a error message becomes shown as follows:

      “The content of element “url-mapping” is not complete. One of ‘{“http://ocpsoft.com/prettyfaces/3.3.3&#8221;:query-param, “http://ocpsoft.vom/prettyfaces/3.3.3&#8243;:view-id}” is expected.”

    2. It’s a lot of work to equip each h:link with <f:param name="languageSelected" value="#{localeHandler.languageSelected}"/>
    3. However, if it works it would be worth to do the effort.

      In that case, is there a chance to get rid of the trailing “/” in case of http://localhost:8080/en/ for the localized welcome page?

      Now I’m curious about trying the version with Rewrite.

      Kind regards

      Stephan Rudolph

    #23446

    The mapping “lang” becomes highlighted by the IDE (IntelliJ IDEA) and a error message becomes shown as follows:

    “The content of element “url-mapping” is not complete. One of ‘{“http://ocpsoft.com/prettyfaces/3.3.3&#8221;:query-param, “http://ocpsoft.vom/prettyfaces/3.3.3&#8243;:view-id}” is expected.”

    Seems like the XSD file for pretty-config.xml is not correct here. Thanks for pointing that out. You can ignore this. It is not a real problem. But you could also add a view-id element with some value. This will be OK because it will be overwritten by the parenting mechanism AFAIK.

    It’s a lot of work to equip each h:link with <f:param name=”languageSelected” value=”#{localeHandler.languageSelected}”/>

    However, if it works it would be worth to do the effort.

    Yeah, that’s true. Perhaps you could try some clever global search-and-replace operation to add this. Something like replace all:

    </h:link>

    with:

    <f:param name="languageSelected" value="#{localeHandler.languageSelected}"/>
    </h:link>

    :)

    Regarding the trailing slash on the welcome page. You could add a view id to the base mapping. Then the base mapping will also be your welcome page. Something like this:

    <url-mapping id="lang">
    <pattern value="/#{languageSelected : localeHandler.languageSelected}" />
    <view-id value="/pages/index.xhtml"/>
    </url-mapping>

    But I’m not 100% sure if this will work. You will have to give it a try.

    #23447

    Anonymous

    Your hint regarding the trailing slash solves two issues at once.

    1. Now it works exacty as I want it :-).
    2. There is no reason for complains, regarding violation of the schema, by the IDE anymore.

    Now the code it looks like this:

    <!-- welcome -->

    <url-mapping id="welcome">

    <pattern value="/"/>

    <view-id value="/pages/index.xhtml"/>

    </url-mapping>

    <!-- lang -->

    <url-mapping id="lang">

    <pattern value="/#{languageSelected : localeHandler.languageSelected}" />

    <view-id value="/pages/index.xhtml"/>

    </url-mapping>

    <!-- company -->

    <url-mapping parentId="lang" id="company">

    <pattern value="/company"/>

    <view-id value="/pages/company.xhtml"/>

    </url-mapping>

    <h:link value="Home"

    outcome="/pages/index">

    <f:param name="languageSelected" value="#{localeHandler.languageSelected}"/>

    </h:link>

    <h:link value="Company"

    outcome="/pages/company">

    <f:param name="languageSelected" value="#{localeHandler.languageSelected}"/>

    </h:link>

    Examples of the resulting URL are:

    http://localhost:8080

    http://localhost:8080/en

    http://localhost:8080/en/company

    So far I equipped only some of the <h:link&#62 tags with the <f:param name="languageSelected" value="#{localeHandler.languageSelected}"/&#62 tag, for testing purpose. For sure, for all other’s, I will use auto-replace function of the IDE :-)

    Regarding the URL mapping with PrettyFaces all works like I want it.

    The next task for me will be to create a breadcrumb menu for this website, based on the method PrettyContext.getCurrentInstance().getRequestURL().

    But now, so all work as I want it, in general, I ask myself, should I try the version using Rewrite as well?

    Is there some more, regarding the URL mapping and creating a breadcrumb menu, that I could gain, against the effort to change my web app from using PrettyFaces to using Rewrite?

    #23448

    Rewrite allows you to do much more compared to PrettyFaces. If you want to get a general idea how Rewrite works, have a look at these examples:

    http://ocpsoft.org/rewrite/examples/

    It’s important to note that PrettyFaces 3.3.3 is stable and I for myself use it in many projects. But there is no more active development happening for it. The future is Rewrite. :)

    #23449

    Anonymous

    Many thanks for your information regarding your intention about the future way of your code.

    Meanwhile I implemented the breadcrumb menu successfully based on FacesContext.getCurrentInstance().getViewRoot().getViewId() instead of PrettyContext.getCurrentInstance().getRequestURL(). So there is no dependency to PrettyFaces anymore.

    Now I like to try rewriting my URL by using Rewrite instead of PrettyFaces, as proposed by you.

    What would you recommend, how could I store an URL fragment (the part after the hash sign # in an URL) as a view parameter for future use in the breadcrumb menu?

    Kind regards

    Stephan Rudolph

    #23450

    Could you tell us some more details on how your current implementation of the breadcrumb menu works?

    If it currently works without PrettyContext, you won’t have to change anything when you switch to Rewrite. The .getViewRoot().getViewId() should work exactly like before.

    #23451

    Anonymous

    The solution consists of three components.

    A managed bean:

    private UIViewRoot vr;
    private ResourceBundle rb;
    /**
    * Constructs a BreadcrumbHandler object handling the breadcrumbs of view.
    */
    public BreadcrumbHandler() {
    this.vr = FacesContext.getCurrentInstance().getViewRoot();
    this.rb = ResourceBundle.getBundle(VALUE_RB_VARNAME, this.vr.getLocale());
    }

    /**
    * Providing the URI of the current view.
    * @return String, representing the URI of the current view.
    */
    private String getUri() {
    String uri;
    uri = this.vr.getViewId();
    uri = uri.substring(0, uri.indexOf(EXT));
    //return this.uri = uri;
    return uri;
    }

    /**
    * Providing an URI reduced by its highest element.
    * @param uri String, representing the given URI.
    * @return String, representing the given URI reduced by its highest element.
    */
    private String getUriBefore(String uri) {
    return uri.substring(0, uri.lastIndexOf("/"));
    }

    /**
    * Providing the label key for an given URI.
    * @param uri String, representing the URI, for what a label key should be provided.
    * @return String, representing the label key for the given URI.
    */
    private String getKey(String uri) {
    String key = uri;
    key = key.substring(key.indexOf(ROOT) + ROOT.length(), key.length());
    key = key.replaceAll("/", ".");
    key = KEY_LABEL_LINK_INTERNAL_PFX + key;
    return key;
    }

    /**
    * Providing the localized label for a given key.
    * @param key String, representing the key, for what a label should be provided.
    * @return String, representing the localized label for the given key.
    */
    private String getLabel(String key) {
    String label;
    label = this.rb.getString(key);
    if (null == label) {
    label = "TBD";
    }
    //return this.label = label;
    return label;
    }

    /**
    * TODO has to become a listener for locale changes
    * Assembling breadcrumbs
    * @return List<String[]> containing all breadcrumbs. Each breadcrumb consists of an URI, a label and
    * a CCS style class name.
    */
    public List<String[]> getBreadcrumbs() {
    List<String[]> breadcrumbs = new ArrayList<String[]>();
    // Retrieving leaf data
    String uri = this.getUri();
    String key = this.getKey(uri);
    String label = this.getLabel(key);
    String style = STYLE_CLASS_LEAF;
    // If we are not at home
    if (!uri.endsWith(HOME)) {
    Boolean notRoot = Boolean.TRUE;
    while (notRoot) {
    String[] bc = new String[3];
    bc[0] = uri;
    bc[1] = label;
    bc[2] = style;
    logger.info("BreadcrumbHandler() getBreadcrumbs [leafe] uri=" + uri + " key=" + key + " label=" + label);
    breadcrumbs.add(bc);
    // Retrieving branch data
    uri = this.getUriBefore(uri);
    key = this.getKey(uri);
    label = this.getLabel(key);
    style = STYLE_CLASS_BRANCH;
    logger.info("BreadcrumbHandler() getBreadcrumbs [branch] uri=" + uri + " key=" + key + " label=" + label);
    if (uri.endsWith(ROOT)) {
    notRoot = Boolean.FALSE;
    }
    }
    // Adding home data
    String[] bc = new String[3];
    uri = URI_HOME;
    key = this.getKey(uri);
    label = this.getLabel(key);
    bc[0] = uri;
    bc[1] = label;
    bc[2] = STYLE_CLASS_HOME;
    logger.info("BreadcrumbHandler() getBreadcrumbs [home] uri=" + uri + " key=" + key + " label=" + label);
    breadcrumbs.add(bc);
    // Removing link from leaf breadcrumb
    breadcrumbs.get(0)[0]="#";
    // Reverse breadcrumbs assembly to: home > branch...branch > leaf
    Collections.reverse(breadcrumbs);
    }
    return breadcrumbs;
    }

    Labels for each view, provided with the resource bundle, like:

    TLD.LABEL_LINK_INTERNAL.index=Start
    TLD.LABEL_LINK_INTERNAL.company=Unternehmen
    TLD.LABEL_LINK_INTERNAL.company.management=Geschäftsleitung
    TLD.LABEL_LINK_INTERNAL.company.location=Standort
    ...
    TLD.LABEL_LINK_INTERNAL.index=Home
    TLD.LABEL_LINK_INTERNAL.company=Company
    TLD.LABEL_LINK_INTERNAL.company.management=Management
    TLD.LABEL_LINK_INTERNAL.company.location=Location

    The tags in the header template:

    <div class="breadcrumb">
    <h:form>
    <c:forEach var="breadcrumb" items="#{breadcrumbHandler.breadcrumbs}">
    <h:link value="#{breadcrumb[1]}"
    outcome="#{breadcrumb[0]}"
    styleClass="#{breadcrumb[2]}"/>
    </c:forEach>
    </h:form>
    </div>

    That’s all.

    The clue between a view and its label is the variable part of the label key, that means the part after TLD.LABEL_LINK_INTERNAL, consisting of the viewid.

    The label becomes reused for the breadcrumbs, every local link as well as the title of each view.

    This works very well and is completely independend of PrettyFaces.

    Now I like to include view fragments (the part in the URL after a # sign) into the breadcrumbs too.

    So far as I know, there is no way to get a fragment from the viewid itself. So I have to retrieve it from the URL or provide it with the internal link as a parameter.

    My question now is, how could I get a fragment from the URL using Rewrite.

    #23452

    It’s not possible to access the fragment of incoming quests. The reason for this is very simple. Browsers do not include the fragment part of the URL in the request. That’s because the fragment is just a link to scroll to after the page has been loaded.

    See for example:

    http://stackoverflow.com/questions/5738627/was-there-ever-a-proposal-to-include-the-url-fragment-into-the-http-request

    #23453

    Anonymous

    Thank you for the interesting link regarding URL fragments and HTTP requests. I’m aware of that. But I have probably expressed myself ambiguous.

    I like to have the fragments for my breadcrumbs only. For my breadcrumbs there are only two cases, fragments come in place.

    1. An internal link refers to a page fragment of our site.
    2. Somebody did a bookmark of a page fragment of our site and uses that bookmark.

    In case 1 I have to provide a view parameter holding the fragment when doing the internal link, like:

    <h:link
    value="#{bundlename['TLD.LABEL_LINK_INTERNAL.solutions.provisioning-weblogic-platform.supported-products']}"
    outcome="/pages/solutions/provisioning-weblogic-platform"
    fragment="supported-products">
    <f:param name="fragment" value="supported-products"/>
    </h:link>

    In case 2 I have to retrieve the fragment from the URL and make it available during rewriting.

    .addRule(
    Join.path("/{lang}/{path}")
    .where("lang").bindsTo(El.property("localeBean.lang"))
    .where("path").matches(".*")
    .to("/{path}")

    So what I’m asking for is just an extention of your proposal mentioned above using Rewrite, that makes a fragment available e.g. for breadcrumbBean.fragment.

    #23454

    In case 2 I have to retrieve the fragment from the URL and make it available during rewriting.

    But this won’t work for incoming request, because the fragment isn’t included in the URL. That’s the problem.

    If I understand you correctly, you need access to the “current fragment” so you can render the breadcrumb correctly, right?

    Perhaps I misunderstand your requirement. :)

Viewing 15 posts - 1 through 15 (of 22 total)

You must be logged in to reply to this topic.

Comments are closed.