├── .gitignore
├── README.md
├── components
├── App.js
├── ArtistDetails.js
├── Card.js
├── Grid.js
├── Header.js
├── Loading.js
├── Nav.js
├── PageImage.js
├── RecordDetails.js
└── ReviewDetails.js
├── docs
└── settings.png
├── lib
├── initApollo.js
└── withData.js
├── package.json
├── pages
├── artists.js
├── artists
│ └── details.js
├── index.js
├── records.js
├── records
│ └── details.js
└── reviews
│ └── details.js
├── server.js
├── static
├── microphone.svg
├── records.svg
└── turntable.svg
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | _STORE
2 | node_modules
3 | *~
4 | .next
5 | npm-debug.log
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Server-side rendered App with Next.js, Apollo and GraphCMS
2 |
3 | 🚀 **[Live Demo](https://vinylbase.now.sh)**
4 |
5 | This example shows how to build a small music blog with [Next.js](https://github.com/zeit/next.js/), [Apollo](http://www.apollodata.com/) and [GraphCMS](https://graphcms.com).
6 |
7 | To connect your app you have to setup a new GraphCMS project and create the required content models as described [here](https://graphcms.com/docs/examples/Server-side_rendered_app_with_nextjs_and_apollo/).
8 |
9 | To get this information, log into GraphCMS and go to your project settings.
10 |
11 | 
12 |
13 | Copy the Endpoint URL for the `Simple Endpoint` from the `ENDPOINTS` section. Insert the URL into the variable `GRAPHCMS_API` in the file `lib/initClient`.
14 |
15 | ## Installation
16 |
17 | `npm install`
18 |
19 | ## Starting
20 |
21 | `npm run start`
22 |
23 | ## Deployment
24 |
25 | Install now:
26 |
27 | `npm install -g now`
28 |
29 | Deploy the app:
30 |
31 | `now`
32 |
--------------------------------------------------------------------------------
/components/App.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 |
3 | const App = ({ children }) => (
4 |
5 |
6 |
Vinylbase
7 |
8 |
9 | {children}
10 |
13 |
44 |
45 | )
46 |
47 | export default App
48 |
--------------------------------------------------------------------------------
/components/ArtistDetails.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactMarkdown from 'react-markdown'
3 | import Grid from './Grid'
4 |
5 | const ArtistDetails = ({ artist }) => (
6 |
7 |
8 | Records:
9 |
10 |
11 |
12 | )
13 |
14 | export default ArtistDetails
15 |
--------------------------------------------------------------------------------
/components/Card.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import StarRatingComponent from 'react-star-rating-component'
3 |
4 | const Card = ({ entry: { createdAt, title, id, image, rating } }) => (
5 |
6 | { image ?

: null }
7 |
8 |
{title}
9 | { rating &&
10 |
11 |
18 |
19 | }
20 |
57 |
58 | )
59 |
60 | export default Card
61 |
--------------------------------------------------------------------------------
/components/Grid.js:
--------------------------------------------------------------------------------
1 | import Card from './Card'
2 | import Link from 'next/link'
3 |
4 | const Grid = ({ entries, type, pageImage }) => (
5 |
6 |
7 | {entries.map(entry =>
8 | -
9 |
16 |
17 | )}
18 |
19 |
20 |
32 |
33 | )
34 |
35 | export default Grid
36 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import PageImage from './PageImage'
2 |
3 | const Header = ({ title, subLine, pageImage, isIcon }) => (
4 |
5 |
6 | {title}
7 | { subLine ? {subLine}
: null}
8 |
9 |
10 |
11 |
12 |
36 |
37 | )
38 |
39 | export default Header
40 |
--------------------------------------------------------------------------------
/components/Loading.js:
--------------------------------------------------------------------------------
1 | const Loading = () => Loading...
2 |
3 | export default Loading
4 |
--------------------------------------------------------------------------------
/components/Nav.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | const Nav = ({ pathname }) => (
4 |
49 | )
50 |
51 | export default Nav
52 |
--------------------------------------------------------------------------------
/components/PageImage.js:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames'
2 |
3 | const PageImage = ({image, isIcon}) => {
4 | if (!image) {
5 | return null
6 | }
7 |
8 | return (
9 |
10 |

11 |
12 |
32 |
33 | )
34 | }
35 |
36 | export default PageImage
--------------------------------------------------------------------------------
/components/RecordDetails.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const RecordDetails = ({ record }) => (
4 |
5 |
6 | {record.tracks.map(({ id: trackId, title, length }) => (
7 | -
8 | {title}
9 | {new Date(1000 * length).toISOString().substr(14, 5)}
10 |
11 | ))}
12 |
13 |
14 |
33 |
34 | )
35 |
36 | export default RecordDetails
37 |
--------------------------------------------------------------------------------
/components/ReviewDetails.js:
--------------------------------------------------------------------------------
1 | import StarRatingComponent from 'react-star-rating-component'
2 | import ReactMarkdown from 'react-markdown'
3 | import Grid from './Grid'
4 |
5 | const ReviewDetails = ({ review: { title, review, rating, record: { artist } } }) => (
6 |
7 |
8 |
14 |
15 |
16 |
17 | The artist:
18 |
19 |
20 |
27 |
28 | )
29 |
30 | export default ReviewDetails
31 |
--------------------------------------------------------------------------------
/docs/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hygraph/example_01_nextjs_apollo/0e0c08cd4600d3535e4d6651c5ece26b6e42c1f4/docs/settings.png
--------------------------------------------------------------------------------
/lib/initApollo.js:
--------------------------------------------------------------------------------
1 | import { ApolloClient } from 'apollo-client'
2 | import { HttpLink } from 'apollo-link-http'
3 | import { InMemoryCache } from 'apollo-cache-inmemory'
4 | import fetch from 'isomorphic-fetch'
5 |
6 | let apolloClient = null
7 |
8 | // Polyfill fetch() on the server (used by apollo-client)
9 | if (!process.browser) {
10 | global.fetch = fetch
11 | }
12 |
13 | // Replace this URL by your APIs simple endpoint URL:
14 | const GRAPHCMS_API = 'https://api.graphcms.com/simple/v1/vinylbase'
15 |
16 | function createClient (initialState) {
17 | const HttpLinkData = {
18 | uri: GRAPHCMS_API,
19 | opts: {
20 | credentials: 'same-origin' // Additional fetch() options like `credentials` or `headers`
21 | }
22 | }
23 | return new ApolloClient({
24 | connectToDevTools: process.browser,
25 | ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
26 | link: new HttpLink(HttpLinkData),
27 | cache: new InMemoryCache().restore(initialState || {})
28 | })
29 | }
30 |
31 | export default function initApollo (initialState) {
32 | // Make sure to create a new client for every server-side request so that data
33 | // isn't shared between connections (which would be bad)
34 | if (!process.browser) {
35 | return createClient(initialState)
36 | }
37 |
38 | // Reuse client on the client-side
39 | if (!apolloClient) {
40 | apolloClient = createClient(initialState)
41 | }
42 | return apolloClient
43 | }
44 |
--------------------------------------------------------------------------------
/lib/withData.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { ApolloProvider, getDataFromTree } from 'react-apollo'
4 | import Head from 'next/head'
5 | import initApollo from './initApollo'
6 |
7 | // Gets the display name of a JSX component for dev tools
8 | function getComponentDisplayName (Component) {
9 | return Component.displayName || Component.name || 'Unknown'
10 | }
11 |
12 | export default ComposedComponent => {
13 | return class WithData extends Component {
14 | static displayName = `WithData(${getComponentDisplayName(ComposedComponent)})`
15 | static propTypes = {
16 | serverState: PropTypes.object.isRequired
17 | }
18 |
19 | static async getInitialProps (ctx) {
20 | let serverState = {}
21 |
22 | // evaluate getInitialProps()
23 | let composedInitialProps = {}
24 | if (ComposedComponent.getInitialProps) {
25 | composedInitialProps = await ComposedComponent.getInitialProps(ctx)
26 | }
27 |
28 | // Running all queries in the tree extracting the data
29 | if (!process.browser) {
30 | const apollo = initApollo()
31 | // url prop if any of our queries needs it
32 | const url = { query: ctx.query, pathname: ctx.pathname }
33 |
34 | try {
35 | // Run all GraphQL queries
36 | await getDataFromTree(
37 |
38 |
39 |
40 | )
41 | } catch (error) {
42 | // Prevent Apollo Client GraphQL errors from crashing SSR.
43 | // Handle them in components via the data.error prop:
44 | // http://dev.apollodata.com/react/api-queries.html#graphql-query-data-error
45 | }
46 | // getDataFromTree does not call componentWillUnmount
47 | // head side effect therefore need to be cleared manually
48 | Head.rewind()
49 |
50 | // Extract query data from the Apollo store
51 | serverState = {
52 | apollo: {
53 | data: apollo.cache.extract()
54 | }
55 | }
56 | }
57 |
58 | return {
59 | serverState,
60 | ...composedInitialProps
61 | }
62 | }
63 |
64 | constructor (props) {
65 | super(props)
66 | this.apollo = initApollo(this.props.serverState)
67 | }
68 |
69 | render () {
70 | return (
71 |
72 |
73 |
74 | )
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vinylbase",
3 | "url": "https://github.com/GraphCMS/exmple_01_nextjs_apollo.git",
4 | "version": "0.0.2",
5 | "description": "Example app using Next, Apollo and GraphCMS",
6 | "scripts": {
7 | "dev": "node server.js",
8 | "next": "next",
9 | "build": "next build",
10 | "start": "NODE_ENV=production node server.js"
11 | },
12 | "author": "GraphCMS",
13 | "license": "ISC",
14 | "standard": {
15 | "parser": "babel-eslint",
16 | "ignore": [
17 | "**/node_modules/**"
18 | ]
19 | },
20 | "devDependencies": {
21 | "babel-eslint": "^7.2.3",
22 | "prop-types": "^15.5.10",
23 | "standard": "^10.0.2"
24 | },
25 | "dependencies": {
26 | "apollo-client-preset": "^1.0.1",
27 | "express": "^4.15.3",
28 | "graphql": "^0.11.7",
29 | "graphql-anywhere": "^4.0.0",
30 | "graphql-tag": "^2.5.0",
31 | "isomorphic-fetch": "^2.2.1",
32 | "next": "^4.1.4",
33 | "react": "^16.0.0",
34 | "react-apollo": "^2.0.0",
35 | "react-dom": "^16.0.0",
36 | "react-markdown": "^2.5.0",
37 | "react-star-rating-component": "1.2.4"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pages/artists.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-apollo'
2 | import gql from 'graphql-tag'
3 | import App from '../components/App'
4 | import Grid from '../components/Grid'
5 | import Header from '../components/Header'
6 | import Loading from '../components/Loading'
7 | import Nav from '../components/Nav'
8 | import withData from '../lib/withData'
9 |
10 | const AllArtists = ({ url: { pathname }, data: { loading, error, allArtists } }) => {
11 | if (error) return Error loading artists.
12 | return (
13 |
14 |
15 | {
16 | loading ? : (
17 |
18 |
19 |
22 |
23 | )
24 | }
25 |
26 | )
27 | }
28 |
29 | const allArtists = gql`
30 | query allArtists {
31 | allArtists(orderBy: createdAt_DESC) {
32 | id
33 | title: name
34 | slug
35 | createdAt
36 | image: picture {
37 | handle
38 | }
39 | }
40 | }`
41 |
42 | export default withData(graphql(allArtists)(AllArtists))
43 |
--------------------------------------------------------------------------------
/pages/artists/details.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-apollo'
2 | import gql from 'graphql-tag'
3 | import App from '../../components/App'
4 | import Nav from '../../components/Nav'
5 | import Loading from '../../components/Loading'
6 | import Header from '../../components/Header'
7 |
8 | import ArtistDetails from '../../components/ArtistDetails'
9 | import withData from '../../lib/withData'
10 |
11 | const Artist = ({ url: { pathname }, data: { loading, error, artist } }) => {
12 | if (error) return Error loading the artist.
13 |
14 | const pageImage = !loading && artist.picture ? `https://media.graphcms.com/resize=w:80,h:80,fit:crop/${artist.picture.handle}` : null
15 |
16 | return (
17 |
18 |
19 | {
20 | loading ? : (
21 |
27 | )
28 | }
29 |
30 | )
31 | }
32 |
33 | const artistDetails = gql`
34 | query artistDetails($slug: String! ) {
35 | artist: Artist(slug: $slug) {
36 | id
37 | name
38 | slug
39 | bio
40 | picture {
41 | handle
42 | }
43 | records {
44 | id
45 | title
46 | slug
47 | image: cover {
48 | handle
49 | }
50 | }
51 | }
52 | }`
53 |
54 | export default withData(graphql(artistDetails, {
55 | options: ({ url: { query: { slug } } }) => ({ variables: { slug } })
56 | })(Artist))
57 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-apollo'
2 | import gql from 'graphql-tag'
3 | import App from '../components/App'
4 | import Header from '../components/Header'
5 | import Loading from '../components/Loading'
6 | import Nav from '../components/Nav'
7 | import Grid from '../components/Grid'
8 | import withData from '../lib/withData'
9 |
10 | const AllReviews = ({ url: { pathname }, data: { loading, error, allReviews } }) => {
11 | if (error) return Error loading reviews.
12 | return (
13 |
14 |
15 | {
16 | loading ? : (
17 |
18 |
24 |
27 |
28 | )
29 | }
30 |
31 | )
32 | }
33 |
34 | const allReviews = gql`
35 | query allReviews {
36 | allReviews(orderBy: createdAt_DESC) {
37 | id
38 | slug
39 | rating
40 | createdAt
41 | title
42 | }
43 | }`
44 |
45 | export default withData(graphql(allReviews)(AllReviews))
46 |
--------------------------------------------------------------------------------
/pages/records.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-apollo'
2 | import gql from 'graphql-tag'
3 | import App from '../components/App'
4 | import Grid from '../components/Grid'
5 | import Header from '../components/Header'
6 | import Loading from '../components/Loading'
7 | import Nav from '../components/Nav'
8 | import withData from '../lib/withData'
9 |
10 | const AllRecords = ({ url: { pathname }, data: { loading, error, allRecords } }) => {
11 | if (error) return Error loading records.
12 | return (
13 |
14 |
15 | {
16 | loading ? : (
17 |
18 |
19 |
22 |
23 | )
24 | }
25 |
26 | )
27 | }
28 |
29 | const allRecords = gql`
30 | query allRecords {
31 | allRecords(orderBy: createdAt_DESC) {
32 | id
33 | title
34 | slug
35 | createdAt
36 | image: cover {
37 | handle
38 | }
39 | }
40 | }`
41 |
42 | export default withData(graphql(allRecords)(AllRecords))
43 |
--------------------------------------------------------------------------------
/pages/records/details.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-apollo'
2 | import gql from 'graphql-tag'
3 | import Loading from '../../components/Loading'
4 |
5 | import App from '../../components/App'
6 | import Nav from '../../components/Nav'
7 | import Header from '../../components/Header'
8 | import RecordDetails from '../../components/RecordDetails'
9 | import withData from '../../lib/withData'
10 |
11 | const Record = ({ url: { pathname }, data: { loading, error, Record } }) => {
12 | if (error) return Error loading the record.
13 |
14 | const pageImage = !loading && Record.cover ? `https://media.graphcms.com/resize=w:80,h:80,fit:crop/${Record.cover.handle}` : null
15 |
16 | return (
17 |
18 |
19 | {
20 | loading ? : (
21 |
22 |
28 |
31 |
32 | )
33 | }
34 |
35 | )
36 | }
37 |
38 | const recordDetails = gql`
39 | query recordDetails($slug: String! ) {
40 | Record(slug: $slug) {
41 | id
42 | title
43 | cover {
44 | handle
45 | }
46 | tracks {
47 | id
48 | title
49 | length
50 | }
51 | }
52 | }`
53 |
54 | export default withData(graphql(recordDetails, {
55 | options: ({ url: { query: { slug } } }) => ({ variables: { slug } })
56 | })(Record))
57 |
--------------------------------------------------------------------------------
/pages/reviews/details.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { graphql } from 'react-apollo'
3 | import gql from 'graphql-tag'
4 | import App from '../../components/App'
5 | import Nav from '../../components/Nav'
6 | import Loading from '../../components/Loading'
7 | import Header from '../../components/Header'
8 | import ReviewDetails from '../../components/ReviewDetails'
9 | import withData from '../../lib/withData'
10 |
11 | function Review ({ url: { pathname }, data: { loading, error, Review } }) {
12 | if (error) return Error loading the review.
13 |
14 | const pageImage = Review && Review.record.image ? `https://media.graphcms.com/resize=w:80,h:80,fit:crop/${Review.record.image.handle}` : null
15 |
16 | return (
17 |
18 |
19 | {
20 | loading ? : (
21 |
22 |
23 |
26 |
27 | )
28 | }
29 |
30 | )
31 | }
32 |
33 | const reviewDetails = gql`
34 | query reviewDetails($slug: String! ) {
35 | Review(slug: $slug) {
36 | id
37 | title
38 | review
39 | rating
40 | record {
41 | title
42 | slug
43 | image: cover {
44 | handle
45 | }
46 | artist {
47 | title: name
48 | slug
49 | image: picture {
50 | handle
51 | }
52 | }
53 | }
54 | }
55 | }`
56 |
57 | const ReviewWithData = graphql(reviewDetails, {
58 | options: ({ url: { query: { slug } } }) => ({ variables: { slug } })
59 | })(Review)
60 |
61 | export default(withData(ReviewWithData))
62 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const next = require('next')
3 |
4 | const dev = process.env.NODE_ENV !== 'production'
5 | const app = next({ dev })
6 | const port = process.env.PORT || 3000
7 |
8 | app.prepare()
9 | .then(() => {
10 | const server = express()
11 |
12 | server.use('/static', express.static('static'))
13 |
14 | server.get('/artists', (req, res) => {
15 | return app.render(req, res, '/artists')
16 | })
17 |
18 | server.get('/artists/:slug', (req, res) => {
19 | return app.render(req, res, '/artists/details', { slug: req.params.slug })
20 | })
21 |
22 | server.get('/reviews', (req, res) => {
23 | return app.render(req, res, '/')
24 | })
25 |
26 | server.get('/reviews/:slug', (req, res) => {
27 | const queryParams = {
28 | slug: req.params.slug,
29 | type: req.params.type
30 | }
31 | return app.render(req, res, '/reviews/details', queryParams)
32 | })
33 |
34 | server.get('/records', (req, res) => {
35 | return app.render(req, res, '/records')
36 | })
37 |
38 | server.get('/records/:slug', (req, res) => {
39 | const queryParams = {
40 | slug: req.params.slug,
41 | type: req.params.type
42 | }
43 | return app.render(req, res, '/records/details', queryParams)
44 | })
45 |
46 | server.get('*', (req, res) => {
47 | return app.render(req, res, '/', req.query)
48 | })
49 |
50 | server.listen(port, (err) => {
51 | if (err) throw err
52 | console.log(`> Ready on http://localhost:${port}`)
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/static/microphone.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/records.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/turntable.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------