Recommended way to expose some page data via JSON API

Hello,

I’m currently trying to expose some page or some page including it’s sub-pages via a JSON API. I have found the following page: Rendering special formats - Rendering - Manual - Neos CMS - Neos Docs or this blog article: Building a Rich Content API with Neos for your React App and this topic: Neos as Headless CMS - #23 by pawankct91.

The first two articles seem to be outdated respective I’m not sure if this is still the way to go. The Headless topic link proposes to use a GraphQL package.

I would like to keep it simple as possible and just write some Fusion/AFX script which would output the current node data, e.g. at /site/my-page called with /de/my-page.json as JSON output.
Is there any good example or tutorial or advices on how to do that with Fusion/AFX? For some pointes I would be glad, thanks!

You can start by adding a new condition to the Fusion root case of Neos, where it decides what to render.

Similar to how the Neos.SEO package registers its sitemap rendering.

And again similar to the sitemap you define a custom renderer with the matching Content-Type that retrieves data from your current document. At the end you have to add a process that will JSON stringify your data.

Does that help?

1 Like

Thanks @sebobo this helps. I’m still quite a Newbie that’s why the question. I will check the Neos.SEO package and try it out. If you still have some “Newbie-Tutorial” according this topic would be great. Else I could add then a new on to the offical Neos docs if I achieve it :).

Why you will use fusion? I think it is better to parse your JSON with PHP on demand.

For me it takes some time to understand but your have to create an action controller and link it in your Routes.yaml.

Do you need an example?
I’m currently on your and not able to poste one before this evening.

I think Fusion is a valid option in many cases. You don’t need to care about routes, policy, etc. and it’s easy to extend for integrators.

For more control or performance PHP is of course the better option.

Thanks @Kollos for your hint, this was also an idea. But we really want to use Neos strength of its content-hierarichal-centric approach, so instead of rendering the page’s content as html we simply want to render it in json. That’s why we think using Fusion/AFX could be a straight-forward fit for this use-case.

see neos/DefaultFusion.fusion at master · neos/neos (github.com)

The root matcher used to start rendering in Neos
The default is to use a render path of “page”, unless the requested format is not “html”
in which case the format string will be used as the render path (with dots replaced by slashes)

so by defining a fusion path /json it should already work right?.. but i would extend the root like:

root.json {
    condition = ${request.format == 'json'}
    renderer = "json!"
}

Im not sure but i think you still need to configure a new route with ‘@format’: json - The question would be how to handle the root node - does this work: https://my-neos.de/.json ?

also have a look at the Routing docs: Routing - Features - Neos CMS - Neos Docs

btw what do you mean exactly?

1 Like

Thanks @Marc !

For example I would like to request /de/some-page/my-page.json and this should render then the data (properties and content child-nodes) of this requested node.

So I have to correct myself, a route is needed, I thought that sometime in the past it wasn’t but maybe I have just bad memory.

So here is the route:

- name:  'Json frontend'
  uriPattern: '{node}.json'
  defaults:
    '@package':    'Neos.Neos'
    '@controller': 'Frontend\Node'
    '@action':     'show'
    '@format':     'json'
  routeParts:
    'node':
      handler: 'Neos\Neos\Routing\FrontendNodeRoutePartHandlerInterface'
  appendExceedingArguments: true

And the Fusion code (no adjustment to the root condition is needed):

json = Neos.Fusion:Http.Message {
    httpResponseHead {
        headers.Content-Type = 'application/json'
    }

    value = Neos.Fusion:Component {
        renderer = Neos.Fusion:DataStructure {
            id = ${node.identifier}
        }
        @process.stringify = ${Json.stringify(value)}
    }
}
2 Likes

I could manage to output a first json :slight_smile: thanks to @sebobo tipps.
I still needed to add the following to my Site Settings.yaml:

    mvc:
      routes:
        'My.Site':
          position: 'before Neos.Neos'
        'Neos.Neos':
          variables:
            # We prefer URLs without the ".html" suffix
            defaultUriSuffix: ''

else I could only render /.json or /de.json, but NOT /de/some-page/my-page.json. With the config above I could also render a json view for /de/some-page/my-page.json.

@sebobo I don’t understand yet, why the Fusion code is called by calling some-page.json? What is the connnection between this Fusion code “json = …” and the Routes definition? Is it because we define this “json = …” property that Neos knows to render it as json?

Next I try to get the right json data out of the ContentCollection nodes, etc.
Currently my challenge is that I would like to render my ContentCollection node “main” but not as html string, but as structured data. So I’m thinking of somehow to define for each content element (e.g. Button, Headline, Text&Image, Sliders, etc.) some “Json Fusion view” and so I could iterate over these content elements with their Json views. Maybe @sebobo you have also some idea how to solve this :slight_smile: .

Similar topic: How can render single content node with fusion

1 Like

You can override the Neos.Neos:ContentCase prototype inside your json renderer to not render the usual prototype but some custom type.

The json path is called by the root case in Neos.Neos. There is a format case that will check if there is a json path in your case and then use it.

1 Like

neos/DefaultFusion.fusion at master · neos/neos (github.com)

root = Neos.Fusion:Case {

  [...]

  format {
    @position = 'end 9997'
    condition = ${request.format != 'html'}
    renderPath = ${'/' + String.replace(request.format, '.', '/')}
    # will be evaluated to: renderPath = "/json"
  }

  [...]
}


json = "hi"
# this path will be rendered by renderPath = "/json"
1 Like

im trying to implement something similar … my prototype so far:

prototype(Mh:ContentCase) < prototype(Neos.Fusion:Case) {
    json {
        condition = false
        // why not directly a renderPath? see:
        // https://github.com/neos/neos-development-collection/issues/3388
        renderer = Neos.Fusion:Renderer {
            renderPath = ${"/<" + "Mh:Heading" + ">/data"}
            @process.json = ${Json.stringify(value)}
        }
    }
    default {
        condition = true
        type = ${"Mh:Heading"}
    }
}

prototype(Mh:Heading) < prototype(Neos.Fusion:Component) {
    
    data = Neos.Fusion:DataStructure {
        text = ${q(node).property("title")}
        class = "big"
    }
    
    renderer = afx`
        <h1 class={props.data.class}>{props.data.text}</h1>
    `
}

my idea is that the “Mh:Heading” will be solved via q(node).property('_nodeType.name') like in the current Neos.Neos:ContentCase

the json.conditions there would be condition = ${request.format == 'json'}

but my doing looks somehow hackish ^^

1 Like

I would keep the json and normal rendering apart. For example by checking for a related data only prototype:

renderJson {
		condition = Neos.Fusion:CanRender {
			type = ${q(documentNode).property('_nodeType.name') + '.Json'}
		}
		type = ${q(documentNode).property('_nodeType.name') + '.Json'}
	}
}

Then you can also define a fallback type for prototypes without JSON version.

1 Like

Thanks @Marc and @sebobo for your great inputs!

I could manage now a solution which I can use for my use case.

I have created a Git repo for this example based on the cool Code.Q Site Skeleton :slight_smile: by @rolandschuetz & Co.!

Check it out here: GitHub - Comvation/NeosExampleJsonApi: A Neos example for implementing a Content Repository based JSON API with Fusion scripts.

Because I’m still quite a Neos and Fusion Newbie I’m very open for improvements.

Here the rough steps I needed to do:

  1. Add JSON route in Routes.yaml (needed to create this file)
  2. Update Settings.yaml so that the new route gets also executed
  3. Add a new Fusion Case for rendering JSON using @sebobo tipp for using custom types
  4. Add a “JSON view” for the pages and/or content elements: page json example here, content element json example here
  5. Finish :slight_smile:

With this approach it is very simple to add some JSON view for some page or content element. And if there is no json view for some content element it simply renders a DefaultView.

I’m thinking of adding some kind of tutorial here: Tutorials - Neos CMS - Neos Docs

What do you think @sebobo ? Would this make sense?

1 Like

Awesome, and yes, a tutorial on neos docs would be great!

Ping me in Slack if you need a backend access if you don’t have one yet to write the tutorial.

i know that most times an explicit json view is the best, but i couldnt resist creating an automatic way ^^

you can put the following fusion code in the case after it checks for ${q(documentNode).property('_nodeType.name') + '.Json'}

if no .Json prototype is specified and the html renderer prototype uses a Neos.Fusion:Component or any inherited version (Neos.Neos:ContentComponent) this code will use the props of your html component and render it as an array:


# generate an array by using all the available props from your normal html rendering.
generatedJsonView {

  # get the php implementation class, that was specified via:
  # prototype(Neos.Fusion:Component).@class = 'Neos\\Fusion\\FusionObjects\\ComponentImplementation'

  fusionObjectPhpImplementation = Neos.Fusion:Renderer {
    renderPath = ${"/element<" + q(node).property('_nodeType.name') + ">/__meta/class"}
  }

  # check if the Fusion object of the current node uses a Neos.Fusion:Component (or something inherited like Neos.Neos:ContentComponent)
  # if so, we can rely that there is a renderer Path and props in the context
  condition = ${this.fusionObjectPhpImplementation == 'Neos\\Fusion\\FusionObjects\\ComponentImplementation' ? true : false}

  type = ${q(node).property('_nodeType.name')}

  # element is referring to the prototype from 'type'
  element {
    renderer >
    renderer = Neos.Fusion:DataStructure {
      nodeId = ${node.identifier}
      nodeType = ${q(node).property('_nodeType.name')}
      @apply.props = ${props}
    }
  }
}

a little demo ; )

1 Like

ahh nice :slight_smile: :+1: