Gatsby, TypeScript, Emotion, Tailwind, MDXでブログを作っている

**この記事は[Gatsby.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/gatsby) 23日目の記事です。**
2時間ぐらい納期過ぎました。すみません。

次の記事は@kazuooooo さんの [**まるで名刺のようなポートフォリオサイトを作れるCardfolio!を公開しました！**](https://qiita.com/kazuooooo/items/311f1e562c47a8327551) です。

# これは

フロントエンド職に就きたいのでReact, TypeScriptの勉強にGatsbyでブログを作ってみた記事です。
ブログがどんな感じで作られてるのか、ライブラリ, プラグイン, ツールの紹介も交えながらかんたんに解説していきます。

動いてる実物は[**ここ**](https://kkrn.me/)から見れます。ソースは[**ここです**](https://github.com/kkrnme/pages-source)。


# 使うもの


- [**Gatsby**](https://gatsbyjs.org)
- [**TypeScript**](https://www.typescriptlang.org/)

#### スタイリングに関するもの

- [**Emotion**](https://emotion.sh/)
- [**TailwindCSS**](https://tailwindcss.com/)
- [**PostCSS**](https://postcss.org/)
- [**PurgeCSS**](https://www.purgecss.com/)

#### ブログの記事を書くのに使うもの

- [**MDX**](https://mdxjs.com/)


# Gatsbyって?

ReactとGraphQLを使って高速なサイトを作るフレームワークです。今回の文脈上は__SSG(Static Site Generator: 静的サイトジェネレータ)__ですが、SSG以外でも使えるそうです。

Gatsbyの具体的な仕組みについては、先人の皆さんの記事を読むと理解が深まると思います。

### [Reactの最強フレームワークGatsby.jsの良さを伝えたい！！ ](https://qiita.com/hppRC/items/00739eaf9ae7fc95c1ca)(Gatsby.js Advent Calendar 2019 1日目)
> - **脳死でサイトが作れる簡単さ & サーバを意識しなくていい！**
> - **生成されたサイトがSPA & サイトがとにかく早い**
> - **プラグインによって導入が超速 -> めんどくさいことをしなくていい**
> - **初心者がとっつきやすい**
>
> ぜひ節目ごとに「Gatsby最高！」と叫びながら読んでみてください。気持ちが高まります。
> 叫びたくないですか？では私が代わりに叫びます。
> **Gatsby最高！！！！！**



---
### [Reactベース静的サイトジェネレータGatsbyの真の力をお見せします](https://qiita.com/uehaj/items/1b7f0a86596353587466)

> 自分が当初Gatsbyについて理解できていなかったのは、Reactとの関係です。

> 静的サイトジェネレータというぐらいだから、GatsbyはReactコードをSSR(Server Side Rendering)のようにあらかじめレンダリングして、静的なHTMLを事前に生成するのかな、と思いました。だから、JSコードはビルド時のみに実行されて、閲覧時には実行されないのかな、と。
(中略)
> しかし、調べると、そうではありませんでした。
> **GatsbyはSSR的な静的HTML生成に加えて、それと連動する通常のReactアプリも生成します**。
Create React AppやNext.jsと同様に、**Reactで開発するSPAの自由度を完全に具備する**ものです。










# プロジェクトを作る

```
npx gatsby new hogefuga https://github.com/gatsbyjs/gatsby-starter-hello-world
```

*starter*というのはプロジェクトのテンプレートのようなもので、余計な環境構築を飛ばすのに便利です。
`gatsby-starter-hello-world`はその名の通りHello, worldを表示するstarterであり、最もシンプルなstaterでもあります。

## Hello, worldする

```
npm start
```

`localhost:8000`にアクセスしましょう。Hello, worldできました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411269/36078ee1-c00c-2e86-1010-9fb24073c37c.png)

## LAN上から見る

`npm start`(`gatsby develop`)が走っている間は`localhost:8000`からサイトを見ることができます。
ところで、__サイトをスマホから見れたら、デザインの確認に便利じゃないですか？__
__やりましょう。__

```json:package.json
{
  "scripts":{
    "develop": "gatsby develop --host 0.0.0.0",
  }
}
```

`"scripts"`の中の`"develop"`というスクリプトに`--host 0.0.0.0`という文言を付け加えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411269/cf910059-b7d9-880d-a7eb-581158f7a593.png)
こんな感じの表示(`192.168.1.xx`)が出たら成功です。LAN上の端末からアクセスしてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411269/b2e55d34-579c-d7d2-8b42-3c9dde9c6338.png)


# `gatsby-config.js`の書き方

Gatsbyの設定ファイルは`gatsby-config.js`です。`gatsby-starter-hello-world`の初期状態ではこんな内容です。

```js:gatsby-config.js
/**
 * Configure your Gatsby site with this file.
 *
 * See: https://www.gatsbyjs.org/docs/gatsby-config/
 */

module.exports = {
  /* Your site config here */
}
```

一番大事な`plugins`を作ってみます。プラグインを登録, 設定します。

```js:gatsby-config.js
module.exports = {
  plugins: [
    
  ]
}
```

`plugins`は配列です。要素はプラグイン名の`string`か, `{resolve: プラグイン名, options: オプションのオブジェクト}`という形になります。

# TypeScript化

筆者は(静的サイトだけに)**静的型の信者**であり「**TS化できるならTS化する**」というTypeScriptの型システム並に硬い意志を持っています。

```
npm i -S gatsby-plugin-typescript typescript ts-node @types/react @types/node @types/react-dom gatsby-plugin-graphql-codegen
```

- `gatsby-plugin-typescript`: GatsbyでTypeScriptを使うためのプラグインです。
- `ts-node`: Node.jsからTypeScriptコンパイラを呼び出します。`gatsby-node.js`をTSで書くのに使います。後で解説します。
- `gatsby-plugin-graphql-codegen`: ソース中のGraphQLクエリから自動的に型定義ファイルを吐き出します。

インストールしたプラグインを`gatsby-config.js`の`plugins`に書いてみましょう。

```js:gatsby-config.json
  plugins: [
    {
      resolve: `gatsby-plugin-typescript`,
      options: {
        isTSX: true,
        allExtensions: true,
      },
    },
    {
      resolve: `gatsby-plugin-graphql-codegen`,
      options: {
        fileName: `types/graphqlTypes.d.ts`,
      },
    },
  ]
```

`gatsby-plugin-graphql-codegen`のオプションの`filename`は、読んで字の如く型定義ファイルが吐き出される場所ですが、`src/`以下の更新を察知してファイルを吐き出しているため`src/types`とかに吐き出させると__まあ悲惨なことになります__。`src`の外に出しましょう。

`tsconfig.json`も書いてみます。

```json:tsconfig.json
{
  "compilerOptions": {
    "strict" : true,
    "esModuleInterop": true,
    "jsx": "react"
  }
}
```


# スタイリングする

真っ白なキャンパスに色をつけましょう。
今回は __TailwindCSS__, __PostCSS__, __Emotion__を使います。
基本はTailwindCSSでスタイリングしていき、こいつで表現できないもの(`::before`とか)はEmotionを使うというスタンスでいきます。
そのほか、不要で低速化の原因になる未使用CSSを __[PurgeCSS](https://www.purgecss.com/)__ で<kbd>ﾊﾞﾁｭ</kbd>っとしちゃって軽量化を図ります。


## Tailwindを使ってみる

### Tailwindって？

1つのクラスに対し1つのプロパティを割り当てる粒度の細かさが特徴の、__Utility-first__ なCSSフレームワークです。
フレームワーク臭さのないサイトを自分でデザインしたい人におすすめです。

Gatsbyで使うには**PostCSSを使う**方法と, **BabelマクロでCSS-in-JSと使う**方法がありますが、今回のブログでは**両方**やります。


```html
<div class="text-gray-900 hover:text-blue-500">Hello, world!</div>
```

こういう`class`を指定すると

```css
.text-gray-900 {
	color: #1a202c;
}
.hover\:text-blue-500:hover {
	color: #4299e1;
}
```

こういうCSSが適用されます。クラスは`tailwind.config.js`からカスタマイズできます。


### Tailwind + PostCSS

まずはPostCSSから。

```
npm i -S tailwindcss postcss gatsby-plugin-postcss
```

パッケージを入れて、

```js:postcss.config.js
module.exports = () => ({
  plugins: [require("tailwindcss")("./tailwind.config.js")],
})
```

```js:gatsby-config.js(抜粋)
plugins: [
  // ...
    "gatsby-plugin-postcss",
  // ...
],
```

```js:tailwind.config.js
module.exports = {}
```

configファイルを書きます。

次に適当な感じで`.css`ファイルを作ります。今回は`src/styles/style.css`にしてみました。

```css:style.css
@tailwind base;

@tailwind components;
@tailwind utilities;
```

で、こいつをコンポーネントから読み込みます。

```
import React from "react"
import "../styles/style.css"

export default () => <div className="text-blue-500">Hello world!</div>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411269/951a351c-c5e3-cdf2-caf3-10d72b622f1f.png)

いちいち`import`するのは面倒なので、「すべてのページが読み込むコンポーネント」を作って、そこ1つに読み込ませるのが賢明かと思われます。

## Emotionを使ってみる

### Emotionって？

CSS-in-JSライブラリです。今回はTailwindでカバーできない複雑なスタイリングに使います。

```
npm i -S @emotion/core @emotion/styled gatsby-plugin-emotion tailwind.macro@next
```

`tailwind.macro@next`: EmotionとTailwindを連携させる。

### 使う準備

```js:gatsby-config.js(抜粋)
plugins: [
  // ...
    "gatsby-plugin-emotion",
  // ...
]
```

### 使う

Visual Studio Codeでは [vscode-styled-components](https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components) を使うと、タグ付きテンプレートリテラル(``css` ``から始まるとこ)にハイライトが効いて幸せになれます。

```tsx:つかいかた
//タグ付きテンプレートリテラルでCSSを書くならこいつをimportしておく
import { css } from "@emotion/core" 

(<div css={css`
  color: #ff0;
`}>
  ほげほげ～
</div>)
```

### Tailwindと使う

`babel-plugin-macros-config.js`を一番上のディレクトリに作ります。

```js:babel-plugin-macros-config.js
module.exports = {
  tailwind: {
    styled: "@emotion/styled",
    config: "./tailwind.config.js",
    format: "auto",
  },
}
```

そうしたら、

```tsx:おためし
import { css } from "@emotion/core"
import tw from "tailwind.macro"

(<div css={css`
    ${tw`text-blue-500`}
    font-size: 10em;
`}>
  ほげほげ～
</div>)
```

こんな感じで、タグ付きテンプレートリテラルとして呼び出します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411269/52b06b9c-b49a-f7c5-5e38-6565eb9d1c55.png)

ちなみに、TypeScriptに「型定義がねえぞ」と怒られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411269/8bd83f3e-6cdb-6d19-9753-981659d1e3f3.png)
気になるなら書きましょう。

```ts:tailwind-macro.d.ts
declare module "tailwind.macro" {
  export const tw: (r: TemplateStringsArray) => any
  export default tw
}
```

## PurgeCSSでﾊﾞﾁｭｯと

[PurgeCSS](), [**gatsby-plugin-purgecss**](https://www.gatsbyjs.org/packages/gatsby-plugin-purgecss)を導入。

```
npm i -S purgecss gatsby-plugin-purgecss
```

```js:gatsby-config.js
plugins: [
  // ...
  // Sass, Less, ,Stylus, PostCSSのプラグインよりあとに書く
  {
    resolve: "gatsby-plugin-purgecss",
    options: {
      content: [
        require("path").join(
          process.cwd(),
          "src/**/!(*.d).{js,jsx,ts,tsx,md,mdx}"
        ),
      ],
      printRejected: true, // 被害者リストをprintする
      develop: false, // gatsby develop の実行時にもPurgeCSSを発動させるか
      tailwind: true, // Tailwindと使う時にonする
      whitelist: ["emoji"], // ここに書いたのは消されない
    },
  },
  // ...
]
```

あとはbuild時に勝手に消し飛ばしてくれます。

# MDXで記事を書いてみる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411269/a11c5065-d7ff-bcf3-60ac-785bf34b0267.png)
__https://mdxjs.com/__

## MDXって？

__JSX, TSXのMarkdown版__です。__MarkdownからReactコンポーネントを呼び出して使えます__。
また、__frontmatter__ というフォーマットで、ファイルの先頭に日付やら執筆者やらの情報を記述できます[^2]。
frontmatterに書いた値は __gatsby-plugin-mdx__ によってGraphQLで取得できるよう変形され、記事の構築で使用できます。

[^2]: frontmatterはMDX固有のものではないようです。よく理解していない。

```md:つかいかた
---
path: "gatsby-blog"
date: "2019-10-23"
title: "亜光速ブログ戦隊: [Gatsby, TypeScript, Emotion, Tailwind, MDX]"
status: "public"
---
import { Hoge } from "hogefuga"

**この記事は[Gatsby.js Advent Calendar 2019](https://qiita.com/advent-calendar/2019/gatsby) 23日目の記事です。**

# これは
```

`path`は名前通り記事のpathです。
`date`は日付で、記事一覧ページではここの値でソートします。
`title`は記事上部に表示するほか、HTMLの`<title>`に渡す値にもします。

## 準備

MDXをブログの記事にするには以下の工程が必要です。

0. `src/post/`に記事のもとになる`.mdx`ファイルを用意する
1. ファイルシステムからMDXファイルを吸い出す…`gatsby-source-filesystem`
2. 吸い出したMDXをGraphQLでクエリできるようにする…`gatsby-plugin-mdx`
3. クエリしてきたデータからページを作る…`gatsby-node.js`
4. データをテンプレートに突っ込み、実際のページの形を作る…`blogTemplate.tsx`(名前はなんでもいい)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411269/88a13173-5289-3832-cf7f-10121dc7b257.png)






```
npm i -S gatsby-plugin-mdx gatsby-source-filesystem @mdx-js/mdx @mdx-js/react
```

`gatsby-source-`から始まる __Source Plugin__ は、__いろいろな場所(PCのディスク, WordPress, …)からデータを引っ張ってこれるようにします__。
`gatsby-source-filesystem`はその名の通り__開発するPCのファイルシステム__からデータを吸い上げ、次の工程に回します。

`gatsby-transformer-`から始まる __Transformer Plugin__ は、__Source Pluginから受け取ったファイルをGraphQLでクエリできるように変形します__。今回の`gatsby-plugin-mdx`は名前的にアレですが立派なTransformerです。

ではconfigを書きます。

```js:gatsby-config.js
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `posts`,
        path: `${__dirname}/src/posts`,
      },
    },
    {
      resolve: `gatsby-plugin-mdx`,
    },
```

これで、`src/posts/`以下に`.mdx`ファイルを書けば変換してくれることになりました。

## `gatsby-node`を書く

`gatsby-node.js`は、__ビルド時にNode.jsで実行__したい処理を書くファイルです。__ブラウザでは実行されません__。
_3. クエリしてきたデータをテンプレートに突っ込み、ページを作る_ ということをするためにこいつを使います。

### TypeScript化

`gatsby-node.js`を`.ts`にしてTypeScriptで書いても動きません。なので、

1. `ts-node`を使い
2. TypeScriptコンパイラを呼び出して
3. 適当なところから`require`した`.ts`ファイルをコンパイルし
4. exports.createPages(ページを作成するのに使うとこ)に渡す

という工程をとります。

「_そこまでしてTypeScriptを使いたいのかー！？_」
『__ええ使いたいですとも！静的型付け万歳！コード補完万歳！Null安全万歳！__』
`ts-node`と`tsconfig.json`の準備はできてますかー？

```gatsby-node.js
"use strict"
require("ts-node").register(require("./tsconfig.json"))
exports.createPages = require("./config/gatsby-node").createPages
```

今回は`config/gatsby-node.ts`に書くことにします。

> :warning: `tsconfig.json`にコメントが含まれてるとパース時に死にます。
> 規格守らないのも悪いが、コメントひとつ書けないJSONもJSONだと思う。



## `config/gatsby-node.ts`を書く

やってることをざっくり解説すると、

1. GraphQLでMDXファイルのデータを取る ``const result = await graphql<Data>(`  ``ら辺)
2. `result`にエラー情報が入ってたらそれを吐いて処理は中止
3. `data.allMdx.edges`がMDXファイル(を変形したデータ)の配列になっているので`forEach`で全部に対して`createPage()`を実行する
4. `null`が出ると困るところが`null`だったらやっぱり中止
5. `createPages()`でページのパス, テンプレート, コンテキストを指定してページを作る


```ts:config/gatsby-node.ts
import { GatsbyNode } from "gatsby"
import Path from "path"
import { MdxConnection, MdxEdge } from "../types/graphqlTypes"
// gatsby-plugin-graphql-codegenで自動生成された型↑

export const createPages: GatsbyNode["createPages"] = async ({
  graphql,
  actions,
  reporter,
}) => {
  type Data = {
    allMdx: MdxConnection
  }
  const { createPage } = actions
//////↓1 MDXのデータを取ってくる//////
  const result = await graphql<Data>(` 
    query gatsbyNode {
      allMdx(
        sort: { fields: frontmatter___date, order: ASC }
        filter: { frontmatter: { status: { ne: "private" } } }
      ) {
        edges {
          node {
            id
            body
            excerpt
            frontmatter {
              path
              status
              tags
              title
              date
              description
            }
          }
          next {
            frontmatter {
              path
              title
            }
          }
          previous {
            frontmatter {
              path
              title
            }
          }
        }
      }
    }
  `)
  if (result.errors) {
    reporter.panicOnBuild(result.errors)
    return
  }
 //↓ 2. エラーだったら中止 ↑/////////
  if (!result.data) return 
  const data = result.data

// ↓ 3. MDXファイルそれぞれに対し実行する
  data.allMdx.edges.forEach(post => {
    if (post.node.frontmatter == null) {
      throw new Error("frontmatter is null")
    }
//////↓ 4. pathとかがnullだと困るのでErrorを投げちゃう ↑//////
    if (post.node.frontmatter.path == null) {
      throw new Error("path is null")
    }
////// 5. パス, テンプレート, コンテキストを渡してページを作る
    createPage({
      path: "/blog/" + post.node.frontmatter.path,
      component: Path.resolve(`./src/components/blog/general/blogTemplate.tsx`),
      context: { post, id: post.node.id },
    })
  })
}
```

frontmatterが早速出てきてますね。

## ブログ記事のテンプレートを書く

`gatsby-node`で`src/components/blog/general/blogTemplate.tsx`と指定しているブログ記事のテンプレートを用意します。たぶんこのファイルがいちばんカオス。

ここでやっていることは以下です。

1. `gatsby-node`から`pageContext`を受け取る
2. 事前に用意しておいたテンプレートに突っ込む

```tsx:ブログのテンプレの簡単な実装例
// MDX描画に使う
import { MDXProvider } from "@mdx-js/react"
import { MDXRenderer } from "gatsby-plugin-mdx"
import React from "react"
// gatsby-plugin-graphql-codegenで自動生成された型定義
import { MdxEdge } from "../../../../types/graphqlTypes"

export const BlogTemplate = ({
  pageContext,
}: {
  pageContext: { post: MdxEdge }
}) => {
  const { post } = pageContext,
    node = post.node
  return (
    <article className="p-3 md:p-5">
      <MDXProvider components={blogArticleComponents}>
        <MDXRenderer>{post.node.body}</MDXRenderer>
      </MDXProvider>
    </article>
  )
}

export default BlogTemplate
```

簡単ですね！
それでは現実に作ったブログでのテンプレートを見てみましょう。

<details><summary>地獄の窯の蓋。そのうち解説を入れる</summary><div>

A. 画面幅が1024pxより小さいかチェックし、`resize`(画面のリサイズ)イベントのたびにチェックするようイベントリスナーを貼る
B. ページのタイトル(`<head>`内の`<title>`に書くやつ)と, `<meta name="description"/>`を指定
C. `<BlogPageWithTOC>`の中は、上から 記事のヘッダー, 書きかけ記事の注意, 画面幅が1024pxより小さいときに表示される目次, __ブログの本文__, 前後の記事[^1]。

[^1]: 「前後の記事が下にだけあるのおかしくね？上にはないの？」と思われるでしょうが、記事のヘッダー(`<BlogPostHead/>`)に含まれてるのでちゃんと入っています。

```tsx:src/components/blog/general/blogTemplate.tsx
// MDX描画に使う
import { MDXProvider } from "@mdx-js/react"
import { MDXRenderer } from "gatsby-plugin-mdx"
import React, { useState } from "react"
// gatsby-plugin-graphql-codegenで自動生成された型定義
import { MdxEdge } from "../../../../types/graphqlTypes"
// TOC(目次)つきのレイアウト
import { BlogPageWithTOC } from "../../Wrappers"
// 目次のコンポーネントと、それに突っ込むデータの型
import { TableOfContents, TOC } from "./TableOfContents"
// ブログのヘッダー
import BlogPostHead from "../BlogPostHead"
// 前の記事, 次の記事
import PrevNextLink from "../PrevNextLink"
// 警告 Qiitaでいうなら「この記事は最終更新日から1年以上が経過しています。」と同じ
import { Warn } from "../Notes"
// MDXからHTMLを作るための要素たち
import blogArticleComponents from "./blogArticleComponents"
// Custom Hooks。 最初のレンダリングで実行される
import { onFirstRender } from "../../../hooks/onFirstRender"

export const BlogTemplate = ({
  pageContext,
}: {
  pageContext: { post: MdxEdge }
}) => {
  const { post } = pageContext,
    node = post.node,
    TOC: TOC = node.tableOfContents
  const [isSmallerThanMd, setISTM] = useState(false)
  onFirstRender(() => {
    setISTM(matchMedia("(max-width: 1023px)").matches)
    addEventListener("resize", () =>
      setISTM(matchMedia("(max-width: 1023px)").matches)
    )
  })
  return (
    <BlogPageWithTOC
      title={`${node.frontmatter?.title} - KKRN.ME`}
      description={node.frontmatter?.description ?? node.excerpt}
      TOC={TOC}
      visible={isSmallerThanMd}
    >
      <BlogPostHead post={post} />

      <article className="p-3 md:p-5">
        {node.frontmatter?.status === "draft" ? (
          <Warn>この記事は書きかけです。</Warn>
        ) : null}
        {isSmallerThanMd ? (
          <TableOfContents TOC={TOC} className="my-1" />
        ) : null}
        <MDXProvider components={blogArticleComponents}>
          <MDXRenderer>{post.node.body}</MDXRenderer>
        </MDXProvider>
      </article>
      <PrevNextLink post={post} />
    </BlogPageWithTOC>
  )
}

export default BlogTemplate
```

</div></details>

# テスト(20-01-05追記)

[Gatsby公式ドキュメントのUnit Testingのとこ](https://www.gatsbyjs.org/docs/unit-testing/)を見ながらテストします。

# おわりに

初アドベントカレンダーに対し雑な見込みを立ててしまい、結果的に遅刻してしまいすみませんでした。SEO/PWAやテンプレートの奥深いところに踏み込めていないので、たくさん加筆修正することになると思います。まだGatsbyの奥は深いようです。
可能性は無限大！！__Gatsby最高！！！！！！！！！！！！！！！！！！！！！！！！__


## 参考リンク
[Reactの最強フレームワークGatsby.jsの良さを伝えたい！！](https://qiita.com/hppRC/items/00739eaf9ae7fc95c1ca)
[GatsbyでGoogle Lighthouseで満点を取るブログを一から作る](https://qiita.com/so99ynoodles/items/87e136d09644baac634c)
[Reactベース静的サイトジェネレータGatsbyの真の力をお見せします](https://qiita.com/uehaj/items/1b7f0a86596353587466#%E4%B8%80%E8%88%AC%E7%9A%84%E3%81%AA%E5%88%A9%E7%82%B9)
[Using Tailwind CSS with Gatsby, React & Emotion Styled Components](https://nystudio107.com/blog/using-tailwind-css-with-gatsby-react-emotion-styled-components)