plone.restapi/plone-only-login: Merge branch 'master' into plone-only-login

GitHub <jenkins-z4DKO/[email protected]> Wed, 26 Jul 2017 07:46:16 -0700 (PDT)
Newsgroups gmane.comp.web.zope.plone.cvs
Message-ID <[email protected]>
Repository: plone.restapi
Branch: refs/heads/plone-only-login
Date: 2017-07-26T16:44:03+02:00
Author: Timo Stollenwerk (tisto) <[email protected]>
Commit: https://github.com/plone/plone.restapi/commit/dbd01fc19c605b966af26d134aad9f56762db7a5

Merge branch 'master' into plone-only-login

Files changed:
A docs/source/_json/component_breadcrumbs.req
A docs/source/_json/component_breadcrumbs.resp
A docs/source/_json/component_navigation.req
A docs/source/_json/component_navigation.resp
A docs/source/_json/expansion.req
A docs/source/_json/expansion.resp
A docs/source/_json/expansion_expanded.req
A docs/source/_json/expansion_expanded.resp
A docs/source/_json/expansion_expanded_full.req
A docs/source/_json/expansion_expanded_full.resp
A docs/source/expansion.rst
A src/plone/restapi/deserializer/site.py
A src/plone/restapi/serializer/expansion.py
A src/plone/restapi/services/components/breadcrumbs.py
A src/plone/restapi/services/components/navigation.py
A src/plone/restapi/tests/mixin_ordering.py
A src/plone/restapi/tests/test_expansion.py
A src/plone/restapi/tests/test_site_deserializer.py
M .travis.yml
M CHANGES.rst
M docs/source/_json/breadcrumbs.req
M docs/source/_json/breadcrumbs.resp
M docs/source/_json/collection.resp
M docs/source/_json/content_get.resp
M docs/source/_json/content_post.resp
M docs/source/_json/document.resp
M docs/source/_json/event.resp
M docs/source/_json/file.resp
M docs/source/_json/folder.resp
M docs/source/_json/image.resp
M docs/source/_json/jwt_logged_in.resp
M docs/source/_json/link.resp
M docs/source/_json/navigation.req
M docs/source/_json/navigation.resp
M docs/source/_json/newsitem.resp
M docs/source/_json/siteroot.resp
M docs/source/_json/workflow_get.resp
M docs/source/components.rst
M docs/source/content.rst
M docs/source/index.rst
M docs/source/searching.rst
M setup.py
M site.cfg
M src/plone/restapi/deserializer/configure.zcml
M src/plone/restapi/deserializer/mixins.py
M src/plone/restapi/interfaces.py
M src/plone/restapi/serializer/atcontent.py
M src/plone/restapi/serializer/dxcontent.py
M src/plone/restapi/serializer/site.py
M src/plone/restapi/services/breadcrumbs/configure.zcml
M src/plone/restapi/services/breadcrumbs/get.py
M src/plone/restapi/services/components/get.py
M src/plone/restapi/services/content/configure.zcml
M src/plone/restapi/services/navigation/configure.zcml
M src/plone/restapi/services/navigation/get.py
M src/plone/restapi/services/workflow/configure.zcml
M src/plone/restapi/services/workflow/info.py
M src/plone/restapi/tests/test_atcontent_deserializer.py
M src/plone/restapi/tests/test_atcontent_serializer.py
M src/plone/restapi/tests/test_components.py
M src/plone/restapi/tests/test_documentation.py
M src/plone/restapi/tests/test_dxcontent_deserializer.py
M src/plone/restapi/tests/test_dxcontent_serializer.py
M src/plone/restapi/tests/test_services_breadcrumbs.py
M src/plone/restapi/tests/test_services_navigation.py
M src/plone/restapi/tests/test_workflow.py
M versions.cfg
D setup.cfg

diff --git a/.travis.yml b/.travis.yml
index 4a535b7e..0cdaf889 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -35,6 +35,6 @@ after_success:
   - pip install coverage==3.7.1 coveralls
   - coveralls
   - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
-  - if [ ! $TRAVIS_TAG ]; then docker build --tag plone/plone.restapi:plone-$PLONE_VERSION-$(git rev-parse HEAD) . ; fi
-  - if [ $TRAVIS_TAG ]; then docker build --tag plone/plone.restapi:plone-$PLONE_VERSION-$(git rev-parse HEAD) . --tag plone/plone.restapi:plone-$PLONE_VERSION-$TRAVIS_TAG; fi
+  - if [ ! $TRAVIS_TAG ]; then docker build --tag plone/plone.restapi:plone-$PLONE_VERSION-$(git rev-parse HEAD) --tag plone/plone.restapi:latest . ; fi
+  - if [ $TRAVIS_TAG ]; then docker build --tag plone/plone.restapi:plone-$PLONE_VERSION-$(git rev-parse HEAD) . --tag plone/plone.restapi:plone-$PLONE_VERSION-$TRAVIS_TAG --tag plone/plone.restapi:latest ; fi
   - docker push plone/plone.restapi
diff --git a/CHANGES.rst b/CHANGES.rst
index 2604ce95..ebcbb2e1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,9 +1,14 @@
 Changelog
 =========
 
-1.0a20 (unreleased)
+1.0a21 (unreleased)
 -------------------
 
+New Features:
+
+- Add support for expandable elements.
+  [buchi]
+
 - Make @login only use acl_users inside the Plone site.
   [jaroel]
 
@@ -12,6 +17,21 @@ Changelog
   [timo]
 
 
+1.0a20 (2017-07-24)
+-------------------
+
+Bugfixes:
+
+- Support content reordering on the site root.
+  [jaroel]
+
+- Support setting Layout on the site root.
+  [jaroel]
+
+- Add clarification when using SearchableText parameter in plone.restapi to avoid confusions
+  [sneridagh]
+
+
 1.0a19 (2017-06-25)
 -------------------
 
@@ -34,8 +54,6 @@ Bugfixes:
 - Tweaks to README.rst
   [tkimnguyen]
 
-Bugfixes:
-
 - Don't list non-DX types in @types endpoint.
   Refs https://github.com/plone/plone.restapi/issues/150
   [jaroel]
diff --git a/docs/source/_json/breadcrumbs.req b/docs/source/_json/breadcrumbs.req
index 9693c4d3..8370e19a 100644
--- a/docs/source/_json/breadcrumbs.req
+++ b/docs/source/_json/breadcrumbs.req
@@ -1,3 +1,3 @@
-GET /plone/front-page/@components/breadcrumbs HTTP/1.1
+GET /plone/front-page/@breadcrumbs HTTP/1.1
 Accept: application/json
 Authorization: Basic YWRtaW46c2VjcmV0
diff --git a/docs/source/_json/breadcrumbs.resp b/docs/source/_json/breadcrumbs.resp
index 647f746a..00d2a168 100644
--- a/docs/source/_json/breadcrumbs.resp
+++ b/docs/source/_json/breadcrumbs.resp
@@ -1,14 +1,12 @@
 HTTP/1.1 200 OK
 Content-Type: application/json
 
-[
-  {
-    "@id": "http://localhost:55001/plone/front-page/@components/breadcrumbs", 
-    "items": [
-      {
-        "title": "Welcome to Plone", 
-        "url": "http://localhost:55001/plone/front-page"
-      }
-    ]
-  }
-]
\ No newline at end of file
+{
+  "@id": "http://localhost:55001/plone/front-page/@breadcrumbs", 
+  "items": [
+    {
+      "title": "Welcome to Plone", 
+      "url": "http://localhost:55001/plone/front-page"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/docs/source/_json/collection.resp b/docs/source/_json/collection.resp
index c0fce081..da417bdf 100644
--- a/docs/source/_json/collection.resp
+++ b/docs/source/_json/collection.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/collection/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/collection/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/collection/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/collection", 
   "@type": "Collection", 
   "UID": "SomeUUID000000000000000000000002", 
diff --git a/docs/source/_json/component_breadcrumbs.req b/docs/source/_json/component_breadcrumbs.req
new file mode 100644
index 00000000..9693c4d3
--- /dev/null
+++ b/docs/source/_json/component_breadcrumbs.req
@@ -0,0 +1,3 @@
+GET /plone/front-page/@components/breadcrumbs HTTP/1.1
+Accept: application/json
+Authorization: Basic YWRtaW46c2VjcmV0
diff --git a/docs/source/_json/component_breadcrumbs.resp b/docs/source/_json/component_breadcrumbs.resp
new file mode 100644
index 00000000..647f746a
--- /dev/null
+++ b/docs/source/_json/component_breadcrumbs.resp
@@ -0,0 +1,14 @@
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+  {
+    "@id": "http://localhost:55001/plone/front-page/@components/breadcrumbs", 
+    "items": [
+      {
+        "title": "Welcome to Plone", 
+        "url": "http://localhost:55001/plone/front-page"
+      }
+    ]
+  }
+]
\ No newline at end of file
diff --git a/docs/source/_json/component_navigation.req b/docs/source/_json/component_navigation.req
new file mode 100644
index 00000000..8de636fd
--- /dev/null
+++ b/docs/source/_json/component_navigation.req
@@ -0,0 +1,3 @@
+GET /plone/front-page/@components/navigation HTTP/1.1
+Accept: application/json
+Authorization: Basic YWRtaW46c2VjcmV0
diff --git a/docs/source/_json/component_navigation.resp b/docs/source/_json/component_navigation.resp
new file mode 100644
index 00000000..da3406d6
--- /dev/null
+++ b/docs/source/_json/component_navigation.resp
@@ -0,0 +1,18 @@
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+  {
+    "@id": "http://localhost:55001/plone/front-page/@components/navigation", 
+    "items": [
+      {
+        "title": "Home", 
+        "url": "http://localhost:55001/plone"
+      }, 
+      {
+        "title": "Welcome to Plone", 
+        "url": "http://localhost:55001/plone/front-page"
+      }
+    ]
+  }
+]
\ No newline at end of file
diff --git a/docs/source/_json/content_get.resp b/docs/source/_json/content_get.resp
index bb605400..dc9149da 100644
--- a/docs/source/_json/content_get.resp
+++ b/docs/source/_json/content_get.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/folder/my-document/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/folder/my-document/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/folder/my-document", 
   "@type": "Document", 
   "UID": "SomeUUID000000000000000000000005", 
diff --git a/docs/source/_json/content_post.resp b/docs/source/_json/content_post.resp
index 85f0a7b6..1de28ad2 100644
--- a/docs/source/_json/content_post.resp
+++ b/docs/source/_json/content_post.resp
@@ -3,6 +3,17 @@ Content-Type: application/json
 Location: http://localhost:55001/plone/folder/my-document
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/folder/my-document/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/folder/my-document/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/folder/my-document/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/folder/my-document", 
   "@type": "Document", 
   "UID": "SomeUUID000000000000000000000005", 
diff --git a/docs/source/_json/document.resp b/docs/source/_json/document.resp
index 39c00b5b..998b22c3 100644
--- a/docs/source/_json/document.resp
+++ b/docs/source/_json/document.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/front-page/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/front-page/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/front-page/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/front-page", 
   "@type": "Document", 
   "UID": "SomeUUID000000000000000000000001", 
diff --git a/docs/source/_json/event.resp b/docs/source/_json/event.resp
index 375ca779..52bfcbd0 100644
--- a/docs/source/_json/event.resp
+++ b/docs/source/_json/event.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/event/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/event/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/event/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/event", 
   "@type": "Event", 
   "UID": "SomeUUID000000000000000000000002", 
diff --git a/docs/source/_json/expansion.req b/docs/source/_json/expansion.req
new file mode 100644
index 00000000..e2d9438e
--- /dev/null
+++ b/docs/source/_json/expansion.req
@@ -0,0 +1,3 @@
+GET /plone/front-page HTTP/1.1
+Accept: application/json
+Authorization: Basic YWRtaW46c2VjcmV0
diff --git a/docs/source/_json/expansion.resp b/docs/source/_json/expansion.resp
new file mode 100644
index 00000000..998b22c3
--- /dev/null
+++ b/docs/source/_json/expansion.resp
@@ -0,0 +1,57 @@
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/front-page/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/front-page/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/front-page/@workflow"
+    }
+  }, 
+  "@id": "http://localhost:55001/plone/front-page", 
+  "@type": "Document", 
+  "UID": "SomeUUID000000000000000000000001", 
+  "allow_discussion": null, 
+  "changeNote": "", 
+  "contributors": [], 
+  "created": "2016-01-21T01:14:48+00:00", 
+  "creators": [
+    "test_user_1_", 
+    "admin"
+  ], 
+  "description": "Congratulations! You have successfully installed Plone.", 
+  "effective": null, 
+  "exclude_from_nav": false, 
+  "expires": null, 
+  "id": "front-page", 
+  "language": "", 
+  "layout": "document_view", 
+  "modified": "2016-01-21T01:24:11+00:00", 
+  "parent": {
+    "@id": "http://localhost:55001/plone", 
+    "@type": "Plone Site", 
+    "description": "", 
+    "title": "Plone site"
+  }, 
+  "relatedItems": [], 
+  "review_state": "private", 
+  "rights": "", 
+  "sharing": {
+    "@id": "http://localhost:55001/plone/front-page/@sharing", 
+    "title": "Sharing"
+  }, 
+  "subjects": [], 
+  "table_of_contents": null, 
+  "text": {
+    "content-type": "text/plain", 
+    "data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>", 
+    "encoding": "utf-8"
+  }, 
+  "title": "Welcome to Plone", 
+  "version": "current"
+}
\ No newline at end of file
diff --git a/docs/source/_json/expansion_expanded.req b/docs/source/_json/expansion_expanded.req
new file mode 100644
index 00000000..26879912
--- /dev/null
+++ b/docs/source/_json/expansion_expanded.req
@@ -0,0 +1,3 @@
+GET /plone/front-page?expand=breadcrumbs HTTP/1.1
+Accept: application/json
+Authorization: Basic YWRtaW46c2VjcmV0
diff --git a/docs/source/_json/expansion_expanded.resp b/docs/source/_json/expansion_expanded.resp
new file mode 100644
index 00000000..abbe6344
--- /dev/null
+++ b/docs/source/_json/expansion_expanded.resp
@@ -0,0 +1,63 @@
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/front-page/@breadcrumbs", 
+      "items": [
+        {
+          "title": "Welcome to Plone", 
+          "url": "http://localhost:55001/plone/front-page"
+        }
+      ]
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/front-page/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/front-page/@workflow"
+    }
+  }, 
+  "@id": "http://localhost:55001/plone/front-page", 
+  "@type": "Document", 
+  "UID": "SomeUUID000000000000000000000001", 
+  "allow_discussion": null, 
+  "changeNote": "", 
+  "contributors": [], 
+  "created": "2016-01-21T01:14:48+00:00", 
+  "creators": [
+    "test_user_1_", 
+    "admin"
+  ], 
+  "description": "Congratulations! You have successfully installed Plone.", 
+  "effective": null, 
+  "exclude_from_nav": false, 
+  "expires": null, 
+  "id": "front-page", 
+  "language": "", 
+  "layout": "document_view", 
+  "modified": "2016-01-21T01:24:11+00:00", 
+  "parent": {
+    "@id": "http://localhost:55001/plone", 
+    "@type": "Plone Site", 
+    "description": "", 
+    "title": "Plone site"
+  }, 
+  "relatedItems": [], 
+  "review_state": "private", 
+  "rights": "", 
+  "sharing": {
+    "@id": "http://localhost:55001/plone/front-page/@sharing", 
+    "title": "Sharing"
+  }, 
+  "subjects": [], 
+  "table_of_contents": null, 
+  "text": {
+    "content-type": "text/plain", 
+    "data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>", 
+    "encoding": "utf-8"
+  }, 
+  "title": "Welcome to Plone", 
+  "version": "current"
+}
\ No newline at end of file
diff --git a/docs/source/_json/expansion_expanded_full.req b/docs/source/_json/expansion_expanded_full.req
new file mode 100644
index 00000000..258f6104
--- /dev/null
+++ b/docs/source/_json/expansion_expanded_full.req
@@ -0,0 +1,3 @@
+GET /plone/front-page?expand=breadcrumbs,navigation,schema,workflow HTTP/1.1
+Accept: application/json
+Authorization: Basic YWRtaW46c2VjcmV0
diff --git a/docs/source/_json/expansion_expanded_full.resp b/docs/source/_json/expansion_expanded_full.resp
new file mode 100644
index 00000000..6b92c4c5
--- /dev/null
+++ b/docs/source/_json/expansion_expanded_full.resp
@@ -0,0 +1,93 @@
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/front-page/@breadcrumbs", 
+      "items": [
+        {
+          "title": "Welcome to Plone", 
+          "url": "http://localhost:55001/plone/front-page"
+        }
+      ]
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/front-page/@navigation", 
+      "items": [
+        {
+          "title": "Home", 
+          "url": "http://localhost:55001/plone"
+        }, 
+        {
+          "title": "Welcome to Plone", 
+          "url": "http://localhost:55001/plone/front-page"
+        }
+      ]
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/front-page/@workflow", 
+      "history": [
+        {
+          "action": null, 
+          "actor": "test_user_1_", 
+          "comments": "", 
+          "review_state": "private", 
+          "time": "2016-10-21T19:00:00+00:00", 
+          "title": "Private"
+        }
+      ], 
+      "transitions": [
+        {
+          "@id": "http://localhost:55001/plone/front-page/@workflow/publish", 
+          "title": "Publish"
+        }, 
+        {
+          "@id": "http://localhost:55001/plone/front-page/@workflow/submit", 
+          "title": "Submit for publication"
+        }
+      ]
+    }
+  }, 
+  "@id": "http://localhost:55001/plone/front-page", 
+  "@type": "Document", 
+  "UID": "SomeUUID000000000000000000000001", 
+  "allow_discussion": null, 
+  "changeNote": "", 
+  "contributors": [], 
+  "created": "2016-01-21T01:14:48+00:00", 
+  "creators": [
+    "test_user_1_", 
+    "admin"
+  ], 
+  "description": "Congratulations! You have successfully installed Plone.", 
+  "effective": null, 
+  "exclude_from_nav": false, 
+  "expires": null, 
+  "id": "front-page", 
+  "language": "", 
+  "layout": "document_view", 
+  "modified": "2016-01-21T01:24:11+00:00", 
+  "parent": {
+    "@id": "http://localhost:55001/plone", 
+    "@type": "Plone Site", 
+    "description": "", 
+    "title": "Plone site"
+  }, 
+  "relatedItems": [], 
+  "review_state": "private", 
+  "rights": "", 
+  "sharing": {
+    "@id": "http://localhost:55001/plone/front-page/@sharing", 
+    "title": "Sharing"
+  }, 
+  "subjects": [], 
+  "table_of_contents": null, 
+  "text": {
+    "content-type": "text/plain", 
+    "data": "<p>If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone mailing lists about this.</p>", 
+    "encoding": "utf-8"
+  }, 
+  "title": "Welcome to Plone", 
+  "version": "current"
+}
\ No newline at end of file
diff --git a/docs/source/_json/file.resp b/docs/source/_json/file.resp
index cac96f4f..979141bd 100644
--- a/docs/source/_json/file.resp
+++ b/docs/source/_json/file.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/file/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/file/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/file/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/file", 
   "@type": "File", 
   "UID": "SomeUUID000000000000000000000002", 
diff --git a/docs/source/_json/folder.resp b/docs/source/_json/folder.resp
index efb105d3..39aa5b40 100644
--- a/docs/source/_json/folder.resp
+++ b/docs/source/_json/folder.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/folder/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/folder/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/folder/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/folder", 
   "@type": "Folder", 
   "UID": "SomeUUID000000000000000000000002", 
diff --git a/docs/source/_json/image.resp b/docs/source/_json/image.resp
index d31dddc0..b198ac97 100644
--- a/docs/source/_json/image.resp
+++ b/docs/source/_json/image.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/image/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/image/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/image/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/image", 
   "@type": "Image", 
   "UID": "SomeUUID000000000000000000000002", 
diff --git a/docs/source/_json/jwt_logged_in.resp b/docs/source/_json/jwt_logged_in.resp
index 0559a5bb..a5842fe6 100644
--- a/docs/source/_json/jwt_logged_in.resp
+++ b/docs/source/_json/jwt_logged_in.resp
@@ -2,6 +2,14 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/@navigation"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/", 
   "@type": "Plone Site", 
   "id": "plone", 
diff --git a/docs/source/_json/link.resp b/docs/source/_json/link.resp
index 62a13159..d235c5cc 100644
--- a/docs/source/_json/link.resp
+++ b/docs/source/_json/link.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/link/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/link/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/link/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/link", 
   "@type": "Link", 
   "UID": "SomeUUID000000000000000000000002", 
diff --git a/docs/source/_json/navigation.req b/docs/source/_json/navigation.req
index 8de636fd..9cdab891 100644
--- a/docs/source/_json/navigation.req
+++ b/docs/source/_json/navigation.req
@@ -1,3 +1,3 @@
-GET /plone/front-page/@components/navigation HTTP/1.1
+GET /plone/front-page/@navigation HTTP/1.1
 Accept: application/json
 Authorization: Basic YWRtaW46c2VjcmV0
diff --git a/docs/source/_json/navigation.resp b/docs/source/_json/navigation.resp
index da3406d6..c44ab952 100644
--- a/docs/source/_json/navigation.resp
+++ b/docs/source/_json/navigation.resp
@@ -1,18 +1,16 @@
 HTTP/1.1 200 OK
 Content-Type: application/json
 
-[
-  {
-    "@id": "http://localhost:55001/plone/front-page/@components/navigation", 
-    "items": [
-      {
-        "title": "Home", 
-        "url": "http://localhost:55001/plone"
-      }, 
-      {
-        "title": "Welcome to Plone", 
-        "url": "http://localhost:55001/plone/front-page"
-      }
-    ]
-  }
-]
\ No newline at end of file
+{
+  "@id": "http://localhost:55001/plone/front-page/@navigation", 
+  "items": [
+    {
+      "title": "Home", 
+      "url": "http://localhost:55001/plone"
+    }, 
+    {
+      "title": "Welcome to Plone", 
+      "url": "http://localhost:55001/plone/front-page"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/docs/source/_json/newsitem.resp b/docs/source/_json/newsitem.resp
index 8d5267f5..b926a819 100644
--- a/docs/source/_json/newsitem.resp
+++ b/docs/source/_json/newsitem.resp
@@ -2,6 +2,17 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/newsitem/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/newsitem/@navigation"
+    }, 
+    "workflow": {
+      "@id": "http://localhost:55001/plone/newsitem/@workflow"
+    }
+  }, 
   "@id": "http://localhost:55001/plone/newsitem", 
   "@type": "News Item", 
   "UID": "SomeUUID000000000000000000000002", 
diff --git a/docs/source/_json/siteroot.resp b/docs/source/_json/siteroot.resp
index db02eb31..fa5c4db4 100644
--- a/docs/source/_json/siteroot.resp
+++ b/docs/source/_json/siteroot.resp
@@ -2,6 +2,14 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@components": {
+    "breadcrumbs": {
+      "@id": "http://localhost:55001/plone/@breadcrumbs"
+    }, 
+    "navigation": {
+      "@id": "http://localhost:55001/plone/@navigation"
+    }
+  }, 
   "@id": "http://localhost:55001/plone", 
   "@type": "Plone Site", 
   "id": "plone", 
diff --git a/docs/source/_json/workflow_get.resp b/docs/source/_json/workflow_get.resp
index 26f93780..6c365e9e 100644
--- a/docs/source/_json/workflow_get.resp
+++ b/docs/source/_json/workflow_get.resp
@@ -2,6 +2,7 @@ HTTP/1.1 200 OK
 Content-Type: application/json
 
 {
+  "@id": "http://localhost:55001/plone/front-page/@workflow", 
   "history": [
     {
       "action": null, 
diff --git a/docs/source/components.rst b/docs/source/components.rst
index 49db7373..647f0708 100644
--- a/docs/source/components.rst
+++ b/docs/source/components.rst
@@ -14,11 +14,11 @@ Breadcrumbs
 Getting the breadcrumbs for the current page:
 
 ..  http:example:: curl httpie python-requests
-    :request: _json/breadcrumbs.req
+    :request: _json/component_breadcrumbs.req
 
 Example response:
 
-.. literalinclude:: _json/breadcrumbs.resp
+.. literalinclude:: _json/component_breadcrumbs.resp
    :language: http
 
 Navigation
@@ -27,9 +27,9 @@ Navigation
 Getting the top navigation items:
 
 ..  http:example:: curl httpie python-requests
-    :request: _json/navigation.req
+    :request: _json/component_navigation.req
 
 Example response:
 
-.. literalinclude:: _json/navigation.resp
+.. literalinclude:: _json/component_navigation.resp
    :language: http
diff --git a/docs/source/content.rst b/docs/source/content.rst
index 396f0cf0..5595bc70 100644
--- a/docs/source/content.rst
+++ b/docs/source/content.rst
@@ -328,8 +328,12 @@ The resources contained within a resource can be reordered using the `ordering`
 
 Use the `obj_id` subkey to specify which resource to reorder.
 The subkey `delta` can be 'top', 'bottom', or a negative or positive integer for moving up or down.
+
 Reordering resources within a subset of resources can be done using the `subset_ids` subkey.
+
 A response 400 BadRequest with a message 'Client/server ordering mismatch' will be returned if the value is not in the same order as serverside.
 
+A response 400 BadRequest with a message 'Content ordering is not supported by this resource' will be returned if the container does not support ordering.
+
 ..  http:example:: curl httpie python-requests
     :request: _json/content_reorder.req
diff --git a/docs/source/expansion.rst b/docs/source/expansion.rst
new file mode 100644
index 00000000..a8f5052a
--- /dev/null
+++ b/docs/source/expansion.rst
@@ -0,0 +1,112 @@
+Expansion
+=========
+
+Expansion is a mechanism in plone.restapi to embed additional "components",
+such as navigation, breadcrumbs, schema, or workflow within the main content
+response. This helps the API consumers to avoid unneccesary request.
+
+Say you want to show a document in Plone together with the breadcrumbs and a
+workflow switcher. Instead of doing three individual requests, you can just
+expand the breadcrumbs and the workflow "components" within the document GET
+request.
+
+The list of expandable components is listed in the "@components" attribute
+in the reponse of any content GET request::
+
+  GET /plone/front-page HTTP/1.1
+  Accept: application/json
+  Authorization: Basic YWRtaW46c2VjcmV0
+
+  {
+    "@id": "http://localhost:55001/plone/front-page",
+    "@type": "Document",
+    "@components": [
+        {"@id": "http://localhost:55001/plone/front-page/@breadcrumbs"},
+        {"@id": "http://localhost:55001/plone/front-page/@navigation"},
+        {"@id": "http://localhost:55001/plone/front-page/@schema"},
+        {"@id": "http://localhost:55001/plone/front-page/@workflow"}
+    },
+    "UID": "1f699ffa110e45afb1ba502f75f7ec33",
+    "title": "Welcome to Plone",
+    ...
+  }
+
+Request Unexpanded:
+
+..  http:example:: curl httpie python-requests
+    :request: _json/expansion.req
+
+Response Unexpanded:
+
+.. literalinclude:: _json/expansion.resp
+   :language: http
+
+In order to expand and embed one or more components, use the "expand" GET
+parameter and provide either a single component or a comma-separated list
+of the components you want to embed. Say you want to expand the "breadcrumbs"
+component::
+
+  GET /plone/front-page?expand=breadcrumbs HTTP/1.1
+  Accept: application/json
+  Authorization: Basic YWRtaW46c2VjcmV0
+
+  {
+    "@id": "http://localhost:55001/plone/front-page",
+    "@type": "Document",
+    "@components": {
+      "breadcrumbs": {
+        "@id": "http://localhost:55001/plone/front-page/@components/breadcrumbs",
+        "items": [
+          {
+            "title": "Welcome to Plone",
+            "url": "http://localhost:55001/plone/front-page"
+          }
+        ]
+      },
+      "navigation": "http://localhost:55001/plone/front-page/@navigation",
+      "schema": "http://localhost:55001/plone/front-page/@schema",
+      "workflow": {
+        "history": [
+          {
+            "action": null,
+            "actor": "test_user_1_",
+            "comments": "",
+            "review_state": "private",
+            "time": "2016-10-21T19:00:00+00:00"
+          }
+        ],
+        "transitions": [
+          {
+            "@id": "http://localhost:55001/plone/front-page/@workflow/publish",
+            "title": "Publish"
+          },
+          {
+            "@id": "http://localhost:55001/plone/front-page/@workflow/submit",
+            "title": "Submit for publication"
+          }
+        ]
+      },
+    },
+    "UID": "1f699ffa110e45afb1ba502f75f7ec33",
+    "title": "Welcome to Plone"
+  }
+
+Request Expanded:
+
+..  http:example:: curl httpie python-requests
+    :request: _json/expansion_expanded.req
+
+Response Expanded:
+
+.. literalinclude:: _json/expansion_expanded.resp
+   :language: http
+
+Here is an exaxmple of a request that expands all possible expansions:
+
+..  http:example:: curl httpie python-requests
+    :request: _json/expansion_expanded_full.req
+
+And the response:
+
+.. literalinclude:: _json/expansion_expanded_full.resp
+   :language: http
diff --git a/docs/source/index.rst b/docs/source/index.rst
index e7016298..ea4278fd 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -26,6 +26,7 @@ Contents
    batching
    comments
    copymove
+   expansion
    workflow
    sharing
    registry
diff --git a/docs/source/searching.rst b/docs/source/searching.rst
index 1b3e5ab6..91b19754 100644
--- a/docs/source/searching.rst
+++ b/docs/source/searching.rst
@@ -30,6 +30,11 @@ In order to return specific metadata columns, see the documentation of the ``met
         resultset exceeds the batch size. See :doc:`/batching` for more
         details on how to work with batched results.
 
+.. warning::
+        The @@search view or in Plone LiveSearch widget are coded in a way that the SearchableText parameter is expanded by including a * wildcard at the end.
+        This is done in order to match also the partial results of the beginning of a search term(s).
+        plone.restapi @search endpoint will not do that for you. You'll have to add it if you want to keep this feature.
+
 
 Query format
 ------------
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 46725688..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-[templer.local]
-template = plone_basic
-
diff --git a/setup.py b/setup.py
index 7a43e0ac..c0038a6e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
 from setuptools import setup, find_packages
 
-version = '1.0a20.dev0'
+version = '1.0a20'
 
 long_description = (
     open('README.rst').read() + '\n' +
diff --git a/site.cfg b/site.cfg
index 6c51af6d..9b2d5824 100644
--- a/site.cfg
+++ b/site.cfg
@@ -3,6 +3,47 @@ extensions = mr.developer
 extends = buildout.cfg
 eggs += plone.restapi
 auto-checkout = plone.restapi
+parts = instance plonesite
+
+
+[instance]
+recipe = plone.recipe.zope2instance
+user = admin:admin
+http-address = 8080
+eggs =
+    Plone
+    plone.restapi
+    collective.folderishtypes [dexterity]
+
+zcml-additional =
+  <configure xmlns="http://namespaces.zope.org/zope"
+             xmlns:plone="http://namespaces.plone.org/plone">
+  <plone:CORSPolicy
+    allow_origin="http://localhost:4300,http://127.0.0.1:4300,http://localhost:3000,http://127.0.0.1:3000"
+    allow_methods="DELETE,GET,OPTIONS,PATCH,POST,PUT"
+    allow_credentials="true"
+    expose_headers="Content-Length,X-My-Header"
+    allow_headers="Accept,Authorization,Content-Type,X-Custom-Header"
+    max_age="3600"
+    />
+  </configure>
+
+
+[plonesite]
+recipe = collective.recipe.plonesite
+site-id = Plone
+instance = instance
+profiles-initial = Products.CMFPlone:dependencies
+profiles =
+    plonetheme.barceloneta:default
+    plone.app.contenttypes:plone-content
+    plone.restapi:default
+    plone.app.mosaic:default
+    collective.folderishtypes.dx:default
+upgrade-portal = False
+upgrade-all-profiles = False
+site-replace = True
+
 
 [sources]
 plone.restapi = git git://github.com/plone/plone.restapi.git
diff --git a/src/plone/restapi/deserializer/configure.zcml b/src/plone/restapi/deserializer/configure.zcml
index 006d4482..21fcf196 100644
--- a/src/plone/restapi/deserializer/configure.zcml
+++ b/src/plone/restapi/deserializer/configure.zcml
@@ -3,6 +3,8 @@
     xmlns:zcml="http://namespaces.zope.org/zcml"
     i18n_domain="plone.restapi">
 
+  <adapter factory=".site.DeserializeSiteRootFromJson" />
+
   <adapter factory=".dxcontent.DeserializeFromJson" />
   <adapter factory=".dxfields.DefaultFieldDeserializer" />
   <adapter factory=".dxfields.DatetimeFieldDeserializer" />
diff --git a/src/plone/restapi/deserializer/mixins.py b/src/plone/restapi/deserializer/mixins.py
index c72f7ef5..98739ed3 100644
--- a/src/plone/restapi/deserializer/mixins.py
+++ b/src/plone/restapi/deserializer/mixins.py
@@ -12,11 +12,20 @@ def handle_ordering(self, data):
             delta = data['ordering']['delta']
             subset_ids = data['ordering'].get('subset_ids')
 
+            # The REST api returns only content items and a Zope resource
+            # may contain non-content items. We need to set the subset_ids
+            # so we'll move items relative to each other.
+            if not subset_ids:
+                subset_ids = self.context.contentIds()
+
             self.reorderItems(obj_id, delta, subset_ids)
 
-    def reorderItems(self, obj_id, delta, subset_ids=None):
+    def reorderItems(self, obj_id, delta, subset_ids):
         # Based on wildcard.foldercontents.viewsItemOrder
         ordering = self.getOrdering()
+        if ordering is None:
+            msg = 'Content ordering is not supported by this resource'
+            raise BadRequest(msg)
 
         # Make sure we're seeing the same order as the client is.
         if subset_ids:
@@ -38,7 +47,7 @@ def reorderItems(self, obj_id, delta, subset_ids=None):
     def getOrdering(self):
         if IPloneSiteRoot.providedBy(self.context):
             return self.context
-        else:
+        elif getattr(self.context, 'getOrdering', None):
             ordering = self.context.getOrdering()
             if not IExplicitOrdering.providedBy(ordering):
                 return None
diff --git a/src/plone/restapi/deserializer/site.py b/src/plone/restapi/deserializer/site.py
new file mode 100644
index 00000000..7b749383
--- /dev/null
+++ b/src/plone/restapi/deserializer/site.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+from Products.CMFPlone.interfaces import IPloneSiteRoot
+from plone.restapi.deserializer import json_body
+from plone.restapi.interfaces import IDeserializeFromJson
+from zope.component import adapter
+from zope.interface import implementer
+
+from plone.restapi.deserializer.mixins import OrderingMixin
+from zope.publisher.interfaces import IRequest
+
+
+@implementer(IDeserializeFromJson)
+@adapter(IPloneSiteRoot, IRequest)
+class DeserializeSiteRootFromJson(OrderingMixin, object):
+    """JSON deserializer for the Plone site root
+    """
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, validate_all=False):
+        # Currently we only do layout and ordering, as the plone site root
+        # has no schema or something like that.
+        data = json_body(self.request)
+
+        if 'layout' in data:
+            layout = data['layout']
+            self.context.setLayout(layout)
+
+        # OrderingMixin
+        if 'ordering' in data and 'subset_ids' not in data['ordering']:
+            data['ordering']['subset_ids'] = self.context.contentIds()
+        self.handle_ordering(data)
+
+        return self.context
diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py
index e3b15f59..eb57f229 100644
--- a/src/plone/restapi/interfaces.py
+++ b/src/plone/restapi/interfaces.py
@@ -65,6 +65,16 @@ def __call__(value):
         """
 
 
+class IExpandableElement(Interface):
+    """A named adapter that deserializes an element in expanded or collapsed
+       form.
+    """
+
+    def __call__(expand=False):
+        """
+        """
+
+
 class IZCatalogCompatibleQuery(Interface):
     """A multi adapter responsible for converting a catalog query provided as
     a Python dictionary, but with possibly incorrect value types, to a
diff --git a/src/plone/restapi/serializer/atcontent.py b/src/plone/restapi/serializer/atcontent.py
index 42a68e18..53125363 100644
--- a/src/plone/restapi/serializer/atcontent.py
+++ b/src/plone/restapi/serializer/atcontent.py
@@ -1,18 +1,19 @@
 # -*- coding: utf-8 -*-
 from Acquisition import aq_inner
 from Acquisition import aq_parent
+from Products.Archetypes.interfaces import IBaseFolder
+from Products.Archetypes.interfaces import IBaseObject
+from Products.CMFCore.utils import getToolByName
 from plone.restapi.batching import HypermediaBatch
 from plone.restapi.interfaces import IFieldSerializer
 from plone.restapi.interfaces import ISerializeToJson
 from plone.restapi.interfaces import ISerializeToJsonSummary
-from Products.Archetypes.interfaces import IBaseFolder
-from Products.Archetypes.interfaces import IBaseObject
-from Products.CMFCore.utils import getToolByName
+from plone.restapi.serializer.expansion import expandable_elements
 from zope.component import adapter
 from zope.component import getMultiAdapter
 from zope.component import queryMultiAdapter
-from zope.interface import implementer
 from zope.interface import Interface
+from zope.interface import implementer
 
 
 @implementer(ISerializeToJson)
@@ -48,6 +49,9 @@ def __call__(self, version=None):
             'layout': self.context.getLayout(),
         }
 
+        # Insert expandable elements
+        result.update(expandable_elements(self.context, self.request))
+
         for field in obj.Schema().fields():
 
             if 'r' not in field.mode or not field.checkPermission('r', obj):
diff --git a/src/plone/restapi/serializer/dxcontent.py b/src/plone/restapi/serializer/dxcontent.py
index b5a89239..3ca280eb 100644
--- a/src/plone/restapi/serializer/dxcontent.py
+++ b/src/plone/restapi/serializer/dxcontent.py
@@ -2,6 +2,7 @@
 from AccessControl import getSecurityManager
 from Acquisition import aq_inner
 from Acquisition import aq_parent
+from Products.CMFCore.utils import getToolByName
 from plone.autoform.interfaces import READ_PERMISSIONS_KEY
 from plone.dexterity.interfaces import IDexterityContainer
 from plone.dexterity.interfaces import IDexterityContent
@@ -11,14 +12,14 @@
 from plone.restapi.interfaces import ISerializeToJson
 from plone.restapi.interfaces import ISerializeToJsonSummary
 from plone.restapi.serializer.converters import json_compatible
+from plone.restapi.serializer.expansion import expandable_elements
 from plone.supermodel.utils import mergedTaggedValueDict
-from Products.CMFCore.utils import getToolByName
 from zope.component import adapter
 from zope.component import getMultiAdapter
 from zope.component import queryMultiAdapter
 from zope.component import queryUtility
-from zope.interface import implementer
 from zope.interface import Interface
+from zope.interface import implementer
 from zope.schema import getFields
 from zope.security.interfaces import IPermission
 
@@ -61,7 +62,11 @@ def __call__(self, version=None):
             'layout': self.context.getLayout(),
         }
 
-        for schema in iterSchemata(obj):
+        # Insert expandable elements
+        result.update(expandable_elements(self.context, self.request))
+
+        # Insert field values
+        for schema in iterSchemata(self.context):
 
             read_permissions = mergedTaggedValueDict(
                 schema, READ_PERMISSIONS_KEY)
diff --git a/src/plone/restapi/serializer/expansion.py b/src/plone/restapi/serializer/expansion.py
new file mode 100644
index 00000000..cdf0eec8
--- /dev/null
+++ b/src/plone/restapi/serializer/expansion.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+from plone.restapi.interfaces import IExpandableElement
+from zope.component import getAdapters
+
+
+def expandable_elements(context, request):
+    """Returns a dict containing all expandable elements.
+    """
+    expands = request.form.get('expand', '').split(',')
+    elements = getAdapters((context, request), IExpandableElement)
+    res = {}
+    for element in elements:
+        if element[0] in expands:
+            update_dict_recursively(res, element[1](expand=True))
+        else:
+            update_dict_recursively(res, element[1](expand=False))
+    return {"@components": res}
+
+
+def update_dict_recursively(d, u):
+    for key, value in u.iteritems():
+        if isinstance(value, dict):
+            r = update_dict_recursively(d.get(key, {}), value)
+            d[key] = r
+        else:
+            d[key] = u[key]
+    return d
diff --git a/src/plone/restapi/serializer/site.py b/src/plone/restapi/serializer/site.py
index 8476f64d..24b8284e 100644
--- a/src/plone/restapi/serializer/site.py
+++ b/src/plone/restapi/serializer/site.py
@@ -1,13 +1,14 @@
 # -*- coding: utf-8 -*-
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces import IPloneSiteRoot
 from plone.restapi.batching import HypermediaBatch
 from plone.restapi.interfaces import ISerializeToJson
 from plone.restapi.interfaces import ISerializeToJsonSummary
-from Products.CMFCore.utils import getToolByName
-from Products.CMFPlone.interfaces import IPloneSiteRoot
+from plone.restapi.serializer.expansion import expandable_elements
 from zope.component import adapter
 from zope.component import getMultiAdapter
-from zope.interface import implementer
 from zope.interface import Interface
+from zope.interface import implementer
 
 
 @implementer(ISerializeToJson)
@@ -44,6 +45,9 @@ def __call__(self, version=None):
             'parent': {},
         }
 
+        # Insert expandable elements
+        result.update(expandable_elements(self.context, self.request))
+
         result['items_total'] = batch.items_total
         if batch.links:
             result['batching'] = batch.links
diff --git a/src/plone/restapi/services/breadcrumbs/configure.zcml b/src/plone/restapi/services/breadcrumbs/configure.zcml
index 0c39c889..167d7168 100644
--- a/src/plone/restapi/services/breadcrumbs/configure.zcml
+++ b/src/plone/restapi/services/breadcrumbs/configure.zcml
@@ -2,6 +2,8 @@
     xmlns="http://namespaces.zope.org/zope"
     xmlns:plone="http://namespaces.plone.org/plone">
 
+  <adapter factory=".get.Breadcrumbs" name="breadcrumbs"/>
+
   <plone:service
     method="GET"
     for="zope.interface.Interface"
@@ -9,5 +11,5 @@
     name="@breadcrumbs"
     permission="zope2.View"
     />
-
+    
 </configure>
diff --git a/src/plone/restapi/services/breadcrumbs/get.py b/src/plone/restapi/services/breadcrumbs/get.py
index b8d9b97a..c09c4c9d 100644
--- a/src/plone/restapi/services/breadcrumbs/get.py
+++ b/src/plone/restapi/services/breadcrumbs/get.py
@@ -1,17 +1,44 @@
 # -*- coding: utf-8 -*-
+from plone.restapi.interfaces import IExpandableElement
 from plone.restapi.services import Service
+from zope.component import adapter
 from zope.component import getMultiAdapter
+from zope.interface import Interface
+from zope.interface import implementer
 
 
-class BreadcrumbsGet(Service):
+@implementer(IExpandableElement)
+@adapter(Interface, Interface)
+class Breadcrumbs(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, expand=False):
+        result = {
+            'breadcrumbs': {
+                '@id': '{}/@breadcrumbs'.format(self.context.absolute_url()),
+            },
+        }
+        if not expand:
+            return result
 
-    def reply(self):
         breadcrumbs_view = getMultiAdapter((self.context, self.request),
                                            name="breadcrumbs_view")
-        result = []
+        items = []
         for crumb in breadcrumbs_view.breadcrumbs():
-            result.append({
+            items.append({
                 'title': crumb['Title'],
                 'url': crumb['absolute_url']
             })
+
+        result['breadcrumbs']['items'] = items
         return result
+
+
+class BreadcrumbsGet(Service):
+
+    def reply(self):
+        breadcrumbs = Breadcrumbs(self.context, self.request)
+        return breadcrumbs(expand=True)['breadcrumbs']
diff --git a/src/plone/restapi/services/components/breadcrumbs.py b/src/plone/restapi/services/components/breadcrumbs.py
new file mode 100644
index 00000000..24287963
--- /dev/null
+++ b/src/plone/restapi/services/components/breadcrumbs.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from plone.restapi.interfaces import IExpandableElement
+from zope.component import adapter
+from zope.component import getMultiAdapter
+from zope.interface import Interface
+from zope.interface import implementer
+
+
+@implementer(IExpandableElement)
+@adapter(Interface, Interface)
+class Breadcrumbs(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, expand=False):
+        if not expand:
+            return {'@components': {'breadcrumbs': {
+                '@id': '{}/@components/breadcrumbs'.format(
+                    self.context.absolute_url()),
+            }}}
+
+        breadcrumbs_view = getMultiAdapter((self.context, self.request),
+                                           name="breadcrumbs_view")
+        result = []
+        for crumb in breadcrumbs_view.breadcrumbs():
+            result.append({
+                'title': crumb['Title'],
+                'url': crumb['absolute_url']
+            })
+        return {'@components': {'breadcrumbs': result}}
diff --git a/src/plone/restapi/services/components/get.py b/src/plone/restapi/services/components/get.py
index 65345dd3..5947d0c3 100644
--- a/src/plone/restapi/services/components/get.py
+++ b/src/plone/restapi/services/components/get.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 from plone.restapi.services import Service
+from plone.restapi.services.components.breadcrumbs import Breadcrumbs
+from plone.restapi.services.components.navigation import Navigation
 from zope.deprecation import deprecate
-from zope.component import getMultiAdapter
 from zope.interface import implements
 from zope.publisher.interfaces import IPublishTraverse
 
@@ -33,33 +34,13 @@ def _wrap_component_items(self, items, component_id):
         }
         return component
 
-    def get_navigation(self):
-        tabs = getMultiAdapter((self.context, self.request),
-                               name="portal_tabs_view")
-        result = []
-        for tab in tabs.topLevelTabs():
-            result.append({
-                'title': tab.get('title', tab.get('name')),
-                'url': tab['url'] + ''
-            })
-        return result
-
-    def get_breadcrumbs(self):
-        breadcrumbs_view = getMultiAdapter((self.context, self.request),
-                                           name="breadcrumbs_view")
-        result = []
-        for crumb in breadcrumbs_view.breadcrumbs():
-            result.append({
-                'title': crumb['Title'],
-                'url': crumb['absolute_url']
-            })
-        return result
-
     def _render_component(self, component_id):
         if component_id == 'navigation':
-            items = self.get_navigation()
+            items = Navigation(self.context, self.request)(expand=True)[
+                '@components']['navigation']
         elif component_id == 'breadcrumbs':
-            items = self.get_breadcrumbs()
+            items = Breadcrumbs(self.context, self.request)(expand=True)[
+                '@components']['breadcrumbs']
         else:
             raise NotImplementedError(
                 'This endpoint does not currently support the '
diff --git a/src/plone/restapi/services/components/navigation.py b/src/plone/restapi/services/components/navigation.py
new file mode 100644
index 00000000..379cbb50
--- /dev/null
+++ b/src/plone/restapi/services/components/navigation.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from plone.restapi.interfaces import IExpandableElement
+from zope.component import adapter
+from zope.component import getMultiAdapter
+from zope.interface import Interface
+from zope.interface import implementer
+
+
+@implementer(IExpandableElement)
+@adapter(Interface, Interface)
+class Navigation(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, expand=False):
+        if not expand:
+            return {'@components': {'navigation': {
+                '@id': '{}/@components/navigation'.format(
+                    self.context.absolute_url()),
+            }}}
+
+        tabs = getMultiAdapter((self.context, self.request),
+                               name="portal_tabs_view")
+        result = []
+        for tab in tabs.topLevelTabs():
+            result.append({
+                'title': tab.get('title', tab.get('name')),
+                'url': tab['url'] + ''
+            })
+        return {'@components': {'navigation': result}}
diff --git a/src/plone/restapi/services/content/configure.zcml b/src/plone/restapi/services/content/configure.zcml
index 9e338f5f..f3cbde14 100644
--- a/src/plone/restapi/services/content/configure.zcml
+++ b/src/plone/restapi/services/content/configure.zcml
@@ -19,6 +19,13 @@
 
   <plone:service
     method="PATCH"
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    factory=".update.ContentPatch"
+    permission="cmf.ModifyPortalContent"
+    />
+
+  <plone:service
+    method="PATCH"
     for="Products.CMFCore.interfaces.IContentish"
     factory=".update.ContentPatch"
     permission="cmf.ModifyPortalContent"
diff --git a/src/plone/restapi/services/navigation/configure.zcml b/src/plone/restapi/services/navigation/configure.zcml
index 05e200e4..090900cd 100644
--- a/src/plone/restapi/services/navigation/configure.zcml
+++ b/src/plone/restapi/services/navigation/configure.zcml
@@ -2,6 +2,8 @@
     xmlns="http://namespaces.zope.org/zope"
     xmlns:plone="http://namespaces.plone.org/plone">
 
+  <adapter factory=".get.Navigation" name="navigation"/>
+
   <plone:service
     method="GET"
     for="zope.interface.Interface"
diff --git a/src/plone/restapi/services/navigation/get.py b/src/plone/restapi/services/navigation/get.py
index a2bf0504..e59168c4 100644
--- a/src/plone/restapi/services/navigation/get.py
+++ b/src/plone/restapi/services/navigation/get.py
@@ -1,17 +1,43 @@
 # -*- coding: utf-8 -*-
+from plone.restapi.interfaces import IExpandableElement
 from plone.restapi.services import Service
+from zope.component import adapter
 from zope.component import getMultiAdapter
+from zope.interface import Interface
+from zope.interface import implementer
 
 
-class NavigationGet(Service):
+@implementer(IExpandableElement)
+@adapter(Interface, Interface)
+class Navigation(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, expand=False):
+        result = {
+            'navigation': {
+                '@id': '{}/@navigation'.format(self.context.absolute_url()),
+            },
+        }
+        if not expand:
+            return result
 
-    def reply(self):
         tabs = getMultiAdapter((self.context, self.request),
                                name="portal_tabs_view")
-        result = []
+        items = []
         for tab in tabs.topLevelTabs():
-            result.append({
+            items.append({
                 'title': tab.get('title', tab.get('name')),
                 'url': tab['url'] + ''
             })
+        result['navigation']['items'] = items
         return result
+
+
+class NavigationGet(Service):
+
+    def reply(self):
+        navigation = Navigation(self.context, self.request)
+        return navigation(expand=True)['navigation']
diff --git a/src/plone/restapi/services/workflow/configure.zcml b/src/plone/restapi/services/workflow/configure.zcml
index 1996ff01..e26831d0 100644
--- a/src/plone/restapi/services/workflow/configure.zcml
+++ b/src/plone/restapi/services/workflow/configure.zcml
@@ -7,7 +7,7 @@
     method="GET"
     name="@workflow"
     for="Products.CMFCore.interfaces.IContentish"
-    factory=".info.WorkflowInfo"
+    factory=".info.WorkflowInfoService"
     permission="zope2.View"
     />
 
@@ -19,4 +19,6 @@
     permission="zope2.View"
     />
 
+  <adapter factory=".info.WorkflowInfo" name="workflow"/>
+
 </configure>
diff --git a/src/plone/restapi/services/workflow/info.py b/src/plone/restapi/services/workflow/info.py
index 729de85d..a8fb73f4 100644
--- a/src/plone/restapi/services/workflow/info.py
+++ b/src/plone/restapi/services/workflow/info.py
@@ -1,13 +1,31 @@
 # -*- coding: utf-8 -*-
 from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.interfaces._content import IWorkflowAware
+from plone.restapi.interfaces import IExpandableElement
 from plone.restapi.serializer.converters import json_compatible
 from plone.restapi.services import Service
+from zope.component import adapter
+from zope.interface import Interface
+from zope.interface import implementer
 
 
-class WorkflowInfo(Service):
-    """Get workflow information
-    """
-    def reply(self):
+@implementer(IExpandableElement)
+@adapter(IWorkflowAware, Interface)
+class WorkflowInfo(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, expand=False):
+        result = {
+            'workflow': {
+                "@id": '{}/@workflow'.format(self.context.absolute_url()),
+            },
+        }
+        if not expand:
+            return result
+
         wftool = getToolByName(self.context, 'portal_workflow')
         history = wftool.getInfoFor(self.context, "review_history")
 
@@ -28,7 +46,16 @@ def reply(self):
                 action['review_state'],
                 self.context.portal_type)
 
-        return {
+        result['workflow'].update({
             'history': json_compatible(history),
             'transitions': transitions,
-        }
+            })
+        return result
+
+
+class WorkflowInfoService(Service):
+    """Get workflow information
+    """
+    def reply(self):
+        info = WorkflowInfo(self.context, self.request)
+        return info(expand=True)['workflow']
diff --git a/src/plone/restapi/tests/mixin_ordering.py b/src/plone/restapi/tests/mixin_ordering.py
new file mode 100644
index 00000000..180cc0e8
--- /dev/null
+++ b/src/plone/restapi/tests/mixin_ordering.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+import json
+
+from zExceptions import BadRequest
+
+
+class OrderingMixin():
+    # This assumes there is a self.folder with 9 documents in it.
+
+    def test_reorder(self):
+        # We run all this in one test, because of dependend ordering.
+        # initial situation
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Normal
+        # Move to top
+        data = {'ordering': {'delta': 'top', 'obj_id': 'doc9'}}
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc9', 'doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Move to bottom
+        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc9'}}
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Delta up
+        data = {'ordering': {'delta': -2, 'obj_id': 'doc5'}}
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Delta down
+        data = {'ordering': {'delta': 2, 'obj_id': 'doc6'}}
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9', ],  # noqa
+            self.folder.contentIds()
+        )
+
+        # subset ids
+        # Move to top
+        data = {'ordering': {'delta': 'top', 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Move to bottom
+        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc8', 'subset_ids': ['doc8', 'doc2', 'doc3']}}  # noqa
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Delta up
+        data = {'ordering': {'delta': -1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc5', 'doc8', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+        # Delta down
+        data = {'ordering': {'delta': 1, 'obj_id': 'doc2', 'subset_ids': ['doc2', 'doc8', 'doc3']}}  # noqa
+        self.deserialize(body=json.dumps(data), context=self.folder)
+        self.assertEquals(
+            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+    def test_reorder_subsetids(self):
+        # sanity check, initial situation
+        self.assertEquals(
+            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9'],  # noqa
+            self.folder.contentIds()
+        )
+
+        data = {'ordering': {'delta': 1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc8', 'doc6']}}  # noqa
+
+        with self.assertRaises(BadRequest) as cm:
+            self.deserialize(body=json.dumps(data), context=self.folder)
+
+        self.assertEquals(
+            u'Client/server ordering mismatch',
+            cm.exception.message
+        )
diff --git a/src/plone/restapi/tests/test_atcontent_deserializer.py b/src/plone/restapi/tests/test_atcontent_deserializer.py
index 2a7d5a2d..a97f3cbd 100644
--- a/src/plone/restapi/tests/test_atcontent_deserializer.py
+++ b/src/plone/restapi/tests/test_atcontent_deserializer.py
@@ -7,6 +7,7 @@
 from plone.restapi.deserializer.atcontent import ValidationRequest
 from plone.restapi.interfaces import IDeserializeFromJson
 from plone.restapi.testing import PLONE_RESTAPI_AT_INTEGRATION_TESTING
+from plone.restapi.tests.mixin_ordering import OrderingMixin
 from zExceptions import BadRequest
 from zope.component import getMultiAdapter
 from zope.component import provideHandler
@@ -17,11 +18,10 @@
 from Products.Archetypes.interfaces import IObjectPreValidation
 
 
-import json
 import unittest
 
 
-class TestATContentDeserializer(unittest.TestCase):
+class TestATContentDeserializer(unittest.TestCase, OrderingMixin):
 
     layer = PLONE_RESTAPI_AT_INTEGRATION_TESTING
 
@@ -152,97 +152,6 @@ def test_set_layout(self):
         self.deserialize(body='{"layout": "my_new_layout"}')
         self.assertEquals('my_new_layout', self.doc1.getLayout())
 
-    def test_reorder(self):
-        # We run all this in one test, because of dependend ordering.
-        # initial situation
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Normal
-        # Move to top
-        data = {'ordering': {'delta': 'top', 'obj_id': 'doc9'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc9', 'doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Move to bottom
-        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc9'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta up
-        data = {'ordering': {'delta': -2, 'obj_id': 'doc5'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta down
-        data = {'ordering': {'delta': 2, 'obj_id': 'doc6'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # subset ids
-        # Move to top
-        data = {'ordering': {'delta': 'top', 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Move to bottom
-        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc8', 'subset_ids': ['doc8', 'doc2', 'doc3']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta up
-        data = {'ordering': {'delta': -1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc8', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta down
-        data = {'ordering': {'delta': 1, 'obj_id': 'doc2', 'subset_ids': ['doc2', 'doc8', 'doc3']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-    def test_reorder_subsetids(self):
-        # sanity check, initial situation
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        data = {'ordering': {'delta': 1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc8', 'doc6']}}  # noqa
-
-        with self.assertRaises(BadRequest) as cm:
-            self.deserialize(body=json.dumps(data), context=self.folder)
-
-        self.assertEquals(
-            u'Client/server ordering mismatch',
-            cm.exception.message
-        )
-
 
 class TestValidationRequest(unittest.TestCase):
 
diff --git a/src/plone/restapi/tests/test_atcontent_serializer.py b/src/plone/restapi/tests/test_atcontent_serializer.py
index 143dde64..ac4c1fad 100644
--- a/src/plone/restapi/tests/test_atcontent_serializer.py
+++ b/src/plone/restapi/tests/test_atcontent_serializer.py
@@ -2,9 +2,14 @@
 from DateTime import DateTime
 from plone.app.testing import TEST_USER_ID
 from plone.app.testing import setRoles
+from plone.restapi.interfaces import IExpandableElement
 from plone.restapi.interfaces import ISerializeToJson
 from plone.restapi.testing import PLONE_RESTAPI_AT_INTEGRATION_TESTING
+from plone.restapi.tests.test_expansion import ExpandableElementFoo
 from zope.component import getMultiAdapter
+from zope.component import provideAdapter
+from zope.interface import Interface
+from zope.publisher.interfaces.browser import IBrowserRequest
 
 import json
 import unittest
@@ -149,3 +154,14 @@ def test_get_layout(self):
         obj = self.serialize(self.doc1)
         self.assertIn('layout', obj)
         self.assertEquals(current_layout, obj['layout'])
+
+    def test_serializer_includes_expansion(self):
+        provideAdapter(
+            ExpandableElementFoo,
+            adapts=(Interface, IBrowserRequest),
+            provides=IExpandableElement,
+            name='foo'
+        )
+        obj = self.serialize(self.doc1)
+        self.assertIn('foo', obj['@components'])
+        self.assertEqual('collapsed', obj['@components']['foo'])
diff --git a/src/plone/restapi/tests/test_components.py b/src/plone/restapi/tests/test_components.py
index 97f1390e..912510f9 100644
--- a/src/plone/restapi/tests/test_components.py
+++ b/src/plone/restapi/tests/test_components.py
@@ -54,6 +54,19 @@ def test_breadcrumb(self):
             }]
         )
 
+    def test_collapsed_breadcrumbs_in_content_serialization(self):
+        obj = self.api_session.get('/folder').json()
+        self.assertIn('@components', obj)
+        self.assertIn('breadcrumbs', obj['@components'])
+        self.assertIn('@id', obj['@components']['breadcrumbs'])
+        self.assertNotIn('items', obj['@components']['breadcrumbs'])
+
+    def test_expanded_breadcrumbs_in_content_serialization(self):
+        obj = self.api_session.get('/folder/doc1?expand=breadcrumbs').json()
+        self.assertIn('@components', obj)
+        self.assertIn('breadcrumbs', obj['@components'])
+        self.assertIn('items', obj['@components']['breadcrumbs'])
+
     def test_navigation(self):
         response = self.api_session.get('/folder/@components/navigation')
 
@@ -72,3 +85,16 @@ def test_navigation(self):
                 }],
             }]
         )
+
+    def test_collapsed_navigation_in_content_serialization(self):
+        obj = self.api_session.get('/folder').json()
+        self.assertIn('@components', obj)
+        self.assertIn('navigation', obj['@components'])
+        self.assertIn('@id', obj['@components']['navigation'])
+        self.assertNotIn('items', obj['@components']['navigation'])
+
+    def test_expanded_navigation_in_content_serialization(self):
+        obj = self.api_session.get('/folder?expand=navigation').json()
+        self.assertIn('@components', obj)
+        self.assertIn('navigation', obj['@components'])
+        self.assertIn('items', obj['@components']['navigation'])
diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py
index 4f426083..cdbc8c3d 100644
--- a/src/plone/restapi/tests/test_documentation.py
+++ b/src/plone/restapi/tests/test_documentation.py
@@ -706,14 +706,24 @@ def test_documentation_groups_delete(self):
 
     def test_documentation_breadcrumbs(self):
         response = self.api_session.get(
-            '{}/@components/breadcrumbs'.format(self.document.absolute_url()))
+            '{}/@breadcrumbs'.format(self.document.absolute_url()))
         save_request_and_response_for_docs('breadcrumbs', response)
 
     def test_documentation_navigation(self):
         response = self.api_session.get(
-            '{}/@components/navigation'.format(self.document.absolute_url()))
+            '{}/@navigation'.format(self.document.absolute_url()))
         save_request_and_response_for_docs('navigation', response)
 
+    def test_documentation_componente_breadcrumbs(self):
+        response = self.api_session.get(
+            '{}/@components/breadcrumbs'.format(self.document.absolute_url()))
+        save_request_and_response_for_docs('component_breadcrumbs', response)
+
+    def test_documentation_components_navigation(self):
+        response = self.api_session.get(
+            '{}/@components/navigation'.format(self.document.absolute_url()))
+        save_request_and_response_for_docs('component_navigation', response)
+
     def test_documentation_principals(self):
         gtool = api.portal.get_tool('portal_groups')
         properties = {
@@ -826,6 +836,24 @@ def test_documentation_sharing_search(self):
         )
         save_request_and_response_for_docs('sharing_search', response)
 
+    def test_documentation_expansion(self):
+        response = self.api_session.get(
+            '/front-page'
+        )
+        save_request_and_response_for_docs('expansion', response)
+
+    def test_documentation_expansion_expanded(self):
+        response = self.api_session.get(
+            '/front-page?expand=breadcrumbs'
+        )
+        save_request_and_response_for_docs('expansion_expanded', response)
+
+    def test_documentation_expansion_expanded_full(self):
+        response = self.api_session.get(
+            '/front-page?expand=breadcrumbs,navigation,schema,workflow'
+        )
+        save_request_and_response_for_docs('expansion_expanded_full', response)
+
     def test_history_get(self):
         self.document.setTitle('My new title')
         url = '{}/@history'.format(self.document.absolute_url())
@@ -1104,3 +1132,9 @@ def test_roles_get(self):
         url = '{}/@roles'.format(self.portal_url)
         response = self.api_session.get(url)
         save_request_and_response_for_docs('roles', response)
+
+    def test_documentation_expansion(self):
+        response = self.api_session.get(
+            '/front-page?expand=breadcrumbs,workflow'
+        )
+        save_request_and_response_for_docs('expansion', response)
diff --git a/src/plone/restapi/tests/test_dxcontent_deserializer.py b/src/plone/restapi/tests/test_dxcontent_deserializer.py
index 7e8dd906..26fd2411 100644
--- a/src/plone/restapi/tests/test_dxcontent_deserializer.py
+++ b/src/plone/restapi/tests/test_dxcontent_deserializer.py
@@ -6,16 +6,16 @@
 from plone.restapi.interfaces import IDeserializeFromJson
 from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
 from plone.restapi.tests.dxtypes import ITestAnnotationsBehavior
+from plone.restapi.tests.mixin_ordering import OrderingMixin
 from zExceptions import BadRequest
 from zope.component import getMultiAdapter
 from zope.component import provideHandler
 from zope.lifecycleevent.interfaces import IObjectModifiedEvent
 
-import json
 import unittest
 
 
-class TestDXContentDeserializer(unittest.TestCase):
+class TestDXContentDeserializer(unittest.TestCase, OrderingMixin):
 
     layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING
 
@@ -147,93 +147,3 @@ def test_set_layout(self):
         self.assertNotEquals(current_layout, "my_new_layout")
         self.deserialize(body='{"layout": "my_new_layout"}')
         self.assertEquals('my_new_layout', self.portal.doc1.getLayout())
-
-    def test_reorder(self):  # noqa: ignore=E501
-        # We run all this in one test, because of dependend ordering.
-        # initial situation
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Normal
-        # Move to top
-        data = {'ordering': {'delta': 'top', 'obj_id': 'doc9'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc9', 'doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', ],  # noqa
-            self.folder.objectIds())
-
-        # Move to bottom
-        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc9'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta up
-        data = {'ordering': {'delta': -2, 'obj_id': 'doc5'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc6', 'doc7', 'doc8', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta down
-        data = {'ordering': {'delta': 2, 'obj_id': 'doc6'}}
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9', ],  # noqa
-            self.folder.objectIds()
-        )
-
-        # subset ids
-        # Move to top
-        data = {'ordering': {'delta': 'top', 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Move to bottom
-        data = {'ordering': {'delta': 'bottom', 'obj_id': 'doc8', 'subset_ids': ['doc8', 'doc2', 'doc3']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc3', 'doc4', 'doc7', 'doc8', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta up
-        data = {'ordering': {'delta': -1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc3', 'doc8']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc5', 'doc8', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        # Delta down
-        data = {'ordering': {'delta': 1, 'obj_id': 'doc2', 'subset_ids': ['doc2', 'doc8', 'doc3']}}  # noqa
-        self.deserialize(body=json.dumps(data), context=self.folder)
-        self.assertEquals(
-            ['doc1', 'doc8', 'doc5', 'doc2', 'doc4', 'doc7', 'doc3', 'doc6', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-    def test_reorder_subsetids(self):
-        # sanity check, initial situation
-        self.assertEquals(
-            ['doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6', 'doc7', 'doc8', 'doc9'],  # noqa
-            self.folder.objectIds()
-        )
-
-        data = {'ordering': {'delta': 1, 'obj_id': 'doc8', 'subset_ids': ['doc2', 'doc8', 'doc6']}}  # noqa
-
-        with self.assertRaises(BadRequest) as cm:
-            self.deserialize(body=json.dumps(data), context=self.folder)
-
-        self.assertEquals(
-            u'Client/server ordering mismatch',
-            cm.exception.message
-        )
diff --git a/src/plone/restapi/tests/test_dxcontent_serializer.py b/src/plone/restapi/tests/test_dxcontent_serializer.py
index ad695176..f9ad1c8f 100644
--- a/src/plone/restapi/tests/test_dxcontent_serializer.py
+++ b/src/plone/restapi/tests/test_dxcontent_serializer.py
@@ -6,10 +6,15 @@
 from datetime import timedelta
 from plone.app.testing import TEST_USER_ID
 from plone.app.testing import setRoles
+from plone.restapi.interfaces import IExpandableElement
 from plone.restapi.interfaces import ISerializeToJson
 from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
+from plone.restapi.tests.test_expansion import ExpandableElementFoo
 from plone.uuid.interfaces import IMutableUUID
 from zope.component import getMultiAdapter
+from zope.component import provideAdapter
+from zope.interface import Interface
+from zope.publisher.interfaces.browser import IBrowserRequest
 
 import json
 import unittest
@@ -122,3 +127,14 @@ def test_get_layout(self):
         obj = self.serialize()
         self.assertIn('layout', obj)
         self.assertEquals(current_layout, obj['layout'])
+
+    def test_serializer_includes_expansion(self):
+        provideAdapter(
+            ExpandableElementFoo,
+            adapts=(Interface, IBrowserRequest),
+            provides=IExpandableElement,
+            name='foo'
+        )
+        obj = self.serialize()
+        self.assertIn('foo', obj['@components'])
+        self.assertEqual('collapsed', obj['@components']['foo'])
diff --git a/src/plone/restapi/tests/test_expansion.py b/src/plone/restapi/tests/test_expansion.py
new file mode 100644
index 00000000..5d5ceafb
--- /dev/null
+++ b/src/plone/restapi/tests/test_expansion.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+from plone.app.testing import setRoles
+from plone.app.testing import SITE_OWNER_NAME
+from plone.app.testing import SITE_OWNER_PASSWORD
+from plone.app.testing import TEST_USER_ID
+from plone.dexterity.utils import createContentInContainer
+from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
+from plone.restapi.testing import RelativeSession
+from plone.restapi.interfaces import IExpandableElement
+from plone.restapi.serializer.expansion import expandable_elements
+from zope.component import provideAdapter
+from zope.interface import Interface
+from zope.publisher.browser import TestRequest
+from zope.publisher.interfaces.browser import IBrowserRequest
+
+import transaction
+import unittest
+
+
+class ExpandableElementFoo(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, expand=False):
+        if expand:
+            return {'foo': 'expanded'}
+        else:
+            return {'foo': 'collapsed'}
+
+
+class ExpandableElementBar(object):
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def __call__(self, expand=False):
+        if expand:
+            return {'bar': 'expanded'}
+        else:
+            return {'bar': 'collapsed'}
+
+
+class TestExpansion(unittest.TestCase):
+
+    def setUp(self):
+        provideAdapter(
+            ExpandableElementFoo,
+            adapts=(Interface, IBrowserRequest),
+            provides=IExpandableElement,
+            name='foo'
+        )
+        provideAdapter(
+            ExpandableElementBar,
+            adapts=(Interface, IBrowserRequest),
+            provides=IExpandableElement,
+            name='bar'
+        )
+
+    def test_expansion_returns_collapsed_elements(self):
+        request = TestRequest()
+        self.assertEqual(
+            {'@components': {'bar': 'collapsed', 'foo': 'collapsed'}},
+            expandable_elements(None, request))
+
+    def test_expansion_returns_expanded_element(self):
+        request = TestRequest(form={'expand': 'foo'})
+        self.assertEqual(
+            {'@components': {'bar': 'collapsed', 'foo': 'expanded'}},
+            expandable_elements(None, request))
+
+    def test_expansion_returns_multiple_expanded_elements(self):
+        request = TestRequest(form={'expand': 'foo,bar'})
+        self.assertEqual(
+            {'@components': {'bar': 'expanded', 'foo': 'expanded'}},
+            expandable_elements(None, request))
+
+
+class TestExpansionFunctional(unittest.TestCase):
+
+    layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
+
+    def setUp(self):
+        self.app = self.layer['app']
+        self.portal = self.layer['portal']
+        self.portal_url = self.portal.absolute_url()
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+
+        self.api_session = RelativeSession(self.portal_url)
+        self.api_session.headers.update({'Accept': 'application/json'})
+        self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
+
+        self.folder = createContentInContainer(
+            self.portal, u'Folder',
+            id=u'folder',
+            title=u'Some Folder')
+        transaction.commit()
+
+    def test_navigation_is_expandable(self):
+        response = self.api_session.get('/folder')
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            'navigation',
+            response.json().get('@components').keys()
+        )
+
+    def test_navigation_expanded(self):
+        response = self.api_session.get(
+            '/folder',
+            params={
+                "expand": "navigation"
+            }
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(
+            [
+                {
+                    u'title': u'Home',
+                    u'url': u'http://localhost:55001/plone'
+                },
+                {
+                    u'title': u'Some Folder',
+                    u'url': u'http://localhost:55001/plone/folder'
+                }
+            ],
+            response.json()['@components']['navigation']['items']
+        )
+
+    def test_breadcrumbs_is_expandable(self):
+        response = self.api_session.get('/folder')
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            'breadcrumbs',
+            response.json().get('@components').keys()
+        )
+
+    def test_breadcrumbs_expanded(self):
+        response = self.api_session.get(
+            '/folder',
+            params={
+                "expand": "breadcrumbs"
+            }
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(
+            [
+                {
+                    u'title': u'Some Folder',
+                    u'url': u'http://localhost:55001/plone/folder'
+                }
+            ],
+            response.json()['@components']['breadcrumbs']['items']
+        )
+
+    def test_workflow_is_expandable(self):
+        response = self.api_session.get('/folder')
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(
+            'workflow',
+            response.json().get('@components').keys()
+        )
+
+    def test_workflow_expanded(self):
+        response = self.api_session.get(
+            '/folder',
+            params={
+                "expand": "workflow"
+            }
+        )
+
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(
+            u'http://localhost:55001/plone/folder/@workflow',
+            response.json().get('@components').get('workflow').get('@id')
+        )
+        self.assertEqual(
+            u'private',
+            response.json()['@components']['workflow']['history'][0]['review_state']  # noqa
+        )
+        self.assertEqual(
+            u'Private',
+            response.json()['@components']['workflow']['history'][0]['title']
+        )
+        self.assertEqual(
+            [
+                {
+                    u'@id': u'http://localhost:55001/plone/folder/@workflow/publish',  # noqa
+                    u'title': u'Publish'
+                },
+                {
+                    u'@id': u'http://localhost:55001/plone/folder/@workflow/submit',  # noqa
+                    u'title': u'Submit for publication'
+                }
+            ],
+            response.json()['@components']['workflow']['transitions']
+        )
diff --git a/src/plone/restapi/tests/test_services_breadcrumbs.py b/src/plone/restapi/tests/test_services_breadcrumbs.py
index 59dfc008..a21002c0 100644
--- a/src/plone/restapi/tests/test_services_breadcrumbs.py
+++ b/src/plone/restapi/tests/test_services_breadcrumbs.py
@@ -35,17 +35,23 @@ def setUp(self):
             title=u'A document')
         transaction.commit()
 
+    @unittest.skip('Planned for future release')
     def test_breadcrumbs(self):
         response = self.api_session.get('/folder/doc1/@breadcrumbs')
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
             response.json(),
-            [{
-                u'url': u'http://localhost:55001/plone/folder',
-                u'title': u'Some Folder'
-            }, {
-                u'url': u'http://localhost:55001/plone/folder/doc1',
-                u'title': u'A document'
-            }]
+            {
+                '@id': 'http://localhost:55001/plone/folder/doc1/@breadcrumbs',
+                'items': [
+                    {
+                        u'url': u'http://localhost:55001/plone/folder',
+                        u'title': u'Some Folder'
+                    }, {
+                        u'url': u'http://localhost:55001/plone/folder/doc1',
+                        u'title': u'A document'
+                    }
+                ]
+            }
         )
diff --git a/src/plone/restapi/tests/test_services_navigation.py b/src/plone/restapi/tests/test_services_navigation.py
index ad55a612..02a04f84 100644
--- a/src/plone/restapi/tests/test_services_navigation.py
+++ b/src/plone/restapi/tests/test_services_navigation.py
@@ -35,20 +35,24 @@ def setUp(self):
             title=u'A document')
         transaction.commit()
 
+    @unittest.skip('Planned for future release')
     def test_navigation(self):
         response = self.api_session.get('/folder/@navigation')
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
             response.json(),
-            [
-                {
-                    u'title': u'Home',
-                    u'url': u'http://localhost:55001/plone'
-                },
-                {
-                    u'title': u'Some Folder',
-                    u'url': u'http://localhost:55001/plone/folder'
-                }
-            ]
+            {
+                '@id': 'http://localhost:55001/plone/folder/@navigation',
+                'items': [
+                    {
+                        u'title': u'Home',
+                        u'url': u'http://localhost:55001/plone'
+                    },
+                    {
+                        u'title': u'Some Folder',
+                        u'url': u'http://localhost:55001/plone/folder'
+                    }
+                ]
+            }
         )
diff --git a/src/plone/restapi/tests/test_site_deserializer.py b/src/plone/restapi/tests/test_site_deserializer.py
new file mode 100644
index 00000000..4374a46b
--- /dev/null
+++ b/src/plone/restapi/tests/test_site_deserializer.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+from plone.restapi.interfaces import IDeserializeFromJson
+from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
+from plone.restapi.tests.mixin_ordering import OrderingMixin
+from zope.component import getMultiAdapter
+
+import unittest
+
+
+class TestDXContentDeserializer(unittest.TestCase, OrderingMixin):
+
+    layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+
+        # ordering setup
+        self.folder = self.portal
+
+        for x in range(1, 10):
+            self.folder.invokeFactory(
+                'Document',
+                id='doc' + str(x),
+                title='Test doc ' + str(x)
+            )
+
+    def deserialize(self, body='{}', validate_all=False, context=None):
+        context = context or self.portal
+        self.request['BODY'] = body
+        deserializer = getMultiAdapter((context, self.request),
+                                       IDeserializeFromJson)
+        return deserializer(validate_all=validate_all)
+
+    def test_set_layout(self):
+        current_layout = self.portal.getLayout()
+        self.assertNotEquals(current_layout, "my_new_layout")
+        self.deserialize(body='{"layout": "my_new_layout"}')
+        self.assertEquals('my_new_layout', self.portal.getLayout())
diff --git a/src/plone/restapi/tests/test_workflow.py b/src/plone/restapi/tests/test_workflow.py
index e4526d35..ed96886f 100644
--- a/src/plone/restapi/tests/test_workflow.py
+++ b/src/plone/restapi/tests/test_workflow.py
@@ -5,6 +5,7 @@
 from plone.app.testing import SITE_OWNER_NAME
 from plone.app.testing import SITE_OWNER_PASSWORD
 from plone.app.testing import login
+from plone.restapi.interfaces import ISerializeToJson
 from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
 from unittest import TestCase
 from zExceptions import NotFound
@@ -43,6 +44,22 @@ def test_workflow_info_includes_transitions(self):
         transitions = info['transitions']
         self.assertEqual(2, len(transitions))
 
+    def test_collapsed_workflow_info_in_content_serialization(self):
+        serializer = getMultiAdapter((self.doc1, self.request),
+                                     ISerializeToJson)
+        obj = serializer()
+        self.assertIn('workflow', obj['@components'])
+        self.assertIn('@id', obj['@components']['workflow'])
+
+    def test_expanded_workflow_info_in_content_serialization(self):
+        self.request.form.update({'expand': 'workflow'})
+        serializer = getMultiAdapter((self.doc1, self.request),
+                                     ISerializeToJson)
+        obj = serializer()
+        self.assertIn('workflow', obj['@components'])
+        self.assertIn('transitions', obj['@components']['workflow'])
+        self.assertIn('history', obj['@components']['workflow'])
+
 
 class TestWorkflowTransition(TestCase):
 
diff --git a/versions.cfg b/versions.cfg
index aa02abe0..ff9698f2 100644
--- a/versions.cfg
+++ b/versions.cfg
@@ -1,24 +1,29 @@
 [versions]
+
 # Build
 setuptools =
 zc.buildout =
 zc.recipe.egg = 2.0.3
 
-pytz = 2017.2
-
-zope.interface = 4.0.5
+# Code-analysis
+plone.recipe.codeanalysis = 2.2
 coverage = 3.7.1
 flake8 = 2.4.0
 flake8-coding = 1.2.2
-plone.recipe.codeanalysis = 2.2
 
+
+# Sphinx
 Sphinx = 1.3.1
 docutils = 0.12
 Pygments = 2.0
 sphinxcontrib-httpexample = 0.5.2
 
-zest.releaser = 6.7
-twine = 1.8.1
-requests = 2.11.1
+# Release
+zest.releaser = 6.12.2
+twine = 1.9.1
+requests = 2.18.1
 
+# Misc
 plone.testing = 4.3.0
+pytz = 2017.2
+zope.interface = 4.0.5



------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot