├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------