Tự làm blog với Keystone CMS và Next.js

KeystoneJS là một trong những CMS ( Content Management System) tốt nhất được viết bằng Nodejs. Với CLI riêng, nó sẽ auto-generated tất cả các thành phần cần thiết để tạo ra 1 trang web với tính tùy biến cao, dễ dàng chỉnh sửa tùy theo nhu cầu sử dụng. Tuy nhiên, về phần frontend thì Keystone đang sử dụng template engine và chúng ta sẽ tìm hiểu cách tích hợp React vào trong CMS này.

Next.js is a popular React SSR (Server Side Rendering) framework which allows you to build an SEO friendly React website with minimal configuration. With server side rendering, you can set SEO meta tags on the server before returning to the client. So search engine can crawl the data without running any Javascript, which is especially important for blogs and news websites.

Next.js là một framework React SSR ( Server Side Rendering ) nổi tiếng, cho phép bạn xây dựng một SEO-friendly website với phần cài đặt nhỏ nhất. Với SSR, bạn có thể đặt các thẻ <meta> ở trên server trước khi mà chúng được trả về cho client. Điều này sẽ giúp search engine lấy được thông tin web của bạn.

Chuẩn bị

node & npm đã được cài đặt từ trước

Cài đặt KeystoneJS

Đầu tiên, hãy cài đặt mongoDB, bạn đọc có thể lấy về tại đây

Tiếp đến, cài đặt Keystone với Yeoman

npm install yo -g 
npm install -g generator-keystone 
yo keystone

Sau khi cài đặt thành công thì sẽ hiện bảng thông báo như này:

... và cấu trúc thư mục sẽ như thế này:

Chúng ta sẽ thử chạy project bằng

node keystone

Cài đặt Next.JS

Chúng ta sẽ cài đặt một số module sau:

npm install next react react-dom axios --save

Tiếp theo, chúng ta xoá 2 thư mục templates & public, thay thế bằng pages , componentsstatic. Project sẽ trông như thế này:

Tiếp theo, ở file keystone.js, thêm dòng này để init next

// Next app 
const next = require('next'); 
const dev = process.env.NODE_ENV !== 'production'; 
const app = next({ dev });
// Start Next app
app.prepare()
 .then(() => {
    
  // Setup common locals for your templates. The following are required for the
  // bundled templates and layouts. Any runtime locals (that should be set uniquely
  // for each request) should be added to ./routes/middleware.js
  keystone.set('locals', {
   _: require('lodash'),
   env: keystone.get('env'),
   utils: keystone.utils,
   editable: keystone.content.editable,
  });
  
  // Load your project's Routes
  keystone.set('routes', require('./routes'));
  
  // Configure the navigation bar in Keystone's Admin UI
  keystone.set('nav', {
   posts: ['posts', 'post-categories'],
   users: 'users',
  });
  
  keystone.start();
 });

File sẽ thành như sau:

// Simulate config options from your production environment by
// customising the .env file in your project's root folder.
require('dotenv').config();

// Next app
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });

// Require keystone
const keystone = require('keystone');

// Initialise Keystone with your project's configuration.
// See http://keystonejs.com/guide/config for available options
// and documentation.
keystone.init({
 'name': 'Keystone Next Example',
 'brand': 'Keystone Next Example',
 'auto update': true,
 'session': true,
 'auth': true,
 'user model': 'User',
});

// Load your project's Models
keystone.import('models');

// Start Next app
app.prepare()
 .then(() => {
 
  // Load your project's Routes
  keystone.set('routes', require('./routes')(app));
  
  // Configure the navigation bar in Keystone's Admin UI
  keystone.set('nav', {
   posts: ['posts', 'post-categories'],
   users: 'users',
  });
  
  keystone.start();
 });

Tiếp đến ở folder routes, xoá folder views và file middleware.js

routes/index.js, chúng ta sẽ viết api từ keystone rồi để Next.js xử lí vụ routes

const keystone = require('keystone');

// Setup Route Bindings
exports = module.exports = nextApp => keystoneApp => {

	// Next request handler
	const handle = nextApp.getRequestHandler();

	keystoneApp.get('/api/posts', (req, res, next) => {
		const Post = keystone.list('Post');
		Post.model
			.find()
			.where('state', 'published')
			.sort('-publishedDate')
			.exec(function (err, results) {
				if (err) throw err;
				res.json(results);
			});
	});

	keystoneApp.get('*', (req, res) => {
		return handle(req, res);
	});
};

Sau khi sửa keystone.jsroutes/index.js, chạy lại keystone

Tiếp đến là phần React, tạo thử 1 trang App.js/pages

import { Component } from 'react';

class App extends Component {
  render() {
    return (
      <div>Hello World</div>
    );
  };
}

export default App;

Implement React và Keystone

Tạo một bài viết mới ở localhost:3000/keystone

Tiếp đến ở folder /pages, tạo 1 file mới tên là _document.js, thêm thư viện Boostrap:

import Document, { Head, Main, NextScript } from 'next/document'
import flush from 'styled-jsx/server'

export default class MyDocument extends Document {
  static getInitialProps({ renderPage }) {
    const { html, head, errorHtml, chunks } = renderPage()
    const styles = flush()
    return { html, head, errorHtml, chunks, styles }
  }

  render() {
    return (
      <html>
        <Head>
          <meta name='viewport' content='width=device-width, initial-scale=1.0' />
          <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css' integrity='sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy' crossOrigin='anonymous' />
        </Head>
        <body>
          <Main />
          <script src='https://code.jquery.com/jquery-3.2.1.slim.min.js' integrity='sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN' crossOrigin='anonymous'></script>
          <script src='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js' integrity='sha384-a5N7Y/aK3qNeh15eJKGWxsqtnX/wWdSZSKp+81YjTmS15nvnvxKHuzaWwXHDli+4' crossOrigin='anonymous'></script>
          <NextScript />
        </body>
      </html>
    )
  }
}

Call api ở App.js

import { Component } from 'react';
import axios from 'axios'

class App extends Component {

  static async getInitialProps() {
    let response = await axios.get('http://localhost:3000/api/posts');
    return { posts: response.data };
  }

  render() {
    return (
      <div className='container'>
        <style jsx>{`
            .header {
              padding: 16px 16px;
            }
            .content {
              padding: 16px 16px;
            }
            .post {
              margin-bottom: 16px;
            }
        `}</style>
        <div className='header'>
          <h1>Keystone Next Example</h1>
        </div>
        <div className='content'>
          { this.props.posts.map((post, i) => {
              return (
                <div className='post' key={i}>
                  <div className='row'>
                    <div className='col-12 col-md-4'>
                      <img className='img-fluid' src={post.image.secure_url}/>
                    </div>
                    <div className='col-12 col-md-8'>
                      <h2>{post.title}</h2>
                      <div dangerouslySetInnerHTML={{__html: post.content.brief}}></div>
                    </div>
                  </div>
                </div>
              );
            }) }
        </div>
      </div>
    );
  };
}

export default App;

Tada !

====================

Bài viết được dịch từ https://medium.com/@victor36max/how-to-build-a-react-driven-blog-with-next-js-and-keystonejs-cae3cd9fb804