💳

【Next.js / App Router】埋め込みリンクカードの実装について

2024/05/25に公開

こんにちは!@Ryo54388667です!☺️

普段は都内でエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。

今回はApp Routerを利用した埋め込みリンクカードの実装を紹介していきます!

📌 埋め込みリンクカードの概要

いろいろ説明するのも良いですが、「百聞は一見にしかず」ということで、下記のようなものを実装します。

https://nextjs.org/blog/next-15-rc

カード状のリンクのUIです!多くの場合、外部リンクが設定されていると思います。Zennのプロダクトでも実装されている通り、さまざまなWebページで見かけるUIですね!今回、実装に思ったよりも工数がかかってしまったので、その詳細を備忘録として記載しますー。

📌 実装について

実装方針としてはこのような感じです!

  1. 同一プロジェクト内に埋め込み用のページを作成する
  2. iframeのsrc属性に作成したページのパスを指定する

おそらくZennの実装もこうなっているのかなと。
違っていたらすいません。。無視してください。。

スタイリングについては、TailwindCSSを利用していきます。

同一プロジェクト内に埋め込み用のページを作成する

まず、Next.jsのAppディレクトリ配下に埋め込みカード用のページを作成します。下記の場合、app/embedded/ というパスになります。

app/
├── embedded
│   ├── error.tsx
│   ├── layout.tsx
│   ├── loading.tsx
│   └── page.tsx
├── layout.tsx
└── page.tsx

埋め込みカードのページに必要なデータは外部リンクのURLです。データの渡し方はいくつかあるかと思いますが、今回はクエリパラメータでURLが渡すようにします。お好みですが、dynamic segments (/embedded/[url]/page.tsx)としても実現可能かと思います。ただし、その場合、url内にhttps://など「/ (スラッシュ)」が含まれますので、decodeURIComponentなどを利用して整形する必要があるかもしれません。

次に、対象ページのURLからメタ情報を取得します。
今回は簡略化のためmeta-fetcherというライブラリを利用しました。ライブラリを利用せず、fetchのレスポンスからパースして必要な情報を抜き出してもOKです👌

//Response Example
{
  "title": "Hoppscotch - Open source API development ecosystem",
  "description": "Helps you create requests faster, saving precious time on development.",
  "image": "https://hoppscotch.io/og.png",
  "url": "https://hoppscotch.io/",
  "siteName": "Hoppscotch",
  "type": "website"
}

OG画像やtitle, descriptionを取得できます。こちらの例にはありませんが、faviconも取得できます。

/embedded/page.tsx

import metaFetcher from 'meta-fetcher';
// import { unstable_cache } from 'next/cache';
import EmbeddedCard from '@/components/EmbeddedCard';

const Page = async ({ searchParams }: { searchParams: { url: string } }) => {
  const url = searchParams.url
  // 任意ですが、頻繁に変更されるものでもありませんしキャッシュしておくと良いかと思います。
  // const metadata = await unstable_cache((url: string) => metaFetcher(url), [url], { revalidate: 24 * 60 * 60 })(url)
  const metadata = await metaFetcher(url)
  return (
      <EmbeddedCard
        url={url}
        title={metadata.metadata.title}
        description={metadata.metadata.description}
        website={metadata.metadata.website}
        banner={metadata.metadata.banner}
      />
  );
}
export default Page

/任意のパス/EmbeddedCard.tsx

type EmbeddedCardProps = {
  url: string;
  title: string;
  description?: string;
  website: string;
  banner?: string;
}

const EmbeddedCard = ({ url, title, description, website, banner }: EmbeddedCardProps) => {
  return (
    <a href={url} className=' transition-opacity hover:opacity-70' target="_blank" rel="noopener noreferrer">
      // お好みのデザインでOK😌
    </a>
  )
}
export default EmbeddedCard;

次に、エラーページがあると親切かと思います。非同期のデータ取得が含まれすし、エラーも発生します。埋め込みカードの意図しない動作の場合は、URLだけ表示したリンクカードになっているものをよく見かけます。

↓下記のようなイメージ

/embedded/error.tsx

"use client"
import { useSearchParams } from "next/navigation"

const Error = () => {
  const searchParams = useSearchParams()
  const url = searchParams.get('url')
  return (
      <a href={url} className="text-lg sm:text-2xl  h-full w-full px-4 py-8 transition-opacity hover:opacity-70" target="_blank" rel="noopener noreferrer">
        {url}
      </a>
  )
}
export default Error

余裕があれば、ローディングのカードもあると良いのかなと思います。スケルトンローディングの一例です。

↓下記のようなイメージ

/embedded/loading.tsx

const Loading = () => {
  return (
    <div className='h-[180px] flex border'>
      <div className='flex-1 my-1 md:my-4 px-2 md:px-6'>
        <div className='rounded h-8 bg-gray-200 mt-2 md:mt-4 animate-pulse'></div>
        <div className='rounded h-8 bg-gray-200 mt-2 md:mt-4 animate-pulse'></div>
        <div className='rounded w-1/5 h-8 bg-gray-200 mt-2 md:mt-4 animate-pulse'></div>
      </div>
      <div className="w-1/3">
        <div className=' rounded h-[117px] bg-gray-200 mt-2 md:mt-4 animate-pulse'></div>
      </div>
    </div>
  );
}
export default Loading

iframeのsrc属性に作成したページのパスを指定する

最後に、iframeに先ほどのパスを指定して、URLのデータを渡せば完了です!😌

/任意のパス/CustomIframe.tsx

import type { ComponentProps } from "react";

type CustomIframeProps = {
  href: string
} & Omit<ComponentProps<"iframe">, "src">;

const CustomIframe = ({ href, className, style, ...restProps }: CustomIframeProps) => {
  return (
    <iframe {...restProps} src={`/embedded?url=${href}`} className={className} style={{ ...style, overflowY: "hidden" }} loading="lazy" />
  )
}
export default CustomIframe;

iframeにはscrolling属性があるのですが、こちらが非推奨になったので、替わりにoverflowY: "hidden" にしてスクロールをオフにしています。

📌 まとめ

埋め込みカードの実装

  1. 同一プロジェクト内に埋め込み用のページを作成する。(例: app/embedded/page.tsx)
  2. iframeのsrc属性に作成したページのパスを指定する。(<iframe src=“/embedded/xxxxx” />)

より良い方法があれば教えてください〜

最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺
https://twitter.com/Ryo54388667/status/1733434994016862256

Discussion