# Get content by IDs, or handles
Source: https://docs.enterspeed.com/api-reference/delivery/get-content-by-ids-or-handles
/enterspeed/enterspeedv1Api.yaml post /v2
The Post version of the Delivery API is used to fetch many view by IDs or handles.
*Note: The maximum handles and ids combined in one request is limited to 1000*
# Get content by URL, IDs, or handles
Source: https://docs.enterspeed.com/api-reference/delivery/get-content-by-url-ids-or-handles
/enterspeed/enterspeedv1Api.yaml get /v2
Used to fetch views by a single URL and/or multiple IDs or handles in a single request.
*Note: The maximum url length is 4096 characters*
# Delete entities
Source: https://docs.enterspeed.com/api-reference/ingest/delete-entities
/enterspeed/enterspeedv1Api.yaml delete /ingest/v2
Starting the process for deleting entities.
*Note: The bulk endpoint can take up to 50 entities in a single request.*
# Delete entity
Source: https://docs.enterspeed.com/api-reference/ingest/delete-entity
/enterspeed/enterspeedv1Api.yaml delete /ingest/v2/{originId}
Starting the process for deleting the entity.
# Save entities
Source: https://docs.enterspeed.com/api-reference/ingest/save-entities
/enterspeed/enterspeedv1Api.yaml post /ingest/v2
Starting the process for saving entities.
*Note: The bulk endpoint can take up to 50 entities in a single request.*
*Note: The total size of the request body must not exceed 200 MB.*
# Save entity
Source: https://docs.enterspeed.com/api-reference/ingest/save-entity
/enterspeed/enterspeedv1Api.yaml post /ingest/v2/{originId}
Starting the process for saving the entity.
This endpoint can be used in two ways:
- **Raw**: The body contains the raw source entity and the required meta data is provided as header parameters.
- **Simplified**: The source entity is given `properties` of the body which also includes meta data.
# Multi query items
Source: https://docs.enterspeed.com/api-reference/query/multi-query-items
/enterspeed/enterspeedv1Api.yaml post /v1
The Query API is used to query items in an index, with this endpoint up to 5 queries can be requested in a single payload, each query defining the index to perform the lookup in.
# Multi query items from Auto Indexing source groups
Source: https://docs.enterspeed.com/api-reference/query/multi-query-items-from-auto-indexing-source-groups
/enterspeed/enterspeedv1Api.yaml post /v1/source
Note: This endpoint is currently in preview
The Query API is used to query items in an index, this endpoint lets you query items ingested to Auto Indexing source groups with up to 5 queries requested in a single payload, each query defining the source groups alias and source entity type to perform the lookup in.
# Query items
Source: https://docs.enterspeed.com/api-reference/query/query-items
/enterspeed/enterspeedv1Api.yaml post /v1/{indexAlias}
The Query API is used to query items in an index.
# Query items from Auto Indexing source groups
Source: https://docs.enterspeed.com/api-reference/query/query-items-from-auto-indexing-source-groups
/enterspeed/enterspeedv1Api.yaml post /v1/sourceGroupAlias/{sourceGroupAlias}/type/{sourceEntityType}
Note: This endpoint is currently in preview
The Query API is used to query items in an index, this endpoint lets you query items ingested to Auto Indexing source groups.
# Get Routes
Source: https://docs.enterspeed.com/api-reference/routes/get-routes
/enterspeed/enterspeedv1Api.yaml get /routes/v1
Get all the routes for a specific environment.
# Get Routes v2
Source: https://docs.enterspeed.com/api-reference/routes/get-routes-v2
/enterspeed/enterspeedv1Api.yaml get /routes/v2
Get all the routes for a specific environment with continuation token.
# Enterspeed API
Source: https://docs.enterspeed.com/enterspeed/api/overview
The Enterspeed API are based on REST principles.
## OpenAPI specification
Download the OpenAPI specification for the Enterspeed API.
## API keys
The API uses two types of API keys. One for reading data and one for writing data.
* **Reading** - The environment client API key is used for reading data from the Delivery API, Query API and Routes API. The key is sent in the request header as `X-Api-Key`.
* **Writing** - The data source API key is used for writing data to the Ingest API. The key is sent in the request header as `X-Api-Key`.
# Indexes
Source: https://docs.enterspeed.com/enterspeed/best-practices/indexes
### Only index the fields you need
Basicly, the more fields you index, the more space the index will take and the longer it takes to index all the items. It also impacts the performance when you query an index with a lot of fields.
In general, this means that you should index as few fields as possible.
As a rule of thumb you should not index more than 50 fields.
You can use the schema [`alias` query feature](/api-reference/query/query-items) to return views for presentation data as part of your query result so you only index the fields you need for filtering and sorting.
### Prefer `keyword` over `text` (unless you need the power of the `text` type)
From the name of the types, it's easy to think that every time you need to index a string, you should use the `text` type.
However we also have the `keyword` type that also works on strings and it's important to know the difference.
#### text
When using the `text` type, the string value is analyzed when an item is inserted into the index. This means it takes a bit longer to index and query the field, but it brings a lot of power as well.
Analyzed means that the string value is split into tokens and stored in the index. This makes it possible to do fuzzy search using the `search` property in the [Query API](/api-reference/query/query-items).
#### keyword
When using the `keyword` type, the string value is not analyzed but indexed as is. This makes it faster and suitable when you just need to filter, sort, build facets or use the field for display on the frontend.
# Ingest integrations
Source: https://docs.enterspeed.com/enterspeed/best-practices/ingest-integrations
### One source group per system
Before you can ingest data (source entities) into Enterspeed, you need to create a source group to store the data. In general, you want to create a source group for every source system that you're ingesting data from. So basically, that's one source group for your CMS data, one source group for you PIM data, one source group for you ERP data, and so on.
Within each source group you can then have multiple sources, typically one per environment, or two per environment if you have both published and preview data which is often used in CMS's.
Names for source groups and sources could be something like:
* CMS
* \[PROD] Preview
* \[PROD] Published
* \[DEV] Preview
* \[DEV] Published
* PIM
* \[PROD] Data
* \[DEV] Data
### Only ingest your source entities when fully updated
When you ingest source entities, it's important that you don't do multiple "partial" ingests, such as ingesting a single source entity every time a single field is updated in the source system.\
This could be if your source system auto saves and sends a save event everytime an editor changes a single field, instead of when the editor is done updating all necessary fields and clicks save.
Doing multiple ingests for a single source entity, one for every single property change, will create multiple unnecessary view generations. And that can result in larger job queues and delay the final view generation.
Instead, make sure to only ingest source entities one time once they're fully updated.
### Exclude unused properties that change often
In general, you can ingest all your properties for a source entity type to Enterspeed - and then only map the properties you actually need in the schemas. Enterspeed will even detect if you're ingesting a source entity with no changes since the last ingested version of the source entity, and will not start processing a new view.
However, if some of your properties change often and you don't use these properties, you shouldn't ingest them. If you do it'll generate new views on every ingest even though the views aren't changing. This could be if you have a stock count property that change every time a customer buys a product - but you either aren't using that stock count or perhaps only use it to return a boolean such as "true" if the stock count is larger than zero or "false" if it's zero. If that's the case, Enterspeed will regenerate the view every time the stock count changes from 100 to 99 to 98 to 97 and so on with the same result.
In this case you should ingest the boolean value instead of the stock count.
You can use the Management App's log function and filter by Ingest API to see exactly how many ingest requests you're doing.
### Don't delete all source entities on every import run
When running import jobs, you should not start by deleting all source entities and then reingesting them again.
First off, it means that you'll delete all your views and they'll stay missing until all source entities has been reingested and all views has been processed again.
Secondly, Enterspeed has a feature to only reprocess views if the source entities have changed. So by deleting all source entities and reingesting them, this feature of course doesn't work - and that can cause an extensive amount of extra jobs and view generations.
If you don't get delete events from your backend system so you can delete source entities right away, you should keep a list of previous ingested source entities, so you can compare which source entities you need to delete in the next run.
### Don't duplicate information on multiple source entity types
When you ingest data to Enterspeed, you should avoid ducplicated information across source entity types. E.g. if you have a product source entity and a price source entity, the sales price should not be ingested as property on both of them.
If you have duplicated data, you need to ingest both source entities every time the sales price changes and then Enterspeed needs to process extra views.
Enterspeed has features like [references](/enterspeed/reference/js/full-schema/properties#reference) and [lookup](/enterspeed/reference/js/full-schema/properties#lookup) that lets you use data from other source entities in the schemas, so use those features instead.
# Overview
Source: https://docs.enterspeed.com/enterspeed/best-practices/overview
# Best Practices
On these pages we have collected some best practices to help you get the best result and performance out of Enterspeed.
Please read the pages as most of the points are applicable to every Enterspeed project - and as always, you are more than welcome to reach out if you have any questions.
Ingesting data from your own sources, like CMS, PIM, ERP and so on, into Enterspeed
Creating and updating views and routes for your frontend application
Optimizing indexes used by the Query API
# Schemas
Source: https://docs.enterspeed.com/enterspeed/best-practices/schemas
### Make your reprocess actions as specific as possible
Reprocess actions can cause execessive processing, especially if you reprocess more than needed. This could result in a larger queue of jobs, and it will take longer for all your views to be updated.
Because of that, it's important to make your reprocess actions as precise as possible, by using `originId` or a precise `filter` so you only target the schemas and source entities you actually need to reprocess.
Read more about [reprocessing](/enterspeed/key-concepts/reprocessing.md).
### Exclude properties from lookup if you don't need them
By default when you do a [lookup](/enterspeed/reference/js/full-schema/properties#lookup) in a schema, the entire source entities are loaded from storage, including the `properties` object.
```json title="Full source entity including all custom properties" theme={null}
{
"sourceId": "gid://Source/e3211d99-e496-4e11-af3d-328eb543619f",
"id": "gid://Source/e3211d99-e496-4e11-af3d-328eb543619f/Entity/1098-en-us",
"type": "home",
"originId": "1098-en-us",
"originParentId": null,
"url": "https://fairy-tales.com/",
"updatedAt": "2023-07-18T08:20:41.8162846Z",
"redirects": [],
"properties": {
"title": "Fairy tales by H. C. Andersen",
"text": "Read the wonderful fairy tales from the danish author and poet Hans Christian Andersen. Classic tales like \"The Emperor's New Clothes\", \"The Snow Queen\" and many more.",
...
}
}
```
But in some cases you don't need all the custom properties, only some of the base fields like `originId`, `originParentId`, or `url`.
In these cases it's important to specify that in the `lookup`, as this will improve performance and lower the amount of time it takes to generate views.
You do that by setting `excludeProperties` to `true` in the `lookupOptions` parameter.
```js title="lookup with excluded custom properties" theme={null}
properties: async function (sourceEntity, context) {
const fairyTales = await context
.lookup("type eq 'fairyTale'", { excludeProperties: true })
.limit(3)
.toPromise();
return {
fairyTales: fairyTales
.map((fairyTale) => ({
// fairyTale.properties is not available
id: fairyTale.originId,
url: fairyTale.url,
}));
}
}
```
# 3 - Delivering data
Source: https://docs.enterspeed.com/enterspeed/deliver
Your data has been ingested into Enterspeed and transformed using our schema definitions, now it's time for the final step - delivering the data.
First, you need to set up an **Environment client** in Enterspeed.
Go to *Settings* --> *Environment settings* --> *Environment clients* and create one. This will generate an API key to use when getting data delivered.
## How to get data delivered
There are currently two ways of getting data delivered from Enterspeed:
1. Using our Delivery .NET SDK
2. Using our API
### Getting data delivered via our Delivery .NET SDK
You can find more information about it here: [https://github.com/enterspeedhq/enterspeed-sdk-delivery-dotnet](https://github.com/enterspeedhq/enterspeed-sdk-delivery-dotnet)
### Getting data delivered via our API
You can of course use our API directly to get your data delivered. [You can find the API documentation right here](/api-reference).
Want to see an example? See how we [fetched Enterspeed-data in Next.js](/enterspeed/tutorials/umbraco-nextjs/4-fetching-data-in-nextjs)
# Adding data sources
Source: https://docs.enterspeed.com/enterspeed/getting-started/data-sources
Data sources are where a connection is created to your data source. This can be a CMS, a PIM-system, or perhaps a development instance of your CMS.
Data sources are always part of a source group. A source group can have multiple data sources attached and each data source have its own unique API key.
The reason data sources are wrapped in source groups is to make it easier to reference in schemas.
That way you only have to make a single reference, even though you may have multiple sources spread across environments (e.g. *Development*, *Staging*, *Production*).
A data source is connected to one or more environments. You can have multiple data sources in the same source group, but these can't share environments.
On each data source, you can see how many source entities have been ingested. A source entity is a single "instance" from your data source, e.g. a page, an article, a product, etc.
You can delete all these entities by clicking on the three dots next to the data source and clicking *Delete all entities*.
Deleting entities, deleting a source group or deleting a data source will permanently delete the data and can't be undone.
## Creating a data source
Click the *Settings*-tab and then click the *Data sources*-tab in the sidemenu. Next, click the *Create group*-button in the right corner.
Give your source group a name and select a type (*the type is only used to better help you differentiate your source groups*).
Next, give your data source a name, select one or more environments and then click the *Add*-button. Once you have added your data sources, click the *Create*-button.
Your source group containing your data source(s) is now created. Each of your data sources will have its own unique API key you can use to ingest data into Enterspeed.
# Setting up domains
Source: https://docs.enterspeed.com/enterspeed/getting-started/domains
Before you can start using Enterspeed, you need to add at least one domain and at least one accompanying hostname.
The domain is merely used as a way of grouping your hostnames, meaning you can call it whatever you wish. We of course recommend calling it something similar to your hostname, so you can keep track of it.
A **domain** in Enterspeed is a collection of **hostnames**. You should always have at least one hostname per domain.
If you for instance had a site called `Tacomania.com`, the domain could be `Tacomania.com` and the hostname(s) could be:
* `tacomania.com`
* `blog.tacomania.com`
* `shop.tacomania.com`
If you're just testing something out, you can add `root.tld` as your hostname - this will work as a wildcard hostname. We don't recommend using this in production.
The domain(s) will be attached to your environment client, which is the "connection" between Enterspeed and your front-end. You will learn more about [environments](/enterspeed/getting-started/environments) and [environment clients](/enterspeed/getting-started/environment-clients) later on.
By linking the domain(s) to your environment client(s), you are able to filter the data based on hostname and only fetch the data which matches the provided hostname(s).
## Why the need for domains?
Domains are used as a way to ensure you always fetch the correct data.
Say you have ingested data from a multisite setup into Enterspeed. Now you have a lot of source entities (*you will learn more about these in the [Data sources](/enterspeed/getting-started/data-sources) section*), which belong to separate "sub-sites". How do you fetch the correct data?
You do this by using domains.
Domains in Enterspeed are linked to Environment Clients. Each domain can have multiple hostnames under it.
When fetching data from your Environment client, only data from this specific domain will be fetched.
## Adding domains and hostnames
If you're using one of our Umbraco [integrations](/enterspeed/integrations), the domain and hostname will automatically be created based on what you have entered under *Culture and Hostnames* in Umbraco.
Click the *Settings*-tab and then click the *Environment settings*-tab in the sidemenu. Scroll down to Domains and click the *Create*-button.
The domain name is only used as visual help. Data will be filtered using hostnames.
Give your domain a name and click *Create*.
Click on the three dots (settings icon) next to the domain name and select *Edit hostnames*.
Click on the *Create*-button in the top right corner. Enter your hostname, e.g. `tacomania.com`. Click *Create* and then click *Save*.
# Using environment clients
Source: https://docs.enterspeed.com/enterspeed/getting-started/environment-clients
Environment clients are the "connection" between Enterspeed and your front-end and, as the name suggests, it is also tied up to one of your environments.
You can only have one environment per environment client.
Moreover, environment clients also have domains attached to them, which makes it possible to filter your data based on the host names attached to the domain name.
However, unlike environments, you can have as many domains attached to your environment client as you wish, but you will need at least one.
An environment client won't work without a domain attached to it.
You can also choose to have a single environment client per domain if you have multiple sites, for instance for separation of concern.
## Creating an environment
Click the *Settings*-tab and then click the *Environment settings*-tab in the sidemenu. Scroll down to Domains and click the *Create*-button.
Give your Environment client a name and select an environment.
Next, select the domains you wish to add to the environment client and then click *Save changes*.
Once the environment client has been created an API key will be available. If needed the key can be regenerated by clicking the three dots next to the environment client (*Settings*) and selecting *Regenerate API Key*.
## API Scopes
API scopes allow you to control which Enterspeed APIs your environment client can access, following the principle of least privilege. This is particularly useful when integrating with AI agents or third-party services where you want to limit access to only the necessary endpoints.
### Default Scopes
When creating a new environment client, it automatically receives these default scopes:
* **Delivery API** - Access to content delivery endpoints
* **Query API** - Access schema-transformed and auto indexed data
* **Routes API** - Access to route management and execution
### Available Scopes
| Scope | Description | Requirements |
| -------------------------------- | ----------------------------------------------- | ------------------ |
| **Delivery API** | Content delivery endpoints | None |
| **Query API** | Access schema-transformed and auto indexed data | None |
| **Routes API** | Routes API access | None |
| **MCP Server (AI Agent Access)** | Enables MCP tool endpoints for AI agents | Requires Query API |
The MCP Server scope is designed for AI agents and does not provide data access by itself. It must be combined with the Query API scope to function properly.
### Scope Presets
For convenience, the Management App provides several preset configurations:
* **Standard** - Delivery + Routes + Query (default for regular applications)
* **AI Assistant** - Query + MCP Server — Full data access for AI agents
* **Custom** - Manually select specific scopes
### Managing Scopes
You can configure scopes when creating or updating an environment client through the Management App. Existing environment clients without explicit scopes will continue to work with the default scope configuration, ensuring backward compatibility.
**Using the Query MCP Server.** If you are setting up an AI agent, pick the **AI Assistant** preset above and see the [Query MCP Server documentation](/enterspeed/mcp-server/query-mcp/overview) for hostnames, authentication, and step-by-step client setup.
## Index-Level Scopes (Advanced)
Beyond controlling which APIs your environment client can access, you can also configure **index-level scopes** within the Query API to restrict access to specific data. This provides fine-grained control over exactly what content an AI agent or integration can access.
### Schema indices
When your environment client has Query API access, you can optionally restrict it to specific schema indices:
* **All indices (default)** - Access to all deployed index schemas
* **Specific indices** - Access only to selected schemas (e.g., `blogpost`, `product`)
* **Wildcard patterns** - Access to schemas matching patterns (e.g., `blog*` matches `blogpost`, `blogpage`, etc.)
**Example use cases:**
* AI agent for blog content: Restrict to `blogpost` and `blogpage` indices only
* Product recommendation system: Restrict to `product` and `category` indices only
* Content migration tool: Use `cms*` pattern for all CMS-related schemas
### Auto indices
You can also optionally restrict Query API access to specific auto-indexed source group and entity type combinations:
* **All sources (default)** - Access to all auto-indexed source entities
* **Specific combinations** - Access only to selected source group + entity type pairs (e.g., `cms:page`, `shop:product`)
* **Wildcard patterns** - Access to all entity types within a source group (e.g., `cms:*`)
**Format:** Index scopes use the pattern `sourceGroup:entityType`
**Example configurations:**
* CMS content only: `cms:page`, `cms:article`, `cms:blogpost`
* E-commerce data: `shop:product`, `shop:category`, `shop:inventory`
* All CMS content: `cms:*`
### Index Scope Behaviour
Index scopes are **optional** and only apply when the **Query API** component scope is enabled. If no index scopes are configured, the client has access to **all** data within its allowed component scopes.
This two-layer approach provides maximum flexibility:
1. **Component scopes** control which APIs can be accessed
2. **Index scopes** control which specific data within those APIs can be accessed
# Configuring environments
Source: https://docs.enterspeed.com/enterspeed/getting-started/environments
To use Enterspeed you need to have at least one environment. When creating a new tenant in Enterspeed, two environments are automatically created: `Development` and `Production`.
Environments play a huge role inside Enterspeed and are used in all parts of the [ITD-process](/enterspeed/getting-started/intro).
**Ingesting data**: When creating a [data source](/enterspeed/getting-started/data-sources), you connect this source to an environment. This is helpful so you don't mix your Production data with your Development data.
**Transforming data**: When deploying a [schema](/enterspeed/transform/intro) you choose which environment to wish to deploy to. Once deployed, views will be generated based on the data defined in the schema. These views will be available for the environment client with the matching environment connected.
**Delivering data**: When delivering data, an environment client is used to make the connection between Enterspeed and your front-end. An environment client is, as the name suggests, tied up on a specific environment. This makes it easy to test changes before deploying to the Production environment.
## Creating an environment
Enterspeed will as mentioned automatically create two environments for you. You can view them under *Environment settings* in the *Settings* section.
Here you can create new environments, edit the name of your environments or delete them. Beware of deleting environments, since this is an irreversible action that will remove all data attached to the environment.
Deleting an environment will cause all data for the environment to be deleted.
# Intro
Source: https://docs.enterspeed.com/enterspeed/getting-started/intro
## The ITD-process
To get started using Enterspeed, you need to go through a three-step process - the ITD-process:
1. **I**ngest data
2. **T**ransform data
3. **D**eliver data
### Ingesting data
In this step, we ingest the data from your current data source(s) into Enterspeed. You can do this by using one of our [integrations](/enterspeed/integrations) or by using our [API](/api).
***[Go to the Ingest data section.](/enterspeed/ingest)***
### Transforming data
Once the data have been ingested into Enterspeed, you can start transforming it.
You do this by using our Schema designer. Here you can combine data from multiple sources and select which data you want to be available to the front-end.
Once the data is transformed it gets stored in a high-performance Redis database that can be stored across multiple geographical regions.
***[Go to the Transforming data section.](/enterspeed/transform/intro)***
### Delivering data
The data is now available to fetch via the Enterspeed Delivery API.
Like working with any other APIs, it's extremely easy to integrate into your front-end project.
***[Go to the Delivering data section.](/enterspeed/deliver)***
## Configuring Enterspeed
However, before you can start the ITD-process, you need to do some configuration first.
We need to set up:
* Domains and hostnames
* Environment and environment clients
* Data sources
You will learn about this in the next sections.
# 1 - Ingesting data
Source: https://docs.enterspeed.com/enterspeed/ingest
The first step when working with Enterspeed is to get your current data sources into Enterspeed.
Go to Data sources under Settings and click the Create group button. This will open a modal that allows you to create a new source group. A source group can have multiple data sources attached, which each have its own unique API key.
## How to ingest data
There are several ways of doing this:
* Using one of our integrations
* Using our .NET SDK
* Using our API
* Using the Management App
### Ingesting data via an integration
Using one of our [integrations](/enterspeed/integrations/overview) is the easiest way to get data ingested. This integration takes care of calling the Enterspeed Ingest API when changes occur in your backend system.
### Ingesting data via our .NET SDK
Another way of ingesting data is by using our .NET SDK. You'll find more information about it here: [https://github.com/enterspeedhq/enterspeed-sdk-dotnet](https://github.com/enterspeedhq/enterspeed-sdk-dotnet)
### Ingesting data via our API
Last but now least, you can of course use our API directly to ingest your data. [You can find the API documentation right here.](/api-reference/ingest)
### Ingesting data via the Management App
You can also use the Enterspeed Management App to manually ingest data directly from the [Source entities page](https://app.enterspeed.com/source-entities). This is useful for testing or for one-time data imports.
## Viewing ingested data
Once your data has been ingested into Enterspeed, it will be visible under Source Entities.
You can click on the View button to see the data for the source entity.
# Algolia
Source: https://docs.enterspeed.com/enterspeed/integrations/algolia
The Enterspeed Algolia integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Algolia index. This means that you can decide on the schema level which views you want to send to Algolia.
You will only have to set the destination field on the entity schema you want to send to Algolia. All schema references are automatically resolved so you don't have to set it on all referenced schemas.
It's possible to configure multiple Algolia destinations if you need to push different types of data to different Algolia indexes.
## Configuration
In order to setup the Algolia configuration you need the following:
| Setting | Description |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Algolia Index Name | The name of the Algolia index you want to integrate to |
| Algolia Application ID | The unique application identifier used to identify you when working with Algolia's API |
| Algolia API Key | The API key needs `addObject` and `deleteObject` rights for the index you want to integrate to |
| Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Algolia |
## Options
Table of available options, that you can optionally specify, if needed for your use case.
| Setting | Description |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| objectId | By default Enterspeed uses view id as the value for object id in Algolia. You can override default object id by providing value for this option. |
| indexName | Option to override what index must be used for current view. By default `IndexName` of Algolia configuration is used. |
| partialUpdate | A boolean indicating if the update of the Algolia record should be a partial update. [See Algolia documentation](https://www.algolia.com/doc/api-reference/api-methods/partial-update-objects/).
Default value: `false` |
| createIfNotExists | A boolean indicating if the partial update should create a record in Algolia if it does not already exists. Only used if `partialUpdate` is set to `true`.
Default value: `false` |
| skipDelete | A boolean indicating if a deleted view should delete the record in Algolia. Typically set to `false` for `partialUpdate` if the schema is not working on the "master" object.
Default value: `false` |
Algolia does not support partial deletes. This means that if you are using partial updates and you want to clear the attributes from the partial update,
you should not delete the source entity that triggers the partial update in Enterspeed.
Instead you should update the source entity and set the properties to default values, like null, an empty string, an empty array and so on.
## Example of usage
```js title="Schema with Algolia destination" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('geodata', ['city']);
},
actions: function (sourceEntity, context) {
context.destination('algolia').options({
objectId: `city-${sourceEntity.originId}`
partialUpdate: false
});
},
properties: function ({url, properties: p}, context) {
return {
url: url,
name: p.name
}
}
}
```
```json title="Schema with Algolia destination" theme={null}
{
"triggers": {
"geodata": ["city"]
},
"destinations": [
{
"alias": "algolia",
"options": {
"objectId": "city-{originId}"
}
}
],
"properties": {
"url": "{url}",
"name": "{p.name}"
}
}
```
## Algolia specific properties
### \_geoloc
`_geoloc` is a special property in Algolia used for doing geo-searching. [See Algolia documentation](https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/)
As stated in the Algolia documentation, the `lat` and `lng` properties must be numeric values. This means that you will need to make sure that your Enterspeed schema is mapping these properties as numeric values and not as strings.
```js title="Schema with Algolia destination" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('geodata', ['city']);
},
actions: function (sourceEntity, context) {
context.destination('algolia').options({
objectId: `city-${sourceEntity.originId}`
});
},
properties: function ({url, properties: p}, context) {
return {
url: url,
name: p.name,
_geoloc: {
lat: p.lat,
lng: p.lng,
}
}
}
}
```
```json title="Schema with Algolia destination" theme={null}
{
"triggers": {
"geodata": ["city"]
},
"destinations": [
{
"alias": "algolia",
"options": {
"objectId": "city-{originId}"
}
}
],
"properties": {
"url": "{url}",
"name": "{p.name}",
"_geoloc": {
"type": "object",
"properties": {
"lat": {
"type": "number",
"value": "40.639751",
"precision": 6
},
"lng": {
"type": "number",
"value": "-73.778925",
"precision": 6
}
}
}
}
}
```
# Clerk.io
Source: https://docs.enterspeed.com/enterspeed/integrations/clerk
The Enterspeed Clerk.io integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Clerk.io store. This means that you can decide on the schema level which views you want to send to Clerk.io.
You will only have to set the destination field on the entity schema you want to send to Clerk.io. All schema references are automatically resolved so you don't have to set it on all referenced schemas.
It's possible to configure multiple Clerk.io destinations if you need to push different types of data to different Clerk.io stores.
As of now the Clerk.io integration only support pages. Contact us if you want to work with other types in Clerk.io.
## Configuration
In order to setup the Clerk.io configuration you need the following:
| Setting | Description |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Clerk Public Key | The unique public key of the Clerk.io store you want to integrate to |
| Clerk Private Key | A private key on the Clerk.io store you want to integrate to |
| Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Clerk.io |
## Options
Table of available options, that you can optionally specify, if needed for your use case.
| Setting | Description |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| id | By default Enterspeed uses view id as the value for object id in Clerk. You can override default object id by providing value for this option. |
| clerkEntityType | The type of entity you want to send to Clerk. Supported values: `page`. |
## Example of usage
```js title="Schema with Clerk destination" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['page']);
},
actions: function (sourceEntity, context) {
context.destination('clerk').options({
id: sourceEntity.originId,
relewiseEntityType: 'page'
});
},
properties: function ({url, properties: p}, context) {
return {
url: url,
title: p.title,
text: p.text
}
}
}
```
```json title="Schema with Clerk destination" theme={null}
{
"triggers": {
"cms": ["page"]
},
"destinations": [
{
"alias": "clerk",
"options": {
"id": "{originId}",
"relewiseEntityType": "page"
}
}
],
"properties": {
"url": "{url}",
"title": "{p.title}",
"text": "{p.text}"
}
}
```
## Clerk.io specific properties
Each type in Clerk.io (page, product, category, ...) has a set of required field which must be mapped in the Enterspeed schema.
See required fields here: [https://docs.clerk.io/reference/page-resource](https://docs.clerk.io/reference/page-resource)
`id` is set automatically or by the options id if you use that, so you don't need to map this property.
# Commercetools
Source: https://docs.enterspeed.com/enterspeed/integrations/commercetools
A ready to use integration service for connecting Commercetools as an Enterspeed data-source by importing products, variants and categories.
Included in the respository is a reference implementation based on Azure Functions, that may be deployed directly - or integrated into your own projects.
The included default implementation includes products, variants, availability, pricing and attributes, along with categories and their custom fields.
## Installation
With .NET CLI
```bash theme={null}
dotnet add package Enterspeed.Source.Commercetools --version
```
Using the Package Manager
```bash theme={null}
Install-Package Enterspeed.Source.Commercetools -Version
```
## Configuration
Check out the [Github repo](https://github.com/enterspeedhq/enterspeed-source-commercetools) for more information on how to configure the Commercetools integration.
# Elastic App Search
Source: https://docs.enterspeed.com/enterspeed/integrations/elastic-app-search
The Enterspeed Elastic App Search integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Elastic App Search engine. This means that you can decide on the schema level which views you want to send to Elastic App Search.
You will only have to set the destination field on the entity schema you want to send to Elastic App Search.
It's possible to configure multiple Elastic App Search destinations if you need to push different types of data to different Elastic App Search engines.
## Configuration
In order to setup the Elastic App Search configuration you need the following:
| Setting | Description |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Engine API Endpoint | The endpoint of Elastic App Search engine you want to integrate to |
| Private API Key | The Private API key used for calling Engine API endpoint |
| Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Enterspeed Elastic App |
## Options
Table of available options, that you can optionally specify, if needed for your use case.
| Setting | Description |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| documentId | By default Enterspeed uses view id as the value for document id in elastic app search. You can override default document id by providing value for this option. |
## Example of usage
```js title="Schema with elastic app search destination" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('geodata', ['city']);
},
actions: function (sourceEntity, context) {
context.destination('elastic-app-search').options({
documentId: `city-${sourceEntity.originId}`
});
},
properties: function ({url, properties: p}, context) {
return {
url: url,
name: p.city,
country: p.country,
population: parseInt(p.population),
location: `${p.lat},${p.lng}`,
photo: p.photo,
}
}
}
```
```json title="Schema with elastic app search destination" theme={null}
{
"triggers": {
"geodata": [
"city"
]
},
"destinations": [
{
"alias": "elastic-app-search",
"options": {
"documentId": "city-{originId}"
}
}
],
"properties": {
"url": "{url}",
"name": "{p.city}",
"country": "{p.country}",
"photo": "{p.photo}",
"location": "{p.lat},{p.lng}",
"population": {
"type": "number",
"value": "{p.population}"
}
}
}
```
## Elastic App Search specific properties
Useful resources about document limitations and other requirements:
* [https://www.elastic.co/guide/en/app-search/current/api-reference.html](https://www.elastic.co/guide/en/app-search/current/api-reference.html)
* [https://www.elastic.co/guide/en/app-search/current/documents.html#documents-create](https://www.elastic.co/guide/en/app-search/current/documents.html#documents-create)
# Elasticsearch
Source: https://docs.enterspeed.com/enterspeed/integrations/elasticsearch
The Enterspeed Elasticsearch integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Elasticsearch. This means that you can decide on the schema level which views you want to send to Elasticsearch cluster.
You will only have to set the destination field on the entity schema you want to send to Elasticsearch.
It's possible to configure multiple Elasticsearch destinations if you need to push different types of data to different Elasticsearch clusters or indexes.
## Configuration
In order to setup the Elasticsearch configuration you need the following:
| Setting | Description |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Elasticsearch endpoint | The endpoint of Elasticsearch cluster you want to integrate to |
| API Key | The API key used for calling Elasticsearch `_bulk` endpoint for creating, updating, deleting documents |
| Index name | Name of the default index to use when creating, updating, deleting documents in Elasticsearch. |
| Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Elasticsearch |
## Options
Table of available options, that you can optionally specify, if needed for your use case.
| Setting | Description |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| documentId | By default Enterspeed uses view id as the value for document id in elasticsearch. You can override default document id by providing value for this option. |
| indexName | Option to override what index must be used for current view. By default `IndexName` of Elasticsearch configuration is used. |
## Example of usage
```js title="Schema with elasticsearch destination" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('geodata', ['city']);
},
actions: function (sourceEntity, context) {
context.destination('elasticsearch').options({
documentId: `city-${sourceEntity.originId}`,
indexName: 'cities'
});
},
properties: function ({url, properties: p}, context) {
return {
url: url,
name: p.city,
country: p.country,
population: parseInt(p.population),
location: `${p.lat},${p.lng}`,
photo: p.photo,
}
}
}
```
```json title="Schema with elasticsearch destination" theme={null}
{
"triggers": {
"geodata": [
"city"
]
},
"destinations": [
{
"alias": "elasticsearch",
"options": {
"documentId": "city-{originId}",
"indexName": "cities"
}
}
],
"properties": {
"url": "{url}",
"name": "{p.city}",
"country": "{p.country}",
"photo": "{p.photo}",
"location": "{p.lat},{p.lng}",
"population": {
"type": "number",
"value": "{p.population}"
}
}
}
```
# Enterspeed Integrations
Source: https://docs.enterspeed.com/enterspeed/integrations/overview
Enterspeed has a growing list of integrations to various systems. The integrations come in two categories.
**Source** integrations are used to push data from a source system into Enterspeed.
**Destination** integrations are used to push processed data from Enterspeed to external systems.
## Integrations
## Integrations currently in beta
Integrations we currently have in beta. Please reach out to us if you would like to use any of these integrations in production.
## Integrations on the roadmap
We are currently looking into the following integrations. If you are missing an integration to a specific system or is interested in one of the integrations on the roadmap, please reach out to us as we would love to look into it.
# Relewise
Source: https://docs.enterspeed.com/enterspeed/integrations/relewise
The Enterspeed Relewise integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Relewise account. This means that you can decide on schema level which views you want to send to Relewise.
You will only have to set the destination field on the entity schema you want to send to Relewise. All schema references are automatically resolved so you don't have to set it on all referenced schemas.
It's possible to configure multiple Relewise destinations if you need to push different types of data to different Relewise accounts.
## Configuration
In order to setup the Relewise configuration you need the following:
| Setting | Description |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Relewise Dataset Id | The unique Dataset Id for the Relewise account you want to integrate to |
| Relewise Server URL | The Server URL for the Relewise account you want to integrate to |
| Relewise API Key | A Relewise API key with update and administrative action permissions for the types you want to integrate (product, content, brand, ...) |
| Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Relewise |
## Options
Table of available options, that you can optionally specify, if needed for your use case.
| Setting | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | By default Enterspeed uses view id as the value for object id in Relewise. You can override default object id by providing value for this option. |
| relewiseEntityType | The type of entity you want to send to Relewise. Supported values: `product`, `productCategory`, `content`, `contentCategory` and `brand`. |
## IntelliSense
In order to send data to Relewise (products, content, ...) the structure of the object you are mapping in the `properties` function must match with the corresponding Relewise model. See [Relewise API in Swagger](https://docs.relewise.com/swagger/index.html).
To help you bulding the right model, Enterspeed can provide you with IntelliSense. Simply just change the type in the top of the schema from `/** @type {Enterspeed.FullSchema} */` to one of the following:
* `/** @type {Enterspeed.Destinations.Relewise.Schemas.Product} */`
* `/** @type {Enterspeed.Destinations.Relewise.Schemas.Content} */`
* `/** @type {Enterspeed.Destinations.Relewise.Schemas.Brand} */`
* `/** @type {Enterspeed.Destinations.Relewise.Schemas.ProductCategory} */`
* `/** @type {Enterspeed.Destinations.Relewise.Schemas.ContentCategory} */`
## Example of usage
```js title="Product schema with Relewise destination" theme={null}
/** @type {Enterspeed.Destinations.Relewise.Schemas.Product} */
export default {
triggers: function(context) {
context.triggers('pim', ['product']);
},
actions: function (sourceEntity, context) {
context.destination('relewise').options({
id: sourceEntity.originId,
relewiseEntityType: 'product'
});
},
properties: function ({properties: p}, context) {
return {
product: {
displayName: {
values: [
{
language: { value: "en-gb" },
text: p.productName
}
]
},
salesPrice: {
values: [
{
amount: p.salesPrice,
currency: { value: "Euro" }
}
]
}
},
variants: context.reference('variantSchema').children()
}
}
}
```
```json title="Product schema with Relewise destination" theme={null}
We recommend using JavaScript schemas when working with Relewise destination
as it provides IntelliSense to map the complex models.
```
```js title="Content schema with Relewise destination" theme={null}
/** @type {Enterspeed.Destinations.Relewise.Schemas.Content} */
export default {
triggers: function(context) {
context.triggers('cms', ['contentPage']);
},
actions: function (sourceEntity, context) {
context.destination('relewise').options({
id: sourceEntity.originId,
relewiseEntityType: 'content'
});
},
properties: function ({properties: p}, context) {
return {
displayName: {
values: [
{
language: { value: "en-gb" },
text: p.title
}
]
},
data: {
contentData: {
type: "String",
value: p.text
}
}
}
}
}
```
```json title="Content schema with Relewise destination" theme={null}
We recommend using JavaScript schemas when working with Relewise destination
as it provides IntelliSense to map the complex models.
```
## What's supported
The integration supports the following types: `product` (with variants), `productCategory`, `content`, `contentCategory` and `brand` and the type needs to be defined in the `relewiseEntityType` property in the destination options together with a value for the id you want in Relewise.
See more details on the Relewise documentation: [https://docs.relewise.com/docs/developer/implementation-steps.html#\_1-provide-entities](https://docs.relewise.com/docs/developer/implementation-steps.html#_1-provide-entities)
All updates are done with `UpdateKind.ClearAndReplace` and all administrative actions are done with `UpdateKind.Disable`
# Azure Service Bus
Source: https://docs.enterspeed.com/enterspeed/integrations/servicebus
The Enterspeed Azure Service Bus integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Azure Service Bus queue or topic. This means that you can decide on schema level which views you want to send to the service bus.
You will only have to set the destination field on the entity schema you want to send to the Azure Service Bus. All schema references are automatically resolved so you don't have to set it on all referenced schemas.
It's possible to configure multiple Azure Service Bus destinations if you need to push different types of data to different service bus queues or topic.
## Configuration
In order to setup the Azure Service Bus configuration you need the following:
| Setting | Description |
| ---------------- | ------------------------------------------------------- |
| ConnectionString | The connection string to the Azure Service Bus |
| QueueOrTopicName | The name of the queue or topic in the Azure Service Bus |
## Message
The message will send the following data.
```json theme={null}
{
"id": "gid://Environment/2052b78d-6c34-4f11-bea5-296cf2d26968/Source/053b598b-c3d1-46fb-91e3-53115169cdb2/Entity/1234/View/product", // the Enterspeed view id
"originId": "1234", // the origin id of the entity
"type": "product", // the type of the entity
"action": "Deploy", // can have the value of Deploy or Remove
"url": "https://weu.delivery.enterspeed.com/v2?id=gid://Environment/40bb2d76-3b71-4121-b9b6-238cf4f325c4/Source/9e78f134-cf84-4ec5-9180-0b72b94949be/Entity/1099-en-us/View/home" // the absolute url for the delivery api to fetch the view
}
```
## Example of usage
```js title="Content schema with Azure Service Bus destination" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['content']);
},
actions: function (sourceEntity, context) {
context.destination('service-bus');
},
properties: function ({properties: p, url}, context) {
return {
product: {
url: url,
title: p.title,
content: p.text
}
}
}
```
```json title="Content schema with Azure Service Bus destination" theme={null}
{
"triggers": {
"cms": ["content"]
},
"destinations": [
{
"alias": "service-bus"
}
],
"properties": {
"url": "{url}",
"title": "{p.title}",
"content": "{p.text}"
}
}
```
# Shopify
Source: https://docs.enterspeed.com/enterspeed/integrations/shopify
The Shopify Destination is currently in preview, reach out if you want to get started.
The Enterspeed Shopify integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Shopify shop. This means that you can decide on schema level which views you want to send to Shopify.
You will only have to set the destination field on the entity schema you want to send to Shopify. All schema references are automatically resolved so you don't have to set it on all referenced schemas.
It's possible to configure multiple Shopify destinations if you need to push data to different Shopify shops.
The jobs are executed in bulk requests to avoid hitting API rate limits in Shopify. This means a new bulk job of up to 1.000 jobs is started every minute unless another bulk job on the same shop connection is still active. If another bulk job is active, the integration will wait another minute and try again.
The model you map in the Enterspeed schema looks to some degree like Shopifys product model, however what you map is a unified model containing both product, variant and translation data for a product making it easy to map out the full product in one go.
## Requirements
It's required to have a meta field called `enterspeed.customId` of type `ID` on the object types the destination is used for. This field must have the `Filter on the product list and in the Admin API` enabled and is used as an identifier when updating or deleting objects in Shopify through the destination.
## Configuration
In order to setup the Shopify configuration you need the following:
| Setting | Description |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Shopify store name | The unique store name in front of .myshopify.com. E.g. \[my store name].myshopify.com |
| Shopify access token | A Developer app API access token with the following permissions: `write_products`, `read_products`, `write_publications`, `read_publications`, `write_locales`, `read_locales`, `write_translations`, `read_translations` |
| Shopify API secret key | A Developer app API secret key |
| Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Shopify |
## Options
Table of available options, that you can optionally specify, if needed for your use case.
| Setting | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| customId | Set your own id of the product, e.g. a SKU number or internal id. |
| shopifyEntityType | The type of entity you want to send to Shopify. Supported values: `product`. |
| statusForNewProducts | The default status for newly created products. Supported values: `Active`, `Draft`, `Archived`, `Unlisted`. If the `status` property is set map directly on the product, this value takes priority over this setting. |
| setOnlyHandleForNewProducts | If the value is `true` the handle value will not be updated when the product is updated, it will on ly be set when the product is created the first time. Default value is `false`. Supported values: `true`, `false`. |
When products are deleted in Enterspeed the Shopify Destination will archive the products i Shopify and not delete them.
## IntelliSense
In order to send data to Shopify the structure of the object you are mapping in the `properties` function must match with the corresponding Shopify model.
To help you bulding the right model, Enterspeed can provide you with IntelliSense. Simply just change the type in the top of the schema from `/** @type {Enterspeed.FullSchema} */` to one of the following:
* `/** @type {Enterspeed.Destinations.Shopify.Schemas.Product} */`
## Example of usage
```js title="Product schema with Shopify destination" theme={null}
/** @type {Enterspeed.Destinations.Shopify.Schemas.Product} */
export default {
triggers: function(context) {
context.triggers('pim', ['product']);
},
actions: function (sourceEntity, context) {
context.destination('shopify').options({
customId: sourceEntity.originId,
shopifyEntityType: 'Product'
});
},
properties: function ({properties: p}, context) {
return {
handle: `${p.name}-${p.itemNumber}`,
title: p.name,
seoTitle: p.name,
type: 'electronic',
tags: ['tag1', 'tag2'],
templateSuffix: p.template
}
}
}
```
```json title="Product schema with Shopify destination" theme={null}
We recommend using JavaScript schemas when working with Shopify destination
as it provides IntelliSense to map the complex models.
```
## What's supported
The integration curerntly only supports the following types: `product` (with variants, prices, translations and so on). The type needs to be defined in the `shopifyEntityType` property in the destination options.
### Meta field types
Meta fields are created as unstructured fields without a definition - see [Shopify documentation](https://shopify.dev/docs/apps/build/custom-data#unstructured-metafields).
You can change the meta fields to structured fields either before or after products are ingested by creating a definition. Note the definition type must match what ever type is mapped in the Enterspeed schema.
Keep in mind that changeing the type in the Enterspeed schema will not change the meta field definition type and the import will fail.
# Field Value Converter
Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/enterspeed-value-converter
# Enterspeed Field Value Converter
A field value converter is a class that will convert the input value from Sitecore into an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties). To implement your own converter you need to implement the IEnterspeedPropertyValueConverter interface
## IEnterspeedFieldValueConverter
This interface contains two methods that need to be implemented
### IsConverter
```csharp theme={null}
bool CanConvert(Field field);
```
This method is called when the EnterspeedPropertyService tries to find the proper converter for this property.
An implementation of this method could look like this:
```csharp theme={null}
public bool CanConvert(Field field)
{
return field != null && field.TypeKey.Equals("number", StringComparison.OrdinalIgnoreCase);
}
```
### Convert
```csharp theme={null}
IEnterspeedProperty Convert(Item item, Field field, EnterspeedSiteInfo siteInfo, List fieldValueConverters, EnterspeedSitecoreConfiguration configuration);
```
This is the method that is converting the Sitecore field to an IEnterspeedProperty.
An implementation of this method could look like this:
```csharp theme={null}
public IEnterspeedProperty Convert(Item item, Field field, EnterspeedSiteInfo siteInfo, List fieldValueConverters, EnterspeedSitecoreConfiguration configuration)
{
if (string.IsNullOrEmpty(field.Value))
{
return null;
}
var value = 0d;
if (field.Value.Contains("."))
{
double.TryParse(field.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
}
else if (field.Value.Contains(","))
{
double.TryParse(field.Value, NumberStyles.Any, new CultureInfo("da-DK"), out value);
}
return new NumberEnterspeedProperty(_fieldService.GetFieldName(field), value);
}
```
## Registering a converter
Converters are registered in a service configurator. You can roll your own service configurator, and register your own converters in this.
Example:
```csharp theme={null}
public class ServicesConfigurator : IServicesConfigurator
{
public void Configure(IServiceCollection services)
{
services.AddSingleton();
}
}
```
Note if you want to override a default converter, you would have to register your own configurator, after the default Enterspeed configurator. This can be done with a patch config file.
Here is an example of the default configurator being set up in a config file.
```xml theme={null}
```
## Default converters
Enterspeed ships with default property value converters for all the built-in fields that Sitecore ships with out of the box.
[Sitecore 8 default property value converters](https://github.com/enterspeedhq/enterspeed-source-sitecore-cms/tree/master/src/Enterspeed.Source.SitecoreCms.V8/Services/DataProperties/DefaultFieldConverters)\
[Sitecore 9 default property value converters](https://github.com/enterspeedhq/enterspeed-source-sitecore-cms/tree/master/src/Enterspeed.Source.SitecoreCms.V9/Services/DataProperties/DefaultFieldConverters)
# Getting data
Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/getting-data
# Getting data from Sitecore to Enterspeed
When you press publish on a piece of content in Sitecore, Sitecore handles the content as normally: validation, trigger events, etc. The integration into Enterspeed is simply just a couple of events that the integration listens to.
## When content is being published
When a piece of content is being published in Sitecore, the integration reacts upon this and pushes it to Enterspeed, when the item has successfully been published.
It has then been sent to the Enterspeed Ingest API for Enterspeed to process it and deliver it to the Delivery API.
## SitecoreContentEntity explained
To make sure you understand the whole process from end to end, an important detail is how the SitecoreContentEntity is created, because these properties define the entity you work with, in Enterspeed.
The properties within the properties object don't change when it gets ingested in Enterspeed, this means that when you are to create your Schemas to model your API, you can rely 1:1 on what you ingest and what you can query in Enterspeed.
## Seeding content to Enterspeed
If you have an existing site, have installed Enterspeed later in the development process or just want to make sure all your content is in Enterspeed, you can seed or re-seed all the content from Sitecore into Enterspeed.
This can simply be done by going to the root item of the Site that you would like to push to Enterspeed, and doing a republish with the language, subitems and relations checked. This way we ensure that all data sources on the pages are sent to Enterspeed as well.
# Getting started
Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/getting-started
# Getting started with Sitecore & Enterspeed
The easiest way to get started with Sitecore and Enterspeed is using the pre-built Sitecore integration.
**GitHub: [Enterspeed Source Sitecore CMS](https://github.com/enterspeedhq/enterspeed-source-sitecore-cms)**
This integration takes care of calling the Enterspeed Ingest API when changes occur in Sitecore.
## Installation
**Prerequisite:** Sitecore 8 or 9
If your want to use Sitecore 10 with Enterspeed please reach out to us as we would love to look into it.
The fastest way to get up and running is to install the Enterspeed Sitecore integration with NuGet.
**NuGet:**\
[Enterspeed.Source.SitecoreCms.V8](https://www.nuget.org/packages/Enterspeed.Source.SitecoreCms.V8/)\
[Enterspeed.Source.SitecoreCms.V9](https://www.nuget.org/packages/Enterspeed.Source.SitecoreCms.V9/)
You can either install it manually from the NuGet manager in Visual Studio or execute one of the Install-Package command:
```bash theme={null}
Install-Package Enterspeed.Source.SitecoreCms.V8
```
```bash theme={null}
Install-Package Enterspeed.Source.SitecoreCms.V9
```
The NuGet package installs config files into this directory; verify that this folder contains config files.
```bash theme={null}
~\App_Config\Include\Enterspeed
```
## Configuration
Once installed, please navigate to `/sitecore/templates/System/Enterspeed` in the Master database, and publish the item, including all descendants.
These templates must exist in the Web database as a prerequisite for Enterspeed configuration items.
You will see that your Sitecore instance is loaded with a new item in `/Sitecore/system` called "Enterspeed Configuration".
You will have to create a Site configuration, for each Enterspeed configuration you would like to create.
In the Site configuration file, we have 7 fields
* API Base Url (required)
> This is the api url for Enterspeed. Unless you have gotten a specific Enterspeed endpoint to call, please use: [https://api.enterspeed.com](https://api.enterspeed.com)
* API Key (required)
> This is the Source API key. This API key can be found in the settings section of your tenant in [https://app.enterspeed.com/](https://app.enterspeed.com/) (Settings/Data sources)
* Enabled Sites
> In this field, you define the area of content covered by the site configuration. All items within this area are pushed to the source specified in this configuration. Selected items must share the same fullPath as the rootPath(s) configured in your site configuration within the Sitecore config files.
* Media Base Url
> Base url for media being pushed to Enterspeed. This would be typically be a url provided by your CDN for media and file hosting
* Site Base Url (required)
> Base Url for your site.
* Publish Hook Url
> You can call an external hook, when publish has finished. This could for example trigger a build in Netlify.
* Enable Preview
> With this checkbox you are defining where this configuration is for a preview site.
## First load after installing the Sitecore connector.
A table called EnterspeedJobs must be created so that jobs can be processed asynchronously. This table is created in the master database, meaning the SQL user specified in the connection string must have the necessary permissions to create tables (e.g., db owner).
Ensure that the master database user has temporary rights to create tables in your master database.
## Enterspeed Logs
If something unexpected occurs or the need for investigation arises, the connector creates an Enterspeed log, which collects any exceptions encountered during data ingestion.
This log can be found in your Sitecore logs folder.
The format of the file is:
`Enterspeed.log.{date}.{time}.txt`
# Processed entities
Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/processed-entities
### Content
Content items that are being sent to Enterspeed, will have references to the renderings inserted on them, along with information of the fields of the given datasources inserted on these renderings.
Each rendering reference sent to Enterspeed could have these properties:
* `name` - the name of this rendering
* `placeholder` - the Sitecore placeholder inserted on either the presentation details or the rendering itself
* `parameters` - an array of key/values inserted on the rendering options
* `datasource` - a reference to the inserted datasource item
### Dictionaries
Published dictionary items will be pushed to Enterspeed and exist as datasources of type dictionaryEntry.
### Renderings
Renderings are processed separately, as well, but only if the rendering is inserted on the presentation details of a content item that resides in an enabled site. This means that newly created renderings are not processed until they're inserted to be rendered on content for which you have enabled.
### Supported field types
* Single-Line Text
* Rich-Text
* Checkbox
* Date
* File
* Image
* Integer
* Multi-Line Text
* Number
* Checklist
* Droplist
* Grouped Droplink
* Grouped Droplist
* Multilist
* Name Value List
* Name Lookup Value List
* Treelist
* Droplink
* Droptree
* General Link
### Field names in Enterspeed
Field names on your content are sanitized when sent to Enterspeed. See below example:
* Content
* Title
* Text
* CTA Link
* Footer
* Contact Link
* Text
The above sections and fields will be sanitized like this - see below:
* `content_title`
* `content_text`
* `content_ctalink`
* `footer_contactlink`
* `footer_text`
# Services
Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/services
## EnterspeedPropertyService : IEnterspeedPropertyService
This service is used for converting a Sitecore field to an IEnterspeedProperty.
### Methods
```csharp theme={null}
IDictionary GetProperties (IPublishedContent content, string culture = null);
IDictionary ConvertProperties (IEnumerable properties, string culture = null;
```
Both methods will find the correct registered Enterspeed Property Value Converter and convert the value to an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties).
# SitecoreContentEntity
Source: https://docs.enterspeed.com/enterspeed/integrations/sitecore/sitecorecontententity
The SitecoreContentEntity is the concrete Sitecore-specific implementation of the [IEnterspeedEntity](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities).
## Implementation details
### Abstract
| Name | Type | Description |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Id | string | Unique identifier e.g. `e61ddc8e-90ad-4d31-bb00-024987a5f2d1` |
| Type | string | ContentType alias |
| Url | string | The current URL of the content, either relative or absolute |
| Redirects | string\[] | Array of redirects for the item |
| ParentId | string | Unique identifier of the parent e.g. `e61ddc8e-90ad-4d31-bb00-024987a5f2d1-en-us` |
| Properties | [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/blob/master/documentation/entities/properties/README.md)> | Dictionary of property alias and value is the converted Enterspeed property |
### Example
```json theme={null}
{
"id": "e61ddc8e-90ad-4d31-bb00-024987a5f2d1",
"type": "site",
"parentId": "e61ddc8e-90ad-4d31-bb00-024987a5f2d1-en-us",
"url": "https://example.com/about-us",
"redirects": ["/about"],
"properties": {
"includeInNavigation": {
"name": "includeInNavigation",
"type": "boolean",
"value": false
},
"title": {
"name": "title",
"type": "string",
"value": "Home"
},
"metaData": {}
}
}
```
### Meta data
To process Sitecore specific properties we have added a `metaData` object that contains:
| Name | Type | Description |
| ------------------ | --------------------------------------- | ------------------------------------------------------------------------ |
| name | string | Name of the item |
| displayName | string | DisplayName of the item |
| sitecoreId | string | Id of the Sitecore Item |
| language | string | e.g. `en-us` |
| sortOrder | number | What order the item is sorted in |
| level | number | What level in the tree the item is in |
| createDate | string | Date for when the item has been created |
| updateDate | string | Date for when the node has last been updated |
| updatedBy | string | Name of the user that has updated the item |
| fullPath | string\[] | Ancestors id's |
| languages | string\[] | Langauge versions available |
| isAccessRestricted | boolean | Value determining if the item is restricted for anonymous users/visitors |
| accessRestrictions | List of users and, and if they can read | |
```json theme={null}
{
"properties": {
"metaData": {
"name": "Name of the item",
"displayName": "displayName of the item",
"sitecoreId": "{0138AB78-5146-4A0C-82F5-DD2FB786C381}",
"language": "en",
"sortOrder": -463,
"level": 4,
"createDate": "2022-05-02T12:33:40",
"updateDate": "2022-05-09T13:03:58",
"updatedBy": "sitecore\\admin",
"fullPath": [
"984169440b16415e8a04d19ca522caed-en",
"a5fb76a88b9341c19d9f7ec0f04c654b-en",
"4228f830dbc64303ba22fbb5faae4af6-en",
"92a5b6dec6474ee4bce685475d10a650-en",
"0138ab7851464a0c82f5dd2fb786c381-en"
],
"languages": ["sv-SE", "en"],
"isAccessRestricted": false,
"accessRestrictions": []
}
}
}
```
# Typesense
Source: https://docs.enterspeed.com/enterspeed/integrations/typesense
The Enterspeed Typesense integration uses [destinations](/enterspeed/reference/js/full-schema/actions#destination) to send data from views directly to a configured Typesense cluster. This means that you can decide on schema level which views you want to send to Typesense.
You will only have to set the destination field on the entity schema you want to send to Typesenses. All schema references are automatically resolved so you don't have to set it on all referenced schemas.
It's possible to configure multiple Typesense destinations if you need to push different types of data to different Typesense clusters.
## Configuration
In order to setup the Typesense configuration you need the following:
| Setting | Description |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Typesense Server URL | The Server URL for the Typesense cluster you want to integrate to |
| Typesense API Key | A Typesense Admin API key |
| Enterspeed Environment Client API Key | The API key for an Enterspeed Environment client. This is used to fetch the view that will be inserted into Typesense |
## Schema options
On Enterspeed schema level you have the following options:
| Option | Required | Description |
| ----------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| collection | Required | The Typesense collection the data should be send to |
| id | Optional | The value that should be used as id in for the Typesense document. If omitted, the Enterspeed view id will be used |
| dirtyValues | Optional | Defines what Typesense should do when the type of a particular field being indexed does not match the Typesense schema.
Refer to the [Typesense documentation](https://typesense.org/docs/latest/api/documents.html#dealing-with-dirty-data) for more information |
## Example of usage
```js title="Content schema with Typesense destination" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['content']);
},
actions: function (sourceEntity, context) {
context.destination('typesense').options({
id: sourceEntity.originId,
collection: 'content'
dirtyValues: 'coerce_or_reject'
});
},
properties: function ({properties: p, url}, context) {
return {
product: {
url: url,
title: p.title,
content: p.text
}
}
}
```
```json title="Content schema with Typesense destination" theme={null}
{
"triggers": {
"cms": ["content"]
},
"destinations": [
{
"alias": "typesense",
"options": {
"id": "{originId}",
"collection": "content",
"dirtyValues": "coerce_or_reject"
}
}
],
"properties": {
"url": "{url}",
"title": "{p.title}",
"content": "{p.text}"
}
}
```
# Umbraco & Cloudinary
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco-cloudinary
This is a add-on to the [Umbraco source integration](./umbraco/getting-started).
This package will automatically upload the Umbraco media to Cloudinary and ingest the Cloudinary url to Enterspeed instead of the Umbraco url.
**GitHub: [Enterspeed Source Umbraco CMS Cloudinary](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms-cloudinary)**
## Installation
The fastest way to get up and running is to install the Enterspeed Umbraco Cloudinary integration with NuGet.
[NuGet: Enterspeed.Source.UmbracoCms.Cloudinary](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms.Cloudinary)
### Version support
| Enterspeed.Source.UmbracoCms.Cloudinary | Enterspeed.Source.UmbracoCms |
| --------------------------------------- | ---------------------------- |
| V1 | V2.1.0 ≤ x \< 4.0.0 |
| V2 | V4.0.0 ≤ x \< 4.3.0 |
| V3 | V4.3.0 ≤ x \< 5.0.0 |
| V4 | V5.0.0 > x |
## Configuration
The only configuration specific to this add-on is the Cloudinary environment credentials. These can be configured in the `appsettings.json` file.
```json theme={null}
"Enterspeed": {
...
"Cloudinary": {
"CloudName": "", // Required
"ApiKey": "", // Required
"ApiSecret": "", // Required
"AssetFolder": "" // Optional
}
}
```
# Additional properties
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/additional-properties
If you need to add additional properties to a content or media node before it's ingested into Enterspeed, you can use `EnterspeedPropertyDataMappers` or `EnterspeedPropertyMetaDataMappers`.
You can also create a [Custom Property Service](/enterspeed/integrations/umbraco/enterspeed-propertys-service#custom-property-service) and override some of the `MapAdditionalProperties` methods,
however mappers are more flexible if you are doing conditional mapping, like only adding additional properties for specific content types.
## Mappers
With mappers you can create small effective classes to map additional properties for types that matches a filter, e.g. all nodes of a specific type.
You can create mappers to add properties to the properties object or the meta-data object.
Note that we in this example are using `StringEnterspeedProperty`. You can choose between multiple property types.
(Array, boolean, number, object and so on.)
### Example
```csharp theme={null}
// Data mapper example
public class SlugDataMapper : IEnterspeedPropertyDataMapper
{
public bool IsMapper(IPublishedContent content)
=> content.ContentType.Alias == "contentPage";
public void MapAdditionalData(IDictionary data, IPublishedContent content, string culture)
{
// some logic
data["slug"] = new StringEnterspeedProperty("slug", "mySlugValue");
}
}
```
```csharp theme={null}
// Meta-data mapper example
public class SlugMetaDataMapper : IEnterspeedPropertyMetaDataMapper
{
public bool IsMapper(IPublishedContent content)
=> content.ContentType.Alias == "contentPage";
public void MapAdditionalData(IDictionary metaData, IPublishedContent content, string culture)
{
// some logic
metaData["slug"] = new StringEnterspeedProperty("slug", "mySlugValue");
}
}
```
### Registering your new property service
The mappers are registered in Umbraco via an [IComposer](https://docs.umbraco.com/umbraco-cms/reference/using-ioc).
**Umbraco 9+**
```csharp theme={null}
public class MyCustomComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedPropertyMetaDataMappers()
.Append();
}
}
```
**Umbraco 8**
```csharp theme={null}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class MyCustomComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.EnterspeedPropertyMetaDataMappers()
.Append();
}
}
```
You should now be able to see the data you have mapped, in the data for your source entities in Enterspeed.
```json theme={null}
"metaData": {
"name": "Tattoo",
"culture": "en-us",
"sortOrder": 0,
"level": 3,
"createDate": "2022-09-05T15.48.36",
"updateDate": "2022-09-05T15.48.37",
"nodePath": [
"1097-en-us",
"1098-en-us",
"1099-en-us"
],
"slug": "mySlugValue" 👈👈👈
}
```
# Background tasks
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/background-tasks
Enterspeed is running two background tasks in Umbraco
## Umbraco 8
### HandleEnterspeedJobsHostedService Task
This task is handling all jobs that are "Pending".
This will usually be when all content has been Seeded.
This task will run every 60 seconds and will handle jobs in batches of 50 until there are no more Pending jobs.
**Note** that there will only be one task running at a time, which means that if a task is already handling pending jobs, a new task will not be created.
### InvalidateEnterspeedJobsHostedService Task
This task will change the state of jobs that has been "Processing" for more than 1 hour, to "Failed".
This is done to clean up the Jobs queue.
This task will run every 10 minutes.
## Umbraco 9+
### HandleEnterspeedJobs Task
This task is handling all jobs that are "Pending".
This will usually be when all content has been Seeded.
This task will run every 60 seconds and will handle jobs in batches of 50 until there are no more Pending jobs.
**Note** that there will only be one task running at a time, which means that if a task is already handling pending jobs, a new task will not be created.
### InvalidateEnterspeedJobs Task
This task will change the state of jobs that has been "Processing" for more than 1 hour, to "Failed".
This is done to clean up the Jobs queue.
This task will run every 10 minutes.
# Content dashboard
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/content-dashboard
The content dashboard is for the editor to get an overview of the Enterspeed integration, ie. seeding content and the data integrity: has something failed while being sent to Enterspeed Ingest and what failed?
The Content dashboard lives in the Content section in the Umbraco backoffice.
## Failed jobs
This list is to get an overview of what, if any, has failed while being sent to the Enterspeed Ingest API.
If an entity fails to be sent, a row will be added and can be unfolded to see error details.
## Seed
Seeding allows the editor to send all published content to Enterspeed for processing.
Seed is an asynchronous action and can take a while to process.
# Culture and hostnames
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/culture-and-hostnames
## Domains for nodes that don't vary by culture
Nodes based on content types that don't allow *vary by culture* will by default use the default language in Umbraco when trying to resolve the domain ([you can customize the logic needed](#customize-the-culture-logic)).
**Example**\
Your Umbraco installation has *en-US* as the default language.
The node you are ingesting or one of its ancestors has the following setup for Culture and hostname.
| Domain | Culture |
| -------------------------------------------------- | ------- |
| [https://enterspeed.com/](https://enterspeed.com/) | en-US |
| [https://enterspeed.dk](https://enterspeed.dk) | da-DK |
If the node you are ingesting is based on a content type that does not allow *vary by culture* it will use the default language and because of that use [https://enterspeed.com/](https://enterspeed.com/) as the domain when ingested into Enterspeed.
## Multiple domains per culture for the same site
For the moment Enterspeed can only handle one domain per culture in Culture and hostnames for each site node. If you have a multi-site Umbraco installation, with multiple sites nodes, you can have as many domains with the same culture as you want. This limitation is for scenarios where one site node has multiple domains with the same culture.
### Non-working Culture and Hostnames setup for the same site
This setup would not work as expected:
| Domain | Culture |
| -------------------------------------------------- | ------- |
| [https://enterspeed.com/](https://enterspeed.com/) | en-US |
| [https://enterspeed.dk](https://enterspeed.dk) | en-US |
It is expected that both URLs would serve the same content from Umbraco. In reality, only one of them would work in Enterspeed.
This is because Umbraco defaults to one of the domains when no current URI is available, and Enterspeed must have a single URL. As the integration just uses Umbracos built-in IPublishedContent.Url() method. This method chooses the best suitable URL by culture.
### Working Culture and Hostnames setup
This setup would work as expected:
| Domain | Culture |
| -------------------------------------------------- | ------- |
| [https://enterspeed.com/](https://enterspeed.com/) | en-US |
| [https://enterspeed.dk](https://enterspeed.dk) | da-DK |
This setup would give English content the [https://enterspeed.com](https://enterspeed.com) **domain** and the danish content the [https://enterspeed.dk](https://enterspeed.dk).
If you do require to have the same content and same language on multiple domains, you have to implement some logic on your frontend that handles that.
### Domains in Enterspeed
It is possible to add more domains in Enterspeed if you wish your content to be available on multiple domains.
## Changing Culture and hostnames
When you change a hostname in Culture and hostnames, you will manually have to re-seed the content into Enterspeed.
This is currently a manual step, so please bare with us while we figure out the best way to automate this.
## Customize the culture logic
If you want to customize the culture logic, eg. if you use another culture then the default culture for a specific site or node that does not vary by culture you can implement your own version of `UmbracoCultureProvider` either by implementing the `IUmbracoCultureProvider` interface or by extending the `UmbracoCultureProvider` class and override the methods you want to customize.
```csharp theme={null}
public class CustomUmbracoCultureProvider : IUmbracoCultureProvider
{
public IEnumerable GetCulturesForCultureVariant(IContent content)
{
// My custom logic
}
public IEnumerable GetCulturesForCultureVariant(IPublishedContent content)
{
// My custom logic
}
public string GetCultureForNonCultureVariant(IContent content)
{
// My custom logic
}
public string GetCultureForNonCultureVariant(IPublishedContent content)
{
// My custom logic
}
}
```
### Registration
See examples of how to register your custom implementations [here](/enterspeed/integrations/umbraco/service-registration#custom-service-registrations).
# Database
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/database
## Tables
### EnterspeedJobs table
This table contains the jobs that need to be executed, in order to sync with Enterspeed.
| Name | Type | Description |
| --------- | -------- | --------------------------------------- |
| Id | integer | Unique identifier, auto increments |
| ContentId | integer | Id of the Umbraco node |
| Culture | string | Culture of the Umbraco node |
| JobType | integer | 0 = Publish, 1 = Delete |
| JobState | integer | 0 = Pending, 1 = Processing, 2 = Failed |
| Exception | string | Exception message if the job failed |
| CreatedAt | datetime | Job creation date |
| UpdatedAt | datetime | Job updated at |
# Guards
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/enterspeed-guards
# Enterspeed guards
Guards are split by type - content or dictionary item, they serve the purpose of ensuring that data that is about to be ingested into Enterspeed, is successfully validated by predefined or your extended guard rules.
## Enterspeed content handling guard
Our package already includes a single guard - `ContentCultureUrlRequiredGuard`, that ensures that if content for publishing to Enterspeed has culture, it must also have a URL for that specific culture available.
To extend guards with your own one, you need to implement the `IEnterspeedContentHandlingGuard` interface.
### IEnterspeedContentHandlingGuard
```csharp theme={null}
public interface IEnterspeedContentHandlingGuard
{
///
/// Validates if content can be ingested.
///
/// Content for ingest.
/// Culture of content.
/// True or false, if is valid for ingest or not.
bool CanIngest(IPublishedContent content, string culture);
}
```
### Registering a content handling guard
Guards are registered in Umbraco via the [Composing](https://our.umbraco.com/documentation/implementation/composing/) functionality.
**Umbraco 9+**
```csharp theme={null}
public class MyCustomerGuardsComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedContentHandlingGuards()
.Append();
}
}
```
**Umbraco 8**
```csharp theme={null}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class MyCustomerGuardsComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.EnterspeedContentHandlingGuards()
.Append();
}
}
```
## Enterspeed dictionary item handling guard
To extend dictionary item guards with your own one, you need to implement the `IEnterspeedDictionaryItemHandlingGuard` interface.
### IEnterspeedDictionaryItemHandlingGuard
```csharp theme={null}
public interface IEnterspeedDictionaryItemHandlingGuard
{
///
/// Validates if dictionary item can be ingested.
///
/// Dictionary item for ingest.
/// Culture of dictionary item.
/// True or false, if is valid for ingest or not.
bool CanIngest(IDictionaryItem dictionaryItem, string culture);
}
```
### Registering a dictionary item handling guard
Guards are registered in Umbraco via the [Composing](https://our.umbraco.com/documentation/implementation/composing/) functionality.
**Umbraco 9+**
```csharp theme={null}
public class MyCustomerGuardsComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedDictionaryItemHandlingGuards()
.Append();
}
}
```
**Umbraco 8**
```csharp theme={null}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class MyCustomerGuardsComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.EnterspeedDictionaryItemHandlingGuards()
.Append();
}
}
```
# Enterspeed Property Service
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/enterspeed-propertys-service
The Enterspeed property service has the role of mapping node data into usable objects for the data sources.
Our package already includes a default Enterspeed property service, that maps all properties including some meta-data that is present on all data sources. You can extend the meta-data, to include relevant data for your solution, on all data sources.
Examples could be site settings or domain name data.
## Custom Property Service
To extend the properties or meta-data on sources, you would need to create your custom property service. This is easily done by inheriting the default property service. In this example, all default behavior of the `EnterspeedPropertyService` is preserved but grants us the option to extend meta-data.
As you can see, the `MapAdditionalProperties` and `MapAdditionalMetaData` can be overridden. Here you only need to add your logic and include your own property + data.
You can also override `MapAdditionalMediaProperties` and `MapAdditionalMediaMetaData` to extend media source entities.
Note that we in this example are using `StringEnterspeedProperty`. You can choose between multiple property types.
(Array, boolean, number, object and so on.)
If you want different logic based on content type or other conditions, you should use [Mappers](/enterspeed/integrations/umbraco/additional-properties) instead.
### Example
```csharp theme={null}
public class CustomPropertyService : EnterspeedPropertyService
{
public CustomPropertyService(EnterspeedPropertyValueConverterCollection converterCollection, IServiceProvider serviceProvider) : base(converterCollection, serviceProvider)
{
}
protected override void MapAdditionalProperties(Dictionary data, IPublishedContent content, string culture)
{
data.Add("mySpecialKey", new StringEnterspeedProperty("my value fetched from my business logic"));
}
protected override void MapAdditionalMediaProperties(Dictionary data, IPublishedContent content, string culture)
{
data.Add("mySpecialKey", new StringEnterspeedProperty("my value fetched from my business logic"));
}
protected override void MapAdditionalMetaData(Dictionary metaData, IPublishedContent content, string culture)
{
metaData.Add("mySpecialKey", new StringEnterspeedProperty("my value fetched from my business logic"));
}
protected override void MapAdditionalMediaMetaData(Dictionary metaData, IPublishedContent content, string culture)
{
metaData.Add("mySpecialKey", new StringEnterspeedProperty("my value fetched from my business logic"));
}
}
```
### Registering your new property service
The property service is registered in Umbraco via an [IComposer](https://our.umbraco.com/documentation/implementation/composing/). The `AddUnique` extension method replaces the normal property service and implements your own.
**Umbraco 9+**
```csharp theme={null}
public class MyCustomComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddUnique(ServiceLifetime.Transient);
}
}
```
**Umbraco 8**
```csharp theme={null}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class MyCustomComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.RegisterUnique();
}
}
```
You should now be able to see the data you have mapped, in the meta-data object of your data sources in Enterspeed.
```json theme={null}
"metaData": {
"name": "Tattoo",
"culture": "en-us",
"sortOrder": 0,
"level": 3,
"createDate": "2022-09-05T15.48.36",
"updateDate": "2022-09-05T15.48.37",
"nodePath": [
"1097-en-us",
"1098-en-us",
"1099-en-us"
],
"mySpecialKey": "my value fetched from my business logic" 👈👈👈
}
```
# Value Converter
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/enterspeed-value-converter
# Enterspeed Value Converter
A property value converter is a class that will convert the input value from Umbraco into an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties). To implement your own converter you need to implement the IEnterspeedPropertyValueConverter interface
## IEnterspeedPropertyValueConverter
This interface contains two methods that need to be implemented
### IsConverter
```csharp theme={null}
bool IsConverter(IPublishedPropertyType propertyType);
```
This method is called when the EnterspeedPropertyService tries to find the proper converter for this property.
An implementation of this method could look like this:
```csharp theme={null}
public bool IsConverter(IPublishedPropertyType propertyType)
{
return propertyType.EditorAlias.Equals("Umbraco.TextBox");
}
```
### Convert
```csharp theme={null}
IEnterspeedProperty Convert(IPublishedProperty property, string culture);
```
This is the method that is converting the Umbraco property to an IEnterspeedProperty.
An implementation of this method could look like this:
```csharp theme={null}
public IEnterspeedProperty Convert(IPublishedProperty property, string culture)
{
var value = property.GetValue(culture);
return new StringEnterspeedProperty(property.Alias, value);
}
```
## Registering a converter
Converters are registered in Umbraco via an [IComposer](https://docs.umbraco.com/umbraco-cms/reference/using-ioc).
**Umbraco 9+**
```csharp theme={null}
public class MyCustomerPropertyValueConverterComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedPropertyValueConverters()
.Append();
}
}
```
**Umbraco 8**
```csharp theme={null}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class MyCustomerPropertyValueConverterComposer : IUserComposer
{
composition.EnterspeedPropertyValueConverters()
.Append();
}
```
**Umbraco 7**
```csharp theme={null}
public class RegisterCustomPropertyValueConverters : ApplicationEventHandler
{
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
EnterspeedContext.Current.EnterspeedPropertyValueConverters
.Append();
}
}
```
Note that the EnterspeedPropertyService will find the converters in the order that they are registered, which means that if you want to replace a default converter with your own, you need to insert your converter like this:
**Umbraco 9+**
```csharp theme={null}
[ComposeAfter(typeof(EnterspeedComposer))]
public class MyCustomerPropertyValueConverterComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedPropertyValueConverters()
.InsertBefore();
}
}
```
**Umbraco 8**
```csharp theme={null}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class MyCustomerPropertyValueConverterComposer : IUserComposer
{
composition.EnterspeedPropertyValueConverters()
.InsertBefore();
}
```
**Umbraco 7**
```csharp theme={null}
public class RegisterCustomPropertyValueConverters : ApplicationEventHandler
{
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
EnterspeedContext.Current.EnterspeedPropertyValueConverters
.InsertBefore();
}
}
```
## Default converters
Enterspeed ships with [default property value converters](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/blob/master/documentation/enterspeed-value-converters/property-value-converters/defaults/README.md) for all the built-in property editors that Umbraco ships with out of the box.
# Getting data
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/getting-data
# Getting data from Umbraco to Enterspeed
When you press Save and publish on a piece of content in Umbraco, Umbraco handles the content as normally: validation, trigger events, etc. The integration into Enterspeed is simply just a couple of events that the integration listens to.
## When content is being published
When a piece of content is published in Umbraco, the integration reacts to this and adds a job for the process in the database. Each job is unique and will only be processed once by the integration. You can read more about the jobs in the reference section.
Directly after the job is added to the database it will be processed by the integration and sent to the Enterspeed Ingest API for Enterspeed to process it and deliver it to the Delivery API.
## UmbracoContentEntity explained
To make sure you understand the whole process from end to end, an important detail is how the UmbracoContentEntity is created, because these properties define the entity you work with in Enterspeed.
The properties within the properties object don't change when it gets ingested in Enterspeed, this means that when you are to create your Schemas to model your API, you can rely 1:1 on what you ingest and what you can query in Enterspeed.
## Seeding content to Enterspeed
If you have an existing site, have installed Enterspeed later in the development process or just want to make sure all your content is in Enterspeed, you can seed or re-seed all the content from Umbraco into Enterspeed.
This can simply be done by going to the Enterspeed Content dashboard and then going to Seed and pressing the button. In theory, the same happens when you press Save and publish. The seeding pulls all the content from the published cache and inserts a new EnterspeedJob in the database for each piece of content.
Every minute the database is checked for pending entities and if it has any it will be sent to the Enterspeed Ingest API, equivalent to the process from steps 3 to 7 in Publishing step by step. You can read more about the jobs in the reference section.
# Getting started
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/getting-started
# Getting started with Umbraco & Enterspeed
The easiest way to get started with Umbraco and Enterspeed is by using the pre-built Umbraco integration.
**GitHub: [Enterspeed Source Umbraco CMS](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms)**
This integration takes care of calling the Enterspeed Ingest API when changes occur in Umbraco. For a full overview of what Umbraco entities are sent to Enterspeed, please see Umbraco entities.
## Installation
The fastest way to get up and running is to install the Enterspeed Umbraco integration with NuGet.
**NuGet:**\
[Umbraco 7](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms.v7/)\
[Umbraco 8](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms.v8/)\
[Umbraco 9+](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms/)
**Using Umbraco 9+**
*Nuget package (Enterspeed.Source.UmbracoCms) *
***Version 5.x:*** Use this if your Umbraco version is 14 or higher.
***Version 4.x:*** Use this if your Umbraco version is 13 or lower.
**Using Umbraco Cloud?** If you have used the Umbraco Cloud UaaS.cmd tool to set up your solution,
you need to manually update the referenced dlls after installing the Enterspeed NuGet package. Specifically, Microsoft.Bcl.AsyncInterfaces.dll needs to be updated.
From Visual Studio navigate to the \[Namespace].Web\bin folder, and right-click on Microsoft.Bcl.AsyncInterfaces.dll, and select "Update Reference".
When the installation above has been completed two new dashboards have been added to your Umbraco solution.
* **Content:** To seed and check errors when data is ingested
* **Settings:** To configure Enterspeed in Umbraco
## Configuration
Before Umbraco starts sending data to Enterspeed you will need to add a little piece of configuration.
Luckily this can easily be done within Umbraco itself or via [appsettings.json](#app-settings-umbraco-9) (Umbraco 9 and up) or [Web.config](#webconfig-umbraco-7-or-8). (Umbraco 7 and 8)
### Source API key and Ingest endpoint
Firstly go to Settings and then select the Enterspeed Settings dashboard in your Umbraco backoffice.
You should see something like this:
#### Enterspeed endpoint
The Enterspeed endpoint is the ingest endpoint, that Umbraco will use to send content to Enterspeed.
Unless you have gotten a specific Enterspeed endpoint to call, please use: [https://api.enterspeed.com](https://api.enterspeed.com/)
#### Media domain
The Media domain is to tell the Enterspeed integration where you have your media placed, e.g. if you have a CDN.
If you leave it empty, it will just use your current Umbraco installation domain.
### API key
Before you can insert an API key, you must have created a Source within the [Enterspeed Management](https://app.enterspeed.com/settings/data-sources).
The API key looks something like this: source-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. When you have gotten it, insert it in the API key input field.
### Preview API key
The optional API key serves the purpose of ingesting draft and unpublished content to the secondary (preview) source. Can be leveraged for your content editors to preview content, similarly to the in-built 'Save & Preview' functionality in Umbraco.
Before you can insert a Preview API key, you must have created a Source within the [Enterspeed Management](https://app.enterspeed.com/settings/data-sources).
Enterspeeds connector will automatically push data to relevant primary or secondary sources based on actions performed in Umbraco backoffice, such as - Unpublish, trash, save, publish, etc.
### Testing connection
When you have inserted the Enterspeed endpoint and the API key(-s) click on Test connection and make sure that you get a successful response. When you do go ahead and Save the configuration.
## App settings (Umbraco 9+)
For appsettings.json or environment-specific settings file, please use the following appSettings example:
```json theme={null}
{
...
"Enterspeed": {
"Endpoint": "https://api.enterspeed.com", // required (deprecated - replaced by `BaseUrl` in 4.4.0 and 5.3.0 but still works as fallback)
"BaseUrl": "", // optional (default https://api.enterspeed.com)
"Apikey": "", // required
"MediaDomain": "", // optional
"PreviewApikey": "", // optional
"EnabledFailedJobsProcessing" : boolean, // optional (default false)
"EnableMasterContent": boolean, // optional (default false)
"RemoveTrailingSlash": boolean, // optional (default false)
"RootDictionariesDisabled": "", // optional (default false)
"RunJobsOnAllServerRoles": "", // optional (default false)
}
...
}
```
There are additional settings that can only be configured in the app settings file. These settings are applicable only for Umbraco 9 and later versions.
### Enable Failed Jobs Processing
This feature automatically reprocesses recent failed jobs. It will retry up to five times.
### Master Content
Allows ingestion of a master variant whenever a language variant is updated. Similarly, the master variant will be deleted when the last language variant is removed.
Master variants have the type of the node with the postfix -master, enabling schemas to be created specifically for the master variants.
### Remove Trailing Slash
Removes trailing slashes from all URLs.
### Root Dictionaries Disabled
Root dictionaries are enabled by default. This functionality ensures that a root source entity is created for the dictionaries in Enterspeed,
allowing the use of handles in Enterspeed.
## Run Jobs in All Server Roles
Enables job processing for all Umbraco server roles. In most cases, this is not recommended, as multiple servers can pick up the same jobs from the shared database, resulting in multiple ingests of the same jobs.
## Web.config (Umbraco 7 or 8)
For Web.config, please use the following appSettings example:
```xml theme={null}
```
## Processed Umbraco entities
Here is an overview of what is processed and send to Enterspeed and what is not.
### Processed entities
* Published content
* Draft content (unpublished content/saved content)
* Media
* Dictionary
### Not processed entities
* Members
* Users
# Grid Editor Value Converters
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/grid-editor-value-converters
# Enterspeed Grid Editor Value Converters
A grid editor value converter is a class that will convert the input value from an Umbraco grid editor into an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties).
To implement your own converter you need to implement the IEnterspeedGridEditorValueConverter interface
If no converter is registered for the grid editor, the DefaultGridLayoutPropertyValueConverter will try
to convert it into an IEnterspeedProprety automatically, by looking at the types.
## IEnterspeedGridEditorValueConverter
This interface contains two methods that need to be implemented
### IsConverter
```csharp theme={null}
bool IsConverter(string alias);
```
This method is called when the EnterspeedGridEditorService tries to find the correct converter for this grid editor.
An implementation of this method could look like this:
```csharp theme={null}
public bool IsConverter(string alias)
{
return alias.InvariantEquals("rte");
}
```
### Convert
```csharp theme={null}
IEnterspeedProperty Convert(GridControl editor, string culture)
```
This is the method that is converting the Umbraco grid editor to an IEnterspeedProperty.
An implementation of this method could look like this:
```csharp theme={null}
public IEnterspeedProperty Convert(GridControl editor, string culture)
{
return new StringEnterspeedProperty(editor.Value.ToString());
}
```
## Registering a converter
Converters are registered in Umbraco via an [IComposer](https://our.umbraco.com/documentation/implementation/composing/).
**Umbraco 9+**
```csharp theme={null}
public class MyCustomerGridEditorValueConverterComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedGridEditorValueConverters()
.Append();
}
}
```
**Umbraco 8**
```csharp theme={null}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class MyCustomerGridEditorValueConverterComposer : IUserComposer
{
composition.EnterspeedGridEditorValueConverters()
.Append();
}
```
Note that the EnterspeedGridEditorService will find the converters in the order that they are registered, which means that, if you want to replace a default converter with your own, you need to insert your converter like this:
**Umbraco 9+**
```csharp theme={null}
public class MyCustomerPropertyValueConverterComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedGridEditorValueConverters()
.InsertBefore();
}
}
```
**Umbraco 8**
```csharp theme={null}
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class MyCustomerPropertyValueConverterComposer : IUserComposer
{
composition.EnterspeedGridEditorValueConverters()
.InsertBefore();
}
```
## Default converters
Enterspeed ships with [default grid editor value converters](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/tree/master/documentation/enterspeed-value-converters/grid-editor-value-converters/defaults) for some of the built-in grid editors that Umbraco ships with out of the box.
# Jobs
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/jobs
A job is an instruction that needs to be executed in order to synchronize data with Enterspeed.
A job contains information about what content from Umbraco, that needs to be handled, and how it should be handled (ie. Publish or Delete).
Jobs are stored in a custom table.
## Job lifecycle
When a content node in Umbraco is being published, unpublished, deleted or moved a new job will be added to the EnterspeedJobs table with the state of "Pending".
Immediately after the jobs have been created, they will be handled by the EnterspeedJobHandler, which is changing the state of the jobs to "Processing".
A job will then either be deleted if it could be handled without any errors, or set to "Failed" with the exception(s) that was thrown.
## Seeding jobs
In the Enterspeed Content dashboard located under Content in Umbraco, you have the possibility to Seed all your content.
When the button is clicked, all content will be queued up for publishing to Enterspeed.
This means that for each published content node (and variant) in Umbraco, there will be created a publishing job.
This will only create publishing jobs and not delete jobs.
## Old processing jobs
If a job has been in the state of "Processing" for more than 1 hour, the state of that job will automatically change to "Failed".
This is done to clean up jobs that, for some reason timed out while processing.
This is done by the InvalidateEnterspeedJobsHostedService background task.
## IEnterspeedJobHandler
The `IEnterspeedJobHandler` is responsible for deciding if it can support and process the requested job. The common process consists of fetching data from Umbraco, converting it to an `IEnterspeedEntity` and sending it to the Enterspeed Ingest API.
For each job that is being handled all previously failed jobs, for the same content node will be fetched and deleted, so we only have a maximum of 1 failed job per content node with the recent exception. If a previously failed job is handled with success, the failed job will also be deleted, since it's no longer failing.
To implement your own job handler you need to implement the `IEnterspeedJobHandler` interface.
### CanHandle
```csharp theme={null}
bool CanHandle(EnterspeedJob job);
```
This method is called when the Enterspeed jobs handling service tries to find a proper handler for this job.
An implementation of this method could look like this:
```csharp theme={null}
public bool CanHandle(EnterspeedJob job)
{
return
// Check if 'Main source/Publish' is configured
_enterspeedConnectionProvider.GetConnection(ConnectionType.Publish) != null
// Check if current job is for 'Content node'
&& job.EntityType == EnterspeedJobEntityType.Content
// Check if content changes were published, rather than saved as draft
&& job.ContentState == EnterspeedContentState.Publish
// Check if we want to Ingest, instead of deleting content
&& job.JobType == EnterspeedJobType.Publish;
}
```
### Handle
```csharp theme={null}
void Handle(EnterspeedJob job);
```
This is the method that is responsible for processing job - lookup relevant Umbraco data, executing validation, mapping, and ingestion.
An implementation of this method could look like this:
```csharp theme={null}
public void Handle(EnterspeedJob job)
{
using (var context = _umbracoContextFactory.EnsureUmbracoContext())
{
var content = GetContent(job, context);
if (!CanIngest(content, job))
{
return;
}
var umbracoData = CreateUmbracoContentEntity(content, job);
Ingest(umbracoData, job);
}
}
```
## Registering a job handler
Job handlers are registered in Umbraco via an [IComposer](https://our.umbraco.com/documentation/implementation/composing/).
Example:
```csharp theme={null}
public class MyCustomJobHandlersComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedJobHandlers()
.Append();
}
}
```
Note that the Enterspeed jobs handling service will find the handlers in the order that they are registered, which means that if you want to replace some of the default handlers with your own, you need to insert your handler like this:
```csharp theme={null}
public class MyCustomJobHandlersComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.EnterspeedJobHandlers()
.InsertBefore();
}
}
```
## Default job handlers
| Name | Action | Triggered by |
| ------------------------------------------------ | --------------- | --------------------------------- |
| EnterspeedContentPublishJobHandler | Ingest - save | Published content |
| EnterspeedContentDeleteJobHandler | Ingest - delete | Trashed/unpublished content |
| EnterspeedDictionaryItemPublishJobHandler | Ingest - save | Saved dictionary item |
| EnterspeedDictionaryItemDeleteJobHandler | Ingest - delete | Deleted dictionary item |
| EnterspeedPreviewContentPublishJobHandler | Ingest - save | Saved draft content |
| EnterspeedPreviewContentDeleteJobHandler | Ingest - delete | Trashed/unpublished draft content |
| EnterspeedPreviewDictionaryItemPublishJobHandler | Ingest - save | Saved dictionary item |
| EnterspeedPreviewDictionaryItemDeleteJobHandler | Ingest - delete | Deleted dictionary item |
[Source code of Job handling related classes (Umbraco 8)](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/tree/master/src/Enterspeed.Source.UmbracoCms.V8/Handlers)\
[Source code of Job handling related classes (Umbraco 9+)](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/tree/master/src/Enterspeed.Source.UmbracoCms/Handlers)
## In case, of more customization
If specifying new job handlers is not enough, or you want to change the flow of how job handlers are assigned and jobs are handled:
* [`IEnterspeedJobsHandler Umbraco 8`](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/blob/master/src/Enterspeed.Source.UmbracoCms.V8/Handlers/EnterspeedJobsHandler.cs)
* [`IEnterspeedJobsHandler Umbraco 9+`](https://github.com/enterspeedhq/enterspeed-source-umbraco-cms/blob/master/src/Enterspeed.Source.UmbracoCms/Handlers/EnterspeedJobsHandler.cs)
# Publishing
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/publishing
# Publishing step by step
When the editor publishes a node the cache refreshed event is triggered and the Enterspeed Umbraco integration listens to this event, to send the updated content to Enterspeed. When this event is triggered these steps are executed synchronously:
1. A new EnterspeedJob is added to a database for processing.
2. The newly added job is requested to be processed immediately.
3. The published content from Umbraco is fetched from the cache.
4. Each property on the IPublishedContent is converted to an IEnterspeedProperty with an IEnterspeedPropertyValueConverter.
5. A UmbracoContentEntity is created.
6. The content entity is sent to the Enterspeed Ingest API.
7. If the response is successful nothing happens, if it is not:
1. The EnterspeedJob is re-inserted in the database as failed.
2. The failed jobs can be viewed on the content dashboard.
If you wish you can check out the sequence diagram for the process here:
```bash theme={null}
@startuml
title Umbraco - publishing
hide footbox
actor "Editor" as EDITOR
boundary "Umbraco" as UMBRACO
participant "Cache refreshed" as CACHE_REFRESHED
database "Jobs" as JOBS
participant "Job service" as JOBS_SERVICE
participant "Enterspeed Property Service " as PROPERTY_SERVICE
participant "IEnterspeedPropertyValueConverter\nimplementation" as PROPERTY_VALUE_CONVERTER
boundary "Enterspeed Ingest" as INGEST
EDITOR -> UMBRACO: Publish content
UMBRACO -> CACHE_REFRESHED: Content has been updated
CACHE_REFRESHED -> JOBS: Insert EnterspeedJobs to process
CACHE_REFRESHED -> JOBS_SERVICE: Process jobs synchronously
JOBS_SERVICE -> JOBS: Fetch jobs for process
JOBS -> JOBS_SERVICE: return EnterspeedJob[]
JOBS_SERVICE -> PROPERTY_SERVICE: Get properties
loop Foreach property in properties
PROPERTY_SERVICE -> PROPERTY_VALUE_CONVERTER: Create IEnterspeedProperty object
PROPERTY_VALUE_CONVERTER --> PROPERTY_SERVICE: return IEnterspeedProperty
end
PROPERTY_SERVICE -> JOBS_SERVICE: return Dictionary
JOBS_SERVICE -> JOBS_SERVICE: Create IEnterspeedEntity
JOBS_SERVICE -> INGEST: Send IEnterspeedEntity to Enterspeed
INGEST -> JOBS_SERVICE: Response
alt Response is not successful
JOBS_SERVICE -> JOBS: Insert failed job
end
```
# Redirects
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/redirects
## Umbraco Redirects
If you use the build-in Umbraco Redirect URL Management the redirects will be ingested into Enterspeed out of the box.
## Custom Redirects
If you are using a 3. party redirect plugin in your Umbraco installation you can implement your own version of the `UmbracoRedirectsService` either by implementing the `IUmbracoRedirectsService` interface or by extending the `UmbracoRedirectsService` class and overriding the \`GetRedirects\`\` method.
### Implementing interface
```csharp theme={null}
public class CustomUmbracoRedirectsService : IUmbracoRedirectsService
{
private readonly IRedirectUrlService _redirectUrlService;
private readonly IUmbracoUrlService _umbracoUrlService;
public CustomUmbracoRedirectsService(IRedirectUrlService redirectUrlService, IUmbracoUrlService umbracoUrlService)
{
_redirectUrlService = redirectUrlService;
_umbracoUrlService = umbracoUrlService;
}
public virtual string[] GetRedirects(Guid contentKey, string culture)
{
// My custom logic
}
}
```
### Overriding
```csharp theme={null}
public class CustomUmbracoRedirectsService : UmbracoRedirectsService
{
public CustomUmbracoRedirectsService(IRedirectUrlService redirectUrlService, IUmbracoUrlService umbracoUrlService)
: base(redirectUrlService, umbracoUrlService)
{
}
public override string[] GetRedirects(Guid contentKey, string culture)
{
var umbracoRedirects = base.GetRedirects(contentKey, culture);
// logic for getting custom redirects
return umbracoRedirects.Concat(customRedirects).ToArray();
}
}
```
### Registration
See examples of how to register your custom implementations [here](/enterspeed/integrations/umbraco/service-registration#custom-service-registrations).
# Service registration
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/service-registration
In a default Umbraco installation all Enterspeed services will automatically be registered because of the call to `.AddComposers()` in the `ConfigureServices` method of the `Startup` class.
```csharp theme={null}
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddComposers()
.Build();
}
...
}
```
## Manual registration
If you, for what ever reason, have removed the `.AddComposers()` call and manually have registered the Umbraco services, you can register Enterspeed using the `.AddEnterspeed()` method.
```csharp theme={null}
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddDeliveryApi()
.AddEnterspeed()
.Build();
}
...
}
```
## Custom service registrations
Sometimes you want to overwrite existing functionality with your own implementation. You can do that by implementing existing interfaces or overwrite existing classes and then register your new custom types. You do that by creating an Umbraco composer class - see example below. You can also read more about [dependency injection in the Umbraco documentation](https://docs.umbraco.com/umbraco-cms/reference/using-ioc)
```csharp theme={null}
[ComposeAfter(typeof(EnterspeedComposer))]
public class CustomComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.Services.AddUnique(ServiceLifetime.Transient);
}
}
```
# Services
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/services
## EnterspeedPropertyService : IEnterspeedPropertyService
This service is used for converting a Umbraco property to an IEnterspeedProperty.
### Methods
```csharp theme={null}
IDictionary GetProperties (IPublishedContent content, string culture = null);
IDictionary ConvertProperties (IEnumerable properties, string culture = null;
```
Both methods will find the correct registered Enterspeed Property Value Converter and convert the value to an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties).
## EnterspeedGridEditorService : IEnterspeedGridEditorService
This service is used for converting a Umbraco grid editor value to an IEnterspeedProperty.
### Methods
```csharp theme={null}
IEnterspeedProperty ConvertGridEditor(GridControl control, string culture = null)
```
This will find the correct registered Enterspeed Grid Editor Value Converter and convert the value to an [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities/properties)
# Troubleshooting
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/troubleshooting
A section of some of the common issues that might occour.
### Ingest skipped if audit/notification pipeline is exisited by other extensions
If another pipeline is changing the audit event or breaking the notification flow, this could cause the Enterspeed Umbraco source package to skip the ingest, as we listen for specific notifications (PublishVariant, Publish, Move)
An example of this below
```csharp theme={null}
public class ContentEventsNotificationNotificationHandler :
INotificationAsyncHandler,
{
public async Task HandleAsync(ContentPublishingNotification notification, CancellationToken cancellationToken)
{
// Prematurely exiting
return;
}
}
```
### Does updating a domainon a root node in Umbraco trigger a re-ingest of the entire sub-tree?
No. Changing a domain on a root node will not trigger a re-seed of the node and its descendants.
If you want to trigger a re-seed in Umbraco after changing the domain, the best option is to use the "Publish with descendants"-feature in Umbraco.
After updating the domain, click the node you changed the domain on and select "Publish with descendants". This will send the updates node and its descendants to Enterspeed.
# Umbraco Cloud
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/umbraco-cloud
# Getting started with Umbraco Cloud
If you are building a new site or you are new to Umbraco Cloud, you should refer to [https://our.umbraco.com/documentation/Umbraco-Cloud/Getting-started/](https://our.umbraco.com/documentation/Umbraco-Cloud/Getting-started/).
# Installation
Described in [Getting started](/enterspeed/integrations/umbraco/getting-started#installation)
# UmbracoContentEntity
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/umbraco-content-entity
The UmbracoContentEntity is the concrete Umbraco specific implementation of the [IEnterspeedEntity](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/tree/master/documentation/entities).
## Implementation details
### Abstract
| Name | Type | Description |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Id | string | Unique identifier ie. "1078-en-us" |
| Type | string | ContentType alias |
| Url | string | The current URL of the content, either relative or absolute |
| Redirects | string\[] | Array of redirects for the node |
| ParentId | string | Unique identifier of the parent ie. "1078-en-us" |
| Properties | [IEnterspeedProperty](https://github.com/enterspeedhq/enterspeed-sdk-dotnet/blob/master/documentation/entities/properties/README.md)> | Dictionary of property alias and value is the converted Enterspeed property |
### Example
```json theme={null}
{
"id": "1055-en-us",
"type": "site",
"parentId": "1054-en-us",
"url": "https://example.com/about-us",
"redirects": ["/about"],
"properties": {
"includeInNavigation": {
"name": "includeInNavigation",
"type": "boolean",
"value": false
},
"title": {
"name": "title",
"type": "string",
"value": "Home"
},
"metaData": {}
}
}
```
### Meta data
To process Umbraco-specific properties we have added a `metaData` object that contains:
| Name | Type | Description |
| ---------- | --------- | -------------------------------------------- |
| culture | string | ie. en-us |
| nodeName | string | Name of the node |
| createDate | string | Date for when the node has been created |
| updateDate | string | Date for when the node has last been updated |
| nodePath | string\[] | Path to ancestor nodes in the tree |
| sortOrder | number | What order the nodes are sorted in |
| level | number | What level in the tree the node has |
```json theme={null}
{
"properties": {
"metaData": {
"culture": {
"name": "culture",
"type": "string",
"value": "en-US"
},
"nodeName": {
"name": "nodeName",
"type": "string",
"value": "This is the name of a node"
},
"createDate": {
"name": "createDate",
"type": "string",
"value": "09-12-2020T10:49:01:00"
},
"updateDate": {
"name": "updateDate",
"type": "string",
"value": "10-12-2020T10:49:01:00"
},
"nodePath": {
"name": "nodePath",
"type": "array",
"items": [
{
"name": null,
"type": "number",
"value": 1061,
"precision": 0
},
{
"name": null,
"type": "number",
"value": 1062,
"precision": 0
},
{
"name": null,
"type": "number",
"value": 1063,
"precision": 0
}
]
},
"sortOrder": {
"name": "sortOrder",
"type": "number",
"value": 1,
"precision": 0
},
"level": {
"name": "level",
"type": "number",
"value": 1,
"precision": 0
}
}
}
}
```
# Tagging in Umbraco
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/umbraco-tagging
The following article describes an example of tagging implementation for content in Umbraco, and how to filter source entities by tag in Enterspeed.
As an example, we are going to build a basic news portal about sports, named "Sports Central".
## Content preparation in Umbraco
In order to build the site's content structure and filter it in Enterspeed, we have to do some groundwork as - creating document types, data types and adding some articles as content.
## Document types
### Article
Used for article content nodes themselves, with the following properties:
| Property | Data type | Description |
| ----------- | ----------- | -------------------------------------- |
| title | Textstring | Title for the article |
| subheader | Textstring | Subheader for the article |
| publishedAt | DatePicker | Published display date for the article |
| tags | Tags Picker | Related tags for the article |
### Period Group
Is a document type without any properties, in the content tree, it will be used a grouping folder for articles per month, e.g. 09/2021.
### Articles
Is a document type for grouping all 'Period group' content nodes.
### Settings
Document type contains various settings for the site and different data types.
### Tags
Used as container node for listing all possible tags underneath it in the content tree.
### Tag
Tag, that can be news article can be associated with.
### Articles By Tag
Is an *Element Type* document type, describing a block for displaying articles by a specific tag. Will be used on the front page of the site.
| Property | Data type | Description |
| -------- | ----------------- | ---------------------------------------------------------------- |
| title | Textstring | Title used for the block, f.x. - *'Latest from football world'.* |
| tag | Single Tag Picker | A tag that you want to include articles for. |
| top | Numeric | Define how many articles you want to see. |
### Site
Document type representing site instance.
| Property | Data type | Description |
| -------- | -------------------- | -------------------------------------------------------------------------------------------------- |
| blocks | Frontpage Block List | Our front page will consist of different blocks as - articles by tag, and potentially many others. |
## Data types
### Single Tag Picker
Is a data type extending Umbraco default 'Content Picker', where we specify 'Tags' content node as a start node.
### Frontpage Block List
Is a data type extending Umbraco default 'Block List', where we add a single available block - 'Articles By Tag', that we have created previously as part of Document types.
## Adding blocks to frontpage
On the site node, we are going to add some blocks for displaying articles by different tags.
Each of them configured with their own criteria for display of articles:
## Defining schemas in Enterspeed
### Creating a Site schema
Site schema will work with source entities of type - 'site' since those are the ones where we have defined our blocks (in Umbraco) to describe what blocks we want to display.
This schema doesn't contain much of the logic, its responsibility is to iterate over blocks defined on the source entity and pass each of the blocks in partial view, with matching name.
```json theme={null}
{
"route": {
"url": "{url}"
},
"triggers": {
"umbraco": ["site"]
},
"properties": {
"blocks": {
"type": "array",
"input": "{p.blocks}",
"items": {
"type": "partial",
"input": "{item}",
"alias": "Block-{item.contentType}"
}
}
}
}
```
The only tricky part to remember is that now we have defined an alias match for partial schema view, where `'item.contentType'` value is an alias of our current block Element document type in Umbraco - `'articlesByTag'`.
## Creating a partial schema - Articles By Tag Block
We are halfway through. The previous schema iterated through blocks, this schema will handle a specific type of block - `Block-articlesByTag`.
As result, we want to display the following properties - `alias` and `title` of this block and collection of `articles` matching criteria defined on this block along with basic information about them - `title`, `subheader`, `publishedAt` date and a list of `tags` associated.
```json theme={null}
{
"alias": "Block-articlesByTag",
"properties": {
"alias": "{item.contentType}",
"title": "{item.content.title}",
"articles": {
"type": "array",
"input": {
"$lookup": {
"filter": "type eq 'article' and properties.tags/any(t: t.id eq '{item.content.tag}')",
"top": "{item.content.top}",
"orderBy": {
"property": "p.publishedAt",
"sort": "desc"
}
}
},
"items": {
"type": "object",
"properties": {
"title": "{item.p.title ?? item.p.metaData.name}",
"subheader": "{item.p.subheader}",
"publishedAt": "{item.p.publishedAt}",
"tags": {
"type": "array",
"input": "{item.p.tags}",
"var": "tag",
"items": "{tag.name}"
}
}
}
}
}
}
```
Notice how we are using `$lookup` in combination with `filter`, `top`, and `orderBy`. In simple words, we want to lookup all source entities that match our filter, meaning where the type of source entity is an article and it has a current block selected tag associated with it.
On our front page, we want to show the latest published news first. That is what we define in our sorting criteria for all filters found in source entities. Lastly, regarding lookup, we defined a limit on how many articles will be displayed for the current block.
## Outcome
After updating and deploying previously mentioned schemas and publishing all content data to Enterspeed, the outcome from the Delivery API would look like this:
```json theme={null}
{
"meta": {
"status": 200,
"redirect": null
},
"route": {
"blocks": [
{
"alias": "articlesByTag",
"title": "Hey Vancouver",
"articles": [
{
"title": "Canucks camp notebook: Sporting new look, Boeser striving for consistency",
"subheader": "Dan Murphy and Iain MacIntyre discuss the latest news surrounding Elias Pettersson and Quinn Hughes, as well as what's happening with Travis Hamonic",
"publishedAt": "09/28/2021 00:00:00",
"tags": ["Vancouver Canucks", "Ice hockey"]
},
{
"title": "Canucks Sign Forward Jason Dickinson",
"subheader": "...to a three-year contract",
"publishedAt": "09/02/2021 00:00:00",
"tags": ["Ice hockey", "Vancouver Canucks"]
},
{
"title": "NHL expects full capacity in all cities except Vancouver and Montreal",
"subheader": "",
"publishedAt": "08/30/2021 00:00:00",
"tags": ["Vancouver Canucks", "Ice hockey"]
}
]
},
{
"alias": "articlesByTag",
"title": "What is happening in #basketball?",
"articles": [
{
"title": "Position breakdown: Maximizing Porzingis a priority among big men",
"subheader": "When you have one of the tallest players in the NBA, one with a rare skill set of inside and outside capabilities, it makes sense to maximize his assets.",
"publishedAt": "09/27/2021 00:00:00",
"tags": ["Basketball"]
},
{
"title": "Will Kidd get the best out of Doncic?",
"subheader": "The savvy former guard looks to bring his Hall of Fame expertise to Dallas.",
"publishedAt": "09/14/2021 00:00:00",
"tags": ["Basketball"]
}
]
}
]
},
"views": {}
}
```
# Updating package
Source: https://docs.enterspeed.com/enterspeed/integrations/umbraco/updating-package
We're trying our best to make sure that your experience is as smooth as possible, but sometimes things might not work as expected.
After the package upgrade, it is recommended to do a hard refresh and clear the browser's cache, to ensure that the latest plugin resources are loaded.
# Webhooks
Source: https://docs.enterspeed.com/enterspeed/integrations/webhooks
Webhooks in Enterspeed are HTTP callbacks that will send all generated views by schemas with [destinations](/enterspeed/reference/js/full-schema/actions#destination) to a URL configured in the Webhook. This means that you can decide on the schema level which views you want to send to the webhook.
You will only have to set the destination field on the entity schema you want to send to the webhook. All schema references are automatically resolved so you don't have to set it on all referenced schemas.
It's possible to configure multiple Webhook destinations if you need to push different types of data to different URL endpoints.
## Configuration
In order to setup the Webhook configuration you need the following:
| Setting | Description |
| ---------- | ----------------------------------------------------------------------------------------- |
| Name | A name to identify the webhook |
| URL | The url to call from the webhook |
| Access Key | An API key the client can use to identify that the call is in fact coming from Enterspeed |
## Request
The request to the client-configured URL will be made with the following configuration.
| Setting | Value | Description |
| ----------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Retry count | 3 | How many times the webhook will try to send the request |
| Timeout | 10 | The request timeout in seconds |
| HTTP method | POST | The requests is made as a POST |
| Headers | `X-Api-Key`
`X-Enterspeed-Webhook-Name`
`X-Enterspeed-System` | The following headers will be send with the request.
X-Api-Key will hold a key so that the client can validate that the request is actually coming from Enterspeed
X-Enterspeed-Webhook-Name will hold the name of the webhook
X-Enterspeed-System will hold the version number of the webhook |
### Payload
The request will send the following data.
```json theme={null}
{
"Id": "gid://Environment/2052b78d-6c34-4f11-bea5-296cf2d26968/Source/053b598b-c3d1-46fb-91e3-53115169cdb2/Entity/1234/View/product", // the Enterspeed view id
"OriginId": "1234", // the origin id of the entity
"Type": "product", // mapping schema alias
"Action": "Deploy", // can have the value of Deploy or Remove
"Url": "https://weu.delivery.enterspeed.com/v2?id=gid://Environment/40bb2d76-3b71-4121-b9b6-238cf4f325c4/Source/9e78f134-cf84-4ec5-9180-0b72b94949be/Entity/1099-en-us/View/home" // the absolute url for the delivery api to fetch the view
}
```
# Domains & hostnames
Source: https://docs.enterspeed.com/enterspeed/key-concepts/domains-hostnames
Domains and hostnames are used to route your delivery API requests to ensure the correct data is returned. This is especially important if you have a multiple-site setup.
## Environment clients
Environment clients are always linked to exactly one environment and provide an API key to be used in the delivery API request.
## Domains
Domains are site-specific and not Environment client specific. This means that an Environment client can have multiple domains as an Environment client can point to an environment with multiple sites. This could be the case if you, e.g. have a single CMS installation containing multiple sites.
Domains are site-specific and should only contain hostnames related to one specific site.
## Hostnames
A domain can have multiple hostnames if the same site is available on multiple hostnames.
Don't use the same domain for hostnames associated with different sites, as domains are site specific.
Using the same domain for hostnames associated with different sites can result in data from the wrong site being returned from the delivery API if two source entities have the same relative path.
## Relative URL's
If you only have one site, you could skip the hostnames and use the relative URL's instead. This will, behind the scene, create a hostname with the value of `root.tld`, making it possible to use relative URL's when requesting the delivery API.
# Index schemas
Source: https://docs.enterspeed.com/enterspeed/key-concepts/index-schemas
This page gives you an introduction to the concept of indexes. Go to [schema references](/reference/js/index-schema/intro) to find the API documentation for index schemas.
An index schema, as the name implies, creates an index, and more precise a search index, for you to query using the [Query API](/api-reference/query).
An index schemas has two responsibilities:
1. Defining the index, including the name and the fields for the index
2. Mapping source entities that goes into the index as items
## Use cases
An Enterspeed search index has multiple obvious use cases.
### Lists
With an index you can create large lists of data that you can filter, sort and paginate using the Query API in you frontend application, integration or whereever you are using the Query API.
Full and partial schemas are not build for larger lists of data from multiple source entities as they generate static views and require reprocessing everytime one of the items in the list is updated. On top of that they don't support dynamic lists and pagination.
### Fetching data dynamic
With an index you can fetch a single or multiple items based on multiple dynamic parameters as opposed to full schema where you can create static handles to fecth a view.
Fetching a view by handle is still more efficient and performant than querying an index. So only use search indexes when needed.
## Configure an index schema
When using an index schema, you need to define the types for each fields, which impact the way the fields can be queried.
In the below examples, we are creating a product index and querying it from the Query API.
## Examples
Example of an index schema for products
```js title="Index schema example" theme={null}
/** @type {Enterspeed.IndexSchema} */
export default {
triggers: function(context) {
context.triggers('pim', ['product'])
},
index: {
fields: {
sku: { type: "keyword" },
title: { type: "text" },
price: { type: "float" },
category: { type: "keyword" }
}
},
properties: function (sourceEntity) {
return {
sku: sourceEntity.properties.sku,
title: sourceEntity.properties.title,
price: sourceEntity.properties.price.salesPrice,
category: sourceEntity.properties.category.name
}
}
}
```
Query API request example for the product index.
```json title="Qeury API request for the product index" theme={null}
{
"filters": {
"and": [
{
"field": "category",
"operator": "qeuals",
"value": "Caps"
},
{
"field": "price",
"operator": "lessThan",
"value": 200
}
]
},
"sort": [
{
"field": "price",
"order": "asc"
}
],
"pagination": {
"page": 0,
"pageSize": 20
}
}
```
See more about the [Query API](/api-reference/query).
# Overview
Source: https://docs.enterspeed.com/enterspeed/key-concepts/overview
To understand the concept of Enterspeed, you would have to look at it as a 3 step process.
1. Ingest data
2. Transform data
3. Delivering data
## Ingesting data
To get started ingesting data we need to do a few things first.
### Environment settings
The first thing to do is ensure that we have an environment available.
Enterspeed will automatically create two environments for you. You can view them under Environment settings in the [settings section](https://app.enterspeed.com/settings/environment-settings)
You can create new environments, edit the name of your environments or delete them. Beware of deleting environments, since this is an irreversible action that will remove all data attached to the environment.
### Data source settings
Then we also need to prepare Enterspeed, so your tenant can receive data. This is done by creating a data source. Data sources are where a connection is created to your CMS, PIM-system, or perhaps a development instance of your CMS.
Go to [data sources](https://app.enterspeed.com/settings/data-sources) and create a data source group. (e.g. Demo CMS), and your data sources.
You should now have an API key available for you. The data source API key is a unique key used for both authentication when pushing data and also as an id for the data source that you will be pushing source entities to.
Read more about data sources and how to manage them [here](/docs/getting-started/data-sources.md).
### Preparing your system
You will need a way to ingest data into Enterspeed from your source system.
We have multiple options to get started with pushing source entities to Enterspeed.
1. Through our API. Find the [API documentation here](/api-reference/ingest).
2. Premade [connectors](https://docs.enterspeed.com/integrations) so you can get started immediately.
3. Our .NET SDK. [https://github.com/enterspeedhq/enterspeed-sdk-dotnet](https://github.com/enterspeedhq/enterspeed-sdk-dotnet)
```json title="Example of data sent to the ingest API" theme={null}
{
"type": "product",
"url": "https://enterspeed.com/product-enterspeed-tshirt-old-xs/",
"originParentId": "123",
"redirects": ["https://enterspeed.com/product-enterspeed-tshirt-old/"],
"properties": {
"name": "Official Enterspeed T-shirt",
"price": 199.99,
"inStock": true,
"features": [
{
"name": "color",
"value": "blue"
},
{
"name": "size",
"value": "M"
}
],
"information": {
"short": "Nice t-shirt",
"long": "Nice t-shirt in cotton"
}
}
}
```
### Source Entities
When the above steps have been applied successfully you are ready to push data to Enterspeed.
Data in Enterspeed is called Source Entities. Source Entities conform to a specific format. The important thing to know about source entities is that these are not representing the final output of your Enterspeed routes, but should be seen as the data that is available for you to work with and [transform](#transforming-data) to your needs through your schemas.
```json title="Source entity example" theme={null}
{
"id": "1044-en-us",
"type": "frontPage",
"url": "https://www.example.com/",
"properties": {
"title": "Welcome",
"description": "description value"
}
}
```
## Transforming data
### Schemas
The data now exists as source entities in Enterspeed and can be formed and modeled easily with data mapping in Enterspeed schemas.
Read more about [schemas](/enterspeed/key-concepts/schemas).
```js title="Schema" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbraco', ['frontPage'])
},
properties: function (sourceEntity, context) {
return {
headline: sourceEntity.properties.title,
description: sourceEntity.properties.description,
}
}
}
```
### Routing
Routing is set up in Schemas and is a part of setting up schemas and API's. We currently offer two ways of setting up routing:
* [Url routing](/enterspeed/key-concepts/schemas)
* [Handles](/enterspeed/key-concepts/schemas)
### Partial Schemas
Partial schemas are a bit different from the typical schema.
A partial schema is a reusable schema that is used across multiple schemas. A typical use case is when you want a specific data structure and type of data across many schemas.
You can read more about partials [here](/enterspeed/key-concepts/partial-schemas) and how it is used.
### Actions
Actions are used when a new view has been generated from a schema. It defines which specific actions to take following the newly generated view.
Currently, Enterspeed supports triggering the `process` of another schema.
Imagine the following.
You have a *product* and *category* source entity type.
When you ingest a *product*, the list of products should be updated in the generated category view and include the changes. This is a typical use-case scenario for actions.
Read more about [Actions](/enterspeed/reference/js/full-schema/actions)
### References
Referencing another schema.
The [Reference](/reference/json/property-types#reference) property type is a bit different than the [Partial](/reference/json/property-types#partial) property type.
The [Reference](/reference/json/property-types#reference) property allows referencing other views created from either this Source Entity or from another source entity and the Partial property can only use data from this Source Entity.
A benefit of a reference field is that the referenced view is resolved when requested by the Delivery API. There is no need the update the requested view if a referenced view has been updated.
Read more about the Reference property [here](/enterspeed/key-concepts/referencing-schemas) with a more in-depth explanation and examples.
### Views
A view should be considered as the output. Data is mapped in a Schema from source entities. When a source entity is created, updated or deleted, all schemas that are set up for the type of this source entity will create, update or delete the view accordingly.
In short, the view is the response when calling the Delivery API.
## Delivering data
Your data has now been ingested as source entities and transformed using schemas.
An example of calling the delivery API can be found [here](/enterspeed/tutorials/umbraco-nextjs/fetching-data-in-nextjs).
```js title="JavaScript example of calling the delivery API" theme={null}
const call = async (query, preview) => {
const url = `https://delivery.enterspeed.com/v1?${query}`;
const response = await fetch(new Request(url), {
headers: {
"Content-Type": "application/json",
"X-Api-Key": preview
? process.env.ENTERSPEED_PREVIEW_ENVIRONMENT_API_KEY
: process.env.ENTERSPEED_PRODUCTION_ENVIRONMENT_API_KEY,
},
});
return response.json();
};
export const getByHandle = async (handle, preview) => {
const response = await call(`handle=${handle}`, preview);
return response.views[handle];
};
export const getByUrl = async (url, preview) => {
const response = await call(`url=${url}`, preview);
return response.route;
};
```
### Environment clients
You need to set up an environment client.
Navigate to Environment clients under Settings -> Environment settings and click the Create button. Give your environment client a name (e.g. My My Client Application) and select one of the environments that you have created. Afterward, click on the Create button.
A Delivery API key has now been created. Copy and save it for use in your client application.
Next to your environment client, select edit domains. Select the Domain and press save changes.
You should now be ready to call the Enterspeed Delivery API. Go to the [docs](/api-reference/delivery) for requirements regarding this. We also have an example [here](/enterspeed/tutorials/umbraco-nextjs/4-fetching-data-in-nextjs)
### Final notes
We would suggest going to the [tutorials section](/enterspeed/tutorials/overview) for examples and some in-depth samples of getting started.
# Partial schemas
Source: https://docs.enterspeed.com/enterspeed/key-concepts/partial-schemas
This page gives you an introduction to the concept of partial schemas. Go to [schema references](/reference/js/partial-schema/intro) to find the API documentation for partial schemas.
Partial schemas are a bit different from the typical schema.
A partial schema is a reusable schema that is used across multiple schemas. A typical use case is when you want a specific data structure and type of data across many schemas.
The difference between using a partial schema versus referencing another schema is that a referenced schema is stored as a separate view.
Using a partial schema with your schema gives the output of a single view when processed.
Partial schemas are typically used when you want a reusable schema for mapping data that is part of the same entity. Eg. metadata (title, description, ...) is the same across different entity types but the data lives on the entity itself.
Don't use partial schemas for mapping data from another entity that's being referenced, as your views can be outdated if the referenced entity is updated. In this case, you should use [reference schemas](/enterspeed/key-concepts/referencing-schemas).
## Configure a partial schema
When using a partial schema, you are giving it a data object. This data object is typically the value of a specific part of the data source you are working on.
In the below example, we are using a partial schema called `seo`, and passing it the value of `p.seo` (a complex JSON object with SEO properties and data)
## Examples
Example of a schema that is referencing a partial schema
```js title="Schema" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
properties: function (sourceEntity, context) {
return {
headline: sourceEntity.properties.title,
seo: context.partial('seo', sourceEntity.properties.seo)
}
}
}
```
Example of the partial schema being used by the schema.
```js title="Schema" theme={null}
/** @type {Enterspeed.PartialSchema} */
export default {
properties: function (input, context) {
return {
metaTitle: input.seoTitle,
metaDescription: input.seoDescription
}
}
}
```
The data source used in this example.
```json title="Data source" theme={null}
{
"sourceId": "gid://Source/bfb8fd65-35d7-48d1-94bc-df0da13469d2",
"id": "gid://Source/bfb8fd65-35d7-48d1-94bc-df0da13469d2/Entity/1103",
"type": "product",
"originId": "1103",
"originParentId": "1098",
"url": "http://localhost:57152/products/bowling-ball/",
"redirects": [],
"properties": {
"title": "Bowling Ball",
"seo": {
"seoTitle": "Bowling Ball",
"seoDescription": "A bowling ball is a hard spherical ball used to knock down bowling pins in the sport of bowling. Balls used in ten-pin bowling and American nine-pin bowling traditionally have holes for two fingers and the thumb."
}
}
}
```
More examples can be found here [here](/enterspeed/tutorials/umbraco-nextjs/3-designing-your-apis#example-schemas--partial-schemas)
# Redirects
Source: https://docs.enterspeed.com/enterspeed/key-concepts/redirects
When changing the URL of a page in a CMS or moving a product to another category, you often want to have that old URL redirected to the new URL.
In Enterspeed you can ingest incoming redirects on your source entities. You do that by using the `X-Enterspeed-Redirects` header in your ingest request or as part of the object in the root property called `redirects` as described in the [API documentation](/api-reference/ingest/save-entity).
Once ingested you can see the redirects on the source entity in the Enterspeed app.
```json title='Source entity with redirect' theme={null}
{
"sourceId": "gid://Source/bce0dd5f-f371-4d5d-b95f-3ed427ec312d",
"id": "gid://Source/bce0dd5f-f371-4d5d-b95f-3ed427ec312d/Entity/1118-en-us",
"type": "contentPage",
"originId": "1118-en-us",
"originParentId": "1056-en-us",
"url": "http://mydomain.com/new-url",
"redirects": ["http://mydomain.com/old-url"],
"properties": {}
}
```
By default, if a schema has a single URL mapped as route, these redirects will automatically be applied to the view as implicit redirects.
If you are using JavaScript schemas you also have the option clear the implicit redirects and create your own explicit redirects.
Implicit redirects are cleared if you map more than one URL, as Enterspeed don't know which one of the URLs the implicit redirects should point to, or if you set explicit redirects in your schema.
Using explicit redirect gives you the option to dynamically build the redirects in your schema and especially if you have multiple URLs for a view you want to specify which of the URLs the different redirects should point to.
```js title="Example of explicit redirects" theme={null}
routes: function(sourceEntity, context) {
context
.url('http://mydomain.com/new-url')
.redirects(['http://mydomain.com/old-url']);
context
.url('http://mydomain.com/another-url');
}
```
[See redirect API documentation](/enterspeed/reference/js/full-schema/routes#url)
No matter if you use implicit or explicit redirects the delivery response will be the same if you request a view on a redirect URL. From the delivery response you will get a redirect response instead of a 404 error.
```json title='A request to the old URL returns a redirect response' theme={null}
{
"meta": {
"status": 301,
"redirect": "http://mydomain.com/new-url",
"missingViewReferences": []
},
"views": null
}
```
# Referencing schemas
Source: https://docs.enterspeed.com/enterspeed/key-concepts/referencing-schemas
The reference property allows referencing other views created from either its Source Entity or from another Source Entity.
Read more about reference property for JSON schemas [here](/reference/json/property-types#reference) or for JS schemas [here](/reference/js/full-schema/properties#reference).
Reference schemas are typically used when you are mapping data from another entity. E.g. a page has a reference to another page entity or media entity.
The reason why you would use reference schemas when mapping data from two different entities is to make sure your views are fully updated if one of the entities is updated.
Let's say you have a page schema that triggers on the page entity type. This means that every time the page is ingested the schema is processed and the view is updated.
Inside the schema, you are mapping the url and name of an image entity that the page is referencing, either directly in the schema or by using a partial schema.
Now, what happens if the image entity is ingested again with a new url or name? Nothing, because our page schema only triggers when a page is ingested.
This means that your page view with the image data is outdated, which is not good. That's why you use reference schemas when mapping data from other entities.
References are resolved on delivery request time. This means that if you have a page schema being processed every time a page is ingested with a schema reference to the image and an image schema being processed every time an image is ingested, then when you make a delivery request to the page, the views are merged together and you view data is always up to date.
Keep in mind, that you can only avoid reprocessing the referencing schema (the page schema) if you build the reference using `byOriginId` or `byOriginIds`. If you build the refernce using the `filter` function, you will still need to reprocess the referencing schema. You can read more about why that is and the reprocessing in general [here](/enterspeed/key-concepts/reprocessing)
## Use cases
Here is a list of examples of how to use the reference property and how it's used to reference schemas.
[Site settings](/enterspeed/reference/schema-example-library/site-settings)
[Breadcrumb navigation](/enterspeed/reference/schema-example-library/breadcrumb-navigation)
[Latest news](/enterspeed/reference/schema-example-library/latest-news)
# Reprocessing
Source: https://docs.enterspeed.com/enterspeed/key-concepts/reprocessing
This page gives you an introduction to the concept of reprocessing. Go to [schema references](/reference/js/full-schema/actions#reprocess) to find the API documentation for reprocessing.
When Enterspeed preprocesses views - which happens either when source entities are ingested or when a schema is deployed -, we also need to reprocess views whenever dependencies are updated or deleted.
Let's say you have a `product` source entity ingested from a PIM system and you want to create a schema to map the product and to enrich the product with some related `contentBlock`s from a CMS system.
The schema could look somewhat like this.
```js title="Schema with dependencies" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('pim', ['product']);
},
properties: async function (sourceEntity, context) {
const relatedContent = async context
.lookup(`type eq 'contentBlock' and properties.sku eq '${sourceEntity.properties.sku}'`)
.SourceGroup('cms')
.toPromise();
return {
sku: sourceEntity.properties.sku,
name: sourceEntity.properties.name,
relatedContent: relatedContent.map((c) => {
return {
title: c.properties.title,
content: c.properties.content
}
})
};
}
}
```
For this schema, the `product` is the trigger and the `contentBlock`s, which we get from the `lookup`, are the dependencies.
The trigger on the `product` source entity makes sure that the schema is triggered every time the `product` is updated, and our view will then be updated too. But what if the related `contentBlock`s are updated, added, or removed? Well, schemas don't automatically trigger when dependencies change, so in this case the `product` schema will not be triggered, and our view will not be updated. We need to handle that in the schemas ourselves.
Before looking into how we can update views when dependencies are changed, let's first take a deeper look at the dependencies and the different types of dependencies we have in Enterspeed.
## Dependencies
In Enterspeed we have two types of data available in a schema.
The first one is source entity that triggers the schema - and this one is passed as a parameter to the different functions in the schema like the [routes function](/reference/js/full-schema/routes), and the [properties function](/reference/js/full-schema/properties).
The second one is dependencies, and dependencies can then be broken further down in two types, [lookups](/reference/js/full-schema/properties#lookup) and [references](/reference/js/full-schema/properties#reference).
### Lookups
When you do a lookup in a schema, the result of the lookup is a list of source entities. As you get the raw source entities with all the data right in the schema, it gives you full flexibility of how you want to map out your data, but that flexibility also comes with a downside. Since you get the raw source entities right in your schema, it means that the data you map out is embedded directly into the view you create and because of that we always need to reprocess the view if one of the dependencies (one of the source entities from the lookup) is updated.
```js title="lookups return a list of source entities" theme={null}
properties: async function (sourceEntity, context) {
const relatedContent = async context
.lookup(`type eq 'contentBlock' and properties.sku eq '${sourceEntity.properties.sku}'`)
.SourceGroup('cms')
.toPromise();
return {
sku: sourceEntity.properties.sku,
name: sourceEntity.properties.name,
relatedContent: relatedContent.map((c) => {
return {
title: c.properties.title,
content: c.properties.content
}
})
};
}
```
When working with dependencies from a `lookup` call, you always need to reprocess the referencing schema when the dependencies are updated.
### References
As the name implies, references don't bring the dependent data directly into the schema, but just a reference to views. With references you can create reusable schemas and reference the views created by that schema across multiple other schemas.
As the referenced views are not directly embedded into the referencing view it also means that it's not always neccessary to reprocess the referencing view.
To understand exactly how references works, let's take a look at a view containing references.
```json title="View with two references" theme={null}
{
"sku": "p-1234",
"name": "Nike running shoe",
"relatedContent": [
{
"view": null,
"id": "gid://Environment/8b1c6be0-535d-4d6c-b690-a7e3590a0643/Source/e3211d99-e496-4e11-af3d-328eb543619g/Entity/1106-en-us/View/contentBlock",
"$type": "ViewReference"
},
{
"view": null,
"id": "gid://Environment/8b1c6be0-535d-4d6c-b690-a7e3590a0643/Source/e3211d99-e496-4e11-af3d-328eb543619g/Entity/1107-en-us/View/contentBlock",
"$type": "ViewReference"
}
]
}
```
Looking at the view we can see that references are not resolved yet; it's still just references to specific view ids. First when you request the view from the Delivery API, the Delivery API will make sure to resolve the references and return the content of the referenced views instead of the internal references with the view ids as you see above.
#### References by filter needs reprocess
Now this is all good. It means that if you update one of the referenced views, you don't need to reprocess the referencing view. But what happens if you ingest a new `contentBlock` source entity or deletes a source entity that matches the `sku` value from the `product`? Then the list of references is not updated, because even though the data of the references isn't stored in the view, it *does* store the result of which views to reference from the `filter` function.
Here's an example of the same schema as above, but with references instead of lookup.
```js title="schema with references using filter" theme={null}
properties: function (sourceEntity, context) {
return {
sku: sourceEntity.properties.sku,
name: sourceEntity.properties.name,
relatedContent: context.reference('contentBlock')
.filter(`type eq 'contentBlock' and properties.sku eq '${sourceEntity.properties.sku}'`)
.SourceGroup('cms')
};
}
```
Whenever you create references based on the `filter` function, you still need to reprocess the referencing view if you want the list of references to be updated when new dependencies are ingested or existing once are deleted, just like with the `lookup` function.
In other words, when the referenced source entities holds to reference key, in this case the `contentBlock`s has the reference to the product via the `sku` property, you need to search using the `filter` function and you need to reprocess the referencing view.
When working with referenced dependencies from a `filter` call, you always need to reprocess the referencing schema when the dependencies are updated.
#### References by origin id(s) doesn't needs reprocess
Let's look at another example where the relationship is turned around and referencing source entity holds the information about the relationship.
```js title="schema with references using origin ids" theme={null}
properties: function (sourceEntity, context) {
return {
sku: sourceEntity.properties.sku,
name: sourceEntity.properties.name,
relatedContent: context.reference('contentBlock')
.byOriginIds(sourceEntity.properties.relatedContentIds)
.SourceGroup('cms')
};
}
```
In this case the triggering source entity (the `product`) now has the knowledge about the relationship and knows the ids of the `contentBlocks`. This means that we can create the references by using the `byOriginIds` function and that whenever the `product` is updated with new relationships it will simply trigger the referencing schema so we don't need any reprocessing.
When working with referenced dependencies from a `byOriginId` or `byOriginIds` call, you never need to reprocess the referencing schema when the dependencies are updated.
## Reprocess actions
We've talked about dependencies, the different types of dependencies, when you need to reprocess, and when you *don't* need to reprocess. So, we just need the last part - How do we reprocess other schemas when a dependency changes?
To do that we use the [action](/reference/js/full-schema/actions) and the [reprocess](/reference/js/full-schema/actions#reprocess) functions.
Here's an example of a `contentBlock` schema that will reprocess the `product` schema for the specific `sku` value whenever the `contentBlock` is updated.
```js title="Schema with reprocess action" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['contentBlock']);
},
action: function(sourceEntity, context) {
context.reprocess('product')
.byOriginId(sourceEntity.properties.sku)
.sourceGroup('pim');
}
properties: function (sourceEntity, context) {
return {
title: sourceEntity.properties.title,
content: sourceEntity.properties.content
};
}
}
```
Reprocess actions can cause execessive processing, especially if you reprocess more than needed. This could result in a larger queue of jobs, and it will take longer for all your views to be updated.\
Because of that, it's important to make your reprocess actions as precise as possible, by using `originId` or a precise `filter` so you only target the schemas and source entities you actually need to reprocess.
# Routing
Source: https://docs.enterspeed.com/enterspeed/key-concepts/routing
This page gives you an introduction to the concept of routing. Go to [schema references](/reference/js/full-schema/routes) to find the API documentation for routing.
You can do routing by either Url or Handle.
## URL
If you want your schema to be routable by an URL, you can specify the `url` as an expression.
If we take a look at this example we can see that we have a Url property available in the Data Source
```json title="Source entity example" theme={null}
{
"id": "1044-en-us",
"type": "frontPage",
"url": "/frontPage",
"properties": {
"title": "Welcome",
"description": "description value"
}
}
```
In the below example of the schema for this Data Source, we can see that the route is mapped to the url property on the Data Source. This means that you can get the data from this source
by making a request to `https://delivery.enterspeed.com/v1?url=/frontpage`
```js theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbraco', ['frontPage'])
},
routes: function(sourceEntity, context) {
context.url(sourceEntity.url)
},
properties: function (sourceEntity, context) {
return sourceEntity.properties
}
}
```
**What does "No environment client configured to support domain name for source entity url /relative-url/" mean?**
If you are using relative URLs (*e.g. /about-us/*) and are trying to test the schema by making a CURL request, you might have seen this error message.
The reason for this is that the URL doesn't have a domain that matches an environment client. Environment clients need to be able to match the URL in the source entity with the hostname provided for the environment client.
The best way to solve this is to use absolute URLs in your schema (*e.g. [https://my-domain.com/about-us/](https://my-domain.com/about-us/)*).
If you however want to use relative URLs, it can be done by adding a domain to your environment client with the hostname `root.tld`.
## Handle
Handle differentiates a bit from URL routing. A handle can be whatever you would like.
In this example, a navigation structure is returned. The schema returns an array of navigation items and utilizes the lookup and [reference](/enterspeed/key-concepts/referencing-schemas) fields.
This handle would be called like this: `https://delivery.enterspeed.com/v1?handle=mainNavigation`
```js theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbraco', ['navigationGroup'])
},
routes: function(sourceEntity, context) {
context.handle('mainNavigation')
},
properties: function (sourceEntity, context) {
return {
children: context.reference('navigationItem')
.children()
.orderBy({
propertyName: 'properties.metaData.sortOrder',
direction: 'desc'
})
}
}
}
```
# Schemas
Source: https://docs.enterspeed.com/enterspeed/key-concepts/schemas
The schemas define the API endpoint and data structure for your data. This is where decoupling is happening since you are specifying the new structure of your [source entities](/enterspeed/key-concepts/source-entities) through schema mappings, and generating [views](/enterspeed/key-concepts/views) based on these mappings.
### How?
A schema acts as a middleman, grabbing the data from the source entities and generating the view with a data structure that is based on the schema definitions.
[Views](/enterspeed/key-concepts/views) are the result of the above, and what we receive as a response when calling the Enterspeed API.
Schema is key in setting up and defining API's, property mapping, [routes](/enterspeed/key-concepts/routing), [actions](/enterspeed/reference/js/full-schema/actions) and more.
## Important topics
[Schema docs](/enterspeed/reference/js/full-schema/intro)
[Referencing schemas](/enterspeed/key-concepts/referencing-schemas)
[Actions](/enterspeed/reference/js/full-schema/actions)
[Routing](/enterspeed/key-concepts/routing)
[Partial schemas](/enterspeed/key-concepts/partial-schemas)
[Reprocessing](/enterspeed/key-concepts/reprocessing)
## Tutorials
[Schema snippets](/enterspeed/reference/snippets)
[Designing a schema](/enterspeed/transform/designing-a-schema)
[Multilevel navigation example](/enterspeed/tutorials/multilevel-navigation/1-getting-started)
[Umbraco & Next.js example](/enterspeed/tutorials/umbraco-nextjs/3-designing-your-apis)
# Source Entities
Source: https://docs.enterspeed.com/enterspeed/key-concepts/source-entities
When pushing to Enterspeed from your source system, data exists in Enterspeed as Source Entities. Source Entities are not a representation of the final output. They should be seen as the data that is available for you to work with.
The important thing to remember is that we are not going to see the output of the Source Entities, since you are specifying the output of your Source Entities through schema mappings. These schema mappings generate views of data, based on the mappings that you have done in the schema.
Read more about [schemas](/enterspeed/key-concepts/schemas) and how to get started working with mapping data from your Source Entities.
# Source Groups
Source: https://docs.enterspeed.com/enterspeed/key-concepts/source-groups
Source groups contains the sources you ingest your source entities into. Typically you have a source per environment inside a source group.
## Modes
Source groups works in two different modes. Modes are selected on source group creation and can't be changed afterwards.
### Schema Transformation (default)
Schema Transformation is the default and recommended mode for most scenarios. It enables you to transform and combine source entities into views or index items using defined schemas.
Once processed, views can be accessed via the [Delivery API](/api-reference/delivery), while index items are available for querying through [Query API](/api-reference/query).
The schema transformation is a powerfull feature, allowing you to tailor and structure data precisely to meet consumer needs. However, because the transformation process takes time, this mode is best suited for non-real-time data.
### Auto Indexing
Auto Indexing mode are currently in preview.
On Auto indexing mode there is no transformation process. Instead, ingested source entities goes directly into an index, which can be queried using the [Query API](/api-reference/query/query-items-from-auto-indexing-source-groups).
Since no transformation is involved, the Auto Indexing mode is suited for scenarios such as handling large volumes of small, custom price objects that don't require transformation, or frequently changing data like stock numbers.
For each source entity type you want to query, you add a type mapping. You name it after the source entity type it indexes — for example `price` — and define the fields and their data types in JSON, as in the example below.
```json theme={null}
{
"fields": { // the fields that are added to the index
"sku": {
"type": "keyword",
"metadata": {
"description": "The product key"
}
},
"price": {
"type": "float",
"metadata": {
"description": "The sales price of the product"
}
},
"validFrom": {
"type": "date",
"metadata": {
"description": "A from date where the price is active"
}
},
"validTo": {
"type": "date",
"metadata": {
"description": "A to date where the price is active"
}
}
},
"metadata": {
"description": "The price of a product for a specific time interval"
}
}
```
The type name isn't part of this JSON — you set it on the mapping itself. Add one mapping per source entity type you want to query, and add more whenever you need them.
Only top-level properties holding a single value, or a list of single values, can be indexed. Nested objects, and lists of objects, are never indexed and can't be added to a mapping.
Properties that aren't defined in the type's fields aren't added to the index, so you can't query them. Your source entities are stored in full, so adding an indexable property to the mapping later populates it for the entities you have already ingested — you don't need to re-ingest them.
You can add, change, and remove type mappings after the source group is created. See [Managing type mappings](#managing-type-mappings).
## Types
Setting the right types for the properties in your index is important. The types defines the intend of the fields and prevents data of other types from going into the index. The types also helps with effeciently index, search, and analyze of the data added to the index.
### Text
| Type | Description |
| ------- | -------------------------------------------------------------------------------------------------- |
| keyword | The `Keyword` type is for string values used in filtering and sorting |
| text | The `text` type will analyze string values, improving results when querying using full text search |
### Numeric
| Type | Description |
| ------- | ------------------------------- |
| integer | A 32 bit signed integer |
| long | A 64 bit signed integer |
| float | A 32 bit signed floating number |
| double | A 64 bit signed floating number |
### Boolean
| Type | Description |
| ------- | ------------------------ |
| boolean | A `true` / `false` value |
### Date
| Type | Description |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| date | Support values like a Javascript Date object, a string like `2024-11-21` or `2024-11-21T23:00:00Z` or number of milliseconds since eclipse |
### List
| Type | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| keyword\[] | List of the `Keyword` type for string values used in filtering and sorting |
| text\[] | List of the `text` type for analyzing string values, improving results when querying using full text search |
| integer\[] | List of 32 bit signed integers |
| long\[] | List of 64 bit signed integers |
| float\[] | List of 32 bit signed floating numbers |
| double\[] | List of 64 bit signed floating numbers |
| boolean\[] | List of `true` / `false` values |
| date\[] | List of dates with supported values like a Javascript Date object, strings like `2024-11-21T23:00:00Z` or numbers of milliseconds since eclipse |
## Managing type mappings
You can add, change, and remove type mappings on an Auto Indexing source group at any time. Each type gets its own index, so a change to one type never affects the others.
### Adding a type
Add a mapping for a type that isn't indexed yet, and Enterspeed creates the index and fills it from the source entities you have already ingested. You don't need to re-ingest anything.
Indexing runs in the background, so it can take a moment before every entity is queryable.
### Changing a type
What happens when you save depends on what you changed.
| Change | What happens |
| ------------------------------------ | -------------------------------------------------------------------- |
| Descriptions and other metadata only | Written to the index straight away. Nothing is re-indexed. |
| Adding or removing a field | The index is rebuilt in the background. |
| Changing a field's type | The index is rebuilt in the background, if the change is compatible. |
Rebuilds are zero-downtime. Enterspeed builds a new version of the index alongside the live one and switches over once it has caught up, so your queries keep returning complete results throughout.
Because a rebuild re-indexes from the source entities Enterspeed already stores, adding a field also populates it for your existing entities — including properties that weren't in the index before.
### Compatible field type changes
A rebuild re-indexes your existing values under the new type, so a type change is only allowed when every value can survive it. Adding and removing fields is always allowed.
| From | To |
| ------------------------------------------------------- | ---------------------------------------------------- |
| `integer` | `long`, `double` |
| `long` | `double` |
| `float` | `double` |
| `keyword` | `text` |
| `integer`, `long`, `float`, `double`, `boolean`, `date` | `text`, `keyword` |
| any type | a list of that type, or of any type it can change to |
Converting a number or date to `text` or `keyword` keeps the value but loses range and sort behaviour, so filter on it as a string afterwards.
These changes are rejected, because existing values would be lost:
* Narrowing a number, such as `double` to `integer`
* `text` to `keyword`, since a `keyword` value is capped at 32,766 bytes and longer text would be rejected outright
* A list to a single value, such as `keyword[]` to `keyword`
* Switching between unrelated types, such as `date` to `integer`
### Replacing a type
Sometimes you need a change that the rules above reject, but your data supports it anyway. Say you mapped a price field as `text` and it should have been `double` — that change is rejected because a `text` field *can* hold values that aren't numbers, not because yours does.
For cases like this you can replace the type instead. Enterspeed tells you which fields are the problem and offers this per type, so you can replace one type and leave the rest of your changes to go through normally.
Replacing rebuilds the index from your stored source entities, skipping the compatibility check. Every entity whose values fit the new type is kept — so if all your `text` values really are numbers, they all survive the change to `double`.
Any entity holding a value that doesn't fit is left out of the index entirely, not just that one field, so you can no longer query it. The entity itself is kept and is queryable again once re-ingested with a matching value — but this is exactly what the compatibility check normally protects you from, so make sure you know your data before replacing a type.
### Removing a type
Removing a mapping deletes the index for that type, so you can no longer query it.
Your source entities are kept. Entities of a removed type — or of any type on a source with no mappings configured yet — are marked **Not configured** in the source entities list, so you can see at a glance which ones aren't indexed. Add the mapping back later and Enterspeed fills the index again from the entities it already has.
# Views
Source: https://docs.enterspeed.com/enterspeed/key-concepts/views
A view is the output of the data that is mapped in a [Schema](/enterspeed/key-concepts/schemas), where the source of the data is a [Source Entity](/enterspeed/key-concepts/source-entities).
Views are reprocessed when a change in the Source Entity has happened.
It is possible to test the output of your schema mappings in your tenant from the administration. Navigate [here](https://app.enterspeed.com/views) for an overview of your generated views.
# Documentation MCP
Source: https://docs.enterspeed.com/enterspeed/mcp-server/documentation-mcp/overview
The Enterspeed Documentation MCP Server lets you search and retrieve content from the [docs.enterspeed.com](https://docs.enterspeed.com) site and even report back issues or suggestions to the content.
It turns the Enterspeed documentation into a set of [Model Context Protocol](https://modelcontextprotocol.io/) tools that AI clients (Claude, Cursor, custom agents, Azure AI Foundry, etc.) can discover and call. You do not need to host anything yourself.
## Hostname
| Environment | URL | Transport |
| -------------- | --------------------------------- | --------- |
| **Production** | `https://docs.enterspeed.com/mcp` | HTTP |
The documentation is public, so this server needs no authentication — there is no key to issue and nothing to scope. That is also why it is the one Enterspeed MCP server that works with browser-based clients.
## Installation
Simply click the arrow next to the Copy page button on the top right of this page and select **Connect to VS Code** and click the install button in VS Code.
Or you can install it yourself in the VS Code configuration.
VS Code's GitHub Copilot reads MCP servers from an `mcp.json` file. For a single workspace, put the config at `.vscode/mcp.json`; for every workspace, use the **MCP: Open User Configuration** command from the command palette.
```json theme={null}
{
"servers": {
"enterspeedDocs": {
"url": "https://docs.enterspeed.com/mcp",
"type": "http"
}
}
}
```
Reload the window (**Developer: Reload Window** from the command palette) and open the Copilot chat pane. The Enterspeed documentation tools appear in the tool picker once Copilot connects.
```bash theme={null}
claude mcp add enterspeedDocs \
--scope user \
--transport http \
https://docs.enterspeed.com/mcp
```
The user scope (`--scope user`) installs the MCP for the current user across all projects. Use `--scope project` to install it for the current project only but for all users (written to a checked-in `.mcp.json`).
The CLI stores the server configuration under `~/.claude.json`. Edit that file if you need to tweak anything afterwards.
Open your Claude Desktop config file:
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
Add the `mcpServers` entry:
```json theme={null}
{
"mcpServers": {
"enterspeedDocs": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://docs.enterspeed.com/mcp"
]
}
}
}
```
Fully quit and relaunch Claude Desktop. The Enterspeed doc tools now appear in the tool picker.
### Other MCP clients
The three clients above are the ones we test against regularly, but the MCP server is client-agnostic. Any MCP-capable client follows the same pattern — point it at `https://docs.enterspeed.com/mcp` with `http` transport.
Or simply just ask your favorite AI agent how to connect to the Enterspeed Documentation MCP Server. It should be able to generate the configuration for you.
# Overview
Source: https://docs.enterspeed.com/enterspeed/mcp-server/management-mcp/overview
Connect an AI client to the Enterspeed Management API — read your tenant configuration, author schemas, and create environments, sources, domains, and clients.
The Enterspeed Management MCP Server turns the Enterspeed Management API into a set of [Model Context Protocol](https://modelcontextprotocol.io/) tools that AI clients (Claude, Cursor, VS Code, custom agents) can discover and call. It is the configuration counterpart to the [Query MCP](/enterspeed/mcp-server/query-mcp/overview) server: where Query MCP reads your *data*, Management MCP reads and changes your *setup*.
You do not need to host anything yourself — Enterspeed runs the server for you.
Typical uses:
* Ask an agent to explain how a tenant is wired together — sources, environments, domains, clients, deployments.
* Have an agent write a mapping schema, dry-run it against a real source entity, and save a new version.
* Investigate a view or route that is not producing what you expect.
* Query your tenant logs and metrics in natural language.
## Hostname
| Environment | URL | Transport |
| -------------- | ---------------------------------------- | ------------------- |
| **Production** | `https://mcp.management.enterspeed.com/` | MCP Streamable HTTP |
A health check is available without authentication:
```bash theme={null}
curl -s https://mcp.management.enterspeed.com/health
# "ok"
```
The server speaks **MCP Streamable HTTP** on the root URL. Responses are delivered as a server-sent event stream on that same URL, so clients that label their transport *SSE* generally work when pointed at the root. There is no separate `/sse` endpoint — always configure the root URL.
## How authentication works
The Management MCP server is a **passthrough proxy**. It holds no credentials of its own. Your MCP client sends a Management API key on every request, the server forwards it unchanged, and the Management API makes every authorisation decision.
```
AI client ──x-api-key──► mcp.management.enterspeed.com ──x-api-key──► the Management API
│
validates the key and its permissions
```
Clients authenticate with a single HTTP header on every MCP request:
```
x-api-key: management-
```
The key is a **Management API key** issued from the Enterspeed Management App. It is *not* an environment client key — that is what [Query MCP](/enterspeed/mcp-server/query-mcp/overview) uses.
There is no query-string fallback on this server. The key must be sent as an `x-api-key` header, because a key in a URL ends up in access logs along the whole request path. If your client cannot set headers, see [Known limitations](#known-limitations).
### Use a read-only key for read-only work
Management clients have an access level — **read-write** or **read-only**. Because the server passes your key straight through, **the key decides what an agent can do** — the server never widens it.
We recommend the smallest key that does the job:
| What you want the agent to do | Recommended access level |
| ----------------------------------------------------------------- | ------------------------ |
| Explain, inspect, investigate, dry-run, validate | **Read-only** |
| Author schemas, create environments, sources, domains, or clients | **Read-write** |
A read-only key reaches every read tool — including both dry-run tools and schema validation, which are read-only despite sounding like they change something. If an agent tries a write tool with a read-only key, the Management API rejects the call and the agent gets a clear permission error. That is the intended outcome, not a fault.
**Management clients created in the Management App are read-write.** The app's create form takes a name only, and the API defaults to read-write when no access level is supplied. To issue a read-only key today, create the management client through the Management API and set the access level explicitly — see [Creating a Management API key](#creating-a-management-api-key) below.
## What the server can and cannot do
The server exposes **35 tools: 22 reads and 13 additive writes**.
* **Reads** cover tenant overview, sources and source entities, mapping schemas, deployments, views and routes, indexes and index documents, domains and hostnames, environment clients, logs, and metrics.
* **Writes** are **additive only** — create and update. They cover schema authoring (create a schema, save a version, deploy), environments, source groups and sources, domains, and environment clients.
**There are no delete tools, by design.** This is not a policy the server can be talked out of — it has no way to express a destructive call. Every request the server can make is a `GET`, or a `POST`/`PUT` that appears on a fixed allow-list checked when the server starts. No tool can delete a schema, environment, source, domain, client, or view.
The practical consequence: an agent connected to this server can create clutter you may need to tidy up manually, but it cannot destroy your configuration or your content.
See [Tools](/enterspeed/mcp-server/management-mcp/tools) for the full inventory with each tool marked read or write.
## Guardrails
Beyond the key's own permissions, the hosted server applies three limits you should know about.
On the hosted server, `deploy_mapping_schema` only deploys to environments **named** `dev`, `development`, `test`, or `staging`. A deploy targeting any other environment — including anything named `prod`, `production`, or `live` — is refused with a `deploy_not_permitted` error naming the environments that are allowed.
This is a restriction of the hosted server, not of your key. Renaming an environment to get around it does not work either: `update_environment` refuses a rename that would move an environment onto the allowed list.
Production deploys stay a deliberate human action in the Management App.
The server strips the `accessKey` field out of every response before your client sees it, at every level of nesting. This covers environment client keys and source ingest keys.
It applies even to `create_environment_client`, which mints a key: the tool creates the client, and the key value is not in the response. The agent is told a key was created and that it must be collected from the app — so it reports the right next step instead of inventing a value. **Fetch the key in the Management App** when you need it.
The field is removed rather than masked, so no tool can observe a key at any point.
Requests are counted per API key over a rolling one-minute window. Exceeding the limit returns:
```
HTTP 429 Too Many Requests
Retry-After:
Too many requests. Retry after seconds.
```
Well-behaved MCP clients back off and retry. The limit applies to the whole MCP endpoint, and `/health` is exempt.
For comparison, [Query MCP](/enterspeed/mcp-server/query-mcp/overview) allows 120 requests per minute per key.
## Security model
If you are reviewing this server before pointing an agent at your tenant, this is the short version:
* **No server-held credentials.** The server has no Enterspeed key of its own and no way to configure one. It can only act with a key you send it.
* **Passthrough authorisation.** Every permission decision is made by the Management API against your key, not by the MCP server.
* **No destructive verbs.** The server cannot issue a `DELETE` or a `PATCH` at all.
* **Secrets are not readable.** Access keys are removed from responses at the HTTP boundary, so no tool can observe one.
* **Production deploys are out of reach** on the hosted server.
* **Least privilege is yours to set.** Use a read-only key unless the agent genuinely needs to write.
## Creating a Management API key
1. Sign in to the **Enterspeed Management App**.
2. Go to **Settings → Management clients**.
3. Click to create a management client and give it a descriptive name, such as `claude-schema-authoring`.
4. Copy the key — it starts with `management-`. Store it in your secret manager immediately.
The key this creates is **read-write**. Use it when the agent genuinely needs to author schemas or create configuration.
To issue a read-only key, create the management client through the Management API and set the access level explicitly:
```bash theme={null}
curl -X POST https://management.enterspeed.com/api/v1/tenant/management-clients/ \
-H "X-Api-Key: management-your-existing-key" \
-H "Content-Type: application/json" \
-d '{ "name": "claude-read-only", "accessLevel": "ReadOnly" }'
```
The response contains the new key. Store it in your secret manager immediately.
Omitting `accessLevel` creates a **read-write** client, which is why the Management App's create form produces read-write keys today.
Give each agent its own key with a name you will recognise later. That way you can revoke one integration without disturbing the others.
## Connecting a client
The pattern is the same for every MCP client: point it at the root URL with **HTTP** transport and set an `x-api-key` header.
```bash theme={null}
claude mcp add enterspeed-management \
--scope user \
--transport http \
https://mcp.management.enterspeed.com/ \
-H "x-api-key: management-your-key"
```
The user scope (`--scope user`) installs the server for the current user across all projects. Use `--scope project` to install it for the current project only but for all users — that writes a checked-in `.mcp.json`, so use an environment variable for the key rather than pasting it.
Then start a session and ask about your tenant:
```bash theme={null}
claude
> Give me an overview of my Enterspeed tenant.
```
VS Code's GitHub Copilot reads MCP servers from an `mcp.json` file. For a single workspace, put the config at `.vscode/mcp.json`; for every workspace, use the **MCP: Open User Configuration** command from the command palette.
```json theme={null}
{
"servers": {
"enterspeedManagement": {
"type": "http",
"url": "https://mcp.management.enterspeed.com/",
"headers": {
"x-api-key": "management-your-key"
}
}
}
}
```
Reload the window (**Developer: Reload Window** from the command palette) and open the Copilot chat pane. The Enterspeed management tools appear in the tool picker once Copilot connects.
Never commit `.vscode/mcp.json` with a real key. Use `${input:enterspeed-management-key}` with a matching [`inputs` entry](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration) so VS Code prompts for the key on first use, or put the config in your user profile instead of the workspace.
Open your Claude Desktop config file:
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
Add the `mcpServers` entry:
```json theme={null}
{
"mcpServers": {
"enterspeedManagement": {
"type": "http",
"url": "https://mcp.management.enterspeed.com/",
"headers": {
"x-api-key": "management-your-key"
}
}
}
}
```
Fully quit and relaunch Claude Desktop. The Enterspeed management tools now appear in the tool picker.
Older Claude Desktop versions cannot open a remote MCP server directly from this file. If the server does not appear, use the `mcp-remote` bridge instead:
```json theme={null}
{
"mcpServers": {
"enterspeedManagement": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://mcp.management.enterspeed.com/",
"--header",
"x-api-key:management-your-key"
]
}
}
}
```
Any MCP-capable client follows the same pattern — the root URL, `http` transport, and an `x-api-key` header. Known-good examples:
* **Cursor** — `.cursor/mcp.json`, same schema as VS Code.
* **Windsurf** — settings → Cascade → MCP Servers, using the URL + header form.
* **Zed** — settings under `"context_servers"`.
Or simply ask your favorite AI agent how to connect to an MCP server that needs an `x-api-key` header. It should be able to generate the configuration for you.
## Known limitations
**Custom connectors on claude.ai in the browser are not supported yet.** Browser-based custom connectors cannot send a custom HTTP header, and this server requires `x-api-key`. Use Claude Code, Claude Desktop, VS Code, Cursor, or your own agent instead.
We are working on OAuth support to remove this restriction. Until it ships, a header-capable client is required.
## Sample prompts
Use these to smoke-test a fresh connection.
**Orientation**
> Give me an overview of my Enterspeed tenant — how many sources, environments, and domains do I have, and how are they connected?
**Schema investigation**
> List my mapping schemas and show me the current version of the one that produces my product pages. What triggers it?
**Authoring loop (needs a write-capable key)**
> Write an index schema that indexes my `article` entities with title, publish date, and author. Dry-run it against a real article first, then save it as a new version.
**Troubleshooting**
> The route for `/products/red-shoe` is not returning what I expect. Inspect the route and the view behind it and tell me what is wrong.
**Logs**
> Show me any processing errors in the last 24 hours, grouped by schema.
## Troubleshooting
| Symptom | Likely cause | Fix |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` on every tool call | Wrong key type — an environment client key instead of a Management API key | Use a key starting with `management-`. Environment client keys belong to [Query MCP](/enterspeed/mcp-server/query-mcp/overview). |
| Reads work, writes return a permission error | The key is read-only | Expected. Use a write-capable key only if the agent genuinely needs to create or update configuration. |
| `deploy_not_permitted` when deploying a schema | The target environment is not one the hosted server may deploy to | Deploy to `dev`, `development`, `test`, or `staging`, or deploy to production from the Management App. |
| `create_environment_client` succeeded but there is no key in the response | Working as intended — access keys are never returned through MCP | Fetch the key in the Management App. |
| `429` with a `Retry-After` header | More than 60 requests in a minute on this key | Let the client back off, or split work across keys. |
| Client cannot connect at all | Configured with a `/sse` path, or a proxy that buffers streamed responses | Use the root URL. Streamed responses must pass through unbuffered. |
## Next steps
* [Tools](/enterspeed/mcp-server/management-mcp/tools) — the full tool inventory, read and write.
* [Query MCP](/enterspeed/mcp-server/query-mcp/overview) — query your Enterspeed data instead of your configuration.
# Tools
Source: https://docs.enterspeed.com/enterspeed/mcp-server/management-mcp/tools
The full Management MCP tool inventory — 22 read tools and 13 additive write tools, with no delete tools by design.
The Management MCP server exposes **35 tools: 22 reads and 13 additive writes**. This page lists all of them so you can see exactly what an agent connected to your tenant is able to do.
The tool set is fixed — it is the same for every tenant and every key. What differs is what your key is *allowed* to call: see [Authentication](/enterspeed/mcp-server/management-mcp/overview#how-authentication-works).
**There are no delete tools, by design.** The server has no way to express a destructive request — every call it can make is a read, or a create/update on a fixed allow-list checked when the server starts. No tool can delete a schema, environment, source, domain, client, or view.
## Reading your tenant
### Orientation
Start here when you want an agent to understand how a tenant is put together.
| Tool | Type | What it does |
| ------------------------------- | ---- | ------------------------------------------------------------------------------------- |
| `get_tenant_overview` | Read | A single summary of the tenant — sources, environments, domains, and how they relate. |
| `list_sources` | Read | Lists the tenant's sources and source groups. |
| `list_environment_clients` | Read | Lists environment clients and their scopes. Access keys are never included. |
| `inspect_domains_and_hostnames` | Read | Domains and the hostnames mapped to them. |
### Source entities
| Tool | Type | What it does |
| ---------------------- | ---- | -------------------------------------------------------------------- |
| `list_source_entities` | Read | Lists ingested source entities, filterable by source group and type. |
| `get_source_entity` | Read | Fetches one source entity in full, as ingested. |
### Mapping schemas
| Tool | Type | What it does |
| ------------------------ | ---- | ------------------------------------------------------------------------------- |
| `list_mapping_schemas` | Read | Lists the tenant's mapping schemas. |
| `get_mapping_schema` | Read | Fetches a schema, including a specific version. |
| `search_mapping_schemas` | Read | Searches across schema content — useful for "which schema sets this property?". |
### Views, routes, and indexes
| Tool | Type | What it does |
| -------------------- | ---- | ------------------------------------------------------------------------------------------- |
| `list_views` | Read | Lists generated views. |
| `get_view` | Read | Fetches a single view's output. |
| `inspect_route` | Read | Resolves a route and shows the view behind it. The first stop for "why is this URL wrong?". |
| `inspect_indexes` | Read | Lists indexes and their field definitions. |
| `get_index_document` | Read | Fetches a single indexed document. |
### Deployments, logs, and metrics
| Tool | Type | What it does |
| ----------------------------- | ---- | ------------------------------------------------------------ |
| `get_environment_deployments` | Read | Deployment history for an environment. |
| `query_tenant_logs` | Read | Queries tenant logs — errors, warnings, processing activity. |
| `aggregate_tenant_logs` | Read | Aggregates logs, for example grouping errors by schema. |
| `get_tenant_metrics` | Read | Tenant-level metrics. |
## Authoring schemas
This group is the reason most people connect the server. The four read tools let an agent design and prove a schema before anything is written.
| Tool | Type | What it does |
| --------------------------------------- | --------- | ------------------------------------------------------------------- |
| `get_schema_type_definitions` | Read | The type definitions an agent needs to write a valid schema. |
| `dry_run_mapping_schema` | Read | Runs a schema without saving it and returns the output. |
| `dry_run_mapping_schema_against_entity` | Read | Same, against one of your real source entities. |
| `validate_mapping_schema_version` | Read | Validates a schema version without saving it. |
| `create_mapping_schema` | **Write** | Creates a new mapping schema. |
| `save_mapping_schema_version` | **Write** | Saves a new version of an existing schema. |
| `deploy_mapping_schema` | **Write** | Deploys a schema version to an environment. Restricted — see below. |
The dry-run and validate tools are reads, despite sounding like they change something. A read-only key can design, test, and validate a schema end to end — it just cannot save it. That makes a read-only key a good default even for authoring work, until you are happy with the result.
`deploy_mapping_schema` on the hosted server only deploys to environments **named** `dev`, `development`, `test`, or `staging`. Anything else — including environments named `prod`, `production`, or `live` — is refused with a `deploy_not_permitted` error. Production deploys stay a deliberate action in the Management App.
## Creating and updating configuration
All writes are **additive**: they create something new or update something that exists. None of them remove anything.
| Tool | Type | What it does |
| --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------- |
| `create_environment` | **Write** | Creates an environment. |
| `update_environment` | **Write** | Updates an environment. Will not rename an environment onto the deploy-allowed list. |
| `create_source_group` | **Write** | Creates a source group. |
| `update_source_group` | **Write** | Updates a source group. |
| `create_source` | **Write** | Creates a source. |
| `update_source` | **Write** | Updates a source. |
| `create_domain` | **Write** | Creates a domain. |
| `update_domain` | **Write** | Updates a domain, including its hostnames. |
| `create_environment_client` | **Write** | Creates an environment client. The generated access key is **not** returned — fetch it in the Management App. |
| `update_environment_client` | **Write** | Updates an environment client's scopes. |
**On an environment client, an empty index scope means *unrestricted*, not *no access*.** A partial update that dropped the field would therefore widen the client rather than narrow it. `update_environment_client` avoids this by reading the current client and merging your changes onto it, so an agent cannot silently remove an index restriction by omitting it — but it is worth knowing when you review what an agent changed.
## What is deliberately not exposed
Some Management API capabilities are intentionally absent from the tool set:
* **Anything that deletes.** No schema, environment, source, source group, domain, client, view, or index can be removed through MCP.
* **Key rotation.** No tool can regenerate an access key. Combined with the redaction applied to every response, that means an agent can neither read an existing key nor replace one.
* **Bulk and production deploy paths.** Deploying broadly, or to production, stays in the Management App.
For the update tools, the permission is the *verb*, not the address. Several of the allowed updates share an address with a delete operation in the underlying API — the server can reach the update and has no way to reach the delete.
## Next steps
* [Overview](/enterspeed/mcp-server/management-mcp/overview) — hostname, authentication, guardrails, and client setup.
* [Query MCP](/enterspeed/mcp-server/query-mcp/overview) — query your Enterspeed data instead of your configuration.
# Overview
Source: https://docs.enterspeed.com/enterspeed/mcp-server/overview
The Enterspeed MCP servers let AI clients query your data, manage your configuration, and search the documentation.
Enterspeed hosts three [Model Context Protocol](https://modelcontextprotocol.io/) servers. Point an AI client at one and it discovers a set of tools it can call on your behalf. You do not need to host anything yourself.
Ask questions about your data in Enterspeed using the MCP server for the Query API.
Inspect and configure your tenant — schemas, environments, sources, domains, and clients.
Search and retrieve documentation content or report back issues or suggestions.
## Which one do you need?
| If you want to… | Use |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| Query content, indexes, and source entities in an environment | [Query MCP](/enterspeed/mcp-server/query-mcp/overview) |
| Understand or change how a tenant is configured, or author schemas | [Management MCP](/enterspeed/mcp-server/management-mcp/overview) |
| Ask questions about Enterspeed itself | [Documentation MCP](/enterspeed/mcp-server/documentation-mcp/overview) |
Query MCP and Management MCP are complementary — many people connect both, so an agent can read a schema, change it, and then query the result.
## How authentication works
Both tenant-facing servers work the same way: they are **passthrough proxies that hold no credentials of their own**. Your client sends a key on every request, the server forwards it unchanged, and the underlying Enterspeed API makes every authorisation decision.
That means the key you issue is the whole boundary. For Query MCP, a key scoped to one environment and one index prefix gives an agent exactly that much and no more. For Management MCP, the key's access level decides whether an agent can change anything at all.
**Custom connectors on claude.ai in the browser are not supported yet.** Browser-based custom connectors cannot send a custom HTTP header, and both tenant-facing servers authenticate with one. Use Claude Code, Claude Desktop, VS Code, Cursor, or your own agent instead.
We are working on OAuth support to remove this restriction. Until it ships, a header-capable client is required. The Documentation MCP server needs no authentication and is unaffected.
## Transport
Both tenant-facing servers speak **MCP Streamable HTTP** on the root URL. Responses are delivered as a server-sent event stream on that same URL, so clients that label their transport *SSE* generally work when pointed at the root. There is no separate `/sse` endpoint — always configure the root URL, and use the `http` transport type where your client offers a choice.
# Connecting a client
Source: https://docs.enterspeed.com/enterspeed/mcp-server/query-mcp/connecting-a-client
Wire VS Code, Claude Code, Claude Desktop, or the Anthropic Messages API to the Enterspeed Query MCP Server.
The Enterspeed Query MCP Server speaks plain MCP Streamable HTTP, so any MCP-capable client can use it. This page covers the four most common integrations; the pattern (MCP URL + `x-api-key` header) is identical for every other MCP client — see [Other MCP clients](#other-mcp-clients) at the bottom.
Prerequisites are the same in every case:
* A scoped environment client key (`Query API` + `MCP Server` at minimum). See the [Overview](/enterspeed/mcp-server/query-mcp/overview#creating-a-scoped-key).
* The production endpoint `https://mcp.query.enterspeed.com/`.
Configure the **root URL** with the `http` transport type. There is no separate `/sse` endpoint — the server delivers its responses as a server-sent event stream on the root URL itself.
## Pick your client
VS Code's GitHub Copilot reads MCP servers from an `mcp.json` file. For a single workspace, put the config at `.vscode/mcp.json`; for every workspace, use the **MCP: Open User Configuration** command from the command palette.
```json theme={null}
{
"servers": {
"enterspeedQuery": {
"type": "http",
"url": "https://mcp.query.enterspeed.com/",
"headers": {
"x-api-key": "environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
}
}
```
Reload the window (**Developer: Reload Window** from the command palette) and open the Copilot chat pane. The Enterspeed tools appear in the tool picker once Copilot connects.
Never commit `.vscode/mcp.json` with a real key. Use `${input:enterspeed-key}` with a matching [`inputs` entry](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration) so VS Code prompts for the key on first use, or put the config in your user profile instead of the workspace.
```bash theme={null}
claude mcp add enterspeed \
--scope user \
--transport http \
https://mcp.query.enterspeed.com/ \
-H "x-api-key: environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```
The user scope (`--scope user`) installs the server for the current user across all projects. Use `--scope project` to install it for the current project only but for all users — that writes a checked-in `.mcp.json`, so use an environment variable for the key rather than pasting it.
Then start a session and ask Claude to list your indices:
```bash theme={null}
claude
> List the Enterspeed indices I have access to.
```
The CLI stores the server configuration under `~/.claude.json`. Edit that file if you need to tweak the header or URL afterwards.
Open your Claude Desktop config file:
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
Add the `mcpServers` entry:
```json theme={null}
{
"mcpServers": {
"enterspeedQuery": {
"type": "http",
"url": "https://mcp.query.enterspeed.com/",
"headers": {
"x-api-key": "environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
}
}
```
Fully quit and relaunch Claude Desktop. The Enterspeed tools now appear in the tool picker.
Older Claude Desktop versions cannot open a remote MCP server directly from this file. If the server does not appear at all, use the `mcp-remote` bridge instead:
```json theme={null}
{
"mcpServers": {
"enterspeedQuery": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://mcp.query.enterspeed.com/",
"--header",
"x-api-key:environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
]
}
}
}
```
If Claude Desktop connects but shows *"no tools discovered"*, the key is almost certainly missing the `MCP Server` scope. Create a new key using the **AI Assistant** preset.
This is the production path: a C# service opens an MCP session to the Enterspeed server, discovers the available tools, and runs a tool-use loop with Claude. The `x-api-key` header is set once on the MCP transport.
**Why not the inline `mcp_servers` feature?** Anthropic's inline remote-MCP connector forwards an `Authorization: Bearer ` header to the upstream MCP server and does not let you override the header name. The Enterspeed MCP server reads `x-api-key`. Driving the tool-use loop yourself (as below) works today and gives you full control over retries, logging, and cost.
### Project setup
The loop uses two NuGet packages: [`Anthropic.SDK`](https://www.nuget.org/packages/Anthropic.SDK) for the Messages API and [`ModelContextProtocol`](https://www.nuget.org/packages/ModelContextProtocol) for the MCP session.
```bash theme={null}
dotnet new console -n EnterspeedClaudeClient
cd EnterspeedClaudeClient
dotnet add package Anthropic.SDK
dotnet add package ModelContextProtocol
dotnet user-secrets init
dotnet user-secrets set "ANTHROPIC_API_KEY" "sk-ant-..."
dotnet user-secrets set "ENTERSPEED_MCP_KEY" "environment-xxxxxxxx-..."
```
### Program.cs
```csharp theme={null}
using Anthropic.SDK;
using Anthropic.SDK.Messaging;
using Microsoft.Extensions.Configuration;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using System.Text.Json;
var config = new ConfigurationBuilder()
.AddUserSecrets()
.AddEnvironmentVariables()
.Build();
var anthropicKey = config["ANTHROPIC_API_KEY"]!;
var enterspeedKey = config["ENTERSPEED_MCP_KEY"]!;
// 1. Open an MCP session against the Enterspeed server
var transport = new SseClientTransport(new SseClientTransportOptions
{
Endpoint = new Uri("https://mcp.query.enterspeed.com/"),
AdditionalHeaders = new Dictionary
{
["x-api-key"] = enterspeedKey
}
});
await using var mcp = await McpClientFactory.CreateAsync(transport);
// 2. Discover the tools this key is allowed to use
var mcpTools = await mcp.ListToolsAsync();
// 3. Hand the tool schemas to Claude as Messages API tools
var claude = new AnthropicClient(new APIAuthentication(anthropicKey));
var tools = mcpTools
.Select(t => new Tool
{
Name = t.Name,
Description = t.Description ?? string.Empty,
InputSchema = t.InputSchema
})
.ToList();
var messages = new List
{
new(RoleType.User,
"Find the three most recent published blog posts and summarise " +
"them in one sentence each.")
};
// 4. Tool-use loop — repeat until Claude produces a final answer
while (true)
{
var response = await claude.Messages.GetClaudeMessageAsync(new MessageParameters
{
Model = AnthropicModels.Claude4Sonnet,
MaxTokens = 4096,
Messages = messages,
Tools = tools
});
// Record Claude's turn
messages.Add(new Message(response));
if (response.StopReason != "tool_use")
{
Console.WriteLine(response.Message);
break;
}
// Execute every tool_use block via the MCP client and feed results back
foreach (var block in response.Content.OfType())
{
var args = block.Input.Deserialize>()
?? new();
var mcpResult = await mcp.CallToolAsync(block.Name, args);
messages.Add(new Message
{
Role = RoleType.User,
Content = new List
{
new ToolResultContent
{
ToolUseId = block.Id,
Content = mcpResult.Content.FirstOrDefault()?.Text ?? string.Empty
}
}
});
}
}
```
Run it:
```bash theme={null}
dotnet run
```
Expected output includes a short summary of three blog posts, and the loop's intermediate turns show Claude calling `query_blog` (or the equivalent per-index tool for whatever index the key is scoped to).
**SDK property names.** The exact property names on `ToolUseContent`, `ToolResultContent`, and the `Message` / `Tool` shapes evolve with the Anthropic.SDK package. If a symbol above does not resolve, check the current release notes for the corresponding type name — the orchestration pattern (list tools once, loop until `StopReason != "tool_use"`) stays the same.
### Keeping costs under control
* **Cache the tool list.** Call `ListToolsAsync()` once per session, not per request. The MCP server also caches per API key for 5 minutes, so repeat calls are cheap even if you do list more often.
* **Narrow the tool set with an Index Scope.** Fewer indices means fewer `query_*` tools surfaced to Claude, which means fewer input tokens.
* **Use prompt caching on the tool list.** When you pass `tools` to the Messages API, mark the list with `cache_control: { type: "ephemeral" }` via `CacheControl`-style helpers in Anthropic.SDK — see the package README for the current property name.
## Other MCP clients
The four clients above are the ones we test against regularly, but the MCP server is client-agnostic. Any MCP-capable client follows the same pattern — point it at `https://mcp.query.enterspeed.com/` with `http` transport and set an `x-api-key` header. Some known-good examples:
* **Cursor** — `.cursor/mcp.json` with the same `servers` schema as VS Code.
* **Windsurf** — settings → Cascade → MCP Servers, using the URL + header form.
* **Continue** — `~/.continue/config.json` under `experimental.modelContextProtocolServers`.
* **Zed** — settings under `"context_servers"`.
If your client does not support custom headers, pass the key as an `?apiKey=` query-string parameter on the MCP URL instead. Avoid that in production — the key ends up in request logs along the whole path.
**Custom connectors on claude.ai in the browser are not supported yet.** Browser-based custom connectors cannot send a custom HTTP header, and the query-string fallback is not a safe substitute for a shared URL. Use one of the clients above instead.
We are working on OAuth support to remove this restriction. Until it ships, a header-capable client is required.
## Troubleshooting
| Symptom | Likely cause | Fix |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Client responds *"I don't have any tools"* | MCP server entry missing, or the URL is wrong | Confirm the config was reloaded by the client; hit `/health` on the MCP endpoint. |
| Every tool call returns *"forbidden"* | `x-api-key` forwarded correctly but scope missing | Add `Query API` to the environment client. |
| `mcp add` / config reload fails with *"failed to establish connection"* | Local proxy, VPN, or corporate firewall stripping SSE headers | Bypass the proxy (`NO_PROXY=mcp.query.enterspeed.com`) or try a different network. |
| Tools list is empty after reload | Client caches tool lists per session | Fully quit and relaunch the client (on macOS, Cmd-Q in Claude Desktop; **Developer: Reload Window** in VS Code). |
| `401` on every request | Wrong key type — Management API token instead of environment-client key | Create an **environment client** key, not a Management API token. |
## Next steps
* [Overview](/enterspeed/mcp-server/query-mcp/overview) — sample prompts and scope reference.
* [Connecting an agent](/enterspeed/mcp-server/query-mcp/connecting-an-agent) — Azure AI Foundry and custom-agent wiring.
# Connecting an agent
Source: https://docs.enterspeed.com/enterspeed/mcp-server/query-mcp/connecting-an-agent
Wire a custom C# agent or Azure AI Foundry to the Enterspeed Query MCP Server.
This guide covers two common agent setups:
1. **Custom C# agent** — you own the LLM orchestration and want MCP tools to be part of a larger workflow. Recommended when you need latency, auth, or retry control.
2. **Azure AI Foundry** — you want the Foundry Responses API or Agent service to handle orchestration, with the MCP server plugged in as a tool provider.
Before you start, you need:
* A scoped environment client key (`Query API` + `MCP Server` at minimum). See the [Overview](/enterspeed/mcp-server/query-mcp/overview#creating-a-scoped-key).
* The production endpoint `https://mcp.query.enterspeed.com/` — the root URL, with `http` transport. There is no separate `/sse` endpoint.
## Part 1 — C# agent (MCP client SDK)
Agents talk to the Enterspeed MCP server over **MCP Streamable HTTP**. In C#, the most direct client is the official [`ModelContextProtocol`](https://www.nuget.org/packages/ModelContextProtocol) NuGet package — it opens the connection, negotiates the protocol, lists tools, and lets you call them. Authentication is a single `x-api-key` header on the transport.
The transport class is named `SseClientTransport` for historical reasons. Pointed at the root URL, as below, it speaks Streamable HTTP — you do not need a separate `/sse` endpoint, and there isn't one.
### Prerequisites
```bash theme={null}
dotnet new console -n EnterspeedMcpAgent
cd EnterspeedMcpAgent
dotnet add package ModelContextProtocol
```
Store secrets with `dotnet user-secrets` (never commit them):
```bash theme={null}
dotnet user-secrets init
dotnet user-secrets set "ENTERSPEED_MCP_KEY" "environment-xxxxxxxx-..."
```
### Program.cs
```csharp theme={null}
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
var enterspeedKey = Environment.GetEnvironmentVariable("ENTERSPEED_MCP_KEY")
?? throw new InvalidOperationException("ENTERSPEED_MCP_KEY not set");
var transport = new SseClientTransport(new SseClientTransportOptions
{
Endpoint = new Uri("https://mcp.query.enterspeed.com/"),
// The MCP server reads `x-api-key` on every request and forwards it to
// the Enterspeed Query API, which validates scope and index restrictions.
AdditionalHeaders = new Dictionary
{
["x-api-key"] = enterspeedKey
}
});
await using var mcpClient = await McpClientFactory.CreateAsync(transport);
// Discover what this key can do
var tools = await mcpClient.ListToolsAsync();
foreach (var tool in tools)
{
Console.WriteLine($"- {tool.Name}: {tool.Description}");
}
// Call a tool — here the static get_indices tool
var result = await mcpClient.CallToolAsync(
"get_indices",
new Dictionary());
Console.WriteLine(result.Content[0]);
```
Run it:
```bash theme={null}
dotnet run
```
The output starts with the tools available to your key (static tools, the dynamic `enterspeed_query` tool, and one `query_` tool per index you have access to) and then prints the result of the first tool call.
### Driving it from an LLM (tool-use loop)
To turn this into an LLM-driven agent, hand the `tools` list to the model of your choice. The pattern is identical for every major LLM provider:
1. Call `ListToolsAsync()` once per session and cache the result.
2. Send the tool schemas to the LLM alongside the user prompt.
3. When the LLM emits a tool call, invoke `mcpClient.CallToolAsync(name, args)` and feed the response back in the next turn.
4. Repeat until the LLM produces a final answer.
For Claude specifically, see [Connecting a client](/enterspeed/mcp-server/query-mcp/connecting-a-client) for a complete Messages-API example that wires this loop.
### Alternative — the Anthropic Remote MCP connector
If you prefer to let Claude open the MCP connection itself via the inline `mcp_servers` feature of the Messages API, you can — but note that Anthropic's spec currently only supports an `Authorization: Bearer ` header on the upstream MCP server; it does not let you set a custom header name. The Enterspeed MCP server reads `x-api-key`, so the inline connector does not work with a raw scoped key today.
Workarounds:
* Pass the key as an `?apiKey=` query-string parameter on the MCP URL (supported by the server, but the key ends up in request logs — acceptable for prototyping only).
* Use the explicit [`ModelContextProtocol`](https://www.nuget.org/packages/ModelContextProtocol) client above and drive the tool-use loop yourself.
### Alternative — Semantic Kernel plugin
If you use Microsoft.SemanticKernel, turn every MCP tool into a `KernelFunction`:
```csharp theme={null}
using Microsoft.SemanticKernel;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
var transport = new SseClientTransport(new SseClientTransportOptions
{
Endpoint = new Uri("https://mcp.query.enterspeed.com/"),
AdditionalHeaders = new Dictionary
{
["x-api-key"] = enterspeedKey
}
});
await using var mcp = await McpClientFactory.CreateAsync(transport);
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion("gpt-4o", azureOpenAiKey)
.Build();
var tools = await mcp.ListToolsAsync();
kernel.Plugins.AddFromFunctions(
"enterspeedQuery",
tools.Select(t => t.AsKernelFunction()));
var answer = await kernel.InvokePromptAsync(
"List the Enterspeed indices. Then describe the first one.");
Console.WriteLine(answer.GetValue());
```
## Part 2 — Azure AI Foundry (step-by-step)
Azure AI Foundry's Responses API and Agent service can attach an MCP server as a tool provider. The flow is: **your code → Foundry (with `mcp_servers` in the request) → MCP server → Query API**.
**Entra ID required for MCP on Foundry.** Foundry's MCP tool support routes through the Agent service orchestration layer, which requires **Microsoft Entra ID** authentication (service principal or user identity). A plain Foundry API key is not accepted for MCP-enabled requests. If you need a "simple API key" façade for end users, place a thin wrapper in front that holds the service principal.
### Step 1 — Create an AI Foundry project
1. In the Azure portal, create an **AI Foundry** resource.
2. Inside it, create a project — e.g. `enterspeed-mcp-demo`.
3. Pick a region that supports the Responses API with MCP tool calling. `swedencentral` is EU-based and supported; `eastus` is the most feature-complete.
4. Deploy a model. `gpt-4o` is a good starting point; Claude models (`claude-sonnet-4`) are also available on `swedencentral`.
### Step 2 — Create a service principal
```bash theme={null}
az ad sp create-for-rbac \
--name "enterspeed-mcp-agent" \
--role "Azure AI User" \
--scopes "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts/"
```
Save the returned `appId`, `password`, and `tenant`.
If the subscription owner has not granted the service principal the **Azure AI User** role on the AI Services account, no MCP-enabled API call will succeed. This is the most common failure mode.
### Step 3 — Configure secrets
```bash theme={null}
dotnet user-secrets set "AZURE_TENANT_ID" ""
dotnet user-secrets set "AZURE_CLIENT_ID" ""
dotnet user-secrets set "AZURE_CLIENT_SECRET" ""
dotnet user-secrets set "FOUNDRY_ENDPOINT" "https://.services.ai.azure.com/api/projects/"
dotnet user-secrets set "FOUNDRY_MODEL" "gpt-4o"
dotnet user-secrets set "ENTERSPEED_MCP_KEY" "environment-xxxxxxxx-..."
```
### Step 4 — Call the Responses API with MCP attached
```csharp theme={null}
using Azure.Identity;
using Azure.Core;
using System.Net.Http.Json;
using Microsoft.Extensions.Configuration;
var config = new ConfigurationBuilder()
.AddUserSecrets()
.Build();
var credential = new ClientSecretCredential(
config["AZURE_TENANT_ID"],
config["AZURE_CLIENT_ID"],
config["AZURE_CLIENT_SECRET"]);
var token = await credential.GetTokenAsync(
new TokenRequestContext(new[] { "https://ai.azure.com/.default" }));
var http = new HttpClient
{
BaseAddress = new Uri(config["FOUNDRY_ENDPOINT"]!)
};
http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
var body = new
{
model = config["FOUNDRY_MODEL"],
input = "List the Enterspeed indices I have access to.",
tools = new object[]
{
new
{
type = "mcp",
server_label = "enterspeedQuery",
server_url = "https://mcp.query.enterspeed.com/",
// Foundry forwards this header to the MCP server on every call
headers = new Dictionary
{
["x-api-key"] = config["ENTERSPEED_MCP_KEY"]!
},
require_approval = "never"
}
}
};
var response = await http.PostAsJsonAsync(
"/openai/responses?api-version=2025-03-01-preview", body);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```
### Step 5 — Validate end-to-end
Run this known-good smoke sequence:
* **"List my Enterspeed indices"** — expect an index-listing tool call, then a list in the response.
* **"Describe index ``"** — expect a `describe_index` call with fields grouped by type.
* **A domain-specific question** — expect one or more `query_*` tool calls and a synthesised answer.
If the model returns "I don't have access to that tool" after step 1 succeeded, the key most likely lacks the `Query API` scope. The tool was listed (because `MCP Server` is present) but the call is rejected by the Query API.
### Foundry Agent service (alternative)
Foundry also has a persistent **Agent service**. The MCP wiring is identical (an `mcp` tool with `server_url` and `headers`) but the agent persists across calls. Use it when you want a long-running conversation with the same tool set attached. The same Entra ID requirement applies.
## Custom domain / enterprise note
If you are proxying the MCP server behind your own domain:
* The Query API still validates the scoped key — the proxy must forward `x-api-key` verbatim.
* The proxy must support HTTP streaming (chunked / SSE). Several AWS ALB and classic CDN configurations do not by default.
* The MCP transport is Streamable HTTP over HTTP/1.1. Check your hop-by-hop limits before forcing HTTP/2.
## Troubleshooting
| Symptom | Likely cause | Fix |
| --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Foundry: `403 Identity (object id: ) does not have permissions for Microsoft.MachineLearningServices/workspaces/agents/action actions.` | Using a Foundry API key, or the service principal is missing the `Azure AI User` role | Switch to service principal auth and grant the role on the AI Services resource. |
| Foundry returns instantly with no tool calls | MCP server not reachable from Foundry, or TLS cert mismatch | Curl the MCP health endpoint from a network location similar to Foundry's egress. |
| Tool list arrives but every call returns empty | `x-api-key` lost along the proxy chain | Trace the header end-to-end; the MCP server requires it on every request, not just at session start. |
| Agent times out on a long query | Default HTTP client timeout of 2 minutes exceeded | Paginate the prompt, or raise the timeout on your side. |
## Next steps
* [Connecting a client](/enterspeed/mcp-server/query-mcp/connecting-a-client) — VS Code, Claude Code, Claude Desktop, and the Anthropic Messages API in C#.
* [Overview](/enterspeed/mcp-server/query-mcp/overview) — sample prompts and the full scope table.
# Overview
Source: https://docs.enterspeed.com/enterspeed/mcp-server/query-mcp/overview
Connect an AI client to the Enterspeed Query API — query your indexes, views, and auto-indexed source entities in natural language.
The Enterspeed Query MCP Server turns the Enterspeed Query API into a set of [Model Context Protocol](https://modelcontextprotocol.io/) tools that AI clients (Claude, Cursor, custom agents, Azure AI Foundry, etc.) can discover and call. You do not need to host anything yourself — Enterspeed runs the server for you.
This page tells you **where the server lives**, **how authentication works**, and **what scopes to request**. For step-by-step wiring guides, see [Connecting a client](/enterspeed/mcp-server/query-mcp/connecting-a-client) and [Connecting an agent](/enterspeed/mcp-server/query-mcp/connecting-an-agent).
To configure your tenant rather than query it, see [Management MCP](/enterspeed/mcp-server/management-mcp/overview).
## Hostname
| Environment | URL | Transport |
| -------------- | ----------------------------------- | ------------------- |
| **Production** | `https://mcp.query.enterspeed.com/` | MCP Streamable HTTP |
A health check is available without authentication:
```bash theme={null}
curl -s https://mcp.query.enterspeed.com/health
# "ok"
```
The server speaks **MCP Streamable HTTP** on the root URL. Responses are delivered as a server-sent event stream on that same URL, so clients that label their transport *SSE* generally work when pointed at the root. There is no separate `/sse` endpoint — always configure the root URL.
## How authentication works
The MCP server is a thin proxy. All authorisation decisions happen in the Enterspeed Query API. Your MCP client sends a scoped environment client key on every request; the MCP server forwards it as-is; the Query API validates the scope and any index restrictions before returning data.
```
AI client ──x-api-key──► mcp.query.enterspeed.com ──x-api-key──► query.enterspeed.com
│
validates scopes + IndexScope
```
Clients authenticate with a single HTTP header on every MCP request:
```
x-api-key: environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
The key is a **scoped environment client key** issued from the Enterspeed Management App. It is *not* a Management API key — that is what [Management MCP](/enterspeed/mcp-server/management-mcp/overview) uses.
The server also accepts an `?apiKey=` query-string fallback for tools that cannot set headers. Prefer the header whenever possible: a key in a URL ends up in access logs along the whole request path.
The key is checked when a tool is *called*, not when the session is opened. A client with a missing or invalid key can still connect and list the static tools — which is why an unexpectedly short tool list is the fastest signal that a key is wrong or under-scoped.
### Rate limit
Requests are counted per API key over a rolling one-minute window, with a limit of **120 requests per minute**. Exceeding it returns:
```
HTTP 429 Too Many Requests
Retry-After:
Too many requests. Retry after seconds.
```
Well-behaved MCP clients back off and retry. The `/health` endpoint is exempt.
## Required scopes
The platform supports component-scoped environment keys. For MCP use, your key **must** include:
| Scope | Why | Grants |
| ------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `MCP Server (AI Agent Access)` | Gates the MCP tool endpoints. Without it, the server sees zero dynamic or per-index tools. | Ability to list MCP tools. |
| `Query API` | Needed to query your Enterspeed data. | Access to both **schema-transformed data** (the `query_*` tools and the unified `enterspeed_query` tool) and **auto-indexed data** (the source tools). |
`MCP Server` alone is not useful — it must be combined with `Query API`, and Enterspeed enforces that pairing when you save the client.
There are exactly four scopes: **Delivery API**, **Query API**, **Routes API**, and **MCP Server**. There is no separate Source API scope — access to auto-indexed source entities comes with **Query API**, narrowed by the [index scope](#index-scopes) below.
### Scope presets
When creating an environment client, the Management App exposes these presets so you do not have to toggle scopes manually:
| Preset | Scopes bundled | Typical use case |
| ---------------- | ------------------------- | ------------------------------------------------------------------------------------ |
| **Standard** | Delivery + Query + Routes | Traditional delivery clients, no AI. |
| **AI Assistant** | Query + MCP Server | Full data access for AI agents — both schema-transformed data and auto-indexed data. |
The **AI Assistant** preset is the right default for MCP integrations.
See [Using environment clients](/enterspeed/getting-started/environment-clients#scope-presets) for the full table and how to manage scopes in the Management App.
## Index scopes
On top of component scopes, each environment client can carry an optional **Index Scope** that restricts *which indices* the key can see. Patterns are matched against the fully-qualified index name.
| Pattern | Matches |
| ---------- | --------------------------------------------- |
| `blog*` | every index whose name starts with `blog` |
| `cms:*` | every index under the `cms` source group |
| `*:entity` | every index ending in `entity` |
| *(unset)* | no restriction — all indices the scope allows |
Filtering is enforced by the Query API, so AI clients get a pre-trimmed tool list. This is ideal when you want to give a public-facing AI assistant access to, say, only a marketing-blog index, without exposing the rest of the environment.
**An empty index scope means unrestricted, not restricted.** Leaving it unset grants every index the key's scopes allow. If you want to limit an AI assistant to a subset of your data, you must set a pattern explicitly — omitting the field is the *open* setting, not the safe one.
Patterns are read in two families, split by the colon:
* **Without a colon** — matches index names, for example `blog*`.
* **With a colon** — matches auto-indexed source entities as `sourceGroup:entityType`, for example `cms:*`.
Each family is only restricted if you supply at least one pattern for it. So a key configured with only `cms:*` restricts auto-indexed source entities while leaving index queries **unrestricted**. Set patterns in both families when you want to narrow both.
## Creating a scoped key
1. Sign in to the **Enterspeed Management App**.
2. Select the tenant and environment you want the AI client to query.
3. Go to **Environment → API Keys**.
4. Click **Create API key**.
5. Pick the **AI Assistant** preset.
6. *(Optional)* Set an **Index Scope** pattern like `blog*` to restrict access.
7. Give the key a descriptive name, e.g. `claude-prod-blog-assistant`.
8. Click **Create** — the key is shown **once**. Store it in your secret manager immediately.
The new key has the form `environment-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
See [Using environment clients](/enterspeed/getting-started/environment-clients) for the full environment-client flow, including regenerating a key.
## What tools the agent gets
Every tool is a read — the server has no way to change your data or your configuration. To configure a tenant, an agent needs [Management MCP](/enterspeed/mcp-server/management-mcp/overview) instead.
The tool list is assembled per key, so two clients pointed at the same server can see different tools. It comes in three layers.
**Always present** — the same eight tools for everyone:
| Tool | What it does |
| ------------------------------------------------------- | ----------------------------------------------------------------------- |
| `get_indices` | Lists the indexes the key can see. The usual starting point. |
| `describe_index` | Describes one index — its fields, and which are searchable or sortable. |
| `get_query_items` | Queries a single index with filters, sorting, pagination, and facets. |
| `get_multiple_query_items` | Queries several indexes in one round-trip. |
| `get_source_items` | Fetches auto-indexed source entities. |
| `get_indices_raw_source_entities_by_source_group_alias` | Discovers which entity types exist in a source group. |
| `get_operators` | Lists the filter operators available to you. |
| `retrieve_query_api_status` | Reports Query API status. |
**Per index** — one `query_` tool for each index the key can reach, generated from that index's own fields. These give an agent a typed, index-specific way to query, so it does not have to construct a generic filter by hand.
**Unified** — an `enterspeed_query` tool that targets several indexes in a single call.
All three layers respect the key's [index scope](#index-scopes), so the agent is handed a pre-trimmed list rather than being told "no" after it tries.
If an agent only ever shows the eight tools above, the per-index layer is missing — which almost always means the key lacks the `MCP Server` scope, or is invalid.
## Sample prompts
Use these when demoing or smoke-testing a fresh MCP connection.
### Connectivity check
> List the Enterspeed indices you have access to, then tell me how many of them there are and what their naming convention looks like.
Expected behaviour: the agent calls the index-listing tool and returns a list filtered by the key's Index Scope.
### Schema discovery
> Describe the `blog` index. What fields does it have, which ones are searchable, and which are sortable?
Expected behaviour: the agent calls `describe_index` with `indexAlias: blog`. A good answer groups fields by type (keyword, text, date, integer, etc.).
### Single-index query
> Find the five most recent blog posts authored by `alice` and return their titles, publish dates, and URLs.
Expected behaviour: the agent calls `query_blog` with a filter on `author = alice`, sort descending by `publishedAt`, and `pagination.pageSize = 5`.
### Multi-index query
> Across the `products` and `productsv2` indices, find items with `category = beverages` and under 100 kcal per 100g. Return the top three sorted by energy ascending.
Expected behaviour: the agent calls the unified `enterspeed_query` tool with a `queries` array that targets both indices in a single round-trip.
### Auto-indexed data exploration
> Show me five raw source entities of type `article` from the `cms` source group. Which fields are set on the first one?
Expected behaviour: the agent discovers types via `get_indices_raw_source_entities_by_source_group_alias`, then pulls five entities via `get_source_items`. Access to auto-indexed data comes with the `Query API` scope — no extra scope is needed beyond the standard `Query API` + `MCP Server` combination.
### Authorisation check
> Which operators can I use to filter queries in Enterspeed?
Expected behaviour: the agent calls the `get_operators` tool. The list typically includes `contains`, `equals`, `notEquals`, `in`, `lessThan`, `lessThanOrEquals`, `greaterThan`, and `greaterThanOrEquals` — the concrete set is data-driven, so trust the tool response over this page.
### Tool-listing sanity check
> What tools do you have available and which Enterspeed indices are they tied to?
Expected behaviour: the agent enumerates the full tool set. If the dynamic and per-index groups are empty, the key is most likely missing the `MCP Server` scope — that is the fastest way to spot a misconfigured key.
## Troubleshooting
| Symptom | Likely cause | Fix |
| ---------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` from the MCP server | `x-api-key` missing or malformed | Confirm the header is present on every MCP request. |
| Agent lists only a small static set of tools | Key is missing the `MCP Server` scope | Recreate the key with the *AI Assistant* preset. |
| Agent sees fewer indices than expected | `IndexScope` pattern is too narrow | Widen the Index Scope on the environment client, or remove it for all-index access. |
| Tools listed but `query_*` returns "forbidden" | Key has `MCP Server` but not `Query API` | Add the `Query API` scope. |
| `401` on every request | Wrong key type — a Management API key instead of an environment client key | Create an **environment client** key. Management API keys belong to [Management MCP](/enterspeed/mcp-server/management-mcp/overview). |
| `429` with a `Retry-After` header | More than 120 requests in a minute on this key | Let the client back off, or split work across keys. |
| Client cannot connect at all | Configured with a `/sse` path, or a proxy that buffers streamed responses | Use the root URL. Streamed responses must pass through unbuffered. |
## Known limitations
**Custom connectors on claude.ai in the browser are not supported yet.** Browser-based custom connectors cannot send a custom HTTP header, and this server requires `x-api-key`. Use Claude Code, Claude Desktop, VS Code, Cursor, or your own agent instead.
We are working on OAuth support to remove this restriction. Until it ships, a header-capable client is required.
## Next steps
* [Connecting a client](/enterspeed/mcp-server/query-mcp/connecting-a-client) — VS Code, Claude Code, Claude Desktop, or the Anthropic Messages API in C#.
* [Connecting an agent](/enterspeed/mcp-server/query-mcp/connecting-an-agent) — wire a custom C# agent or Azure AI Foundry to the MCP server.
* [Management MCP](/enterspeed/mcp-server/management-mcp/overview) — configure your tenant instead of querying it.
# Filter Expressions
Source: https://docs.enterspeed.com/enterspeed/reference/filter-expressions
Filter expressions are used when doing lookups in JSON or JavaScript schemas.
Below is a list of examples of what you can do with the filter.
If you are using JavaScript and your filter contains expressions, it's often easier to use string interpolation instead of string concatination. See [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#string_interpolation) .
If you are comparing values containing URI special characters, you should URI encode the value in your filter expression. See [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent).
```javascript theme={null}
filter(`originParentId eq '${encodeURIComponent(sourceEntity.originId)}'`)
```
## Examples
Examples of supported binary operators & expressions:
```javascript title="EQUALS operator" theme={null}
// Property value equal to constant string value
originId eq '100'
// Property value equal to expressed string value
originId eq '${sourceEntity.properties.selectedOtherPageId}'
// Property value is null
originParentId eq null
// Property value equal to integer value
properties.price eq 9
// Property value equal to decimal value
properties.price eq 9.99
// Property value equal to boolean value (true/false)
properties.isFeatured eq true
```
```javascript title="EQUALS operator" theme={null}
// Property value equal to constant string value
originId eq '100'
// Property value equal to expressed string value
originId eq '{p.selectedOtherPageId}'
// Property value is null
originParentId eq null
// Property value equal to integer value
properties.price eq 9
// Property value equal to decimal value
properties.price eq 9.99
// Property value equal to boolean value (true/false)
properties.isFeatured eq true
```
```javascript title="NOT EQUALS operator" theme={null}
// Property value not equal to constant string value
originId ne '100'
// Property value not equal to expressed string value
originId ne '${sourceEntity.properties.selectedOtherPageId}'
// Property value is not null
originParentId ne null
// Property value not equal to integer value
properties.price ne 9
// Property value not equal to decimal value
properties.price ne 9.99
// Property value not equal to boolean value (true/false)
properties.isFeatured ne true
```
```javascript title="NOT EQUALS operator" theme={null}
// Property value not equal to constant string value
originId ne '100'
// Property value not equal to expressed string value
originId ne '{p.selectedOtherPageId}'
// Property value is not null
originParentId ne null
// Property value not equal to integer value
properties.price ne 9
// Property value not equal to decimal value
properties.price ne 9.99
// Property value not equal to boolean value (true/false)
properties.isFeatured ne true
```
```javascript title="AND operator" theme={null}
type eq 'article' and properties.isFeatured eq true
```
```javascript title="AND operator" theme={null}
type eq 'article' and properties.isFeatured eq true
```
```javascript title="OR operator" theme={null}
type eq 'article' or type eq 'contentPage'
```
```javascript title="OR operator" theme={null}
type eq 'article' or type eq 'contentPage'
```
```javascript title="An array contains any matching value:" theme={null}
// Contains specific redirect with constant string value
redirects/any(r: r eq '/old-page')
// Contains specific tags with expression string value
properties.tags/any(t: t eq '${sourceEntity.properties.selectedTag}')
// Contains articles that feature, matching constant boolean value
properties.articles/any(a: a.isFeatured eq true)
```
```javascript title="An array contains any matching value:" theme={null}
// Contains specific redirect with constant string value
redirects/any(r: r eq '/old-page')
// Contains specific tags with expression string value
properties.tags/any(t: t eq '{p.selectedTag}')
// Contains articles that feature, matching constant boolean value
properties.articles/any(a: a.isFeatured eq true)
```
Please note that our filter expressions only support one use of `any` per expression.\
E.g. `properties.tags/any(t: t eq 'tag1') or properties.tags/any(t: t eq 'tag2')` is not supported.
```javascript title="Check whether the element exists in this collection" theme={null}
// Finds matches in predefined integers array
properties.favoriteId in (100, 200)
// Finds matches in predefined strings array
properties.color in ('blue', 'red', 'green')
// Finds matches in favorites selection (array of integers)
properties.favoriteId in (${sourceEntity.properties.favoriteIds})
// Finds matches in colors selection (array of strings)
properties.color in (${sourceEntity.properties.colors.map(c => `'${c}'`)})
```
```javascript title="Check whether the element exists in this collection" theme={null}
// Finds matches in predefined integers array
properties.favoriteId in (100, 200)
// Finds matches in predefined strings array
properties.color in ('blue', 'red', 'green')
// Finds matches in favorites selection (array of integers)
properties.favoriteId in {properties.favoriteIds}
// Finds matches in colors selection (array of strings)
properties.color in {properties.selectedColors}
```
```js title="Full example" theme={null}
export default {
// ...
properties: function (sourceEntity, context) {
return {
// ...
sportArticles: context
.reference('featuredSportArticle')
.filter(`type eq 'article' and properties.tags/any(t: t eq '${sourceEntity.properties.tag}')`)
.orderBy({ propertyName: 'properties.metaData.sortOrder', direction: "asc"})
.limit(5)
}
},
};
```
```json title="Full example" theme={null}
"sportArticles": {
"type": "array",
"input": {
"$lookup": {
"filter": "type eq 'article' and properties.tags/any(t: t eq '{p.tag}')",
"orderBy": {
"property": "properties.metaData.sortOrder",
"sort": "asc"
},
"top": 5
}
},
"var": "article",
"items": {
"type": "object",
"properties": {
"title": "{article.p.title}",
"content": "{article.p.content}"
}
}
}
```
# Organising your schemas
Source: https://docs.enterspeed.com/enterspeed/reference/folders
To better structure and organize your schemas, we recommend using our folder feature.
When naming your schema, you can organize them into folders by prepending the name with your desired folder name and slash (`/`).
For instance, if you have a schema called `Currency`, you can name or rename it to `Global/Currency`. This will create a folder called `Global`, while the schema is still called `Currency`.
If you want to delete the folder, simply remove the `Global/` part from the name.
We support folders up to two levels deep, meaning a folder can also have subfolders, for instance `Global/Various/Currency`.
When creating a schema, an alias is automatically generated while you're typing.
In order to not have the folder names "spill" into the alias, folder names will automatically be removed from the alias as soon as you type a slash `/`.
# Actions
Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/actions
The `actions` method is used if you need to trigger new processing - which could be processing another schema or pushing generated views to a third-party application - using web hooks or such as Algolia.
# ActionsContext object
The `ActionsContext` object is passed into the actions method and gives you access to the `reprocess` and `destination` functions which are described below.
## Methods
| Method | Description |
| --------------------------- | ---------------------------------------------------------------- |
| [reprocess](#reprocess) | Reprocess a schema based on its alias. |
| [destination](#destination) | Specifies the destination where the generated view is pushed to. |
### reprocess
`reprocess(schemaAlias)`
Note that `reprocess` only triggers whenever the source entity is changed. This means that a deploy of the schema will not trigger the reprocess.
Read more about how reprocessing is working, when to use it and when not to use it in the [key concepts](/enterspeed/key-concepts/reprocessing).
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ---------------------- |
| `schemaAlias` | string | The alias of a schema. |
#### Required function calls
After the `reprocess` function it's required to call one of the following functions to define what source entities you want to reprocess.
Use the `byOriginId` function to reprocess a specific entity based on its originId.
#### Parameters
| Parameter | Type | Description |
| ---------- | ------ | --------------------------------------- |
| `originId` | string | The original id from the source system. |
```js title="reprocess by byOriginId" theme={null}
context
.reprocess('mySchemaAlias')
.byOriginId(sourceEntity.properties.link.id)
```
Use the `bySchema` function to reprocess all source entities a specific schema has a trigger on.
Caution: Often it's better to reprocess a specific source entity instead a schema and all of its matching source entities, but sometimes it's necessary to reprocess an entire schema.
Note: Even though you are reprocessing an entire schema, you still need to specific the source group (see optional function call) if it's different from the source group of the source entity triggering the reprocess.
```js title="reprocess by bySchema" theme={null}
context
.reprocess('mySchemaAlias')
.bySchema()
```
Using the `filter` function lets you do a dynamic search for source entities you want to reprocess.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------------------------------------------------------ |
| `filter` | string | Your filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) |
```js title="reprocess by filter" theme={null}
context
.reprocess('mySchemaAlias')
.filter("type eq 'account' and properties.internalId eq '1234'")
```
The `filter` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed.
The default limits are:
* Free & Premium plans: 100 items
* Enterprise plan: 500 items
Use the `parent` function to reprocess the parent entity based on its originParentId.
```js title="reprocess by byOriginId" theme={null}
context
.reprocess('mySchemaAlias')
.parent()
```
#### Optional function calls
To filter the source entities you want to reprocess even further you can call some of the following optional functions.
The `limit` function limits the number of source entities.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | -------------------------------------- |
| `limit` | number | The maximum number of source entities. |
```js title="reprocess by filter and limit" theme={null}
context
.reprocess('mySchemaAlias')
.filter("type eq 'account'")
.limit(5)
```
The `orderBy` function sorts the source entities. This is typically used in combination with `limit`.
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------- | ------------------------------------------------- |
| `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. |
```js title="reprocess by filter and orderBy" theme={null}
context
.reprocess('mySchemaAlias')
.filter("type eq 'account'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
```
Defines the source group of an source entity to process. If the source entity you want to reprocess is located in another source group than the current source entity, you must specific the source group.
Note: `sourceGroup` can't be used on `parent` as parent is implicit in the same source group as the child.
#### Parameters
| Parameter | Type | Description |
| ------------------ | ------ | -------------------------------------------------------------------------- |
| `sourceGroupAlias` | string | The alias of the source group for the source entity you want to reprocess. |
```js title="reprocess by origin id and source group" theme={null}
context
.reprocess("mySchemaAlias")
.byOriginId(sourceEntity.properties.link.id)
.sourceGroup('anotherSourceGroup')
```
### destination
`destination(destinationAlias)`
The `destination` method is used to push generated views for a schema to a webhook, to Algolia or another third-party application.
#### Parameters
| Parameter | Type | Description |
| ------------------ | ------ | ---------------------------------------------------------------------- |
| `destinationAlias` | string | The alias of the destination you want the generated view is pushed to. |
## Examples
```js title="actions example" theme={null}
actions: function(sourceEntity, context) {
context.reprocess('productCategory')
.byOriginId(sourceEntity.properties.productCategoryPage.id)
.sourceGroup('commerce');
context.reprocess('account')
.filter("type eq 'account' and properties.internalId eq '1234'");
context.destination('webhook');
}
```
```js title="actions can also be expressed as an arrow function expression to make it even more compact" theme={null}
actions: (sourceEntity, context) => context.reprocess('productCategory').parent()
```
# Intro
Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/intro
A full schema generates views of JSON data that you can fetch from the [Delivery API](/api-reference/delivery).
In a full schema you can map properties, reference other full or partial schemas, define routes like URLs and more.
```js title="JavaScript full schema example" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['page']);
},
routes: function(sourceEntity, context) {
context.url(sourceEntity.url);
},
properties: function (sourceEntity, context) {
return {
title: sourceEntity.properties.title,
blocks: context.partial("blocks", sourceEntity.properties.blocks),
aboutUsPage: context.reference("page").byOriginId(sourceEntity.properties.aboutUsPage.id),
};
}
}
```
# Properties
Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/properties
The `properties` method is where you define the output that goes into the view you fetch from the [Delivery API](/api-reference/delivery)
```js title="Properties example" theme={null}
properties: function (sourceEntity, context) {
return {
title: sourceEntity.properties.title,
seo: {
title: sourceEntity.properties.seoTitle,
description: sourceEntity.properties.seoDescription,
}
categoryIds: sourceEntity.properties.categoryIds
.map(categoryId => parseInt(categoryId))
};
}
```
The properties method must return an object with the data you want to include in your view.
# PropertiesContext object
The `PropertiesContext` object is passed into the `properties` method and gives you access to a set of methods described below.
## Methods
| Method | Description |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [lookup](#lookup) | Lookup allows you to search source entities using a filter string and work with the source entities directly in the schema. |
| [partial](#partial) | Referencing a partial schema. Mapped data from a partial schema is embedded into the calling schema. |
| [reference](#reference) | Referencing a full schema. References to other schemas are resolved on delivery request time. |
### lookup
Lookup source entities.
Lookup allows you to search source entities using a filter string and work with the data from the source entities directly in the schema.
Note the `lookup` is an async function so you have to use [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) when you are using the `lookup` function.
`lookup(filter)`
The `lookup` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed.
The default limits are:
* Free & Premium plans: 100 items
* Enterprise plan: 500 items
#### Parameters
| Parameter | Type | Description |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) |
| `lookupOptions` | object | An optional object with the following structure: `{ excludeProperties?: boolean; }`
If `excludeProperties` is set to `true` the `lookup` will only return the base fields of the source entities and not all custom properties. This will help improve performance if you don't need the custom properties, but only some of the base fields like `originId`, `originParentId`, `url` |
#### Required function calls
After the `lookup` function it's required to call `toPromise` to excecute the query and return a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
Using the toPromise function excecutes the query and return a promise you must resolve.
```js title="look toPromise" theme={null}
const newsArticles: await context
.lookup("type eq 'newsArticle'")
.toPromise()
```
#### Optional function calls
To filter the source entities even further you can call some of the following optional functions.
The `limit` function limits the number of source entities.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------ |
| `limit` | number | The maximum number of source entities to return. |
```js title="look and limit" theme={null}
const newsArticles: await context
.lookup("type eq 'newsArticle'")
.limit(5)
.toPromise()
```
The order sequence of the source entities.
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------- | ------------------------------------------------- |
| `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. |
```js title="lookup and orderBy" theme={null}
const newsArticles: await context
.lookup("type eq 'newsArticle'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
.toPromise()
```
The sourceGroup function lets you specify the source group. By default the source group of the current source entity is used.
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. |
If not defined, it uses the current source group.
```js title="lookup and sourceGroup" theme={null}
const newsArticles: await context
.lookup("type eq 'newsArticle'")
.sourceGroup("anotherSourceGroup")
.toPromise()
```
#### Examples
```js title="lookup with async/await" theme={null}
properties: async function (sourceEntity, context) {
const latestNews = await context
.lookup("type eq 'newsArticle'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
.limit(3)
.toPromise();
const mappedNews = latestNews
.map((news) => ({
url: news.url,
title: news.properties.title,
teaser: news.properties.teaser
}));
return {
latestNews: mappedNews
}
}
```
```js title="lookup without custom properties" theme={null}
properties: async function (sourceEntity, context) {
const baseNewsArticles = await context
.lookup("type eq 'newsArticle'", { excludeProperties: true })
.limit(3)
.toPromise();
const mappedNews = baseNewsArticles
.map((news) => ({
originId: news.originId,
url: news.url
}));
return {
latestNews: mappedNews
}
}
```
***
### partial
Referencing a partial schema.
The partial mapping property type allows for dynamically including partial schemas into the main schema. This is useful when you need to iterate an array of different objects that has an identifier, like an ID, alias or similar.
Partial schemas are typically used when you want a reusable schema for mapping data that is part of the same entity. E.g. if metadata (title, description, etc.) is the same across different entity types, but the data lives on the entity itself.
Read more about partial schemas [here](/enterspeed/key-concepts/partial-schemas)
`partial(schemaAlias, input)`
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ------------------------------------------------------------ |
| `schemaAlias` | string | The alias of a partial schema. |
| `input` | object | You can pass whatever data you need for your partial schema. |
#### Examples
```js title="partial" theme={null}
contentBlocks: sourceEntity.properties.contentBlocks.map((contentBlock) =>
context.partial(`block-${contentBlock.contentType}`, contentBlock)
)
```
```js title="passing extra properties to the partial schema" theme={null}
contentBlocks: sourceEntity.properties.contentBlocks.map((contentBlock) =>
context.partial(`block-${contentBlock.contentType}`, {
block: contentBlock,
pageOriginId: sourceEntity.originId
})
)
```
The `input` defines what goes into the partial schema and can be any custom object, and the `schemaAlias` is used to resolve what partial schema to use. So, in this case, we could have a partial schema with an alias: block-headline.
***
### reference
The `reference` are used to reference other views created from either this source entity or another source entity.
When referenced, Enterspeed will resolve the view when requested by the Delivery API so that the data will stay up-to-date if a reference view is updated.
The `reference` function is the starting point where you can use our fluent API to configure the reference(s) you want (by filter, by originId, take only top 5, and so on).
Reference schemas are typically used when you are mapping data from another entity. E.g. a page has a reference to another page entity or media entity.
Read more about reference schemas [here](/enterspeed/key-concepts/referencing-schemas)
`reference(schemaAlias)`
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ---------------------- |
| `schemaAlias` | string | The alias of a schema. |
#### Required function calls
After the `reference` function it's required to call one of the following functions to define what source entities you want references to.
Using the byOriginId function lets you create a reference to a source entity by its oringinId.
#### Parameters
| Parameter | Type | Description |
| ---------- | ------ | --------------------------------------- |
| `originId` | string | The original id from the source system. |
```js title="reference by byOriginId" theme={null}
contentTeaser: context
.reference("contentTeaser")
.byOriginId(sourceEntity.properties.link.id)
```
Using the byOriginIds function lets you create a references to a list of source entity by their oringinId.
#### Parameters
| Parameter | Type | Description |
| ----------- | --------- | ---------------------------------------------- |
| `originIds` | string\[] | A list of original ids from the source system. |
```js title="reference by byOriginIds" theme={null}
contentTeaser: context
.reference("contentTeaser")
.byOriginIds(sourceEntity.properties.links.map(link => link.id))
```
The children function creates a reference to all the children of the current source entity. It's basicly a shortcut for `.filter("originParentId eq '{sourceEntity.properties.originId}'")`.
```js title="reference by children" theme={null}
childPages: context.reference("page").children()
```
```js title="reference by children and type" theme={null}
childPages: context.reference("page").children("type eq 'subpage'")
```
The `children` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed.
The default limits are:
* Free & Premium plans: 100 items
* Enterprise plan: 500 items
Using the filter function lets you do a dynamic search for source entities you want to make references to.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | --------------------------------------------------------------------------------------------- |
| `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) |
```js title="reference by filter" theme={null}
newsTeasers: context
.reference("newsTeaser")
.filter("type eq 'newsArticle'")
```
The `filter` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed.
The default limits are:
* Free & Premium plans: 100 items
* Enterprise plan: 500 items
The parent function creates a reference to the parent of the current source entity. It's basicly a shortcut for `.filter("originId eq '{sourceEntity.properties.originParentId}'")`.
```js title="reference by parent" theme={null}
parentPage: context
.reference("page")
.parent()
```
The self method creates a reference to a view created by the specified schema on the current source entity. The helper method is equivalent to calling `.byOriginId(sourceEntity.originId)`.
```js title="reference by self" theme={null}
selfPage: context
.reference("page")
.self()
```
#### Optional function calls
To filter the source entities you are making references to even further you can call some of the following optional functions.
The `limit` function limits the number of references.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------- |
| `limit` | number | The maximum number of references to return. |
```js title="reference by filter and limit" theme={null}
topFiveNewsTeasers: context
.reference("newsTeaser")
.filter("type eq 'newsArticle'")
.limit(5)
```
The order sequence of references.
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------- | ------------------------------------------------- |
| `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. |
```js title="reference by filter and orderBy" theme={null}
newsArticles: context
.reference("newsArticle")
.filter("type eq 'newsArticle'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
```
The `sourceGroup` function lets you specify the source group. By default the source group of the current source entity is used.
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. |
If not defined, it uses the current source group.
```js title="reference by filter and sourceGroup" theme={null}
newsTeasers: context
.reference("newsTeaser")
.filter("type eq 'newsArticle'")
.sourceGroup("anotherSourceGroup")
```
The `first` function return the first item in the result set. The item i returned as an object instead of an array with one item.
The function must always be called as the last call in the chain.
```js title="reference to the first news teaser" theme={null}
newsTeaser: context
.reference("newsTeaser")
.filter("type eq 'newsArticle'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
.first()
```
#### Examples
```js title="Property type: reference (static value) with originId" theme={null}
seoData: context.reference("seo").byOriginId(sourceEntity.originId)
```
```js title="reference by filter and sourceGroup" theme={null}
products: context
.reference("product")
.filter("type eq 'product'")
.orderBy({ propertyName: name, direction: "asc"})
.limit(10)
.sourceGroup("commerce")
```
The `schemaAlias` can either be a static value, like "seo", or it can be a dynamic value being resolved from the Source Entity that is being processed by Enterspeed. This allows for supporting almost any use case.
#### Reference response
The response of references differs in V1 and V2+ of the delivery API. V1 return the id, type and wraps the properties from the referenced view in a `view` property. The response of references in V2+ is much cleaner and only returns the actual properties from the referenced schema. If a reference is not found in V2+, the reference information is added in the `missingViewReference` property in the `meta` object for debugging purpose.
```json title="Delivery API V1 uses a nested view property" theme={null}
{
// This example shows a response of a view with two schema references.
// image1 is referencing a found view with a url and name property
// and image2 is referencing a view that doesn't exist.
"meta": {
"missingViewReferences": []
},
"route": {
"image1": {
"id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1234/View/image",
"view": {
"url": "https://test.com/how-to-write-a-good-blog-post.png",
"name": "How To Write A Good Blog Post"
},
"type": "ViewReference"
},
"image2": {
"id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image",
"view": null,
"type": "ViewReference"
}
}
}
```
```json title="Delivery API V2+ only returns the actual view properties" theme={null}
{
// This example shows a response of a view with two schema references.
// image1 is referencing a found view with a url and name property
// and image2 is referencing a view that doesn't exist.
"meta": {
"missingViewReferences": [
{
"path": "image2",
"viewId": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image"
}
]
},
"route": {
"image1": {
"url": "https://test.com/how-to-write-a-good-blog-post.png",
"name": "How To Write A Good Blog Post"
},
"image2": null
}
}
```
# Routes
Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/routes
The `routes` method is where you define how you fetch the generated view from the [Delivery API](/api-reference/delivery)
If your view should be routable you must implement the `routes` method and call context methods to build routes.
```js title="Routes example" theme={null}
routes: function(sourceEntity, context) {
context.url(sourceEntity.url);
context.handle('origin-' + sourceEntity.originId);
}
```
# RoutesContext object
The `RoutesContext` object is passed into the `routes` method and gives you access to a set of methods which is described below.
## Methods
| Method | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [handle](#handle) | A handle is a key from which you can fetch the view. A view can have multiple handles. |
| [lookup](#lookup) | Lookup allows you to search source entities using a filter string and work with the source entities directly in the schema. |
| [url](#url) | A string value that represents the url you want to fetch the view by. A view can only have one url. |
### handle
Handle allows you to specify a key from which you can fetch the view from the Delivery API.
`handle(handle)`
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------------- |
| `handle` | string | The key you use to fetch the view from the Delivery API |
### lookup
Lookup allows you to search source entities using a filter string and build dynamic handles or URLS based on data from other source entities.
Note the `lookup` is an async function so you have to use [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) or [`then`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) when you are using the `lookup` function.
`lookup(filter)`
The `lookup` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed.
The default limits are:
* Free & Premium plans: 100 items
* Enterprise plan: 500 items
#### Parameters
| Parameter | Type | Description |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) |
| `lookupOptions` | object | An optional object with the following structure: `{ excludeProperties?: boolean; }`
If `excludeProperties` is set to `true` the `lookup` will only return the base fields of the source entities and not all custom properties. This will help improve performance if you don't need the custom properties, but only some of the base fields like `originId`, `originParentId`, `url` |
#### Required function calls
After the `lookup` function it's required to call `toPromise` to excecute the query and return a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
Using the toPromise function excecutes the query and return a promise you must resolve.
```js title="look toPromise" theme={null}
const category: await context
.lookup(`originId eq '${sourceEntity.properties.categoryId}'`)
.toPromise()
```
#### Optional function calls
To filter the source entities even further you can call some of the following optional functions.
The `limit` function limits the number of source entities.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------ |
| `limit` | number | The maximum number of source entities to return. |
```js title="look and limit" theme={null}
const mainCategory: await context
.lookup(`type eq 'mainCategory'`)
.limit(1)
.toPromise()
```
The order sequence of the source entities.
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------- | ------------------------------------------------- |
| `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. |
```js title="lookup and orderBy" theme={null}
const categories: await context
.lookup(`type eq 'category'`)
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
.toPromise()
```
The sourceGroup function lets you specify the source group. By default the source group of the current source entity is used.
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. |
If not defined, it uses the current source group.
```js title="lookup and sourceGroup" theme={null}
const categories: await context
.lookup(`type eq 'category'`)
.sourceGroup("anotherSourceGroup")
.toPromise()
```
#### Examples
```js title="lookup using async/await" theme={null}
routes: async function(sourceEntity, context) {
const categories = await context.lookup(`originId in (${sourceEntity.properties.categoryIds.map(c => `'${c}'`)})`).toPromise();
categories.forEach((category) => {
context.url(`${category.url}/${sourceEntity.properties.slug}`)
})
}
```
```js title="lookup using 'then'" theme={null}
routes: function(sourceEntity, context) {
context.lookup(`originId in (${sourceEntity.properties.categoryIds.map(c => `'${c}'`)})`)
.toPromise()
.then((categories) => {
categories.forEach((category) => {
context.url(`${category.url}/${sourceEntity.properties.slug}`)
})
});
}
```
### url
url allows you to specify a url from which you can fetch the view from the delivery.
`url(url)`
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------------- |
| `url` | string | The url you use to fetch the view from the Delivery API |
##### Optional function calls
To the `url` function you can call some of the following optional functions.
The `redirects` function creates incomming redirects to a specific url.
Setting the redirects to an empty array clears any potential implicit redirects.
#### Parameters
| Parameter | Type | Description |
| ----------- | --------- | ---------------------------------------------- |
| `redirects` | string\[] | Adds a list of incomming redirects to the URL. |
```js title="routes with url and redirects" theme={null}
context
.url(sourceEntity.url)
.redirects(sourceEntity.redirects);
```
## Examples
```js title="routes example with url and multiple handles" theme={null}
routes: function(sourceEntity, context) {
context.url(sourceEntity.url);
context.handle('origin-' + sourceEntity.originId);
context.handle(sourceEntity.properties.entityKey);
}
```
```js title="routes example with single url" theme={null}
routes: function(sourceEntity, context) {
context.url(sourceEntity.url);
}
```
```js title="routes can also be expressed as an arrow function expression to make it even more compact" theme={null}
routes: (sourceEntity, context) => context.url(sourceEntity.url)
```
You can then fetch the view using our [Delivery API](/api-reference/delivery) using either the url or one of the handles.
You can also fetch multiple views in one request, although in this case it doesn't make sense to fetch the same view three time, but just to demonstrate if you want to fetch multiple different views in one request.
```
https://delivery.enterspeed.com/v2
?url=/fairy-tales/the-emperors-new-clothes/
&handle=origin-1234
&handle=5678
```
# Triggers
Source: https://docs.enterspeed.com/enterspeed/reference/js/full-schema/triggers
The `triggers` method is where you define the source group and the types the schema should process.
You can add multiple triggers to a schema.
```js title="Triggers example" theme={null}
triggers: function(context) {
context.triggers('cms', ['contentPage', 'articlePage']);
context.triggers('pim', ['product']);
}
```
# TriggersContext object
The `TriggersContext` object is passed into the `triggers` method and gives you access to a set of methods which is described below.
## Methods
| Method | Description |
| ----------------------- | ------------------------------------------------------------------------------- |
| [triggers](#triggers-1) | Defines the source group and the source entity types the schema should process. |
### triggers
Defines the source group and the source entity types the schema should process.
`triggers(sourceGroupAlias, sourceEntityTypes)`
#### Parameters
| Parameter | Type | Description |
| ------------------- | --------- | ---------------------------------------------------------------- |
| `sourceGroupAlias` | string | The alias of the source group you want your schema to trigger on |
| `sourceEntityTypes` | string\[] | A list of source entity types you want your schema to trigger on |
## Examples
```js title="example of triggers usage" theme={null}
triggers: function(context) {
context.triggers('cms', ['contentPage', 'articlePage']);
}
```
```js title="triggers can also be expressed as an arrow function expression to make it even more compact" theme={null}
triggers: (context) => context.triggers('cms', ['contentPage', 'articlePage'])
```
# Index
Source: https://docs.enterspeed.com/enterspeed/reference/js/index-schema/index-method
The `index` object is where you define the structure of the index. The index defines what kind of data you can add to the index in the [properties](/enterspeed/reference/js/index-schema/properties) method.
```js title="Index example" theme={null}
index: {
fields: {
sku: { type: "keyword" },
title: { type: "text" },
description: { type: "text" },
isActive: { type: "boolean" }
}
}
```
The index object must have a `fields` property with the fields you want to include in the index.
Each field object must include `type`. The `type` specifies the data type for the index field.
## Metadata descriptions
### Index
The `metadata` property on the index is optional.
Use it to add a description to the index. The description is exposed by the [Enterspeed Query MCP](/enterspeed/mcp-server/query-mcp/overview) to give AI agents meaningful context about what the index contains.
```js title="Index description example" theme={null}
index: {
fields: { },
metadata: { description: "The product index contains all products available in the assortment" }
}
```
### Field
The `metadata` property on a field is optional.
Use it to add a description to the field. The description is exposed by the [Enterspeed Query MCP](/enterspeed/mcp-server/query-mcp/overview) to give AI agents meaningful context about what the field is used for.
*The max character length of the field description is 50 characters.*
```js title="Field description example" theme={null}
index: {
fields: {
isActive: { type: "boolean", metadata: { description: "A boolean value indicating if the SKU is active" } }
}
}
```
## Types
Setting the right types for the properties in your index is important. The types defines the intend of the fields and prevents data of other types from going into the index. The types also helps with effeciently index, search, and analyze of the data added to the index.
### Text
| Type | Description |
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| keyword | The `Keyword` type is for non-analyzed string values used in e.g. filtering and sorting |
| text | The `text` type will analyze string values. This makes it possible to do fuzzy search using the `search` property in the [Query API](/api-reference/query/query-items) but index time takes a bit longer |
### Numeric
| Type | Description |
| ------- | ------------------------------- |
| integer | A 32 bit signed integer |
| long | A 64 bit signed integer |
| float | A 32 bit signed floating number |
| double | A 64 bit signed floating number |
### Boolean
| Type | Description |
| ------- | ------------------------ |
| boolean | A `true` / `false` value |
### Date
| Type | Description |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| date | Support values like a Javascript Date object, a string like `2024-11-21` or `2024-11-21T23:00:00Z` or number of milliseconds since eclipse |
### List
| Type | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| keyword\[] | List of the `Keyword` type for non-analyzed string values used in e.g. filtering and sorting |
| text\[] | List of the `text` type for analyzing string values. This makes it possible to do fuzzy search using the `search` property in the [Query API](/api-reference/query/query-items) but index time takes a bit longer |
| integer\[] | List of 32 bit signed integers |
| long\[] | List of 64 bit signed integers |
| float\[] | List of 32 bit signed floating numbers |
| double\[] | List of 64 bit signed floating numbers |
| boolean\[] | List of `true` / `false` values |
| date\[] | List of dates with supported values like a Javascript Date object, strings like `2024-11-21T23:00:00Z` or numbers of milliseconds since eclipse |
### Range
| Type | Description |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| integerRange | Must be an object like `{"gt": 10, "lt": 20}` or `{"gte": 10, "lte": 20}` |
| longRange | Must be an object like `{"gt": 10, "lt": 20 }` or `{"gte": 10, "lte": 20}` |
| floatRange | Must be an object like `{"gt": 10.0, "lt": 20.0}` or `{"gte": 10.0, "lte": 20.0}` |
| doubleRange | Must be an object like `{"gt": 10.0, "lt": 20.0}` or `{"gte": 10.0, "lte": 20.0}` |
| dateRange | Must be an object like `{"gt": "2025-01-01", "lt": "2025-02-01"}` or `{"gte": "2025-01-01", "lte": "2025-02-01"}` |
### Object
| Type | Description |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| object | Supports any value of an object type, this object is not indexed and therefore not searchable, it can be used to associate a complex data type to an index item. |
# Intro
Source: https://docs.enterspeed.com/enterspeed/reference/js/index-schema/intro
An index schema defines the structure of a Query index as well as the mapping of the items that goes into the index which are queryable from the [Query API](/api-reference/query).
```js title="JavaScript index schema example" theme={null}
/** @type {Enterspeed.IndexSchema} */
export default {
triggers: function(context) {
context.triggers('pim', ['product'])
},
index: {
fields: {
sku: { type: "keyword" },
title: { type: "text" },
description: { type: "text" },
isActive: { type: "boolean" }
}
},
properties: function (sourceEntity) {
return {
sku: sourceEntity.properties.sku,
title: sourceEntity.properties.title,
description: sourceEntity.properties.description,
isActive: sourceEntity.properties.isActive
}
}
}
```
# Properties
Source: https://docs.enterspeed.com/enterspeed/reference/js/index-schema/properties
The `properties` method is where you define the output of the items that goes into index you can query from the [Query API](/api-reference/query)
```js title="Properties example" theme={null}
properties: function (sourceEntity) {
return {
sku: sourceEntity.properties.sku,
title: sourceEntity.properties.title,
description: sourceEntity.properties.description,
isActive: sourceEntity.properties.isActive
};
}
```
The properties method must return an object with the values for the fields you defined in the [index](/enterspeed/reference/js/index-schema/index-method) object.
# Triggers
Source: https://docs.enterspeed.com/enterspeed/reference/js/index-schema/triggers
The `triggers` method is where you define the source group and the types the schema should process.
You can add multiple triggers to a schema.
```js title="Triggers example" theme={null}
triggers: function(context) {
context.triggers('cms', ['contentPage', 'articlePage']);
context.triggers('pim', ['product']);
}
```
# TriggersContext object
The `TriggersContext` object is passed into the `triggers` method and gives you access to a set of methods which is described below.
## Methods
| Method | Description |
| ----------------------- | ------------------------------------------------------------------------------- |
| [triggers](#triggers-1) | Defines the source group and the source entity types the schema should process. |
### triggers
Defines the source group and the source entity types the schema should process.
`triggers(sourceGroupAlias, sourceEntityTypes)`
#### Parameters
| Parameter | Type | Description |
| ------------------- | --------- | ---------------------------------------------------------------- |
| `sourceGroupAlias` | string | The alias of the source group you want your schema to trigger on |
| `sourceEntityTypes` | string\[] | A list of source entity types you want your schema to trigger on |
## Examples
```js title="example of triggers usage" theme={null}
triggers: function(context) {
context.triggers('cms', ['contentPage', 'articlePage']);
}
```
```js title="triggers can also be expressed as an arrow function expression to make it even more compact" theme={null}
triggers: (context) => context.triggers('cms', ['contentPage', 'articlePage'])
```
# Intro
Source: https://docs.enterspeed.com/enterspeed/reference/js/intro
JavaScript is the default and prefered schema format in Enterspeed.
With JavaScript schemas, you can build your schemas in a standard language most developers are already familiar with. This means that you have all the power and flexibility from JavaScript available when you are creating your schemas.
```js title="JavaScript schema example" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['page']);
},
routes: function(sourceEntity, context) {
context.url(sourceEntity.url);
},
properties: function (sourceEntity, context) {
const p = sourceEntity.properties;
return {
title: p.title,
blocks: context.partial("blocks", p.blocks),
aboutUsPage: context.reference("page").byOriginId(p.aboutUsPage.id),
};
}
}
```
The concepts in JavaScript schemas are similar to the concepts from JSON schemas, with triggers, route, properties and so on, and since the source entity and a `context` object (used for making references, partials, etc.) are passed in as parameters you can even create unit tests of your schemas if you want to.
## Destructuring
You can destruct parameters, so the `routes` and `properties` methods in the above example can be simplified to:
```js title="JavaScript destruct schema example" highlight={6,9} theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['page']);
},
routes: function({url}, context) {
context.url(url);
},
properties: function ({properties: p}, context) {
return {
title: p.title,
blocks: context.partial('blocks', p.blocks),
aboutUsPage: context.reference('page').byOriginId(p.aboutUsPage.id)
};
}
}
```
## Arrow function expression
You can use arrow function expression to simplify or make your schema even more compact:
```js title="JavaScript arrow function expression schema example" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: (context) => context.triggers('cms', ['page']),
routes: ({url}, context) => context.url(url),
properties: ({properties: p}, context) =>({
title: p.title,
blocks: context.partial('blocks', p.blocks),
aboutUsPage: context.reference('page').byOriginId(p.aboutUsPage.id)
})
}
```
## Console object
When debugging JavaScript it's often useful to use the [console object](https://developer.mozilla.org/en-US/docs/Web/API/console) to print out values to the console.
When using the Test schema feature in the Enterspeed Management App, the following methods are supported:
* debug
* error
* info
* log
* trace
* warn
```js title="JavaScript schema with conole usage" highlight={10} theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['page']);
},
routes: function({url}, context) {
context.url(url);
},
properties: function ({properties: p}, context) {
console.log('block type', p.block.type)
return {
title: p.title,
block: context.partial(`block-${p.block.type}`, p.block),
aboutUsPage: context.reference('page').byOriginId(p.aboutUsPage.id)
};
}
}
```
If you use any of the other native console methods, the schema is still working but the methods will just not output anything.
## Limitations
Now, we said that you have all the power of the JavaScript language available for you in your JavaScript schemas, but we have added some limitations because of security.
This means that the following areas has been restricted.
* No access to the filesystem
* No network traffic
* Maximum processing time of 60 sec pr schema
# JSDoc
Source: https://docs.enterspeed.com/enterspeed/reference/js/jsdoc
By default, JavaScript schemas are created with a `@type` expression, describing the type of schema and providing IntelliSense in the editor using [JSDoc](https://jsdoc.app/).
```js title="@type expression" theme={null}
/** @type {Enterspeed.FullSchema} */
```
With the `@type` expression, you get IntelliSense on all the functions in your schema, the parameters (sourceEntity and context objects), and the return types.
However, since Enterspeed is so flexible and you can ingest all types of data, Enterspeed doesn't know about the custom properties you have ingested in the source entities and therefore can't provide IntelliSense on these properties by default.
The schema types are publicly available from our NPM package: [@enterspeed/js-schema-types](https://www.npmjs.com/package/@enterspeed/js-schema-types).
## Creating your own type definitions
All schema types also come in a generic version where you can provide the type of your source entity described in JSDoc. Hereby enabling IntelliSense for all your custom properties of a source entity.
First, you need to create a [type definition](https://jsdoc.app/tags-typedef) describing your source entity.
```js title="@type definition" theme={null}
/** @typedef {object} ContentPage
* @property {string} title
* @property {string} content
* @property {object} metaData
* @property {boolean} metaData.isPublished
* @property {number} metaData.sortOrder
*/
```
If you prefer not to create your type definitions manually, there are many free only tools available that can generate them for you. Simply paste the JSON from your source entity into your chosen tool.
[transform.tools](https://transform.tools/json-to-jsdoc) is a great example.
## Apply a custom type definition
In order to use your custom type definition you simply use the generic version of the schema type you are working with (FullSchema, PartialSchema, ...).
```js title="Generic @type expression" theme={null}
/** @type {Enterspeed.FullSchema} */
```
You now have IntelliSense on your custom source entity properties.
## Lookups and custom functions
The generic version of the schema type only describes the source entity passed into the schema. But if you do lookups in your schema, you can also use JSDoc to describe the source entities you fetch with lookup.
```js title="Lookup call with defined return type" theme={null}
const products = /** @type {Enterspeed.ISourceEntity[]} */
(await context.lookup("type eq 'product'").toPromise());
```
For custom functions you can also describe the parameters and return type.
```js title="Function with described parameters" theme={null}
/**
* @param {Enterspeed.ISourceEntity} sourceEntity
* @returns {boolean}
*/
function isProductAvailable(sourceEntity) {
return sourceEntity.properties.status == 'available'
&& sourceEntity.properties.stockCount > 0;
}
```
# Intro
Source: https://docs.enterspeed.com/enterspeed/reference/js/partial-schema/intro
A partial schema is a reusable component that can be used across multiple schemas, e.g. if you have repetitive logic or if you just want to seperate out some of the schema logic into smaller components. A partial schema does not generete views on it's own, instead the output is embedded in the schema wehere the partial schema i referenced.
```js title="JavaScript partial schema example" theme={null}
/** @type {Enterspeed.PartialSchema} */
export default {
properties: function (input, context) {
return {
seoTitle: input.seoTitle,
seoDescription: input.seoDescription
}
}
}
```
# Properties
Source: https://docs.enterspeed.com/enterspeed/reference/js/partial-schema/properties
The `properties` method is where you define the output that is embedded where the partial schema is referenced.
```js title="Properties example" theme={null}
properties: function (input, context) {
return {
title: input.properties.title,
seo: {
title: input.properties.seoTitle,
description: input.properties.seoDescription,
}
categoryIds: input.properties.categoryIds
.map(categoryId => parseInt(categoryId))
};
}
```
The properties method must return an object with the data you want to return.
# PropertiesContext object
The `PropertiesContext` object is passed into the `properties` method and gives you access to a set of methods described below.
## Methods
| Method | Description |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [lookup](#lookup) | Lookup allows you to search source entities using a filter string and work with the source entities directly in the schema. |
| [partial](#partial) | Referencing a partial schema. Mapped data from a partial schema is embedded into the calling schema. |
| [reference](#reference) | Referencing a full schema. References to other schemas are resolved on delivery request time. |
### lookup
Lookup source entities.
Lookup allows you to search source entities using a filter string and work with the data from the source entities directly in the schema.
Note the `lookup` is an async function so you have to use [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) when you are using the `lookup` function.
`lookup(filter)`
The `lookup` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed.
The default limits are:
* Free & Premium plans: 100 items
* Enterprise plan: 500 items
#### Parameters
| Parameter | Type | Description |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) |
| `lookupOptions` | object | An optional object with the following structure: `{ excludeProperties?: boolean; }`
If `excludeProperties` is set to `true` the `lookup` will only return the base fields of the source entities and not all custom properties. This will help improve performance if you don't need the custom properties, but only some of the base fields like `originId`, `originParentId`, `url` |
#### Required function calls
After the `lookup` function it's required to call `toPromise` to excecute the query and return a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
Using the toPromise function excecutes the query and return a promise you must resolve.
```js title="look toPromise" theme={null}
const newsArticles: await context
.lookup("type eq 'newsArticle'")
.toPromise()
```
#### Optional function calls
To filter the source entities even further you can call some of the following optional functions.
The `limit` function limits the number of source entities.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------------ |
| `limit` | number | The maximum number of source entities to return. |
```js title="look and limit" theme={null}
const newsArticles: await context
.lookup("type eq 'newsArticle'")
.limit(5)
.toPromise()
```
The order sequence of the source entities.
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------- | ------------------------------------------------- |
| `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. |
```js title="lookup and orderBy" theme={null}
const newsArticles: await context
.lookup("type eq 'newsArticle'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
.toPromise()
```
The sourceGroup function lets you specify the source group. By default the source group of the current source entity is used.
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. |
If not defined, it uses the current source group.
```js title="lookup and sourceGroup" theme={null}
const newsArticles: await context
.lookup("type eq 'newsArticle'")
.sourceGroup("anotherSourceGroup")
.toPromise()
```
#### Examples
```js title="lookup with async/await" theme={null}
properties: async function (input, context) {
const latestNews = await context
.lookup("type eq 'newsArticle'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
.limit(3)
.toPromise();
const mappedNews = latestNews
.map((news) => ({
url: news.url,
title: news.properties.title,
teaser: news.properties.teaser
}));
return {
latestNews: mappedNews
}
}
```
```js title="lookup without custom properties" theme={null}
properties: async function (input, context) {
const baseNewsArticles = await context
.lookup("type eq 'newsArticle'", { excludeProperties: true })
.limit(3)
.toPromise();
const mappedNews = baseNewsArticles
.map((news) => ({
originId: news.originId,
url: news.url
}));
return {
latestNews: mappedNews
}
}
```
***
### partial
Referencing a partial schema.
The partial mapping property type allows for dynamically including partial schemas into the main schema. This is useful when you need to iterate an array of different objects that has an identifier, like an ID, alias or similar.
Partial schemas are typically used when you want a reusable schema for mapping data that is part of the same entity. E.g. if metadata (title, description, etc.) is the same across different entity types, but the data lives on the entity itself.
Read more about partial schemas [here](/enterspeed/key-concepts/partial-schemas)
`partial(schemaAlias, input)`
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ------------------------------------------------------------ |
| `schemaAlias` | string | The alias of a partial schema. |
| `input` | object | You can pass whatever data you need for your partial schema. |
#### Examples
```js title="partial" theme={null}
contentBlocks: input.properties.contentBlocks.map((contentBlock) =>
context.partial(`block-${contentBlock.contentType}`, contentBlock)
)
```
```js title="passing extra properties to the partial schema" theme={null}
contentBlocks: input.properties.contentBlocks.map((contentBlock) =>
context.partial(`block-${contentBlock.contentType}`, {
block: contentBlock,
pageOriginId: input.originId
})
)
```
The `input` defines what goes into the partial schema and can be any custom object, and the `schemaAlias` is used to resolve what partial schema to use. So, in this case, we could have a partial schema with an alias: block-headline.
***
### reference
The `reference` are used to reference other views created from either this source entity or another source entity.
When referenced, Enterspeed will resolve the view when requested by the Delivery API so that the data will stay up-to-date if a reference view is updated.
The `reference` function is the starting point where you can use our fluent API to configure the reference(s) you want (by filter, by originId, take only top 5, and so on).
Reference schemas are typically used when you are mapping data from another entity. E.g. a page has a reference to another page entity or media entity.
Read more about reference schemas [here](/enterspeed/key-concepts/referencing-schemas)
`reference(schemaAlias)`
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ---------------------- |
| `schemaAlias` | string | The alias of a schema. |
#### Required function calls
After the `reference` function it's required to call one of the following functions to define what source entities you want references to.
Using the byOriginId function lets you create a reference to a source entity by its oringinId.
#### Parameters
| Parameter | Type | Description |
| ---------- | ------ | --------------------------------------- |
| `originId` | string | The original id from the source system. |
```js title="reference by byOriginId" theme={null}
contentTeaser: context
.reference("contentTeaser")
.byOriginId(input.properties.link.id)
```
Using the byOriginIds function lets you create a references to a list of source entity by their oringinId.
#### Parameters
| Parameter | Type | Description |
| ----------- | --------- | ---------------------------------------------- |
| `originIds` | string\[] | A list of original ids from the source system. |
```js title="reference by byOriginIds" theme={null}
contentTeaser: context
.reference("contentTeaser")
.byOriginIds(input.properties.links.map(link => link.id))
```
The children function creates a reference to all the children of the current source entity. It's basicly a shortcut for `.filter("originParentId eq '{input.properties.originId}'")`.
```js title="reference by children" theme={null}
childPages: context.reference("page").children()
```
```js title="reference by children and type" theme={null}
childPages: context.reference("page").children("type eq 'subpage'")
```
The `children` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed.
The default limits are:
* Free & Premium plans: 100 items
* Enterprise plan: 500 items
Using the filter function lets you do a dynamic search for source entities you want to make references to.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | --------------------------------------------------------------------------------------------- |
| `filter` | string | A filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) |
```js title="reference by filter" theme={null}
newsTeasers: context
.reference("newsTeaser")
.filter("type eq 'newsArticle'")
```
The `filter` function is limited to a maximum number of items to return, based on the tenant plan. It can however be increased per tenant basis. Please reach out to Enterspeed if needed.
The default limits are:
* Free & Premium plans: 100 items
* Enterprise plan: 500 items
The parent function creates a reference to the parent of the current source entity. It's basicly a shortcut for `.filter("originId eq '{input.properties.originParentId}'")`.
```js title="reference by parent" theme={null}
parentPage: context
.reference("page")
.parent()
```
The self method creates a reference to a view created by the specified schema on the current source entity. The helper method is equivalent to calling `.byOriginId(input.originId)`.
```js title="reference by self" theme={null}
selfPage: context
.reference("page")
.self()
```
#### Optional function calls
To filter the source entities you are making references to even further you can call some of the following optional functions.
The `limit` function limits the number of references.
#### Parameters
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------------- |
| `limit` | number | The maximum number of references to return. |
```js title="reference by filter and limit" theme={null}
topFiveNewsTeasers: context
.reference("newsTeaser")
.filter("type eq 'newsArticle'")
.limit(5)
```
The order sequence of references.
#### Parameters
| Parameter | Type | Description |
| --------- | ----------------------------------------------------- | ------------------------------------------------- |
| `orderBy` | \{ propertyName: string, direction: "asc" \| "desc" } | Allows you to specify your desired sorting order. |
```js title="reference by filter and orderBy" theme={null}
newsArticles: context
.reference("newsArticle")
.filter("type eq 'newsArticle'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
```
The `sourceGroup` function lets you specify the source group. By default the source group of the current source entity is used.
#### Parameters
| Parameter | Type | Description |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sourceGroup` | string | Allows you to define a different source group. The sourceGroupAlias should be equal to the desired source group alias where you want to look for source entities. |
If not defined, it uses the current source group.
```js title="reference by filter and sourceGroup" theme={null}
newsTeasers: context
.reference("newsTeaser")
.filter("type eq 'newsArticle'")
.sourceGroup("anotherSourceGroup")
```
The `first` function return the first item in the result set. The item i returned as an object instead of an array with one item.
The function must always be called as the last call in the chain.
```js title="reference to the first news teaser" theme={null}
newsTeaser: context
.reference("newsTeaser")
.filter("type eq 'newsArticle'")
.orderBy({ propertyName: "properties.createdDate", direction: "desc"})
.first()
```
#### Examples
```js title="Property type: reference (static value) with originId" theme={null}
seoData: context.reference("seo").byOriginId(input.originId)
```
```js title="reference by filter and sourceGroup" theme={null}
products: context
.reference("product")
.filter("type eq 'product'")
.orderBy({ propertyName: name, direction: "asc"})
.limit(10)
.sourceGroup("commerce")
```
The `schemaAlias` can either be a static value, like "seo", or it can be a dynamic value being resolved from the Source Entity that is being processed by Enterspeed. This allows for supporting almost any use case.
#### Reference response
The response of references differs in V1 and V2+ of the delivery API. V1 return the id, type and wraps the properties from the referenced view in a `view` property. The response of references in V2+ is much cleaner and only returns the actual properties from the referenced schema. If a reference is not found in V2+, the reference information is added in the `missingViewReference` property in the `meta` object for debugging purpose.
```json title="Delivery API V1 uses a nested view property" theme={null}
{
// This example shows a response of a view with two schema references.
// image1 is referencing a found view with a url and name property
// and image2 is referencing a view that doesn't exist.
"meta": {
"missingViewReferences": []
},
"route": {
"image1": {
"id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1234/View/image",
"view": {
"url": "https://test.com/how-to-write-a-good-blog-post.png",
"name": "How To Write A Good Blog Post"
},
"type": "ViewReference"
},
"image2": {
"id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image",
"view": null,
"type": "ViewReference"
}
}
}
```
```json title="Delivery API V2+ only returns the actual view properties" theme={null}
{
// This example shows a response of a view with two schema references.
// image1 is referencing a found view with a url and name property
// and image2 is referencing a view that doesn't exist.
"meta": {
"missingViewReferences": [
{
"path": "image2",
"viewId": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image"
}
]
},
"route": {
"image1": {
"url": "https://test.com/how-to-write-a-good-blog-post.png",
"name": "How To Write A Good Blog Post"
},
"image2": null
}
}
```
# Expression
Source: https://docs.enterspeed.com/enterspeed/reference/json/expression
Expressions will often be used as simple placeholders to map data from the **source entity**.
Most values in schemas and partial schemas can be expressed. An expression is identified by using curly brackets `{}`:
```json theme={null}
"headline": "{p.title}"
```
A value that support expression can have multiple expressions:
```json theme={null}
"headline": "{p.title}: {p.subTitle}"
```
In combination with regular text:
```json theme={null}
"headline": "Blog post: {p.title}"
```
## Null check
Trying to access properties of a none existing object will cause the view generation to fail.
If that's not intended add a null check using `?`.
In the following example `headline` will be set to the value of `p.meta.description`.
If `p.meta` is null (or doesn't exist) `headline`.
```json theme={null}
"description": "{p.meta?.description}"
```
### Null coalescing
```json theme={null}
"headline": "{p.title ?? p.header}"
```
Combined with null check:
```json theme={null}
"headline": "{p.meta?.description ?? p.description}"
```
The expression can also be grouped using parentheses:
```json theme={null}
"x": "{(p.a ?? p.b) ?? p.c}"
```
## Accessing properties
Accessing properties can be done by typing `p.` followed by the name of the property.
```json title="Property with default type (string)" theme={null}
{
"triggers": {
"umbraco": ["frontPage"]
},
"route": {
"url": "{url}"
},
"properties": {
"headline": "{p.title}"
}
}
```
The default type of a property is a **string**. If you need another property type, simply change your property to an object and use `type` and `value`.
```json title="Property with type number" theme={null}
{
"triggers": {
"umbraco": ["frontPage"]
},
"route": {
"url": "{url}"
},
"properties": {
"stock": {
"type": "number",
"value": "{p.inventoryQuantity}"
}
}
}
```
# Path selector
Source: https://docs.enterspeed.com/enterspeed/reference/json/path-selector
The path selector is based on JSON Path. It can be used to select and filter data from the source entity.
This is available as input for [array](./property-types#path-input) and for the [dynamic property type](./property-types#dynamic) to map data from the source entity.
For dynamic mapping, the selector should result in a property value (single token).
When using `$path` for the array input the result is expected to be a list of property values (multiple tokens).
Mapping all source entity properties:
```json theme={null}
"product": {
"*": "p"
}
```
Consider, that you want the entire list of product features:
```json theme={null}
"features": {
"*": "p.features"
}
```
If you only want the `name` of the features:
```json theme={null}
"tabs": {
"type": "array",
"input": {
"$path": "p.features[*]"
},
"var": "feature",
"items": {
"type": "string",
"value": "{item.name}"
}
}
```
If you want all features with its `display` property equals true:
```json theme={null}
"tabs": {
"type": "array",
"input": {
"$path": "p.features[?(@.display==true)]"
},
"var": "feature",
"items": {
"type": "string",
"value": "{item.name}"
}
}
```
You can combine dynamic mapping and arrays:
```json theme={null}
"tabs": {
"type": "array",
"input": {
"$path": "p.features[?(@.display==true)]"
},
"var": "feature",
"items": {
"type": "object",
"feature": {
"*": "{feature}"
}
}
}
```
If you want the feature with its `id` property equals to a dynamic value:
```json theme={null}
"tabs": {
"type": "array",
"input": {
"$path": "p.features[?(@.id=='{p.primaryFeatureId}')]"
},
"var": "feature",
"items": {
"type": "string",
"value": "{item.name}"
}
}
```
# Property types
Source: https://docs.enterspeed.com/enterspeed/reference/json/property-types
| Property | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [String](#string) | Basic string mapping.
Accepts the following fields: `type` `value` `default`
Note: `type` and `value` are required if you aren't using the shorthand - e.g. `"title": "{p.headline}"` |
| [Number](#number) | Basic number or integer mapping.
Required fields: `type` `alias` - use `id` **OR** `originId`
Optional fields: `source` |
| [Partial](#partial) | Referencing a partial schema to map the data into.
Required fields: `type` `input` `alias` |
| [Dynamic](#dynamic) | Dynamic mapping using the [path selector](./path-selector).
Required fields: `*` |
There is no `Date` property type. But if you ingest your dates as strings in the following format `yyyy-MM-ddTHH:mm:ss`, you can still sort by date and the value can easily be parsed to Date on the client in JavaScript and other languages.
## string
Basic string mapping.
Since string is the most commonly used property type you can access it without writing the `type` and `value` property. This is meant as syntactic sugar making it easier and quicker to work with.
### Fields
| Property | Required? | Description |
| --------- | --------- | --------------------------------------------------------------- |
| `type` | **Yes** | The property type - here `string` |
| `value` | **Yes** | The value of the property |
| `default` | No | The default value is used if `value` is null or an empty string |
### Examples
```json title="Property type: string" theme={null}
"title": "{p.headline}"
```
```json title="Property type: string with value and default" theme={null}
"title": {
"type": "string",
"value": "{p.headline}",
"default": "Unknown title"
}
```
***
## number
Basic number or integer mapping.
The size of the number is limited to the size of a [C# double](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types#characteristics-of-the-floating-point-types).
### Fields
| Property | Required? | Description |
| ----------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | **Yes** | The property type - here `number` |
| `value` | **Yes** | The value of the property |
| `default` | No | The default value is used if `value` is null or can be parsed as a number |
| `precision` | No | Rounds a decimal value to a specified number of fractional digits, and rounds midpoint values to the nearest even number. Default value is 0 |
### Examples
```json title="Property type: number" theme={null}
"stock": {
"type": "number",
"value": "{p.inventoryQuantity}"
}
```
***
## boolean
Basic boolean mapping.
### Fields
| Property | Required? | Description |
| --------- | --------- | -------------------------------------------------------------------------- |
| `type` | **Yes** | The property type - here `boolean` |
| `value` | **Yes** | The value of the property |
| `default` | No | The default value is used if `value` is null or can be parsed as a boolean |
### Examples
```json title="Property type: boolean" theme={null}
"isPublished": {
"type": "boolean",
"value": "{p.published}",
"default": true
}
```
***
## array
Mapping of an array, defining the input to iterate and the items definition.
Array property type is designed for working with collections.
### Fields
| Property | Required? | Description |
| -------- | --------- | ------------------------------------------------------------------------------------------------------------------ |
| `type` | **Yes** | property type - here `array` |
| `input` | **Yes** | States input, where to retrieve items collection to work with from. Support input types: `string` `$exp` `$lookup` |
| `items` | No | Used for mapping results. |
| `var` | No | Collection iteration variable name. Default value is - `item`. |
### `string` type
A simple example showing how to use a `string` type in `items`.
```json title="string simple example" theme={null}
"categories": {
"type": "array",
"input": "{p.categories}",
"items": {
"type": "string",
"value": "{item}"
}
},
```
### `$exp` input
With expression input, you can reference the desired property on the source entity that is an array.
```json title="$exp input" theme={null}
"tabs": {
"type": "array",
"input": "{p.tabs}",
"var": "tab",
"items": {
"type": "object",
"properties": {
"title": "{tab.title}",
"content": "{tab.content}"
}
}
}
```
### `$path` input
With path input, you can select the desired items on the source entity using the [path selector](./path-selector).
```json title="$path input" theme={null}
"tabs": {
"type": "array",
"input": {
"$path": "p.tabs[*]"
},
"var": "tab",
"items": {
"type": "object",
"properties": {
"title": "{tab.title}",
"content": "{tab.content}"
}
}
}
```
Filter items:
```json title="$path input with filter" theme={null}
"tabs": {
"type": "array",
"input": {
"$path": "p.tabs[?(@.display==true)]"
},
"var": "tab",
"items": {
"type": "object",
"properties": {
"title": "{tab.title}",
"content": "{tab.content}"
}
}
}
```
### `$lookup` input
Lookup input comparing to \$exp allows you to define query-like and criteria match source entities lookup conditions.
#### `$lookup` with single property value match
| Property | Required? | Description |
| -------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| operator | **Yes** | The operator for the lookup. Supported operators: `equals` `contains` |
| sourceEntityProperty | **Yes** | The property to match the value on |
| matchValue | **Yes** | The value to match the sourceEntityProperty |
| sourceEntityType | No | Type of source entity to use for lookup. Default value is include all entity types available: `*` |
| orderBy | No | Allows you to specify your desired sorting order |
| top | No | Allows limiting the size of items collection. Can be a number, a number as a text, or an expression. |
| source | No | Allows you to define a different source as the property. The `source` should be equal to the desired source group alias, where you want to look for source entities.
If not defined, it uses the current source group. |
```json title="$lookup with a single property" theme={null}
"navigationItems": {
"type": "array",
"input": {
"$lookup": {
"operator": "equals",
"sourceEntityType": "*",
"sourceEntityProperty": "originParentId",
"matchValue": "{originId}",
"orderBy": {
"property": "properties.metaData.sortOrder",
"sort": "asc"
},
"top": 5
}
},
"items": {
"type": "reference",
"id": "{input.id}",
"alias": "NavigationItem"
}
}
```
#### `$lookup` with filter
Particular lookup filter type allows you to be more flexible with your matching criteria.
| Property | Required? | Description |
| -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| filter | **Yes** | Your filtering criteria. [See list of filter examples](/enterspeed/reference/filter-expressions) |
| orderBy | No | Allows you to specify your desired sorting order |
| top | No | Allows limiting the size of items collection. Can be a number, a number as a text, or an expression. |
| source | No | Allows you to define a different source as the property. The `source` should be equal to the desired source group alias, where you want to look for source entities.
If not defined, it uses the current source group. |
### Additional
Arrays have some additional properties available:
* `root`
For schemas it will contain the source entity and for partial schemas the input.
It is possible to access everything from the source entity like `root.originId` or `root.properties.headline`.
All property values in `items` that supports expressions can access `root`.
```json theme={null}
"tabs": {
"type": "array",
"input": "{p.tabs}",
"var": "tab",
"items": {
"type": "object",
"properties": {
"title": "{root.p.headline}: {tab.title}",
"content": "{tab.content}"
}
}
}
```
* `parent`
If having multidimensional arrays `parent` can be used to access items of the parent array.
For the first array `parent` will be equal to `root`. For the next levels `parent` will be equal to `item` of the parent array.
All property values in `items` that supports expressions can use `parent`.
```json theme={null}
"tabs": {
"type": "array",
"input": "{p.tabs}",
"var": "tab",
"items": {
"type": "object",
"properties": {
"title": "{tab.title}",
"content": "{tab.content}",
"subTabs": {
"type": "array",
"input": "{tab.tabs}",
"var": "subTab",
"items": {
"title": "{parent.title}: {subTab.title}",
"content": "{subTab.content}",
}
}
}
}
}
```
***
## object
Mapping of an object.
### Fields
| Property | Required? | Description |
| ------------ | --------- | --------------------------------- |
| `type` | **Yes** | The property type - here `object` |
| `properties` | **Yes** | The properties of the object |
### Examples
```json title="Property type: object" theme={null}
"item": {
"type": "object",
"properties": {
"title": "{p.title}",
"content": "{p.content}"
}
}
```
***
## reference
Referencing another schema.
The reference property type is a bit different than string, number, boolean, etc.
This property types allows referencing other views created from either this Source Entity or from another source entity.
When referenced Enterspeed will resolve the view when requested by the Delivery API, so that the data will stay up-to-date.
In order to reference desired source entity, you can use `alias` of the schema and `id` or `originId` of the source entity and optionally a different source than the current source entity.
Reference schemas are typically used when you are mapping data from another entity. Eg. a page has a reference another page entity or media entity.
Read more about reference schemas [here](/enterspeed/key-concepts/referencing-schemas)
### Fields
| Property | Required? | Description |
| ---------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | **Yes** | The property type - here `alias` |
| `alias` | **Yes** | The alias of the schema you wish to reference |
| `id` | **Yes** / No | The id of the source entity. `originId` can be used instead |
| `originId` | **Yes** / No | The originId of the source entity. `id` can be used instead |
| `source` | No | Allows you to define a different source. The source should be equal to the desired source group alias, where the view is located.
If not defined, it uses the current source group.
Note: This property is only used if `originId` is used. If `id` is used the source of the id will take priority over the source |
### Examples
```json title="Property type: reference (static value) with originId" theme={null}
"seoData": {
"type": "reference",
"originId": "{originId}",
"alias": "Seo",
"source": "anotherSourceGroupAlias"
}
```
```json title="Property type: reference (static value)" theme={null}
"seoData": {
"type": "reference",
"id": "{id}",
"alias": "Seo"
}
```
```json title="Property type: reference (dynamic value)" theme={null}
"seoData": {
"type": "reference",
"id": "{id}",
"alias": "{p.seoAlias}"
}
```
The `alias` can either be a static value, like "Seo" or it can be a dynamic value being resolved from the Source Entity that is being processed by Enterspeed. This allows for supporting almost any use case.
### Reference response
The response of references differs in V1 and V2+ of the Delivery API. V1 return the id, type and wraps the properties from the referenced view in a `view` property. The response of references in V2+ is much more clean and only return the actual properties from the referenced schema. If a reference is not found in V2+ the reference information are added in the `missingViewReference` property in the `meta` object for debug purpose.
```json title="Delivery API V1 uses a nested view property" theme={null}
{
// This example shows a response of a view with two schema references.
// image1 is referencing a found view with a url and name property
// and image2 is referencing a view that doesn't exist.
"meta": {
"missingViewReferences": []
},
"route": {
"image1": {
"id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1234/View/image",
"view": {
"url": "https://test.com/how-to-write-a-good-blog-post.png",
"name": "How To Write A Good Blog Post"
},
"type": "ViewReference"
},
"image2": {
"id": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image",
"view": null,
"type": "ViewReference"
}
}
}
```
```json title="Delivery API V2+ only returns the actual view properties" theme={null}
{
// This example shows a response of a view with two schema references.
// image1 is referencing a found view with a url and name property
// and image2 is referencing a view that doesn't exist.
"meta": {
"missingViewReferences": [
{
"path": "image2",
"viewId": "gid://Environment/8ef2bdc0-c352-4190-a344-c51d1f5e72ea/Source/dc5d9518-b96a-428a-a9b3-31fb601376c2/Entity/1235/View/image"
}
]
},
"route": {
"image1": {
"url": "https://test.com/how-to-write-a-good-blog-post.png",
"name": "How To Write A Good Blog Post"
},
"image2": null
}
}
```
***
## partial
Referencing a partial schema to map the data into.
The partial mapping property type allows for dynamically including partial schemas into the main schema. This is useful when you need to iterate an array of different objects that has an identifier, like an ID, alias or similar.
Partial schemas are typically used when you want a reusable schema for mapping data that is part of the same entity. Eg. meta data (title, description, ...) is the same across different entity types but the data lives on the entity it self.
Read more about partial schemas [here](/enterspeed/key-concepts/partial-schemas)
### Fields
| Property | Required? | Description |
| -------- | --------- | --------------------------------------------- |
| `type` | **Yes** | The property type - here `partial` |
| `input` | **Yes** | Defines what goes into the partial schema |
| `alias` | **Yes** | Is used to resolve what partial schema to use |
### Examples
```json title="Property type: partial" theme={null}
"blocks": {
"type": "array",
"input": "{p.contentBlocks}",
"items": {
"type": "partial",
"input": "{item}",
"alias": "Block-{item.contentType}"
}
}
```
The `input` defines what goes into the partial schema and the `alias` is used to resolve what partial schema to use. So in this case we could have partial schema with alias: Block-headline.
If you want to pass the entire current source entity to input, you can use `{root}`.
***
## dynamic
### Fields
| Property | Required? | Description |
| -------- | --------- | -------------------------------- |
| `*` | **Yes** | [Path selector](./path-selector) |
### Examples
```json title="Property type: dynamic" theme={null}
"blocks": {
"*": "p.contentBlocks"
}
```
# Overview
Source: https://docs.enterspeed.com/enterspeed/reference/schema-example-library/overview
Below you'll find a collection of useful schema snippets. Use these as a starting point or inspiration when designing your next schema.
These snippets are meant as examples and are meant to be modified to fit your own data structure.
Breadcrumb navigation schema example using references.
Combining data from multiple source entities into a single view and returning it from the Query API
Recommended ways of mapping dictionary items.
Different ways of doing dynamic mapping of properties from the source entity.
Schema example for listing the 3 latest news articles filtered by category.
Partial schema example for a reuseable SEO composition across different page types.
Example of a site settings schema with various configuration options and references to other pages.
Examples of how to build a sitemap.xml or llms.txt file.
# Service Limits
Source: https://docs.enterspeed.com/enterspeed/service-limits
Enterspeed sets some upper limits to its services to ensure stability and performance for all customers, please note that these limits may be capped before reaching the maximum by your plan. Limits that are marked with \* may be extended.
**Service limits:**
* Maximum monthly Delivery API and Query API requests (counted together in a single pooled quota)\*: 5,000,000 per tenant
* Maximum monthly changed Ingest Requests\*: 500,000 per tenant
* Maximum number of source entities\*: 200,000 per tenant
* Maximum number of index items\*: 200,000 per tenant
* Maximum index item size: 1 MB
* Maximum storage\*: 2 GB per tenant
* Ingest API rate limit\*: 25 requests/sec per tenant
* Ingest API request size limit: 1 MB per request
* Source entity property count limit: 5,000 per source entity
# Continuous Deployment
Source: https://docs.enterspeed.com/enterspeed/tooling/cli/continuous-deployment
Continuous Deployment enables you to push code changes to your different environments all the way to production without any manually steps.
When working with Enterspeed, your schemas are often as much part of the codebase as the code it-self. And because of that, you'll need to deploy the schemas together with the rest of the codebase. With the Enterspeed CLI, you can easily do that.
## CLI commands
Let's start by introducing the two Enterspeed CLI commands we will be using in this guide.
#### Extract deployment plan
```
es-cli deployment extract -e [source environment name]
```
This will extract a deployment plan from your source environment. The deployment plan is a `json` file containing the schema alias and version number for all schemas deployed to the environment.
```json title="Deployment plan example" theme={null}
{
"schemas": [
{
"schema": "category",
"version": 1
},
{
"schema": "contentPage",
"version": 1
},
{
"schema": "homePage",
"version": 1
},
{
"schema": "product",
"version": 2
}
]
}
```
#### Deploy deployment plan
```
es-cli deployment deploy -e [target environment name]
```
This will deploy the deployment plan to your target environment. For the deployment plan example above, the four schemas will be deployed to the target environment in the corresponding versions.
## Setting up Continuous deployment
For this example project, we have a CMS and a frontend application in the same repo. It's completely up to you want to structure your project.
This guide demonstrates how you can set up your CD pipeline using Azure pipelines, but the same concepts apply if you use Github Action, Jenkins, or any other CD tool.
### 1. Add the Enterspeed CLI and a pipeline file to the repo
The first step is to [download the Enterspeed CLI](https://github.com/enterspeedhq/enterspeed-cli/releases) and add the `es-cli.exe` file as well as a `azure-pipeline.yml` file to the root of the repo.
### 2. Update pipeline file
Next step is to update the Azure pipeline file to the functionality that you want in the CD pipeline.
Below is an example of a pipeline file that automatically runs on every push to the `master` branch.
The pipeline consists of three stages, `Build`, `ReleaseToStage` and `ReleaseToProduction`.
#### `Build` stage
The first stage, the `Build` stage, is where we build the frontend and the CMS application.
*Note: In this pipeline example the `Build` stage is just adding a dummy step that prints out a text because it's not really the important part of the guide and because the `build` stage could vary a lot depending on the project you are building.*
```yml title="Build stage" theme={null}
variables:
- group: Enterspeed
trigger:
branches:
include:
- master
stages:
- stage: Build
displayName: Build
jobs:
- job: BuildFrontend
displayName: Build frontend
steps:
- task: CmdLine@2
displayName: Simulate frontend build
inputs:
script: 'echo Build frontend'
- job: BuildCms
displayName: Build cms
steps:
- task: CmdLine@2
displayName: Simulate cms build
inputs:
script: 'echo Build cms'
- stage: ReleaseToStage
...
- stage: ReleaseToProduction
...
```
#### `ReleaseToStage` stage
The second stage, the `ReleaseToStage`, is way more interesting. Here we use the two Enterspeed CLI commands we showed in the beginning of this guide.
With the first task, we extract the deployment plan from the `Development` environment.
The `&& type deploymentplan.json` at the end of the command will print out the content of the deployment plan in the log. This way you can always go back to a given release and see exactly which schemas were deployed as part of that release.
```
es-cli.exe deployment extract -e Development --apiKey $(apiKey) && type deploymentplan.json
```
In the second task we take the extracted deployment plan and deploy the schemas to the `Stage` environment.
```
es-cli.exe deployment deploy -e Stage --apiKey $(apiKey)
```
*Note: As part of this stage, you will probably also deploy your code for the frontend and the CMS application.*
```yml title="ReleaseToStage stage" theme={null}
variables:
- group: Enterspeed
trigger:
branches:
include:
- master
stages:
- stage: Build
...
- stage: ReleaseToStage
displayName: Release to stage
jobs:
- deployment: ReleaseToStage
displayName: Release to stage
environment: Stage
pool:
vmImage: windows-latest
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: CmdLine@2
displayName: "Extract Enterspeed deployment plan from Development"
inputs:
script: 'es-cli.exe deployment extract -e Development --apiKey $(apiKey) && type deploymentplan.json'
- task: CmdLine@2
displayName: "Deploys Enterspeed deployment plan to Stage"
inputs:
script: 'es-cli.exe deployment deploy -e Stage --apiKey $(apiKey)'
- stage: ReleaseToProduction
...
```
#### `ReleaseToProduction` stage
The third stage, the `ReleaseToProduction`, is almost an exact copy of the second stage. The only difference is that we are now extracting the deployment plan from the `Stage` environment and deploying it to the `Production` environment.
```yml title="ReleaseToProduction stage" theme={null}
variables:
- group: Enterspeed
trigger:
branches:
include:
- master
stages:
- stage: Build
...
- stage: ReleaseToStage
...
- stage: ReleaseToProduction
displayName: Release to production
jobs:
- deployment: ReleaseToProduction
displayName: Release to production
environment: Production
pool:
vmImage: windows-latest
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: CmdLine@2
displayName: "Extract Enterspeed deployment plan from Stage"
inputs:
script: 'es-cli.exe deployment extract -e Stage --apiKey $(apiKey) && type deploymentplan.json'
- task: CmdLine@2
displayName: "Deploys Enterspeed deployment plan to Production"
inputs:
script: 'es-cli.exe deployment deploy -e Production --apiKey $(apiKey)'
```
If you want to have more control and be 100% sure of which schemas and versions you deploy, you can skip the `extract` command from the CD pipeline and instead have the deployment plan as part of your source code in version control.
If you do that, the `deploy` command will deploy the deployment plan from your source code of the current branch you are deploying.
What the best option is depends on the project you are building and how you want your workflow to be.
### 3. Run the pipeline
Now it's time to run the pipeline and see how the schemas are deployed to the different environments.
#### Development
But before we do the first run, let's go to the `Versions` page in Enterspeed and see the state of our environments.
Here we can see that we have three schemas deployed in version 1 and one schema deployed in version 2 on the `Development` environment. After deployment to the `Stage` environment, we should get the same state as on the `Development` environment.
#### Stage
After pushing the changes to the `master` branch, the pipeline starts to deploy the changes and the schemas to the `Stage` environment.
If we go back to the `Versions` page in Enterspeed, we can now see that the schemas from `Development` have automatically been deployed to `Stage`.
#### Production
Let's continue the pipeline and take the release the last step out to `Production`.
If we go back to the `Versions` page in Enterspeed, we can now see that the schemas from `Stage` have automatically been deployed to `Production`.
### 4. Release completed
The CD pipeline has now fully completed and all our schemas (and code) have now been released to production without any manually steps in the release flow.
# Developer setup
Source: https://docs.enterspeed.com/enterspeed/tooling/cli/developer-setup
Having multiple developers working on the same project can cause some problems without the right setup. This is a general issue when having shared resources with distributed code bases.
Imagine you have a shared database on a project. First, you clone the code base and start making some changes to the code. These changes of cause only affect you as the changes are made locally. At some point, you might need to do some changes in the database schema to support the new code changes you made. And now the problems arise.
The changes you made to the shared database will affect all developers but only you have the code changes that work with the new database changes. This means that all other developers might not be able to run the project until you push the code changes.
The same is the case when working with a shared environment in Enterspeed.
## Use individual Enterspeed environments
The solution to the problem above is to have developer-specific environments.
If you create developer-specific environments, sources and environment clients for all developers on the project, you can deploy schema changes to your own specific environment and test the changes without affecting other developers.
Once your changes are ready, you can push them to the main branch (or create a pull request of cause).
Now other developers can pull the changes and run a full deployment to their own environment to make sure they have deployed the latest changes and that all is working.
See our guide on how to [Keep schemas in version control](/enterspeed/tooling/cli/keep-schemas-in-version-control).
## Use the Enterspeed CLI to create new environments
If you do decide to work with developer-specific environments, you will have to create a new environment every time a new developer joins the team.
This means that you will have to go to the Enterspeed app and create a new environment, a new source, a new environment client and a new domain with a hostname.\
Or you could create a simple script using the Enterspeed CLI.
The following PowerShell script will create all the required resources for you in one go.
```powershell theme={null}
# User specific variables
$environmentName = 'developer specific environment name'
$hostname = 'developer specific hostname'
# Solution specific variables
$managementApiKey = 'your management api key'
$sourceGroupType = 'cms'
$sourceGroupId = 'the id of the source group'
# Create environment
$environmentResponse = & es-cli environment create -n $environmentName --apiKey $managementApiKey | ConvertFrom-Json
# Create source
$sourceResponse = & es-cli source create $sourceGroupId -n $environmentName -e $environmentResponse.IdValue -t sourceGroupType --apiKey $managementApiKey | ConvertFrom-Json
# Create domain
$domainResponse = & es-cli domain create -n $environmentName -h $hostname --apiKey $managementApiKey | ConvertFrom-Json
# Create environment client
$sourceResponse = & es-cli environment-client create -n $environmentName -e $environmentName --apiKey $managementApiKey | ConvertFrom-Json
# Update environment client with domain
$sourceResponse = & es-cli environment-client update $environmentName -d $environmentName --apiKey $managementApiKey | ConvertFrom-Json
```
# Getting started
Source: https://docs.enterspeed.com/enterspeed/tooling/cli/getting-started
## 1. Installation
[Download the latest release](https://github.com/enterspeedhq/enterspeed-cli/releases) and select the asset for your operation system.
Also, you can add the location to your es-cli in the environment path to make the CLI available globally if you want.
## 2. Help options
If you don't want to go to the [documentation](https://github.com/enterspeedhq/enterspeed-cli) every time you are using new commands, you can use the help option to see available commands and options
```
es-cli -h
```
```
Description:
Usage:
es-cli [command] [options]
Options:
-v, --verbose verbose
--apiKey
--version Show version information
-?, -h, --help Show help and usage information
Commands:
login Login using OAuth
tenant Tenant
environment Environment
environment-client Environment client
domain Domain
source-group Source group
source Source
views Generated views
source-entity Source entities
schema Schemas
deployment
```
## 3. Authentication
Before you can call any of the commands you must first authenticate. You do that using the `login` command.
This will open a browser window where you can sign in with your Enterspeed credentials.
```
es-cli login
```
It's also possible to authenticate using an **[API key](https://github.com/enterspeedhq/enterspeed-cli#api-key--authentication)**. This is especially useful if you are using the CLI in a pipeline context like CI/CD.
## 4. Setting the right tenant
After authenticating the first thing you must do, is to make sure you are working on the right tenant.
To get a list of all available tenants, use the following command.
After that, you can set the tenant using the id from the list.
```
es-cli tenant set gid://Tenant/5197b4d4-6bdf-4f63-a91b-872905ad1941
```
## 5. Cloning schemas from Enterspeed to your local machine
Now that we have authenticated and selected the right tenant we can start working with the schemas.
You can use the help options for the individual commands as well to get specific help for e.g. the schemas: `es-cli schema -h`
The `schema clone` command will clone all the schemas from your tenant to your local machine so you can start working with your schemas locally.
```
es-cli schema clone
```
This will create a folder called schemas with all your schemas.
## 6. Saving a schema
Now open up one of your schemas, make a change to the schema and save it. Once you save the schema in your editor, note that the schema is only saved on your local machine.
To save the new changes to your schema in Enterspeed you use the `schema save` which takes the alias of the schema as an argument, in this case `product`.
```
es-cli schema save product
```
Now if you open up the same schema in Enterspeed you can see the schemas have been updated in Enterspeed.
## 7. Deploying a schema
The last step for this guide is to deploy the schema to Enterspeed so that the changes to our schema will be reflected in the views.
To deploy a schema we would need to once again provide the alias of the schema that we want to deploy, and also we would need to provide the environment, that we would deploy our schema to.
```
es-cli schema deploy product -e "Development"
```
This was a quick guide to show how to get started and to show just some of the tasks you can do with the CLI.
# Keep schemas in version control
Source: https://docs.enterspeed.com/enterspeed/tooling/cli/keep-schemas-in-version-control
## Why would you have your schemas in version control
One of the prime benefits of having your schemas locally is that you will have the changes to your Enterspeed schemas as part of the version control history.
Let's imagine you are creating a new feature for your site.
By having your schemas in version control you will now get the full story of changes made to support this new feature, not only the code changes you did to your site but also the schema changes you did to support the new feature.
## Prepare your local environment
### 1. Install Enterspeed CLI
First, you must install the Enterspeed CLI. If you haven't worked with the CLI, take a look at the [Getting started guide](/enterspeed/tooling/cli/getting-started).
The guide includes everything, incl. installation, authenticating and selecting the right tenant.
### 2. Clone schemas and deployment plan
If you already have a tenant with multiple schemas deployed to multiple environments you can clone these schemas and extract a deployment plan to your local machine.
If you are starting a new tenant, you can just skip this step.
#### Clone schemas
```
es-cli schema clone
```
This will clone all your schemas from Enterspeed to your local machine in a folder called `Schemas`.
#### Extract deployment plan
```
es-cli deployment extract -e [your environment name]
```
This will extract a deployment plan for the given environment. The deployment plan is a `json` file containing the schema alias and version number for all schemas deployed to the environment.
#### Push changes to your repository
You can now commit and push all your schemas and deployment plan to your repository and we are ready to start working with the schemas.
## Working with schema locally
### 3. Create a new schema or update an existing one
Let's say you start working on a new feature for your site.
Depending on your version control branch strategy, you might create a feature branch to keep changes isolated until ready for a PR or to be merged into the main branch.
Now you can make the code and schemas changes you need.
After creating or updating a schema use the following command to import the schema into Enterspeed.
```
es-cli schema import -a [schema alias]
```
The import command will import the schema even if it's a new schema or an updated schema. This way you don't have to call either a create or save command.
### 4. Testing schemas
Once you have imported a schema you want to test if the schema is working as expected.
You do that as normal by going to the schema in the Enterspeed app where you can test the schema against real source entities.
### 5. Deploying schemas
Now that you have tested your schema changes, it's time to deploy the schema.
You do that by running the `schema deploy` command
```
es-cli schema deploy [schema alias] -e [your environment name]
```
This will deploy the schema to the given environment which will start the processing and generate the views.
The deploy command will also update your local deployment plan to match the new version of the deployed schema.
This means that the deployment plan is always up to date and ready when you merge your change into the main branch where other developers can run a full deployment to update their own environment.
```
es-cli deployment deployment deploy -e [your environment name]
```
The deployment plan can also be used to deploy the schemas in the correct version in a CD pipeline. [Use Enterspeed CLI in your release pipeline](/enterspeed/tooling/cli/continuous-deployment)
# Overview
Source: https://docs.enterspeed.com/enterspeed/tooling/cli/overview
# CLI
The Enterspeed CLI lets you work with your Enterspeed tenant for all sorts of tasks like managing environments, source groups and creating and deploying schemas. You can work with the CLI locally or you can use it in a CI/CD pipeline it's up to you and how your workflow is.
Go to our GitHub repository to find download links and documentation:
[Download and installation](https://github.com/enterspeedhq/enterspeed-cli#installation)\
[Documentation](https://github.com/enterspeedhq/enterspeed-cli#usage)
# Designing a schema
Source: https://docs.enterspeed.com/enterspeed/transform/designing-a-schema
Our schema can be divided into two areas: "Settings" and the actual data.
In our settings, we define:
1. Which source groups and source entity types to use (`triggers`)
2. Set if the schema should be available via routes
3. Set if actions should occur when the schema is processed.
And in our data, we map our existing data to our new content. This is all done under the "properties"-object.
When creating schemas, Enterspeed will automatically generate an alias in camel case based on the name you provide. You can set your own alias by clicking the lock to the left of the alias.
## Configuring the settings
The first thing you need to define is your **triggers**. Triggers consist of one or more source groups, which contain one or more source entity types (the data your schema should use). You can find a list of all the Source Entity Types in the Source entities table in your Enterspeed-project under Type.
The Source entity types are fetched from your data source, e.g. your CMS.
Let's have a look. Say we wish to use data from our Umbraco Cloud (*alias: **umbracoCloud***) source group which have the **contentPage** type, we simply define it like this:
```js theme={null}
triggers: function(context) {
context.triggers('umbracoCloud', ['contentPage'])
}
```
Next, we need to define how we can fetch the data. We do this under **routes**. Fetching can be done by URL, Handle, and ID. In this example, we do it by URL.
```js theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbracoCloud', ['contentPage'])
},
routes: function(sourceEntity, context) {
context.url(sourceEntity.url)
}
}
```
To keep this example simple, we're not going to define any actions.
Great, so we have defined our settings, where we chose which data our schema should use (triggers) and how we should be able to fetch it (routes).
## Defining the data
Now it's time to tie our existing data to our new content. We do this under **properties**.
We can define as many properties as we wish. For this example, we are going to define just one: a title.
First, we give our new property a name. Let's simply call it **title**.
Next, we need to set the **value** for this property. We do this by mapping to the data we wish to use from our source entity type, which we defined under **triggers** earlier.
Click the **Source entities** button on the Schema page and click View next to the Source you want to use. Here you can see all the available data.
The title we wish to use is called **pageTitle** in the data source and is inside the **properties**-object.
```js theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbracoCloud', ['contentPage'])
},
routes: function(sourceEntity, context) {
context.url(sourceEntity.url)
},
properties: function (sourceEntity, context) {
return {
title: sourceEntity.properties.pageTitle
}
}
}
```
## Deploying and testing your schema
Let's deploy and test our new schema.
Click on the *Deploy schemas*-button in the top right corner. This will open a pane where you can see current active deployments per environment and deploy changes to your desired environment.
You can now test your schema via Postman, Insomnia, etc.
# 2 - Transforming data
Source: https://docs.enterspeed.com/enterspeed/transform/intro
All data transformation in Enterspeed is done from a set of schema definitions. Schemas are the glue that ties your existing data to your new layout.
So when designing your schema, you get to choose which of your existing data you wish to use in your new layout- and how it should be structured.
## How does it work?
You start by choosing which data sources you want to use (`triggers`).
Afterward, you begin designing your schema by mapping which data from your data sources you want to use, e.g., a *title*. The actual schema design is done in the `properties` object.
If you want to make your schema routable you define a [`route`](/enterspeed/reference/js/full-schema/routes). By defining a `route`, you can fetch it via the [Delivery API](/api-reference/delivery).
When you finish designing your schema, it's time to deploy it. Deploying the schema will automatically generate a **View** for each Source Entity Type.
A View is the actual data that has been transformed. If you have 10 Source Entity Types linked to your schema, this will generate 10 Views.
Views will be generated if you:
* Deploy the schema again (this will regenerate all Views).
* A new Source Entity with the same type is available (this will only generate Views for the new Source Entities).
* A Source Entity gets Created, Updated, or Deleted via the Ingest API (This will affect the Views for the specific Source Entity).
## Example
Below is a simple example showing how a schema can look for transforming *source entities* with the type of `frontPage`.
Given the `frontPage` source entity have the following content.
```json title="Source entity example" theme={null}
{
"id": "1044-en-us",
"type": "frontPage",
"url": "/frontPage",
"properties": {
"title": "Welcome"
}
}
```
When designing your schema, you access the **title** property from the `sourceEntity`.
You make the schema **routable** by using `routes` and using the `url` from the source entity.
```js title="Schema example" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('cms', ['frontpage'])
},
routes: function(sourceEntity, context) {
context.url(sourceEntity.url)
},
properties: function (sourceEntity, context) {
return {
headline: sourceEntity.properties.title
}
}
}
```
When querying the Delivery API with `url=/frontPage` the output will be:
```json title="Delivery API output example" theme={null}
{
"title": "Welcome"
}
```
***Next, let's learning how to design a schema. [Go to designing a schema](/enterspeed/transform/designing-a-schema)***
# Schema versioning
Source: https://docs.enterspeed.com/enterspeed/transform/schema-versioning
Every schema in Enterspeed can have multiple versions, that can be deployed to different environments and sources. When a new schema is created it is assigned the default version number: `1`.
While working on schema, you can save the latest version as many times as you want, it will not affect any of the existing deployments. When you are happy with your schema and ready to move on, make sure your latest changes are saved, then you can proceed by clicking the **"Deploy schemas"**-button in the top right corner.
This will open a pane where you can see current active deployments per environment and deploy changes to your desired environment.
## Terminology
* **Deployment** - a combination of Version and Environment (e.g. in the screenshot above, you can see 2 deployments). Only one version can be deployed per environment.
* **Delivery data** - data exposed in Delivery API (such as views, routes)
* **Locked version** - Schema version that is not latest, editing is disabled.
## FAQ
**How do I deploy?**\
In the deploy pane, you can choose the environment and version for it, which will be used for chosen sources.
**How do I unpublish my changes?**\
To unpublish a schema version, click the **"Unpublish"**-button next to the schema in the deploy pane.
Deployment with no sources linked to it is a valid deployment state in Enterspeed, which results in no delivery data created.
**When is the new version of schema created?**\
A new latest version is created on save action after you have deployed your previous latest version.
**Can I deploy old schema?**\
Yes, you can always pick an older version and deploy it.
# 1. Sources & Environments
Source: https://docs.enterspeed.com/enterspeed/tutorials/creating-a-simple-site/1-sources-environments
# Sources & Environments in Enterspeed
Start by logging into Enterspeed: [https://app.enterspeed.com](https://app.enterspeed.com)
In this step, we're going to:
1. Create sources
2. Create and configure domains
3. Create environment clients
## Creating sources
Go to **Data sources** under *Settings* and click the **Create group** button. This will open a modal that allows you to create a new source group. A source group can have multiple data sources attached, which each have its own unique API key.
We will be using [Postman](https://www.postman.com/) to ingest data into Enterspeed, which we will cover in the next step. Feel free to use an alternative like [Insomnia](https://insomnia.rest/), etc.
Give your source a name (e.g. *Postman*) and select a type (e.g. *Tool*). Afterward, add a data source by giving it a name (e.g. *Postman development*) and select an environment (e.g. *Production*), then click the **Add** button. Lastly, click on the **Create** button.
An API key is now generated. Save the API key for now (e.g., in a Notepad document) – you'll need it in the next step. Don't worry, you can always access the key again under **Data sources**.
## Creating and configuring domains
Go to **Domains** under *Settings -> Environment settings* and click the **Create** button. Give your domain a name (e.g. *DomainName.com*) and click on the **Create** button.
**Domains** are used in Enterspeed as a way of filtering your data if you have multiple data sources. You can name it whatever you like, but we recommend using the same name as your domain name for ease of use.
Afterward, click on the three dots next to the domain name and select **Edit hostnames**. Click **Create new** and insert the URL you're going to use for your new project.
If you're working on your project locally, you can insert for instance *localhost:8888*.
If you're working on an environment like Netlify you can insert the site URL for instance *[https://my-new-kickass-enterspeed-project.netlify.app](https://my-new-kickass-enterspeed-project.netlify.app)*
## Creating environment clients
Navigate to **Environment clients** under *Settings -> Environment settings* and click the **Create** button. Give your environment client a name (e.g. *Frontend*) and select the **Environment** you just created. Afterward, click on the **Create** button.
Also save this API key for now (e.g., in a Notepad document) - we're going to use it in our JavaScript application. Don't worry, you can always access the key again under **Environment clients**.
Now, next to your new environment client click on the three dots and select **Edit domains**. Select the domain you just created and click the "**Save changes**" button.
Fantastic, next it's time to ingest some data into Enterspeed.
# 2. Ingesting data
Source: https://docs.enterspeed.com/enterspeed/tutorials/creating-a-simple-site/2-ingesting-data
# Ingesting data into Enterspeed
Now it's time to ingest dummy data into Enterspeed. This is done via the [Enterspeed Ingest API](/api-reference/ingest). We have created a [Postman collection](https://github.com/enterspeedhq/enterspeed-demos/blob/master/vanilla-js/DEMO-DATA/blog.postman_collection.json), so you can easily import it to Postman.
If you prefer to do it manually, or if you're using another application, you can copy the cURL requests below.
Start by ingesting what will be your blog collection page. Remember to replace the value of `X-Api-Key` with your Data Source API key.
```shell title="Blog collection" theme={null}
curl --location --request POST 'https://api.enterspeed.com/ingest/v2/1' \
--header 'X-Api-Key: YOUR-DATA-SOURCE-API-KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "blog",
"url": "/blog"
}'
```
The **originId** is the value provided after `v2` in the URL, in the example below it's `1`.
The **originId** must be unique, or else it will override the entity that has the **originId**.
Example: `https://api.enterspeed.com/ingest/v2/THE-ORIGIN-ID-GOES-HERE`
*You can read more about our [Delivery API here](/api-reference/delivery).*
Next, it's time to ingest the individual blog posts. Remember to change the **originId** if you ingest multiple. Notice that the `originParentId` refers to the *Blog collection* we ingested above.
```shell title="Blog post #1" theme={null}
curl --location --request POST 'https://api.enterspeed.com/ingest/v2/2' \
--header 'X-Api-Key: YOUR-DATA-SOURCE-API-KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "blogPost",
"url": "/5-key-web-performance-metrics-to-guide-your-seo",
"originParentId": "1",
"properties": {
"title": "5 Key Web Performance Metrics to Guide Your SEO",
"featuredImage": "https://res.cloudinary.com/enterspeed/image/upload/v1660647408/Enterspeed%20demos/article-1.png",
"date": "16/8 2022",
"author": {
"name": "Enterspeed Team"
},
"excerpt": "Lorem. ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilisi morbi tempus.",
"content": "
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vitae elementum ipsum. Proin vitae faucibus enim, vel condimentum tortor. Nam feugiat fermentum orci, ut blandit tellus hendrerit ut. Sed at dui vel erat faucibus facilisis tincidunt ut metus. Maecenas consectetur ipsum et tempus venenatis. Morbi massa nulla, egestas vitae lacus eget, cursus finibus nisi. Aenean ultrices sem vitae varius tristique.
Sed quis turpis vel nisl tempus cursus. Donec id arcu ac massa blandit laoreet eu sed libero. Aliquam non erat vitae urna mollis rhoncus. Vivamus et urna et lectus scelerisque tincidunt. In hac habitasse platea dictumst. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nulla nec nisl ligula. Curabitur tincidunt sapien ac massa tincidunt, et mattis risus gravida. Curabitur mollis, quam a iaculis porta, orci risus condimentum purus, et viverra tellus turpis vel magna. Aenean et nunc ac purus luctus pellentesque ut sed ex. Quisque interdum pulvinar eros vitae ornare. Suspendisse potenti. Aenean ac turpis at quam fermentum rhoncus in vitae enim. Quisque dignissim velit posuere, sagittis neque quis, consectetur tellus.
Nullam non aliquam mi. Praesent at metus posuere, pellentesque nisl in, fermentum dui. In ipsum nulla, varius id ipsum a, porta faucibus leo. Sed molestie turpis in tellus placerat, ut malesuada felis gravida. Suspendisse tempus lectus ut nunc tempor, at pellentesque sapien suscipit. Pellentesque porttitor urna at lectus sagittis, eu tempus nisl vulputate. Cras eleifend lectus erat, ut elementum sem cursus et. Vivamus elementum tortor vitae feugiat porta. Suspendisse potenti.
Aenean quis odio eu enim bibendum pellentesque eget ut mi. Donec nec justo lectus. Phasellus ac odio nec nulla laoreet elementum sed sed felis. Suspendisse a nunc vel neque lacinia commodo. Cras in mattis dolor, eget mollis lectus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras ut urna eget urna placerat vehicula non eget purus. Nullam placerat ac sapien ac scelerisque. Praesent vehicula semper est quis fermentum. Ut facilisis risus quis porttitor aliquet. Nunc dignissim felis mauris, at scelerisque arcu scelerisque ut. Mauris scelerisque et nulla vitae luctus. Duis placerat bibendum tellus, vel tincidunt mauris laoreet id. Etiam ac lectus pharetra, sagittis lorem non, suscipit magna. Etiam molestie augue bibendum vulputate semper. Donec ut vehicula ipsum.
Proin eleifend sem in urna maximus, id condimentum risus convallis. Aliquam erat volutpat. Proin et orci vel ligula feugiat tincidunt non sed justo. Duis lobortis mauris vel diam volutpat pellentesque. Cras ut accumsan dolor, sed aliquam risus. Maecenas tempor vitae sapien quis pellentesque. Suspendisse dictum feugiat lacus. Curabitur semper vel tortor vitae luctus. Morbi consectetur dignissim fermentum. Vestibulum dignissim elementum nunc, tempor ornare ante tristique vel. Cras euismod ac lectus vitae finibus. Nulla fringilla ac tortor mollis posuere. Donec dictum in justo non fermentum. In augue risus, auctor vitae ante quis, iaculis sagittis arcu. Integer pharetra, tellus non efficitur malesuada, metus nibh finibus tortor, nec fermentum elit nunc nec eros.
Nunc vitae fermentum tellus, a tempor odio. Etiam accumsan ultrices diam, tempor vehicula arcu lacinia et. Vivamus iaculis turpis ac erat condimentum venenatis. Quisque sagittis vel quam quis consequat. Etiam sollicitudin eget purus vel viverra. Pellentesque semper finibus nunc ac ultricies. Donec condimentum molestie ipsum aliquam feugiat. Proin ut rutrum nulla. Ut semper orci nec diam condimentum, non sodales turpis imperdiet. Ut venenatis nisl tempus rutrum convallis. Donec egestas dolor vitae lectus malesuada, vel blandit mauris pulvinar. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eleifend condimentum dolor ac tristique. Nunc tincidunt neque quam, quis porta nulla sodales sit amet. Praesent ullamcorper elit sit amet eros vestibulum vestibulum. Cras eget volutpat lectus, id accumsan metus.
Ut euismod justo ut massa congue elementum quis ut nunc. Integer at eros vitae ligula vehicula sodales a sed metus. Vivamus sagittis sagittis mi. Vivamus aliquam tristique euismod. Praesent mauris purus, accumsan in est sit amet, hendrerit molestie nisl. Mauris sed porta ligula. Etiam vel dolor suscipit, ornare elit nec, gravida ligula. Pellentesque sed congue nibh. Suspendisse iaculis tristique dolor eu dictum. Fusce non nisi sit amet tortor porta eleifend. Nulla tincidunt tincidunt purus viverra elementum. Phasellus ornare ipsum nunc, in tempus augue consequat eget. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla laoreet vel justo ac euismod. Proin iaculis egestas magna, et euismod purus semper sed.
Suspendisse interdum sollicitudin dignissim. Mauris feugiat augue nec neque maximus, mattis facilisis est maximus. Nullam urna ligula, mattis vel lectus at, convallis blandit nisi. Quisque laoreet, lorem eget ornare euismod, tellus tortor pharetra nulla, ut pulvinar turpis lectus id nunc. Duis eu commodo leo, efficitur pulvinar turpis. In fermentum quam ante, eu iaculis quam dictum vitae. Ut accumsan sem sit amet dignissim sagittis. Ut at lobortis metus. Praesent non nulla nec massa laoreet lacinia sit amet ultricies diam. Ut leo mauris, commodo ac sem at, consectetur cursus elit. Etiam nec sollicitudin tortor. Vestibulum nec sodales tellus. Donec at sapien et massa imperdiet malesuada.
Aenean facilisis tincidunt nunc, at aliquet lectus mattis vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Donec consequat nulla id magna sollicitudin aliquam. Nam ornare lectus ut felis aliquam, ac lobortis enim suscipit. Vivamus ex sapien, tincidunt in nulla non, laoreet tempus nibh. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras aliquam at sem et egestas. Quisque aliquet odio nec aliquet porttitor. Cras vitae lacinia elit. Vivamus dictum tempor rhoncus. Phasellus et euismod lectus. Fusce tempus nec ligula vitae eleifend. Curabitur ut mollis nisi. Aenean arcu turpis, bibendum ac accumsan sit amet, lacinia non nulla. Mauris sagittis sodales magna ac porttitor. Donec non quam metus.
Phasellus imperdiet eleifend metus, eget facilisis lacus auctor sit amet. Donec facilisis, augue ut vehicula sodales, enim velit placerat leo, ac suscipit risus justo et ligula. Proin ante dolor, posuere sit amet cursus nec, mollis id metus. Mauris tortor ante, aliquet nec eros id, luctus venenatis felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla in vehicula orci, et interdum turpis. Aenean eget lorem purus.
"
}
}'
```
Once this is done, it's time to begin designing your APIs via our schema designer.
# 3. Designing your APIs
Source: https://docs.enterspeed.com/enterspeed/tutorials/creating-a-simple-site/3-designing-your-apis
# Designing your APIs in Enterspeed
Now for the fun part - designing the APIs we're going to use. This will be the glue that ties our Sources and Environments together. We do this by setting up schemas.
You find the schema editor in Enterspeed under **Schemas**.
The powerful thing about setting up schemas yourself is you get to decide precisely which data you need and how it gets structured.
## How does a schema work?
The schema is the way you design your API. Here you define which sources you want to use, how you should be able to fetch them, and which data you want available.
The sources you define will be transformed into individual **Generated Views** which are available to fetch via our Delivery API.
**Generated views** are the views that are available via the Delivery API. **Source entities** get transformed into **Generated Views** using **Schemas**.
Each time data changes (source entities) in the sources you have defined, a new View will be generated or an existing one updated.
## Designing the `Blog Post` schema
Let's take a look at how a schema can be structured. We'll look at the **Blog Post**-schema, which will be responsible for showing the individual blog posts.
Go to **Schemas** and create a new schema called *Blog Post*. Open the editor and replace the content with the snippet below.
```js title="Blog Post schema" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('postman', ['blogPost'])
},
routes: function (sourceEntity, context) {
context.url(sourceEntity.url)
},
actions: function (sourceEntity, context) {
context.reprocess('blogList').parent()
},
properties: function (sourceEntity, context) {
return {
url: sourceEntity.url,
title: sourceEntity.properties.title,
thumbnail: sourceEntity.properties.featuredImage,
content: sourceEntity.properties.content,
excerpt: sourceEntity.properties.excerpt,
date: sourceEntity.properties.date,
author: {
name: sourceEntity.properties.author.name
}
}
}
}
```
Let's break down the functions in the schema.
### Triggers
The first thing you need to define is your **triggers**. Triggers consist of one or more source groups, which contain one or more source entity types (the data your schema should use). You can find a list of all the Source Entity Types in the **Source entities** table in your Enterspeed-project under **Type**.
In our case, we have a source group called `postman` (note that we're using the **alias** of the source group and not the name).
```js title="blogPost schema -- triggers" theme={null}
triggers: function(context) {
context.triggers('postman', ['blogPost'])
}
```
### Route
Next, we need to define the **route** (how we should be able to fetch the data). We can do this by URL, Handle, or ID. For our blog posts URL makes the most sense.
```js title="blogPost schema -- route" theme={null}
routes: function (sourceEntity, context) {
context.url(sourceEntity.url)
}
```
### Actions
Next, we need to set up an action.
We're going to create a list of all the blog posts we have (we'll do this in the next schema). The reason we do this is to get the performance benefits of having a static (already generated) list, instead of having to do expensive lookups.
For this list to update, it needs to know *when* to update.
When we ingested our blog posts we assigned each blog post an `originParentId`, which was our blog collection source.
In the actions below, we define that we want the parent source entity with the schema `blogList`, to reprocess (generate a new view) each time data is updated in this schema.
```js title="blogPost schema -- actions" theme={null}
actions: function (sourceEntity, context) {
context.reprocess('blogList').parent()
}
```
### Properties
Lastly, but certainly not least, we need to define which data we want in our schema. We do this under **properties**.
Here we are mapping the properties that we need in our frontend.
```js title="blogPost schema -- properties" theme={null}
properties: function (sourceEntity, context) {
return {
url: sourceEntity.url,
title: sourceEntity.properties.title,
thumbnail: sourceEntity.properties.featuredImage,
content: sourceEntity.properties.content,
excerpt: sourceEntity.properties.excerpt,
date: sourceEntity.properties.date,
author: {
name: sourceEntity.properties.author.name
}
}
}
```
When designing your schema, use the **Source entities** button (found in the bottom right corner of the editor) to both test and view your Source entities.
## Designing the `Blog list` schema
Now it's time to design the schema which contains a list of all our blog posts. Just like before, create a schema called **Blog list**, replace the content with the snippet below and lets break it down.
```js title="blogList schema -- finished result" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('postman', ['blog'])
},
routes: function (sourceEntity, context) {
context.handle('blogList')
},
properties: function (sourceEntity, context) {
return {
content: context.reference('blogPost').children()
}
}
}
```
### Triggers
Unlike what you may think, we're not going to use the `blogPost` source entity type, as our trigger, but rather the `blog` source entity type. The `blog` source entity type works as a parent for all our blog posts.
```js title="blogList schema -- triggers" theme={null}
triggers: function(context) {
context.triggers('postman', ['blog'])
}
```
### Route
Now it's time to define how we want to fetch it. Since our list of blog posts can be used on multiple pages (The blog page itself, the homepage, "Latest posts"-widgets", etc.) we are going to fetch via a handle, which we call `blogList`.
```js title="blogList schema -- route" theme={null}
routes: function (sourceEntity, context) {
context.handle('blogList')
}
```
This schema doesn't need any actions, so we will go straight to defining how the data will look using **Properties**.
### Properties
For the properties, we are referencing the `blogPost` schema for all the children of the blog and returning an object with a `content` property.
```js title="blogList schema -- properties" theme={null}
properties: function (sourceEntity, context) {
return {
content: context.reference('blogPost').children()
}
}
```
When we ingested the source entity used for this schema we gave it `originId` **`1`**:
```shell title="Blog collection (ingest)" theme={null}
curl --location --request POST 'https://api.enterspeed.com/ingest/v2/1' 👈👀 \
--header 'X-Api-Key: YOUR-DATA-SOURCE-API-KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "blog",
"url": "/blog"
}'
```
Now we tell our schema to look through all child source entities of the blog source entity. This will return all source entities with a property called `originParentId`, which then should be `equal` to our `originId`. We defined the `originParentId` in the Blog post source entities we ingested:
```shell title="Blog post #1 (ingest)" theme={null}
curl --location --request POST 'https://api.enterspeed.com/ingest/v2/2' \
--header 'X-Api-Key: YOUR-DATA-SOURCE-API-KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"type": "blogPost",
"url": "/5-key-web-performance-metrics-to-guide-your-seo",
"originParentId": "1", 👈👀
"properties": {
"title": "5 Key Web Performance Metrics to Guide Your SEO",
"featuredImage": "https://res.cloudinary.com/enterspeed/image/upload/v1660647408/Enterspeed%20demos/article-1.png",
"date": "16/8 2022",
"author": {
"name": "Enterspeed Team"
},
"excerpt": "Lorem. ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilisi morbi tempus.",
"content": "
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vitae elementum ipsum. Proin vitae faucibus enim, vel condimentum tortor. Nam feugiat fermentum orci, ut blandit tellus hendrerit ut. Sed at dui vel erat faucibus facilisis tincidunt ut metus. Maecenas consectetur ipsum et tempus venenatis. Morbi massa nulla, egestas vitae lacus eget, cursus finibus nisi. Aenean ultrices sem vitae varius tristique.
Sed quis turpis vel nisl tempus cursus. Donec id arcu ac massa blandit laoreet eu sed libero. Aliquam non erat vitae urna mollis rhoncus. Vivamus et urna et lectus scelerisque tincidunt. In hac habitasse platea dictumst. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nulla nec nisl ligula. Curabitur tincidunt sapien ac massa tincidunt, et mattis risus gravida. Curabitur mollis, quam a iaculis porta, orci risus condimentum purus, et viverra tellus turpis vel magna. Aenean et nunc ac purus luctus pellentesque ut sed ex. Quisque interdum pulvinar eros vitae ornare. Suspendisse potenti. Aenean ac turpis at quam fermentum rhoncus in vitae enim. Quisque dignissim velit posuere, sagittis neque quis, consectetur tellus.
Nullam non aliquam mi. Praesent at metus posuere, pellentesque nisl in, fermentum dui. In ipsum nulla, varius id ipsum a, porta faucibus leo. Sed molestie turpis in tellus placerat, ut malesuada felis gravida. Suspendisse tempus lectus ut nunc tempor, at pellentesque sapien suscipit. Pellentesque porttitor urna at lectus sagittis, eu tempus nisl vulputate. Cras eleifend lectus erat, ut elementum sem cursus et. Vivamus elementum tortor vitae feugiat porta. Suspendisse potenti.
Aenean quis odio eu enim bibendum pellentesque eget ut mi. Donec nec justo lectus. Phasellus ac odio nec nulla laoreet elementum sed sed felis. Suspendisse a nunc vel neque lacinia commodo. Cras in mattis dolor, eget mollis lectus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras ut urna eget urna placerat vehicula non eget purus. Nullam placerat ac sapien ac scelerisque. Praesent vehicula semper est quis fermentum. Ut facilisis risus quis porttitor aliquet. Nunc dignissim felis mauris, at scelerisque arcu scelerisque ut. Mauris scelerisque et nulla vitae luctus. Duis placerat bibendum tellus, vel tincidunt mauris laoreet id. Etiam ac lectus pharetra, sagittis lorem non, suscipit magna. Etiam molestie augue bibendum vulputate semper. Donec ut vehicula ipsum.
Proin eleifend sem in urna maximus, id condimentum risus convallis. Aliquam erat volutpat. Proin et orci vel ligula feugiat tincidunt non sed justo. Duis lobortis mauris vel diam volutpat pellentesque. Cras ut accumsan dolor, sed aliquam risus. Maecenas tempor vitae sapien quis pellentesque. Suspendisse dictum feugiat lacus. Curabitur semper vel tortor vitae luctus. Morbi consectetur dignissim fermentum. Vestibulum dignissim elementum nunc, tempor ornare ante tristique vel. Cras euismod ac lectus vitae finibus. Nulla fringilla ac tortor mollis posuere. Donec dictum in justo non fermentum. In augue risus, auctor vitae ante quis, iaculis sagittis arcu. Integer pharetra, tellus non efficitur malesuada, metus nibh finibus tortor, nec fermentum elit nunc nec eros.
Nunc vitae fermentum tellus, a tempor odio. Etiam accumsan ultrices diam, tempor vehicula arcu lacinia et. Vivamus iaculis turpis ac erat condimentum venenatis. Quisque sagittis vel quam quis consequat. Etiam sollicitudin eget purus vel viverra. Pellentesque semper finibus nunc ac ultricies. Donec condimentum molestie ipsum aliquam feugiat. Proin ut rutrum nulla. Ut semper orci nec diam condimentum, non sodales turpis imperdiet. Ut venenatis nisl tempus rutrum convallis. Donec egestas dolor vitae lectus malesuada, vel blandit mauris pulvinar. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eleifend condimentum dolor ac tristique. Nunc tincidunt neque quam, quis porta nulla sodales sit amet. Praesent ullamcorper elit sit amet eros vestibulum vestibulum. Cras eget volutpat lectus, id accumsan metus.
Ut euismod justo ut massa congue elementum quis ut nunc. Integer at eros vitae ligula vehicula sodales a sed metus. Vivamus sagittis sagittis mi. Vivamus aliquam tristique euismod. Praesent mauris purus, accumsan in est sit amet, hendrerit molestie nisl. Mauris sed porta ligula. Etiam vel dolor suscipit, ornare elit nec, gravida ligula. Pellentesque sed congue nibh. Suspendisse iaculis tristique dolor eu dictum. Fusce non nisi sit amet tortor porta eleifend. Nulla tincidunt tincidunt purus viverra elementum. Phasellus ornare ipsum nunc, in tempus augue consequat eget. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla laoreet vel justo ac euismod. Proin iaculis egestas magna, et euismod purus semper sed.
Suspendisse interdum sollicitudin dignissim. Mauris feugiat augue nec neque maximus, mattis facilisis est maximus. Nullam urna ligula, mattis vel lectus at, convallis blandit nisi. Quisque laoreet, lorem eget ornare euismod, tellus tortor pharetra nulla, ut pulvinar turpis lectus id nunc. Duis eu commodo leo, efficitur pulvinar turpis. In fermentum quam ante, eu iaculis quam dictum vitae. Ut accumsan sem sit amet dignissim sagittis. Ut at lobortis metus. Praesent non nulla nec massa laoreet lacinia sit amet ultricies diam. Ut leo mauris, commodo ac sem at, consectetur cursus elit. Etiam nec sollicitudin tortor. Vestibulum nec sodales tellus. Donec at sapien et massa imperdiet malesuada.
Aenean facilisis tincidunt nunc, at aliquet lectus mattis vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Donec consequat nulla id magna sollicitudin aliquam. Nam ornare lectus ut felis aliquam, ac lobortis enim suscipit. Vivamus ex sapien, tincidunt in nulla non, laoreet tempus nibh. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras aliquam at sem et egestas. Quisque aliquet odio nec aliquet porttitor. Cras vitae lacinia elit. Vivamus dictum tempor rhoncus. Phasellus et euismod lectus. Fusce tempus nec ligula vitae eleifend. Curabitur ut mollis nisi. Aenean arcu turpis, bibendum ac accumsan sit amet, lacinia non nulla. Mauris sagittis sodales magna ac porttitor. Donec non quam metus.
Phasellus imperdiet eleifend metus, eget facilisis lacus auctor sit amet. Donec facilisis, augue ut vehicula sodales, enim velit placerat leo, ac suscipit risus justo et ligula. Proin ante dolor, posuere sit amet cursus nec, mollis id metus. Mauris tortor ante, aliquet nec eros id, luctus venenatis felis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla in vehicula orci, et interdum turpis. Aenean eget lorem purus.
"
}
}'
```
## Deploying the schemas
Now it's time to deploy the schemas. Deploying schemas will automatically generate views which will then be available in the Delivery API.
Click the **Deploy schemas** button in the top right corner.
This will open the deploy pane. Choose the environment you wish to deploy to (e.g. Production) and click the **Deploy 2 changes**-button.
Once the schemas have been deployed you will be able to see the results in **Generated views**.
# 4. Fetching data
Source: https://docs.enterspeed.com/enterspeed/tutorials/creating-a-simple-site/4-fetching-data
# Fetching data in the frontend
Now it's time to get the data into the frontend. If you wish you can copy the demo project below, or simply follow along.
## Setting up the demo project
This project uses Netlify functions to securely fetch data from Enterspeed. If you don't wish to use Netlify functions simply move the `enterspeed.js` to another folder and remove the `netlify-cli` dependency.
Go to our GitHub-repo ([https://github.com/enterspeedhq/enterspeed-demos/tree/master/vanilla-js](https://github.com/enterspeedhq/enterspeed-demos/tree/master/vanilla-js)) and clone the project.
cd into the **vanilla-js** folder and run:
```bash theme={null}
npm install
netlify dev
```
Create a file called **.env.local** and insert your environment API key generated in the Enterspeed-app under Environments like this:
```javascript title=".env.local" theme={null}
ENTERSPEED_PRODUCTION_ENVIRONMENT_API_KEY = [YOUR_ENTERSPEED_API_KEY_HERE];
```
For a production environment, your API key should be injected on build time or via a serverless function.
## Ways of fetching data
Fetching data from Enterspeed can be done in three different ways:
* By using **Handle**
* By using **URL**
* By using **ID**
On our demo site, we've used **handle** and **URL**.
**Handle** and **ID** are good ways of fetching data for "non-site-specific data", e.g. the navigation. In this demo, we're using the **handle** method to fetch the blog list.
We're using the **URL** method to fetch all of the individual blog posts.
When the data is fetched you can choose to either generate it client-side, server-side, or at build time. In this demo we're generating it all client-side.
Data can be fetched using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) or a library like [Axios](https://www.npmjs.com/package/axios).
In our demo, we have made a component that fetches the data. You'll find it in the project under "netlify/functions/enterspeed.js". The components look like this:
```javascript title="enterspeed.js" theme={null}
require("dotenv").config();
const fetch = (...args) =>
import("node-fetch").then(({ default: fetch }) => fetch(...args));
const API_ENDPOINT = "https://delivery.enterspeed.com/v2";
exports.handler = async (event, context) => {
try {
const response = await fetch(`${API_ENDPOINT}?${event.rawQuery}`, {
headers: {
"Content-Type": "application/json",
"X-Api-key": process.env.ENTERSPEED_PRODUCTION_ENVIRONMENT_API_KEY,
},
});
const data = await response.json();
return { statusCode: 200, body: JSON.stringify({ data }) };
} catch (error) {
console.log(error);
return {
statusCode: 500,
body: JSON.stringify({ error: "Failed fetching data" }),
};
}
};
```
## Creating the website
### Setting up the HTML
We start by setting up all the HTML that we need. We're including two libraries:
* [Tailwind CSS](https://tailwindcss.com/) to style the page
* [Navigo](https://github.com/krasimir/navigo) to have a simple JavaScript router
```html title="The HTML" theme={null}
Vanilla JS demo
```
### Initializing the router
Now that we got the structure in place, let's look at the JavaScript needed. First, let's Initialize our router, which takes one mandatory argument - the root path of our site.
```javascript title="Initializing router" theme={null}
const router = new Navigo("/");
```
### Creating the renderHomepage function
Next, let's create a function that renders our homepage.
```javascript title="renderHomePage function" theme={null}
const renderHomePage = () => {
fetch("/.netlify/functions/enterspeed?handle=blogList")
.then((response) => response.json())
.then((data) => {
const posts = data.data.views.blogList.content;
posts.forEach((post) => {
const singlePost = `
`;
document.getElementById("posts").innerHTML += singlePost;
});
})
.then(() => {
router.updatePageLinks();
router
.on("/post/:postLink", ({ data }) => {
renderPostPage(data.postLink);
})
.resolve();
});
};
```
The first thing we do in the function is to fetch content via our `enterspeed.js` function.
We include the query `?handle=blogList` since we want to map out the blog posts in a nice grid overview.
The content is stored in `data.data.views.blogList.content`, which we assign to a const called `posts`.
```javascript title="renderHomePage function -- fetch" theme={null}
fetch("/.netlify/functions/enterspeed?handle=blogList")
.then((response) => response.json())
.then((data) => {
const posts = data.data.views.blogList.content;
```
Next, we iterate over each of these posts. We create the HTML for the "card" and map the data appropriately. Then we take each card and add it to the `posts` HTML element:
```html title="The posts HTML element" theme={null}
```
```javascript title="renderHomePage function -- mapping posts" theme={null}
posts.forEach((post) => {
const singlePost = `
`;
document.getElementById("posts").innerHTML += singlePost;
});
```
Notice that we have inserted `data-navigo` in the `a` tag. This is so Navigo attaches a click handler which fires the router's navigate method.
Finally, to get Navigo to recognize these links we just created, we call the `updatePageLinks` method. When any of the link which has the `data-navigo` attribute gets clicked, it should call the `renderPostPage` function, which we will create next, and give it the post link as an argument.
```javascript title="renderHomePage function -- updatePageLinks" theme={null}
.then(() => {
router.updatePageLinks();
router
.on("/post/:postLink", ({ data }) => {
renderPostPage(data.postLink);
})
.resolve();
});
```
Now, before moving on to the `renderPostPage` function, let's remember to initialize our `renderHomepage` function by calling it.
```javascript title="Initializing renderHomePage()" theme={null}
renderHomePage();
```
### Creating the renderPostPage function
```javascript title="renderPostPage function" theme={null}
const renderPostPage = (postLink) => {
fetch(`/.netlify/functions/enterspeed?url=/${postLink}`)
.then((response) => response.json())
.then((data) => {
const post = data.data.route.content;
const postPage = `
`;
document.getElementById("main").innerHTML = postPage;
document.getElementById("article").innerHTML = post.content;
});
};
```
Just like before, we fetch the content via our `enterspeed.js` function. We use `url=` instead of `handle=`, since we set the route to `url` in our blog posts schema.
The content here is stored in `data.data.route.content`, which we assign to a const called post.
We then create the HTML for the "post" and map the data appropriately.
Lastly, we take our `postPage` and add it to our `main` HTML element:
```html title="The main HTML element" theme={null}
```
...as well as the article itself and add to the article HTML element:
```html title="The article HTML element" theme={null}
```
Now when navigating to one of the blog posts, we will have a nice, simple article page.
The finished results should look something like this:
```html title="The complete index.html" theme={null}
Vanilla JS demo
```
# Intro
Source: https://docs.enterspeed.com/enterspeed/tutorials/creating-a-simple-site/intro
# Creating a simple site with Enterspeed & Vanilla JS
In this tutorial, we're going to see how we can set up a simple site using Enterspeed and (vanilla) JavaScript. We've made a [demo website](https://enterspeed-vanilla-js.netlify.app/), which has a list of blog posts.
The demo website is made as a simple SPA (Single Page Application) that renders everything client-side. Internal links only work by "clicking on them" and not by visiting the URL directly (this also includes refreshing and using the history back-button).
The demo website is purely for illustration purposes. We recommend using a production-ready framework like Next.js for your actual project.
You can choose to [clone the project from Github](https://github.com/enterspeedhq/enterspeed-demos/tree/master/vanilla-js) and build this example site, or you can choose to start from scratch and build your own.
**To get started you'll need:**
* An Enterspeed account ([sign up here](https://app.enterspeed.com/signup))
* An environment to run the application (this can be done locally).
If you don't want to run it locally, we suggest using [Netlify](https://www.netlify.com/) or [Vercel](https://vercel.com/) (both have a free tier available).
# 1. Getting started
Source: https://docs.enterspeed.com/enterspeed/tutorials/multilevel-navigation/1-getting-started
# Create a multilevel navigation from your CMS
This is a how-to for creating a simple two-level navigation for you website using data from your CMS.
We are assuming you are familiar with Enterspeed and will focus on transforming your data it using schemas so it fits a web app.
**Prerequisites:**
* You have an Enterspeed user and is logged in
* Tenants, data sources etc. is setup
* You have Postman, Insomnia og similar app installed
* You are comfortable getting and posting data
## The data
Our data is a common and straight forward example, that consists of two types: *mainNavigation* and *navigationItem*.
The *mainNavigation* is the parent and can have *navigationItems* as children. We have kept the properties to a minimum of having a title, this can of course be extended with URL and more.
In this example we only have one level, but you could have as many you choose.
The structure in your CMS could look like this:
The output like this:
And finally the (simplified) data we are working with:
```json theme={null}
{
"originId": "1",
"type": "mainNavigation",
"properties": {
"title": "Main navigation"
},
"originId": "2",
"parentId": "1",
"type": "navigationItem",
"properties": {
"title": "Home"
},
"originId": "3",
"originParentId": "1",
"type": "navigationItem",
"properties": {
"title": "Books"
},
"originId": "4",
"originParentId": "3",
"type": "navigationItem",
"properties": {
"title": "Book 1"
},
"originId": "5",
"originParentId": "3",
"type": "navigationItem",
"properties": {
"title": "Book 2"
}
}
```
This is what a single data entity will look like when ingested into Enterspeed:
```json theme={null}
{
"sourceId": "gid://Source/0edb780d-e053-46b1-bed9-d8fbb3417d37",
"id": "gid://Source/0edb780d-e053-46b1-bed9-d8fbb3417d37/Entity/1",
"type": "mainNavigation",
"originId": "1",
"originParentId": null,
"url": null,
"redirects": [],
"properties": {
"title": "Main navigation"
}
}
```
# 2. Create schemas
Source: https://docs.enterspeed.com/enterspeed/tutorials/multilevel-navigation/2-create-schemas
# Create schemas
Now to the fun part: creating schemas and transforming our data into something we can use in the frontend app.
## Navigation Item schema
Go to Schemas and hit the Create button, use "Navigation Item" as name. Navigate to the schema and replace the content with this snippet:
```js theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbraco', ['navigationItem'])
},
actions: function (sourceEntity, context) {
context.reprocess('navigationItem').parent()
context.reprocess('mainNavigation').parent()
},
properties: function (sourceEntity, context) {
return {
title: sourceEntity.properties.title,
children: context.reference('navigationItem')
.children()
.orderBy({
propertyName: 'properties.metaData.sortOrder',
direction: 'desc'
})
}
}
}
```
*Save* the draft and *deploy* your schema.
**Actions**
The key part of the schema is actions:
```js theme={null}
actions: function (sourceEntity, context) {
context.reprocess('navigationItem').parent()
context.reprocess('mainNavigation').parent()
}
```
In short this tells another schema to update if this one affected.
[Read more about actions](https://docs.enterspeed.com/reference/fields#actions)
## Navigation Group schema
Next we want to create a schema called "Main Navigation", so hit that create button again.
Replace the content with this snippet:
```js theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbraco', ['mainNavigation'])
},
routes: function(sourceEntity, context) {
context.handle('mainNavigation')
},
properties: function (sourceEntity, context) {
return {
children: context.reference('navigationItem')
.children()
.orderBy({
propertyName: 'properties.metaData.sortOrder',
direction: 'desc'
})
}
}
}
```
The key part here is reference for the children. This will make sure to resolve the view when referenced.
[Read more about reference](https://docs.enterspeed.com/reference/property-types#reference)
## Output
To make sure we get the output we are looking for, generate a curl request by testing your NavigationGroup entity (remember to include handle):
(replace "\[your\_environment\_key]" with your key)
```curl theme={null}
curl -L -X GET 'https://delivery.enterspeed.com/v2?handle=mainNavigation' -H 'X-Api-Key: [your_environment_key]'
```
You should get a response that looks like this:
```json theme={null}
{
"meta": {
"status": 200,
"redirect": null,
"missingViewReferences": []
},
"views": {
"mainNavigation": {
"children": [
{
"title": "Home",
"children": []
},
{
"title": "Books",
"children": [
{
"title": "Book 1",
"children": []
},
{
"title": "Book 2",
"children": []
}
]
}
]
}
}
}
```
Now you can go to your web app start creating your navigation!
# 3. Going further
Source: https://docs.enterspeed.com/enterspeed/tutorials/multilevel-navigation/3-going-further
# Going further: Hamburger navigation
If you have a more than one level of items, the schema will cover that as well. E.g. if you need to create a hamburger navigation with deeper levels:
The schema will automatically include that in the output:
```json theme={null}
{
"meta": {
"status": 200,
"redirect": null,
"missingViewReferences": []
},
"views": {
"mainNavigation": {
"children": [
{
"title": "Home",
"children": []
},
{
"title": "Books",
"children": [
{
"title": "Book 1",
"children": [
{
"title": "Page 1",
"children": [
{
"title": "Paragraph 1",
"children": []
},
{
"title": "Paragraph 2",
"children": []
}
]
}
]
},
{
"title": "Book 2",
"children": []
}
]
}
]
}
}
}
```
# 4. Wrap up
Source: https://docs.enterspeed.com/enterspeed/tutorials/multilevel-navigation/4-wrap-up
# Wrap up
This is of course a pretty basic example, but the concept behind it can be extended and modified to fit a lot of different cases.
If you haven't already, you can take a look at our [Setting up Enterspeed with Umbraco and Next.js
](https://docs.enterspeed.com/tutorials/umbraco-nextjs/intro) tutorial.
## Don't get stuck - we're here to help
If you have any questions, don't hesitate to contact us. You have several options, you can:
* Send us an email: [support@enterspeed.com](mailto:support@enterspeed.com)
* Submit a ticket: [Create a new ticket](https://support.enterspeed.com/support/tickets/new)
* Use our contact form: Go to [Enterspeed.com](https://enterspeed.com)
# Tutorials
Source: https://docs.enterspeed.com/enterspeed/tutorials/overview
Learn how to create a simple site with Enterspeed and Vanilla JS.
Learn how to create a multilevel navigation for your website.
Learn how to use Enterspeed with Umbraco V8, Umbraco V9, and Umbraco V10.
Learn how to create a product list with data from your CMS and PIM.
# 1. Getting started
Source: https://docs.enterspeed.com/enterspeed/tutorials/product-list-cms-pim/1-getting-started
# Create a product list with data from your CMS and PIM
This is a how-to for working with multiple data sources in Enterspeed. In this example, we have created a fictional case with data from your CMS and PIM.
We are assuming you are familiar with Enterspeed and will focus on transforming your data using schemas so it fits a web app.
**Prerequisites:**
* You have an Enterspeed user and are logged in
* You have created a tenant
* You have Postman, Insomnia or similar app installed
* You are comfortable getting and posting data
Let's dig into it!
## Case example
Let's imagine we have a commerce website with a product listing page showing a combination of content from the PIM and CMS.
It could look like this:
Following this tutorial, you will end up having a setup with Enterspeed and an endpoint you can use in your app or test in Postman/Insomnia. For the sake of keeping things as simple as possible, we did not include CMS and PIM integrations but relied on dummy data for this part.
Next up, let's set up the basics in Enterspeed!
# 2. Data sources and environment
Source: https://docs.enterspeed.com/enterspeed/tutorials/product-list-cms-pim/2-data-sources-environment
# Data sources and environment
First off we need to set up environment settings and then create the required data sources.
## Environment settings
Go to [https://app.enterspeed.com/settings/environment-settings](https://app.enterspeed.com/settings/environment-settings).
Create a domain called *localhost*. Once you have created the domain, click the three dots next to the domain name (settings) and select *Edit hostnames*. Create a hostname called *root.tld* and click save.
Create an Environment client named *Web* and select the *localhost* domain when prompted.
Finally, create an Environment called *Demo*.
You should end up with something like this:
## Data sources
Go to [https://app.enterspeed.com/settings/data-sources](https://app.enterspeed.com/settings/data-sources) and create a data source group named *Product List Demo (CMS)* and *Product List Demo (PIM)*.
In the *Product List Demo (CMS)*-group created a data source called *Product List Demo (CMS) - DEMO* and select the *Demo* environment. For the *Product List Demo (PIM)*-group create a data source called *Product List Demo (PIM) - Demo* and select the *Demo* environment.
# 3. Ingest data
Source: https://docs.enterspeed.com/enterspeed/tutorials/product-list-cms-pim/3-ingest-data
# Ingest data
Now for ingesting some data. As mentioned this is dummy data, so feel free to change it as you see fit. For a real-life solution, an integration is of course required, you can see which [integrations we already have](/enterspeed/integrations/overview) saving you the trouble.
Take a look at how to ingest data here: [Ingesting data](/enterspeed/ingest).
## CMS
Use the `API key` from your *Product List Demo (**CMS**)* when ingesting.
### CMS Product List
First, we want to create the *Product list* that comes from the CMS and holds settings such as which URL it should be available at.
Use this snippet for ingesting:
```json theme={null}
{
"type": "ProductList",
"url": "/product-list",
"properties": {
"headline": "Browse our categories",
"lead": "We have headsets, retro cameras, cars, old school tapes and watches! If we don't have it we will get it for you! You get a car, you get headset and you get a gameboy!"
}
}
```
Use the `API key` from your *Product List Demo (CMS)* when ingesting and note the `id`.
## PIM
Use the `API key` from your *Product List Demo (**PIM**)*.
### PIM categories
Then we need to create the PIM categories, you can create as many as you want.
And here are some snippets:
```json title="id: pim-category-1" theme={null}
{
"type": "PimCategory",
"url": null,
"properties": {
"image": "headsets.jpg",
"title": "Headsets",
"lead": "We have headsets. Over-under-in-ear, noise canceling and whatever you like.",
"button": "Your new headset"
}
}
```
```json title="id: pim-category-2" theme={null}
{
"type": "PimCategory",
"url": null,
"properties": {
"image": "cameras.jpg",
"title": "Retro cameras",
"lead": "Digital is soo last year, keep your hands busy and your hipster level to the maximum.",
"button": "Keep your hands busy"
}
}
```
```json title="id: pim-category-3" theme={null}
{
"type": "PimCategory",
"url": null,
"properties": {
"image": "tape.jpg",
"title": "Tapes",
"lead": "We are not saying you are old, but if you remember these you probably want to buy a pencil to go with it.",
"button": "Manually rewind"
}
}
```
```json title="id: pim-category-4" theme={null}
{
"type": "PimCategory",
"url": null,
"properties": {
"image": "watches.jpg",
"title": "Very clean watches",
"lead": "The design of these are so clean, that Apple will be jealous (they actually get their inspiration from us).",
"button": "Squeaky clean"
}
}
```
```json title="id: pim-category-5" theme={null}
{
"type": "PimCategory",
"url": null,
"properties": {
"image": "car.jpg",
"title": "Cars!",
"lead": "You know cars? Like back in the day where you would get you daily workout, just by turning the steering wheel.",
"button": "Just cars!"
}
}
```
```json title="id: pim-category-6" theme={null}
{
"type": "PimCategory",
"url": null,
"properties": {
"image": "gameboy.jpg",
"title": "Retro gaming consoles",
"lead": "Everybody has these, if not they want one. With mediocre graphics and gameplay, this is a must.",
"button": "Retro mediocre"
}
}
```
# 4. Create schemas
Source: https://docs.enterspeed.com/enterspeed/tutorials/product-list-cms-pim/4-create-schemas
# Create schemas
Now let's transform that data and create the output we need for our frontend.
## PIM categories
First, create a schema named *Pim Category*:
```js theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('productListDemoPIM', ['PimCategory'])
},
actions: function (sourceEntity, context) {
context.reprocess('productCategories')
.bySchema()
.sourceGroup('productListDemoCMS')
},
properties: function ({properties: p}, context) {
return {
image: p.image,
title: p.title,
lead: p.lead,
button: p.button
}
}
}
```
This will hold the data model for our category coming from the PIM, creating a view for each individual category.
## Product Categories
Finally, we need a *Product Categories* schema.
```js theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('productListDemoCMS', ['ProductList'])
},
routes: function (sourceEntity, context) {
context.url(sourceEntity.url)
},
properties: function ({properties: p}, context) {
return {
headline: p.headline,
lead: p.lead,
categories: context.reference('pimCategory')
.filter("type eq 'PimCategory'")
.sourceGroup('productListDemoPIM')
.limit(4)
}
}
}
```
This schema holds the data model for our CMS data (*headline* and *lead*) and looks up the PIM.
## Deploy and test output
Now we are ready to deploy the schemas and make sure the output, is as expected.
Click on the "**Deploy changes**"-button in the top right corner. Choose your environment and click on the "**Deploy 3 changes**"-button.
Once the changes have been deployed, you will be able to view them under **Generated Views**.
# 5. Wrap up
Source: https://docs.enterspeed.com/enterspeed/tutorials/product-list-cms-pim/5-wrap-up
# Wrap up
We hope this tutorial has piqued your curiosity and given you a new perspective on how to work with data from your PIM and CMS.
If you have an upcoming or existing PIM project, where you think Enterspeed could be a fit don't hesitate to contact us.
* Send us an email: [support@enterspeed.com](mailto:support@enterspeed.com)
* Use our contact form: Go to [Enterspeed.com](https://enterspeed.com/contact)
# 1. Sources & Environments
Source: https://docs.enterspeed.com/enterspeed/tutorials/umbraco-nextjs/1-sources-environments
# Sources & Environments in Enterspeed
Start by logging into Enterspeed: [https://app.enterspeed.com](https://app.enterspeed.com)
In this step, we're going to:
1. Create sources
2. Create and configure domains
3. Create environment clients
## Creating sources
Go to **Data sources** under *Settings* and click the **Create group** button. This will open a modal that allows you to create a new source group. A source group can have multiple data sources attached, which each have its own unique API key.
Give your source a name (e.g. Umbraco Cloud) and select **CMS** in type. Afterward, add a data source by giving it a name (e.g. Umbraco Cloud Production) and select an environment (e.g. Production), then click the **Add** button. Lastly, click on the **Create** button.
An API key is now generated. Save the API key for now (e.g., in a notepad document) - you'll need it when configuring your Umbraco project.
## Creating and configuring domains
Go to **Domains** under *Settings -> Environment settings* and click the **Create** button. Give your domain a name (e.g. DomainName.com) and click on the **Create** button.
**Domains** are used in Enterspeed as a way of filtering your data if you have multiple data sources. You can name it whatever you like, but we recommend using the same name as your domain name for ease of use.
Afterward, click on the three dots next to the domain name and select **Edit hostnames**. Click **Create new** and insert the URL you're going to use for your new project. We're going to use the same hostname when configuring Umbraco.
If you're working on your project locally, you can insert for instance **localhost:3000**.
If you're working on an environment like Netlify you can insert the site URL for instance **[https://my-new-kickass-enterspeed-project.netlify.app](https://my-new-kickass-enterspeed-project.netlify.app)**
## Creating environment clients
Navigate to **Environment clients** under *Settings -> Environment settings* and click the **Create** button. Give your environment client a name (e.g. My Next.js Application) and select the **Environment** you just created. Afterward, click on the **Create** button.
Also save this API key for now (e.g., in a notepad document) - we're going to use it in our Next.js application.
Next to your new environment client click on the three dots and select **Edit domains**. Select the domain you just created and click the "**Save changes**" button.
# 2. Configuring Umbraco
Source: https://docs.enterspeed.com/enterspeed/tutorials/umbraco-nextjs/2-configuring-umbraco
# Configuring Umbraco
In this step, we're going to:
1. Install the Enterspeed Umbraco package
2. Import our demo content
3. Configure our hostname
4. Seed content to Enterspeed manually
## Installation
### Installation for Umbraco 7 & 8
Sign into your Umbraco instance and navigate to the backoffice.
In order to send data to Enterspeed, we need to install the Enterspeed Umbraco package. Go to the **Packages** tab in the top menu and search for **"Enterspeed Umbraco source integration"**. Select the package and hit install.
For production setups, we recommend installing the NuGet package instead. You can find it here: [Enterspeed.Source.UmbracoCms.V7](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms.V7/) or [Enterspeed.Source.UmbracoCms.V8](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms.V8/)
### Installation for Umbraco 9+
Since Umbraco 9 the "packages installation"-feature from the backoffice has been removed. Because of that, we need to install the following NuGet package:
[Enterspeed.Source.UmbracoCms](https://www.nuget.org/packages/Enterspeed.Source.UmbracoCms/)
## Configuring
Go to the **Settings** tab in the top menu and select **Enterspeed settings**.
1. Set the **Enterspeed endpoint** to [https://api.enterspeed.com/](https://api.enterspeed.com/)
2. Set your media domain (optional).
3. Insert your API key generated in the Enterspeed-app under "Sources".
4. Test the connection and save the configuration afterward.
Your data will now be synced to Enterspeed each time you publish content.
You can view synced data in Enterspeed by navigating to **Source Entities** and selecting your source (the name you provided when creating it) in the **Source** dropdown.
## Importing content from the demo project
If you wish, you can import the Umbraco content from our [demo project](https://enterspeed-umbraco-nextjs.netlify.app/).
### Importing demo content for Umbraco v9+
Since Umbraco 9 the "packages installation"-feature from the backoffice has been removed. Because of that, we need to install our NuGet package:
[Enterspeed.Demos.UmbracoCms.FairyTales](https://www.nuget.org/packages/Enterspeed.Demos.UmbracoCms.FairyTales/)
The NuGet package will import content, data types and document types required for this demo on startup of your application.
After package installation build and start your application.
### Publishing and syncing imported demo content
Publish the site by navigating to the **Content** tab, and select the **Home** node.
Then click **the arrow** on the green "Save and Publish" button in the lower right corner, and select "**Publish with descendants**".
Check the "**Include unpublished content items**" option, and click the "**Publish with descendants**" button.
This will publish the entire site, and send the data to Enterspeed.
Go to your Enterspeed-project and navigate to Sources --> Sources entities to verify that the data have synced.
## Configuring your hostname
Go to the **Content** tab and click the three dots next to **Home**. Click the **Do something else** button at the bottom and select **Culture and Hostnames**.
Insert the same domain name as the one you entered when you configured your **hostname** in Enterspeed.
## Sending content to Enterspeed manually
When you click **Save and publish** in Umbraco, the content will automatically be sent to Enterspeed. But since we just updated our hostname, all of our content now has a new URL, which we need to be synced to Enterspeed. We do this by manually triggering an update.
To send content manually to Enterspeed go to the **Content** tab in the top menu. Click the **Enterspeed content** tab and select **Seed**.
To send published content to Enterspeed, click the **Seed** button. This will queue a transfer of all your Umbraco content to Enterspeed.
# 3. Designing your APIs
Source: https://docs.enterspeed.com/enterspeed/tutorials/umbraco-nextjs/3-designing-your-apis
# Designing your APIs in Enterspeed
If you get stuck on the way, don't hesitate to reach out to us. We're more than happy to help! You can contact us at [support@enterspeed.com](mailto:support@enterspeed.com).
Now for the fun part - designing the APIs we're going to use. This will be the glue that ties our Sources and Environments together. We do this by setting up schemas.
You find the schema editor in Enterspeed under **Schemas**.
The powerful thing about setting up schemas yourself is you get to decide precisely which data you need and how it gets structured.
Let's take a look at how a schema can be structured. In *Example schemas & partial schemas* below you'll see three examples from this project. We'll look at the **contentPage** example.
The first thing you need to define is your **triggers**. Triggers consist of one or more source groups, which contain one or more source entity types (the data your schema should use). You can find a list of all the Source Entity Types in the **Source entities** table in your Enterspeed-project under **Type**.
```js theme={null}
triggers: function(context) {
context.triggers('umbracoCloud', ['contentPage'])
}
```
Next, we need to define the **routes** - how should we be able to fetch the data? We can do this by URL, Handle, and ID. For our contentPages URL makes the most sense.
```js theme={null}
routes: function (sourceEntity, context) {
context.url(sourceEntity.url)
}
```
Lastly, but certainly not least, we need to define which data we want in our schema. We do this under **properties**.
The name of the object is the name we're going to use in our application. As you can see in the example, we have chosen to rename **pageTitle** to **headline** for our use case.
Notice how we refer to a alias called . This is our partial schema **umbraco-blockText** we are referencing.
If we don't create this partial schema, the ContentPage-schema won't work.
```js theme={null}
properties: function (sourceEntity, context) {
return {
type: sourceEntity.type,
headline: sourceEntity.properties.pageTitle,
blocks: sourceEntity.properties.contentBlocks.map((contentBlock) =>
context.partial(`umbraco-${contentBlock.contentType}`, contentBlock)
)
}
}
```
When designing your schema, use the **Source entities** button to both test and view your Source entities.
When you're finished designing your schema, it's time to deploy it. Click the **Save draft** button. Next click the **Deploy schemas** button in the top right corner.
This will open the deploy pane. Choose the environment you wish to deploy to (e.g. Production) and click the **Deploy 1 change**-button.
## Example schemas & partial schemas
### ContentPage
```json title="Example schema: ContentPage" theme={null}
{
"triggers": {
"umbracoCloud": ["contentPage"]
},
"route": {
"url": "{url}"
},
"properties": {
"type": "{type}",
"headline": "{p.pageTitle}",
"blocks": {
"type": "array",
"input": "{p.contentBlocks}",
"items": {
"type": "partial",
"input": "{item}",
"alias": "umbraco-{item.contentType}"
}
}
}
}
```
```js title="Example schema: ContentPage" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbracoCloud', ['contentPage'])
},
routes: function (sourceEntity, context) {
context.url(sourceEntity.url)
},
properties: function (sourceEntity, context) {
return {
type: sourceEntity.type,
headline: sourceEntity.properties.pageTitle,
blocks: sourceEntity.properties.contentBlocks.map((contentBlock) =>
context.partial(`umbraco-${contentBlock.contentType}`, contentBlock)
)
}
}
}
```
### umbraco-blockText (Partial schema)
Notice how the alias is automatically generated as **umbracoBlockText**.
Click the lock to the right in the **Create new partial schema** dialog and change the alias to **umbraco-blockText**, so that it matches the reference we created in the ContentPage-schema.
```js title="Example partial schema: umbraco-blockText" theme={null}
/** @type {Enterspeed.PartialSchema} */
export default {
properties: function (input, context) {
return {
text: input.content.text,
alias: input.contentType,
}
}
}
```
### Navigation
```js title="Example schema: Navigation" theme={null}
/** @type {Enterspeed.FullSchema} */
export default {
triggers: function(context) {
context.triggers('umbracoCloud', ['home'])
},
routes: function (sourceEntity, context) {
context.handle('navigation')
},
properties: function (sourceEntity, context) {
return {
navigationItems: context.reference('navigationItem').children()
}
}
}
```
You can find all of the example schemas on [Github](https://github.com/enterspeedhq/enterspeed-demos/tree/master/umbraco-next/DEMO-DATA/enterspeed-schemas). The project consists of the following schemas and partial schemas:
* [ContentPage](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-contentPage.js)
* [Currency](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-curreny.js)
* [Home](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-home.js)
* [Link](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-link.js)
* [Navigation](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-navigation.js)
* [NavigationItem](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-navigationItem.js)
* [Product](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-product.js)
* [ProductListingView](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-ProductListingView.js)
* [Products](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-products.js)
* [umbraco-blockText](https://github.com/enterspeedhq/enterspeed-demos/blob/master/umbraco-next/DEMO-DATA/enterspeed-partial-umbraco-blockText.js) **(Partial schema)**
# 4. Fetching data in Next.js
Source: https://docs.enterspeed.com/enterspeed/tutorials/umbraco-nextjs/4-fetching-data-in-nextjs
# Fetching Enterspeed-data in Next.js
For this tutorial, we assume you already have some knowledge of setting up a Next.js-project. If not, you can read the getting started guide here: [https://nextjs.org/docs/getting-started](https://nextjs.org/docs/getting-started).
## Setting up the demo project
If you don't wish to set up the demo project, simply skip this step.
Go to GitHub ([https://github.com/enterspeedhq/enterspeed-demos/tree/master/umbraco-next](https://github.com/enterspeedhq/enterspeed-demos/tree/master/umbraco-next)) and clone the project.
cd into the **next** folder and run:
```bash theme={null}
npm install
npx next dev
```
Create a file called **.env.local** and insert your environment API key generated in the Enterspeed-app under Environments like this:
```javascript title=".env.local" theme={null}
ENTERSPEED_PRODUCTION_ENVIRONMENT_API_KEY = [YOUR_ENTERSPEED_API_KEY_HERE]
// If you're using preview-mode, also insert the following:
ENTERSPEED_PREVIEW_ENVIRONMENT_API_KEY = [YOUR_ENTERSPEED_API_KEY_HERE]
ENTERSPEED_PREVIEW_SECRET = [A_SECRET_TOKEN_OF_YOUR_CHOICE]
```
For a production environment, your API key should be injected on build time.
## Ways of fetching data
Fetching data from Enterspeed can be done in three different ways:
* By using **Handle**
* By using **URL**
* By using **ID**
On our demo site, we've used **handle** and **URL**.
**Handle** and **ID** are good ways of fetching data for "non-site-specific data", e.g. the navigation. In our demo, we're using the **handle** method to fetch the navigation.
We're using the **URL** method to fetch all of our content- and product pages.
When the data is fetched you can choose to either generate it client-side, server-side, or at build time. In or demo we have used all three methods to illustrate this:
* The navigation is generated at build time.
* The products are generated client-side.
* Our homepage is generated server-side.
Data can be fetched using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) or a library like [Axios](https://www.npmjs.com/package/axios).
You can read more about fetching data in Next.js here: [https://nextjs.org/docs/basic-features/data-fetching](https://nextjs.org/docs/basic-features/data-fetching).
In our demo, we have made a component that fetches the data. You'll find it in the project under "app/lib/enterspeed.js". The components look like this:
```javascript title="Example: enterspeed.js" theme={null}
const call = async (query, preview) => {
const url = `https://delivery.enterspeed.com/v1?${query}`;
const response = await fetch(new Request(url), {
headers: {
"Content-Type": "application/json",
"X-Api-Key": preview
? process.env.ENTERSPEED_PREVIEW_ENVIRONMENT_API_KEY
: process.env.ENTERSPEED_PRODUCTION_ENVIRONMENT_API_KEY,
},
});
return response.json();
};
export const getByHandle = async (handle, preview) => {
const response = await call(`handle=${handle}`, preview);
return response.views[handle];
};
export const getByUrl = async (url, preview) => {
const response = await call(`url=${url}`, preview);
return response.route;
};
```
## How to generate content dynamically
In order to generate our content dynamically (this can be pages, blog posts, products, etc.) we need to create a component that does this for us.
Luckily Next.js supports [Dynamic Routes](https://nextjs.org/docs/routing/dynamic-routes), meaning our slugs can get generated dynamically.
In our demo project, we have created a file called **\[..slug].js**. You will find it in the **pages** folder (you can also view it below).
In the file, you'll find the following functions:
* **getStaticPaths**: Fetches the routes that need to get build.
* **getStaticProps**: Fetches the data for each route that needs to get build.
* **buildPaths**: Sends the navigation paths to the generateNavBarPaths function.
* **generateNavBarPaths**: Generates the navigation.
* **generateSubMenu**: Generates the submenus.
* **Content**: Inserts the data in a JSX-component.
```javascript title="[...slug].js" theme={null}
import Entity from "../components/Entity";
import { getByHandle, getByUrl } from "../lib/enterspeed";
const paths = [];
const generateSubMenu = (children) => {
children.forEach((child) => {
paths.push({
params: { slug: child.href.split("/").filter((a) => !!a) },
});
if (child.children) {
generateSubMenu(child.children);
}
});
};
const generateNavBarPaths = (navigation) => {
if (navigation) {
navigation.forEach((nav) => {
paths.push({
params: { slug: nav.view.href.split("/").filter((a) => !!a) },
});
if (nav.view.children) {
generateSubMenu(nav.view.children);
}
});
}
};
const buildPaths = (navigation) => {
generateNavBarPaths(navigation);
return paths;
};
export async function getStaticPaths() {
const data = await getByHandle("navigation");
buildPaths(data.navigationItems);
return {
paths,
fallback: "blocking",
};
}
export async function getStaticProps({ params, preview }) {
const data = await getByUrl(
encodeURIComponent(`/${params.slug.join("/")}/`),
preview
);
return {
props: {
view: data,
preview: preview || null,
},
revalidate: 60,
};
}
const Content = ({ view }) => {
return ;
};
export default Content;
```
# Intro
Source: https://docs.enterspeed.com/enterspeed/tutorials/umbraco-nextjs/intro
# Setting up Enterspeed with Umbraco and Next.js
This tutorial describes how to use it with Umbraco V8, Umbraco V9 and Umbraco V10.
Feel free to use Umbraco Cloud or a different installation.
In this tutorial, we're going to see how we can set up a Next.js application, using data from Enterspeed and Umbraco.
We've made a demo website called [The Flying Trunk](https://enterspeed-umbraco-nextjs.netlify.app/), which lists H.C. Andersen fairy tales and also "sells" a book containing these fairy tales.
You can choose to [clone the project from Github](https://github.com/enterspeedhq/enterspeed-demos/tree/master/umbraco-next) and build this example site, or you can choose to start from scratch and build your own.
**So what do you need to get started? You'll need:**
* An Enterspeed account ([sign up here](https://app.enterspeed.com/signup))
* A Umbraco Cloud account ([Start 14-day free trial here](https://umbraco.com/products/umbraco-cloud/)) or any other local installation of Umbraco.
* A Next.js environment (we recommend running Next.js locally).
If you don't want to run Next.js locally, we suggest using [Netlify](https://www.netlify.com/) or [Vercel](https://vercel.com/) (both have a free tier available).
# User Login
Source: https://docs.enterspeed.com/enterspeed/user-login
Using the Enterspeed Management App you can sign in using a email and password or an Microsoft Entra ID work or school account.
## Email and password
If you want to work with a email and password user, you can create one by clicking the Sign up now link on the [login page](https://app.enterspeed.com).
Here you can create a user by providing an email and a password.
Once a user is created and signed in, you can start creating your first tenant or if you got an invite to an existing tenant you can accept the invite.
## Microsoft Entra ID work or school account
If you prefer to work with Microsoft Entra ID user, you must first have a [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/microsoft-entra) user (former know as Azure Active Directory).
Once you have that in place you can simply click the Sign-in with work account button, no configuration needed in Enterspeed.
The first time you sign in with an Microsoft Entra ID user a new Enterspeed user will automatically be created for you.
If you already have one or more tenants on a email and password users and you want to move these tenant to a new Microsoft Entra ID user, just let us know, and we can move the tenants for you.
## FAQ
**Got a question? We might already have the answer!**
Currently we support Microsoft Entra ID work or school, former known as Azure Active Directory.
If you need support for other identity providers, please reach out to us.
No, an Enterspeed user can either logon using email/password or a Microsoft Entra ID.
No, in Enterspeed the email is unique per user.
# Enterspeed Documentation
Source: https://docs.enterspeed.com/enterspeed/welcome
At Enterspeed, we're all about delivering high performance at scale with intelligent caching.
Enterspeed lets you sync all of your content from your CMS, PIM, etc. to Enterspeed. This enables you to design a whole new front-end application, while still using data from your existing content system.
*Not familiar with Enterspeed yet? Check out [Enterspeed.com](https://www.enterspeed.com/) for use cases and more.*
## Getting started
Getting started with Enterspeed is easy. Setup your tenant and you are ready to ingest, transform and serve your data to any application.
Create your account, set up your first environment, and get the API keys you need to start working with Enterspeed. [Setup your tenant](/enterspeed/getting-started/intro)
Ingest content from your CMS, PIM, or any other source into Enterspeed as source entities. [Ingest your data](/enterspeed/ingest)
Define schemas to transform and combine your source entities into exactly the views your applications need. [Transform your data](/enterspeed/transform/intro)
Serve your transformed views to any application through the high-performance, Delivery or Query API. [Deliver your data](/enterspeed/deliver)
## Browse categories
**Browse the documentation by categories**
Start here to learn how to set up and use Enterspeed.Understand the core concepts behind Enterspeed.Full reference for all schema field types and options.Explore the Enterspeed REST API.Use the Enterspeed MCP servers for agent access.Manage Enterspeed from the command line.Connect Enterspeed to your existing tools and platforms.Step-by-step guides for common use cases.Recommended patterns for working with Enterspeed.
## Support
**Didn't find what you were looking for? Don't worry, we're here to help**
If you have any questions, don't hesitate to contact us. You have several options, you can:
* Send us an email: [support@enterspeed.com](mailto:support@enterspeed.com)
* Use our contact form: [Go to Enterspeed.com](https://www.enterspeed.com/contact)
* Chat with us: Click the icon in the bottom right corner.