DEV

2022-03-17 12:46

Proj. surf-log (블로그 프로젝트)

github repo

1. Spec

  • NextJS + Typescript
  • marked + codemirror + prismjs

2. markdown meta build 기능 구현

const fs = require('fs')
const path = require('path')
const matter = require('gray-matter')

const thumbnailDirPathStr = '../thumbnails'

function generateUniqSerial() {
    return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => {
        const r = Math.floor(Math.random() * 16)
        return r.toString(16)
    })
}

function main() {
    const mdFilesDirectoryPath = path.resolve(__dirname, '../articles')
    const mdFileNames = fs.readdirSync(mdFilesDirectoryPath, 'utf8')
    const mdFilesMetaArray = mdFileNames
        .map((fileName) => {
            const mdFilePath = path.resolve(
                __dirname,
                `../articles/${fileName}`
            )
            const mdFileContent = fs.readFileSync(mdFilePath, 'utf8')
            const mdFileMeta = matter(mdFileContent, {
                excerpt: function (file, options) {
                    file.excerpt = encodeURI(file.data.excerpt)
                    if (file.data.thumbnail) {
                        const thumbnailFileBase64Encoded = fs.readFileSync(
                            path.resolve(
                                __dirname,
                                `${thumbnailDirPathStr}/${file.data.thumbnail}`
                            ),
                            'base64'
                        )
                        file.thumbnailBase64 = thumbnailFileBase64Encoded
                    }
                },
            })
            return mdFileMeta
        })
        .sort((a, b) => {
            return new Date(a.data.createdAt) - new Date(b.data.createdAt)
        })
        .reduce((prev, curr) => {
            if (prev.find((prevItem) => prevItem.excerpt === curr.excerpt)) {
                curr.excerpt = `${curr.excerpt}-${generateUniqSerial()}`
            }
            return prev.concat(curr)
        }, [])
        .sort((a, b) => {
            return new Date(b.data.createdAt) - new Date(a.data.createdAt)
        })

    const categories = mdFilesMetaArray
        .map((meta) => meta.data.category)
        .filter((value, index, self) => self.indexOf(value) === index)

    const mdFilesMeta = mdFilesMetaArray.reduce((prev, curr) => {
        prev[curr.excerpt] = curr
        return prev
    }, {})

    const metaFilePath = path.resolve(__dirname, '../public/article-meta.json')

    const meta = {
        articles: mdFilesMeta,
        categories,
    }
    fs.writeFileSync(metaFilePath, JSON.stringify(meta))
}

main()

3. editor 기능 구현

codemirror로 editor 기능을 구현하였다.

4. markdown renderer 기능 구현

marked 모듈을 사용하여 markdown으로 작성된 string text를 html로 변환
prismjs로 코드 부분 Highlight

5. markdown 파일 쓰기(write) 기능 구현

next page api를 사용하여 구현.

import { NextApiHandler } from 'next'
import fs from 'fs'
import path from 'path'

function generateUniqSerial() {
    return 'xxxx-xxxx-xxx-xxxx'.replace(/[x]/g, (c) => {
        const r = Math.floor(Math.random() * 16)
        return r.toString(16)
    })
}

const SaveAPI: NextApiHandler = (req, res) => {
    if (req.method === 'POST') {
        const { title, excerpt, category, thumbnail, text } = req.body
        let articlePath = path.resolve(
            __dirname,
            `../../../../articles/${title}.md`
        )
        if (fs.existsSync(articlePath)) {
            articlePath = path.resolve(
                __dirname,
                `../../../../articles/${`${title}-${generateUniqSerial()}`}.md`
            )
        }
        let content = `---
title: ${title}
excerpt: ${excerpt}
category: ${category}
thumbnail: ${thumbnail}
createdAt: ${new Date().toISOString()}
---
${text}`
        fs.writeFileSync(articlePath, content)
        return res.status(200).json({
            error: null,
        })
    }
    return res.status(501).json({
        error: 'not implemented',
    })
}

export default SaveAPI

6. markdown meta 정보 가져오기

nextjs server side api를 사용하여 다음과 같이 초기 fetching 후 pageProps로 페이지에 전달

export const getStaticProps: GetStaticProps<ServerProps> = async (ctx) => {
    const articleMeta = (await import('../../public/article-meta.json')) as {
        articles: {
            [key: string]: Article
        }
        categories: string[]
    }
    return {
        props: {
            articles: Object.entries(articleMeta.articles).map(
                ([key, content]) => content
            ),
        },
    }
}

7. emotion으로 스타일링

emotion을 사용하여 style 코드 작성.
꽤나 편리하다. styled-components가 제공해주는 기능도 제공하면서 css로 children의 tag들도 모조리 styling 할 수 있는게 장점이다.

import { css } from '@emotion/css'

...

<SaveModal
    className={css`
        label {
            font-weight: bold;
        }
        input {
            height: 1.5rem;
            margin-top: 0.5rem;
        }

        input + label {
            margin-top: 0.8rem;
        }

        button {
            margin-top: 1rem;
            font-weight: bold;
            background-color: #000000;
            border: 1px solid #000000;
            border-radius: 3px;
            color: #ffffff;
            cursor: pointer;
            height: 1.95rem;

            &:active {
                background-color: #ffffff;
                color: #000000;
            }
        }
    `}
>
    <label>Title</label>
    <input
        name="title"
        value={modalValues.title}
        onChange={handleChange}
    />
    <label>Excerpt</label>
    <input
        name="excerpt"
        value={modalValues.excerpt}
        onChange={handleChange}
    />
    <label>Category</label>
    <input
        name="category"
        value={modalValues.category}
        onChange={handleChange}
    />
    <label>Thumbnail</label>
    <input
        name="thumbnail"
        value={modalValues.thumbnail}
        onChange={handleChange}
    />
    <button onClick={handleClickSaveModalSave}>save</button>
</SaveModal>

앞으로 애용할 것 같다.

8. 배포

vercel을 사용하여 domain 연결 후 배포

9. 깃 전략

git flow를 사용