Optimizely Graph Best Practices - Content Modelling and Querying
Introduction
With the Mando Group team having worked extensively with Optimizely Graph over the last 12+ months, we have uncovered a number of best practices along the way.
For readability, I have split the best practices across two posts. This post includes sections on content modelling and querying content within Optimizely Graph. Part 2 will contain our best practice findings relating to managing and maintaining the implementation, including: Performance optimisation, security, managing API keys, integration strategies and governance.
These posts are designed for developers, solution architects, DevOps teams, and anyone responsible for building Optimizely Graph-powered applications or sites.
As with a lot of best practice guides, this post leans heavily on the existing contributions of others from within the Optimizely Community and I've given a list of related links at the bottom of the post for those that have most significantly contributed.
1. Content Modelling Best Practices
Good content modelling is essential to getting value from Graph. However, most of the items in this area are just normal content modelling best practices, rather than anything Optimizely Graph-specific. These include:
Do:
1. Do: Design with reuse in mind
Use modular content types (e.g. Author, PromoBlock, CTA, FAQ) that can be linked and reused in multiple places.
Doing this allows content to be shared in multiple places – providing familiarity and reducing the content maintenance burden for editors.
Additionally, it provides a consistent structure for Optimizely Graph content consumers (e.g. headless websites, mobile applications, etc.) and facilitates more sophisticated caching strategies.
2. Do: Use structured fields
Favour structured fields (int, bool, DateTime, ContentReference, etc.) over (plain or rich) text fields where appropriate. This allows cleaner queries against these fields and more reliable filtering.
3. Do: Establish meaningful relationships
Use ContentArea, ContentReference, and ContentReferenceList fields to link content objects logically (e.g. a BlogPost with related Author, Tags, Category, or PromoBlock).
Whilst having more generic content types (e.g. 'RichTextField') may reduce the overall number of content types, it makes understanding and working with the content less intuitive.
4. Do: Use naming conventions consistently
Standardise field names to simplify query building and team handovers.
For example, don't call the main H1 field 'Title' in one content type and then 'Heading' in another defined content type.
5. Do: Set PropertyIndexMode correctly
As stated in the online documentation (here), if you know for sure that a particular field within a page or block will not be used for filtering or searching on then you can set the `OutputOnly` mode using the following syntax:
[GraphProperty(PropertyIndexingMode.OutputOnly)]public virtual string Description { get; set; }
Additionally, if you want the field to be searchable (using a where clause) but it does not need to be searchable with full-text search then you can set this as follows:
[GraphProperty(PropertyIndexingMode.Default)]public virtual string Description { get; set; }
Just be mindful that if the Graph client requirements change around these fields then you'll need to make the necessary attribute change/removal, re-deploy the application and re-index the Graph content.
Don't:
1. Don't: Use website-specific field names
Keep in mind that content stored in Graph may be used in lots of different contexts (e.g. Website, mobile app, kiosk application, etc.) and avoid using field names that only make sense in one of these.
For example, don't call a field `RightColumnText` if the content could also be used on a mobile app where the content is shown in a single column. Instead, name the field something like `SecondaryContentText` - which makes sense in both contexts.
2. Don't: Overuse XhtmlString (a.k.a. Rich text) fields
Breaking larger blocks of rich text into more structured, separate fields may make the content more flexible and reuse simpler.
For example, rather than having a rich text field that contains some formatted text, an image and a table, split this content into separate fields to allow each part to be used independently (without the need for additional client-side processing).
3. Don't: Duplicate similar content types across sites or channels
Having consistent content types across as much of the client's digital estate as possible makes reuse simpler. For example, don't have a 'Blog Article' content type on their main site and then a different 'Blog Post' content type on one of their microsites – Having shared content model projects allows reuse of content types as much as possible.
2. Querying Best Practices
Do:
1. Do: Use specific, shallow queries
Avoid querying deeply nested objects unless required. Shallow, purpose-specific queries are faster and more maintainable.
Request only the fields that you will use on your frontend. There can sometimes be a temptation to fetch additional fields for testing or verification purposes. Ensure that you remove these from your final queries.
2. Do: Paginate large result sets
Optimizely Graph has two mechanisms for paginating large data sets: Cursor-Based vs Offset Pagination.
In line with the Optimizely documentation (here), using the traditional offset pagination (using the standard 'skip' and 'limit' parameters) is typically more efficient on smaller data sets (e.g. fewer than 10,000 results), but the selected mechanism should depend on your individual use case.
Whichever approach, the principle still applies that selecting only the number of results needed, and not needing to discard results, is far more efficient.
3. Do: Use variables for flexibility
Define filters and projections as GraphQL variables to avoid hardcoding and to support front-end reuse.
Always use GraphQL variables instead of hardcoding values directly into your queries.
# Instead of hardcoding values:
query { BlogPost(where: { Status: { eq: "Published" } }, limit: 50) { ... } }
# Use variables:
query GetBlogPosts($status: String!, $limit: Int!) {
BlogPost(where: { Status: { eq: $status } }, limit: $limit) { ... }
}
Doing it this way allows for using cached templates and queries (which I will cover in the second post) – providing significant efficiency gains. However, beyond performance, variables also enable query reusability across components, provide type safety and validation, prevent injection attacks, and make queries easier to maintain.
Ensure that you define explicit types for all variables and use non-null types (!) when values are required. This is particularly valuable in component-based frameworks where the same query can be shared across multiple components with different parameter values.
4. Do: Filter and sort server-side (i.e. Within the Graph query)
A broad rule of thumb should be to apply where and order conditions within Graph queries rather than handling data manipulation in the client-side.
The where clause allows you to filter content based on property values, reducing the amount of data transferred and improving response times. However, not all filtering approaches are created equal.
Use exact matches and equality operators (eq, in) whenever possible, as these execute faster than partial matches or complex string operations. For example, filtering by content type, status, or ID is highly efficient, while using contains or startsWith on large text fields requires more processing.
5. Do: Experiment and consider alternative options
Despite the broad-brush best practice guidelines above, I would still recommend testing your queries and considering all approaches.
For example, if you know that your website visitors almost always scroll to view more articles that are dynamically loaded, then pre-fetching these with a slightly less efficient Graph query (with a bigger page size) and hiding/caching the extra values may provide a faster and more enjoyable user experience.
Don't:
1. Query entire collections with no limits
Avoid querying large collections without specifying pagination parameters. Queries like BlogPost { items { ... } } without a first or limit parameter will attempt to retrieve all matching content, which can cause serious performance issues, timeouts, and excessive memory consumption (on both the server and client).
Optimizely Graph enforces limits, but relying on default behaviours is risky. In production environments with thousands of content items, an unlimited query can easily timeout or overwhelm your application's memory, particularly on mobile devices or lower-powered clients.
2. Request unnecessary fields "just in case" — this increases payload and processing time
Avoid the temptation to request fields that you don't immediately need in your frontend. Queries that fetch every available field—or include extra fields for testing, debugging, or potential future use—significantly increase payload size, processing time, and network transfer costs.
GraphQL's primary advantage is its ability to request exactly the data you need, nothing more. When you query for 20 fields but only render 5, you're wasting resources on both the server (which must retrieve and serialize the extra data) and the client (which must parse and store it). This is particularly problematic on mobile devices or slower connections where bandwidth and processing power are limited.
It's common during development to fetch additional fields for verification or debugging purposes. However, these extra fields often remain in production queries long after they're no longer needed, quietly degrading performance.
Regularly audit your queries as part of code reviews or performance optimization sessions. Lean queries result in faster response times, reduced bandwidth usage, and better overall application performance.
Summary
Effective use of Optimizely Graph starts with thoughtful content modelling and efficient querying practices. By designing reusable, well-structured content types with consistent naming conventions and appropriate indexing modes, you create a foundation that scales across multiple channels and applications. Similarly, crafting specific, shallow queries with proper pagination and server-side filtering ensures optimal performance and maintainability.
The practices outlined in this post - from avoiding deeply nested queries to leveraging GraphQL variables for flexibility - will help you build faster, more reliable Optimizely Graph implementations. Remember that while these guidelines provide a strong framework, every project has unique requirements, so testing and measuring performance in your specific context remains essential.
In Part 2, we'll dive into the operational aspects of running Optimizely Graph in Production, including performance optimisation techniques, security considerations, API key management strategies, integration patterns, and governance approaches. These additional practices will help ensure your Graph implementation remains secure, performant, and maintainable over time.
Stay tuned for Part 2, which will be published in the coming weeks.
Related Links
- https://docs.developers.optimizely.com/platform-optimizely/docs/introduction-optimizely-graph
- https://docs.developers.optimizely.com/platform-optimizely/docs/cursor
- https://www.oshyn.com/blog/optimizely-graph
- https://world.optimizely.com/blogs/nguyen-nguyen/dates/2024/3/exclude-cms-content-properties-from-being-indexed-in-optimizely-graph/
Comments