オーマイガー東京

category

  • all

articles

  • ohmg.tokyo
  • オーマイガー東京

    category

      articles

        ブログ制作 - Nuxtでmarkdown対応のブログを作る

        Written by: maru_u___
        Published: 2020-12-28 | Updated: 2020-12-31

        目次 #

        • 目次
        • タイムライン
        • 課題
        • 要件
        • 構成要素
        • ブログ作成手順
          • nuxt-app を作成
          • 必要なライブラリを導入
          • 記事を置くためのディレクトリを作成する
          • nuxt.config.js を設定
          • 記事表示用の vue ファイルを作成
          • canva で favicon と ogimage のデザインを作成
          • favicon は favicon generator で作成
          • 作成されたファイルを全て ~/static にインポート
          • TS の規約を緩める
          • デザインを整える
          • 記事取得用の store 作成
          • Google Analytics を導入する
          • Netlify へデプロイ
          • 静的ページ用のルーティング書く
          • 過去記事で持ってきたいものを持ってくる
          • できれば、お名前 com から google domains に移行
          • ドメインの向き先を変更する
          • 動作確認後、Xserver 解約
          • markdown でテーブル書くのがだるくない状態になっている
          • markdown で参照元リンクを書くのがだるくない状態になっている
          • markdown で目次を書くのがだるくない状態になっている
          • markdown で画像差し込むのがだるくない状態になっている
          • スマホ対応
          • sitemap 対応
          • robots.txt 作成
        • その他
        • 参照リンク

        タイムライン #

        • 2020/12/28
          • 年末年始でブログ作り直そうと決める
          • markdown ファイル以外のファイルを使わず、カテゴリを作るのに苦戦
        • 2020/12/31
          • カテゴリ問題をゴリ押しで解決
          • デザインもそこそこ整える
        • 2021/01/01
          • 過去記事の移植完了
          • Netlify にデプロイ
          • ドメインの向き先変更完了
        • 2021/01/02
          • スマホ対応完了
        • 2021/01/03
          • 画像の移植完了
          • sitemap の対応
          • ogp の対応

        課題 #

        • 勉強したものをまとめる場所が新しく欲しい
        • かつてのブログのデザインが気に入らない
        • かつてのブログの文体が気に入らない

        要件 #

        • Must
          • 機能面
          • 運用面
          • デザイン面
        • More
          • 機能面
          • 技術面
          • 運用面
            • お名前.com から Google Domain に移行
          • デザイン面

        構成要素 #

        • Nuxt
          • 業務でも使っていて慣れているのと、単純に便利
        • Buefy
          • Vuetify 以外のデザインシステムを使用したい
          • 最初、素の Bulma 使って失敗した
        • TypeScript
        • Netlify
          • 以前試してみたらとても便利だった
          • デプロイするだけで勝手にデプロイしてくれる

        ブログ作成手順 #

        作業ログ

        nuxt-app を作成 #

        yarn create nuxt-app ohmg-tokyo
        
        ...
        
        create-nuxt-app v3.4.0
        ✨  Generating Nuxt.js project in ohmg-tokyo
        ? Project name: ohmg-tokyo
        ? Programming language: TypeScript
        ? Package manager: Yarn
        ? UI framework: Buefy
        ? Nuxt.js modules: Axios
        ? Linting tools: ESLint, Prettier, StyleLint
        ? Testing framework: Jest
        ? Rendering mode: Universal (SSR / SSG)
        ? Deployment target: Static (Static/JAMStack hosting)
        ? Development tools:
        ? Continuous integration: GitHub Actions (GitHub only)
        ? What is your GitHub username? maru-u
        ? Version control system: Git
        

        必要なライブラリを導入 #

        yarn add @nuxtjs/dayjs @nuxtjs/google-gtag markdown-it markdown-it-anchor markdown-it-table-of-contents markdown-it-checkbox frontmatter-markdown-loader highlight.js @types/markdown-it @types/markdown-it-anchor
        yarn add -D sass raw-loader sass sass-loader
        

        記事を置くためのディレクトリを作成する #

        ~/articles/development/XXXX.md
        ~/articles/business/XXXX.md
        ~/articles/art/XXXX.md
        ...
        

        nuxt.config.js を設定 #

        import fs from 'fs'
        import glob from 'glob'
        import frontMatter from 'front-matter'
        
        let routes = []
        glob('./articles/**/*', (err, files) => {
          files.forEach((filePath) => {
            if (/.*\.md$/.test(filePath)) {
              const content = fs.readFileSync(filePath, 'utf-8')
              const meta = frontMatter(content)
        
              if (meta.attributes.isPrivate) {
                return
              }
              const splitedPath = filePath.split('/')
        
              const categoryPath = '/docs/' + splitedPath[2]
              routes.push(categoryPath)
        
              splitedPath.splice(0, 2)
              const articlePath = '/docs/' + splitedPath.join('/').slice(0, -3)
              routes.push(articlePath)
        
              routes = Array.from(new Set(routes))
            }
          })
        })
        
        export default {
          // Target (https://go.nuxtjs.dev/config-target)
          target: 'static',
        
          // Global page headers (https://go.nuxtjs.dev/config-head)
          head: {
            title: 'オーマイガー東京',
            titleTemplate: '%s - オーマイガー東京',
            meta: [
              {
                charset: 'utf-8',
              },
              {
                name: 'viewport',
                content: 'width=device-width, initial-scale=1',
              },
              {
                hid: 'og:site_name',
                property: 'og:site_name',
                content: 'オーマイガー東京',
              },
              {
                hid: 'og:image',
                property: 'og:image',
                content: '/ohmg.png',
              },
              {
                name: 'twitter:card',
                content: 'summary_large_image',
              },
              {
                name: 'twitter:site',
                content: '@maru_u___',
              },
              {
                name: 'twitter:creator',
                content: '@maru_u___',
              },
            ],
            link: [
              {
                rel: 'icon',
                type: 'image/x-icon',
                href: '/favicon.ico',
              },
              {
                rel: 'apple-touch-icon',
                sizes: '180x180',
                href: '/apple-touch-icon.png',
              },
              {
                rel: 'icon',
                type: 'image/png',
                sizes: '32x32',
                href: '/favicon-32x32.png',
              },
              {
                rel: 'icon',
                type: 'image/png',
                sizes: '16x16',
                href: '/favicon-16x16.png',
              },
              {
                rel: 'manifest',
                href: '/manifest.json',
              },
            ],
          },
        
          // Global CSS (https://go.nuxtjs.dev/config-css)
          css: [
            '~/assets/style.scss',
            { src: '~/node_modules/highlight.js/styles/vs2015.css', lang: 'css' },
          ],
        
          // Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
          plugins: [],
        
          // Auto import components (https://go.nuxtjs.dev/config-components)
          components: true,
        
          // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
          buildModules: [
            // https://go.nuxtjs.dev/typescript
            '@nuxt/typescript-build',
            // https://go.nuxtjs.dev/stylelint
            '@nuxtjs/stylelint-module',
          ],
        
          // Modules (https://go.nuxtjs.dev/config-modules)
          modules: [
            // https://go.nuxtjs.dev/buefy
            'nuxt-buefy',
            // https://go.nuxtjs.dev/axios
            '@nuxtjs/axios',
            '@nuxtjs/dayjs',
            '@nuxtjs/google-gtag',
          ],
        
          // Axios module configuration (https://go.nuxtjs.dev/config-axios)
          axios: {},
        
          // Build Configuration (https://go.nuxtjs.dev/config-build)
          build: {
            extend(config, _) {
              config.module.rules.push({
                test: /\.md$/,
                use: [{ loader: 'raw-loader' }],
              })
            },
          },
        
          dayjs: {
            locales: ['ja'],
            defaultLocale: 'ja',
            // defaultTimeZone: 'Asia/Tokyo',
            plugins: [
              'utc', // import 'dayjs/plugin/utc'
            ],
          },
        
          generate: {
            routes: routes,
          },
        
          // Google Analytics の設定
          'google-gtag': {
            id: 'UA-67919239-4',
            debug: true,
          },
        }
        

        記事表示用の vue ファイルを作成 #

        共通化とかは必要になったらやる

        <template>
          <div class="sidebar-page">
            <section class="sidebar-layout">
              <navigation />
              <div class="content">
                <h1>{{ title }}</h1>
                <div class="content-meta">
                  <p>
                    Written by: {{ attributes.author }}<br />
                    Published: {{ createdAt }} | Updated: {{ updatedAt }}
                  </p>
                </div>
                <div v-html="content" class="blog-content"></div>
              </div>
            </section>
          </div>
        </template>
        
        <script lang="ts">
        import Vue from 'vue'
        import hljs from 'highlight.js'
        import frontMatter from 'front-matter'
        import markdownItAnchor from 'markdown-it-anchor'
        import markdownItTableOfContents from 'markdown-it-table-of-contents'
        import markdownItCheckbox from 'markdown-it-checkbox'
        
        import Navigation from '~/components/Navigation.vue'
        
        let markdownIt = require('markdown-it')({
          html: true,
          xhtmlOut: true,
          linkify: true,
          breaks: true,
          typography: true,
          langPrefix: 'hljs language-',
          highlight: function (str: any, lang: any) {
            if (lang && hljs.getLanguage(lang)) {
              try {
                return (
                  '<pre class="hljs"><code>' +
                  hljs.highlight(lang, str, true).value +
                  '</code></pre>'
                )
              } catch (__) {}
            }
        
            return (
              '<pre class="hljs"><code>' +
              markdownIt.utils.escapeHtml(str) +
              '</code></pre>'
            )
          },
        })
        
        markdownIt.use(markdownItAnchor, {
          level: 2,
          permalink: true,
          permalinkSymbol: '#',
        })
        
        markdownIt.use(markdownItTableOfContents, {
          slugify: (markdownItAnchor as any).defaults.slugify,
          includeLevel: [2, 3, 4, 5, 6],
        })
        
        markdownIt.use(markdownItCheckbox)
        
        export default Vue.extend({
          components: {
            Navigation,
          },
        
          async asyncData({ params, $dayjs }) {
            const fileContent = await import(
              `~/articles/${params.category}/${params.doc}.md`
            )
            const res = frontMatter(fileContent.default) as any
        
            if (res.attributes.isPrivate) {
              return {
                title: '秘密',
                attributes: { description: '秘密の記事です' },
                content: '秘密です',
                createdAt: $dayjs(res.attributes.createdAt).format('YYYY-MM-DD'),
                updatedAt: $dayjs(res.attributes.updatedAt).format('YYYY-MM-DD'),
              }
            }
        
            return {
              title: res.attributes.title,
              attributes: res.attributes,
              content: markdownIt.render(res.body),
              createdAt: $dayjs(res.attributes.createdAt).format('YYYY-MM-DD'),
              updatedAt: $dayjs(res.attributes.updatedAt).format('YYYY-MM-DD'),
            }
          },
        
          head() {
            const self = this as any
            const title = self.attributes.title
            return {
              title,
              meta: [
                { name: 'description', content: `${self.attributes.description}` },
                { name: 'creation date', content: `${self.createdAt}` },
                { name: 'date', content: `${self.updatedAt}` },
                { property: 'og:type', content: 'article' },
                { property: 'og:title', content: title },
                {
                  property: 'og:description',
                  content: `${self.attributes.description}`,
                },
              ],
            }
          },
        })
        </script>
        
        <style scoped>
        .content {
          margin-left: 260px;
          margin-bottom: 100px;
          padding: 40px 5%;
        }
        
        .content-meta {
          font-weight: 600;
          color: #999;
          font-size: 0.8em;
          margin: 30px 0;
        }
        
        .content >>> h1 {
          color: #666;
        }
        
        .content >>> h2,
        .content >>> h3,
        .content >>> h4,
        .content >>> h5,
        .content >>> h6 {
          color: #666;
          padding: 0.6em 0;
          margin: 0.2em 0;
        }
        
        .content >>> h2 {
          border-bottom: 1px solid #666;
          margin-top: 2em;
          margin-bottom: 1em;
        }
        
        .content >>> h3 {
          margin-top: 1.5em;
          margin-bottom: 0.7em;
        }
        
        .content >>> h4 {
          margin-top: 1em;
        }
        
        .content >>> input[type='checkbox'] {
          margin-right: 10px !important;
          background-color: #999;
        }
        
        .content >>> input[type='checkbox']:checked ::before {
          background-color: #999;
        }
        
        .content >>> pre {
          max-height: 600px;
        }
        
        .content >>> a {
          font-weight: 600;
          color: #999;
          font-size: 0.8em;
          border-bottom: 1px solid #999;
        }
        </style>
        

        canva で favicon と ogimage のデザインを作成 #

        • ホーム - Canva

        favicon は favicon generator で作成 #

        • Favicon Generator for perfect icons on all browsers

        作成されたファイルを全て ~/static にインポート #

        site.webmanifest は manifest.json にリネーム。

        TS の規約を緩める #

        以下を追加

            "noImplicitAny": false,
            "allowSyntheticDefaultImports": true
        

        デザインを整える #

        • Sidebar | Buefy とかみながら作っていく

        記事取得用の store 作成 #

        かなり強引な仕上がりになってしまった

        import frontMatter from 'front-matter'
        
        const context = (require as any).context('~/articles/', true, /\.md$/)
        let articles = []
        let categories = []
        // map だと処理が追いつかず、Promise で渡っちゃうので forEach
        context.keys().forEach(async (path: string) => {
          const content = await import(`~/articles${path.slice(1)}`)
          const res = await frontMatter(content.default) as any
          const link = path.slice(1).slice(0, -3) as string
        
          if (res.attributes.isPrivate) {
            return
          }
        
          const splitedLink = link.split('/');
          let category = 'root';
          if (splitedLink.length > 2) {
            category = splitedLink[1]
          }
        
          (categories as any).push(category)
          categories = Array.from(new Set(categories)) as any
        
          (articles as any).push({
            link: '/docs' + link,
            category,
            title: res.attributes.title,
            description: res.attributes.description,
            updatedAt: res.attributes.updatedAt,
          })
        })
        
        export const state = () => ({
          articles: [],
          categories: [],
        })
        
        export const getters = {
          articles(state: any) {
            return state.articles
          },
        
          articlesByCategory: (state: any) => (category) => {
            return state.articles.filter(article => {
              return article.category === category
            })
          },
        
          categories(state: any) {
            return state.categories
          },
        }
        
        export const actions = {
          async fetchArticles({ commit }: any) {
            try {
              commit('setArticles', articles)
              commit('setCategories', categories)
            } catch (error) {
              console.error(error)
            }
          },
        }
        
        export const mutations = {
          setArticles(state: any, articles: [any]) {
            state.articles = articles
          },
        
          setCategories(state: any, categories: [any]) {
            state.categories = categories
          },
        }
        

        Google Analytics を導入する #

        nuxt.config.js に以下を追加

          // Google Analytics の設定
          'google-gtag': {
            id: 'UA-67919239-4',
            debug: true,
          },
        

        nuxt.config.js の modules に以下を追加

        '@nuxtjs/google-gtag',
        

        Netlify へデプロイ #

        • Netlify: All-in-one platform for automating modern web projects にログインする
        • 該当する github のリポジトリを紐付ける
        • Build command: yarn run generate
        • Publish directory: dist/

        静的ページ用のルーティング書く #

        nuxt v2.13 以降はルーティング自動で作ってくれるようになったのですが、markdown ファイルから強引にリンク作ってるせいでその対象にならなかったみたいなので、こちらでも強引に記述する必要がありました。(記事数が増えてきたときの処理が心配なところ)

        nuxt.config.js に以下を追加して、generate のオプションに追加しました。

        import fs from 'fs'
        import glob from 'glob'
        import frontMatter from 'front-matter'
        
        let routes = []
        const entries = glob.sync('./articles/**/*')
        entries.forEach((filePath) => {
          if (/.*\.md$/.test(filePath)) {
            const content = fs.readFileSync(filePath, 'utf-8')
            const meta = frontMatter(content)
        
            if (meta.attributes.isPrivate) {
              return
            }
            const splitedPath = filePath.split('/')
        
            const categoryPath = '/docs/' + splitedPath[2]
            routes.push(categoryPath)
        
            splitedPath.splice(0, 2)
            const articlePath = '/docs/' + splitedPath.join('/').slice(0, -3)
            routes.push(articlePath)
        
            routes = Array.from(new Set(routes))
          }
        })
        

        過去記事で持ってきたいものを持ってくる #

        地道に持ってきた。文体気に入らないものも多いけど、とりあえず移行完了とする。
        その他の記事は wordpress の export 機能で書き出して、ドライブにバックアップしておきました。

        できれば、お名前 com から google domains に移行 #

        以下を参考に作業

        お名前.com から Google Domains にドメイン移管する

        悲しいことに Google Domains が .tokyo ドメインをサポートしておらず、 .co ドメインのみ移行するという一番面倒臭い結果に。

        さくらで登録してるドメインもいずれまとめるぞ。

        【さくら →Google 移行作戦 前半】ドメイン移管編 | 青星総合研究所

        ドメインの向き先を変更する #

        以下の記事を参考にネームサーバを変更します

        【簡単】Netlify にお名前ドットコムで取得したドメインを設定しよう!【たったの 10 分】 | JAMstack -JP-

        動作確認後、Xserver 解約 #

        解約完了

        markdown でテーブル書くのがだるくない状態になっている #

        google スプレットシートの表を markdown で出力したい - Qiita

        markdown で参照元リンクを書くのがだるくない状態になっている #

        以下をブックマークに保存して対応完了

        javascript:(function(){const titleTag=document.getElementsByTagName("title"),pageUrl=location.href;let pageTitle=pageUrl;titleTag.length>0&&titleTag[0].innerHTML!==""&&(pageTitle=titleTag[0].innerHTML);const md=`- [${pageTitle}](${pageUrl})`;navigator.clipboard&&navigator.clipboard.writeText(md);})();
        

        markdown で目次を書くのがだるくない状態になっている #

        以下の方法で実装完了

        import markdownItAnchor from 'markdown-it-anchor'
        import markdownItTableOfContents from 'markdown-it-table-of-contents'
        
        markdownIt.use(markdownItAnchor, {
          level: 2,
          permalink: true,
          permalinkSymbol: '#',
        })
        
        markdownIt.use(markdownItTableOfContents, {
          slugify: (markdownItAnchor as any).defaults.slugify,
          includeLevel: [2, 3, 4, 5, 6],
        })
        

        Nuxt+Contentful で markdown-it を使ったブログで目次を作る方法 - Izm Log

        markdown で画像差し込むのがだるくない状態になっている #

        Dropbox が吐き出すリンクはそのままだと使えないので、外部ブログ用に整形するブックマークレットを作成し、登録

        javascript:(function(){navigator.clipboard.readText().then(text => {if(text[0]==="h"){const dbPath=text.replace('www.dropbox.com', 'dl.dropboxusercontent.com').replace('?dl=0', '');navigator.clipboard.writeText(`![](${dbPath})`);}}).catch(err => {console.error('err: ', err);});})();
        
        • dropbox に上げる
        • 共有リンク作る
        • リンクコピーする
        • 登録したブックマークレット 2,3 回押す

        で対応することに。改善したい気持ちはある

        スマホ対応 #

        自分用のドキュメントを載せるためのブログにしようと思っていたので PC 対応だけで良いかと思いつつも、見栄が出てスマホ対応もすることにしました。

        もうだいぶ実装が面倒くさくなってきたので、とりあえず width: 600px 以下のデザインを変更することで完了としました。

        @media screen and (max-width: 600px) {
        

        sitemap 対応 #

        yarn add @nuxtjs/sitemap
        
          modules: [
            '@nuxtjs/sitemap',
          ],
        
          sitemap: {
            path: '/sitemap.xml',
            hostname: 'https://ohmg.tokyo',
            exclude: [],
            routes: routes,
          },
        

        robots.txt 作成 #

        static ディレクトリに作成

        User-agent: *
        Sitemap: https://ohmg.tokyo/sitemap.xml
        

        その他 #

        • style 関連のエラーがつまづいたけど、sass と sass-loader 入れたらなんとかなった
        • filesystem をクライアント側から触る方法が分からなくてすごいつまづいたし、かなり強引な解決方法になってしまった
        • Contentful も途中から視野に入ってきたけど、依存するプラットフォームを減らしたかったのでやめたけど、時期にまた移行するかもしれない(強引な解決方法がやはり気になる、、、)
        • nuxt/content 最初から使えばよかった説が、、、markdownit みたいな拡張性がない可能性がある点が懸念点かも(単純に調査不足かもしれない)

        参照リンク #

        • Nuxt.js で Markdown なブログのつくりかた - 俺の外付け HDD
        • markdown-it の導入方法 - ROXX(旧 SCOUTER)開発者ブログ
        • Sidebar | Buefy
        • Typography helpers | Bulma: Free, open source, and modern CSS framework based on Flexbox
        • highlight.js を利用して本ブログのコードをハイライト表示する
        • tsconfig.json の全オプションを理解する(随時追加中) - Qiita
        • Variables | Bulma: Free, open source, and modern CSS framework based on Flexbox
        • markdown-it で外部へのリンクのみ target="_blank"する方法 - Izm Log
        • Create a Blog with Nuxt Content - NuxtJS