<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Watatakuのブログ]]></title><description><![CDATA[Watatakuのブログです]]></description><link>https://watataku-blog.vercel.app</link><generator>RSS for Node</generator><lastBuildDate>Tue, 07 Apr 2026 20:15:04 GMT</lastBuildDate><atom:link href="/feed" rel="self" type="application/rss+xml"/><language><![CDATA[ja]]></language><item><title><![CDATA[Next.jsで星のレーティング評価を作ってみた | Watatakuのブログ]]></title><description><![CDATA[<p>実務で作る機会があったので今回書いてみようと思い書いています。誰かの参考になれば幸いです。</p><h2 id="h4e42bd2021">星のレーティング評価とは</h2><p>聞き馴染みがそんなにないとは思いますが (少なくとも筆者は初耳)レビューなどで星で評価するやつです。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/5333feb7cbec4aaab806f7bb71884dfc/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202024-04-13%2016.21.46.png" alt=""></p><h2 id="hed900658dc">環境</h2><ul><li>next.js : 14.1.4</li><li>react : ^18</li><li>TypeScript : ^5</li><li>TailwindCSS : ^3.3.0</li></ul><h2 id="ha31bd80de1">要件</h2><p>1~5までの数字(小数点含む)をコンポーネントに渡すと、星のレーティング評価を返すものを作る</p><h3 id="h78fd6f2c2c">第一段階・星の配列を表示する</h3><pre><code>const StarRating = () =&gt; {
  const stars = Array.from({ length: 5 }, (_, index) =&gt; {

    // 星のスタイルと描画内容を定義します。
    const starContent = "★";
    let starClassName = "text-gray-400"; 

    // 星を表示します。
    return (
      &lt;span key={index} className={`text-xl ${starClassName}`}&gt;
        {starContent}
      &lt;/span&gt;
    );
  });

  // 星を表示します。
  return &lt;div className="flex"&gt;{stars}&lt;/div&gt;;
};</code></pre><h3 id="he8bc0ed745">第二段階・評価を受け取る</h3><pre><code>import React from "react";

type StarRatingProps = {
  rating: number;
};

const StarRating: React.FC&lt;StarRatingProps&gt; = ({ rating }) =&gt; {
  const stars = Array.from({ length: 5 }, (_, index) =&gt; {
    const starContent = "★";
    let starClassName = "text-gray-400";

    return (
      &lt;span key={index} className={`text-xl ${starClassName}`}&gt;
        {starContent}
      &lt;/span&gt;
    );
  });

  return &lt;div className="flex"&gt;{stars}&lt;/div&gt;;
};

export default StarRating;</code></pre><p>Reactが少しわかる方ならわかるかもしれませんが1~5の評価をプロップスで受け取れるようにしただけです。</p><h3 id="h0e1e7d3646">第三段階・評価の数字分黄色に塗りつぶす</h3><p>ここが少し難しいところではありますがやっていきましょう。<br>まずは、「<strong>評価に基づく塗りつぶしの割合を計算</strong>」します。</p><pre><code> const fillRatio = Math.min(Math.max(rating - index, 0), 1);</code></pre><p>割合が分かったら、下記コードのように星が完全に塗りつぶされている星と一部しか塗りつぶされていない場合の場合分けを行っていきます。</p><pre><code>// 星が完全に塗りつぶされている場合
if (fillRatio === 1) {
  starClassName = "text-yellow-500"; 
}
// 星が半分または一部塗りつぶされている場合
else if (fillRatio &gt; 0) {
  return (
    &lt;span key={index} className="relative text-xl"&gt;
      {/* 塗りつぶされた部分 */}
      &lt;span
        className="absolute text-yellow-500"
        style={{
        width: `${fillRatio * 100}%`, // 塗りつぶしの割合
          overflow: "hidden",
        }}
      &gt;
        ★
      &lt;/span&gt;
      {/* 塗りつぶされていない星 */}
      &lt;span className="text-gray-400"&gt;★&lt;/span&gt;
    &lt;/span&gt;
  );
}</code></pre><p>これで完成となります。<br><br>最後にコードの全体をご覧ください。</p><pre><code>// StarRanking.tsx
import React from "react";

type StarRatingProps = {
  rating: number;
};

const StarRating: React.FC&lt;StarRatingProps&gt; = ({ rating }) =&gt; {
  const stars = Array.from({ length: 5 }, (_, index) =&gt; {
    const starContent = "★";
    let starClassName = "text-gray-400"; 
    
    const fillRatio = Math.min(Math.max(rating - index, 0), 1);

    if (fillRatio === 1) {
      starClassName = "text-yellow-500"; 
    }
    else if (fillRatio &gt; 0) {
      return (
        &lt;span key={index} className="relative text-xl"&gt;
          &lt;span
            className="absolute text-yellow-500"
            style={{
              width: `${fillRatio * 100}%`, 
              overflow: "hidden",
            }}
          &gt;
            ★
          &lt;/span&gt;
          &lt;span className="text-gray-400"&gt;★&lt;/span&gt;
        &lt;/span&gt;
      );
    }

    return (
      &lt;span key={index} className={`text-xl ${starClassName}`}&gt;
        {starContent}
      &lt;/span&gt;
    );
  });

  return &lt;div className="flex"&gt;{stars}&lt;/div&gt;;
};

export default StarRating;</code></pre><p>あとは、このコンポーネントを親で呼んであげます</p><pre><code>&lt;StarRanking ranking={4} /&gt;</code></pre><p><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/b07bf88201ff49938c7d0f32962fa31c/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202024-04-13%2017.14.33.png" alt=""><br>次にこのコンポーネントに「2.5」を渡すと下記のような表示になります。</p><pre><code>&lt;StarRanking ranking={2.5} /&gt;</code></pre><p><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/8d7e158834dc44c796961ef9d983c0ac/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202024-04-13%2017.20.05.png" alt=""><br>うん。いい感じだと思います。<br>お疲れ様でした。<br><br><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/bu2g6uj2g</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/bu2g6uj2g</guid><pubDate>Sat, 13 Apr 2024 07:18:41 GMT</pubDate></item><item><title><![CDATA[Next.jsでvcfファイルダウンロードボタンを作ってみた。(Safari対応) | Watatakuのブログ]]></title><description><![CDATA[<p>今回はNext.jsでvcfファイルをダウンロードするボタンを作ってみたので、是非参考にしてみてください。</p><h2 id="hed900658dc">環境</h2><ul><li>next: 14.0.4</li><li>react: ^18</li><li>typescript: ^5</li></ul><h2 id="hacdb522e87">vcfファイルとは</h2><blockquote>連絡先とその関連情報を保存する仮想連絡先ファイルです。 VCF ファイルは vCard ファイルとしても知られ、電話番号、住所、連絡先の写真、Web サイトのアドレス、電子メール ID などの連絡先情報を保存するための設計されたフィールドで構成されます。</blockquote><p><a href="https://www.msoutlooktools.com/jp/open-vcf-file-on-computer/" target="_blank" rel="noopener noreferrer">https://www.msoutlooktools.com/jp/open-vcf-file-on-computer/</a></p><h2 id="h51049a6b64">モジュールをインストールする</h2><p><a href="https://www.npmjs.com/package/vcards-js" target="_blank" rel="noopener noreferrer">https://www.npmjs.com/package/vcards-js</a></p><pre><code>$ npm i vcards-js</code></pre><h2 id="h1fe54e2a6d">vcfファイルをダウンロードさせるボタンの作成</h2><p>まずは普通のボタンを作ります。</p><pre><code>const VcfDownloadBtn = () =&gt; {
  return (
    &lt;button type="button"&gt;ダウンロード&lt;/button&gt;
  )
}</code></pre><p>ここにダウンロードボタンを押したら、vcfファイルをダウンロードをさせる処理を書いていきます。</p><pre><code>const VcfDownloadBtn = () =&gt; {
  const CreateVCard = (
    firstName = "",
    lastName = "",
    organization = "",
    postalCode = "",
    street = "",
    homePhone = "",
    homeFax = "",
    email?: string,
    url = "",
    note = ""
  ): string =&gt; {
    const vCardsJS = require("vcards-js");
    const vCard = vCardsJS();

    vCard.firstName = firstName;
    vCard.lastName = lastName;
    vCard.organization = organization;
    vCard.homeAddress.postalCode = postalCode;
    vCard.homeAddress.street = street;
    vCard.homePhone = homePhone;
    vCard.homeFax = homeFax;
    vCard.email = email;
    vCard.url = url;
    vCard.note = note;

    return vCard.getFormattedString();
  };

  const handleClick = (vcfText: string) =&gt; {
    const element = document.createElement("a");
    const file = new Blob([vcfText], { type: "text/vcard;charset=utf-8" });
    element.href = URL.createObjectURL(file);
    element.download = "myFile.vcf";
    document.body.appendChild(element);
    element.click();
  };
  return (
    &lt;button 
      type="button"
      onClick={() =&gt;
            handleClick(
              CreateVCard(
                "山田",
                "太郎",
                "WEBエンジニア",
                "999-9999",
                "バーリント西区公園通り128",
                "99-9999-9999",
                "00-0000-0000",
                "hoge@xxx.xx",
                "https://xxxxx.xxxxxx",
                "架空の人物です。"
              )
            )
          }
     &gt;ダウンロード&lt;/button&gt;
  )
}</code></pre><p>以上で完成なのですがこのままだと下記エラーが出ます。</p><pre><code>Module not found: Can't resolve 'fs'</code></pre><p>対処法として、<code>next.config.js</code>に以下を追加してください。</p><pre><code>webpack: (config) =&gt; {
  config.resolve.fallback = { fs: false };
  return config;
},</code></pre><p>これでエラーが無くなり、無事vcfファイルがダウンロードされると思います。<br><br>詳しくはドキュメントをチェックしてください。<br><a href="https://github.com/enesser/vCards-JS" target="_blank" rel="noopener noreferrer">https://github.com/enesser/vCards-JS</a><br><br><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/wtuct6mypb</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/wtuct6mypb</guid><pubDate>Wed, 20 Mar 2024 08:16:40 GMT</pubDate></item><item><title><![CDATA[Next.js(App Router)×TypescriptでGoogleアナリティクスを導入する | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h2295b759c0">対象者</h2><ul><li>Next.jsで構築したサイトにGoogleアナリティクスを導入したい方。</li><li>または、その方法がわからない方。</li></ul><h2 id="h23ef74d045">書かないこと</h2><p>トラッキングID・gtagの取得の仕方(取得した前提で話を進めます。)</p><h2 id="hed900658dc">環境</h2><ul><li>next: 14.0.4</li><li>react: ^18</li><li>typescript: ^5</li></ul><h2 id="hadcb3d82a0">導入していく</h2><ul><li>トラッキングID・gtagを環境変数で持っておく</li></ul><pre><code>// .env.local
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID="G-****************************"</code></pre><ul><li>ibs/gtag.tsを作成する</li></ul><pre><code>export const GA_TAG_ID = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID || "";

export const IS_GATAG = GA_TAG_ID !== "";

export const pageview = (path: string) =&gt; {
  window.gtag("config", GA_TAG_ID, {
    page_path: path,
  });
};</code></pre><p><br></p><pre><code>window.gtag("config", GA_TAG_ID, {
  page_path: path,
});</code></pre><p>上記の箇所に目見線が出ていると思うので下記コマンドで型をインストールします。</p><pre><code>$ npm i -D @types/gtag.js</code></pre><ul><li>GoogleAnalyticsコンポーネントの作成</li></ul><pre><code>// components/GoogleAnalytics.tsx 
"use client";

import { usePathname, useSearchParams } from "next/navigation";
import Script from "next/script";
import { useEffect } from "react";
import { IS_GATAG, GA_TAG_ID, pageview } from "@/libs/gtag";

const GoogleAnalytics = () =&gt; {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() =&gt; {
    if (!IS_GATAG) {
      return;
    }
    const url = pathname + searchParams.toString();
    pageview(url);
  }, [pathname, searchParams]);

  return (
    &lt;&gt;
      &lt;Script
        strategy="lazyOnload"
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_TAG_ID}`}
      /&gt;
      &lt;Script id="gtag-init" strategy="afterInteractive"&gt;
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', '${GA_TAG_ID}', {
            page_path: window.location.pathname,
          });
        `}
      &lt;/Script&gt;
    &lt;/&gt;
  );
};

export default GoogleAnalytics;</code></pre><ul><li>layout.tsxでGoogleAnalyticsコンポーネントを読み込む</li></ul><pre><code>// layout.tsx

import { Inter } from "next/font/google";
import "./globals.css";
import GoogleAnalytics from "@/components/GoogleAnalytics";
import { Suspense } from "react";
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang="ja" className="scroll-smooth"&gt;
      &lt;head&gt;
        &lt;Suspense fallback={null}&gt;
          &lt;GoogleAnalytics /&gt;
        &lt;/Suspense&gt;
      &lt;/head&gt;
      &lt;body className={inter.className}&gt;
        {children}
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre><p>ここで少し注意が必要なんですけど、Next.jsでApp Routerを使用するときに<code>useSearchParams()</code>を使用すると、<code>Entire page deopted into client-side rendering</code>というエラーが出てしまいます。 <br><a href="https://nextjs.org/docs/messages/deopted-into-client-rendering" target="_blank" rel="noopener noreferrer">https://nextjs.org/docs/messages/deopted-into-client-rendering</a><br><br>その対処方法として、<code>useSearchParams()</code>を使用しているコンポーネント(今回の場合、GoogleAnalytics.tsx)に<code>&lt;Suspence /&gt;</code>でラップしてやるとエラーが無くなります。<br><br>以上でGoogleアナリティクスを導入できました。</p>]]></description><link>https://watataku-blog.vercel.app/blog/scvkpbqpa5j</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/scvkpbqpa5j</guid><pubDate>Wed, 20 Mar 2024 03:59:10 GMT</pubDate></item><item><title><![CDATA[Next.jsでChakra UIを使ってみる。 | Watatakuのブログ]]></title><description><![CDATA[<p>明けましておめでとうございます。今年もよろしくお願いします！！ってことで今年初めてのブログはChakra UIです。<br><a href="https://chakra-ui.com/" target="_blank" rel="noopener noreferrer">https://chakra-ui.com/</a><br><br>以下の環境でChakra UIを使用しています。</p><ul><li>Next.js : 14.0.3</li><li>React : 18.0.0</li></ul><h2 id="h2295b759c0">対象者</h2><ul><li>Chakra UIって何？美味しいの？って状態の方。</li><li>Chakra UIを使ったことがない方。</li><li>Chakra UIを使ってみたい方。</li></ul><h2 id="hd8d793506b">インストール</h2><p>Chakra UIを使うためのモジュール群をインストールします。</p><pre><code>$ npm i @chakra-ui/react @chakra-ui/next-js @emotion/react @emotion/styled framer-motion</code></pre><p>Chakra UIはemotionとframer-motionに依存しているっぽいのでこれらも併せてインストールします。</p><h2 id="h31d727ccda">準備</h2><p>Chakra UIを使うためには、最も階層が上位（この場合、<code>app/layout.tsx</code>）に<code>&lt;ChakraProvider /&gt;</code> を設置する必要があります。必ずChakra UIを利用する際にはこのProviderを設定するようにしてください。</p><pre><code>// app/layout.tsx
・・・
import { ChakraProvider } from "@chakra-ui/react"; // 追加
・・・

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang="en"&gt;
      &lt;body className={inter.className}&gt;
        &lt;ChakraProvider&gt;{children}&lt;/ChakraProvider&gt; {/* &lt;ChakraProvider /&gt;でラップする。 */}
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre><p><br>ここまでくると、Chakra UIが使えますので実際に使っていきたいと思います。</p><pre><code>// app/page.tsx
import { Heading } from "@chakra-ui/react";

export default function Home() {
  return (
    &lt;Heading color={"red"} fontSize={"64px"}&gt;
      Next.js × Chakra UI
    &lt;/Heading&gt;
  )
}</code></pre><p><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/2c882a2e02ca4392bd26dc57dacc9528/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-11-26%2013.02.34.png" alt=""><br>ちなみに、ブラウザ上では<u>&lt;h2&gt;</u>でレンダリングされます。</p><h2 id="hdae3825bce">基本となるスタイルの付け方</h2><p>Chakra UIでは、<a href="https://github.com/styled-system/styled-system" target="_blank" rel="noopener noreferrer">styled-system</a>を基礎としている部分があるという特徴があるので、<u>コンポーネントのpropsに直接style属性を記述することでスタイリング</u>します。</p><pre><code>&lt;Box
  width={"100px"}
  height={"100px"}
  backgroundColor={"blue"}
  borderRadius={"50%"}
  padding={"8px"}
  margin={"8px"}
&gt;&lt;/Box&gt;</code></pre><h3 id="h29c2b42b1d">スタイルのショートハンドラ</h3><p>styleの値にはさまざまなショートハンドラが用意されています。<code>padding</code> なら<code>p</code>、<code>margin</code> なら<code>m</code>です。</p><pre><code>&lt;Box
  w={"100px"}
  h={"100px"} 
  bgColor={"blue"}
  rounded={"50%"}
  p={"8px"} 
  m={"8px"}
&gt;&lt;/Box&gt;</code></pre><p>また、縦方向・横方向をまとめて指定する<code>py(padding-top/padding-bottom)・px(padding-left/padding-right)</code> 、<code>my(margin-top/margin-bottom)・mx(margin-left/margin-right)</code> ことができます。</p><pre><code>&lt;Box px={"10px"} my={"10px"}&gt;・・・&lt;/Box&gt;</code></pre><p>何だかこの辺りは<a href="https://tailwindcss.com/" target="_blank" rel="noopener noreferrer">TailwindCSS</a>に近しい感じがしますね。</p><h2 id="h02e85881c2">Theme</h2><p>Chakra UIにも他のUIライブラリみたいにTheme機能があります。ここでは、デフォルトのTheme機能について解説します。<br><a href="https://chakra-ui.com/docs/styled-system/theme" target="_blank" rel="noopener noreferrer">https://chakra-ui.com/docs/styled-system/theme</a></p><h3 id="h6ca073dc9d">色のThemeの使い方</h3><p>色のThemeに関しては<code>色.50、100~900</code> のような指定で、50と100〜900の100刻みで計10段階あり、<u>50が一番薄く900が一番濃い</u>と言う感じです。<a href="https://chakra-ui.com/docs/styled-system/theme#colors" target="_blank" rel="noopener noreferrer">https://chakra-ui.com/docs/styled-system/theme#colors</a></p><pre><code>&lt;Box backgroundColor={"red.50"} borderColor={"blue.200"} color={"yellow.800"}&gt;・・・&lt;/Box&gt;</code></pre><h3 id="h0111671636">サイズ関連の使い方</h3><ul><li>font-size、letter-spacingなどのフォント系</li></ul><p><a href="https://chakra-ui.com/docs/styled-system/theme#typography" target="_blank" rel="noopener noreferrer">https://chakra-ui.com/docs/styled-system/theme#typography</a></p><pre><code>// 7xl = 72px
&lt;Heading fontSize={"7xl"}&gt;
  Next.js × Chakra UI
&lt;/Heading&gt;</code></pre><ul><li>margin、padding</li></ul><p><a href="https://chakra-ui.com/docs/styled-system/theme#spacing" target="_blank" rel="noopener noreferrer">https://chakra-ui.com/docs/styled-system/theme#spacing</a></p><pre><code>// 2=0.5rem=8pxのpaddingが付く。
&lt;Box p={2}&gt;・・・&lt;/Box&gt;

// そのまま2pxのpaddingが付く。
&lt;Box p={"2px"}&gt;・・・&lt;/Box&gt;</code></pre><ul><li>width、height</li></ul><p><a href="unsafe:[object Object]">https://chakra-ui.com/docs/styled-system/theme#sizes</a></p><pre><code>&lt;Box 
  w={"full"} // full=100%
  h={100} // 1=1pxなので、100=100px
&gt;・・・&lt;/Box&gt;</code></pre><h3 id="h0201d4f644">レスポンシブ</h3><p>propsの値に配列またはオブジェクトを渡すだけでレスポンシブすることができます。<br><a href="https://chakra-ui.com/docs/styled-system/responsive-styles#the-object-syntax" target="_blank" rel="noopener noreferrer">https://chakra-ui.com/docs/styled-system/responsive-styles#the-object-syntax</a></p><pre><code>&lt;Box bg={["red.500", "yellow.500", "green.500", "blue.500"]} &gt;
  ・・・
&lt;/Box&gt;</code></pre><p>このように、配列を渡すことでレスポンシブを指定することができます。<br>配列を渡した場合、左に書いた値が<code>sm</code>その右の値が<code>md</code>、<code>lg</code>そして一番右の値が<code>xl</code>となっています。</p><pre><code>sm: "30em", // 480px 
md: "48em", // 768px
lg: "62em", // 992px
xl: "80em", // 1280px</code></pre><p><a href="https://chakra-ui.com/docs/styled-system/theme#breakpoints" target="_blank" rel="noopener noreferrer">https://chakra-ui.com/docs/styled-system/theme#breakpoints</a><br><br>また、以下のようにオブジェクトを使って表現することもできます。</p><pre><code>&lt;Box bg={{ base: "red.500", sm: "yellow.500", md: "green.500", lg: "blue.500" }} &gt;
&nbsp; ・・・
&lt;/Box&gt;</code></pre><h2 id="h2d319ccfc7">疑似要素</h2><p>疑似要素を指定する場合、<code>_before</code> 、<code>_after</code> のように指定します。</p><pre><code>&lt;Box _before={{ content: "", ・・・ }}　_after={{ content: "", ・・・ }}&gt;・・・&lt;/Box&gt;</code></pre><p><br>hoverの時も同様に<code>_hover</code>と指定します。</p><pre><code>&lt;Heading color={"red"} fontSize={"7xl"} _hover={{ color: "blue" }}&gt;
  Next.js × Chakra UI
&lt;/Heading&gt;</code></pre><h2 id="h3bcda3e6b0">最後に</h2><p>今回はChakra UIの基本的な使い方についてご紹介しました。<br>大体この辺りを押さえておくといいかんじにChakraライフを過ごせるのではないかと思います。他に足りない部分であったり応用的な使い方などについてはタイミングが合えば、また書きたいと思います。<br><br><br><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/6aas-k1ncq</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/6aas-k1ncq</guid><pubDate>Sun, 26 Nov 2023 03:48:42 GMT</pubDate></item><item><title><![CDATA[[JavaScript不要]　アコーディオンの作り方 | Watatakuのブログ]]></title><description><![CDATA[<p>今記事ではJavaScriptを使わずにアコーディオンの作り方を紹介します。<br>細かいアニメーションなどはまだまだJavaScriptのお力を借りなければなりませんが、HTMLだけでアコーディオンが作れるようになったのは驚きですねw<br></p><h2 id="h116fd24dee">とりあえず作ってみた。</h2><p><code>details</code>と<code>summary</code>といったタグを使うことにより簡単にアコーディオンを作ることができます。</p><pre><code>&lt;details class="accordion"&gt;
  &lt;summary class="label"&gt;フロントエンドエンジニア&lt;/summary&gt;
  ダミーテキストダミーテキストダミーテキストダミーテキスト
&lt;/details&gt;
&lt;details class="accordion"&gt;
  &lt;summary class="label"&gt;バックエンドエンジニア&lt;/summary&gt;
  ダミーテキストダミーテキストダミーテキストダミーテキスト
&lt;/details&gt;
&lt;details class="accordion"&gt;
  &lt;summary class="label"&gt;デザイナー&lt;/summary&gt;
  ダミーテキストダミーテキストダミーテキストダミーテキスト
&lt;/details&gt;</code></pre><p><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/d6b83c8a3bf04fabaff687c5a456848e/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-11-14%2017.18.56.png" alt=""></p><h2 id="h8ba5c4e0d9">応用編</h2><p>では、応用編です。今回は<a href="https://code-jump.com/" target="_blank" rel="noopener noreferrer">CodeJump</a>さんの<a href="https://code-jump.com/extra2-menu/" target="_blank" rel="noopener noreferrer">「【jQuery コーディング練習】番外編：アコーディオンメニューの練習」</a>を参考にデザインを作っています。</p><iframe class="embedly-embed" src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fcodepen.io%2Fwatataku8911%2Fembed%2Fpreview%2FJjxyezr%3Fdefault-tabs%3Dhtml%252Cresult%26height%3D600%26host%3Dhttps%253A%252F%252Fcodepen.io%26slug-hash%3DJjxyezr&display_name=CodePen&url=https%3A%2F%2Fcodepen.io%2Fwatataku8911%2Fpen%2FJjxyezr&image=https%3A%2F%2Fshots.codepen.io%2Fusername%2Fpen%2FJjxyezr-512.jpg%3Fversion%3D1699953628&key=94335756f04b424b8ce3ce71cbe3de0a&type=text%2Fhtml&schema=codepen" width="800" height="600" scrolling="no" title="CodePen embed" frameborder="0" allow="autoplay; fullscreen; encrypted-media; picture-in-picture;" allowfullscreen="true"></iframe><p><br></p><pre><code>&lt;div class="wrapper"&gt;
&nbsp; &lt;h1 class="title"&gt;募集職種&lt;/h1&gt;
&nbsp; &lt;details class="accordion"&gt;
&nbsp; &nbsp; &lt;summary class="label"&gt;1. フロントエンドエンジニア&lt;/summary&gt;
&nbsp; &nbsp; &lt;dl class="detail"&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;仕事内容&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;応募資格&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;必須スキル&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;給与&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;XXX万～XXX万 (スキル・経験・実績により優遇)&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;休日・休暇&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;
&nbsp; &nbsp; &nbsp; &nbsp; 土日祝祭日、有給休暇、夏季休暇、年末年始休暇、産前産後休暇、育児休暇
&nbsp; &nbsp; &nbsp; &lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;雇用形態&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;正社員（試用期間3ヶ月）&lt;br /&gt;業務委託&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;勤務時間&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;フレックスタイム&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;勤務地&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;東京&lt;/dd&gt;
&nbsp; &nbsp; &lt;/dl&gt;
&nbsp; &lt;/details&gt;
&nbsp; &lt;details class="accordion"&gt;
&nbsp; &nbsp; &lt;summary class="label"&gt;2. バックエンドエンジニア&lt;/summary&gt;
&nbsp; &nbsp; &lt;dl class="detail"&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;仕事内容&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;応募資格&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;必須スキル&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;給与&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;XXX万～XXX万 (スキル・経験・実績により優遇)&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;休日・休暇&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;
&nbsp; &nbsp; &nbsp; &nbsp; 土日祝祭日、有給休暇、夏季休暇、年末年始休暇、産前産後休暇、育児休暇
&nbsp; &nbsp; &nbsp; &lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;雇用形態&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;正社員（試用期間3ヶ月）&lt;br /&gt;業務委託&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;勤務時間&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;フレックスタイム&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;勤務地&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;東京&lt;/dd&gt;
&nbsp; &nbsp; &lt;/dl&gt;
&nbsp; &lt;/details&gt;
&nbsp; &lt;details class="accordion"&gt;
&nbsp; &nbsp; &lt;summary class="label"&gt;3. デザイナー&lt;/summary&gt;
&nbsp; &nbsp; &lt;dl class="detail"&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;仕事内容&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;応募資格&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;必須スキル&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;テキストテキストテキストテキスト&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;給与&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;XXX万～XXX万 (スキル・経験・実績により優遇)&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;休日・休暇&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;
&nbsp; &nbsp; &nbsp; &nbsp; 土日祝祭日、有給休暇、夏季休暇、年末年始休暇、産前産後休暇、育児休暇
&nbsp; &nbsp; &nbsp; &lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;雇用形態&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;正社員（試用期間3ヶ月）&lt;br /&gt;業務委託&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;勤務時間&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;フレックスタイム&lt;/dd&gt;
&nbsp; &nbsp; &nbsp; &lt;dt&gt;勤務地&lt;/dt&gt;
&nbsp; &nbsp; &nbsp; &lt;dd&gt;東京&lt;/dd&gt;
&nbsp; &nbsp; &lt;/dl&gt;
&nbsp; &lt;/details&gt;
&lt;/div&gt;</code></pre><p><br></p><pre><code>* {
&nbsp; margin: 0;
&nbsp; padding: 0;
&nbsp; box-sizing: border-box;
}

body {
&nbsp; background-color: #addff2;
&nbsp; color: #000;
}

.wrapper {
&nbsp; max-width: 960px;
&nbsp; padding: 0 20px;
&nbsp; margin: 40px auto;
}

.title {
&nbsp; font-size: 2rem;
&nbsp; font-weight: normal;
&nbsp; margin-bottom: 30px;
}

.accordion {
&nbsp; border-top: solid 1px #000;
}

.accordion:last-child {
&nbsp; border-bottom: solid 1px #000;
}

.accordion[open] {
&nbsp; background-color: #5bbee5;
}

.accordion .label {
&nbsp; list-style: none;
&nbsp; cursor: pointer;
&nbsp; font-size: 1.125rem;
&nbsp; font-weight: bold;
&nbsp; padding: 40px 30px;
&nbsp; position: relative;
&nbsp; transition: 0.5s;
}

.accordion .label::-webkit-details-marker&nbsp;{
  display: none;
}

.accordion .label:hover {
&nbsp; background-color: #5bbee5;
}

.accordion .label::before,
.accordion .label::after {
&nbsp; content: "";
&nbsp; width: 20px;
&nbsp; height: 1px;
&nbsp; background: #000;
&nbsp; position: absolute;
&nbsp; top: 50%;
&nbsp; right: 5%;
&nbsp; transform: translateY(-50%);
}

.accordion .label::after {
&nbsp; transform: translateY(-50%) rotate(90deg);
&nbsp; transition: 0.5s;
}

.accordion[open] .label::after {
&nbsp; transform: rotate(180deg);
}

.accordion .detail {
&nbsp; border-top: solid 1px #ccc;
&nbsp; padding: 32px 24px;
&nbsp; display: flex;
&nbsp; flex-wrap: wrap;
&nbsp; background-color: #addff2;
}

.accordion .detail dt {
&nbsp; padding-bottom: 32px;
&nbsp; font-weight: bold;
&nbsp; width: 20%;
}

.accordion .detail dd {
&nbsp; padding-bottom: 32px;
&nbsp; width: 80%;
}</code></pre><h3 id="hc499e4ad38">解説</h3><p>HTMLの方は最初の方に解説したので省略しますね。<br></p><h4 id="h2a6e107b90">デフォルトの三角形を消す</h4><p>デフォルトで左についてある開閉式ができるやつです。<br>作成するデザインによってはそれが不要になる場合があります。</p><pre><code>.accordion .label {
&nbsp; list-style: none;
}</code></pre><p><br>通常は上記の指定で消えてくれますが、Safariだけ<code>-webkit-details-marker&nbsp;</code>というCSSの擬似要素で三角形を表示しています。以下のように別途指定して三角形を消してやる必要があります。</p><pre><code>.accordion .label::-webkit-details-marker&nbsp;{
  display: none;
}</code></pre><p>ちなみにデフォルトでは<code>display: list-item;</code>が指定されています。</p><h4 id="h7c6f0987d7">開閉状態のスタイルの切り替え</h4><p>アコーディオンには開いている時、閉じている時があります。<br>開いている時のスタイルは「こう！」閉じている時のスタイルは「こう！」のように開閉状態を調べてスタイルを切り替える方法をここでは紹介します。</p><pre><code>.accordion {
  /* アコーディオンが閉じている時 */
}

.accordion[open] {
  /* アコーディオンが開いている時 */
}</code></pre><p>アコーディオンが開いているときは<code>open</code>属性が付与され、閉じている時には付与されない為、上記のような指定の仕方になるわけです。</p><h2 id="h44e51f96ce">参考資料</h2><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/HTML/Element/details" target="_blank" rel="noopener noreferrer">&lt;details&gt;:詳細折りたたみ要素 - MDN</a></li><li><a href="https://developer.mozilla.org/ja/docs/Web/HTML/Element/summary" target="_blank" rel="noopener noreferrer">&lt;summary&gt;:概要明示要素 - MDN</a></li><li><a href="https://ics.media/entry/220901/" target="_blank" rel="noopener noreferrer">detailsとsummaryタグで作るアコーディオンUI - アニメーションのより良い実装方法</a></li></ul><p><br><br><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/0xi2m2vw75i9</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/0xi2m2vw75i9</guid><pubDate>Tue, 14 Nov 2023 07:27:36 GMT</pubDate></item><item><title><![CDATA[SwiperをReactで使う方法 | Watatakuのブログ]]></title><description><![CDATA[<p>※ビルドツールにViteを使っています。<br>※Swiperのバージョンは<code>11.0.3</code>を使用しています。( お使いのバージョンによってモジュールの読み込み先など若干違うことがあるため注意してください。)<br><br><a href="https://swiperjs.com/react" target="_blank" rel="noopener noreferrer">https://swiperjs.com/react</a><br></p><h2 id="hd8d793506b">インストール</h2><pre><code>$ npm install swiper</code></pre><h2 id="h88dac47172">雛形を作ってみる</h2><p>とりあえず雛形を作ってみます。</p><pre><code>import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";

function App() {
  return (
    &lt;&gt;
      &lt;Swiper&gt;
        &lt;SwiperSlide&gt;
          &lt;img src="./img/mainvisual1.jpg" alt="" /&gt;
        &lt;/SwiperSlide&gt;
        &lt;SwiperSlide&gt;
          &lt;img src="./img/mainvisual2.jpg" alt="" /&gt;
        &lt;/SwiperSlide&gt;
        &lt;SwiperSlide&gt;
          &lt;img src="./img/mainvisual3.jpg" alt="" /&gt;
        &lt;/SwiperSlide&gt;
        &lt;SwiperSlide&gt;
          &lt;img src="./img/mainvisual4.jpg" alt="" /&gt;
        &lt;/SwiperSlide&gt;
      &lt;/Swiper&gt;
    &lt;/&gt;
  );
}

export default App;</code></pre><p>簡単ですね。上記のように記述するとシンプルなスライダーができたと思います。でも今のままで使っていく場合はほぼないと思いますので次の章からナビゲーションやページネーションのつけ方や自動再生の設定の仕方などのオプションの設定方法についてみていきます。</p><h2 id="h32a67f9128">オプション</h2><p>オプションは基本的に<code>&lt;Swiper /&gt;</code>に「props」で渡す形になります。<br>オプションにはそのまま使えるものや別途モジュールから読み込んできて使う2種類があることを覚えておいてください。<br></p><h3 id="hdd867680d8">ナビゲーション</h3><p>ナビーゲーションをつける方法をご紹介します。<br>これは、別途モジュールを読み込んで使うパターンのオプションです。</p><pre><code>import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation } from "swiper/modules"; // 追加
import "swiper/css";
import "swiper/css/navigation"; // 追加

function App() {
  return (
    &lt;&gt;
      &lt;Swiper
        modules={[Navigation]}  {/* 追加 */}
        navigation {/* 追加 */}
      &gt;
        ・・・
      &lt;/Swiper&gt;
    &lt;/&gt;
  );
}

export default App;</code></pre><h3 id="h43a144f4e4">ページネーション</h3><p>ページネーションのつけ方について解説します。<br>こちらも別途モジュールを読み込んで使う方法です。</p><pre><code>import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules"; // 修正
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination"; // 追加

function App() {
  return (
    &lt;&gt;
      &lt;Swiper
        modules={[Navigation,Pagination]} {/* 修正 */}
        navigation
        pagination={{
          clickable: true {/* 追加({{ clickable: true }} ← クリック操作を可能にする) */}
        }}
      &gt;
        ・・・
      &lt;/Swiper&gt;
    &lt;/&gt;
  );
}

export default App;</code></pre><h3 id="hd264691107">自動再生</h3><p>自動再生もよく使うオプションなのですが、これも別途モジュールを読み込んでします。<br>今回は2秒ごとに自動再生するように設定しています。</p><pre><code>import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination, Autoplay } from "swiper/modules"; // 修正
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";

function App() {
  return (
    &lt;&gt;
      &lt;Swiper
        modules={[Navigation,Pagination,Autoplay]} {/* 修正 */}
        navigation
        pagination={{ clickable: true }}
        autoplay={{
            delay: 2000　{/* 追加（{{ delay: 5000 }} ← デフォルト） */}
        }}
      &gt;
        ・・・
      &lt;/Swiper&gt;
    &lt;/&gt;
  );
}

export default App;</code></pre><p>このほかにもモジュールを読み込んで使っていくオプションはありますが数が膨大にあるため、個人的によく使うものを中心に書いてみました。その他のオプションに関してはご自分で調べてみてください。<a href="https://swiperjs.com/swiper-api#modules" target="_blank" rel="noopener noreferrer">https://swiperjs.com/swiper-api#modules</a><br><br>ここからはモジュールの読み込みが必要なく使えるオプションで個人的によく使いそうなやつを紹介していきます。</p><h3 id="h0ab02f34d7">ループ</h3><p>スライドをループさせます。</p><pre><code>return (
  &lt;Swiper 
    loop={true}
  &gt;
   ・・・
  &lt;/Swiper&gt;
)</code></pre><h3 id="hf7ee70b482">スピード</h3><p>スライドスピードを変更したいときに使うオプションです。<u>デフォルトの値は300</u></p><pre><code>return (
  &lt;Swiper 
    speed={500}
  &gt;
   ・・・
  &lt;/Swiper&gt;
)</code></pre><h3 id="hf1186109fd">画面に表示させる枚数の指定</h3><pre><code>return (
  &lt;Swiper 
    slidesPerView={3}
  &gt;
   ・・・
  &lt;/Swiper&gt;
)</code></pre><p>また、<code>slidesPerView</code>には少数点数を指定でき、横にずれるスライドも実装できる。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/0ccc323f3d1942cba88a9e778f05bd8a/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-11-05%2011.11.40.png" alt=""></p><pre><code>return (
  &lt;Swiper 
    slidesPerView={1.5}
    centeredSlides={true}
  &gt;
   ・・・
  &lt;/Swiper&gt;
)</code></pre><p>また、<code>slidesPerView</code>には「"auto"」も指定でき<u>CSSでサイズを調整したい</u>ときに使用します。</p><h3 id="h6c4d03bd93">間隔を開ける</h3><pre><code>return (
  &lt;Swiper 
    spaceBetween={50}
  &gt;
   ・・・
  &lt;/Swiper&gt;
)</code></pre><p>CSSの<code>margin</code>で間隔を開けるより、こちらが推奨されています。</p><h3 id="h9179513367">ブレークポイント</h3><p>デバイスの大きさによって<code>slidesPerView</code>の値を変更したい時などに使います。</p><pre><code>return (
  &lt;Swiper 
     breakpoints={{
        {/* ウィンドウの幅が480px以上の場合 */}
        480: { slidesPerView: 2 },
        {/* ウィンドウの幅が1056px以上の場合 */}
        1056: { slidesPerView: 3 },
        ・・・
     }}
  &gt;
   ・・・
  &lt;/Swiper&gt;
)</code></pre><p>以上が私が個人的によく使うオプションです。<br>何度も言うかもですがSwiperのオプションの数は膨大です。なので、今回はこの程度で止めておきたいと思います。もし、ほかにも気になる方は<a href="https://swiperjs.com/swiper-api#parameters" target="_blank" rel="noopener noreferrer">こちら</a>をご覧ください。</p><h2 id="ha214098e44">まとめ</h2><p>今回はReactでのSwiperの使い方についてのお話でしたが、Vueなどの各種フレームワーク用のコンポーネントも用意されています。<br>また機会があれば本ブログでもご紹介できればと思っています。<br>もちろんフレームワークではなくVanillaJSでも使えるので、是非スライダーを実装する際にはSwiperを使用してみてください。<br><br><a href="https://watataku-blog.vercel.app/blog/9t4yqq-zq" target="_blank" rel="noopener noreferrer">https://watataku-blog.vercel.app/blog/9t4yqq-zq</a> ← VanillaJSでの使い方を紹介した記事<br><br><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/l07oog1yp0</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/l07oog1yp0</guid><pubDate>Sat, 04 Nov 2023 03:53:08 GMT</pubDate></item><item><title><![CDATA[Swiperのデザインをいい感じにカスタマイズする方法 | Watatakuのブログ]]></title><description><![CDATA[<p>こんにちは。<br>久しぶりの更新になってしまいました。<br>今回のお話はSwiperを使って「前へ、次へ」の矢印のデザインなどを自身でカスタマイズする方法を紹介していきます。<br></p><h2 id="h2295b759c0">対象者</h2><ul><li>Swiperのデザインをカスタマイズしたい方。</li><li>Swiperの基礎がある程度わかっている方。</li></ul><h2 id="h07e283c641">「前へ、次へ」の矢印のデザインとページネーションのデザインを変更する方法</h2><p>デフォルトで設定した「前へ、次へ」の矢印はしたの画像のような青色の矢印であったりページネーションです。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/f79c08e9649441129b0650d43852d5ca/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-11-01%2012.09.05.png" alt=""><br>それを今回はしたの画像のようにCSSで変更してみたいと思います。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/d5cb0b9e947a48a4b2b0d3b903b0519f/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-11-01%2012.14.39.png" alt=""><br>まずは「前へ、次へ」の矢印のデザインから変えていきます。</p><pre><code>.swiper-button-prev::after,
.swiper-button-next::after {
  content: "";
}</code></pre><p>上記のセレクタを弄るのですが、デフォルトでは<code>.swiper-button-prev::after</code>に<code>content: "prev";</code> 、<code>.swiper-button-next::after</code>に<code>content: "next";</code> が設定されていて、各青い矢印が表示されています。これを<code>content: "";</code> で打ち消してやります。<br>打ち消した後に、各セレクタに矢印とかをCSSで記述していきます。</p><pre><code>.swiper-button-prev::after,
.swiper-button-next::after {
  content: "";
}

.swiper-button-prev {
  width: 40px;
  height: 40px;
  background-color: #fff;
  cursor: pointer;
  opacity: 0.7;
  left: 0;
}

.swiper-button-prev::after {
  display: inline-block;
  vertical-align: middle;
  color: #000;
  line-height: 1;
  width: 14px;
  height: 14px;
  border: 4px solid currentColor;
  border-left: 0;
  border-bottom: 0;
  box-sizing: border-box;
  transform: translateX(23%) rotate(-135deg);
}

.swiper-button-next {
  width: 40px;
  height: 40px;
  background-color: #fff;
  cursor: pointer;
  opacity: 0.7;
  right: 0;
}

.swiper-button-next::after {
  display: inline-block;
  vertical-align: middle;
  color: #000;
  line-height: 1;
  width: 14px;
  height: 14px;
  border: 4px solid currentColor;
  border-left: 0;
  border-bottom: 0;
  box-sizing: border-box;
  transform: translateX(-20%) rotate(-315deg);
}</code></pre><p>次にページネーションのデザインを変更していきます。<br>ページネーションのデザインを変更する場合は<code>.swiper-pagination-bullet</code>を触ります。</p><pre><code>.swiper-pagination-bullet {
  background-color: #111;
  height: 6px;
  width: 6px;
  margin: 0 16px !important;
}</code></pre><h2 id="hf730a4b43c">左右にはみ出すスライド</h2><p>自分自身も執筆していて表現が正しいのか微妙なかんじですが下記の画像のようなデザインです。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/ac4b7e2db6824b01b4afec846885aef6/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-11-01%2016.12.48.png" alt=""></p><pre><code>new Swiper(".swiper", {
  loop: true,
  speed: 500,
  autoplay: {
    delay: 3500,
    disableOnInteraction: false,
  },
  pagination: {
    el: ".swiper-pagination",
    clickable: true,
  },
  navigation: {
    nextEl: ".swiper-button-next",
    prevEl: ".swiper-button-prev",
  },
  centeredSlides: true,
  slidesPerView: 1,
  breakpoints: {
    600: {
      slidesPerView: 1.6, 
      spaceBetween: 40,
    },
  },
});</code></pre><p><br>左右にはみ出すスライドを実装するために重要になってくるプロパティは<code>slidesPerView</code>です。<br>このプロパティは画面に何枚のスライドを表示させるかを指定するものでデフォルトの値は「1」です。<br>これを「1.6」などのように小数にすることで、<u>複数スライドを表示かつスライドをはみ出させる</u>ことができます。<br><br>もう一つ、重要なプロパティがあります。それが<code>centeredSlides</code>です。<br>このプロパティの値を「true」にすることで<u>スライドが中央寄せで表示</u>されるようになります。<br><br>あとは<code>spaceBetween</code>でスライド間の余白を調整すれば完成です。<br><span style="color:#d75969">※CSSのmarginプロパティでスライド間を開けるより、こちらを使う方が推奨されています。</span><br></p><h2 id="h73d3c0432b">「前へ、次へ」の矢印、ページネーションを外に出す</h2><p>「前へ、次へ」の矢印、ページネーションを外に出す場合まずはHTML構造を変更する必要があります。<br>通常のHTML構造では下記の通りです。</p><pre><code>&lt;div class="swiper"&gt;
  &lt;div class="swiper-wrapper"&gt;
    &lt;div class="swiper-slide"&gt;
      &lt;img src="img/・・・" alt="" /&gt;
    &lt;/div&gt;
    &lt;div class="swiper-slide"&gt;
      &lt;img src="img/・・・" alt="" /&gt;
    &lt;/div&gt;
    &lt;div class="swiper-slide"&gt;
      &lt;img src="img/・・・" alt="" /&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="swiper-pagination"&gt;&lt;/div&gt;
  &lt;div class="swiper-button-prev"&gt;&lt;/div&gt;
  &lt;div class="swiper-button-next"&gt;&lt;/div&gt;
&lt;/div&gt;</code></pre><p>これを以下のように作り替えます。</p><pre><code>&lt;div class="wrapper"&gt;
  &lt;div class="swiper"&gt;
    &lt;div class="swiper-wrapper"&gt;
      &lt;div class="swiper-slide"&gt;
        &lt;img src="img/・・・" alt="" /&gt;
      &lt;/div&gt;
      &lt;div class="swiper-slide"&gt;
        &lt;img src="img/・・・" alt="" /&gt;
      &lt;/div&gt;
      &lt;div class="swiper-slide"&gt;
        &lt;img src="img/・・・" alt="" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="swiper-pagination"&gt;&lt;/div&gt;
  &lt;div class="swiper-button-prev"&gt;&lt;/div&gt;
  &lt;div class="swiper-button-next"&gt;&lt;/div&gt;
&lt;/div&gt;</code></pre><p><code>.swiper</code>をさらに<code>div要素</code>で囲み<code>.swiper-pagination</code>、<code>.swiper-button-prev</code>、<code>.swiper-button-next</code>を<code>.swiper</code>と同じ階層に配置します。<br><code>.swiper</code>には「overflow:hidden」がついており、これはswiperスライダにおいて外せないものなので、「前へ、次へ」の矢印、ページネーションのパーツを外に移動させたいからです。<br><br>HTML構造を書き換えたらCSSで位置を調整します。<br>ここで注意が必要なのですが必ず一番外側の<code>div要素</code>に「position: relative」を指定してください。</p><pre><code>.wrapper {
  position: relative;
}</code></pre><p>ここまで来たら後は各々適当に各々調整してください。</p><pre><code>.swiper-button-prev {
  left: -10px;
}

.swiper-button-next {
  right: -10px;
}

.swiper-pagination {
  bottom: -30px !important;
}</code></pre><p><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/2a1d3142679945cd8afa3739e0890007/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-11-03%2018.36.29.png" alt=""></p><h2 id="ha214098e44">まとめ</h2><p>今回はSwiperのデザインを変更する方法をご紹介しました。<br>デフォルトの青のままのデザインだけでもいいのですが、Web制作の場面でほぼデフォルトの青のままのデザインで終わる場面はほぼないと思います。そんな場面でも自分でデザインをカスタマイズできるようになればSwiperがもっと使いやすくなるはずです。<br><br>この記事を読んで少しでも参考になれば幸いです。<br><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/2i5p5ecox</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/2i5p5ecox</guid><pubDate>Sun, 15 Oct 2023 06:24:11 GMT</pubDate></item><item><title><![CDATA[Next.jsで動的OGPを作った時のエピソードを書いてみました | Watatakuのブログ]]></title><description><![CDATA[<p>このブログサイトで動的OGPを実装しようとしたお話をします。Next.jsを使用しているので<code>@vercel/og</code>で簡単に実装できます。<u>ただし、Next.js 12.2.3以上でないとこのモジュールが動作しません。</u></p><pre><code>$ npm install @vercel/og</code></pre><p>※インストールした<code>@vercel/og</code>のバージョンは<strong>0.5.6</strong>です。<br></p><pre><code>import { ImageResponse } from "@vercel/og";</code></pre><p>また、Next.js 13.3.0から<code>@vercel/og</code>をインストールしなくても、Next.js APIの<code>next/server</code>から<code>ImageResponse</code>をインポートできるようになり動的OGPを生成できるようになりました。</p><pre><code>import { ImageResponse } from "next/server";</code></pre><h2 id="h32ccf42033">画像を生成してみる</h2><pre><code>// pages/api/og.tsx
import { ImageResponse } from '@vercel/og' // Next.js 12.2.3以上13.3.0未満の場合
import { ImageResponse } from 'next/sever' // Next.js 13.3.0以上の場合

export const config = {
&nbsp; runtime: 'experimental-edge',
}

export default function handler() {
&nbsp; return new ImageResponse(
&nbsp; &nbsp; (
&nbsp; &nbsp; &nbsp; &lt;div
&nbsp; &nbsp; &nbsp; &nbsp; style={{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; display: 'flex',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; justifyContent: 'center',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; alignItems: 'center',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; width: '100%',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; height: '100%',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; backgroundColor: 'white',
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fontSize: '128px',
&nbsp; &nbsp; &nbsp; &nbsp; }}
&nbsp; &nbsp; &nbsp; &gt;
&nbsp; &nbsp; &nbsp; &nbsp; Hello world!
&nbsp; &nbsp; &nbsp; &lt;/div&gt;
&nbsp; &nbsp; ),
     {
      width: 1200,
      height: 600,
    }
&nbsp; )
}</code></pre><p><strong>http://localhost:3000/api/og</strong>にアクセスする。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/546bf59648704a65a16949a23af3f1f0/og%20(2).png" alt=""><br>あとは任意のページにmetaタグを挿入すればOGPが出来上がります。</p><pre><code>import Head from 'next/head'

&lt;Head&gt;
  &lt;title&gt;タイトルです&lt;/title&gt;
  &lt;meta
    property="og:image"
    content="https//hogehoge.com/api/og"
  /&gt;
&lt;/Head&gt;</code></pre><h2 id="h0984899113">動的にタイトルを挿入する</h2><p>それでは、次に動的にタイトルを挿入し、動的なOGPっぽくしたいと思います。<br>結論としては、<strong>https://hogehoge.com/api/og?title=タイトル</strong>のようにクエリパラメータを使用して動的にタイトルを挿入します。</p><pre><code>// pages/api/og.tsx
import { NextApiHandler } from "next";
import { ImageResponse } from "@vercel/og";

export const config = {
  runtime: "experimental-edge",
};

const handler: NextApiHandler = async (req) =&gt; {
  if (!req.url) throw Error("not supported.");
  const { searchParams } = new URL(req.url);
  
  const title = searchParams.get("title");
  const postDate = searchParams.get("postDate");
  return new ImageResponse(
    (
      &lt;div
        style={{
          borderWidth: "36px",
          borderColor: "#5bbee5",
          backgroundColor: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          textAlign: "center",
          alignItems: "center",
          justifyContent: "center",
          color: "black",
          position: "relative",
        }}
      &gt;
        &lt;h2
          style={{
            color: "black",
            fontSize: 64,
          }}
        &gt;
          {title}
        &lt;/h2&gt;
        &lt;div
          style={{
            display: "flex",
            position: "absolute",
            width: "100%",
            bottom: 0,
            paddingRight: 32,
            justifyContent: "flex-end",
          }}
        &gt;
          &lt;h2
            style={{
              color: "black",
              fontSize: 40,
            }}
          &gt;
            {postDate}
          &lt;/h2&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    ),
    {
      width: 1200,
      height: 600,
    }
  );
};

export default handler;</code></pre><p>これで<strong>http://localhost:3000/api/og?title=Next.jsでSVGを表示する&amp;postDate=2022年02月02日投稿</strong>にアクセスすると画像が表示されるはずですがされません。？？？？？？？<br><br><code>@vercel/og</code>ではデフォルトで日本語での表示をサポートしているのですが、文字列に漢字が含まれているとsatori内でパースできずにエラーになってしまいます。</p><pre><code>error - uncaughtException: Error [ERR_UNHANDLED_ERROR]: Unhandled error. (Error: Unsupported OpenType signature &lt;htm
&nbsp; &nbsp; at Object.parseBuffer [as parse] (webpack-internal:///(middleware)/./node_modules/@shuding/opentype.js/dist/opentype.module.js:11455:15)
&nbsp; &nbsp; at vt.addFonts (webpack-internal:///(middleware)/./node_modules/satori/dist/index.wasm.js:18:19200)
&nbsp; &nbsp; at gu (webpack-internal:///(middleware)/./node_modules/satori/dist/index.wasm.js:19:29617)
&nbsp; &nbsp; at async Object.start (webpack-internal:///(middleware)/./node_modules/@vercel/og/dist/index.js:10:2912))</code></pre><p>この問題は日本語フォントを適用することで解決できます。</p><h3 id="h0b769a4504">カスタムフォントを適応させる</h3><p><a href="https://fonts.google.com/" target="_blank" rel="noopener noreferrer">Google Fons</a>あたりから好きなフォントをダウンロードして下さい。<br>ダウンロードしたフォントファイルを<code>public/</code>に格納(※今回は<code>public/font</code>配下に格納しました。)し、以下のように書き換えて下さい。</p><pre><code>// pages/api/og.tsx
import { NextApiHandler } from "next";
import { ImageResponse } from "@vercel/og";

export const config = {
  runtime: "experimental-edge",
};

const notoSansJP = fetch(
  new URL("../../public/font/NotoSansJP-Bold.woff", import.meta.url).toString()
).then((res) =&gt; res.arrayBuffer());

const handler: NextApiHandler = async (req) =&gt; {
  if (!req.url) throw Error("not supported.");
  const { searchParams } = new URL(req.url);
  const fontNoto = await notoSansJP;

  const title = searchParams.get("title");
  const postDate = searchParams.get("postDate");
  return new ImageResponse(
    (
      &lt;div
        style={{
          borderWidth: "36px",
          borderColor: "#5bbee5",
          backgroundColor: "white",
          width: "100%",
          height: "100%",
          display: "flex",
          textAlign: "center",
          alignItems: "center",
          justifyContent: "center",
          color: "black",
          position: "relative",
        }}
      &gt;
        &lt;h2
          style={{
            color: "black",
            fontSize: 64,
            fontFamily: '"NotoSansJP"',
          }}
        &gt;
          {title}
        &lt;/h2&gt;
        &lt;div
          style={{
            display: "flex",
            position: "absolute",
            width: "100%",
            bottom: 0,
            paddingRight: 32,
            justifyContent: "flex-end",
          }}
        &gt;
          &lt;h2
            style={{
              color: "black",
              fontSize: 40,
              fontFamily: '"NotoSansJP"',
            }}
          &gt;
            {postDate}
          &lt;/h2&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    ),
    {
      width: 1200,
      height: 600,
      fonts: [
        {
          name: "NotoSansJP",
          data: fontNoto,
          style: "normal",
        },
      ],
    }
  );
};

export default handler;</code></pre><p>これで、<strong>http://localhost:3000/api/og?title=Next.jsでSVGを表示する&amp;postDate=2022年02月02日投稿</strong><br>にアクセスすると上手く表示されるはずです。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/de034134fc43492aa7df23d072381e0a/og%20(1)%20(1).png" alt=""><br><br>ですが、vercelにデプロイする際エラーになりました。<br>vercelのhobbyプランではEdge関数は1MB以下ではならなければいけません。日本語フォントは重い・・・<br>フォントファイルはサブセット化し4.7MBから527KBに落としたのですがEdge関数の大きさは1.12MBでした。(筆者調べ)<br>他の日本語フォントを試したりしたのですがEdge関数の大きさは1MB以下になりませんでした。hobbyプラン以上のプラン変更も検討しましたがこのためだけにプランを変更するのは流石に厳しい・・・<br>だから諦めました。<br><br><code>@vercel/og</code>のバージョンによって漢字が含まれていてもエラーが出ないバージョンがありました。全て確認したわけではありませんですがバージョン<strong>0.0.19</strong>では漢字が含まれていてもエラーがでませんでした。文字列中に全角スペースがあったり、<code>！</code>や<code>？</code>のような全角の記号があるとsatori内でパースできずエラーになってしまうようです。ちなみに、なぜか<code>。</code>は大丈夫でした。<br>ですが、この方法ではvercelにはデプロイできましたが、vercelでは上手く動いてくれませんでした。<br></p><h2 id="h7ed021f0e3">Next.js 13.3.0以上だと関係なかった</h2><p>結論としてはこちらの方法を使って実装しました。Next.js13.3.0以上から<code>next/server</code>でできるようになりましたと冒頭で述べさせていただきました。</p><pre><code>// pages/api/og.tsx
import { ImageResponse } from "next/server";
import { NextApiHandler } from "next";

export const config = {
&nbsp; runtime: "edge",
};

const handler: NextApiHandler = async (req) =&gt; {
&nbsp; if (!req.url) throw Error("not supported.");

&nbsp; const { searchParams } = new URL(req.url);
&nbsp; const title = searchParams.get("title");
&nbsp; const postDate = searchParams.get("postDate");

&nbsp; return new ImageResponse(
&nbsp; &nbsp; (
&nbsp; &nbsp; &nbsp; &lt;div
&nbsp; &nbsp; &nbsp; &nbsp; lang="ja-JP"
&nbsp; &nbsp; &nbsp; &nbsp; style={{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; borderWidth: "36px",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; borderColor: "#5bbee5",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; backgroundColor: "white",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; width: "100%",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; height: "100%",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; display: "flex",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; textAlign: "center",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; alignItems: "center",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; justifyContent: "center",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color: "black",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; position: "relative",
&nbsp; &nbsp; &nbsp; &nbsp; }}
&nbsp; &nbsp; &nbsp; &gt;
&nbsp; &nbsp; &nbsp; &nbsp; &lt;h2
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; style={{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color: "black",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fontSize: 64,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }}
&nbsp; &nbsp; &nbsp; &nbsp; &gt;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {title}
&nbsp; &nbsp; &nbsp; &nbsp; &lt;/h2&gt;
&nbsp; &nbsp; &nbsp; &nbsp; &lt;div
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; style={{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; display: "flex",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; position: "absolute",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; width: "100%",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; bottom: 0,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; paddingRight: 32,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; justifyContent: "flex-end",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }}
&nbsp; &nbsp; &nbsp; &nbsp; &gt;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;h2
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; style={{
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color: "black",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fontSize: 40,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &gt;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {postDate}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;/h2&gt;
&nbsp; &nbsp; &nbsp; &nbsp; &lt;/div&gt;
&nbsp; &nbsp; &nbsp; &lt;/div&gt;
&nbsp; &nbsp; ),
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; width: 1200,
&nbsp; &nbsp; &nbsp; height: 600,
&nbsp; &nbsp; }
&nbsp; );
};

export default handler;</code></pre><p>Next.js 13.3.0以上だとフォントファイルとか関係なく1MB以下に収めることができ無事vercelにデプロイすることができました。<br><br>元々このブログサイトはNext.js13では動いてなかったのでNext.js13にバージョンを上げたりとか、結構苦労しましたがその苦労に関してはまた別の機会にw<br>今思えば他のやり方があったのでは？と思う気持ちもあるのですが今回はこの辺りで失礼します。それでは良いNext.jsライフを〜！！</p><h2 id="hb4905a33a8">参照</h2><ul><li><a href="https://www.newt.so/docs/tutorials/nextjs-og-image-generation" target="_blank" rel="noopener noreferrer">https://www.newt.so/docs/tutorials/nextjs-og-image-generation</a></li><li><a href="https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation" target="_blank" rel="noopener noreferrer">https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation</a></li><li><a href="https://nextjs.org/docs/app/api-reference/functions/image-response" target="_blank" rel="noopener noreferrer">https://nextjs.org/docs/app/api-reference/functions/image-response</a></li></ul><p><br><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/xvefjk67-ep</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/xvefjk67-ep</guid><pubDate>Sat, 03 Jun 2023 03:56:40 GMT</pubDate></item><item><title><![CDATA[Next.jsでSVGの表示方法を調べてみました。 | Watatakuのブログ]]></title><description><![CDATA[<p>Next.jsでSVGを表示する方法について考える機会があったので、今回書いてみようと思った次第です。このブログサイトにも至る所にSVGを使っていますが全て今記事の表示方法で行なっています。<br><br>WebサイトにSVGを表示する手段は以下の通りです。</p><ul><li>HTMLの<code>&lt;img&gt;</code>で表示する</li><li>CSSの<code>background-image</code>プロパティで表示する</li><li>SVG内のコードをHTMLに埋め込む</li></ul><p><br>上の二つの手段で表示した場合は、SVG内のスタイルをHTMLもしくはCSSで変更できません。<br>一方、最後に書いた手段を使えばスタイルを変更することができ、外部ファイルへのHTTPリクエスト回数を減らせることからもSVGはインラインで埋め込むことをお勧めします。<br><br>ただし、Next.js(React)内にSVG内のコードを埋め込むためにSVG内のコードをJSXに対応させるために修正しないといけません。この作業って意外とめんどくさいんですよねw<br>そこで、今回は<strong>SVGファイルをReactコンポーネントとして直接importできる</strong>ようにしました。</p><h2 id="h6099da804d">SVGファイルをReactコンポーネントとして直接importできるように準備する</h2><p>まずは<code>@svgr/webpack</code>というものをインストールします。</p><pre><code>$ npm install @svgr/webpack</code></pre><p>次に、<code>next.config.js</code>に以下を記述して下さい。</p><pre><code>/** @type {import('next').NextConfig} */
const nextConfig = {
&nbsp; webpack: (config) =&gt; {
&nbsp; &nbsp; config.module.rules.push({
&nbsp; &nbsp; &nbsp; test: /\.svg$/,
&nbsp; &nbsp; &nbsp; use: [
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; loader: "@svgr/webpack",
&nbsp; &nbsp; &nbsp; &nbsp; },
&nbsp; &nbsp; &nbsp; ],
&nbsp; &nbsp; });
&nbsp; &nbsp; return config;
&nbsp; },
};

module.exports = nextConfig;</code></pre><p>以上の設定を行うことによって、SVGファイルをReactコンポーネントとしてimportできるようになります。</p><h3 id="h36f25b84a1">TypeScriptを使っている場合</h3><p>先ほどの<code>next.config.js</code>に以下を追加して下さい。</p><pre><code>/** @type {import('next').NextConfig} */
const nextConfig = {
&nbsp; webpack: (config) =&gt; {
&nbsp; &nbsp; config.module.rules.push({
&nbsp; &nbsp; &nbsp; test: /\.svg$/,
&nbsp; &nbsp; &nbsp; use: [
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; loader: "@svgr/webpack",
&nbsp; &nbsp; &nbsp; &nbsp; },
&nbsp; &nbsp; &nbsp; ],
&nbsp; &nbsp; });
&nbsp; &nbsp; return config;
&nbsp; },
   // -----------------------追加---------------------------
&nbsp; images: {
&nbsp; &nbsp; disableStaticImages: true,
&nbsp; },
   // ---------------------------------------------------------
};

module.exports = nextConfig;</code></pre><p><code>disableStaticImages: true</code>を設定することで、Next.jsでデフォルトで定義されている画像の型定義設定を無効にできます。<br>無効にすることで、次に行うSVGの型定義を有効化できます。<br>任意の場所に<code>index.d.ts</code>を作成して下さい。SVGの型定義を行います。</p><pre><code>declare module '*.svg' {
  const content: React.FC&lt;React.SVGProps&lt;SVGElement&gt;&gt;;
  export default content;
}</code></pre><h2 id="hb8ab4fa7ac">実際にSVGをReactコンポーネントとして読み込む</h2><p>上記の設定が完了したら、実際にSVGをコンポーネントとして呼び出します。</p><pre><code>import Icon from "../public/icon/icon.svg";

const IconViewComponent = () =&gt; {
  return &lt;Icon /&gt;
}

export default IconViewComponent;</code></pre><p>いい感じに表示できていると思います。<br><br><a href="https://www.npmjs.com/package/@svgr/webpack" target="_blank" rel="noopener noreferrer">https://www.npmjs.com/package/@svgr/webpack</a></p>]]></description><link>https://watataku-blog.vercel.app/blog/fe1rtsgxrf_w</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/fe1rtsgxrf_w</guid><pubDate>Sat, 27 May 2023 04:06:19 GMT</pubDate></item><item><title><![CDATA[Next.js13のApp Routerについて勉強した | Watatakuのブログ]]></title><description><![CDATA[<p>Next.js13.4からApp Routerが<strong>Stable</strong>になったので改めて勉強してみようと思ったので記事にしてみました。<br>この記事ではブログサイトを作りながら、Next.js13の機能について紹介していきたいと思います。<br><a href="https://watataku-blog-app-router.vercel.app/" target="_blank" rel="noopener noreferrer">https://watataku-blog-app-router.vercel.app/</a>　← 今回のデモ</p><h2 id="hbe10bea12e">プロジェクトの作成</h2><p>まずは下記コマンドを入力し、アプリの雛形を作成します。</p><pre><code>$ npx create-next-app@latest</code></pre><p>上記コマンド入力後、いくつか質問されるので答えていきましょう。</p><ul><li>このプロジェクトの名前はなんですか？</li></ul><pre><code>What is your project named? … 任意の名前</code></pre><ul><li>このプロジェクトで TypeScript を使用しますか?</li></ul><pre><code>Would you like to use TypeScript with this project? … No / Yes</code></pre><p>自分は「Yes」を選択しました。</p><ul><li>このプロジェクトで ESLint を使用しますか?</li></ul><pre><code>Would you like to use ESLint with this project? … No / Yes</code></pre><p>自分は「Yes」を選択しました。</p><ul><li>このプロジェクトで Tailwind CSS を使用しますか?</li></ul><pre><code>Would you like to use Tailwind CSS with this project? … No / Yes</code></pre><p>自分は「Yes」を選択しました。</p><ul><li>このプロジェクトで <code>src/</code> を使用しますか?</li></ul><pre><code>Would you like to use `src/` directory with this project? … No / Yes</code></pre><p>自分は「No」を選択しました。</p><ul><li>App Router を使用しますか (推奨)?</li></ul><pre><code>Use App Router (recommended)? … No / Yes</code></pre><p>自分は「Yes」を選択しました。</p><ul><li>デフォルトのインポートエイリアスをカスタマイズしますか?</li></ul><pre><code>Would you like to customize the default import alias? … No / Yes</code></pre><p>自分は「No」を選択しました。<br><br>全ての解答後以下のコマンドを入力後「ウェルカムページが表示されます」</p><pre><code>$ npm run dev</code></pre><p><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/85ac65f3f88c442a8de9a6f31e85bf81/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-05-19%2016.40.42.png" alt=""></p><h2 id="h6120fc7ce7">App Routerについて</h2><p>Next.js13の1番の目玉の新機能ではないでしょうか？ここではApp Router(app/)について解説します。</p><ul><li>ルーティング</li></ul><p>今回肝となるファイルが<code>page.tsx</code>です。雛形を作った段階では<code>app/</code>の直下に<code>page.tsx</code>があります。これによりルートにアクセスすると「ウェルカムページ」が表示されるわけです。<br>では、<code>/about</code>のページを作るにはどうすればいいのでしょうか？<br><code>app/about/page.tsx</code> を作成するだけです。</p><pre><code>// app/about/page.tsx

const About = () =&gt; {
  return &lt;h1&gt;Aboutページ&lt;/h1&gt;;
};

export default About;</code></pre><ul><li>データフェッチ</li></ul><p><code>getStaticProps</code>や&nbsp;<code>getServerSideProps</code>&nbsp;は App Router では使えません。ではどうすればいいのかというと、代わりに Server Component(後述) で&nbsp;<code>async/await</code>を使用してデータを取得できます。また、データ取得時には<code>fetch</code>APIを使用します。(今回の作ったブログサイトでは<code>microcms-js-sdk</code>を使用しています。)</p><pre><code>fetch('https://...'); or fetch('https://...', { cache: 'force-cache' }); // キャッシュを利用するしてデータを取得する</code></pre><p><br></p><pre><code>fetch('https://...', { cache: 'no-store' }); // キャッシュを利用せずに常に新しいデータを取得する</code></pre><p><br></p><pre><code>fetch('https://...', { next: { revalidate: 10 } }); // キャッシュされたデータを一定の間隔で再検証する。リソースの有効期間 (秒)</code></pre><ul><li>コンポーネント</li></ul><p>コンポーネントは<code>app/</code>に作っていきます。ただし、デフォルトでは<strong>Server Component</strong>になります。<br><strong>Client Component</strong>として扱いたい場合、ファイルの最初の１行に<code>"use client"</code> と記述します。</p><h2 id="hfa8e971522">ブログサイトのトップページの作成</h2><p>以上を踏まえ、トップページを作成していきます。コンテンツ管理には「microCMS」を使っていきますので「microCMS」を使う準備をしていきましょう。</p><pre><code>$ npm install --save microcms-js-sdk</code></pre><p><u>※バージョンが</u><strong><u>2.5.0</u></strong><u>(2022/06/15時点)以上であることを確認すること。</u>確認する理由は2.5.0以上でないとApp Routerに対応していないから(表現が正しいかどうかはわかりませんw)。詳しくは下記リンクから<br><a href="https://blog.microcms.io/microcms-js-sdk-2_5_0/" target="_blank" rel="noopener noreferrer">https://blog.microcms.io/microcms-js-sdk-2</a><a href="https://blog.microcms.io/microcms-js-sdk-2_5_0/" target="_blank" rel="noopener noreferrer"><em>5</em></a><a href="https://blog.microcms.io/microcms-js-sdk-2_5_0/" target="_blank" rel="noopener noreferrer">0/</a><br><br><u>※今回の記事データ新規でデータを作成するのがめんどくさかったので、既存のブログデータを使用しています。</u><br></p><pre><code>// libs/client.js
import { createClient } from "microcms-js-sdk";

export const client = createClient({
  serviceDomain: process.env.NEXT_PUBLIC_SERVICE_DOMAIN as string,
  apiKey: process.env.NEXT_PUBLIC_API_KEY as string,
});</code></pre><h3 id="h7d151f5b96">ブログ一覧を作成する</h3><pre><code>// app/page.tsx
import { client } from "../libs/client";
import type { Blog, BlogContents } from "../types/blog";
import Card from "./Card";

const getAllPosts = async (): Promise&lt;Blog[]&gt; =&gt; {
  const data: BlogContents = await client
    .get({
      customRequestInit: {
        next: {
          revalidate: 10,
        },
      },
      endpoint: "blog",
    })
    .catch((e) =&gt; {});

  // エラーハンドリングを行うことが推奨されている
  if (!data) {
    throw new Error("Failed to fetch articles");
  }

  return data.contents;
};
export default async function Home() {
  const blogs = await getAllPosts();
  return (
    &lt;ul className="w-[1100px] tbpc:w-[95%] maxsp:w-[100%] min-h-[calc(100vh_-_170px)] m-auto flex flex-wrap justify-between maxsp:justify-center"&gt;
      {blogs.map((blog: Blog) =&gt; {
        return (
          &lt;Card
            id={blog.id}
            thumbnail={blog.thumbnail.url}
            title={blog.title}
            tags={blog.tags}
            publishedAt={blog.publishedAt}
            key={blog.id}
          /&gt;
        );
      })}
    &lt;/ul&gt;
  );
}</code></pre><p>getAllPosts()でデータフェッチしています。</p><h4 id="h45d723b204">エラーハンドリング</h4><p>サーバーコンポーネント内で例外がthrowされた場合<code>app/error.tsx</code>の内容が表示されます。</p><pre><code>// app/error.tsx
"use client"; // Error components must be Client components
import { useEffect } from "react";

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () =&gt; void;
}) {
  useEffect(() =&gt; {
    console.error(error);
  }, [error]);

  return (
    &lt;main className="relative flex justify-center items-center min-h-[calc(100vh_-_170px)]"&gt;
      &lt;h2 className="text-4xl dark:text-white"&gt;予期せぬエラーが発生しました&lt;/h2&gt;
      &lt;div className="absolute bottom-3"&gt;
        &lt;button onClick={() =&gt; reset()}&gt;Try again&lt;/button&gt;
      &lt;/div&gt;
    &lt;/main&gt;
  );
}</code></pre><p>Errorコンポーネントは以下のpropsを受け取ります</p><ul><li><code>error</code> : throwされた例外オブジェクト</li><li> <code>reset</code>: 例外が発生したコンポーネントを再レンダリングするための関数</li></ul><p>また、<code>app/error.tsx</code>は<strong>Client component</strong>として扱われるため<code>use client</code>をつけましょう。</p><h4 id="hce5b268dc3">Cardコンポーネントを作成する</h4><p>先ほど、コンポーネントは<code>app/</code>に作成すると説明しました。なので、<code>app/Card.tsx</code>を作成します。</p><pre><code>// app/Card.tsx
import Link from "next/link";
import Image from "next/legacy/image";
import type { Tags } from "../../types/blog";

type Props = {
  id: string;
  thumbnail: string;
  title: string;
  tags: Tags[];
  publishedAt: string;
};

const Card = (props: Props) =&gt; {
  return (
    &lt;li className="relative mb-2.5 mt-3.5 cursor-pointer  border w-[350px] h-[380px] tbpc:w-[30vw] tbpc:h-[310px] maxsp:w-[95%] border-black dark:border-white  hover:translate-x-0 hover:translate-y-1.5 bg-white dark:bg-black "&gt;
      &lt;Link href={`/blog/${props.id}`} passHref&gt;
        &lt;Image
          className="object-cover aspect-video w-full h-auto"
          src={props.thumbnail}
          unoptimized={true}
          width={350}
          height={200}
          alt={"サムネイル"}
        /&gt;

        &lt;h2 className="pt-3 pr-3 pl-3 text-lg font-medium overflow-hidden webkit-line-clamp dark:text-white"&gt;
          {props.title}
        &lt;/h2&gt;

        &lt;ul className="flex flex-start flex-wrap mt-1"&gt;
          {props.tags.map((tag: Tags) =&gt; {
            return (
              &lt;li
                key={tag.id}
                className="flex justify-center items-center p-0.5 border-solid border-2 ml-2 mb-2 border-[#5bbee5] dark:border-[#7388c0] dark:text-white"
              &gt;
                {tag.tag_name}
              &lt;/li&gt;
            );
          })}
        &lt;/ul&gt;

        &lt;time
          datatype={props.publishedAt}
          className="absolute bottom-[5px] right-[5px] dark:text-white"
        &gt;
          {props.publishedAt}
        &lt;/time&gt;
      &lt;/Link&gt;
    &lt;/li&gt;
  );
};
export default Card;</code></pre><h4 id="hb2f1499b6d">next/imageについて</h4><p>今回の内容から少しずれちゃいますがNext.js13から<code>next/image</code>の仕様が変更されました。<br>詳しくは下記URLをご参照いただきたいのですが今回の話で言うと<code>layout</code>と<code>objectFit</code>が廃止されました。<br>さらに、imgのラッパーがなくなりました。<br>具体的に、Next.js12以前で<code>next/image</code>を仕様すれば下記のようになっていました。</p><pre><code>// Next.js 12未満
&lt;div ... &gt;
  &lt;img ... /&gt;
&lt;/div&gt;

// Next.js 12
&lt;span ... &gt;
  &lt;img ... /&gt;
&lt;/span&gt;</code></pre><p>これが、Next.js13ではラッピングされなくなりました</p><pre><code>&lt;img ... / &gt;</code></pre><p><a href="https://nextjs.org/docs/pages/api-reference/components/image" target="_blank" rel="noopener noreferrer">https://nextjs.org/docs/pages/api-reference/components/image</a><br><br>話を戻しますが、サンプルコードでは<code>next/legacy/image</code>を使ってNext.js12以前のものを使っています。これを使うと、以前までの<code>next/image</code>を使うことができます。<br>ちなみにNext.js13の<code>next/image</code>を使ってサンプルの画像表示と同じように実装すると下記のように実装します。(抜粋)</p><pre><code>import Image from "next/image";

&lt;Image
  className="object-cover aspect-video w-full h-auto"
  src={props.thumbnail}
  unoptimized={true}
  width={350}
  height={200}
  alt={"サムネイル"}
/&gt;</code></pre><h2 id="haade94062b">ブログサイトの個別ページを作る</h2><p>ブログサイトの個別ページを作るためにはダイナミックルーティングとやらを使わないといけません。以前までなら<code>getStaticPaths</code>、<br><code>getServerSidePaths</code>を使っていましたが、当然のことそんなものは使えません。<br>では、どうすればいいのか紹介していきます。</p><h3 id="h95b477fd89">ファイルを作成する</h3><p>以前までは<code>pages/[slug].tsx</code>みたいな感じで、ダイナミックルーティング用のファイルを作成していました。<br>今回のApp Routerでは<code>app/[slug]/page.tsx</code>とします。</p><pre><code>export default function Page({ params }: { params: { slug: string } }) {
  return &lt;h1&gt;{params.slug}&lt;/h1&gt;;
}</code></pre><p>このように書くとparamsにパラメータが渡ってきます。<br><br>これを踏まえて今回のブログページを作っていきます。</p><pre><code>// app/blog/[slug]/page.ts
import { client } from "../../../libs/client";
import type { Blog } from "../../../types/blog";

const getPost = async (slug: string): Promise&lt;Blog&gt; =&gt; {
  const blog: Blog = await client.get({
    customRequestInit: {
      next: {
        revalidate: 10,
      },
    },
    endpoint: "blog",
    contentId: slug,
  });
  // エラーハンドリングを行うことが推奨されている
  if (!blog) {
    throw new Error("Failed to fetch articles");
  }
  return blog;
};

export default async function Page({ params }: { params: { slug: string } }) {
  const blog = await getPost(params.slug);
  return &lt;div dangerouslySetInnerHTML={{ __html: blog.body }} /&gt;;
}

export async function generateStaticParams() {
  const Limit = 999;
  const blogs: BlogContents = await getMicroCMSBlogs(Limit);

  return blogs.contents.map((blog: Blog) =&gt; ({
    slug: blog.id.toString(),
  }));
}</code></pre><h4 id="h1527ee47ab">generateStaticParamsとは</h4><p>Next.js側で用意されているもので、<code>getStaticPaths</code>の代わりに使うもの。(App Routerでは使えないので)<br>※この関数がなくても問題はないがSSGにはならない。</p><h3 id="hc949487255">メタデータを仕込む</h3><p>Metadata APIというもが存在し、そこにメタ情報を書いていきます。(layout.tsxの<code>export const metadata = {}</code>)</p><h4 id="h41914be986">layout.tsxとは</h4><p><code>app/layout.tsx</code>はRoot Layoutと呼ばれ、<strong>すべてのページに適用されるレイアウト</strong>を定義します。<br>Next.jsでは、自動的に<code>&lt;html&gt;</code>や<code>&lt;body&gt;</code>タグを生成しないため、必ず<code>app/layout.tsx</code>でこれらを定義する必要があります。<br><br>少し脱線しましたが、メタデータの設定方法を紹介していきます。</p><pre><code>export const metadata = {
  title: {
    default: "サイトのタイトル",
    template: `%s | サイトのタイトル`, 
  },
  description: "サイトのディスクリプション",
  openGraph: {
    title: "og:title",
    description: "og:description",
    url: "og:url",
    siteName: "og:site_name",
    type: "og:type",
    images: "og:image",
  },
  twitter: {
    card: "twitter:card",
    title: "twitter:title",
    description: "twitter:description",
    images: "twitter:image:src",
  },
};</code></pre><h3 id="hb88ea45833">動的にメタデータを仕込む</h3><p><code>genarateMetadata()</code>と言うものが用意されています。<br>これを使用することで動的にメタデータというものを仕込むことができます。<br>今回のブログサイトの個別ページにメタデータを仕込みます。</p><pre><code>// app/blog/[slug]/page.tsx
import { MetaData } from "next";
...

const getPost = async (slug: string): Promise&lt;Blog&gt; =&gt; {
  // 省略
};

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise&lt;Metadata&gt; {
  const blog = await getPost(params.slug);
  return {
    title: blog.title,
    description: blog.body,
    openGraph: {
      title: blog.title,
      description: blog.body,
      url: "http://localhost:3000/blog/" + blog.id,
      siteName: blog.title,
      type: "article",
      images: blog.thumbnail.url,
    },
    twitter: {
      card: "summary_large_image",
      title: blog.title,
      description: blog.body,
      images: blog.thumbnail.url,
    },
  };
}

export default async function Page({ params }: { params: { slug: string } }) {
  // 省略
}</code></pre><p>これで個別のブログサイトごとに個別のメタ情報、個別のOGP画像を仕込むことができました。</p><h2 id="h41fef054d9">ページング機能を実装する</h2><p>最後にページング機能を実装します。<br><code>/blog/page/2</code>みたいな感じで静的レンダリングしたいのでを生成したいので下記のようなコードになります。</p><pre><code>// app/blog/page/[pageNo]/page.tsx
import { client } from "@/libs/client";
import Card from "@/app/Card";
import Pagination from "@/app/Pagination";
import { range } from "@/functions/function";

const PER_PAGE = 9; // １ページに表示するコンテンツ数

const getAllPosts = async (pageNo: number): Promise&lt;BlogContents&gt; =&gt; {
  const data = await client.get({
    customRequestInit: {
      next: {
        revalidate: 10,
      },
    },
    endpoint: "blog",
    queries: {
      limit:  PER_PAGE,
      offset: (pageNo - 1) * PER_PAGE
    },
  });
  // エラーハンドリングを行うことが推奨されている
  if (!data) {
    throw new Error("Failed to fetch articles");
  }
  return data;
};
export default async function Home({ params }: { params: { pageNo: string } }) {
  const pageNo = Number(params.pageNo);
  const blogs = await getAllPosts(pageNo);
  return (
    &lt;&gt;
      &lt;ul className="w-[1100px] tbpc:w-[95%] maxsp:w-[100%] min-h-[calc(100vh_-_170px)] m-auto flex flex-wrap justify-between maxsp:justify-center"&gt;
      {blogs.map((blog: Blog) =&gt; {
          return (
            &lt;Card
              id={blog.id}
              thumbnail={blog.thumbnail.url}
              title={blog.title}
              tags={blog.tags}
              publishedAt={blog.publishedAt}
              key={blog.id}
            /&gt;
          );
        })}
      &lt;/ul&gt;
      {blogs.totalCount &gt; PER_PAGE &amp;&amp; (
        &lt;Pagination totalCount={blogs.totalCount} /&gt;
      )}
    &lt;/&gt;
  );
}

export async function generateStaticParams() {
  const blogs = awat client.get({
    customRequestInit: {
      next: {
        revalidate: 10,
      },
    },
    endpoint: "blog",
  });
  return range(1, Math.ceil(blogs.totalCount / PER_PAGE)).map((repo) =&gt; ({
    pageNo: repo.toString(),
  }));
}</code></pre><p>次にページングのコンポーネントを見ていきます。</p><pre><code>import Link from "next/link";
import { range } from "../functions/function";

type Props = {
  totalCount: number;
};
const Pagination = (props: Props) =&gt; {
  const PER_PAGE = 9;

  return (
    &lt;div className="flex justify-center mt-4"&gt;
      {range(1, Math.ceil(props.totalCount / PER_PAGE)).map((number, index) =&gt; (
        &lt;p key={index} className="text-center list-none"&gt;
            &lt;Link
              href={`/page/${number}`}
              className="mx-0.5 w-[30px] h-[40px] flex justify-center items-center text-2xl p-[2.5%] rounded-md text-black bg-[#5bbee5] hover:bg-blue-800 hover:text-white dark:text-white dark:bg-[#7388c0] dark:hover:text-black dark:hover:bg-white"
            &gt;
              {number}
            &lt;/Link&gt;
        &lt;/p&gt;
      ))}
    &lt;/div&gt;
  );
};

export default Pagination;</code></pre><p>ページング機能を実装するにあたって<code>page.tsx</code>,<code>Pagination.tsx</code>の両方に出てくる<code>range()</code>について紹介します。<br><br>この<code>range()</code>は自身が作った関数になっていてページング機能を実装するにあたっての「キモ」となる関数です。</p><pre><code>// functions/function.ts
export const range = (start: number, end: number) =&gt;
  [...Array(end - start + 1)].map((_, i) =&gt; start + i);</code></pre><h2 id="hd51b1e9cb3">Not Foundの設定</h2><p>現時点では、4ページ目は存在しません。手動でブラウザの URL に/page/4にアクセスを行います。ですが、表示されるページにはブログの情報が含まれていないだけでエラーは表示されません。<br>ページが存在しない4ページ目にアクセスがあった場合には 404 ページを表示させるために<code>next/navigation</code>の<code>NotFound関数</code>を利用します。</p><pre><code>// app/blog/page/[pageNo]/page.tsx
...
import { notFound } from "next/navigation"; // 追加

const PER_PAGE = 9;

const getAllPosts = async (pageNo: number): Promise&lt;BlogContents&gt; =&gt; {
  // 省略
};
export default async function Home({ params }: { params: { pageNo: string } }) {
  const pageNo = Number(params.pageNo);
  const blogs = await getAllPosts(pageNo);
  // ----------------------追加-------------------------
  if (blogs.contents.length == 0) {
    notFound();
  }
  // ----------------------追加-------------------------
  return (
    &lt;&gt;
      {/* 省略 */}
    &lt;/&gt;
  );
}

export async function generateStaticParams() {
 // 省略
}</code></pre><p>以上を追記することでページが存在しないページにアクセスがあったときは404ページに飛んでくれるようになります。</p><h3 id="h0e6cc330e1">オリジナルのNot Foundページを作成する</h3><p>Pages Routerを使用していた時には、<code>pages/404.tsx</code>に作成していましたが、App Routerでは<code>app/not-found.tsx</code>に作成します。</p><pre><code>// app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    &lt;section className="text-center min-h-[calc(100vh_-_170px)]"&gt;
      &lt;h2 className="text-3xl font-bold mb-6 dark:text-white"&gt;
        &lt;span className="text-red-700 tracking-[5px] text-9xl maxsp:text-8xl mb-4"&gt;
          404
        &lt;/span&gt;
        &lt;br /&gt;
        お探しのページは見つかりませんでした。
      &lt;/h2&gt;
      &lt;p className="text-xl mb-8 maxsp:text-base dark:text-white"&gt;
        あなたがアクセスしようとしたページは削除されたかURLが変更されているため、
        見つけることができません。
        &lt;br /&gt;
        以下の理由が考えられます。
      &lt;/p&gt;
      &lt;ul className="p-5 mb-6 m-auto text-left border-4 border-gray-400 dark:border-gray-50 w-[55%] tbpc:w-[65%] maxsp:w-[85%] dark:text-white"&gt;
        &lt;li className="list-disc ml-5 maxsp:text-xs"&gt;
          記事がまだ公開されていない。
        &lt;/li&gt;
        &lt;li className="list-disc ml-5 maxsp:text-xs"&gt;
          アクセスしようとしたファイルが存在しない。（ファイルの設置箇所を誤っている。）
        &lt;/li&gt;
        &lt;li className="list-disc ml-5 maxsp:text-xs"&gt;URLが間違っている。&lt;/li&gt;
      &lt;/ul&gt;
      &lt;div className="w-[90%] text-right"&gt;
        &lt;Link
          className=" text-blue-800 border-b-blue-800 dark:border-[#ff36ab] dark:text-[#ff36ab] hover:border-b ml-auto"
          href={"/"}
        &gt;
          TOPへ戻る
        &lt;/Link&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  );
}</code></pre><p>最後に今回の記事で作成したApp Routerを使用してのブログサイトのコードも下記リンクにおいておくのでよければご覧下さい。<br><a href="https://github.com/watataku8911/watataku-blog-app-router" target="_blank" rel="noopener noreferrer">https://github.com/watataku8911/watataku-blog-app-router</a><br><br>まだまだ自分もNext.js13についてキャッチアップ中でわかってないところだらけですがこれからどんどんとキャッチアップしていきたいです。それでは良い Next.js ライフを！</p><h2 id="h3de35099b3">参考</h2><p><a href="https://nextjs.org/docs" target="_blank" rel="noopener noreferrer">https://nextjs.org/docs</a><br><a href="https://blog.microcms.io/microcms-next-jamstack-blog/" target="_blank" rel="noopener noreferrer">https://blog.microcms.io/microcms-next-jamstack-blog/</a><br><br><br><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/u9yukxk-d0</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/u9yukxk-d0</guid><pubDate>Fri, 19 May 2023 07:28:22 GMT</pubDate></item><item><title><![CDATA[Reactのパフォーマンスチューニングについて勉強したところをまとめてみた | Watatakuのブログ]]></title><description><![CDATA[<p>Reactにはパフォーマンスを最適化する為のAPIが用意されています。具体的には<code>React.memo</code>,<code>useCallback</code>,<code>useMemo</code>があります。<code>React.memo</code>ではコンポーネントをメモ化し、 <code>useCallBack</code>では関数(コールバック関数)をメモ化。<code>useMemo</code>は値(関数の結果)のメモ化といった役割があります。</p><h2 id="h8ac88859d1">用語解説</h2><blockquote>メモ化とは、計算結果を保持し、それを再利用する手法のことです。メモ化によって各コンポーネントで都度計算する必要がなくなるため、パフォーマンスの向上が期待できます。</blockquote><p><br></p><blockquote>コールバック関数とは、ある関数を呼び出すときに引数に指定する別の関数のこと。</blockquote><p><br></p><blockquote>Reactのレンダリングとは、コンポーネントの本体である関数を評価(実行)すること</blockquote><h3 id="hb46ee580da">Reactの再レンダリングの条件</h3><p>まず前提知識として、Reactの再レンダリングの条件を見ていきます。</p><ol><li>propsの変更</li><li>stateの変更</li><li>contextの変更</li><li>forceUpdate()を使用しての、強制的な再レンダリング</li><li> 親コンポーネントの再レンダリング</li></ol><p>などが挙げられます。</p><h2 id="h692f02ca4c">useMemo</h2><p><code>useMemo</code>は値(関数の結果)を保持するためのhooksです。<br>何回やっても結果が同じ場合の値などをメモリに保存(メモ化)して、初回レンダリング以外に行われる際の再レンダリングでは高価な計算を避けることができます。<br>このhooksを使う事により、不要な再計算をスキップすることができ、パフォーマンスの向上が期待出来ます。</p><h3 id="h92c54cb2ef">書き方</h3><pre><code>useMemo(() =&gt; { 重い処理 }, [依存配列]);</code></pre><p>依存配列に指定した変数に変更がない場合、重い処理が実行されないようになる。ちなみに、依存配列の価を複数指定した場合はどちらか一方が変更されたら重い処理が実行される。<br>依存配列が空なら、何にも依存しないことから、初回1回のみ実行。つまり、依存関係が変わらない場合はキャッシュから値(関数の結果の値)を取ってきます。</p><h3 id="h512de7d259">使用例</h3><pre><code>import React, { useMemo } from "react";

const App = () =&gt; {
  // 再レンダリングされるたびに重い処理が実行される
　　const method = superHeavyProcess(a, b);

  //aまたはbが変化したときだけ重い処理が実行される
  const method = useMemo(() =&gt; {
    superHeavyProcess(a, b)
  }, [a, b]);

  return (
    ・・・
  )
};

export default App;</code></pre><p>このように<code>useMemo</code>を使う事により依存配列に指定しているaまたはbが変更されない限り、再レンダリングが行われてもsuperHevyProcess()が実行されなくなります。→ パフォーマンス向上が期待できる。<br>以下は<code>useMemo</code>のBad Caseです。</p><pre><code>import React, { useMemo } from "react";

const App = () =&gt; {
  // Bad Case①
  const hoge = useMemo(() =&gt; retern "hoge" , [])

  // Bad Case②
  const method = useMemo(() =&gt; {
    superLightProcess(a, b)
  }, [a, b]);

  return (
    ・・・
  )
};

export default App;</code></pre><h2 id="h4886ea6007">useCallback</h2><p><code>useCallBack</code>では関数(コールバック関数)をメモ化するhooksです。<br>このhooksを使う事により、関数の再生成を防ぐことができパフォーマンス向上が期待できます。<br><br>具体的には<strong>「親コンポーネントで作られた関数を子コンポーネント」</strong>に渡す際に使います。<br><em>子コンポーネントに渡さない関数は関数の再生成が行われない</em>ので<code>useCallback</code>を使う必要がありません。</p><h3 id="h92c54cb2ef">書き方</h3><pre><code>useCallback(() =&gt; { callbackFunction }, [依存配列]);</code></pre><p>依存配列に指定した変数に変更がない場合、callbackFunctionが実行されないようになる。ちなみに、依存配列の価を複数指定した場合はどちらか一方が変更されたらcallbackFunctionが実行される。依存配列が変わらなければ、前回のcallbackFunctionを再利用する。<br>依存配列が空なら、コンポーネントがレンダリングされた時に関数が一度生成されますが、以降は同じ関数オブジェクトを再利用するため、再評価されることはありません。</p><h3 id="h512de7d259">使用例</h3><pre><code>import React, { useState, useCallback } from "react"

const App = () =&gt; {
  const [count, setCount] = useState&lt;number&gt;(0);

  const handleClick = useCallback(() =&gt; {
    setCount(prevState =&gt; prevState + 1);
  });

  return (
    &lt;p&gt;count:{ count }&lt;/p&gt;
    &lt;Button onClick={handleClick} /&gt;
  )
}

export default App;</code></pre><p><br></p><pre><code>import React, { FC } from "react"

type Props = {
  handleClick: (event: React.MouseEvent&lt;HTMLElement, MouseEvent&gt;) =&gt; void;
}
const Button:  FC&lt;Props&gt; = ({ handleClick }) =&gt; {
  return (
    &lt;button onClick={handleClick}&gt;カウントアップ&lt;/button&gt;
  )
}

export default Button;</code></pre><p>このように親コンポーネント(App)で作った関数(handleClick)を子コンポーネント (Button)に渡すときに使います。</p><h2 id="h6ce25814b7">React.memo</h2><p><code>React.memo</code>はコンポーネントをメモ化したいときに使います。親コンポーネントから渡ってきたpropsが変更されていたら再レンダリングされ、変更がなければ再レンダリングしなくなります。<br><br>コンポーネント内で複雑な計算や大量のデータの処理を行っている場合や、プロパティが同一であるコンポーネントの再レンダリングを避けたい場合に使用されます。<br>また、前章で紹介させてもらった<code>useCallback</code>と一緒に使うことが推奨されています。<br>これらを一緒に使用することで、propsが変更された場合にのみ、新しい関数を生成して再レンダリングをトリガーすることができます。つまり、propsが変更されていない場合には、以前にキャッシュされた関数が使用され、不要な再レンダリングを回避できるということです。なので、必ずしも一緒に使わなければならないわけではありませんが、パフォーマンスの向上を目的とする場合は、一緒に使用することをおすすめします。</p><h3 id="h92c54cb2ef">書き方</h3><pre><code>const MemoChild = React.memo(親コンポーネントからpropsを受け取る子コンポーネント)</code></pre><h3 id="h512de7d259">使用例</h3><pre><code>import React, { FC } from "react"

type Props = {
  message: string
}
const MemoChild: FC&lt;Props&gt; = React.memo(({ message }) =&gt; {
  return &lt;div&gt;{message}&lt;/div&gt;;
});
export default MemoChild;</code></pre><p>このように<code>React.memo</code>を使うと、親コンポーネントから渡ってくるmessageに変化がなければ<em>MemoChildは再レンダリングしない</em>となります。</p><h3 id="h5afc6dbdcb">TIPS</h3><pre><code>Component definition is missing display name&nbsp; react/display-name</code></pre><p>上記のようなエラーが出る場合があります。<br>このエラーは、<code>React.memo</code>を使用している場合によく発生します。<br><code>React.memo</code>を使用しているときにこのようなエラーが発生する場合、コンポーネントに名前を付けることで解決できます。名前を付ける方法は、以下のように行うことができます<span style="color:#374151">。</span></p><pre><code>const MemoChild: FC&lt;Props&gt; = React.memo(({ message }) =&gt; {
  return &lt;div&gt;{message}&lt;/div&gt;;
});

MemoChild.displayName = 'MemoChild';</code></pre><p>ここで、displayNameプロパティを使用して、コンポーネントに表示名を指定しています。このようにすることで、React.memoでメモ化されたコンポーネントのエラーが解決されます。<br>また調べてみると他にも解決策があるそうだったのでこちらの記事を参考にしてみて下さい。<br><a href="https://qiita.com/tkmd35/items/fb20e1eb36f84dcbc4a1" target="_blank" rel="noopener noreferrer">https://qiita.com/tkmd35/items/fb20e1eb36f84dcbc4a1</a><br><br>また、Next.js では、<code>React.memo</code> が無効になることはありませんが、Next.js が提供する最適化技術の一部が、React.memo が提供する効果と重複していることがあります。そのため、<strong>「Next.js で React.memo を使用することがあまり意味がない」</strong>場合があります。<br><br>Next.jsが提供するSSRやSSGなどの機能により、コンポーネントの初回レンダリング時に必要なデータやプロパティがすでにサーバーサイドで取得され、生成されたHTMLに含まれているため、クライアントサイドでの再レンダリング時には不要なデータの再取得が必要なくなります。これにより、コンポーネントの再レンダリングが高速化され、<code>React.memo</code>によるパフォーマンスの向上効果が相対的に小さくなる可能性があります。また、Next.jsのプリフェッチング機能により、事前に必要なデータやリソースを取得し、コンポーネントが必要になったときにすぐに利用できるようになるため、<code>React.memo</code>による再レンダリングの最適化が不要になる場合もあります。<br>→元々、Next.jsはページプリレンダリングやコンポーネントレンダリングの最適化を提供しているため。<br><em>必ずしも</em><em><code>React.memo</code></em><em>を使用する必要がない場合があるということです。ただし、コンポーネントが多くのプロップスを持ち、再レンダリングのオーバーヘッドが高い場合には、</em><em><code>React.memo</code></em><em>を使用することが依然として効果的であることがあります。</em></p><h2 id="h878c02911b">まとめ </h2><p>これらを使うとパフォーマンスが最適化されますが逆に、<strong>「使いすぎたり適切に使わなかったりすればパフォーマンスが下がったりする」</strong>ので注意が必要です。<br>これらを使いこなしてReact初学者から中級者、上級者になりましょう。</p>]]></description><link>https://watataku-blog.vercel.app/blog/17l_n9b8e0</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/17l_n9b8e0</guid><pubDate>Wed, 03 May 2023 10:08:12 GMT</pubDate></item><item><title><![CDATA[microCMSから取得してきたデータをNext.jsを使ってRSSフィードを生成する方法 | Watatakuのブログ]]></title><description><![CDATA[<p>私が運営しているブログサイトにRSSフィードを実装しました。今回はその時の話をしたいと思います。<br><br>このブログサイトには、コンテンツ管理(ブログ記事)をmicroCMSで管理しているので、microCMSで管理しているブログの記事を<br>取得しxmlで吐いていきたいと思います。<br>尚、フロントエンドはNext.jsです。</p><h2 id="hc3cf2129da">ライブラリをインストール</h2><p>動的にRSSフィードを実装をする上で簡単に実装できるライブラリがあるのでそれをインストールします。</p><pre><code>$ npm install rss</code></pre><p>また今回はtypescriptを使うので、合わせてこちらもインストールします。</p><pre><code>$ npm install --save-dev @types/rss</code></pre><h2 id="hedfa5ee1ed">getServerSidePropsで動的にRSSフィードを生成する</h2><p><code>page/feed.tsx</code>を作成する。</p><pre><code>import React from "react";
import { GetServerSidePropsContext } from "next";
import generateFeed from "../functions/genalate-rss-feed";

export const getServerSideProps = async ({ res }: GetServerSidePropsContext) =&gt; {
  const xml = await generateFeed(); // フィードのXMLを生成する（後述）
	  
  res.statusCode = 200;
  res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate"); // 24時間のキャッシュ
  res.setHeader("Content-Type", "text/xml");
  res.end(xml);
	
  return {
	props: {},
  };
};
	
const feed = () =&gt; null;
export default feed;</code></pre><p><code>getServerSideProps</code>を使用してRSSデータを生成し、直接レスポンスを返すことで、余計なレイアウトやメタタグなどが混入しないようにします。Pageコンポーネント内でHTMLやXMLを返しても、<code>_app.tsx</code>や<code>_document.tsx</code>の中に埋め込まれるため、余計なレイアウトやメタタグなどが混じってしまう可能性があるため、直接レスポンスを返すことでそれを防ぎます。<br><br><code>getServerSideProps</code>の引数で<code>res</code>を受け取ることができ、Headerに<code>Content-Type</code>や<code>Cache-Control</code>を付けた上で、<code>res.end(RSSフィードの中身)</code>という形でレスポンスを返します。これにより、生成されたRSSデータが正しい形式でクライアントに返されるようになります。</p><h2 id="ha10a36ae39">RSSフィードを生成する</h2><p><code>src/functions/genalate-rss-feed.ts</code>を作成します。ここで先ほどインストールした<a href="https://www.npmjs.com/package/rss" target="_blank" rel="noopener noreferrer">rss</a>の登場です。</p><pre><code>import RSS from "rss";
import { createClient } from "microcms-js-sdk"; 
	
export const client = createClient({
　　serviceDomain: process.env.NEXT_PUBLIC_SERVICE_DOMAIN as string, 
  apiKey: process.env.NEXT_PUBLIC_API_KEY as string,
});

const SITE_URL = "サイトのURL";

const generateFeed = async (): Promise&lt;string&gt; =&gt; {
  const feed = new RSS({
    title: "タイトルです",
    description: "ディスクリプションです",
    site_url: SITE_URL,
    feed_url: "/feed",
    language: "ja",
  });
  
  const data = await client.get({
    endpoint: "blog",
  	queries: {
  	  limit: 999,
  	},
  });
  data.contents.forEach((post) =&gt; {
  	feed.item({
  	  title: returnTitle(post.title),
  	  description: returnDiscription(post.body),
  	  date: new Date(post.createdAt),
  	  url: `${SITE_URL}/blog/${post.id}`,
  	});
  });
  
  // XML形式の文字列にする
  return feed.xml();
};
  
export default generateFeed;</code></pre><p><br><a href="https://zenn.dev/catnose99/articles/c7754ba6e4adac" target="_blank" rel="noopener noreferrer">https://zenn.dev/catnose99/articles/c7754ba6e4adac</a></p>]]></description><link>https://watataku-blog.vercel.app/blog/cy_qcxk2o90</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/cy_qcxk2o90</guid><pubDate>Thu, 27 Apr 2023 06:55:05 GMT</pubDate></item><item><title><![CDATA[Swiperを使ってサクッとスライドを作ってみた。 | Watatakuのブログ]]></title><description><![CDATA[<p><a href="https://swiperjs.com/" target="_blank" rel="noopener noreferrer">https://swiperjs.com/</a></p><h2 id="hd8d793506b">インストール</h2><p>インストール方法は3種類あります。</p><ul><li>CDN</li></ul><pre><code>&lt;link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/swiper@9/swiper-bundle.min.css"
/&gt;

&lt;script src="https://cdn.jsdelivr.net/npm/swiper@9/swiper-bundle.min.js"&gt;&lt;/script&gt;</code></pre><ul><li>NPMでインストール</li></ul><pre><code>$ npm install swiper</code></pre><p>また、typescriptをお使いの方は以下のコマンドも叩いてください。</p><pre><code>$ npm install --save-dev @types/swiper</code></pre><p><br><span style="color:#cf3e51">もう一つ、ファイルをダウンロードして使用する方法もあるのですが今回は割愛とします。興味のある方はご自身でお調べになってください。</span></p><h2 id="h81a5ad2f19">Swiperを扱う</h2><p>では、インストールが完了できたと思うので早速Swiperを使ったスライダーを作っていきましょう。<br>まずはHTMLです。</p><pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="ja"&gt;
&nbsp;
&lt;head&gt;
&lt;meta charset="utf-8"&gt;
&lt;meta name="viewport" content="width=device-width"&gt;
&lt;title&gt;Swiper&lt;/title&gt;

&lt;!-- CDNを使う場合のみ記述 --&gt;
&lt;link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/swiper@9/swiper-bundle.min.css"
/&gt;

&lt;/head&gt;
&nbsp;
&lt;body&gt;
  &lt;div class="swiper"&gt;
&nbsp;  &lt;div class="swiper-wrapper"&gt;
      &lt;div class="swiper-slide"&gt;
        &lt;img
&nbsp; &nbsp; &nbsp; &nbsp; src="・・・"
&nbsp; &nbsp; &nbsp; &nbsp; alt=""
&nbsp; &nbsp; &nbsp;  /&gt;
&nbsp; &nbsp;   &lt;/div&gt;
&nbsp; &nbsp;   &lt;div class="swiper-slide"&gt;
&nbsp; &nbsp; &nbsp;  &lt;img
&nbsp; &nbsp; &nbsp; &nbsp;  src="・・・"
&nbsp; &nbsp; &nbsp; &nbsp;  alt=""
&nbsp; &nbsp; &nbsp; /&gt;
&nbsp; &nbsp;   &lt;/div&gt;
&nbsp; &nbsp;   &lt;div class="swiper-slide"&gt;
&nbsp; &nbsp; &nbsp;   &lt;img
&nbsp; &nbsp; &nbsp; &nbsp;   src="・・・"
&nbsp; &nbsp; &nbsp; &nbsp;   alt=""
&nbsp; &nbsp; &nbsp;   /&gt;
&nbsp; &nbsp;   &lt;/div&gt;
&nbsp;   &lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- CDNを使う場合のみ記述 --&gt;
　　&lt;script src="https://cdn.jsdelivr.net/npm/swiper@9/swiper-bundle.min.js"&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p>こちらが基本の作りです。<br><code>.swiper</code><span style="color:#333333"> </span><code>.swiper-wrapper</code><span style="color:#333333">&nbsp;</span><code>.swiper-slide</code>&nbsp;の3つは必須です。<br><br><code>.swiper-wrapper</code><span style="color:#cf3e51">&nbsp;と&nbsp;</span><code>.swiper-slide</code><span style="color:#cf3e51">&nbsp;の間に「他の要素を割り込ませたりするとうまく動かなくなる」のでご注意ください。</span><br><br>次にJavaScriptの記述です。</p><pre><code>// npmでインストールした場合のみ記述
import Swiper　from "swiper";
import "swiper/swiper-bundle.css";
// -----------------------------------------

const mySwiper = new Swiper('.swiper', {
&nbsp;&nbsp;// この中にオプションを書いていく。
});</code></pre><p>ここまで行うとスライダーが動き出します！</p><h3 id="h32a67f9128">オプション</h3><p>ここからオプションについて記述していくのですが数が膨大のため私がよく使うオプションだけを抜粋して書きます。<br></p><h4 id="hd76776d2c4">ページング</h4><pre><code>&lt;div class="swiper"&gt;
&nbsp;&lt;div class="swiper-wrapper"&gt;
  &lt;!-- 省略 --&gt;    
&nbsp;&lt;/div&gt;
  &lt;div class="swiper-pagination"&gt;&lt;/div&gt;&lt;!-- 追加 --&gt;
&lt;/div&gt;</code></pre><p><br></p><pre><code>// npmでインストールした場合、以下の２行を記述しないといけないので注意
import  Swiper, { Pagination } from "swiper";
Swiper.use([Pagination]);
// ----------------------------------------------------

const mySwiper = new Swiper('.swiper', {
  pagination: {
    el: ".swiper-pagination",
    clickable: true, // クリックを可能にする
  },
});</code></pre><h4 id="he3e924b320">前後の矢印</h4><pre><code>&lt;div class="swiper"&gt;
&nbsp;&lt;div class="swiper-wrapper"&gt;
  &lt;!-- 省略 --&gt;    
&nbsp;&lt;/div&gt;
　　&lt;div class="swiper-button-prev"&gt;&lt;/div&gt;&lt;!-- 追加 --&gt;
  &lt;div class="swiper-button-next"&gt;&lt;/div&gt;&lt;!-- 追加 --&gt;
&lt;/div&gt;</code></pre><p><br></p><pre><code>// npmでインストールした場合、以下の２行を記述しないといけないので注意
import  Swiper, { Navigation } from "swiper";
Swiper.use([Navigation]);
// ----------------------------------------------------

const mySwiper = new Swiper('.swiper', {
  navigation: {
    nextEl: ".swiper-button-next",
    prevEl: ".swiper-button-prev",
  },
});</code></pre><h4 id="hf7ee70b482">スピード</h4><p>スライドのスピードを設定できます。デフォルトでは300です。</p><pre><code>const mySwiper = new Swiper('.swiper', {
  speed: 500
});</code></pre><h4 id="hcbe3576d32">ループさせる</h4><p>簡単です。下記コードをつけるだけです。</p><pre><code>const mySwiper = new Swiper('.swiper', {
  loop: true
});</code></pre><h4 id="hd264691107">自動再生</h4><p>デフォルトでは自動再生されないので、させたい場合はオプションを書く必要があります。</p><pre><code>// npmでインストールした場合、以下の２行を記述しないといけないので注意
import  Swiper, { Autoplay } from "swiper";
Swiper.use([Autoplay]);
// ----------------------------------------------------

const mySwiper = new Swiper('.swiper', {
  autoplay: true,
});</code></pre><p>パラメータを追記して細かい設定をすることも可能です。</p><pre><code>const mySwiper = new Swiper('.swiper', {
  autoplay: {
    delay: 1500, // (デフォルトは3000)1.5秒後に次のスライド
&nbsp; &nbsp; disableOnInteraction: false, // 矢印またはページネーションをクリックしても自動再生を止めない
&nbsp; }
});</code></pre><h4 id="h11244fc379">表示させるスライドの枚数を指定する</h4><p>指定した枚数分がコンテナ内に収まるように調整される。</p><pre><code>const mySwiper = new Swiper('.swiper', {
  slidesPerView: 3 // デフォルトでは1
});</code></pre><p>小数点以下の値も指定可能(2.8などの小数を指定すると左右にはみ出たスライダが実装できる)。<br>CSSでサイズ調整したい場合は<code>‘auto’</code>&nbsp;を指定。</p><h4 id="h627921dd59">アクティブなスライドが中央に来るようにする</h4><p>主に、<code>slidesPreView</code>の値が小数を指定したときに使用します。</p><pre><code>const mySwiper = new Swiper('.swiper', {
  centeredSlides: true,
  slidesPerView:  1.4
});</code></pre><h4 id="h0201d4f644">レスポンシブ</h4><p>デバイスのサイズによって<code>slidesPreView</code>の数を変えたいときなどに使います。</p><pre><code>const mySwiper = new Swiper('.swiper', {
  breakpoints: {
    // デバイスが320px以上の時
    320: {
       slidesPerView:  1.4
    },
    // デバイスが640px以上の時
    640: {
       slidesPerView:  2.8
    }
  },
  slidesPerView:  1,
  centeredSlides: true
});</code></pre><h4 id="hb3954a0c7e">スライド感の余白を指定する</h4><p>CSSで margin を付けるとスライドの位置がずれていくことがあるため、こちらで指定するのが推奨。</p><pre><code>const mySwiper = new Swiper('.swiper', {
  spaceBetween: 40 // px
});</code></pre><p><br>先ほども記載しましたが、ここに記載したオプションは本の一部で、僕自身がよく使うものだけ取り上げさせていただきました。なので、ご興味がある方は各自で調べてみてください。また、Jqueryに依存せず使えることやオプションが多く<span style="color:#000000">カスタマイズ性が豊かなポイントもグッドです。</span><br><span style="color:#000000">皆さんもよければSwiperを使ってみてくださいね。</span></p>]]></description><link>https://watataku-blog.vercel.app/blog/9t4yqq-zq</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/9t4yqq-zq</guid><pubDate>Mon, 17 Apr 2023 07:37:26 GMT</pubDate></item><item><title><![CDATA[Tailwind CSSでこう言うときどすればいいのかを解決してみた。 | Watatakuのブログ]]></title><description><![CDATA[<p>みなさんはTailwind CSSを使ったことがありますか？<br>例えば、<code>&lt;h1&gt;</code>に<code>color: red</code>を適用させたけば<code>&lt;h1 class="text-red-800"&gt;</code>のように特定のclassを指定することでStyleを当てていくCSSフレームワークです。<br>導入方法は下記公式サイトにわかりやすく記載がありますのでそちらをご覧ください。<br><a href="https://tailwindcss.com/" target="_blank" rel="noopener noreferrer">https://tailwindcss.com</a></p><h2 id="h86672c8acc">本題</h2><p><a href="https://tailwindcomponents.com/cheatsheet/" target="_blank" rel="noopener noreferrer">https://tailwindcomponents.com/cheatsheet/</a><br>基本はこのサイトに書いてあるクラスを指定すればいいのですが、この中に無いものを指定したい時はどうすればいいかと言うのが本題です。</p><h3 id="h5de200be56">固定値</h3><p>例えば<code>&lt;div&gt;</code>に<code> width: 165px</code>を指定したくてもどこにもありません。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/fa52ecc563594c63a5b0f7c7957a0521/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-04-03%2012.38.02.png" alt=""><br>ではこう言う時はどうすればいいのでしょうか？<br><br><code>[]</code>で固定値を指定する。<br>つまり、この場合で言うと下記のように指定します。</p><pre><code>&lt;div class="w-[165px]"&gt;&lt;/div&gt;</code></pre><h3 id="h378d3365f9">calc()計算</h3><p>Tailwind CSSでも<code>calc()</code>も使えます。</p><pre><code>&lt;div class="h-[calc(100vh_-_80px)]"&gt;&lt;/div&gt;</code></pre><p><br>ミソは空白に<code>_</code>を入れること。<br><em>「Tailwind CSSでは空白には</em><em><code>_</code></em><em>を入れなければなりません。」</em><br>自分はそれを知らなくていつまで経ってもできませんでしたw</p><h3 id="hcb2b18c4f8">Google Fonts</h3><p><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/b2a263d9c1fb4283bcab8b8859a93a1e/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-04-03%2014.49.57.png" alt=""><br><code>font-family</code>もあります。<br>では、どのようにして上記以外のフォントを使えば良いのでしょうか？<br>その方法をこの章ではお伝えします。<br><br>今回は<code>Bad Script</code>というフォントを使用してみます。<br><a href="https://fonts.google.com/specimen/Bad+Script?query=bad" target="_blank" rel="noopener noreferrer">https://fonts.google.com/specimen/Bad+Script?query=bad</a><br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/e2298b266a7543e38a9463d3ac31cc01/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-04-03%2016.09.42.png" alt=""><br>上記のサイトにアクセスし、下記を記述したファイルに<code>@import</code>を追加します。</p><pre><code>@tailwind base;
@tailwind components;
@tailwind utilities;</code></pre><p><br>次のステップです。<br><em>tailwind.config.js</em>を下記のように編集します。</p><pre><code>module.exports = {
  theme: {
    extend: {
      // フォントを追加
      fontFamily: {
        badScript: ["Bad Script", "cursive"],
      },
  // ...</code></pre><p>これで準備完了です。<br>ではこのフォントを実際に使ってみます。</p><pre><code>&lt;h1 class="font-badScript"&gt;TITLE&lt;/h1&gt;</code></pre><h3 id="hf3a351c61b">カスタムプロパティ</h3><p>Tailwind CSS側が用意していないプロパティは自分で作成することができます。<br><br>例えば、文字列が2行を超えると<code>・・・</code>にするプロパティを作成します。<br>先ほどの</p><pre><code>@tailwind base;
@tailwind components;
@tailwind utilities;</code></pre><p>にとある構文を追加すれば、自分でプロパティを作成できます。</p><pre><code>@layer utilities {
  .webkit-line-clamp {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
}</code></pre><p><br></p><pre><code>&lt;p class="webkit-line-clamp"&gt;hogehogehoge・・・・・・・・・・・・・・・&lt;/p&gt;</code></pre><h3 id="h0201d4f644">レスポンシブ</h3><p>Tailwind CSSでは下記のブレークポイントは用意されています。(モバイルファースト)<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/6769f63aa5e24774a4acfa06986f77f1/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3633353230352f66323236613532392d333138342d626361352d303465342d3562336433623561616162372e706e67.png" alt=""><br>今回は自分でブレイクポイントを作ります。<br><br>作り方は<em>tailwind.config.js</em>を下記のように記載しブレークポイントを作成します。</p><pre><code>module.exports = {
  theme: {
    screens: {
      // --------------------------------------------------
      // custom settings
      // --------------------------------------------------
      maxsp: { max: "７６８px" }, 
      sptb: { min: "769px", max: "1279px" }, 
      maxpc: {max: "1280"},
      // --------------------------------------------------

      // -----------------------------------
      // tailwind default
      // --------------------------------------------------
      sm: "640px",
      md: "768px",
      lg: "1024px",
      xl: "1280px",
    },
  // ...</code></pre><p><br></p><pre><code>&lt;div class="w-80 maxsp:w-full"&gt;BOX&lt;/div&gt;

div {
  width: 320px;
}
@media screen and (max-width:768px) {
　　div {
    　　width: 100%;
　　}
}</code></pre><p><br>Tailwind CSSで用意されているレスポンシブはモバイルファーストです。<br>自分はモバイルファーストのレスポンシブに慣れていないので今回PCファーストに書き換えました。<br>この機会にモバイルファーストのレスポンシブに慣れていかないとなぁw<br><br>以上が個人的に「この場合Tailwind CSSでどのように書けばいい」でした。</p><h2 id="he45680f0bc">参考記事</h2><ul><li><a href="https://www.ibulog.com/posts/2021/08/tailwind-fixed-value" target="_blank" rel="noopener noreferrer">https://www.ibulog.com/posts/2021/08/tailwind-fixed-value</a></li><li><a href="https://qiita.com/dtakkiy/items/dd161e2646695b387277" target="_blank" rel="noopener noreferrer">https://qiita.com/dtakkiy/items/dd161e2646695b387277</a></li></ul><h2 id="h7ee4f2333d">追記(2023-04-30)</h2><h3 id="hd28e9553ca">ダークモード</h3><p>Tailwind CSSでダークモードを実装すればいいのでしょうか？</p><h4 id="h6b2b5d41ab">tailwind.config.jsの設定</h4><p>Tailwind CSS でダークモードを有効化するためには、<code>tailwind.config.js</code>&nbsp;ファイルを修正します。</p><pre><code>module.exports = {
 // 省略
  darkMode: // 'media' or 'class'
}</code></pre><ul><li>media</li></ul><p><code>media</code>&nbsp;を指定した場合には、OS の設定に基づいてダークモードを適用するかどうか決定します。ただし、手動でダークモードとライトモードを切り替えることができません。<br>これにより、OS のダークモードの設定値は<code>prefers-color-scheme</code>によって取得されます。</p><ul><li>class</li></ul><p><code>class</code>を指定した場合、ユーザーによって手動でダークモードを切り替えられるようにするために指定します。設定すると、自身のルート要素に対して&nbsp;<code>.darkクラス</code>が付与されてます。付与される場合のみダークモードが適用されるという事になります。</p><pre><code>&lt;html class="dark"&gt;
  &lt;body&gt;
     &lt;!-- ... --&gt;
  &lt;/body&gt;
&lt;/html&gt;</code></pre><p>また、<code>.darkクラス</code>を付与するかしないかはJavaScriptのお力になります。</p><pre><code>&lt;label class="relative inline-flex items-center cursor-pointer"&gt;
  &lt;input  id="checkbox" type="checkbox" value="" class="sr-only peer"&gt;
  &lt;div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"&gt;&lt;/div&gt;
  &lt;span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"&gt;Toggle me&lt;/span&gt;
&lt;/label&gt;

&lt;script&gt;
const element = document.querySelector("#checkbox");
element.addEventListener("change", myfunc);

function myfunc() {
  if (element.checked) {
    document.documentElement.classList.add('dark')
    localStorage.theme = 'dark'
&nbsp;} else {
&nbsp; &nbsp;document.documentElement.classList.remove('dark')
     localStorage.theme = 'light'
&nbsp;}
}
&lt;/script&gt;</code></pre><p>HTMLのスイッチの箇所は下記リンクのサンプルコードから拝借してきました。<br><a href="https://flowbite.com/docs/forms/toggle/" target="_blank" rel="noopener noreferrer">https://flowbite.com/docs/forms/toggle/</a><br>では、最後に現在の状態がダークモードなのかを判別させるコードです。<br>公式サイトにいい感じのコードがありましたのでこちらも拝借です。</p><pre><code>// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (localStorage.theme === 'dark' || (!('theme' in localStorage) &amp;&amp; window.matchMedia('(prefers-color-scheme: dark)').matches)) {
  document.documentElement.classList.add('dark')
} else {
  document.documentElement.classList.remove('dark')
}

// Whenever the user explicitly chooses light mode
localStorage.theme = 'light'

// Whenever the user explicitly chooses dark mode
localStorage.theme = 'dark'

// Whenever the user explicitly chooses to respect the OS preference
localStorage.removeItem('theme')</code></pre><p><a href="https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually" target="_blank" rel="noopener noreferrer">https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually</a><br>ローカルストレージの&nbsp;<code>theme</code>&nbsp;の値を確認し、値が&nbsp;<code>dark</code>&nbsp;であった場合には、<code>&lt;html&gt;</code>&nbsp;タグに&nbsp;<code>.darkクラス</code>を付与します。<br>ローカルストレージに値が存在しない場合、<code>window.matchMedia('(prefers-color-scheme: dark)').matches</code>&nbsp;によって OS のダークモードの設定を確認します。</p><h4 id="hbfc727c1ce">ダークモード時のスタイル</h4><pre><code>&lt;section class="h-screen flex justufy-center items-center bg-gray-100 dark:bg-gray-800"&gt;
  &lt;h1 class="text-3xl text-gray-800 dark:text-gray-100"&gt;こんにちは&lt;/h1&gt;
&lt;/section&gt;</code></pre><p>このように<code>dark:</code>を指定してダークモード時のスタイルを指定します。</p><h2 id="h8b60accf33">追記(2022-05-20)</h2><h3 id="hc04e156993">!important</h3><p>以下のものTailwind CSSで実装したいときどうすればいいのか？</p><pre><code>position: relative !important;</code></pre><p>この<code>!important</code>ってどうすんねん？下記のようにすればいい。</p><pre><code>!relative</code></pre><p><code>!</code>を頭につけるだけでした。</p>]]></description><link>https://watataku-blog.vercel.app/blog/pybgcxv1exz</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/pybgcxv1exz</guid><pubDate>Mon, 03 Apr 2023 03:57:56 GMT</pubDate></item><item><title><![CDATA[Astroでポートフォリオサイトを作った | Watatakuのブログ]]></title><description><![CDATA[<p>今回はAstroというwebフレームワークを使って、自分のポートフォリオサイトを作り直した時の話をしたいと思います。<br><a href="https://watataku-portfolio.vercel.app/" target="_blank" rel="noopener noreferrer">https://watataku-portfolio.vercel.app/</a></p><h2 id="hd8fb52f923">Astro</h2><p>まず初めにAstroというフレームワークを知らない方のために軽く触れておきます。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/9275dd3674cb4b75a4935787a148595e/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-03-24%2018.26.26.png" alt=""><br><a href="https://astro.build/" target="_blank" rel="noopener noreferrer">https://astro.build/</a></p><blockquote>Astroとは、<strong>ReactやVue.jsのようにコンポーネント指向で開発ができる</strong>UIフレームワークです。さらに、<strong>ウェブアプリ</strong>ではなく<strong>ウェブサイト</strong>を作るために用意されたWebフレームワークであったり、<strong>React、Preact、Svelte、Vue、Solidなど様々なUIフレームワーク</strong>を用いて開発できる特徴もあります。</blockquote><p><br>そしてAstroを語る上でもう一つ欠かせないのがAstro Islandsです。<br>これも公式のドキュメントで説明されています。<br><a href="https://docs.astro.build/en/concepts/islands/" target="_blank" rel="noopener noreferrer">https://docs.astro.build/en/concepts/islands/</a><br>簡単に解説すると、ページの各UIコンポーネントを別モノとして扱うことができます。<br>これによりコンポーネント間の優先度設定によるパフォーマンス調整やコンポーネント毎にUIフレームワークを変えられることがメリットとなっています。</p><h2 id="h8304d8c7ba">Astroを採用した理由</h2><ul><li>ウェブアプリではなくウェブサイトを作りたい</li><li>コンポーネント指向で開発したい</li><li>Typescriptで書きたい</li><li>勉強コストをできるだけ抑えたい</li><li>Jamstack！！</li></ul><p>などがありました。これらのことを考慮して今回マッチしたのが「Astro」でした。</p><h2 id="h51801b8ac5">プロジェクトのセットアップ</h2><p>Node.jsは<code>v16.12.0</code>またはそれ以上のバージョンをインストールされている前提で、以下のコマンドでプロジェクトを作成します。</p><pre><code>$ npm create astro@latest</code></pre><p>コマンド入力後、プロジェクト名やTypescriptを使用するかなどを聞かれますのでいい感じに答える。<br>聞かれることは以下の通りです。</p><ul><li>プロジェクト名を指定</li></ul><pre><code>Where would you like to create your new project? … ./watataku-portforio</code></pre><p>『watataku-portforio』という名前のプロジェクト名で作成したいので&nbsp;<code>./watataku-portforio</code>&nbsp;と入力して次に進みます。</p><ul><li>テンプレートの指定</li></ul><pre><code>? Which template would you like to use? › - Use arrow-keys. Return to submit.
❯   Just the basics (recommended)
    Blog
    Portfolio
    Documentation Site
    Empty project</code></pre><p>一番最初の『Just the basics (recommended)』の『基本のみ（推奨）』を選択します。</p><ul><li>依存モジュールのインストール</li></ul><pre><code>? Would you like to install yarn dependencies? (recommended) › (Y/n)</code></pre><p>『Y』をしていして依存モジュールのインストールを開始させます。</p><ul><li>Git リポジトリの初期化を実行</li></ul><pre><code>? Would you like to initialize a new git repository? (optional) › (Y/n)</code></pre><p>Gitリポジトリの初期化をしたいので『Y』を入力して次に進みます。</p><ul><li>TypeScriptの設定</li></ul><pre><code>? How would you like to setup TypeScript? › - Use arrow-keys. Return to submit.
❯   Relaxed
    Strict (recommended)
    Strictest
    I prefer not to use TypeScript</code></pre><p>推奨となっている『Strict (recommended)』を選択します。<br><br>回答後、以下のコマンドで開発サーバを立ち上げ、http://localhost:3000にアクセスします。</p><pre><code>$ cd ./watataku-portforio

$ npm run dev</code></pre><p><br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/bcbbb2a641ec421d905bf6e3c265b09e/astro-init-site.png" alt=""></p><h2 id="hc806dee726">Reactを追加する</h2><p>以下のコマンドを叩けばReactを追加(使えるように)できます。</p><pre><code>$ npx astro add react</code></pre><p><br></p><pre><code>&nbsp;Resolving packages...


&nbsp; Astro will run the following command:
&nbsp; If you skip this step, you can always run it yourself later


&nbsp;╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
&nbsp;│ npm install @astrojs/react @types/react-dom@^18.0.6 @types/react@^18.0.21 react-dom@^18.0.0 react@^18.0.0&nbsp; │
&nbsp;╰─────────────────────────────────────────────────────
? Continue? › (Y/n)</code></pre><p>Y を指定します。</p><pre><code>✔ Resolving packages...

  Astro will make the following changes to your config file:

 ╭ astro.config.mjs ─────────────────────────────╮
 │ import { defineConfig } from 'astro/config';  │
 │                                               │
 │ import react from "@astrojs/react";           │
 │                                               │
 │ // https://astro.build/config                 │
 │ export default defineConfig({                 │
 │   integrations: [react()]                     │
 │ });                                           │
 ╰───────────────────
? Continue? › (Y/n)</code></pre><p>Y を指定します。<br><span style="color:#d75969">※今回はReactを追加しますがVue.jsやSvelteなども追加できます。詳しくはドキュメントをご覧ください。</span><br><a href="https://docs.astro.build/ja/guides/integrations-guide/" target="_blank" rel="noopener noreferrer">https://docs.astro.build/ja/guides/integrations-guide/</a></p><h2 id="h81f6f126c0">microCMSの準備</h2><p>今回のサイトでのコンテンツ管理には「microCMS」と言うHedress CMSを使っています。<br><a href="https://microcms.io/" target="_blank" rel="noopener noreferrer">https://microcms.io/</a><br>microCMSのアクセス方法や設定方法などは今回説明しないので自身でドキュメント等などでご覧ください。<br><br>この辺りを参考にしてみると良いかもです。<br><a href="https://blog.microcms.io/astro-microcms-introduction" target="_blank" rel="noopener noreferrer">https://blog.microcms.io/astro-microcms-introduction</a></p><h2 id="h222fd0f152">ポートフォリオに取り入れた機能</h2><ul><li>慣性スクロール</li></ul><p><a href="https://www.weblio.jp/content/慣性スクロール" target="_blank" rel="noopener noreferrer">https://www.weblio.jp/content/慣性スクロール</a><br>ぬるっと動く気持ちのいいスクロールですねw個人的にこのスクロール方式が好きなので、実装してみたいと思い実装しました。<br><br>今回は「ASScroll」と言うライブラリのお力をお借りして実装しました。<br><a href="https://github.com/ashthornton/asscroll" target="_blank" rel="noopener noreferrer">https://github.com/ashthornton/asscroll</a><br>導入方法は簡単です。<br>まず最初にライブラリをインストールします。</p><pre><code>$ npm install --save @ashthornton/asscroll</code></pre><p>次に下記Javascriptのコードを書いてあげます。</p><pre><code>import ASScroll from "@ashthornton/asscroll";

const asscroll = new ASScroll();
window.addEventListener("load", () =&gt; {
  	asscroll.enable();
});</code></pre><p>あとは慣性スクロールしたいところに<code>&lt;div asscroll-container&gt;</code>で囲むだけ。</p><pre><code>&lt;div asscroll-container&gt;
	 &lt;main&gt;
	       &lt;!-- The Y translation will be applied to this element --&gt;
     &lt;/main&gt;
&lt;/div&gt;</code></pre><ul><li>ダークモード</li></ul><p>こちらで詳しく記載しております。<br><a href="https://watataku-blog.vercel.app/blog/tb7epr3_-z" target="_blank" rel="noopener noreferrer">https://watataku-blog.vercel.app/blog/tb7epr3_-z</a><br>ちなみにダークモードで見た際のサイト(ライトモードはサムネイル)<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/806a1c38d1254371bee941836ff15dd6/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%202023-03-14%2018.30.12.png" alt=""></p><ul><li>ページング</li></ul><p>Astroではページング機能を簡単に実装できる関数があるので、それを今回使用させていただきました。<br>その関数が<code>paginate()</code>です。早速ページングの実装方法を見ていきましょう。<br><br><code>./page/[...page].astro</code>を作成し下記のように<code>pagenate()</code>を使用します。</p><pre><code>// ---　
import type { Page, GetStaticPathsOptions } from "astro";

type Props = {
  page: Page&lt;Contents&gt;;
};

type Work = {
  contents: Contents[];
}

type Contents = {
  // 以下略
};

const { page } = Astro.props;  

export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
  　　const myMicroCMSApiUrlWork = `https://${
	    import.meta.env.MICROCMS_SERVICE_DOMAIN_PORTFOLIO
    }.microcms.io/api/v1/works?orders=-publishedAt?limit=999`;
	
	const response = await fetch(myMicroCMSApiUrlWork, {
	    headers: {
	      "X-MICROCMS-API-KEY": import.meta.env.MICROCMS_API_KEY_PORTFOLIO,
	    },
	});
	const getFetchDataWork: Work = await response.json();
	return paginate(getFetchDataWork.contents, { pageSize: 6 });
}
// ---</code></pre><p><code>getStaticPaths()</code> 関数の戻り値で<code>paginate()</code>を返すだけです。<br>※コメントにしている「<strong>---</strong>」はハイラウトの都合上、わざと付けています。Astroを書く際は「<strong style="background-color:#ffffff">---</strong>」をお忘れなくお願いします。<br><br>中身を見てみると、表示したいコンテンツを第 1 引数に、ページ内に表示したいコンテンツの数を第 2 引数に設定しておきます。<br>これでページングの設定が完了しましたのでテンプレートで使っていきましょう。</p><pre><code>&lt;div&gt;
  {
    page.data.map((work: Contents) =&gt;
      &lt;!-- 以下略 --&gt;
    ))
  }
&lt;/div&gt;
{
　　page.total &gt;= 6 &amp;&amp; (
	&lt;nav&gt;
	    {/* page.currentPageで現在ページ、page.lastPageで総ページ数を表示 */}
	     &lt;p&gt;
	        page {page.currentPage}/{page.lastPage}
	     &lt;/p&gt;
	     &lt;ul&gt;
	       　{/* page.url.prevがある場合は前ページへのリンクを表示 */}
	        {page.url.prev &amp;&amp; (
	         　　&lt;li&gt;
	              &lt;a href={page.url.prev}&gt;PREV&lt;/a&gt;
	            &lt;/li&gt;
	         )}
	          {/* page.url.nextがある場合は次ページへのリンクを表示 */}
	          {page.url.next &amp;&amp; (
	             &lt;li&gt;
	                &lt;a href={page.url.next}&gt;NEXT&lt;/a&gt;
	             &lt;/li&gt;
	           )}
	       &lt;/ul&gt;
　　　　&lt;/nav&gt;
　　)　
}</code></pre><h2 id="h3bcda3e6b0">最後に</h2><p>以上がAstroでポートフォリオサイトを作り直したと言うお話でした。<br>個人的にAstroとても書きやすくサクッと使える印象を受けました。また、他のUIフレームワークとも組み合わせて開発できるのも私としては嬉しいポイントでした。<br><br>みなさんもよければAstro使ってみてください</p>]]></description><link>https://watataku-blog.vercel.app/blog/k_-bnidghv</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/k_-bnidghv</guid><pubDate>Thu, 16 Mar 2023 02:40:41 GMT</pubDate></item><item><title><![CDATA[ダークモードとライトモードを切り替えるスイッチャーを作りました。 | Watatakuのブログ]]></title><description><![CDATA[<h2 id="hd28e9553ca">ダークモード</h2><p>一昨年あたりからwebデザインにおいてもトレンドになったダークモード。<br>「目に優しい」などのさまざまな効果がありますが今回はデモのようなダークモードとライトモードを切り替えるスイッチャーを作りたいと思います。</p><iframe class="embedly-embed" src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fcodepen.io%2Fwatataku8911%2Fembed%2Fpreview%2FNWzzPPG%3Fdefault-tabs%3Dcss%252Cresult%26height%3D600%26host%3Dhttps%253A%252F%252Fcodepen.io%26slug-hash%3DNWzzPPG&display_name=CodePen&url=https%3A%2F%2Fcodepen.io%2Fwatataku8911%2Fpen%2FNWzzPPG&image=https%3A%2F%2Fshots.codepen.io%2Fwatataku8911%2Fpen%2FNWzzPPG-512.jpg%3Fversion%3D1671265134&key=94335756f04b424b8ce3ce71cbe3de0a&type=text%2Fhtml&schema=codepen" width="800" height="600" scrolling="no" title="CodePen embed" frameborder="0" allow="autoplay; fullscreen" allowfullscreen="true"></iframe><p><br></p><h2 id="hedbfa7dcce">スイッチを作る。</h2><p>では、早速ダークモードとライトモードを切り替えるスイッチを作っていきます。<br>スイッチを作るためにはHTMLでチェックボックスを用意する必要があります。</p><pre><code>&lt;form action="#"&gt;
    &lt;label class="switch"&gt;
      &lt;input type="checkbox" class="checkbox"/&gt;
      &lt;span class="slider"&gt;&lt;/span&gt;
    &lt;/label&gt;
&lt;/form&gt;</code></pre><p>これだけでは普通のチェックボックスがひとつできあがるだけです。<br>なのでCSSを適応させます。</p><pre><code>.switch {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 34px;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

.slider:before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  background-color: white;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

input:checked + .slider {
  background-color: #33f321;
}

input:focus + .slider {
  box-shadow: 0 0 1px #33f321;
}

input:checked + .slider:before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}

.slider {
  border-radius: 34px;
}

.slider:before {
  border-radius: 50%;
}</code></pre><p>少し長いかもですがこれで切り替え用のスイッチができました。<br>ですが、このままでは何も変化しない普通のスイッチです。ですのでスイッチをオンにするとダークモードに切り替え、オフるとライトモードに切り替わるようにします。</p><h2 id="hd148c32f21">JavaScript</h2><p>まず、必要になってくるのは「今、スイッチがオンなのかオフなのか」の状態を知らなければいけません。<br>その情報をとってくるには下記コードで取得可能です。</p><pre><code>const element = document.querySelector(".checkbox");
element.addEventListener("change", myfunc);

function myfunc() {
  if (element.checked) {
     console.log("スイッチがオンになりました。");
  } else {
     console.log("スイッチがオフになりました。");
  }
}</code></pre><p>無事チェックボックス(スイッチ)の状態を取得することができました。</p><h2 id="hcfd88cd66f">ダークモードにする</h2><p>ではいよいよダークモードにします。<br>以下のコードでダークモードにできます。</p><pre><code>document.documentElement.setAttribute("data-theme", "dark");</code></pre><p><br>ちなみにダークモードを解除、即ちライトモードにする方法は下記コードです。</p><pre><code>document.documentElement.removeAttribute("data-theme");</code></pre><p><br>従ってスイッチがオンの時にダークモードにオフの時ライトモードにするには</p><pre><code>const element = document.querySelector(".checkbox");
element.addEventListener("change", myfunc);

function myfunc() {
  if (element.checked) {
    document.documentElement.setAttribute("data-theme", "dark");
  } else {
    document.documentElement.removeAttribute("data-theme");
  }
}</code></pre><p>これでスイッチがオンの時にダークモードにオフの時ライトモードに切り替わりましたがまだ何も変わりません。<br><br>最後にCSSでダークモードならこの色、ライトモードならこの色を適応させるようにすれば終わりです。</p><pre><code>:root {
  --bgColor: #fff;
}

[data-theme="dark"] {
  --bgColor: #101010;
}

body {
  background-color: var(--bgColor);
}</code></pre><p>これで完成!!<br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/tb7epr3_-z</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/tb7epr3_-z</guid><pubDate>Fri, 06 Jan 2023 09:48:08 GMT</pubDate></item><item><title><![CDATA[ぴえん。。。突然Vercelにデプロイできなくなったよ〜。。。 | Watatakuのブログ]]></title><description><![CDATA[<p>こんにちは。<br>今回は題名にもありますが、突然Vercelにデプロイできなくなってしまったので私がどのように解決したのかダラダラやっていきます。</p><h2 id="h3a5ddd2120">ログを確認しろ！！</h2><p>当たり前ですが「ログを確認しろ！！」です。<br>ログにはエラーの原因が必ず書いています。(英語なので読む気が失せますが。。。)<br><br>自分の場合も遠回りしましたがログを見ました。<br>その内容がこれです。</p><pre><code>[16:53:54.204] Cloning github.com/watataku8911/watataku-blog (Branch: develop_ver.14, Commit: 7200375) [16:53:54.952] Cloning completed: 748.213ms [16:53:55.338] No Build Cache available [16:53:55.375] Running "vercel build" [16:53:55.851] Vercel CLI 28.2.5 [16:53:56.135] Detected `package-lock.json` generated by npm 7+... [16:53:56.143] Installing dependencies... [16:54:20.016] [16:54:20.016] added 824 packages in 24s [16:54:20.017] [16:54:20.017] 133 packages are looking for funding [16:54:20.017] run `npm fund` for details [16:54:20.044] Detected Next.js version: 12.0.7 [16:54:20.052] Running "npm run build" [16:54:20.346] [16:54:20.347] &gt; build [16:54:20.347] &gt; next build [16:54:20.347] [16:54:22.229] Attention: Next.js now collects completely anonymous telemetry regarding usage. [16:54:22.230] This information is used to shape Next.js' roadmap and prioritize features. [16:54:22.230] You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL: [16:54:22.230] https://nextjs.org/telemetry [16:54:22.230] [16:54:22.245] Browserslist: caniuse-lite is outdated. Please run: [16:54:22.245] npx browserslist@latest --update-db [16:54:22.245] [16:54:22.245] Why you should do it regularly: [16:54:22.245] https://github.com/browserslist/browserslist#browsers-data-updating [16:54:22.286] info - Checking validity of types... [16:54:27.983] info - Creating an optimized production build... [16:54:28.045] &gt; [PWA] Compile client (static) [16:54:28.045] &gt; [PWA] Auto register service worker with: /vercel/path0/node_modules/next-pwa/register.js [16:54:28.046] &gt; [PWA] Service worker: /vercel/path0/.next/sw.js [16:54:28.046] &gt; [PWA] url: /sw.js [16:54:28.047] &gt; [PWA] scope: / [16:54:28.214] Browserslist: caniuse-lite is outdated. Please run: [16:54:28.215] npx browserslist@latest --update-db [16:54:28.215] [16:54:28.215] Why you should do it regularly: [16:54:28.215] https://github.com/browserslist/browserslist#browsers-data-updating [16:54:28.238] &gt; [PWA] Compile server [16:54:38.048] Failed to compile. [16:54:38.048] [16:54:38.048] Please check your GenerateSW plugin configuration: [16:54:38.048] [WebpackGenerateSW] 'pwa' property is not expected to be here. [16:54:38.048] [16:54:38.048] [16:54:38.048] &gt; Build failed because of webpack errors [16:54:38.075] Error: Command "npm run build" exited with 1</code></pre><p>見てるだけで吐き気を起こしてしまいます。<br>でも、よく見るとエラーの原因が書いてありました。</p><pre><code>Browserslist: caniuse-lite is outdated. Please run: [16:54:28.215] npx browserslist@latest --update-db</code></pre><blockquote>Browserslist: caniuse-lite は古くなっています。npx browserslist@latest --update-dbを実行してください。</blockquote><p>とりあえず指示通りに書きコマンドを実行しました。</p><pre><code>$ npx browserslist@latest --update-db</code></pre><p>実行後、デプロイできました。良かった良かったw</p><h2 id="ha214098e44">まとめ</h2><p>ログを見ろ！！以上。さすれば解決できる。</p><h2 id="h7605dc6208">参考文献</h2><ul><li><a href="https://dev.classmethod.jp/articles/asked-to-update-the-browserslist-when-building-react-app/" target="_blank" rel="noopener noreferrer">アプリのビルド時にBrowserslistの更新を要求されるので対処してみる</a></li><li><a href="https://mutter.monotalk.xyz/posts/0e47ac7712c43f814af104b7161672e7" target="_blank" rel="noopener noreferrer">Browserslistをdbを更新しろというエラー</a></li></ul>]]></description><link>https://watataku-blog.vercel.app/blog/3hlxzsbb7iu</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/3hlxzsbb7iu</guid><pubDate>Mon, 17 Oct 2022 09:25:11 GMT</pubDate></item><item><title><![CDATA[IntersectionObserverを使ったスクロールイベント実装 | Watatakuのブログ]]></title><description><![CDATA[<p>従来、スクロールに合わせて要素を操るには&nbsp;<code>scroll</code>&nbsp;というイベントを利用していました。ただ、それだと画面サイズが変わったら再計算しないといけなかったり、スクロールするたびに関数を呼び出すので、パフォーマンスへの悪影響が懸念されていました。<br>それを解決するが本日紹介する<code>IntersectionObserver</code>です。</p><h2 id="h64f1545640">IntersectionObserverって？</h2><p>IntersectionObserverとは、<code>Intersection：交差　, Observer：観察者</code>という意味があります。つまり、<em>特定の要素が指定領域内に入ったかどうかを検知するAPI</em>のことです。なので、スクロールイベント等を実装するときに使用するAPIです。<br><a href="https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API" target="_blank" rel="noopener noreferrer">https://developer.mozilla.org/ja/docs/Web/API/Intersection</a><a href="https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API" target="_blank" rel="noopener noreferrer"><em>Observer</em></a><a href="https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API" target="_blank" rel="noopener noreferrer">API</a></p><h2 id="hfb33a5fbbe">IntersectionObserverの基本的な使い方</h2><p>基本的には&nbsp;<code>IntersectionObserver</code>&nbsp;を呼び出して、第1引数に実行したい関数、第2引数にオプション設定を記述します</p><pre><code>const options = {
&nbsp;&nbsp;root: document.querySelector('#scrollArea'),
&nbsp;&nbsp;rootMargin: '0px',
&nbsp;&nbsp;threshold: [0.2, 1.0]
}
&nbsp;
const observer = new IntersectionObserver(callback, options);</code></pre><p><code>options</code>&nbsp;オブジェクトは、以下のフィールドがあります:</p><h3 id="h1907563bc5">root</h3><p>ターゲットとなる要素が見えるかどうかを判定するためのベース部分を指定します。デフォルトはブラウザのビューポートです。</p><h3 id="h0e63ef043f">rootMargin</h3><p>交差を計算する際は、実際は基準要素の領域にここで指定した余白値を足した領域が計算基準となる。つまり例えばここに正の値を指定すれば、実際に見える前に交差していると判定させることができる。デフォルトは&nbsp;<code>0px</code>&nbsp;です。</p><h3 id="hf6a8a7bf34">threshold</h3><p>関数を実行するタイミングを 0〜1 の間で記述します。コールバックを実行する交差の閾値リスト。交差の割合が閾値を上回るか下回ったときのみコールバック関数が実行される。<em><code>[0, 0.5, 1]</code></em>&nbsp;のように配列形式でも記述できます。上の例でいうと、見えている割合が20％および100％を上回るか下回ったときにコールバックが実行される。デフォルトでは<code>0</code>&nbsp;です。</p><h2 id="h870778ef3e">IntersectionObserverの設定</h2><p>では、実際に<code>IntersecftionObserver</code>を設定し、要素の監視をしてみましょう。</p><pre><code>// 交差を監視する要素の取得
const boxes = document.querySelectorAll(".box");

const options = {
  root: null, // 今回はビューポートをルート要素とする
  rootMargin: "-50% 0px", // ビューポートの中心を判定基準にする
  threshold: 0 // 閾値は0
};
const observer = new IntersectionObserver(doWhenIntersect, options);

// それぞれのboxを監視する
boxes.forEach(box =&gt; {
  observer.observe(box);
});

/**
 * 交差したときに呼び出す関数
 * @param entries
 */
function doWhenIntersect(entries) {
  entries.forEach(entry =&gt; {

    // --------------------- この構文が肝 ------------------------------
　　　 // 交差検知をしたもののなかで、isIntersectingがtrueのDOMにactiveクラスを付与。そうでなければ、activeクラスを取り除く
    if (entry.isIntersecting) {
      entry.target.classList.add("active");
    } else {
      entry.target.classList.remove("active");
    }
    // ---------------------------------------------------------------------

  });
}</code></pre><p><br></p><pre><code>.box {
  option: 0;
}
.box.active {
  option: 1;
}</code></pre><p>最後までお読みいただきありがとうございました。<br>参考になれば幸いです<br><br><span style="color:#cf3e51">※</span><strong style="color:#cf3e51">閾値(しきいち)</strong><span style="color:#cf3e51">とは、境界となる値。</span></p>]]></description><link>https://watataku-blog.vercel.app/blog/wz5p-uh3-0m</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/wz5p-uh3-0m</guid><pubDate>Wed, 10 Aug 2022 05:16:41 GMT</pubDate></item><item><title><![CDATA[注意！！firebaseの仕様が変わってました(storage編) | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。<br>Watatakuです。<br>今回の書いていく内容なんですけれども、タイトルから分かる通り仕様が変わっていました。<br>v8までを使用している方は以前の書き方で大丈夫なのですが、これから新規でfirebaseのプロジェクトを始めるときに書き方が変わりますというお話です。<br>その中でも今回はstorageをやっていきます。<br>storageはcloud storageと呼ばれ、Firebaseが提供している機能の一つで、写真や動画など、ユーザーが作成したコンテンツを保管、提供する必要のあるアプリ デベロッパー向けに構築されています。<br><br>以前はQiitaの方に書かせて頂いたのは仕様が変わる前の記事になっているので、仕様変更前と後の見比べに合わせてご覧ください。<br><a href="https://qiita.com/watataku8911/items/7292a04590aad5b4dd19" target="_blank" rel="noopener noreferrer">https://qiita.com/watataku8911/items/7292a04590aad5b4dd19</a><br></p><h2 id="h3ed8c36a02">画像をアップロードする</h2><pre><code>import { initializeApp } from "firebase/app";
import { getStorage, ref, uploadBytes  } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const storage = getStorage(app);

// ファイルへの参照を作る。
const　imageRef = ref(storage, 'hogehoge.jpg');

// 'file' は Blob または File API から取得される。
uploadBytes(storageRef, file).then((snapshot) =&gt; {
  console.log('アップロードができました');
}).catch(() =&gt; {
  console.log('アップロードができませんでした');
});</code></pre><p>「ファイルへの参照を作る」に関しては下記のように記述するとフォルダとファイルを作れるようになります。</p><pre><code>const　imageRef = ref(storage, 'bowbow/hogehoge.jpg');</code></pre><h2 id="h99197c576f">保存した画像の保存先URLを取得する</h2><p>保存した画像の保存先URLを取得するメソッドは<code>getDownloadURL()</code>を使用します。 </p><pre><code>import { initializeApp } from "firebase/app";
import { getStorage, ref, getDownloadURL } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const storage = getStorage(app);

const　imageRef = ref(storage, 'hogehoge.jpg');
getDownloadURL(imageRef).then((downloadURL) =&gt; {
  console.log(downloadURL);
}).catch(() =&gt; {
  console.log("ダウンロードURLが取得せきませんでした");
});</code></pre><p>この取得した、downloadURLを<strong>firestore</strong>などに保存してやってください。<br><br><a href="https://firebase.google.com/docs/storage/web/start?hl=ja" target="_blank" rel="noopener noreferrer">https://firebase.google.com/docs/storage/web/start?hl=ja</a><br><br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/w89_n5q20evt</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/w89_n5q20evt</guid><pubDate>Wed, 27 Apr 2022 08:00:20 GMT</pubDate></item><item><title><![CDATA[注意！！firebaseの仕様が変わってました。(authentication編) | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。<br>Watatakuです。<br>今回の書いていく内容なんですけれども、タイトルから分かる通り仕様が変わっていました。<br>v8までを使用している方は以前の書き方で大丈夫なのですが、これから新規でfirebaseのプロジェクトを始めるときに書き方が変わりますというお話です。<br>その中でも今回はauthenticationをやっていきます。<br><br>以前はQiitaの方に書かせて頂いたのは仕様が変わる前の記事になっているので、仕様変更前と後の見比べに合わせてご覧ください。<br><a href="https://qiita.com/watataku8911/items/9f134b44ead087abe68b" target="_blank" rel="noopener noreferrer">https://qiita.com/watataku8911/items/9f134b44ead087abe68b</a><br></p><h2 id="h60abfd515b">authentication</h2><p><a href="https://firebase.google.com/docs/auth/?hl=ja" target="_blank" rel="noopener noreferrer">公式ページ</a><br><br>Firebaseが提供してくれているサービスの１つでFirebase Authentication を使用すると、ユーザーがアプリにログインする際に、<br>メールアドレスとパスワードのログイン、Google ログインや Facebook ログインなどのフェデレーション ID プロバイダなど、複数のログイン方法を使用できるようになります。</p><h3 id="hd854b561c4">ユーザ登録</h3><p>使用するメソッド</p><pre><code>createUserWithEmailAndPassword(authインスタンス, "emailアドレス", "パスワード")</code></pre><p><br></p><pre><code>import { initializeApp } from "firebase/app";
import { createUserWithEmailAndPassword, getAuth } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
createUserWithEmailAndPassword(auth, "hoge@gmail.com", "passW0rd")
.then(( userCredential) =&gt; {
  console.log('user created');
  console.log(userCredential)
})
.catch((error) =&gt; {
  alert(error.message)
  console.error(error)
}); </code></pre><p><br></p><h3 id="ha6fec2e82b">ログイン</h3><p>使用するメソッド</p><pre><code>signInWithEmailAndPassword(authインスタンス, "emailアドレス", "パスワード")</code></pre><p><br></p><pre><code>import { initializeApp } from "firebase/app";
import { createUserWithEmailAndPassword, getAuth } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
signInWithEmailAndPassword(auth, "hoge@gmail.com", "passW0rd")
.then((user) =&gt; {
  console.log('ログイン成功=', user.user.uid)
})
.catch((error) =&gt; {
  console.error(error)
});</code></pre><p><br></p><h4 id="h217ef38d18">その他のログイン</h4><ul><li>Googleログイン</li></ul><pre><code>import { initializeApp } from "firebase/app";
import { getAuth, signInWithPopup, GoogleAuthProvider } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
.then((user) =&gt; {
  console.log('Googleアカウントでログインしました。')
})
.catch((error) =&gt; {
  console.error(error)
});</code></pre><ul><li>Facebookログイン</li></ul><pre><code>import { initializeApp } from "firebase/app";
import { getAuth, signInWithPopup, FacebookAuthProvider } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

const provider = new FacebookAuthProvider();
signInWithPopup(auth, provider)
.then((user) =&gt; {
  console.log('Facebookアカウントでログインしました。')
})
.catch((error) =&gt; {
  console.error(error)
});</code></pre><ul><li>Twitterログイン</li></ul><pre><code>import { initializeApp } from "firebase/app";
import { getAuth, signInWithPopup, TwitterAuthProvider } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

const provider = new TwitterAuthProvider();
signInWithPopup(auth, provider)
.then((user) =&gt; {
  console.log('Twitterアカウントでログインしました。')
})
.catch((error) =&gt; {
  console.error(error)
});</code></pre><ul><li> Githubログイン</li></ul><pre><code>import { initializeApp } from "firebase/app";
import { getAuth, signInWithPopup, GithubAuthProvider } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

const provider = new GithubAuthProvider();
signInWithPopup(auth, provider)
.then((user) =&gt; {
  console.log('Githubアカウントでログインしました。')
})
.catch((error) =&gt; {
  console.error(error)
});</code></pre><p><br></p><h3 id="h5ae5a82600">サインアウト</h3><p>使用するメソッド</p><pre><code>signOut(authインスタンス)</code></pre><p><br></p><pre><code>import { initializeApp } from "firebase/app";
import { createUserWithEmailAndPassword, getAuth } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
signOut(auth)
.then((user) =&gt; {
  console.log('ログアウトしました')
})
.catch((error) =&gt; {
  console.log(`ログアウト時にエラーが発生しました (${error})`);
});</code></pre><p><br></p><h3 id="h07516b8bcf">現在ログインしているユーザーを取得する</h3><p>使用するメソッド</p><pre><code>onAuthStateChanged(authインスタンス, () =&gt; {})</code></pre><p><br></p><pre><code>import { initializeApp } from "firebase/app";
import { onAuthStateChanged, getAuth } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
onAuthStateChanged(auth, (user) =&gt; {
&nbsp; if (user) {
&nbsp; &nbsp; // User is signed in, see docs for a list of available properties
&nbsp; &nbsp; const uid = user.uid;
&nbsp; &nbsp; // ...
&nbsp; } else {
&nbsp; &nbsp; // User is signed out
&nbsp; &nbsp; // ...
&nbsp; }
});</code></pre><p>あとは、ドキュメントを見て。。。。</p>]]></description><link>https://watataku-blog.vercel.app/blog/qt6nfpe1fo</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/qt6nfpe1fo</guid><pubDate>Tue, 26 Apr 2022 09:45:30 GMT</pubDate></item><item><title><![CDATA[注意！！firebaseの仕様が変わってました。(firestore編) | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。<br>Watatakuです。<br>今回の書いていく内容なんですけれども、タイトルから分かる通り仕様が変わっていました。<br>v8までを使用している方は以前の書き方で大丈夫なのですが、これから新規でfirebaseのプロジェクトを始めるときに書き方が変わりますというお話です。<br>その中でも今回はfirestoreをやっていきます。</p><h2 id="hdd4f8a207b">firestore</h2><p><a href="https://firebase.google.com/docs/firestore?hl=ja" target="_blank" rel="noopener noreferrer">公式ページ</a></p><ul><li>firebaseが提供しているNoSQLのデータベース。</li><li>firebaseには同じく<em>RealTimeDatabase</em>というものもありますが基本的には<em>firestore</em>の仕様が推奨されている。</li></ul><p><br></p><h3 id="ha278f9d5a2">create</h3><p>データの作成は、ドキュメントIDを指定する場合と指定せずに自動採番してもらう方法があります。<br>ID指定してドキュメントを作成する方法</p><pre><code>import { initializeApp } from "firebase/app";
import { doc , setDoc , getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// データの書き込み＝通信を挟む＝非同期処理
await setDoc(doc(db,"users", "000"), {
  address: {
    prefecture: "沖縄",
    region: "沖縄",
  },
  age: 20,
  createdAt: "2020年11月10日 15:00:00 UTC+9",
  friends: ["003"],
  userName: "山田太郎",
});
await setDoc(doc(db,"users", "001"), {
  address: {
    prefecture: "北海道",
    region: "北海道",
  },
  age: 20,
  createdAt: "2020年11月8日 12:00:00 UTC+9",
  friends: [],
  userName: "小島三郎",
});
await setDoc(doc(db,"users", "002"), {
  address: {
    prefecture: "東京",
    region: "関東",
  },
  age: 19,
  createdAt: "2020年11月1日 9:14:00 UTC+9",
  friends: ["001"],
  userName: "山田花子",
});
await setDoc(doc(db,"users", "003"), {
  address: {
    prefecture: "大阪",
    region: "関西",
  },
  age: 50,
  createdAt: "2020年11月10日 15:00:00 UTC+9",
  friends: [""],
  userName: "山田花子",
});</code></pre><h3 id="hc4ed09a5f8">[TIPS]</h3><p>IDを指定せずに作るパターン</p><pre><code>import { initializeApp } from "firebase/app";
import { collection , addDoc , getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const userReference = addDoc(collection(db, "users_sub"), {
  address: {
    prefecture: "名古屋",
    region: "中部",
  },
  age: 26,
  createdAt: "2021年1月3日 19:4:00 UTC+9",
  friends: ["004"],
  userName: "小一のしょういち",
});</code></pre><p>createだけを見てみても前との書き方の差がありますね。</p><h3 id="hf4867d209f">update</h3><p>データの更新になります。</p><pre><code>import { initializeApp } from "firebase/app";
import { doc. updateDoc, getFirestore } form "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const userRef = doc(db, "users" , "００３")
await updateDoc(userRef, {
  address: {
    prefecture: "群馬",
    region: "北関東",
  },
  age: 27,
  createdAt: "2022年12月2日 19:4:00 UTC+9",
  friends: ["001"],
  userName: "小五のしょうご",
});</code></pre><h3 id="hb22c116794">delete</h3><p>ドキュメントの削除についてです。</p><pre><code>import { initializeApp } from "firebase/app";
import { doc , deleteDoc, getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

// 匿名アカウントの情報を削除します
await deleteDoc(doc(db, "users" , "００3");</code></pre><h3 id="h6ffa98d953">read</h3><p>今のデータを見るとこうなっています。</p><pre><code>// コレクション
users: {
  // ドキュメント
  "000": {
    // フィールド
    address: {
      prefecture: "沖縄",
      region: "沖縄",
    },
    age: 20,
    createdAt: "2020年11月10日 15:00:00 UTC+9",
    friends: ["003"],
    userName: "山田太郎",
  },
  "001": {
    address: {
      prefecture: "北海道",
      region: "北海道",
    },
    age: 29,
    createdAt: "2020年11月8日 12:00:00 UTC+9",
    friends: [],
    userName: "小島三郎",
  },
  "002": {
    address: {
      prefecture: "東京",
      region: "関東",
    },
    age: 19,
    createdAt: "2020年11月1日 9:14:00 UTC+9",
    friends: ["001"],
    userName: "山田花子",
  },
}</code></pre><p>このデータを用いてクエリを投げていきます。<br></p><h4 id="h03b293e3a8">全件取得</h4><pre><code>import { initializeApp } from "firebase/app";
import {
  doc,
  getDocs,
  getFirestore
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = doc(db, "users");

getDocs(usersRef).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>000:山田太郎
001:小島三郎
002: 山田花子</code></pre><p><br></p><h4 id="h920c70ab4f">where</h4><h5 id="h074a4c8443">&lt; :  ~より小さい</h5><ul><li>未成年（20歳未満）のユーザを抽出</li></ul><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("age", "&lt;", 20))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>002: 山田花子</code></pre><h5 id="h1fe5fb4b42">&lt;= ： 以下</h5><ul><li>ユーザIDが001以下のユーザを抽出する</li></ul><p><code>firebase.firestore.FieldPath.documentId()</code>でドキュメントIDを参照できる</p><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  documentId,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where(documentId(), "&lt;=", "001"))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>000: 山田太郎
001: 小島三郎</code></pre><h5 id="h9ad7a2b909">== ： 等しい</h5><ul><li>東京在住のユーザを抽出する</li></ul><p>フィールド内のオブジェクトにあるプロパティはjsで参照するときと同様に、<code>オブジェクト名.プロパティ名</code>で参照できる</p><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("address.prefecture", "==", "東京"))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>002: 山田花子</code></pre><h5 id="hc1f8c519bd">!= ： 等しくない</h5><ul><li>東京に住んでいないユーザを抽出</li></ul><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("address.prefecture", "!=", "東京"))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>000: 山田太郎
001: 小島三郎</code></pre><h5 id="h51be8b22e0">&gt;= ： ~以上</h5><ul><li>作成日が2020年11月9日以降のユーザを抽出</li></ul><p>FirebaseのTimestamp型は、日付オブジェクトをクエリに利用できる。</p><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

// 2020年11月9日の日付インスタンスを生成
const targetDate = new Date("2020-11-09");

getDocs(query(usersRef, where("createdAt", "&gt;=", targetDate))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>000: 山田太郎</code></pre><h5 id="h4c4776cefb">array-contains：配列内に、右辺の要素が含まれている</h5><ul><li>ユーザID「003」がフレンドのユーザを抽出</li></ul><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("friends", "array-contains", "003"))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>000: 山田太郎</code></pre><h5 id="h602b9f48df">array-contains-any：配列内に、右辺の要素のいずれかが含まれている</h5><ul><li>ユーザID「002, 003, 004」のいずれかがフレンドのユーザを抽出</li></ul><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("friends", "array-contains-any", ["001", "002", "003"]))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>000: 山田太郎</code></pre><h5 id="h7cd03b4aca">in：右辺のいずれかが含まれている</h5><ul><li>ユーザ名が「山田太郎」もしくは「山田花子」を抽出</li></ul><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("userName", "in", ["山田太郎", "山田花子"]))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>000: 山田太郎
002: 山田花子</code></pre><h5 id="h58eb1a6426">not-in：右辺のいずれも含まれない</h5><ul><li>ユーザ名が「山田太郎」でも「山田花子」でもないユーザを抽出</li></ul><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("userName", "not-in", ["山田太郎", "山田花子"]))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>001: 小島三郎</code></pre><h5 id="hc1602b438e">複合クエリ ： AND</h5><ul><li>20代のユーザを抽出する</li></ul><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("age", "&gt;=", 20), where("age", "&lt;=", 29))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>000: 山田太郎
001: 小島三郎</code></pre><h4 id="hc38d8b0af6">orderBy</h4><p>ソートは<code>orderBy</code>で行う。<code>orderBy("age")</code>とすることで、ageで昇順ソート、<code>orderBy("age", "desc")</code>とすると、ageで降順ソートされる。</p><h5 id="h8c5f7ef737">年齢を昇順で表示</h5><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  orderBy,
  query,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, orderBy("age"))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>山田花子, 19歳
山田太郎, 20歳
小島三郎, 29歳</code></pre><h5 id="h20a737eef6">年齢を降順で表示</h5><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  orderBy,
  query,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, orderBy("age", "desc"))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り</p><pre><code>小島三郎, 29歳
山田太郎, 20歳
山田花子, 19歳</code></pre><h4 id="h4cd61f9fa1">limit</h4><p>最大表示件数の指定は<code>limit()</code>で行う。<code>limit(1)</code>とすると最大1件分、<code>limit(10)</code>とすると最大10件分取得する</p><pre><code>import { initializeApp } from "firebase/app";
import {
  collection,
  getDocs,
  getFirestore,
  limit,
  query,
} from 'firebase/firestore';

const firebaseConfig = {
  apiKey: " process.env.API_KEY",
  authDomain: "process.env.AUTH_DOMAIN",
  projectId: "process.env.PROJECT_ID",
  storageBucket: "process.env.STORAGE_BUCKET",
  messagingSenderId: "process.env.MESSAGING_SENDER_ID",
  appId: "process.env.APP_ID",
  measurementId: "process.env.MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const usersRef = collection(db, "users");

getDocs(query(usersRef, limit(1))).then(snapshot =&gt; {
  snapshot.forEach(doc =&gt; {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})</code></pre><p>上記の結果は以下の通り。</p><pre><code>山田太郎, 20歳</code></pre><h2 id="h3bcda3e6b0">最後に</h2><p>firebase本当に便利なので使って見てください。<br>この記事を見てみてfirebaseの設定がわからない方や以前の書き方ってどうだったんだろうと気になるかたは、<br>以前私がQiitaにまとめた記事がありますので良ければご覧ください。<br><a href="https://qiita.com/watataku8911/items/ac040f4671c0f9a62bd4" target="_blank" rel="noopener noreferrer">https://qiita.com/watataku8911/items/ac040f4671c0f9a62bd4</a><br><br>また近いうちに「firebase storage」や「firebase authentication」について書きます。<br></p>]]></description><link>https://watataku-blog.vercel.app/blog/33i6uh2dlx</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/33i6uh2dlx</guid><pubDate>Fri, 22 Apr 2022 07:04:30 GMT</pubDate></item><item><title><![CDATA[$ npx create-react-appができなくなってしまった。。。 | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。<br>Watatakuです。<br>突然ですが、<code>$ npx creat-react-app</code> ができなくなりました。</p><pre><code>$ npx create-react-app my-app
Need to install the following packages:
  create-react-app
Ok to proceed? (y) y

You are running `create-react-app` 4.0.3, which is behind the latest release (5.0.0).

We no longer support global installation of Create React App.

Please remove any global installs with one of the following commands:
- npm uninstall -g create-react-app
- yarn global remove create-react-app

The latest instructions for creating a new app can be found here:
https://create-react-app.dev/docs/getting-started/</code></pre><p><br>は？<br></p><h2 id="h5d27caa348">とりあえず、指示通りやる</h2><pre><code>$ npm uninstall -g create-react-app

up to date, audited 1 package in 237ms

found 0 vulnerabilities</code></pre><p><br>無理。。。<br></p><h2 id="h7e60ca8c44">ちょっと待て・・・！！</h2><pre><code>Need to install the following packages:
  create-react-app</code></pre><p><br>あっ・・・<br>ちゃんとエラー見よな。。。<br></p><h2 id="h141087ae17">解決法</h2><p>キャッシュクリアする。</p><pre><code>$ npx clear-npx-cache</code></pre><p><br></p><h2 id="h3de35099b3">参考</h2><p><a href="https://qiita.com/kurab/items/6c6056d06d053631b596" target="_blank" rel="noopener noreferrer">npx create-react-app できない時</a><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/ajcd5iximk</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/ajcd5iximk</guid><pubDate>Fri, 25 Mar 2022 07:14:45 GMT</pubDate></item><item><title><![CDATA[Next.jsにおける環境変数の設定方法。 | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんわ。<br>Watatakuです。<br>今回の書いていく内容は「Next.js における環境変数の基本的な設定方法」です。<br><br><code>env</code>&nbsp;ファイルの基本的な使い方の参考になれば幸いです！</p><h2 id="hdad28025e6">環境変数の基本的な設定方法</h2><p>バージョン 9.4 以上の Next.js から、環境変数のサポートが組み込まれています。<br><br>環境変数を設定する基本的な方法は以下の通りです。</p><ul><li>基本的には&nbsp;<code>.env.local</code>&nbsp;に環境変数を定義（詳細は後述）</li><li>設定した環境変数を呼び出すときは&nbsp;<code>process.env.環境変数名</code>&nbsp;とする</li><li>ブラウザで環境変数を使用したい場合は、先頭の文字を&nbsp;<code>NEXT_PUBLIC_</code>&nbsp;とする</li></ul><pre><code>// .env
API_KEY=abcdefghijk</code></pre><p><br></p><pre><code>// pages/index.tsx
import { GetStaticProps } from 'next';

// サーバー側で実行
export const getStaticProps: GetStaticProps = async () =&gt; {
  console.log(process.env.API_KEY);
  ...
};

// クライアント側で実行
export default function Index(): JSX.Element {
  return (
    &lt;&gt;
      {process.env.API_KEY}
    &lt;/&gt;
  );
}</code></pre><p>Next.js では&nbsp;<code>env</code>&nbsp;ファイル名に応じて、<strong>開発 / 本番環境で切り替えることができます</strong>。具体的なルールは以下の通りです。<br></p><ul><li><code>.env</code>：常に読み込み</li><li><code>.env.development</code>：開発環境</li><li><code>.env.production</code>：本番環境</li><li><code>.env.local</code>：常に読み込み</li><li><code>.env.development.local</code>：開発環境</li><li><code>.env.production.local</code>：本番環境</li></ul><p><br>基本的にシークレットな値は&nbsp;<code>.env*.local</code>&nbsp;で定義を行い、<code>.gitignore</code>&nbsp;でリポジトリから除外します。<br>一方で、<code>.env</code>&nbsp;/&nbsp;<code>.env.development</code>&nbsp;/&nbsp;<code>.env.production</code>&nbsp;は、リポジトリに含めます。<strong>URL や企業名などデフォルト値などを定義するときに使いましょう</strong>。<br></p><h2 id="h7605dc6208">参考文献</h2><p><a href="https://nextjs-ja-translation-docs.vercel.app/docs/basic-features/environment-variables" target="_blank" rel="noopener noreferrer">https://nextjs-ja-translation-docs.vercel.app/docs/basic-features/environment-variables</a><br><a href="https://fwywd.com/tech/next-env" target="_blank" rel="noopener noreferrer">https://fwywd.com/tech/next-env</a><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/uc7uqx65s</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/uc7uqx65s</guid><pubDate>Thu, 17 Mar 2022 08:34:32 GMT</pubDate></item><item><title><![CDATA[Next.jsで作成したこのブログに目次をつけたい | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。Watatakuです。<br>今回のお話はこのサイトに「目次をつけた」ってお話です。<br><br>目次をつけたいけどどうやってつけるかわからなかったのですが<br>調べると意外と簡単につけれたのでご紹介したいと思います。<br></p><h2 id="h1eea724d7c">必要パッケージのインストール</h2><p>以下のパッケージをインストールしてください。</p><pre><code>$ npm install cheerio</code></pre><p>これは、<u>データクロール用の非常に強力なJavaScriptライブラリ</u>です。<br><br>また、今回はTypescriptを扱うのでこちらの方もインストールします。</p><pre><code>$ npm install --save @types/cheerio</code></pre><h2 id="h6957ba1257">目次を作る</h2><p>インストールしたパッケージを読み込む。</p><pre><code>import cheerio from "cheerio";</code></pre><p><br><code>getstaticProps()</code>内に以下のコードを追加してください。</p><pre><code>export const getStaticProps = async (
  context: GetStaticPropsContext&lt;{ id: string }&gt;
) =&gt; {
  const data = await fetch( ・・・ ); // ブログデータ取得
  
// ---------------------------追加----------------------------------
  const $ = cheerio.load(data.body);　// data.bodyにはブログの記事情報が入っている。

  const headings = $("h1, h2, h3, h4, h5").toArray();

  const toc = headings.map((data: any) =&gt; ({
    text: data.children[0].data,
    id: data.attribs.id,
    name: data.name,
  }));
// ---------------------------追加----------------------------------


  return {
    props: {
      toc,
    },
  };
};</code></pre><p><br>目次を表示する。</p><pre><code>&lt;ul id="lists"&gt;
  {toc.map((toc, index) =&gt; (
    &lt;li id={"list" + toc.name} key={index}&gt;
        &lt;a href={"#" + toc.id}&gt;{toc.text}&lt;/a&gt;
    &lt;/li&gt;
  ))}
&lt;/ul&gt;</code></pre><h2 id="ha214098e44">まとめ</h2><p>以上。<br>意外と簡単にできましたね。<br>もし目次の付け方がわからない人はご参考に。</p>]]></description><link>https://watataku-blog.vercel.app/blog/f7t03gpoh7</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/f7t03gpoh7</guid><pubDate>Sun, 06 Feb 2022 08:03:42 GMT</pubDate></item><item><title><![CDATA[ソースコードにシンタックスハイライトをつける方法 | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。<br>Watatakuです。<br><br>今回の記事はこのブログにシンタックスハイライトをつけた話です。<br>では、やっていきます。<br></p><h2 id="h9404e7a89f">必要モジュールをインストールする</h2><p>まずは下記コマンドで必要なモジュールをインストールします。</p><pre><code>$ npm install highlight.js cheerio</code></pre><p><br>typescriptをお使いの方は下記も一緒に。</p><pre><code>$ npm install --save @types/highlightjs @types/cheerio</code></pre><p><br></p><h2 id="h0ff70c627e">実装する</h2><p>下記コードを<code>getStaticProps</code> に追加してください。</p><pre><code>import cheerio from "cheerio";
import hljs from "highlight.js";
import type {
  InferGetStaticPropsType,
  GetStaticPropsContext,
  NextPage,
} from "next";

type Props = InferGetStaticPropsType&lt;typeof getStaticProps&gt;;

export const getStaticProps = async (
  context: GetStaticPropsContext&lt;{ id: string }&gt;
) =&gt; {
  const data = await client.get({
    endpoint: "blog",
    contentId: context.params?.id,
  });

  const $ = cheerio.load(data.body); // data.body はブログの本文が入っていると思ってください

// ------------------------------------------ 追加 ------------------------------------------
  $("pre code").each((_, elm) =&gt; {
    const result = hljs.highlightAuto($(elm).text());
    $(elm).html(result.value);
    $(elm).addClass("hljs");
  });
// ------------------------------------------ 追加 ------------------------------------------
  return {
    props: {
      highlightedBody: $.html(),
    },
  };
};

const Detail: NextPage&lt;Props&gt; = ({ highlightedBody }) =&gt; {
    return (
        &lt;div dangerouslySetInnerHTML={{ __html: highlightedBody }} /&gt;
    )
};
export default Detail;
 </code></pre><p><br>なお、このページに、</p><pre><code>import 'highlight.js/styles/hybrid.css';</code></pre><p>を追加して、見た目を修正しています。<br>これで、このブログのようにソースコードのシンタックスハイライトが実現できました。<br></p><h2 id="h3bcda3e6b0">最後に</h2><p>技術ブログを作るにあたり、シンタックスハイライトをつけたい方のご参考に<br>なればと思います。</p><ul><li><a href="https://qiita.com/cawauchi/items/ff6489b17800c5676908" target="_blank" rel="noopener noreferrer">参考</a></li><li><a href="https://www.blogchin.net/blogs/p9f3s1x5k5yb/" target="_blank" rel="noopener noreferrer">参考２</a></li></ul><p><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/s014yfhs15c</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/s014yfhs15c</guid><pubDate>Wed, 02 Feb 2022 07:57:21 GMT</pubDate></item><item><title><![CDATA[node.jsは公式サイトからダウンロードするのではなくnodebrewでインストールせよ。 | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。<br>Watatakuです。<br>今回の記事は題名にもなっていますが、公式ページからnode.jsをダウンロードするよりnodebrewでインストールしましょう<br>という記事になります。<br><br>その理由は、もちろん公式のサイトからnode.jsをダウンロードすれば簡単にお持ちのPC にインストールされますが、<br>node.jsのバージョンの管理がめちゃめちゃ大変になります。<br>経験を積んでいくとプロジェクトにより、node.jsのバージョンのハシゴをしないといけないこともあるでしょう。<br>公式サイトからダウンロードをしていればその度にnode.jsのダウンロード先に行ってインストーラを削除し、必要なバージョンを入れ直さなければなりません。めんどくさいですよね。<br>なので、<code>nodebrew</code> を使いましょうってことです。<br><br>ここからは実際に<code>nodebrew</code>の使い方からインストール方法を見ていきましょう。<br></p><h2 id="hd8d793506b">インストール</h2><p><code>nodebrew</code>は<strong>Homebrew</strong>を使ってインストールします。<br>Homebrewのインストール方法は<a href="https://qiita.com/zaburo/items/29fe23c1ceb6056109fd" target="_blank" rel="noopener noreferrer">こちら</a><br>※nodebrewは別にHomebrewをインストールしなくてもいいのですが、<br>今後、Homebrewを扱う機会がいっぱいあると思うのでこの機会に。。。<br><br>もしHomebrewインストールしたくないよって方がいれば<a href="https://github.com/hokaccha/nodebrew" target="_blank" rel="noopener noreferrer">こちら</a>を<br><br>インストールが完了すれば下記コマンドを入力してください。</p><pre><code>$ brew install nodebrew</code></pre><p><br>インストールが完了すれば下記コマンドで確認を入力してください。</p><pre><code>$ nodebrew -v</code></pre><p><br>確認後、環境変数を追加します。<br>下記コマンドを入力します。</p><pre><code>$ vi ~/.bash_profile</code></pre><p><br>この中に下記を追加してください。</p><pre><code>export PATH=$HOME/.nodebrew/current/bin:$PATH</code></pre><p><br>追記後<code>コロン + w + Enter</code>を入力し、<code>コロン + q + Enter</code>を入力してください。<br>※場合によっては<span style="color:#ff357f;font-family: Monaco, Courier New, monospace">コロン + w or q + ! + Enter</span><br><br>入力後、下記コマンド類を入力し<code>nodebrew</code>のセットアップが完了です。<br>お疲れ様でした。<br></p><h2 id="h970533ad80">nodebrewを使ってnode.jsをインストールする。</h2><p>インストールする前に下記コマンドを入力し、インストール可能なバージョンを確認しましょう。</p><pre><code>$ nodebrew ls-remote</code></pre><p>計り知れないバージョンがインストールできますね。<br><br>ver.17.0.0をインストールしてみましょう。</p><pre><code>$ nodebrew install-binary v17.0.0</code></pre><p><span style="color:#c7243a">※　nodebrew installでインストールするとスゴイ時間がかかるので、上記のコマンドが良いらしい。</span><br><br>インストールされたかどうかの確認は下記コマンドで確認します。</p><pre><code>$ nodebrew list or $ nodebrew ls</code></pre><p>正確にいうとインストールされているバージョンの一覧です。<br>因みに、<code>current</code>の値が現在使用しているnodeのバージョンです。</p><h2 id="h9a0715d8cd">node.jsのバージョンの切り替え</h2><p>下記コマンドでnode.jsのバージョンを切り替えます。</p><pre><code>$ nodebrew use vバージョンNo</code></pre><p>ex)v15.11.0に切り替える</p><pre><code>$ nodebrew use v15.11.0</code></pre><p><br>切り替え後、$ nodebrew ls を入力し<code>current</code>の値が変わっていればOKです。<br>変わらなければ、過去に公式サイトからインストーラーをダウンロードしちゃっているので、そいつが悪さしています。<br>インストーラーを uninstall すれば大丈夫です。<br></p><h2 id="ha214098e44">まとめ</h2><p>以上。node.jsのインストールにはnodebrewを使いましょうと言うお話でした。</p>]]></description><link>https://watataku-blog.vercel.app/blog/ahv9hz4k0y</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/ahv9hz4k0y</guid><pubDate>Fri, 28 Jan 2022 05:56:57 GMT</pubDate></item><item><title><![CDATA[Next.jsでのPWAの仕方 | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。<br>Watatakuです。<br>今回の記事はこのサイトをPWAにしたよと報告と<br>Next.jsでのPWAの仕方を書いていきます。</p><h2 id="hc4fc4a6bbf">PWAとは</h2><p><span style="color:#333333">P</span>WAはプログレッシブウェブアプリ(Progressive web apps)の略で、<br>ウェブアプリをネイティブアプリのように使えるようにする仕組みのことです。</p><h2 id="h467bf686e0">実際にやっていく</h2><p>以下のコマンドを用いて、<a href="https://www.npmjs.com/package/next-pwa" target="_blank" rel="noopener noreferrer">next-pwa</a>をインストールします。</p><pre><code>$ npm install next-pwa</code></pre><p><br>インストール後、<code>next.config.js</code>に以下を記述。</p><pre><code>const withPWA = require("next-pwa");

module.exports = withPWA({
  pwa: {
    dest: "public",
  },
});</code></pre><p><br>PWA実装に必要なものは</p><ul><li>アイコン</li><li>App Manifest（manifest.json）</li><li>Service Worker</li></ul><p>他にもWEBサイトのURLが<code>https</code>でないといけないなど様々な条件があります。詳しくは<a href="https://flxy.jp/article/628" target="_blank" rel="noopener noreferrer">こちら</a>。<br><br>そこで便利なのがこちらの<a href="https://ao-system.net/favicongenerator/" target="_blank" rel="noopener noreferrer">様々なファビコンを一括生成。favicon generator</a><br>こちらでアイコンとApp Manifestなどが作られるので、全て<code>publicディレクトリ</code><br> に格納してください。<br><br>格納後、manifest.jsonを編集します。<br>※下記マニフェストはこのサイトでのマニフェストです。</p><pre><code>{
  "name": "Watataku's ブログ",
  "short_name": "ブログ",
  "description": "",
  "start_url": "/",
  "display": "standalone",
  "orientation": "any",
  "background_color": "#fff",
  "theme_color": "#fff",
  "icons": [
    {
      "src": "/android-chrome-36x36.png",
      "sizes": "36x36",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-48x48.png",
      "sizes": "48x48",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}</code></pre><p><br>編集後、src/pagesディレクトリに<code>_document.tsx</code>を作成し、<br>以下のように<code>Head</code>のなかでアイコン、manifest.jsonなどを読み込んでいきます。</p><pre><code>import Document, {
  Html,
  Head,
  Main,
  NextScript,
} from "next/document";

class MyDocument extends Document {
  render() {
    return (
      &lt;Html lang="ja-JP" dir="ltr"&gt;
        &lt;Head&gt;
          {/* windows */}
          &lt;meta
            name="msapplication-square70x70logo"
            content="/site-tile-70x70.png"
          /&gt;
          &lt;meta
            name="msapplication-square150x150logo"
            content="/site-tile-150x150.png"
          /&gt;
          &lt;meta
            name="msapplication-wide310x150logo"
            content="/site-tile-310x150.png"
          /&gt;
          &lt;meta
            name="msapplication-square310x310logo"
            content="/site-tile-310x310.png"
          /&gt;
          &lt;meta name="msapplication-TileColor" content="#000" /&gt;


          {/* safari */}
          &lt;meta name="apple-mobile-web-app-capable" content="yes" /&gt;
          &lt;meta name="apple-mobile-web-app-status-bar-style" content="#000" /&gt;
          &lt;meta name="apple-mobile-web-app-title" content="myapp" /&gt;
          &lt;link
            rel="apple-touch-icon"
            sizes="180x180"
            href="/apple-touch-icon-180x180.png"
          /&gt;
          {/* 一般 */}
          &lt;meta name="application-name" content="myapp" /&gt;
          &lt;meta name="theme-color" content="#000" /&gt;
          &lt;link rel="icon" sizes="192x192" href="/icon-192x192.png" /&gt;
          &lt;link rel="icon" href="/favicon.ico" /&gt;
          &lt;link rel="manifest" href="/manifest.json" /&gt;
        &lt;/Head&gt;
        &lt;body&gt;
          &lt;Main /&gt;
          &lt;NextScript /&gt;
        &lt;/body&gt;
      &lt;/Html&gt;
    );
  }
}

export default MyDocument;</code></pre><p><br></p><h2 id="h6fd83e2648">PWAの確認</h2><p>localhostで開き、デベロッパーツールを見るとServiceWorkerが登録されていることがわかります。<br>一回読み込んでしまえば、オフラインにして再読み込みしてもちゃんと読み込まれます。これでオフラインで動作する事は確認はできました！<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/0b9672f5988e442996efd52a44599be5/Watataku_s_%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%99-3.png" alt=""><br>また、右上に以下のダウンロードボタンも確認できます。<br><img src="https://images.microcms-assets.io/assets/2679e05ff37441a18a78cb51394b1a4a/0449632c209e4c12a7f703963bcccd4d/Watataku_s_%E3%83%95%E3%82%99%E3%83%AD%E3%82%AF%E3%82%99-2.png" alt=""></p><h2 id="he88af0846d">追記(2022.01.27)</h2><p>PWAになっていない問題。<br><a href="https://qiita.com/take_3/items/fe178568498d4589453b" target="_blank" rel="noopener noreferrer">この記事</a>で解決できました。</p><h2 id="hdd6c10bcaf">追記(2023.06.11)</h2><p><code>next-pwa</code>の設定が方法が<strong>5.6</strong>から変わっていました。<br>下記のように<code>next.config.js</code>を書き換えてください。出ないとエラりますw</p><pre><code> /** @type {import('next').NextConfig} */
const withPWA = require('next-pwa')({
  dest: 'public'
})

module.exports = withPWA({
  reactStrictMode: true
});</code></pre><h2 id="ha214098e44">まとめ</h2><p>以上がNext.jsでのPWAの仕方です。<br>もし、Next.jsのPWAの仕方がわからない方へのご参考になればと思います。<br><br></p>]]></description><link>https://watataku-blog.vercel.app/blog/uxgr0en1jj3</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/uxgr0en1jj3</guid><pubDate>Wed, 26 Jan 2022 08:26:55 GMT</pubDate></item><item><title><![CDATA[絶対パスの画像パスではImageコンポーネントは表示されない。 | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>Next.jsのImageコンポーネントで画像が表示されない<br>事態が起こってしまったので、調べてみた。</p><h2 id="h86672c8acc">本題</h2><ul><li>表示される例</li></ul><p>ローカルに画像ファイルを持っている場合</p><pre><code>const imagePath = "./image.png"

&lt;Image
  src={imagePath}
  width={100}
  height={80}
  alt={"画像"}
/&gt;</code></pre><ul><li>表示されない例</li></ul><p>外部サーバに画像ファイルがある場合</p><pre><code>const imagePath = "https://example.com/img/image.png"

&lt;Image
  src={imagePath}
  width={100}
  height={80}
  alt={"画像"}
/&gt;</code></pre><p><br></p><h2 id="had0b1b94af">解決方法</h2><p><code>next.config.js</code>に以下を追記。</p><pre><code>module.exports = {
  images: {
    domains: ["example.com"] // 画像ファイルがあるドメインを指定
  },
};</code></pre>]]></description><link>https://watataku-blog.vercel.app/blog/em1u_wnvbk</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/em1u_wnvbk</guid><pubDate>Wed, 19 Jan 2022 01:51:21 GMT</pubDate></item><item><title><![CDATA[技術ブログを作ったよ。 | Watatakuのブログ]]></title><description><![CDATA[<h2 id="h8d027c8ed3">はじめに</h2><p>おはようございます。こんにちは。こんばんは。<br>Watatakuと申します。技術ブログを作りました。よろしくお願いします。<br><br>今流行りのJamstackという手法で開発しています。<br>ホスティングはVercelを使用しています。<br><br>初回の今回はこのブログのことを書いていきます。<br><a href="https://github.com/watataku8911/watataku-blog" target="_blank" rel="noopener noreferrer">ソースコード</a></p><h2 id="hed900658dc">環境</h2><ul><li>React:17.0.2</li><li>Next.js:v.12.0.7 (7/11日現在 : v.13.4.3)</li><li>typescript:v4.5.4</li></ul><h2 id="h49af616052">このブログで書いていくこと</h2><p>フロントエンドのことを中心にWeb（Jamatack）について書いていきます。</p><h2 id="hbd7b1c9fdb">このブログを作成した目的</h2><p>単純にブログを作りたかったからw<br>以上。<br><br>・・・ではなく、真面目に回答していくと上記の動機もありますが他に下記に記載の理由があります。</p><ul><li>将来の自分の為の知識の貯蔵</li><li>自分と同じ問題にぶち当たっている人に対して、解決策を紹介したい</li><li>文章力向上の為</li></ul><p><br>投稿頻度はさほど多くはないとは思いますがよろしくお願いします。<br></p>]]></description><link>https://watataku-blog.vercel.app/blog/kmoozkd7m</link><guid isPermaLink="true">https://watataku-blog.vercel.app/blog/kmoozkd7m</guid><pubDate>Tue, 11 Jan 2022 02:24:11 GMT</pubDate></item></channel></rss>