KAEDE Hack blog

JavaScript 中心に ライブラリなどの使い方を解説する技術ブログ。

Next.js Tutorial -- .md files の ヘッダーデータ のリスト表示と CSS

starter

nextjs.org

f:id:kei_s_lifehack:20210102114417p:plain

ブラウザキャッシュをクリアして en/ に飛ばされる問題を解決し、 空の next app を動かした

Hello Next

qiita.com

エディタを使うので VScode をセットアップする

pages/ に posts/first-post.js を作って

export default function FirstPost() {
  return (
    <div>
      <h1>
        First Post
      </h1>
    </div>
  )
}

これでだすと return の中身が pages/post/first-post にアクセスした時に出る

f:id:kei_s_lifehack:20210102121415p:plain

f:id:kei_s_lifehack:20210102121500p:plain


root から post/first-post にリンクする

import Head from 'next/head'
import Link from 'next/link'

export default function Home() {
  return (
    <div className="container">
      <Head>
        <title>Next.js Blog Tutorial</title>
      </Head>
      <h1>
        This is posts list page
      </h1>
      <h2>
        <Link href='/posts/first-post'>
          <a>
            READ MY FIRST POST
          </a>
        </Link>
      </h2>
    </div>
  )
}

f:id:kei_s_lifehack:20210102163907p:plain

  • first-post.js に root から飛べるようにリンクを置く

first post から root にもリンクする

import Link from 'next/link'
import Head from 'next/head'
export default function FirstPost() {
  return (
    <div>
      <Head>
        <title>
          First Post
        </title>
      </Head>
      <h1>
        This is my first post
      </h1>
      <h2>
        <Link href='/'>
          <a>
            Go to posts list
          </a>
        </Link>
      </h2>
    </div>
  )
}

f:id:kei_s_lifehack:20210102163735p:plain

  • これで root ( / ) からも posts/first-post からも相互的に飛べるようになった。

16:40


styling

スタイリングをする

nextjs.org

  • pages/ の外、 root 直下に components/ を作る
  • そこに layout.js をお

Layout を作る

export default function Layout({children}) {
  return (
    <div className='container'>
      {children}
    </div>
  )
}
  • 中身を container class のついた div でくくるだけの Layout を作って出す

FirstPost を Layout で wrap する

import Link from 'next/link'
import Head from 'next/head'
import Layout from '../../components/layout'
export default function FirstPost() {
  return (
    <Layout>
      <Head>
        <title>
          First Post
        </title>
      </Head>
      <h1>
        This is my first post
      </h1>
      <h2>
        <Link href='/'>
          <a>
            Go to posts list
          </a>
        </Link>
      </h2>
    </Layout>
  )
}
  • その Layout を FirstPost で使う

f:id:kei_s_lifehack:20210102165358p:plain

  • Chrome の divtool で確認すると div class container で wrap されてるのを確認できる

root も Layout で wrap する

import Head from 'next/head'
import Link from 'next/link'
import Layout from '../components/layout'

export default function Home() {
  return (
    <Layout>
      <Head>
        <title>posts list</title>
      </Head>
      <h1>
        This is posts list page
      </h1>
      <h2>
        <Link href='/posts/first-post'>
          <a>
            READ MY FIRST POST
          </a>
        </Link>
      </h2>
    </Layout>
  )
}
  • スタイリングを統一するために root も Layout でくくる。

17:00

Layout の module を作る

いい感じでカードにしたいから自分で考える

Tutorial のはレスポンシブにならない中央に寄らない

kei-s-lifehack.hatenablog.com

前試したこれを使う

  • components/layout.module.css を作成
.wrapper {
  display: grid;
  height: 100vh;
  margin: 0;
  place-items: center center;
}
  • ど真ん中に揃えるようにスタイリングする

Layout で layout.module.css の wrapper を使う

import styles from './layout.module.css'
export default function Layout({children}) {
  return (
    <div className={styles.wrapper}>
      {children}
    </div>
  )
}
  • layout.module.css を styles として一括して読み込む
  • その中の wrapper を読み込んで div の className にして children を wrap する

f:id:kei_s_lifehack:20210102171546p:plain

  • root が真ん中揃えにできた

f:id:kei_s_lifehack:20210102171622p:plain

  • FirstPost もできた

f:id:kei_s_lifehack:20210102171722p:plain

  • SP でも見えはしてる

文字の感覚がやばいけどな!!!!!!!

とりあえず一応の 全体レイアウトはできた!

17:20

global styling

さっきはコンテナ枠自体のスタイリングをした

今回は全てに共通するスタイリングをする

root の pages/ に _app.js を作成する

これが root なので真っ先に読み込まれることになる

export default function App( { Component, pageProps }) {
  return <Component {...pageProps}/>
}

ここに App コンポーネントを作り

1st 引数の Component の value ではない内部?に

2nd 引数の pageProps を入れる

tips spread attiributes

<HogeComponent hoge={hogeProps.hoge} />
<HogeComponent {...hogeProps} />

は同じ意味らしい

reactjs.org

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}

function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

App2 では props の中身を一度定義して、変数で Greeting に渡せる

global styling 続き

import '../styles/global.css'

export default function App( { Component, pageProps }) {
  return <Component {...pageProps}/>
}

先ほどの pages/_app.js/App で

styles/global.css を読み込むようにする

global.css を作る

a {
  color:#0070f3;
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}

リンクのところだけ、色を青にして、アンダーラインを無くして

マウスカーソルがホバーしてるときだけアンダーラインが出るようにする

_app をいじったので server を再起動すると

f:id:kei_s_lifehack:20210104121437p:plain

デフォルト

f:id:kei_s_lifehack:20210104121451p:plain

ホバー時

として css が反映されている

f:id:kei_s_lifehack:20210104121533p:plain

もちろん、さっきの post list のページだけでなく、 First Post のページでも反映されている。

共通 CSS なら layout.css.module.css の container に書けばいいと思ってしまうが、こちらは Layout コンポーネントを使わない場合でも適用されて欲しい、もっと汎用的なアプリ全体のスタイリングだと解釈する。

1/4 12:17

Polish CSS

省略

SSG

getStaticProps とは

Static Generation with and without Data - Pre-rendering and Data Fetching | Learn Next.js

export default function Home(props) { ... }

export async function getStaticProps() {
  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: ...
  }
}

使うコンポーネントの中で getStaticProps を使用し

その中で API や DB などからデータをとってきて

return で props に渡す事で

Essentially, getStaticProps allows you to tell Next.js:

“Hey, this page has some data dependencies

— so when you pre-render this page at build time,

make sure to resolve them first!”

Next.js に 「このページはデータの依存性があるから、事前描画をビルド時に するとき、先にそのデータの依存性を解決してくれ」

と伝えることができる

Insert Blog Data

Blog の記事データを挿入する。マークダウンで。

nextjs.org

/pages/posts/ とは別に、/posts/ をルートから作成する

その中に

pre-rending.md

---
title: 'Two Forms of Pre-rendering'
date: '2020-01-01'
---

Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.

- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.

Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.

ssg-ssr.md

---
title: 'When to Use Static Generation v.s. Server-side Rendering'
date: '2020-01-02'
---

We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.

You can use Static Generation for many types of pages, including:

- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation

You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.

On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.

In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.

の2つのデータを用意する

次にこの /posts/hogehoge.md のファイルを index.js で読み込めるようにする

titile, date, url(id) を表示するようにする

index.js で マークダウンファイルを読み込む

必要な npm ライブラリとして

npm i gray-matter で gray-matter を入れる

fs や path 自体は 既に Next.js のプロジェクトで入っている

このライブラリを使ったライブラリコンポーネントとして

/lib/posts.js を用意する

中身には

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

fs, path, gray-matter を持ってくる

このままだとfs が読み込めないので

package.json

  "browser": {
    "fs": false
  },

これを書く(なぜかは不明, 後回しにする)

const postsDirectory = path.join(process.cwd(), 'posts')
console.log('postsDirectory: ',postsDirectory)

postsDirectory として 現在のプロセスのファイル構造?に posts を足して表示する

index で lib/post を読み込み process.cwd() をみる

import { getSortedPostsData } from '../lib/posts'

export function getSortedPostsData() {

で宣言した関数は、デフォルトじゃないから

import { getSortedPostsData } from '../lib/posts'

で { } で囲って 持ってくる、覚えてる

読み込んだ時点でさっきの fileNames が動き

postsDirectory:  /posts

が ブラウザで index にアクセスすると console に表示される。

先ほどの lib/post で

console.log('process.cwd: ', process.cwd() );
const postsDirectory = path.join(process.cwd(), 'posts');
console.log('postsDirectory: ',postsDirectory);

こうして process.cwd() と それに 'posts' を join させた結果を見てみる

すると index では

posts.js?9754:5 process.cwd:  /
posts.js?9754:7 postsDirectory:  /posts

となる。

また今は root にあたる pages/index で使用したが

同じ ライブラリ、 lib/posts を

pages/posts/first-posts で使用する

import {getSortedPostsData} from '../../lib/'

すると console では

webpack-internal:///./lib/posts.js:12 process.cwd:  /
webpack-internal:///./lib/posts.js:14 postsDirectory:  /posts

となり、変わらず root が起点になっているので、ブラウザで読んでいる位置とは関係ないことがわかった

process.cwd() は起点の root の位置のみを表す。 単に文字列の / にしないのはドメインによって変化する可能性があるからだろう

getSortedPostsData() で マークダウンファイルを取得する

次に getSortedPostsData() , 並び替えられた投稿データを取得する関数を作

前半で 投稿たちのファイル名をとってパスに入れて

コンテンツをとってきて matter でパースする

最後に return のスコープのなかで 並び替えて return する。

export function getSortedPostsData() {
//
}

この中の処理を書いていく

まだ console.log では表示できない。

  const fileNames = fs.readdirSync(postsDirectory);

まずさっき取得した {root}/posts/ を fs を使って読み込み、配列を取得する

  const allPostsData = fileNames.map(fileName => {

今取得した配列を展開し、一つづつ fileName の単数として処理する

allPostsData と言う変数を作る。中身は

    const id = fileName.replace( /\.md$/, '');
    console.log('id: ', id);

fileName と言う 一つのマークダウンファイルの名前から行末の .md を 何もないものに置換

    const fullPath = path.join(postsDirectory, fileName);

変数 fullPath に最初に取得した root + /posts にそのファイル名を足したものを入れる

    const fileContents = fs.readFileSync(fullPath, 'utf8')

変数 fileContents では 今の fullPath から utf8 で fileContents を読み込む

    const matterResult = matter(fileContents);

matterResult では それに matter を通して、ヘッダー部分の情報を読み込んだものを返す

    return {
      id, ...matterResult.data
    }

そして allPostData の返り値として id, matterRult の data 、中身を返すようにする

  return allPostsData;

この後の日付での並び替えは一旦省略して、今の allPostData の結果を

getSortedPostsData() の返り値にする

この関数の全体はこうなった

export function getSortedPostsData() {
  const fileNames = fs.readdirSync(postsDirectory);
  console.table(fileNames);

  const allPostsData = fileNames.map(fileName => {
    const id = fileName.replace( /\.md$/, '');
    console.log('id: ', id);
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');
    console.table('fileContents: ', fileContents);
    const matterResult = matter(fileContents);
    console.table('matterResult', matterResult)
    return {
      id, ...matterResult.data
    }
  })

  return allPostsData;
}

これですぐ使えて結果が見えるわけではなく、getStaticProps に渡す必要がある

index で getStaticProps に渡して 事前生成してもらう

まず index で getStaticProps に渡す

getStaticProps は Next の基礎機能だからか、import しなくても入っている

Home の上部で getStaticProps を使用する

export async function getStaticProps() {
  const allPostsData = getSortedPostsData() 
  return {
    props: {
      allPostsData
    }
  }
}

先ほど Markdown ファイルたちから id と中身のセットの配列にパースした getSortedPosts を index の getStaticProps で props : { hoge } としてセットする

これでビルド時に先に読み込んでくれることになるはず

index/Home で データを展開する

export default function Home( { allPostsData } ) {

まず getStaticProps の props に渡した allPostsData を Home で引数に

( { hoge } ) の形で読み込む

return (
      {
        allPostsData.map( ( { id, date, title, } ) => (
          <h2 key={id}>
            title: {title}
            <br/>
            id: {id}
            <br/>
            date: {date}
            <br/>
          </h2>
      ))
      }
)

return 部分で allPostsData を展開し

( { hoge, hoge, hoge, } ) の形で中身の id, date, titile を使い

タグに挟み込む。

これで

f:id:kei_s_lifehack:20210113221956p:plain

CSS はおかしいけど、マークダウンのファイルたちの

ヘッダー部分のタイトル、ファイル名、ヘッダー部分の日付、

が index で表示できた。

中身のコンテンツの表示はこれから。

lib/posts でさっき出したもとを確認

  const allPostsData = fileNames.map(fileName => {
    const id = fileName.replace( /\.md$/, '');
    console.log('id: ', id);
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');
    console.log('fileContents: ', fileContents);
    const matterResult = matter(fileContents);
    console.log('matterResult', matterResult)
    return {
      id, ...matterResult.data
    }
  })

allPostsData はこうなっている。

path からコンテンツを読み取り、それを matter にかけてその data を渡している

id は fileName を変換しているが、それ以外の titile, date, は出てきてすらいない。

matter がよしなにやっているってことだろう。

読み込んだ元のファイルは

---
title: 'aaaa'
date: '2020-01-12'
---

ああああああああああ

こんな感じ。

この 「あああああああ」の部分に名前がない。

先に進んで詳細部分のページでみると

nextjs.org

export async function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // Use gray-matter to parse the post metadata section
  const matterResult = matter(fileContents)

  // Use remark to convert markdown into HTML string
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)
  const contentHtml = processedContent.toString()

  // Combine the data with the id and contentHtml
  return {
    id,
    contentHtml,
    ...matterResult.data
  }
}

remark を使って読み取るらしい。

matter では無理そう

さらに表示するときは

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
      <br />
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </Layout>
  )
}

危険に HTML の内部に入れるやつを使ってぶち込むようだ。 とりあえずまだ無理。

CSS デフォルトに

nextjs.org

見た目がダメなので、CSS を規定のにもどす

import styles from './layout.module.css'
export default function Layout({children}) {
  return (
    <div className={styles.container}>
      {children}
    </div>
  )
}

Layout.js で container に当てるようにして

.container {
  max-width: 36rem;
  padding: 0 1rem;
  margin: 3rem auto 6rem;
}

layout.module.css

幅の最大を 36rem

文字と箱の枠の幅を 0, 高さを 1rem,

箱とその外の枠の、 上を 3rem, 幅を自動中央揃え、下を 6rem,

にする

f:id:kei_s_lifehack:20210114211844p:plain

これである程度マシになった。

ヘッダー部分の CSS を作る

.container {
  max-width: 36rem;
  padding: 0 1rem;
  margin: 3rem auto 6rem;
}

.header {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.headerImage {
  width: 6rem;
  height: 6rem;
}

.headerHomeImage {
  width: 8rem;
  height: 8rem;
}

.backToHome {
  margin: 3rem 0 0;
}

コンテナの中のヘッダー部分のレイアウトの CSS を追加する

ヘッダー全体 では flex の上から流すレイアウトで 縦の中央揃えをする

ヘッダーの画像は 幅も高さも 6rem

ホームでのヘッダー部分の画像は 幅も高さも 6rem に

リスト表示からホームに戻るリンクのマージンは 上 3rem 他 0

にする

サイズ別の CSS を作る

nextjs.org

sytles/ に utils.module.css

機能たちのモジュール CSS を作る

.heading2Xl {
  font-size: 2.5rem;
  line-height: 1.2;
  font-weight: 800;
  letter-spacing: -0.05rem;
  margin: 1rem 0;
}

まずは 2XL サイズの ヘッディング。h1, h2 のカスタムみたいなものだろう

文字サイズが 2.5rem,

行と行の感覚が 1.2 倍

フォントの太さが 800, 1.3 倍くらい

文字と文字の空白を 0.05 rem つめる

マージンは 左右 1rem で上下は 0

.headingXl {
  font-size: 2rem;
  line-height: 1.3;
  font-weight: 800;
  letter-spacing: -0.05rem;
  margin: 1rem 0;
}

XL では

文字サイズが 2XL の 2.5 から2.0 に

行間が 1.2 から 1.3 に

文字の太さ、文字と文字の間隔、マージン、は同じスタイル

を適用

.headingLg {
  font-size: 1.5rem;
  line-height: 1.4;
  margin: 1rem 0;
}

Large では

サイズが 1.5

行間が 1.4

文字の太さ、文字の間隔、を通常の大きさにして

マージンは同じスタイルを適用する

.headingMd {
  font-size: 1.2rem;
  line-height: 1.5;
}

Medium では

1.2rem size

1.5 height

.borderCircle {
  border-radius: 9999px;
}

.colorInherit {
  color: inherit;
}

.padding1px {
  padding-top: 1px;
}

.list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.listItem {
  margin: 0 0 1.25rem;
}

.lightText {
  color: #999;
}

あとは

角を丸める

親と同じ色を使う

1px 上から pdg とる

リストの丸点をなくし、mgn, pdg, を全て 0 にする

リストの一つ一つの下の mgn のみ 1.25rem 空ける

グレーの文字色にする

の細かい スタイルをコピペした

Layout に ヘッダーとメタデータを入れる

import styles from './layout.module.css'
export default function Layout({children}) {
  return (
    <div className={styles.container}>
      {children}
    </div>
  )
}

children として受け取った コンポーネントに コンテナの クラス名がついた div で包むだけだった Layout から変更する

新しい Layout では

import Head from 'next/head'
import Link from 'next/link'

import styles from './layout.module.css'
import utilStyles from '../styles/utils.module.css'
  • Head, Link の Next 基礎機能を import
  • Layout の基本 スタイリングを layout.module.css から import
  • サイズ別などの 便利 スタイリングを utils.module.css から import
const name = 'Your Name'
export const siteTitle = 'Next.js Sample Website'

header に表示する 著者の名前を定義

サイト自体のタイトルを 定義して (別ページでも使えるように?) export

Head, header, main, Link fo Home, を定義

export default function Layout({children, home}) {
  return (
    <div className={styles.container}>
      <Head>
      </Head>
      <header>
      </header>
      <main>
        {children}
      </main>
      <Link href='/'>
        <a>BACK TO HOME</a>
      </Link>
    </div>
  )
}

基礎的な枠を先に作る

まず div コンテナで全体を包む

メタデータを入れるために Head を置く

ブログ情報を入れるための header を置く これは home の内容のあるなしで表示を分岐させる

本文を入れるための main を置く

home がある場合のみ現れる、root へのリンクを置く

Head の中身を書く

realfavicongenerator.net

この辺りで ヘッダーの丸アイコン兼 ブラウザのタブアイコンを作る

        <link rel="icon" href="/favicon.ico" />
        <meta name='description' content='nextjs trial blog'/>
        <meta 
          property='og:image'
          content={`https://picsum.photos/400/250`}
        />
        <meta name='og:titile' content={siteTitle}/>
        <meta name='twitter:card' content='summary_large_image'/>
  • Next の Link ではなく、小文字の link で favicon のリンクを貼る
  • 説明として Next の練習ブログと記載
  • OG 画像として 400x250 の適当な画像を読み込む
  • OG タイトルとしてサイトタイトルを記載
  • Twitter カードとして 大きい画像と 要約を記載

この次に Header 情報を書く。どのページにも表示されているプロフとか書くとこだと思う

f:id:kei_s_lifehack:20210115211347p:plain

私のブログで言うとここのこと

header を作る

layout.js の先ほどの の下に書く

      <header>
        {home? (
          <>
            <img 
              src='/images/profile.jpeg'
              className={`
                ${styles.headerHomeImage}
                ${utilStyles.borderCircle}
              `}
              alt={name}
            />
            <h1 className={utilStyles.heading2Xl}>
              {name}
            </h1>
          </>
        ) : (
          <div>hoge</div>
        )}
      </header>

引数の home コンポーネントがある場合は

  • プロフ画像を ヘッダーの画像用途として layout css で 8 rem x 8rem にして
  • 円にするときの便利 util CSS を使って 円にして
  • 便利 CSS で 2XL サイズの h1 にして
  • プロフィールの名前を出す

んで home がなければ違うものを出すって処理だな

index で <Layout home> とすることで

f:id:kei_s_lifehack:20210116222421p:plain

<Layout> だけだと

f:id:kei_s_lifehack:20210116222458p:plain

その心は Layout コンポーネントに 「これは Home (ブログのホーム画面) だから 著者の画像とタイトル出してあげてね」って伝えるってことかなあ

次の記事に続く