Enabling fans to discover their favorite team’s gear
How we built a navigation module that is configurable, smart, fast, and scalable
At Fanatics, Product Discovery and Site Navigation are the heart of our customer experience. We want you to be able to find the jersey of the player you’ve worshiped since you were seven and be able to buy one for your best friend, your brother, or your partner without missing a beat.
Navigation features on our sites are powered by tools and services involving numerous tech teams and site operators. Some specialize in presenting a clean and intuitive UI experience, while others are responsible for delivering high-quality, curated, sports data, component configurations, and site settings. These services fetch teams within leagues within orgs and populate our scores of requests on sites ranging from Overwatch Shop to the NBA Store.
In this article, we’ll discuss our new navigation component. This project aims to ease, automate and standardize the setup of navigation elements across our organization.
Why We need Navigation Components
Many navigation features on our sites are set up and configured manually by site operators. The URLs lists in these components depend on external data sources and maintaining them is very time-consuming. For example, we have curated lists of current top-selling sports teams and players for easier navigation, and these require regular updates by our business team.
For this project, we created a new UI interface and a dynamic backend implementation that can respond to changes in sales data. This allows for more standardized components as services are now in charge of configuring and generating the URLs and navigation layouts. With this project who hope to eliminate the need for manual configurations.
Background on the Fanatics Stack
This project required tying together our internal business tool, the web front-end, our GraphQL product finding layer, and our product finding backend. A lot of the interesting work throughout this process has been in negotiating the contract between these services.
Creating a nav component starts with tooling. Site operators define the activation context (e.g. site, page, placement) and configure a component in our content management system (CMS). The CMS publishes these configurations (and many other site settings) into our production web tier where our React-based frontend can access them. When handling a request, the web tier parses the request context to determine which components to activate and then retrieves the configurations to form component-specific GQL queries. The GQL layer translates a query into one or more requests to the Product Finding backend to fetch the required data elements (URL aggregations and image paths) for the navigation component. When it receives the response to a GQL query, the front end will render a beautiful nav component. Don’t worry, we will deep dive into each of these steps later in the article.
Where did we start?
We started working on this project by examining the structure of our backend request, knowing that this would drive the rest of the contract. Working from this request, we determined the options we had for the request fields and how these property requests could be combined within the limits of our service. Once we had an understanding of the possible request structures, we went through and narrowed the requirements according to business needs. Some of the questions we came up with:
- Which combinations of properties would the backend need to be able to support? (Fetch all Site Teams with Department sub-aggregates? Fetch all leagues, or only the Top 5? Did we need to support every combination of properties that the backend knows?)
- Should we support multiple properties in a single request? Would multiple properties ever be shown on the same “level”? (Imagine Top Teams and Top Players together)
- How much support did we want to give the business teams to choose these configurations?
Answering each of these questions required a good amount of back and forth with our Product Managers. In the end, the answers to most of these questions ended up being yes — we do want to support more, we do need to leave the flexibility to be able to support more combinations, properties, etc. Similar to the business questions we had some major technical questions, one of the main ones being: should we be requesting this all on the server-side only or make an additional client-side request?
Deciding on our Request Pattern (All Server vs Server and Client)
The product-discovery backend must be able to return both the data for a higher-level property and the data for its sub-aggregates. For example, given a league, we must be able to retrieve data for a combination of teams associated with that league. But do we need to fetch all this data in a single request, or should we split it up into multiple requests? There were a few important factors to consider.
One concern was the HTML document size. Our site has had trouble in the past with documents reaching sizes large enough to cause issues in our ESI caching layer. With a lot of content and product carousels set up on team landing pages, we occasionally see document payloads of over 1MB. Minimizing the amount of data returned in an initial server-side request and augmenting that with additional client-side calls should keep document size in check.
We also considered response time. A heavy response creates a delay in the client — a loading delay for data that may not even be visible to the user. We have mitigated increases in latency due to heavy payloads in the past. For example, we limited the number of product recommendation carousels to five per request. Similarly, we imposed limits on nav component requests. But any measure taken to improve site speed has to be balanced against business requirements which generally call for more flexibility.
We eventually decided that making a secondary client-side request to fetch sub-aggregate data elements (next level down) would allay our concerns.
We ended up with a request that looks something like this:
This template can be used for initial server-side requests and client-side requests. On the front-end, we just need to pass in the aggregate properties and sorting desired, as well as a filter override for any secondary request. Given this request structure, our backend can provide all the information needed by the nav components.
Deciding the Settings Contract
Next, we focused on the design of the CMS configuration tool for the nav component. We wanted to provide maximum flexibility for the business and enforce certain constraints to prevent invalid property combinations like “league with the sub-aggregate league”.
Our CMS tool provides basic UI functionality to edit JSON settings for the front-end, but for a business user to be able to have a validated template, we needed a new, more user-friendly, UI for the nav component. Instead of taking additional resources to build special tools, we decided that the best approach would be to have a set of predefined configurations that a site operator could choose from — preconfigured cases like, “All Leagues”, “Primary Leagues with Drilldown Teams”, and “All Gender Age Groups Featured Sort”, that were mapped to a simple enum setting:
In the example from above Data Template Type is the most important field. It specifies the structure of the backend request for a given nav component. Our expectation is that business teams will come up with new ideas that require new data templates, and it will be relatively easy for us to extend our toolkit with new configurations. In the future, we may add a tool where site operators can create their own templates.
Displaying the Component
Once our data contracts were finalized, we needed to clarify how we would control the display of the navigation component. This was complicated by the fact that this new generic navigation component was to replace a wide variety of existing, manually configured, components on our site.
Populating the Backend Response
Let’s dive into how we added support for the URL and product aggregations for navigation components to our product-discovery backend. The same backend currently powers most other navigation controls on our sites.
The request to the backend includes information such as filters that need to be applied to each of the generated URLs and which property, like league or team, the URLs should be generated for. For example, a request might come in that has a League filter for NFL and is asking for the team URLs. This would prompt our service to construct a URL for each team in the NFL, gather product counts, and fetch other internal information that exists for each team. Let’s go into more detail on the steps taken to get all required information for a response, using the component for teams in the NFL as an example.
When a request from the front-end is received, our product-discovery backend first creates a request to our search service. This search service queries Elasticsearch to get each of the teams, along with some other basic information related to each value. The search response will have an entry for each NFL team with identifying information such as the team’s name and product counts. The backend next makes a call to our Product Data Service to get product images, used by the front-end, for each resource value returned. The final step is to build a URL to navigate to each of the NFL teams using the data collected from these external calls. For each team, we build an object that includes:
- The Destination URL
- Product Image URL (if required)
- Product Count from search service
- Identifying Information about each Resource (like team name)
- The Sort Order based on Sales Data
The backend responds with the objects containing all of the above information, which will be used by the front-end to build and render the navigation component for NFL Teams. The response object for “Denver Broncos” would look something like this:
Backend Implementation Challenges and Solutions
The backend response for our current navigation and the new navigation components are very similar. The main difference in the contract is that the configuration for the navigation is now coming from the front-end request rather than being read from a static JSON file. The front-end is already calling the product-discovery backend to get information about products and navigation, so moving this information to the request doesn’t require additional calls between the two.
We decided to keep the structure of the new contract similar to the structure of our static configuration files so we could leverage existing product finding methods for external service calls and URL creation. Where needed, we also added branching logic to handle any differences that exist between the navigation components and the configuration-based navigation. We were able to reuse the logic we already had, while also providing the flexibility to add in new logic unique to navigation components
We have several more ideas aimed at improving our site navigation, adding more internal tools for navigation updates, and page-orchestration improvements. We are excited to continue to enhance our sites with innovations like these, providing our “fanatics” with a fluid, effortless shopping experience. This is the best part about working on a fairly new platform — there’s always more to do.
We would like to extend our appreciation to Matthias Spycher, for contributing to this article.