gatsby-source-sensenet
Source plugin for pulling data from sensenet into Gatsby websites.
sensenet is an open-source headless content management system (CMS) built mainly for developers and development companies. It is a content repository where you can store all your content and reach them through APIs. It is a solid base for your custom solutions, offering an enterprise-grade security and permission system, versioning, dynamic content types and even more.
An example of how to use this plugin is at sn-blog-gatsby
How to install
npm install gatsby-source-sensenet
Available options
You have to pass environment variables to the build process, so secrets and other secured data will not commited along the source code.
// In your gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-source-sensenet`,
options: {
host: '<YOUR REPOSITORY URL>',
path: '<RELATIVE PATH TO YOUR CONTENTS>',
oDataOptions: '<ODATA OPTIONS>'
accessToken: '<ACCESS TOKEN FOR AUTHENTICATION>'
level: '<NUMBER OF LEVELS TO READ CONTENTS>'
},
},
],
}
host
[string][required]
The url of your repository, e.g.: 'https://dev.demo.sensenet.com'
path
[string][optional] [default: '/Root/Content']
This is the root path of the container where your stuff are located in your repository. The default value is 'Root/Content' but if you don't want to load all the content from your repository you can reduce the number of content request by specifying a container (e.g.: folder, workspace or library) that contains the contents you need to build up your static site.
oDataOptions
[ODataParams][optional] [default:
{
"enableautofilters": false,
"enablelifespanfilter": false,
"inlinecount": "allpages",
"metadata": "no",
"select": ["DisplayName", "Description", "Icon"],
"top": 10000
}
]
Query options are query string parameters that the client may specify controling the amount and order of the data the service returns. In sensenet there're two types of query options available OData System Query Options and custom sensenet query options. The OData standard query options' names are prefixed with a "$" character, sensenet query options should be used without a prefix.
option | |
---|---|
$select | specifies that a response from the service should return a subset of properties |
$expand | allows you to identify related entries with a single URI such that a graph of entries could be retrieved with a single HTTP request (e.g. creator user or any other related content) |
$orderby | allows you to sort results by one or more properties, forward or reverse direction |
$top | identifies a subset selecting only the first N items of the set |
$skip | identifies a subset that is defined by seeking N entries into the collection and selecting only the remaining ones |
$filter | identifies a subset determined by selecting only the entries that satisfy the predicate expression specified by the query option |
$format | specifies that a response to the request MUST use the media type specified by the query option (Atom and xml formats are not implemented yet in sensenet) |
$inlinecount | controls the __count property that can be found in every collection response |
query | filter the result collection using sensenet Content Query |
metadata | controls the metadata content in output entities |
level
[number][optional]
The value of the level option should be a positive whole number (integer) to specify the number of levels from which you want to query content. If it is not specified, the plugin will read all levels until it finds a child content.
accessToken
[string | Function][required]
accessToken can be a string or a function as well, it is for authentication purposes. If you would like to generate access token programmatically you can use codeLogin function from sn-authentication-oidc-react package.
npm install @sensenet/authentication-oidc-react
Example:
// In your gatsby-config.js
const fetch = require('node-fetch')
const { codeLogin } = require('@sensenet/authentication-oidc-react')
const { configuration } = require('./configuration')
module.exports = {
plugins: [
{
resolve: `gatsby-source-sensenet`,
options: {
host: '<YOUR REPOSITORY URL>',
path: '<RELATIVE PATH TO YOUR CONTENTS>',
oDataOptions: '<ODATA OPTIONS>'
accessToken: async () => {
const authData = await codeLogin({ ...configuration, fetchMethod: fetch })
return authData.access_token
}
},
},
],
}
You can overwrite fetch method according to your own needs. In this particular example we are using node-fetch, so you have to install it first:
npm install node-fetch
For using environmental variables you should install dotenv:
npm install dotenv
Configuration should be something like this:
// Create a new file in root folder with the name configuration.js
require('dotenv').config()
exports.repositoryUrl = '<YOUR REPOSITORY URL>'
exports.configuration = {
clientId: process.env.GATSBY_REACT_APP_CLIENT_ID || '',
clientSecret: process.env.GATSBY_REACT_APP_CLIENT_SECRET || '',
identityServerUrl: '<YOUR IDENTITY SERVER URL>',
}
GATSBY_REACT_APP_CLIENT_ID and GATSBY_REACT_APP_CLIENT_SECRET environmental variables should be defined.
You can easily store them in .env files by doing the following:
// In your .env file
GATSBY_REACT_APP_CLIENT_ID=<YOUR CLIENT ID>
GATSBY_REACT_APP_CLIENT_SECRET=<YOUR SECRET>
Where can you get the missing information?
There are two ways to get your client_id and client_secret: You can find them on your snaas user profile or on the admin-ui logged-in to the repository as well. Also here can be found the repository url and url of the identity server.
When do I use this plugin?
sensenet is the single hub for all your content packed with enterprise grade features. In sensenet everything is a content (blog posts, files, users, roles, comments, etc.) delivered the same way through the API, making it super easy to work with any type of data. With the flexibility and power of sensenet and Gatsby you can build any kind of app or website you need. For more details check this guide.
Examples of usage
// In your gatsby-config.js
const fetch = require('node-fetch')
const { codeLogin } = require('@sensenet/authentication-oidc-react')
const { configuration } = require('./configuration')
module.exports = {
plugins: [
{
resolve: `gatsby-source-sensenet`,
options: {
host: 'https://dev.demo.sensenet.com',
path: '/Root/Content/SampleWorkspace/Blog',
oDataOptions: {
select: 'all',
expand: ['LeadImage'],
metadata: 'no',
},
accessToken: async () => {
const authData = await codeLogin({ ...configuration, fetchMethod: fetch })
return authData.access_token
},
},
},
],
}
How to query for data
gatsby-source-sensenet creates gatsby nodes from all the contents returned by a query. The internal.type of all nodes consists of the "sensenet" prefix and the original type of the content. If you have a "BlogPost" content type then all the content created with this type will be presented as gatsby nodes whose internal.type will be: "sensenetBlogPost".
To query for all nodes with a specific type e.g.: sensenetBlogPost:
query MyQuery {
allSensenetBlogPost {
edges {
node {
Name
DisplayName
}
}
}
To query for a specific blogpost with the name '2021-02-23-how-we-post':
query MyQuery {
sensenetBlogPost(Name: { eq: "2021-02-23-how-we-post" }) {
DisplayName
Name
}
}
gatsby-source-sensenet is also capable to build a tree of content. The root comes from the path param you've passed to your gatsby-config or the default path (/Root/Content). If your collection has multiple types (e.g.: Folder and BlogPost) you can query both for them, and if you would like to go deeper in the tree (selecting the Folder's children which contains Images) you can create a nested childrenSensenet[Type] into another childrenSensenet[Type].
query MyQuery {
sensenetBlog {
childrenSensenetBlogPost {
Name
DisplayName
}
childrenSensenetFolder {
Name
DisplayName
childrenSensenetImage {
Name
DisplayName
}
}
}
}
Create pages
If you are building pages from contents you may have to create custom URL for your blogpost contents. E.g. if you have a content with the markdown body at /Root/Content/SampleWorkspace/Blog/2021-02-23-how-we-post, you might want to turn that path into example.com/2021-02-23-how-we-post/.
Following example shows you how you can achieve this:
When "sensenetBlogPost" node is created you should create a new field with the name of "slug" on the node:
//In your gatsby-node.js
exports.onCreateNode = async ({ node, actions: { createNodeField } }) => {
if (node.internal.type === 'sensenetBlogPost') {
createNodeField({
node,
name: 'slug',
value: node.Name || '',
})
}
}
Then iterate through all sensenetBlogPost nodes and create a page for all using a template.
//In your gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const allSensenetBlogPost = await graphql(`
{
allSensenetBlogPost(limit: 1000) {
edges {
node {
fields {
slug
}
}
}
}
}
`)
if (allSensenetBlogPost.errors) {
console.error(allSensenetBlogPost.errors)
throw new Error(allSensenetBlogPost.errors)
}
const blogPostTemplate = path.resolve('<YOUR TEMPLATE>')
allSensenetBlogPost.data.allSensenetBlogPost.edges.forEach(({ node }) => {
const { slug } = node.fields
createPage({
path: `/${slug}/`,
component: blogPostTemplate,
context: {
slug,
},
})
})
}
Complex types
Image
Images in sensenet could be handled as references. In this case the "LeadImage" field of a Blogpost will be something like this in the response:
"LeadImage": {
"__deferred": {
"uri": "/odata.svc/Root/Content/SampleWorkspace/Blog('2021-01-13-docviewer-updates')/LeadImage"
}
},
To have their actual data, reference fields have to be expanded. You can define the fields you would like expand in the gatsby-config by listing the fieldNames in an array, eg.:
oDataOptions: {expand: ['LeadImage']},
Then you can create a remote file node from the returned data:
exports.onCreateNode = async ({ node, actions: { createNode }, createNodeId, getCache }) => {
if (node.internal.type === 'sensenetBlogPost') {
const leadImageNode = await createRemoteFileNode({
url: `<YOUR REPOSITORY URL>${node.LeadImage.Path}`,
parentNodeId: node.Id.toString(),
createNode,
createNodeId,
getCache,
})
if (leadImageNode) {
node.leadImage___NODE = leadImageNode.id ///connect to blog post node
}
}
}
If you want to request content from a sensenet repository, they should be public otherwise you have to provide a httpHeaders: { Authorization: Bearer <yourAccessToken>
} attribute to createRemoteFileNode function
You can query for the value of the leadImage field on all blogpost:
query MyQuery {
allSensenetBlogPost {
edges {
node {
leadImage {
childImageSharp {
gatsbyImageData(layout: FIXED)
}
}
}
}
}
}
For rendering these images you can use gatsby-plugin-image.
Mdx
If your content contains a field with value in mdx format you can process it using gatsby-plugin-mdx.
First, you have to create a node from the mardkdown itself and save it with internal.mediaType ='text/markdown'
.
//In your gatsby-node.js
exports.onCreateNode = async ({ node, actions: { createNode } }) => {
if (node.internal.type === 'sensenetBlogPost') {
const bodyMdxNode = {
id: `${node.Id.toString()}-MarkdownBody`,
parent: node.Id.toString(),
internal: {
type: `${node.internal.type}MarkdownBody`,
mediaType: 'text/markdown',
content: node.Body,
contentDigest: node.Body,
},
}
createNode(bodyMdxNode)
if (bodyMdxNode) {
node.markdownBody___NODE = bodyMdxNode.id //connect to blog post node
}
}
}
This node is now connected to the blogpost, so you can query for it this way:
query MyQuery {
allSensenetBlogPost {
edges {
node {
markdownBody {
childMdx {
body
}
}
}
}
}
}
To render the markdown use <MDXRenderer><YOUR MARKDOWN BODY></MDXRenderer>
Richtext
RichText is a field type that enables authors to create rich text content. By default sensenet will provide you the field as HTML format but there is also possible to get the response as JSON. For getting both you have to add richtexteditor: 'all'
to the oDataOptions in gatsby-config. The API response contains a text part (which is the HTML code) and an editor returned as a JSON array of nodes that follows the format of an abstract syntax tree.
{
"type":"doc",
"content":[
{
"type":"paragraph",
"attrs":{
"textAlign":"center"
},
"content":[
{
"type":"text",
"marks":[
{
"type":"bold"
}
],
"text":"Lorem ipsum dolor sit amet ..."
}
]
}
]
}
JSON can be rendered with renderHtml method from @sensenet/editor-react package.
How to contribute
Before you start working on this package please check the contribution guide first.