├── .env ├── .gitignore ├── README.md ├── next-env.d.ts ├── package.json ├── pages ├── _app.tsx ├── index.tsx └── post │ └── [slug].tsx ├── public ├── favicon.ico └── vercel.svg ├── styles ├── Home.module.scss └── globals.scss ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | CONTENT_API_KEY=5b920e0acdf1d69b540a80b7e9 2 | BLOG_URL=https://ghostcms-nextjs-backend.herokuapp.com -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Changes 18 | 19 | Please make sure to update your Ghost API keys and your blog URL according to where you have deployed your Ghost CMS. You can watch the [building and deployment process here](https://www.youtube.com/watch?v=1SYU1GorO6Y) 20 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghost-cms-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "next": "9.5.2", 12 | "react": "16.13.1", 13 | "react-dom": "16.13.1" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^14.0.27", 17 | "@types/react": "^16.9.46", 18 | "sass": "^1.26.10", 19 | "typescript": "^3.9.7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.scss' 2 | 3 | export default function App({ Component, pageProps }) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import styles from '../styles/Home.module.scss' 3 | import Link from 'next/link' 4 | 5 | const { BLOG_URL, CONTENT_API_KEY } = process.env 6 | 7 | type Post = { 8 | title: string 9 | slug: string 10 | } 11 | 12 | async function getPosts() { 13 | // curl "" 14 | const res = await fetch( 15 | `${BLOG_URL}/ghost/api/v3/content/posts/?key=${CONTENT_API_KEY}&fields=title,slug,custom_excerpt` 16 | ).then((res) => res.json()) 17 | 18 | const posts = res.posts 19 | 20 | return posts 21 | } 22 | 23 | export const getStaticProps = async ({ params }) => { 24 | const posts = await getPosts() 25 | return { 26 | revalidate: 10, 27 | props: { posts } 28 | } 29 | } 30 | 31 | const Home: React.FC<{ posts: Post[] }> = (props) => { 32 | const { posts } = props 33 | 34 | return ( 35 |
36 |

Hello to my blog

37 |
    38 | {posts.map((post, index) => { 39 | return ( 40 |
  • 41 | 42 | {post.title} 43 | 44 |
  • 45 | ) 46 | })} 47 |
48 |
49 | ) 50 | } 51 | 52 | export default Home 53 | -------------------------------------------------------------------------------- /pages/post/[slug].tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { useRouter } from 'next/router' 3 | import styles from '../../styles/Home.module.scss' 4 | import { useState } from 'react' 5 | 6 | const { BLOG_URL, CONTENT_API_KEY } = process.env 7 | 8 | async function getPost(slug: string) { 9 | const res = await fetch( 10 | `${BLOG_URL}/ghost/api/v3/content/posts/slug/${slug}?key=${CONTENT_API_KEY}&fields=title,slug,html` 11 | ).then((res) => res.json()) 12 | 13 | const posts = res.posts 14 | 15 | return posts[0] 16 | } 17 | 18 | // Ghost CMS Request 19 | export const getStaticProps = async ({ params }) => { 20 | const post = await getPost(params.slug) 21 | return { 22 | props: { post }, 23 | revalidate: 10 24 | } 25 | } 26 | 27 | // hello-world - on first request = Ghost CMS call is made (1) 28 | // hello-world - on other requests ... = filesystem is called (1M) 29 | 30 | export const getStaticPaths = () => { 31 | // paths -> slugs which are allowed 32 | // fallback -> 33 | return { 34 | paths: [], 35 | fallback: true 36 | } 37 | } 38 | 39 | type Post = { 40 | title: string 41 | html: string 42 | slug: string 43 | } 44 | 45 | const Post: React.FC<{ post: Post }> = (props) => { 46 | console.log(props) 47 | 48 | const { post } = props 49 | const [enableLoadComments, setEnableLoadComments] = useState(true) 50 | 51 | const router = useRouter() 52 | 53 | if (router.isFallback) { 54 | return

Loading...

55 | } 56 | 57 | function loadComments() { 58 | setEnableLoadComments(false) 59 | ;(window as any).disqus_config = function () { 60 | this.page.url = window.location.href 61 | this.page.identifier = post.slug 62 | } 63 | 64 | const script = document.createElement('script') 65 | script.src = 'https://ghostcms-nextjs.disqus.com/embed.js' 66 | script.setAttribute('data-timestamp', Date.now().toString()) 67 | 68 | document.body.appendChild(script) 69 | } 70 | 71 | return ( 72 |
73 |

74 | 75 | Go back 76 | 77 |

78 |

{post.title}

79 |
80 | 81 | {enableLoadComments && ( 82 |

83 | Load Comments 84 |

85 | )} 86 | 87 |
88 |
89 | ) 90 | } 91 | 92 | export default Post 93 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mehulmpt/ghost-cms-nextjs/1872e7b02894bea1c531d159a387f14fb31b1a67/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | width: 960px; 7 | margin: 0 auto; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | .postitem { 13 | padding: 10px 0; 14 | } 15 | 16 | .goback { 17 | text-align: left; 18 | align-self: self-start; 19 | cursor: pointer; 20 | } 21 | 22 | .main { 23 | padding: 5rem 0; 24 | flex: 1; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | align-items: center; 29 | } 30 | 31 | .footer { 32 | width: 100%; 33 | height: 100px; 34 | border-top: 1px solid #eaeaea; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | } 39 | 40 | .footer img { 41 | margin-left: 0.5rem; 42 | } 43 | 44 | .footer a { 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | } 49 | 50 | .title a { 51 | color: #0070f3; 52 | text-decoration: none; 53 | } 54 | 55 | .title a:hover, 56 | .title a:focus, 57 | .title a:active { 58 | text-decoration: underline; 59 | } 60 | 61 | .title { 62 | margin: 0; 63 | line-height: 1.15; 64 | font-size: 4rem; 65 | } 66 | 67 | .title, 68 | .description { 69 | text-align: center; 70 | } 71 | 72 | .description { 73 | line-height: 1.5; 74 | font-size: 1.5rem; 75 | } 76 | 77 | .code { 78 | background: #fafafa; 79 | border-radius: 5px; 80 | padding: 0.75rem; 81 | font-size: 1.1rem; 82 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 83 | Bitstream Vera Sans Mono, Courier New, monospace; 84 | } 85 | 86 | .grid { 87 | display: flex; 88 | align-items: center; 89 | justify-content: center; 90 | flex-wrap: wrap; 91 | 92 | max-width: 800px; 93 | margin-top: 3rem; 94 | } 95 | 96 | .card { 97 | margin: 1rem; 98 | flex-basis: 45%; 99 | padding: 1.5rem; 100 | text-align: left; 101 | color: inherit; 102 | text-decoration: none; 103 | border: 1px solid #eaeaea; 104 | border-radius: 10px; 105 | transition: color 0.15s ease, border-color 0.15s ease; 106 | } 107 | 108 | .card:hover, 109 | .card:focus, 110 | .card:active { 111 | color: #0070f3; 112 | border-color: #0070f3; 113 | } 114 | 115 | .card h3 { 116 | margin: 0 0 1rem 0; 117 | font-size: 1.5rem; 118 | } 119 | 120 | .card p { 121 | margin: 0; 122 | font-size: 1.25rem; 123 | line-height: 1.5; 124 | } 125 | 126 | .logo { 127 | height: 1em; 128 | } 129 | 130 | @media (max-width: 600px) { 131 | .grid { 132 | width: 100%; 133 | flex-direction: column; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, 6 | Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | max-width: 100%; 17 | } 18 | 19 | img { 20 | height: auto; 21 | } 22 | 23 | #disqus_thread { 24 | width: 100%; 25 | margin: 20px 0; 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve" 20 | }, 21 | "include": [ 22 | "next-env.d.ts", 23 | "**/*.ts", 24 | "**/*.tsx" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------