Strona główna / Artykuły / Gatsby - Dynamiczne generowanie stron

Gatsby - Dynamiczne generowanie stron

Czas zacząć pracę z danymi! W tej części tworzymy artykuły w oparciu o Markdown.

Olaf Sulich

2020-11-23

Na początku wyjaśnijmy sobie czym jest Markdown. Markdown to język znaczników przeznaczony do formatowania tekstu. Możesz go kojarzyć chociażby z README na GitHubie. Jego konstrukcja jest bardzo prosta i przyjemna w użyciu, spójrzmy na przykład:

Pogrubiony tekst - **bold**
Nagłówek pierwszego stopnia - # Nagłówek 
Link - [Google](https://google.com)

Nie marnujmy czasu i stwórzmy folder content/ w katalogu głównym. W nim dodajemy również foldery images/ oraz posts/. W tym drugim utwórzmy plik first-post.md.

---
title: "My awesome first post"
slug: "first-post"
date: "20-08-2020"
category: "Gear"
image: "../images/light.png"
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Podczas tworzenia artykułów w Markdownie mamy możliwość załączenia dodatkowych informacji w postaci kluczy i wartości. Ta konstrukcja nazywana jest frontmatterem. Będziemy mogli później, za pomocą GraphQL, wyciągnąć te informacje.

Podajemy tutaj tytuł, slug, czyli ścieżkę pod którą ma się wyświetlać artykuł, datę, kategorię i zdjęcie.

Wszystkie zdjęcia znajdziesz w repozytorium na GitHubie.

Konfiguracja

Artykuł gotowy, to teraz przyszedł czas, żeby odczytać z niego treść. Zanim to jednak zrobimy zainstalujmy specjalny plugin, który nam to umożliwi:

npm install gatsby-transformer-remark

Przejdźmy do naszego pliku konfiguracyjnego i dodajmy plugin:

const path = require(`path`);

module.exports = {
  siteMetadata: {
    title: "Photo blog",
  },
  plugins: [
    `gatsby-plugin-sharp`,
    `gatsby-transformer-sharp`,
    `gatsby-transformer-remark`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: path.join(__dirname, `src/assets/images`),
      },
    },
    {
      resolve: `gatsby-plugin-sass`,
      options: {
        postCssPlugins: [
          require("tailwindcss"),
          require("./tailwind.config.js"),
        ],
      },
    },
  ],
};

Ten plugin parsuje nasze pliki markdown używając procesora Remark. Ostatnią rzeczą, którą musimy zrobić, żeby odczytać artykuł, jest poinformowanie Gatsbiego skąd ma czerpać dane. Robiliśmy to już wcześniej zaczytując obrazki. W tym celu wykorzystujemy plugin gatsby-source-filesystem, a w nim podajemy ścieżkę do naszych artykułów:

const path = require(`path`);

module.exports = {
  siteMetadata: {
    title: "Photo blog",
  },
  plugins: [
    `gatsby-plugin-sharp`,
    `gatsby-transformer-sharp`,
    `gatsby-transformer-remark`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: path.join(__dirname, `src/assets/images`),
      },
    },
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `content`,
        path: path.join(__dirname, `content`),
      },
    },
    {
      resolve: `gatsby-plugin-sass`,
      options: {
        postCssPlugins: [
          require("tailwindcss"),
          require("./tailwind.config.js"),
        ],
      },
    },
  ],
};

GraphQL i Markdown

Odpalmy serwer developerski i przejdźmy na http://localhost:8000/___graphql. Jeśli wszystko poszło po naszej myśli, to powinniśmy zauważyć nowe pozycje w menu zapytań - allMarkdownRemark i markdownRemark.

Załóżmy, że chcemy pobrać wszystkie artykuły, mamy tutaj wiele opcji, ale nas na ten moment interesują dane z frontmattera i oczywiście sama treść.

Wykonajmy zapytanie:

{
  allMarkdownRemark {
    edges {
      node {
        frontmatter {
          category
          date
          slug
          title
        }
        html
      }
    }
  }
}

W odpowiedzi dostajemy interesujące nas informacje:

Zapytanie w GraphiQL o dane z pierwszego artykułu

Dynamiczne generowanie stron

W poprzednich artykułach z tej serii poznałaś/eś specjalne pliki dostarczane przez Gatsbiego - gatsby.config.js i gatsby-browser.js. Przyszedł czas na kolejny - gatsby-node.js. To w nim będziemy dynamicznie tworzyć statyczne strony.

Żeby wygenerować strony potrzebujemy wyeksportować specjalną, asynchroniczną funkcję createPages:

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

Jak widzisz mamy tutaj dostęp do graphql, którego wykorzystywaliśmy wcześniej przy zapytaniach i obiektu actions. Skorzystajmy z tego pierwszego i wykonajmy zapytanie, które zwróci slug naszego artykułu:

exports.createPages = async ({ graphql, actions }) => {
 
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            frontmatter {
              slug
            }
          }
        }
      }
    }
  `);

  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    const { slug } = node.frontmatter;
    console.log(slug);
  });

};

Jeśli zrestartujesz teraz serwer developerski, zobaczysz w konsoli slug dla każdego stworzonego artykułu, w naszym przypadku będzie on tylko jeden.

Lećmy dalej, z wspomnianego wcześniej obiektu wyciągnijmy akcję createPage. Przyjmuje ona obiekt z opcjami, do którego podajemy ścieżkę dla strony, komponent, na którego podstawie ma się zbudować strona oraz kontekst.

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

  const { createPage } = actions;
  
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            frontmatter {
              slug
            }
          }
        }
      }
    }
  `);

  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    const { slug } = node.frontmatter;

    createPage({
      path: slug,
      component: null,
      context: {
        slug,
      },
    });

  });

};

Szablon dla strony

Każda dynamicznie generowana strona potrzebuje swojego szablonu. Stwórzmy więc nasz komponent w katalogu templates . Nazwiemy go postTemplate.

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

export const query = graphql`
  query($slug: String!) {
    markdownRemark(frontmatter: { slug: { eq: $slug } }) {
      frontmatter {
        title
      }
    }
  }
`;

const PostTemplate = ({ data }) => {
  return <h1>{data.markdownRemark.frontmatter.title}</h1>
};

export default PostTemplate;

Wyjaśnimy co się tutaj stało. W szablonach, tak samo jak na stronach, korzystamy z zapytań GraphQL eksportując je.

W tym zapytaniu pojawiło się jednak coś nowego - zmienne. Zmienną w GraphQL poprzedzamy znakiem dolara i po dwukropku podajemy jej typ. W naszym przypadku jest to zmienna $slug o typie String!. W markdownRemark wyszukujemy artykuł, którego slug z frontmattera będzie taki sam jak $slug z naszej zmiennej. Następnie wybieramy tytuł artykułu z frontmattera i renderujemy go w komponencie.

Wróćmy do gatsby-node.js i wykorzystajmy stworzony template.

const path = require("path");

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

  const { createPage } = actions;
  const pageTemplate = path.resolve(`./src/templates/postTemplate.js`);

  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            frontmatter {
              slug
            }
          }
        }
      }
    }
  `);

  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    const { slug } = node.frontmatter;

    createPage({
      path: slug,
      component: pageTemplate,
      context: {
        slug,
      },
    });

  });

};

Szablon podajemy do funkcji createPage. Wspominałem Ci wcześniej o kontekście, to właśnie on definiuje jaką zmienną podamy do naszego szablonu! Jest on łącznikiem pomiędzy naszą funkcją a szablonem.

Jeśli zrestartujemy teraz serwer developerki i przejdziemy na http://localhost:8000/first-post, to powinniśmy zobaczyć wygenerowaną dynamicznie stronę:

Dynamicznie wygenerowana strona dla pierwszego artykułu

Dodajmy brakujące dane i nadajemy podstawowe style dla naszego template'u:

import React from "react";
import Img from "gatsby-image";
import styles from "./postTemplate.module.scss";
import { graphql } from "gatsby";

export const query = graphql`
  query($slug: String!) {
    markdownRemark(frontmatter: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
        date
        category
        image {
          childImageSharp {
            fixed(quality: 100, width: 200, height: 200) {
              ...GatsbyImageSharpFixed_tracedSVG
            }
          }
        }
      }
    }
  }
`;

const PostTemplate = ({ data }) => {
  const { frontmatter, html } = data.markdownRemark;
  const { image, category, title, date } = frontmatter;
  return (
    <article className="mx-auto flex flex-col w-full justify-center items-center my-24 max-w-3xl">
      <div className="flex w-full justify-start	items-start my-24">
        <Img
          className="object-cover object-center rounded-lg"
          fixed={image.childImageSharp.fixed}
          alt=""
        />
        <div className="flex flex-col m-3 mx-6 justify-start	items-start">
          <span className="text-blue-600">{category}</span>
          <h1 className="text-3xl">{title}</h1>
          <span>{date}</span>
        </div>
      </div>
      <div
        className={styles.markdown}
        dangerouslySetInnerHTML={{ __html: html }}
      />
    </article>
  );
};

export default PostTemplate;

I gotowe!

Ostylowana, dynamicznie wygenerowana strona dla pierwszego artykułu

Praktyka czyni mistrza

Przygotowałem dla Ciebie małe zadanie, w ramach którego utrwalisz sobie zdobytą dotychczas wiedzę. Twoim zadaniem będzie przygotowanie listy artykułów na stronie głównej. Moja implementacja wygląda w następujący sposób:

Sekcja artykułów na stronie głównej

Oczywiście możesz tutaj eksperymentować, na przykład stworzyć osobną stronę i na niej wyświetlić wszystkie posty, wszystko zależy od Ciebie! Jeśli napotkasz na jakieś problemy lub po prostu nie chcesz wykonywać tego zadanie, zawsze możesz skorzystać z rozwiązania dostępnego w repozytorium na GitHubie.