diff --git a/CHANGES.rst b/CHANGES.rst
index 5b7abd84e1..43dd2c3041 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -6,6 +6,10 @@ Changelog
New Features:
+- Add navigation and breadcrumbs as top-level services. Deprecate navigation
+ and breadcrumbs as components.
+ [timo]
+
- Add support for setting/modifying 'layout' on DX and AT content endpoints.
[jaroel]
diff --git a/docs/source/breadcrumbs.rst b/docs/source/breadcrumbs.rst
new file mode 100644
index 0000000000..f377ff3087
--- /dev/null
+++ b/docs/source/breadcrumbs.rst
@@ -0,0 +1,12 @@
+Breadcrumbs
+===========
+
+Getting the breadcrumbs for the current page:
+
+.. http:example:: curl httpie python-requests
+ :request: _json/breadcrumbs.req
+
+Example response:
+
+.. literalinclude:: _json/breadcrumbs.resp
+ :language: http
diff --git a/docs/source/components.rst b/docs/source/components.rst
index f052d35dff..49db73734e 100644
--- a/docs/source/components.rst
+++ b/docs/source/components.rst
@@ -1,8 +1,13 @@
Components
==========
+.. warning::
+ The @components endpoint is deprecated and will be removed in plone.restapi
+ 1.0b1. :doc:`breadcrumbs` and :doc:`navigation` are now top-level endpoints.
+
How to get pages components (i.e. everything but the main content), like breadcrumbs, navigations, actions, etc.
+
Breadcrumbs
-----------
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 66c5b74261..761a46d3d3 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -30,6 +30,8 @@ Contents
types
users
components
+ breadcrumbs
+ navigation
serialization
searching
vocabularies
diff --git a/docs/source/navigation.rst b/docs/source/navigation.rst
new file mode 100644
index 0000000000..8027e2d640
--- /dev/null
+++ b/docs/source/navigation.rst
@@ -0,0 +1,12 @@
+Navigation
+==========
+
+Getting the top navigation items:
+
+.. http:example:: curl httpie python-requests
+ :request: _json/navigation.req
+
+Example response:
+
+.. literalinclude:: _json/navigation.resp
+ :language: http
diff --git a/src/plone/restapi/services/breadcrumbs/__init__.py b/src/plone/restapi/services/breadcrumbs/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/plone/restapi/services/breadcrumbs/configure.zcml b/src/plone/restapi/services/breadcrumbs/configure.zcml
new file mode 100644
index 0000000000..0c39c889ca
--- /dev/null
+++ b/src/plone/restapi/services/breadcrumbs/configure.zcml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/plone/restapi/services/breadcrumbs/get.py b/src/plone/restapi/services/breadcrumbs/get.py
new file mode 100644
index 0000000000..b8d9b97aac
--- /dev/null
+++ b/src/plone/restapi/services/breadcrumbs/get.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+from plone.restapi.services import Service
+from zope.component import getMultiAdapter
+
+
+class BreadcrumbsGet(Service):
+
+ def reply(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
diff --git a/src/plone/restapi/services/configure.zcml b/src/plone/restapi/services/configure.zcml
index a8c77c5cea..4c495bba27 100644
--- a/src/plone/restapi/services/configure.zcml
+++ b/src/plone/restapi/services/configure.zcml
@@ -2,10 +2,12 @@
xmlns="http://namespaces.zope.org/zope">
+
+
diff --git a/src/plone/restapi/services/navigation/__init__.py b/src/plone/restapi/services/navigation/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/plone/restapi/services/navigation/configure.zcml b/src/plone/restapi/services/navigation/configure.zcml
new file mode 100644
index 0000000000..05e200e434
--- /dev/null
+++ b/src/plone/restapi/services/navigation/configure.zcml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/plone/restapi/services/navigation/get.py b/src/plone/restapi/services/navigation/get.py
new file mode 100644
index 0000000000..a2bf05047e
--- /dev/null
+++ b/src/plone/restapi/services/navigation/get.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+from plone.restapi.services import Service
+from zope.component import getMultiAdapter
+
+
+class NavigationGet(Service):
+
+ def reply(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
diff --git a/src/plone/restapi/tests/test_services_breadcrumbs.py b/src/plone/restapi/tests/test_services_breadcrumbs.py
new file mode 100644
index 0000000000..59dfc00842
--- /dev/null
+++ b/src/plone/restapi/tests/test_services_breadcrumbs.py
@@ -0,0 +1,51 @@
+# -*- 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
+
+import transaction
+import unittest
+
+
+class TestServicesBreadcrumbs(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')
+ createContentInContainer(
+ self.folder, u'Document',
+ id=u'doc1',
+ title=u'A document')
+ transaction.commit()
+
+ 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'
+ }]
+ )
diff --git a/src/plone/restapi/tests/test_services_navigation.py b/src/plone/restapi/tests/test_services_navigation.py
new file mode 100644
index 0000000000..ad55a612ed
--- /dev/null
+++ b/src/plone/restapi/tests/test_services_navigation.py
@@ -0,0 +1,54 @@
+# -*- 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
+
+import transaction
+import unittest
+
+
+class TestServicesNavigation(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')
+ createContentInContainer(
+ self.folder, u'Document',
+ id=u'doc1',
+ title=u'A document')
+ transaction.commit()
+
+ 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'
+ }
+ ]
+ )