Gatsby程序化產生頁面


文章目錄

  1. Gatsby初試啼聲
  2. 利用react-bootstrap打造React風格頁面
  3. 了解Gatsby中GraphQL如何運作
  4. Gatsby GraphQL讀取JSON
  5. 在Gatsby GraphQL中組合出完美資料
  6. Gatsby程序化產生頁面
  7. 上線個人頁面到Netlify+啟動Netlify CMS

在上一篇文章把成果展示相關的頁面以及元件做好之後,個人頁面其實已經初步完成了。不過在成果展示的頁面只有案例的名稱以及簡單的敘述而已,如果有人對一個案例感到有興趣,想要了解更多就沒辦法了。所以接下來我會仿照我們既有的部落格系統,為作品集打造詳細資訊頁面。

觀察部落格系統

首先我們也一樣觀察一下部落格系統背後產生頁面的原理。
gatsby-node.js

const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions

  const blogPost = path.resolve(`./src/templates/blog-post.js`)
  return graphql(
    `
      {
        allMdx(
          sort: { fields: [frontmatter___date], order: DESC }
          limit: 1000
        ) {
          edges {
            node {
              fields {
                slug
              }
              frontmatter {
                title
              }
            }
          }
        }
      }
    `
  ).then(result => {
    if (result.errors) {
      throw result.errors
    }

    // Create blog posts pages.
    const posts = result.data.allMdx.edges

    posts.forEach((post, index) => {
      const previous = index === posts.length - 1 ? null : posts[index + 1].node
      const next = index === 0 ? null : posts[index - 1].node

      createPage({
        path: `blog${post.node.fields.slug}`,
        component: blogPost,
        context: {
          slug: post.node.fields.slug,
          previous,
          next,
        },
      })
    })

    return null
  })
}

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === `Mdx`) {
    const value = createFilePath({ node, getNode })
    createNodeField({
      name: `slug`,
      node,
      value,
    })
  }
}

配合官方的教學文件,我們可以知道

  • onCreateNode是產生節點+頁面路徑的步驟。
  • createPage則是根據onCreateNode產生出來的頁面路徑,建立頁面的地方。

所以我們只要能夠完成以下任務就成功了

  1. onCreateNode幫JSON裡的每一筆資料產生頁面路徑
  2. createPage也產生頁面

讓GraphQL從JSON產生頁面

第一步先看看JSON的node.internal.type會是長什麼模樣。加入console.log()後重啟Gatsby。

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions
  console.log(node.internal.type);
  ...
}

結果發現JSON的類型是WorkJson,而我們新做出的客製化節點是WorkProject。接下來我們可以加入判斷式,讓他在只有在WorkProject的時候才會製造節點。

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === `Mdx`) {
    const value = createFilePath({ node, getNode })
    createNodeField({
      name: `slug`,
      node,
      value,
    })
  }
  if (node.internal.type === `WorkProject`) {
    const value = '/' + node.title.split(` `)
      .map(elem => elem.toLowerCase())
      .join(`-`)
    createNodeField({
      name: `slug`,
      node,
      value,
    })
  }
}

對於Mdx而言,他的每一個檔名都不一樣,所以可以使用createFilePath來製作出不一樣的字串,但是JSON資料都在同一個檔案內,沒辦法用檔名,所以我們拿資料裡面的title來做每筆資料不重複的路徑。
執行以下query就可以看到我們已經幫每個案例製作出路徑了。

query SlugQuery {
  allWorkProject {
    nodes {
      fields {
        slug
      }
    }
  }
}

下一步我們來建立頁面。

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions

  const blogPost = path.resolve(`./src/templates/blog-post.js`)
  const workProject = path.resolve(`./src/templates/work-project.js`)
  return graphql(
    `
      {
        allMdx(
          sort: { fields: [frontmatter___date], order: DESC }
          limit: 1000
        ) {
          edges {
            node {
              fields {
                slug
              }
              frontmatter {
                title
              }
            }
          }
        }
        allWorkProject {
          edges {
            node {
              fields {
                slug
              }
            }
          }
        }
      }
    `
  ).then(result => {
    if (result.errors) {
      throw result.errors
    }

    // Create blog posts pages.
    const posts = result.data.allMdx.edges
    const projects = result.data.allWorkJson.edges

    posts.forEach((post, index) => {
      const previous = index === posts.length - 1 ? null : posts[index + 1].node
      const next = index === 0 ? null : posts[index - 1].node

      createPage({
        path: `blog${post.node.fields.slug}`,
        component: blogPost,
        context: {
          slug: post.node.fields.slug,
          previous,
          next,
        },
      })
    })

    projects.forEach((project) => {
      createPage({
        path: `work${project.node.fields.slug}`,
        component: workProject,
        context: {
          slug: post.node.fields.slug,
        },
      })
    })

    return null
  })
}

這裡仿照部落格產生頁面的流程即可。中間有一些額外資訊不需要就去除了。
不過先不要急著重啟Gatsby,因為我們需要跟部落格一樣,建立頁面模板讓他從模板產生。在src/templates新建work-project.js
src/templates/work-project.js

import React from "react";
import { graphql } from "gatsby";

import Layout from "../components/layout";
import SEO from "../components/seo";

class WorkProjectTemplate extends React.Component {
  render() {
    const project = this.props.data.workJson

    return (
      <Layout>
        <SEO
          title={project.title}
          description={project.description}
        />
        // 待做
      </Layout>
    )
  }
}

export default WorkProjectTemplate

export const pageQuery = graphql`
  query WorkProjectBySlug($slug: String!) {
    workProject(fields: { slug: { eq: $slug } }) {
      title
      description
      image {
        childImageSharp {
          fluid {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }
  }
`

加入案例詳細頁面資料

除了原本的標題、簡介、圖片之外,我們可以規劃一下在詳細頁面要顯示什麼額外的資訊。一樣也可以按照個人需求更改。
我會再加入:

  • 運用的技術、框架
  • 詳細介紹文字

content/data/work.json

[
  {
    "title": "XChange",
    "description": "The redesigned media package website for XChange.",
    "image": "xchange.png",
    "technology": ["VueJS", "Bootstrap"],
    "url": "http://xchange.com.tw/",
    "body": "<p>A website originally designed by some friends of XChange founder Theon, with the intention of it becoming another information channel. However, it turned out that Facebook was still the preferred channel to update such information, so plans were made to revamp this website. While redesigning the website to align more with a modern style, the new purpose of the website was also to serve as a media package, which would aid members in introducing XChange to potential partners.</p><p>With the help of two XChange members as a designer and a PM, the website was completed within 2 months. Since the old website was built on VueJS, it was more efficient to continue building under the same framework. Therefore, I learned how to write in VueJS and utilize existing plugins, such as vue-i18n.</p>"
  },
  {
    "title": "Stihl Taiwan",
    "description": "The revamped brand website for Stihl Taiwan.",
    "image": "stihl.png",
    "technology": ["React", "React Redux", "NodeJS", "Strapi"],
    "url": "http://www.stihl.com.tw/",
    "body": "<p>Melchers Trading GmbH (Taiwan Branch) had been the exclusive partner of well-known German brand Stihl for several years in Taiwan. With Stihl gaining brand reputation over time, it was time for a brand website overhaul to expand business going forward.</p><p>After meeting up with the client, it was clear that the website needed a CMS system for the client to manage information on the website, and an API for the website to interact with. We successfully accomplished the mission with the help of the open source NodeJS CMS <a href='https://strapi.io/'>Strapi</a> and a bit of React Redux.</p>"
  },
  {
    "title": "LinkMyGoods",
    "description": "LinkMyGoods is a CRM system designed to assist trading companies.",
    "image": "linkmygoods.png",
    "technology": ["Vanilla JS", "dc.js", "crossfilter", "d3.js", "SASS", "PHP", "MySQL"],
    "url": "https://www.linkmygoods.com/",
    "body": "<p>LinkMyGoods is a CRM system tailored to the needs of a trading company. With LinkMyGoods, every step of the trading process, from quotations, orders, shipment handling, to project management, can be completed with ease.</p><p>Having worked 3 years in Celinius on LinkMyGoods, I have <ul><li>developed new modules, including but not limited to: payment term configuration tool, auto SKU generation tool, order approval system, and more</li><li>revamped the layout of project management tool</li><li>built an interactive statistics chart with <a href='https://github.com/dc-js/dc.js'>dc.js</a>, a charting tool based on <a href='http://crossfilter.github.io/crossfilter/'>crossfilter</a> and <a href='https://d3js.org/'>d3.js</a></li></ul>Since our team uses an in-house library, I also have excellent understanding of vanilla javascript.</p>"
  }
]

這裡我們詳細說明寫HTML控制格式。

如果有無法資料/圖片無法更新的狀況,可以試著使用gatsby clean && gatsby develop把暫存清掉。

多了新的欄位我們也需要修改前一篇文章裡製作的節點架構,以及相連接的work-project模板。
gatsby-node.js

exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
  work.forEach((project, index) => {
    const {
      ...
      technology,
      url,
      body,
    } = project;

    ...

    const node = {
      ...
      technology,
      url,
      body,
      ...
    };

  });
};

src/templates/work-project.js

export const pageQuery = graphql`
  query WorkProjectBySlug($slug: String!) {
    workProject(fields: { slug: { eq: $slug } }) {
      title
      description
      technology
      url
      body
      image {
        childImageSharp {
          fluid {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }
  }
`

有了資料之後,接著簡單做一下模板的界面。

import styled from "styled-components";
import Img from "gatsby-image";
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Badge from 'react-bootstrap/Badge';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

...

const WrapperWithIcon = styled.div`
  padding-left: 30px;
  margin-bottom: 15px;
  position: relative;
  display: flex;
  flex-wrap: wrap;
  & > svg {
    position: absolute;
    left: 0;
    top: 5px;
  }
  & .badge {
    margin-right: 9px;
    margin-bottom: 9px;
    font-size: 1rem;
  }
`;

class WorkProjectTemplate extends React.Component {
  render() {
    const project = this.props.data.workProject;

    return (
      <Layout>
        <SEO
          title={project.title}
          description={project.description}
        />
        <Row>
          <Col>
            <h2 className="mb-4">{project.title}</h2>
            <Img
              fluid={project.image.childImageSharp.fluid}
              alt={project.title}
            />
          </Col>
        </Row>
        <Row className="mt-4">
          <Col md={3}>
            <WrapperWithIcon className="project-url">
              <FontAwesomeIcon icon="link" />
              <a href={project.url} >{project.url}</a>
            </WrapperWithIcon>
            <WrapperWithIcon className="project-technology">
              <FontAwesomeIcon icon="layer-group" />
              {project.technology.map(elem => <Badge variant="secondary" key={elem}>{elem}</Badge>)}
            </WrapperWithIcon>
          </Col>
          <Col md={9} dangerouslySetInnerHTML={{ __html: project.body }} />
        </Row>
      </Layout>
    )
  }
}

因為在這裡也想要使用react-fontawesome,所以之前在src/components/socialmedia.js所做的相關事情要搬到src/pages/index.js
程式碼移動後,再加上faLink以及faLayerGroup圖示。詳情可參考fontawesome官方文檔

npm i --save @fortawesome/free-solid-svg-icons

src/pages/index.js

import { library } from '@fortawesome/fontawesome-svg-core';
import { fab } from '@fortawesome/free-brands-svg-icons';
import { faLink, faLayerGroup } from '@fortawesome/free-solid-svg-icons';
...
library.add(
  fab,
  faLink,
  faLayerGroup
);
...

實際效果如下:

作品集加上頁面連結

我們已經做好了各個案例的頁面,但是卻沒有連結。我們回到之前做的Portfolio裡面加上新增頁面的連結。
src/components/portfolio.js

...
                      <Card.Body>
                        <Card.Title>
                          <Link to={`work${work.fields.slug}`}>
                            {work.title}
                          </Link>
                        </Card.Title>
                        <Card.Text>{work.description}</Card.Text>
                      </Card.Body>
...

const portfolioQuery = graphql`
  query WorkQuery {
    ...
        fields {
          slug
        }
    ...
  }
`;

這樣就可以透過作品集到達頁面了。

本篇文章的程式碼的實作可以在github repo中 6-programmatically-create-page 找到

參考資源

#React #gatsby #graphql







你可能感興趣的文章

DAY8:Odd or Even?

DAY8:Odd or Even?

關於 Fetch API

關於 Fetch API

[進階 js 07] Temporal Dead Zone

[進階 js 07] Temporal Dead Zone






留言討論