arcpublishing.github.io

Taking Advantage of Multisite Features in ANS 0.6.0

Introduction

In ANS 0.6.0, Arc Publishing has implemented the first set of features for publishing content across multiple publications (or “websites”) within a single umbrella organization.

ANS 0.6.0 will be available for all Arc customers on January 25th.

This document demonstrates how to take advantage of the new multisite features in Arc Publishing using the public API.

Goals

This guide will show how to use the Arc APIs to:

Prerequisites

Terminology

Accessing the APIs

See “Accessing the APIs” in the Publishing a Document Example. We’ll make the same assumptions here.

Note for several key Arc APIs, new API versions have been released. In order to use multisite features, you’ll need to use the following API versions:

For Content API, Story API and URL API endpoints that expect or return an ANS document, ANS 0.6.0 or higher is also required to use multisite features.

Create distinct section taxonomies

We’ll start by creating the websites and sections for our organization. Websites and sections are defined via the Site API. Full documentation for the Site API v3 is available at (https://arcpublishing.github.io/docs/api/site/).

Let’s create two websites called “The River City News” and “The Mountain Village Gazette.”

curl  -X PUT https://api.thepost.arcpublishing.com/site/v3/website/rivercitynews -d '{
  "_id":"rivercitynews",
  "display_name": "The River City News"
}'

{"_id":"rivercitynews","display_name":"The River City News"}

curl -X PUT https://api.thepost.arcpublishing.com/site/v3/website/mountainvillagegazette -d '{
  "_id":"mountainvillagegazette",
  "display_name": "The Mountain Village Gazette"
}'

{"_id":"mountainvillagegazette","display_name":"The Mountain Village Gazette"}

Now let’s add two sections to The River City News: “News” and “Sports.”

curl -X PUT 'https://api.thepost.arcpublishing.com/site/v3/website/rivercitynews/section/?_id=/news' -d '{
  "_id":"/news",
  "_website": "rivercitynews",
  "site": {"site_title": "News" },
  "parent": { "default": "/" }
}'

{"_id":"/news","_website":"rivercitynews","site":{"site_title":"News"},"parent":{"default":"/"},"inactive":false}

curl -X PUT 'https://api.thepost.arcpublishing.com/site/v3/website/rivercitynews/section/?_id=/sports' -d '{
  "_id":"/sports",
  "_website": "rivercitynews",
  "site": {"site_title": "Sports" },
  "parent": { "default": "/" }
}'

{"_id":"/sports","_website":"rivercitynews","site":{"site_title":"Sports"},"parent":{"default":"/"},"inactive":false}

We’ll add two similar sections to The Mountain Village Gazette. But since Mountain Village residents are extremely passionate about croquet, we’ll also add a section for their croquet team, The Mountain Goats.

curl -X PUT 'https://api.thepost.arcpublishing.com/site/v3/website/mountainvillagegazette/section/?_id=/news' -d '{
  "_id":"/news",
  "_website": "mountainvillagegazette",
  "site": {"site_title": "News" },
  "parent": { "default": "/" }
}'

{"_id":"/news","_website":"mountainvillagegazette","site":{"site_title":"News"},"parent":{"default":"/"},"inactive":false}

curl -X PUT 'https://api.thepost.arcpublishing.com/site/v3/website/mountainvillagegazette/section/?_id=/sports' -d '{
  "_id":"/sports",
  "_website": "mountainvillagegazette",
  "site": {"site_title": "Sports" },
  "parent": { "default": "/" }
}'

{"_id":"/sports","_website":"mountainvillagegazette","site":{"site_title":"Sports"},"parent":{"default":"/"},"inactive":false}

curl -X PUT 'https://api.thepost.arcpublishing.com/site/v3/website/mountainvillagegazette/section/?_id=/sports' -d '{
  "_id":"/sports",
  "_website": "mountainvillagegazette",
  "site": {"site_title": "Sports" },
  "parent": { "default": "/" }
}'

{"_id":"/sports/the-mountain-goats","_website":"mountainvillagegazette","site":{"site_title":"The Mountain Goats"},"parent":{"default":"/sports"},"inactive":false}

Note that we’ve declared “The Mountain Goats” to be a child section of “Sports” by setting the value of parent.default to “/sports” in the final PUT request.

The sections in our two websites now look like this:

The River City News

The Mountain Village Gazette

Categorize a single document in sections in different websites

Now that our websites and sections are created, we can create a document and assign it to different sections.

Here’s a story document that describes a recent croquet game between The Mountain Goats and The River Turtles. It was written by Brooks Robinson of the The River City News, but will also be published by The Mountain Village Gazette. The content of the document is the same for both websites, but it will appear in different sections in each. The River City News would like to highlight the Turtles’ victory at the top of the News section, while the The Mountain Village Gazette relegates the story to the dedicated Mountain Goats section. Both newspapers will include the story in their respective Sports sections.

{
  "_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  "type": "story",
  "version": "0.6.0",

  "revision": {
    "revision_id": "BCDEFGHIJKLMNOPQRSTUVWXYZA"
  },

  "headlines": {
    "basic": "River Turtles Defeat Mountain Goats in Annual Croquet Match"
  },

  "content_elements": [
    {
      "type": "text",
      "content": "In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."
    }
  ],

  "credits": {
    "by": [
      {
        "type": "author",
        "version": "0.6.0",
        "name": "Brooks Robinson"
      }
    ]
  },

  "display_date": "2018-01-18T12:00:00Z",

  "taxonomy": {
    "sections": [
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports",
          "website": "rivercitynews"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/news",
          "website": "rivercitynews"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports",
          "website": "mountainvillagegazette"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports/the-mountain-goats",
          "website": "mountainvillagegazette"
        }
      }
    ]
  },

  "websites": {
    "rivercitynews": {},
    "mountainvillagegazette": {}
  },

  "canonical_website": "rivercitynews"
}

What’s going on here?

Let’s submit this as a draft to Story API.

curl -X PUT http://api.thepost.arcpublishing.com/story/v2/story/ABCDEFGHIJKLMNOPQRSTUVWXYZ --data @/path/to/river-turtles-defeat-mountain-goats.json

{"_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","type":"story","version":"0.6.0","content_elements":[{"_id":"2L565CADAZGXBCCJHRCZPA4L2A","type":"text","content":"In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."}],"created_date":"2018-01-18T22:15:10.044Z","revision":{"revision_id":"BCDEFGHIJKLMNOPQRSTUVWXYZA","branch":"default"},"last_updated_date":"2018-01-18T22:15:10.044Z","headlines":{"basic":"River Turtles Defeat Mountain Goats in Annual Croquet Match"},"owner":{"id":"thepost"},"display_date":"2018-01-18T12:00:00Z","credits":{"by":[{"type":"author","version":"0.6.0","name":"Brooks Robinson"}]},"websites":{"rivercitynews":{},"mountainvillagegazette":{}},"taxonomy":{"sections":[{"type":"reference","referent":{"type":"section","id":"/sports","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/news","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/sports","website":"mountainvillagegazette"}},{"type":"reference","referent":{"type":"section","id":"/sports/the-mountain-goats","website":"mountainvillagegazette"}}]},"additional_properties":{"has_published_copy":false},"canonical_website":"rivercitynews"}

We can then fetch a denormalized version of this draft document from the Content API’s mutlisite endpoint, from either website:

curl -X GET 'https://api.thepost.arcpublishing.com/content/v4/stories?_id=ABCDEFGHIJKLMNOPQRSTUVWXYZ&published=false&website=rivercitynews'

curl -X GET 'https://api.thepost.arcpublishing.com/content/v4/stories?_id=ABCDEFGHIJKLMNOPQRSTUVWXYZ&published=false&website=mountainvillagegazette'

Publishing this draft works the same as in the single-site workflow: create an edition that points to the revision.

curl -X PUT https://api.thepost.arcpublishing.com/story/v2/story/ABCDEFGHIJKLMNOPQRSTUVWXYZ/edition/default -d '{ "revision_id": "BCDEFGHIJKLMNOPQRSTUVWXYZA" }'

{"_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","type":"story","version":"0.6.0","content_elements":[{"_id":"2L565CADAZGXBCCJHRCZPA4L2A","type":"text","content":"In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."}],"created_date":"2018-01-18T22:15:10.044Z","revision":{"revision_id":"BCDEFGHIJKLMNOPQRSTUVWXYZA","branch":"default"},"last_updated_date":"2018-01-18T22:15:10.044Z","headlines":{"basic":"River Turtles Defeat Mountain Goats in Annual Croquet Match"},"display_date":"2018-01-18T22:34:28.023Z","credits":{"by":[{"type":"author","version":"0.6.0","name":"Brooks Robinson"}]},"first_publish_date":"2018-01-18T22:34:28.023Z","websites":{"rivercitynews":{},"mountainvillagegazette":{}},"taxonomy":{"sections":[{"type":"reference","referent":{"type":"section","id":"/sports","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/news","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/sports","website":"mountainvillagegazette"}},{"type":"reference","referent":{"type":"section","id":"/sports/the-mountain-goats","website":"mountainvillagegazette"}}]},"additional_properties":{"has_published_copy":false},"publish_date":"2018-01-18T22:34:28.023Z","canonical_website":"rivercitynews"}

Query the Content API within a website

Now the document can be fetched from Content API with two different values for the website query parameter. But it’s the same data in both places. How does this help us build out two different websites?

The key comes in the new website-specific query support. Here’s an Elasticsearch query to retrieve all the published articles in the News section in The River City News:

{
  "query": {
    "bool": {
      "must": [
        {
          "term": { "revision.published": 1 }
        },
        {
          "nested": {
            "path": "taxonomy.sections",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "taxonomy.sections._id": "/news"
                    }
                  },
                  {
                    "term": {
                      "taxonomy.sections._website": "rivercitynews"
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

After minifying and URL-encoding this query, we can run it in the Content API like so:

curl -X GET 'https://api.thepost.arcpublishing.com/content/v4/search?website=rivercitynews&body=%7B%22query%22%3A%7B%22bool%22%3A%7B%22must%22%3A%5B%7B%22term%22%3A%7B%22revision.published%22%3A1%7D%7D%2C%7B%22nested%22%3A%7B%22path%22%3A%22taxonomy.sections%22%2C%22query%22%3A%7B%22bool%22%3A%7B%22must%22%3A%5B%7B%22term%22%3A%7B%22taxonomy.sections._id%22%3A%22%2Fnews%22%7D%7D%2C%7B%22term%22%3A%7B%22taxonomy.sections._website%22%3A%22rivercitynews%22%7D%7D%5D%7D%7D%7D%7D%5D%7D%7D%7D'

{"type":"results","version":"0.6.0","content_elements":[{"type":"story","version":"0.6.0","content_elements":[{"_id":"2L565CADAZGXBCCJHRCZPA4L2A","type":"text","content":"In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."}],"created_date":"2018-01-18T22:15:10.044Z","revision":{"revision_id":"BCDEFGHIJKLMNOPQRSTUVWXYZA","editions":["default"],"branch":"default","published":true},"last_updated_date":"2018-01-18T22:15:10.044Z","headlines":{"basic":"River Turtles Defeat Mountain Goats in Annual Croquet Match"},"owner":{"id":"thepost"},"display_date":"2018-01-18T22:34:28.023Z","credits":{"by":[{"type":"author","version":"0.6.0","name":"Brooks Robinson"}]},"first_publish_date":"2018-01-18T22:34:28.023Z","websites":{"rivercitynews":{},"mountainvillagegazette":{}},"taxonomy":{"sections":[{"_id":"/sports","_website":"rivercitynews","type":"section","version":"0.6.0","name":"Sports","path":"/sports","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/sports","_website":"rivercitynews","site":{"site_title":"Sports"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"rivercitynews./sports"},{"_id":"/news","_website":"rivercitynews","type":"section","version":"0.6.0","name":"News","path":"/news","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/news","_website":"rivercitynews","site":{"site_title":"News"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"rivercitynews./news"},{"_id":"/sports","_website":"mountainvillagegazette","type":"section","version":"0.6.0","name":"Sports","path":"/sports","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/sports","_website":"mountainvillagegazette","site":{"site_title":"Sports"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"mountainvillagegazette./sports"},{"_id":"/sports/the-mountain-goats","_website":"mountainvillagegazette","type":"section","version":"0.6.0","name":"The Mountain Goats","path":"/sports/the-mountain-goats","parent_id":"/sports","parent":{"default":"/sports"},"additional_properties":{"original":{"_id":"/sports/the-mountain-goats","_website":"mountainvillagegazette","site":{"site_title":"The Mountain Goats"},"parent":{"default":"/sports"},"inactive":false}},"_website_section_id":"mountainvillagegazette./sports/the-mountain-goats"}]},"additional_properties":{"has_published_copy":false},"publish_date":"2018-01-19T19:31:08.910Z","canonical_website":"rivercitynews","_website_ids":["rivercitynews","mountainvillagegazette"],"publishing":{"scheduled_operations":{"publish_edition":[],"unpublish_edition":[]}},"_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ"}],"additional_properties":{"took":2,"timed_out":false},"count":1}

As expected, the Content API returned the article.

But, what if we change the query to search for articles in News section from The Mountain Village Gazette? The updated query would be:

{
  "query": {
    "bool": {
      "must": [
        {
          "term": { "revision.published": 1 }
        },
        {
          "nested": {
            "path": "taxonomy.sections",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "taxonomy.sections._id": "/news"
                    }
                  },
                  {
                    "term": {
                      "taxonomy.sections._website": "themountainvillagegazette"
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

…and the corresponding Content API call would be…

curl -X GET 'https://api.thepost.arcpublishing.com/content/v4/search?website=mountainvillagegazette&body=%7B%22query%22%3A%7B%22bool%22%3A%7B%22must%22%3A%5B%7B%22term%22%3A%7B%22revision.published%22%3A1%7D%7D%2C%7B%22nested%22%3A%7B%22path%22%3A%22taxonomy.sections%22%2C%22query%22%3A%7B%22bool%22%3A%7B%22must%22%3A%5B%7B%22term%22%3A%7B%22taxonomy.sections._id%22%3A%22%2Fnews%22%7D%7D%2C%7B%22term%22%3A%7B%22taxonomy.sections._website%22%3A%22mountainvillagegazette%22%7D%7D%5D%7D%7D%7D%7D%5D%7D%7D%7D'

{"type":"results","version":"0.6.0","content_elements":[],"additional_properties":{"took":5,"timed_out":false},"count":0}

So the story does not appear in queries for the News section in The Mountain Village Gazette.

That means this query syntax can be used to create distinct landing pages for sections with the same name across multiple different websites, all while pulling in the same content!

Assign a distinct URL for each website to a single document.

All of what we’ve done so far is nice, but we haven’t fully addressed how documents make it out onto the web. If the same article appears in the News section in River City News and the section The Mountain Goats in The Mountain Village Gazette, shouldn’t the urls for each website reflect that? And what is the impact on SEO?

In ANS 0.5.8, the only url you had to populate to make your document appear on the web was the canonical url. In ANS 0.6.0, we’ve also added the ability to save distinct urls per website (called website_url) on a single document. Let’s use website_url to save urls for The River City News and The Mountain Village Gazette on the document above.

Our new document looks like this:

{
  "_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  "type": "story",
  "version": "0.6.0",

  "revision": {
    "revision_id": "BCDEFGHIJKLMNOPQRSTUVWXYZA"
  },

  "headlines": {
    "basic": "River Turtles Defeat Mountain Goats in Annual Croquet Match"
  },

  "content_elements": [
    {
      "type": "text",
      "content": "In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."
    }
  ],

  "credits": {
    "by": [
      {
        "type": "author",
        "version": "0.6.0",
        "name": "Brooks Robinson"
      }
    ]
  },

  "display_date": "2018-01-18T12:00:00Z",

  "taxonomy": {
    "sections": [
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports",
          "website": "rivercitynews"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/news",
          "website": "rivercitynews"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports",
          "website": "mountainvillagegazette"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports/the-mountain-goats",
          "website": "mountainvillagegazette"
        }
      }
    ]
  },

  "websites": {
    "rivercitynews": {
      "website_url": "/news/river-turtles-defeat-mountain-goats-croquet-match"
    },
    "mountainvillagegazette": {
      "website_url": "/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-croquet-match"
    }
  },

  "canonical_website": "rivercitynews"
}

This is the same document we’ve seen before, but with an updated websites field.

To save these urls in URL Service, we can post the document to one of the new URL endpoints:

curl -X POST 'https://api.thepost.arcpublishing.com/url/v2/url/allwebsiteurls' --data @/path/to/river-turtles-defeat-mountain-goats.json

{"websites":{"mountainvillagegazette":{"url":"/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-croquet-match","format":null},"rivercitynews":{"url":"/news/river-turtles-defeat-mountain-goats-croquet-match","format":null}}}

We can then re-publish the story:

curl -X PUT https://api.thepost.arcpublishing.com/story/v2/story/ABCDEFGHIJKLMNOPQRSTUVWXYZ/edition/default -d '{ "revision_id": "BCDEFGHIJKLMNOPQRSTUVWXYZA" }'

{"_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","type":"story","version":"0.6.0","content_elements":[{"_id":"2L565CADAZGXBCCJHRCZPA4L2A","type":"text","content":"In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."}],"created_date":"2018-01-18T22:15:10.044Z","revision":{"revision_id":"BCDEFGHIJKLMNOPQRSTUVWXYZA","branch":"default"},"last_updated_date":"2018-01-18T22:15:10.044Z","headlines":{"basic":"River Turtles Defeat Mountain Goats in Annual Croquet Match"},"owner":{"id":"thepost"},"display_date":"2018-01-18T12:00:00Z","credits":{"by":[{"type":"author","version":"0.6.0","name":"Brooks Robinson"}]},"websites":{"rivercitynews":{},"mountainvillagegazette":{}},"taxonomy":{"sections":[{"type":"reference","referent":{"type":"section","id":"/sports","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/news","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/sports","website":"mountainvillagegazette"}},{"type":"reference","referent":{"type":"section","id":"/sports/the-mountain-goats","website":"mountainvillagegazette"}}]},"additional_properties":{"has_published_copy":false},"canonical_website":"rivercitynews"}

Now, we can fetch the document by each site’s respective url:


curl -X GET 'https://api.thepost.arcpublishing.com/content/v4/stories/?website=mountainvillagegazette&website_url=/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-croquet-match'

{"_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","type":"story","version":"0.6.0","content_elements":[{"_id":"2L565CADAZGXBCCJHRCZPA4L2A","type":"text","content":"In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."}],"created_date":"2018-01-18T22:15:10.044Z","revision":{"revision_id":"BCDEFGHIJKLMNOPQRSTUVWXYZA","editions":["default"],"branch":"default","published":true},"last_updated_date":"2018-01-18T22:15:10.044Z","headlines":{"basic":"River Turtles Defeat Mountain Goats in Annual Croquet Match"},"owner":{"id":"thepost"},"display_date":"2018-01-18T22:34:28.023Z","credits":{"by":[{"type":"author","version":"0.6.0","name":"Brooks Robinson"}]},"first_publish_date":"2018-01-18T22:34:28.023Z","websites":{"rivercitynews":{"website_url":"/news/river-turtles-defeat-mountain-goats-croquet-match"},"mountainvillagegazette":{"website_url":"/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-croquet-match"}},"taxonomy":{"sections":[{"_id":"/sports","_website":"rivercitynews","type":"section","version":"0.6.0","name":"Sports","path":"/sports","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/sports","_website":"rivercitynews","site":{"site_title":"Sports"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"rivercitynews./sports"},{"_id":"/news","_website":"rivercitynews","type":"section","version":"0.6.0","name":"News","path":"/news","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/news","_website":"rivercitynews","site":{"site_title":"News"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"rivercitynews./news"},{"_id":"/sports","_website":"mountainvillagegazette","type":"section","version":"0.6.0","name":"Sports","path":"/sports","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/sports","_website":"mountainvillagegazette","site":{"site_title":"Sports"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"mountainvillagegazette./sports"},{"_id":"/sports/the-mountain-goats","_website":"mountainvillagegazette","type":"section","version":"0.6.0","name":"The Mountain Goats","path":"/sports/the-mountain-goats","parent_id":"/sports","parent":{"default":"/sports"},"additional_properties":{"original":{"_id":"/sports/the-mountain-goats","_website":"mountainvillagegazette","site":{"site_title":"The Mountain Goats"},"parent":{"default":"/sports"},"inactive":false}},"_website_section_id":"mountainvillagegazette./sports/the-mountain-goats"}]},"additional_properties":{"has_published_copy":false},"publish_date":"2018-01-19T21:55:33.666Z","canonical_website":"rivercitynews","canonical_url":"/news/river-turtles-defeat-mountain-goats-croquet-match","publishing":{"scheduled_operations":{"publish_edition":[],"unpublish_edition":[]}},"website":"mountainvillagegazette","website_url":"/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-croquet-match"}

Note in particular:

"canonical_url":"/news/river-turtles-defeat-mountain-goats-croquet-match"

The canonical_url field was populated by finding the appropriate website_url for the canonical_website. This field can now be used to reliably indicate the original source url for a story that exists in multiple places. This is important for SEO purposes!

Create different URL formatting rules for each website.

The above scenario works if the content author has a particular url in mind for each document on each website. Most of the time, however, authors just want to publish a story and have the URL generated for them. Furthermore, what if different websites have different ideas about how URLs should look? A simple blog might want all articles to have dates in the url, e.g., /2018/01/18/river-turtles-defeat-mountain-goats.html, while a large newspaper might want everything subdivided into sections, like The Mountain Village Gazette above.

Both scenarios can be handled using URL formats, which is Arc’s solution for automatically generating URLs, and is now fully multisite compatible. The complete rules for URL generation are beyond the scope of the this document, but can be found the in URL Service API Documentation, as well as in the URL Serice web app.

For now, let’s create a URL-formatting rule for The Mountain Village Gazette which would generate the URL we used above:

curl -X POST 'https://api.thepost.arcpublishing.com/url/v2/format?website=mountainvillagegazette' -d '{ "criteria": "{ \"type\": \"story\" }", "priority": 10, "format": "%website_section._id|trimForwardSlash()%/%headlines.basic|removeWords()|slugify()%/" }'

{"success":true,"r":{"ok":1,"nModified":0,"n":1,"upserted":[{"index":0,"_id":"1516638491271"}]}}

And another rule for The River City News. Let’s include the display date in this rule:

curl -X POST 'https://api.thepost.arcpublishing.com/url/v2/format?website=rivercitynews' -d '{ "criteria": "{ \"type\": \"story\" }", "priority": 10, "format": "%website_section._id|trimForwardSlash()%/%display_date|year()%/%display_date|month()%/%display_date|day()%/%headlines.basic|removeWords()|slugify()%/" }'

{"success":true,"r":{"ok":1,"nModified":0,"n":1,"upserted":[{"index":0,"_id":"1516638491272"}]}}

We will also need to delete the old URLs we created manually:

curl -X DELETE 'https://api.thepost.arcpublishing.com/url/v2/url?website=mountainvillagegazette&url=/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-croquet-match'

{"_id":"/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-croquet-match","content_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","created_date":"2018-01-19T21:54:57.288Z","last_updated_date":"2018-01-19T21:54:57.288Z"}

curl -X DELETE 'https://api.thepost.arcpublishing.com/url/v2/url?website=rivercitynews&url=/news/river-turtles-defeat-mountain-goats-croquet-match'

{"_id":"/news/river-turtles-defeat-mountain-goats-croquet-match","content_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","created_date":"2018-01-19T21:37:41.385Z","last_updated_date":"2018-01-19T21:37:41.385Z"}

Finally, let’s remove the user-created urls from the story document we made earlier:


{
  "_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  "type": "story",
  "version": "0.6.0",

  "revision": {
    "revision_id": "BCDEFGHIJKLMNOPQRSTUVWXYZF",
    "parent_id": "BCDEFGHIJKLMNOPQRSTUVWXYZE"
  },

  "headlines": {
    "basic": "River Turtles Defeat Mountain Goats in Annual Croquet Match"
  },

  "content_elements": [
    {
      "type": "text",
      "content": "In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."
    }
  ],

  "credits": {
    "by": [
      {
        "type": "author",
        "version": "0.6.0",
        "name": "Brooks Robinson"
      }
    ]
  },

  "display_date": "2018-01-18T12:00:00Z",

  "taxonomy": {
    "sections": [
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports",
          "website": "rivercitynews"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/news",
          "website": "rivercitynews"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports",
          "website": "mountainvillagegazette"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports/the-mountain-goats",
          "website": "mountainvillagegazette"
        }
      }
    ]
  },

  "websites": {
    "rivercitynews": {
    },
    "mountainvillagegazette": {
    }
  },

  "canonical_website": "rivercitynews"
}

And submit it to the URL Service, as we did earlier:


curl -X POST `https://api.thepost.arcpublishing.com/url/v2/url/allwebsiteurls' --data @/path/to/river-turtles-defeat-mountain-goats.json

{"websites":{},"_errors":{"mountainvillagegazette":"The ANS object is missing the following item required by the url format:  website_section._id.","rivercitynews":"The ANS object is missing the following item required by the url format:  website_section._id."}}

Hmmm, that didn’t work. We got an error an error about a missing field: website_section._id.

The Formatting Rules

Let’s take a closer look at the formatting rules we established at the beginning of this step.

Both rules have the same criteria { "type": "story:" } and priority (10). The criteria and priority control when our formatting rule gets triggered. For now, all we need to worry about is that our rules for each website are definitely getting triggered.

They differ, however, in their format. River City News’ rule looks like this:

and Mountain Village Gazette’s rule looks like this

The general format for rules is to put expressions inside a pair of % characters. An expression in a URL formatting rule is usually an ANS field, optionally followed by a series of modifiers. Each modifier is preceded by a | character, and modifiers are processed in sequence from left to right.

Knowing this, and with a bit of deduction (or consulting the reference documentation), we can guess that the URLs generated by each of these rules should look something like this:

Now we can start to see where the error message comes from. The beginning of each rule requires the website_section._id field, but this doesn’t exist on our story!

It turns out that website_section is a special field that does not exist as a top-level ANS field. However, it is treated like one in the URL Service as a convenience. To specify it, we add it in the appropriate websites block. Let’s modify our story document and try again:


{
  "_id": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  "type": "story",
  "version": "0.6.0",

  "revision": {
    "revision_id": "BCDEFGHIJKLMNOPQRSTUVWXYZF",
    "parent_id": "BCDEFGHIJKLMNOPQRSTUVWXYZA"
  },

  "headlines": {
    "basic": "River Turtles Defeat Mountain Goats in Annual Croquet Match"
  },

  "content_elements": [
    {
      "type": "text",
      "content": "In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."
    }
  ],

  "credits": {
    "by": [
      {
        "type": "author",
        "version": "0.6.0",
        "name": "Brooks Robinson"
      }
    ]
  },

  "display_date": "2018-01-18T12:00:00Z",

  "taxonomy": {
    "sections": [
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports",
          "website": "rivercitynews"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/news",
          "website": "rivercitynews"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports",
          "website": "mountainvillagegazette"
        }
      },
      {
        "type": "reference",
        "referent": {
          "type": "section",
          "id": "/sports/the-mountain-goats",
          "website": "mountainvillagegazette"
        }
      }
    ]
  },

  "websites": {
    "rivercitynews": {
      "website_section": {
        "type": "section",
        "version": "0.6.0",
        "_id": "/news",
        "name": "News"
      }
    },
    "mountainvillagegazette": {
      "website_section": {
        "type": "section",
        "version": "0.6.0",
        "_id": "/sports/the-mountain-goats",
        "name": "The Mountain Goats"
      }
    }
  },

  "canonical_website": "rivercitynews"
}

Re-submit it to the URL Service:


curl -X POST `https://api.thepost.arcpublishing.com/url/v2/url/allwebsiteurls' --data @/path/to/river-turtles-defeat-mountain-goats.json

{"websites":{"rivercitynews":{"url":"/news/2018/01/18/river-turtles-defeat-mountain-goats-annual-croquet-match/","format":{"_id":"1516639609928","format":"%website_section._id|trimForwardSlash()%/%display_date|year()%/%display_date|month()%/%display_date|day()%/%headlines.basic|removeWords()|slugify()%/","priority":10,"criteria":{"type":"story"}}},"mountainvillagegazette":{"url":"/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-annual-croquet-match/","format":{"_id":"1516638491271","format":"%website_section._id|trimForwardSlash()%/%headlines.basic|removeWords()|slugify()%/","priority":10,"criteria":{"type":"story"}}}}}

The response includes the URLs that were generated for each website as well as the rule that was used to generate them.

For completeness, let’s also update the document in Story API to remember the website_section fields we added, and re-publish. (We’ve changed the revision field appropriately for this to succeed.)


curl -X PUT 'https://api.thepost.arcpublishing.com/story/v2/story/ABCDEFGHIJKLMNOPQRSTUVWXYZ' --data @/path/to/river-turtles-defeat-mountain-goats.json

{"_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","type":"story","version":"0.6.0","content_elements":[{"_id":"GX4CJVKAAZBUJH7TPKC3LMUMSA","type":"text","content":"In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."}],"created_date":"2018-01-18T22:15:10.044Z","revision":{"revision_id":"BCDEFGHIJKLMNOPQRSTUVWXYZF","parent_id":"BCDEFGHIJKLMNOPQRSTUVWXYZA","branch":"default"},"last_updated_date":"2018-01-22T17:54:23.806Z","headlines":{"basic":"River Turtles Defeat Mountain Goats in Annual Croquet Match"},"owner":{"id":"thepost"},"display_date":"2018-01-18T12:00:00Z","credits":{"by":[{"type":"author","version":"0.6.0","name":"Brooks Robinson"}]},"websites":{"rivercitynews":{"website_section":{"type":"section","version":"0.6.0","_id":"/news","name":"News"}},"mountainvillagegazette":{"website_section":{"type":"section","version":"0.6.0","_id":"/sports/the-mountain-goats","name":"The Mountain Goats"}}},"taxonomy":{"sections":[{"type":"reference","referent":{"type":"section","id":"/sports","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/news","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/sports","website":"mountainvillagegazette"}},{"type":"reference","referent":{"type":"section","id":"/sports/the-mountain-goats","website":"mountainvillagegazette"}}]},"additional_properties":{"has_published_copy":true},"canonical_website":"rivercitynews"}


curl -X PUT 'https://api.thepost.arcpublishing.com/story/v2/story/ABCDEFGHIJKLMNOPQRSTUVWXYZ/edition/default' -d '{"revision_id": "BCDEFGHIJKLMNOPQRSTUVWXYZF" }'

{"_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","type":"story","version":"0.6.0","content_elements":[{"_id":"GX4CJVKAAZBUJH7TPKC3LMUMSA","type":"text","content":"In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."}],"created_date":"2018-01-18T22:15:10.044Z","revision":{"revision_id":"BCDEFGHIJKLMNOPQRSTUVWXYZF","parent_id":"BCDEFGHIJKLMNOPQRSTUVWXYZA","branch":"default"},"last_updated_date":"2018-01-22T17:54:23.806Z","headlines":{"basic":"River Turtles Defeat Mountain Goats in Annual Croquet Match"},"display_date":"2018-01-18T22:34:28.023Z","credits":{"by":[{"type":"author","version":"0.6.0","name":"Brooks Robinson"}]},"first_publish_date":"2018-01-18T22:34:28.023Z","websites":{"rivercitynews":{"website_section":{"type":"section","version":"0.6.0","_id":"/news","name":"News"}},"mountainvillagegazette":{"website_section":{"type":"section","version":"0.6.0","_id":"/sports/the-mountain-goats","name":"The Mountain Goats"}}},"taxonomy":{"sections":[{"type":"reference","referent":{"type":"section","id":"/sports","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/news","website":"rivercitynews"}},{"type":"reference","referent":{"type":"section","id":"/sports","website":"mountainvillagegazette"}},{"type":"reference","referent":{"type":"section","id":"/sports/the-mountain-goats","website":"mountainvillagegazette"}}]},"additional_properties":{"has_published_copy":true},"publish_date":"2018-01-22T17:56:40.912Z","canonical_website":"rivercitynews"}

And now we can fetch from the auto-generated URLs in Content API:

curl -X GET `https://api.thepost.arcpublishing.com/content/v4/stories/?website=rivercitynews&website_url=/news/2018/01/18/river-turtles-defeat-mountain-goats-annual-croquet-match/'

{"_id":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","type":"story","version":"0.6.0","content_elements":[{"_id":"GX4CJVKAAZBUJH7TPKC3LMUMSA","type":"text","content":"In a surprise upset, The River Turtles of River City have defeated their long-time rivals, The Mountain Goats of Mountain Village, in a tightly-contested match lasting over five hours. The final score was 26-25."}],"created_date":"2018-01-18T22:15:10.044Z","revision":{"revision_id":"BCDEFGHIJKLMNOPQRSTUVWXYZF","parent_id":"BCDEFGHIJKLMNOPQRSTUVWXYZA","editions":["default"],"branch":"default","published":true},"last_updated_date":"2018-01-22T17:54:23.806Z","headlines":{"basic":"River Turtles Defeat Mountain Goats in Annual Croquet Match"},"owner":{"id":"thepost"},"display_date":"2018-01-18T22:34:28.023Z","credits":{"by":[{"type":"author","version":"0.6.0","name":"Brooks Robinson"}]},"first_publish_date":"2018-01-18T22:34:28.023Z","websites":{"rivercitynews":{"website_section":{"type":"section","version":"0.6.0","_id":"/news","name":"News"},"website_url":"/news/2018/01/18/river-turtles-defeat-mountain-goats-annual-croquet-match/"},"mountainvillagegazette":{"website_section":{"type":"section","version":"0.6.0","_id":"/sports/the-mountain-goats","name":"The Mountain Goats"},"website_url":"/sports/the-mountain-goats/river-turtles-defeat-mountain-goats-annual-croquet-match/"}},"taxonomy":{"sections":[{"_id":"/sports","_website":"rivercitynews","type":"section","version":"0.6.0","name":"Sports","path":"/sports","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/sports","_website":"rivercitynews","site":{"site_title":"Sports"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"rivercitynews./sports"},{"_id":"/news","_website":"rivercitynews","type":"section","version":"0.6.0","name":"News","path":"/news","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/news","_website":"rivercitynews","site":{"site_title":"News"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"rivercitynews./news"},{"_id":"/sports","_website":"mountainvillagegazette","type":"section","version":"0.6.0","name":"Sports","path":"/sports","parent_id":"/","parent":{"default":"/"},"additional_properties":{"original":{"_id":"/sports","_website":"mountainvillagegazette","site":{"site_title":"Sports"},"parent":{"default":"/"},"inactive":false}},"_website_section_id":"mountainvillagegazette./sports"},{"_id":"/sports/the-mountain-goats","_website":"mountainvillagegazette","type":"section","version":"0.6.0","name":"The Mountain Goats","path":"/sports/the-mountain-goats","parent_id":"/sports","parent":{"default":"/sports"},"additional_properties":{"original":{"_id":"/sports/the-mountain-goats","_website":"mountainvillagegazette","site":{"site_title":"The Mountain Goats"},"parent":{"default":"/sports"},"inactive":false}},"_website_section_id":"mountainvillagegazette./sports/the-mountain-goats"}]},"additional_properties":{"has_published_copy":true},"publish_date":"2018-01-22T17:56:40.912Z","canonical_website":"rivercitynews","canonical_url":"/news/2018/01/18/river-turtles-defeat-mountain-goats-annual-croquet-match/","publishing":{"scheduled_operations":{"publish_edition":[],"unpublish_edition":[]}},"website":"rivercitynews","website_url":"/news/2018/01/18/river-turtles-defeat-mountain-goats-annual-croquet-match/"}