{"id":13423876,"url":"https://github.com/janit/decoupled-cms-nextjs-graphql","last_synced_at":"2025-04-29T11:32:23.724Z","repository":{"id":69358358,"uuid":"103824755","full_name":"janit/decoupled-cms-nextjs-graphql","owner":"janit","description":"A working example of a decoupled implementation using Next.js and GraphQL with the eZ Platform CMS","archived":false,"fork":false,"pushed_at":"2018-12-21T08:56:12.000Z","size":254,"stargazers_count":30,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-26T04:25:32.135Z","etag":null,"topics":["cms","graphql","javascript","nextjs"],"latest_commit_sha":null,"homepage":"https://janit.iki.fi/cms-graphql-nextjs","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/janit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2017-09-17T11:49:19.000Z","updated_at":"2024-08-27T12:31:07.000Z","dependencies_parsed_at":"2023-02-22T01:30:31.207Z","dependency_job_id":null,"html_url":"https://github.com/janit/decoupled-cms-nextjs-graphql","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janit%2Fdecoupled-cms-nextjs-graphql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janit%2Fdecoupled-cms-nextjs-graphql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janit%2Fdecoupled-cms-nextjs-graphql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/janit%2Fdecoupled-cms-nextjs-graphql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/janit","download_url":"https://codeload.github.com/janit/decoupled-cms-nextjs-graphql/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251494094,"owners_count":21598233,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cms","graphql","javascript","nextjs"],"created_at":"2024-07-31T00:00:44.237Z","updated_at":"2025-04-29T11:32:19.366Z","avatar_url":"https://github.com/janit.png","language":"JavaScript","funding_links":[],"categories":["Apps","JavaScript"],"sub_categories":[],"readme":"\n# Decoupled CMS example with GraphQL and Next.js\n\nThis repository contains the source code for the sample application from the talk \"Easy decoupled sitebuilding with \u003ca href=\"https://graphql.org\"\u003eGraphQL\u003c/a\u003e and \u003ca href=\"https://github.com/zeit/next.js/\"\u003eNext.js\u003c/a\u003e\" held in September 2017 at Drupalcon Vienna and Helsinki.js Meetup.\n\nThe application is an example of a front end implementation decoupled from the CMS backend used for content storage only. This example uses a hosted \u003ca href=\"http://ezplatform.com\"\u003eeZ Platform\u003c/a\u003e instance with GraphQL enabled using the \u003ca href=\"https://github.com/bdunogier/ezplatform-graphql-bundle\"\u003eGraphQL Bundle\u003c/a\u003e. This API is consumed by a Next.js frontend that uses React.js for templating and handles Server Side Rendering (SSR), etc. boilerplate.\n\nThe application is online at https://react.nu/ slides for the presentation are available online here: https://janit.iki.fi/cms-graphql-nextjs\n\n\n## Installation\n\nThe application can be ran either using NPM scripts in development and production mode, as per documentation of Next.js.\n\nIf you've got a recent Node.js and NPM versions installed, you should be able to do this for development mode:\n\n```\n$ npm i\n$ npm run dev\n```\n\nAfter that the app is available in http://localhost:3000/ . The first click is server generated, but subsequent pageloads are done by the browser using the GraphQL.\n\nFor production mode you'll need to perform a build and then serve:\n\n```\n$ npm i\n$ npm run build\n$ npm run start\n```\n### Docker deployment\n\nAlternatively you can use Docker to host the application. Once you have Docker installed, perform the build from the Dockerfile and then run the image:\n\n```\ndocker build -t react_rauma .\ndocker run -p 3000:3000 -d --name=react_rauma --restart=always react_rauma\n```\n\nThis will make the container run and restart the upon crash or a reboot. The app is again running in port 3000 on localhost.\n\n## Application description\n\n### Simple static page\n\nThe application uses a standard Next.js structure, with entrypoints in the pages-directory.\n\nThe most simple example is the about page, which is a stateless React component:\n\n```jsx\nimport Link from 'next/link'\nimport Head from 'next/head'\nimport Layout from '../components/Layout'\n\nexport default () =\u003e (\n  \u003cLayout\u003e\n    \u003cHead\u003e\n      \u003ctitle\u003eTietoa maailman suurimmasta suomenkielisestä React.js-konferenssista\u003c/title\u003e\n    \u003c/Head\u003e\n    \u003cmain\u003e\n      \u003ch1\u003eMaailman suurin suomalainen React.js-konferenssi\u003c/h1\u003e\n      \u003cp\u003eOnks tää joku vitsi?! No tavallaan.\u003c/p\u003e\n    \u003c/main\u003e\n    \u003caside\u003e\n        \u003cLink href=\"/\"\u003e\u003ca\u003eEtusivulle\u003c/a\u003e\u003c/Link\u003e\n    \u003c/aside\u003e\n  \u003c/Layout\u003e\n);\n```\n\nThe above file in `pages/about.js` automatically maps to the address http://localhost:3000/about\n\nNote the `Link` component which can be used to provide navigation from page to page in your application.\n\n## Serving the front page from eZ Platform GraphQL API\n\nThe eZ Platform repository has a tree structure of content, which we will get the parent page and some properties from the child location \"activity\" objects for navigation with a single GraphQL query:\n\n - Content\n    - frontpage\n        - fields\n            - title\n            - body\n            - main_image\n    - activities\n        - activity 1\n            - id\n            - name\n        - activity 2\n            - id\n            - name\n        - activity 3\n            - id\n            - name\n        - ...\n\n\nTo  get content from the eZ Platform repository using the GraphQL API we need a class that has the `getInitialProps` method that will perform a GraphQL query (in pages/index.js).\n\nThe GraphQL query in the call is using eZ Platform content repository Public API via the GraphQL Bundle. The `rootLocationId` is a configuration value set in `.env`:\n\n```jsx\nstatic async getInitialProps() {\nlet query = `\n\n    {\n        frontpage: location(id: ${rootLocationId}) {\n            content {\n            fields(identifier: [\"title\", \"body\"]) {\n                fieldDefIdentifier,\n                value {\n                ... on TextLineFieldValue {\n                    text\n                    }\n                ... on RichTextFieldValue {\n                    html5\n                }\n                ... on ImageFieldValue {\n                    uri\n                }\n                }\n            }\n            }\n        }\n        activities: locationChildren(id: ${rootLocationId}) {\n            content {\n                id\n                name\n            }\n        }\n        }\n\n    `;\n\nlet variables = {\n    query: \"Search Query\",\n};\n\nreturn await client.query(query, variables);\n}\n```\n\n\n\n\nThe result is set as `props` to the component and is rendered in the render function:\n\n```jsx\nrender() {\n    let fields = simplifyFields(this.props.data.frontpage.content.fields);\n\n    return (\n        \u003cLayout\u003e\n        \u003cHead\u003e\n            \u003ctitle\u003e{fields.title}\u003c/title\u003e\n        \u003c/Head\u003e\n        \u003cmain\u003e\n            \u003ch1\u003e{fields.title}\u003c/h1\u003e\n            {Parser(fields.body)}\n        \u003c/main\u003e\n        \u003caside\u003e\n            \u003cNavigation title=\"Ohjelmaa\" items={this.props.data.activities} /\u003e\n        \u003c/aside\u003e\n        \u003c/Layout\u003e\n    );\n}\n```\n\nNote the use of some helpers to simplify the field structure to ease use, the Parse function to enable HTML output and the Navigation component that could be reused from page to page.\n\n## Passing parameters to a view\n\nTo pass parameters to the script to enable URL specific content for URLs, we will need to run a custom server script and pass URL params to it.\n\nThe script is based on the example from the Next.js repository. It only matches URLs with the pattern `/activity/:id` and can be seen below:\n\n```jsx\nconst match = route(\"/activity/:id\");\n\napp.prepare().then(() =\u003e {\n  createServer((req, res) =\u003e {\n    const { pathname, query } = parse(req.url, true);\n    const params = match(pathname);\n    if (params === false) {\n      handle(req, res);\n      return;\n    }\n    app.render(req, res, \"/activity\", Object.assign(params, query));\n  }).listen(port, err =\u003e {\n    if (err) throw err;\n    console.log(`\u003e Ready on http://localhost:${port}`);\n  });\n});\n```\n\nThis script defined to be used via NPM scripts in package JSON when serving the app:\n\n```jsx\n  \"scripts\": {\n    \"dev\": \"node server.js\",\n    \"build\": \"next build\",\n    \"start\": \"NODE_ENV=production node server.js\"\n  },\n```\n\nNote that you could use Express, Koa or other Node.js frameworks to process request routing, etc. with Next.js.\n\nNow that we've got access to URL variables, let's take a look at `pages/activity.js` which is our entrypoint to the view displaying individual activities.\n\nFirst off the `getInitialProps` method contains a GraphQL query, but it's using the dynamic variable via the ES6 template literal format (${id}):\n\n```jsx\nstatic async getInitialProps({ query: { id } }) {\nlet query = `\n\n    {\n    content(id: ${id}) {\n        name\n        fieldDefIdentifier,\n        fields(identifier: [\"title\", \"body\", \"main_image\"]) {\n        value {\n            ... on TextLineFieldValue {\n            text\n            }\n            ... on RichTextFieldValue {\n            html5\n            }\n            ... on ImageFieldValue {\n            uri\n            }\n        }\n        }\n    }\n    }\n\n    `;\n\nlet variables = {};\n\nreturn await client.query(query, variables);\n}\n\n```\n\nSecondly, the `render` method is familiar to the one from `pages/index.js`:\n\n```jsx\nrender() {\nlet fields = simplifyFields(this.props.data.content.fields);\n\nreturn (\n    \u003cLayout\u003e\n    \u003cHead\u003e\n        \u003ctitle\u003e{fields.title} - React Rauma\u003c/title\u003e\n    \u003c/Head\u003e\n    \u003cmain\u003e\n        \u003ch1\u003e{fields.title}\u003c/h1\u003e\n        {Parser(fields.body)}\n        {fields.main_image ? (\n        \u003cp\u003e\n            \u003cimg src={apiRoot + fields.main_image} /\u003e\n        \u003c/p\u003e\n        ) : null}\n    \u003c/main\u003e\n    \u003caside\u003e\n        \u003cnav\u003e\n        \u003cp\u003e\n            \u003cLink href=\"/\"\u003e\n            \u003ca\u003eEtusivulle\u003c/a\u003e\n            \u003c/Link\u003e\n        \u003c/p\u003e\n        \u003c/nav\u003e\n    \u003c/aside\u003e\n    \u003c/Layout\u003e\n);\n```\n\n### Linking to dynamic pages\n\nIn the navigation component we are linking to individual pages. Here you use the Next.js provided `Link` component that will handle the core linking functionality with the `href` attribute.\n\nWhat is noteworthy is the `as` attribute that allows aliasing paths to be displayed as `/activity/123` instead of with parameters, e.g. `/activity?id=123`. Otherwise the link generation is inline with standard JSX/Next.js methods:\n\n```jsx\n\u003cul\u003e\n{items.map(item =\u003e (\n\u003cli key={item.content.id}\u003e\n    \u003cLink\n    href={\"/activity?id=\" + item.content.id}\n    as={\"activity/\" + item.content.id}\n    \u003e\n    \u003ca\u003e{item.content.name}\u003c/a\u003e\n    \u003c/Link\u003e\n\u003c/li\u003e\n))}\n\u003c/ul\u003e\n```\n\nNote: In our case the GraphQL backend does not currently expose loading content objects by URL Aliases (e.g. /this/is/my/page), so our URLs end up being in the format `http://example.com/activities/123`, but the `id` parameter could just as well be called something like `slug` or `path` and allow pretty URLs to content views.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjanit%2Fdecoupled-cms-nextjs-graphql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjanit%2Fdecoupled-cms-nextjs-graphql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjanit%2Fdecoupled-cms-nextjs-graphql/lists"}