SEO: Friendly URL construction with Spring MVC

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.

Octocat 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

  • constructed logically so that is most intelligible to humans (when possible, readable words rather than long ID numbers). For example I find that a URL link like http://www.podcastpedia.org/podcasts/1183/NPR-TED-Radio-Hour-Podcast is much more appealing than something like this www.podcastpedia.org/id-1183.html. For one you can figure out just from the URL it’s about a podcast, and briefly what the podcast might be about.
  • not too complex and not cryptic – visitors may be intimidated by extremely long and cryptic URLs that contain few recognizable words. Some users might link to your page using the URL of that page as the anchor text. If your URL contains relevant words, this provides users and search engines with more information about the page than an ID or oddly named parameter would
  • flexible – allow for the possibility of a part of the URL being removed. For example, instead of using the breadcrumb links on the page, a user might drop off a part of the URL in the hopes of finding more general content – if the user tipped http://www.podcastpedia.org/podcasts, he or she would get redirected to a list of all the podcasts available on Podcastpedia.org. Or what if the user might be visiting http://www.podcastpedia.org/podcasts/742/Distillations, but then enter just http://www.podcastpedia.org/podcasts/742/ into the browser’s address bar. Should she get a 404 or also the right page?
  • makint use of hyphens (-) – there’s a reason why Google recommends it – it’s easier to read in comparison with underscore (_) or white spaces which get encoded to %20.
  • 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:

    goole search results page highlighted

    highlight of keywords in URLs

    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:

    Episode URLs samples:

    In the following sections I will present how an episode URL is constructed, as it is more complex:

    Episode URL construction

    Episode URL construction

    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.

    Octocat Source code for this post is available on Github - podcastpedia.org is an open source project.

    Resources

    1. Google Webmaster Tools – URL Structure
    2. Search-engine-optimization-starter-guide
    Podcastpedia image

    Adrian Matei

    Creator of Podcastpedia.org and Codingpedia.org, computer science engineer, husband, father, curious and passionate about science, computers, software, education, economics, social equity, philosophy - but these are just outside labels and not that important, deep inside we are all just consciousness, right?

    Adrian’s favorite Spring and Java books

    How to enable the CORS Filter in RESTEasy

    In this post I will shortly present how to enable the provided RESTEasy CORS Filter in a JAX-RS REST API backend Continue reading