SEO: Friendly URL construction with Spring MVC

(P) Codever is an open source bookmarks and snippets manager for developers & co. See our How To guides to help you get started. Public bookmarks repos on Github ⭐🙏
Contents
What is a friendly URL?
Since I started to really use the internet, about 13 years ago, I liked to bookmark useful and interesting links for later access. Of course my bookmarks list has been growing steadily ever since and it’s become difficult to just LOOK for some bookmark on a topic I remember I might have added to the list. So I do it the Google-way, I search for it by typing some relevant words. Besides tags and good titles, URLs that contain some of the words I look for, land at the top of the search results. The same is valid for search engines, who favour friendly or well constructed URLs.
Source code for this post is available on Github - podcastpedia.org is an open source project.
But what is a “friendly” URL? I would say it’s
Lastly, remember that the URL to a document is displayed as part of a search result in Google, below the document’s title and snippet. Like the title and snippet, words in the URL on the search result appear in bold if they appear in the user’s query:
Friendly URL construction with Spring MVC
On Podcastpedia.org, the URLs for podcast and episode pages are “friendly”-built. These are the core pages of the website and therefore should be highly optimized for search engines and easy to bookmark.
Podcast URLs samples:
- https://github.com/CodepediaOrg/podcastpedia/podcasts/676/A-History-of-the-World-in-100-Objects
- https://github.com/CodepediaOrg/podcastpedia/podcasts/1/Quarks-Co-zum-Mitnehmen
- www.podcastpedia.org/podcasts/1183/NPR-TED-Radio-Hour-Podcast
Episode URLs samples:
- https://github.com/CodepediaOrg/podcastpedia/podcasts/676/A-History-of-the-World-in-100-Objects/episodes/2/AHOW-100-Solar-powered-lamp-and-charger-22-Oct-2010
- https://github.com/CodepediaOrg/podcastpedia/podcasts/1/Quarks-Co-zum-Mitnehmen/episodes/229/Quarks-Co-08-10-2013-Auf-Teilchenjagd
- https://github.com/CodepediaOrg/podcastpedia/podcasts/1183/NPR-TED-Radio-Hour-Podcast/episodes/63/Haves-and-Have-Nots
In the following sections I will present how an episode URL is constructed, as it is more complex:
The podcast URL construction resembles the same process.
Model
As you’ve seen, the following elements are present in the URL of the episode:
- podcast id
- podcast title
- episode id
- episode title
Among other things, these are java bean properties of the Episode
domain class. Object of this class type will get loaded in the model
, and the via the controller
be presented in the view
:
package org.podcastpedia.domain; import java.io.Serializable; import java.util.Date; public class Episode implements Serializable{ /** * automatic generated serialVersionUID */ private static final long serialVersionUID = -1957667986801174870L; /** identifies the podcast the episode belongs to */ public Integer podcastId; /** episode id - unique identifier of a podcast's episode */ public Integer episodeId; /** description of the episode */ public String description; /** title of the episode */ public String title; /** title of the podcast the episode belongs to */ public String podcastTitle; /** episode's transformed title with hyphens to be added in the URL for SEO optimization */ private String titleInUrl; /** episode's transformed title with hyphens to be added in the URL for SEO optimization */ private String podcastTitleInUrl; .............................. }
The two properties titleInUrl
and podcastTitleInUrl
hold the “transformed” episode’s and respectively, the podcast’s title. In the transformation process, the spaces between words are replaced with hyphens (-) and if the length exceeds a certain limit (TITLE_IN_URL_MAX_LENGTH = 100), it will be shortened:
//build the title that appears in the URL when accessing a podcast from the main application String titleInUrl = podcastTitle.trim().replaceAll("[^a-zA-Z0-9\\-\\s\\.]", ""); titleInUrl = titleInUrl.replaceAll("[\\-| |\\.]+", "-"); if(titleInUrl.length() > TITLE_IN_URL_MAX_LENGTH){ podcast.setTitleInUrl(titleInUrl.substring(0, TITLE_IN_URL_MAX_LENGTH)); } else { podcast.setTitleInUrl(titleInUrl); }
View
Let’s consider a use case, for example when searching for episodes, you get a list of results. This list of Episode
objects is loaded in the Model
via MyBatis (see my post Integrate MyBatis with Spring for that), and then via the Controller
are presented in the View
, which in this case is a JSP
file. The user has the possibility to see the details of a specific episode by clicking on the episode’s URL, that is constructed in the following manner:
<div class="results_list"> <c:forEach items="${advancedSearchResult.episodes}" var="episode" varStatus="loop"> <c:url var="episodeUrl" value="/podcasts/${episode.podcastId}/${episode.podcastTitleInUrl}/episodes/${episode.episodeId}/${episode.titleInUrl}"/> <div class="bg_color shadowy item_wrapper"> .... <div class="metadata_desc"> <a href="${episodeUrl}"> <c:out value="${episode.title}"/> </a> <div class="pub_date_media_type"> <div class="pub_date"> <fmt:formatDate pattern="yyyy-MM-dd" value="${episode.publicationDate}" /> <c:choose> <c:when test="${episode.isNew == 1}"> <span class="ep_is_new"><spring:message code="new"/></span> </c:when> </c:choose> </div> </div> <div class="ep_desc"> ${fn:substring(episode.description,0,600)} </div> </div> ....... </div> </c:forEach> </div>
Controller
This is perhaps the most interesting part in the “friendly” URL construction. Let’s have a look at the EpisodeController
, which handles episode “friendly” URLs:
package org.podcastpedia.controllers; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; ................. /** * Annotation-driven controller that handles requests to display episodes * and episode archive pages. * * @author ama */ @Controller @RequestMapping("/podcasts") public class EpisodeController { @Autowired private EpisodeService episodeService; ............ /** * Controller method for episode page. * * @param podcastId * @param episodeId * @param show_other_episodes * @param model * @param httpRequest * @return * @throws BusinessException */ @RequestMapping(value="{podcastId}/*/episodes/{episodeId}/*", method=RequestMethod.GET) public String getEpisodeDetails(@PathVariable("podcastId") int podcastId, @PathVariable("episodeId") int episodeId, @RequestParam(value="show_other_episodes", required=false) Boolean show_other_episodes, ModelMap model, HttpServletRequest httpRequest) throws BusinessException { LOG.debug("------ getEpisodeDetails : Received request to show details for episode id " + episodeId + " of the podcast id " + podcastId + " ------"); EpisodeWrapper episodeDetails = episodeService.getEpisodeDetails(podcastId, episodeId); ............. SitePreference currentSitePreference = SitePreferenceUtils.getCurrentSitePreference(httpRequest); if(currentSitePreference.isMobile() || currentSitePreference.isTablet()){ return "m_episodeDetails_def"; } else { return "episodeDetails_def"; } } .............. }
The @RequestMapping
annotation is used for mapping web requests onto specific handler classes and/or handler methods. A @RequestMapping
on the class level is not required. Without it, all paths are simply absolute, and not relative. The @PathVariable
indicates that the annotated method argument is bound to the URI template variable specified by the value in parenthesis.
So if you add the values of the @RequestMapping
at the class level (line 16) – /podcasts
– and the @RequestMapping
annotation at the method level (line=32) – {podcastId}/*/episodes/{episodeId}/*
– you get /podcasts/{podcastId}/*/episodes/{episodeId}/*
, which reproduce exactly the “friendly” episode URLs as mentioned before. The star (*) in the URL structure means it can be filled with any text. I chose to fill that with the titles of the podcast and of the episode respectively.
Note: The SitePreference part (lines 44-49) routes the visitor to the desktop or mobile version, depending on her device or selected preferences. See my post Going mobile with Spring mobile and responsive web design for more details.
Well, you see it’s pretty easy to build friendly URLs with Spring MVC 3.x. This is just one measure to improve your website visibility for search engines. But stay tuned, more posts on this topic will follow.
Source code for this post is available on Github - podcastpedia.org is an open source project.
Resources
Adrian’s favorite Spring and Java books