Headless or coupled
v0.0.1
A Mishka CMS site can draw its own pages, serve its content as JSON for another frontend, or do both. Each site picks one of three modes. This page explains the modes, what each one answers, and how a request finds its site. Read it before you decide whether a website's frontend is Mishka CMS itself or a separate app.
The three modes
Every site has a Mode on the Sites screen, under Runtime in the sidebar. Only a master can change it.
| Mode | Web pages | JSON:API | Typical use |
|---|---|---|---|
| Hybrid | Served | Served | The default. The CMS draws the pages, and other apps can still read and write through the API. |
| Phoenix Only | Served | Refused | A website that needs no API. |
| API Only | Refused | Served | A website whose frontend is a separate app. |
A Phoenix Only site answers an API request with status 403 and the code api_access_denied, and it sends no CORS headers. An API Only site answers a web visit with a 403 page that reads Web Access Not Allowed.
How the coupled side works
The host's router ends with one catch-all route. It sends every path that no other route claims to a single LiveView, MishkaCmsWeb.RuntimeLive. For each request, the CMS:
- finds the site from the host name of the request;
- finds the page whose path matches the address and the site's language settings;
- renders the page's compiled module inside its layout.
Pages, layouts, components, error pages, stylesheets and JS hooks are rows in the database. Publishing a change compiles it while the app runs, so a coupled site changes without a deploy. The sign-in pages are runtime pages as well. How a page reaches visitors follows one page from the editor to the browser.
How the headless side works
The JSON:API lives under /api/json. Its route families are:
Path under /api/json | Who calls it |
|---|---|
/v1/user/… | Readers of a site. The Blog extension serves its published posts at /v1/user/mishka-blog/post/published with no token. |
/v1/auth/user/… | Anyone signing in, registering or asking who they are. |
/v1/tenant/admin/… | The admins of one site. Pages, layouts, components, media and redirects are under /v1/tenant/admin/runtime/…. |
/v1/master/admin/… | Masters, across sites. |
- The site. Each request is scoped to the site its
Hostheader names. An unknown host answers 404 with the codedomain_not_found, and a switched-off site answers 503 withwebsite_unavailable. A master calling the master site's address can sendx-no-tenant: trueto work across sites. - Signing in.
POST /api/json/v1/auth/user/sign-inreturns a token astokenin the response'smeta. Send it asAuthorization: Bearer <token>on later requests. - Permissions. Admin routes check names that start with
admin:api:. A name such asadmin:dashboard:*opens nothing here. See How permissions work. - Browsers on other origins. The CMS sends CORS headers when the request's origin has the site's own host, the site's Frontend Domain, or a host listed under Allowed Origins. It compares hosts and ignores ports.
- The spec.
/api/json/open_apiand/api/json/swaggeruilist every route, filter and field. They open for an account that holds anything underadmin:api. - Maintenance. While a site is offline, its API answers only API admins, masters who hold an
admin:api:name included. Sign-in stays open, so they can still get a token.
Example: a separate frontend for a blog
A team runs its marketing site as its own app at app.example.com and keeps the posts in Mishka CMS at cms.example.com.
- A master adds a site with the host
cms.example.comon the Sites screen, sets Mode to API Only and Frontend Domain toapp.example.com. To allow another origin, such as a staging copy, click Add Origin under Allowed Origins. - The master sets the Blog extension up on that site with the Site list on the blog dashboard, then writes the posts and picks that site in each post's Site field.
- The app asks for the published posts:
curl https://cms.example.com/api/json/v1/user/mishka-blog/post/published \
-H "Accept: application/vnd.api+json"The Host header names cms.example.com, so the request runs against that site. A browser on app.example.com gets the CORS headers it needs, and an unlisted origin gets none.