👻

【Next.js】Server Actionsを使えばリンクのプレビューを簡単に実装できてしまう(非推奨)

2024/04/18に公開

はじめに

ServerActionsとは何かについては割愛します。
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

一般的なリンクプレビューの実現方法

  1. 提供されているAPIを使う
    LinkPreviewAPIなどを使ってサイトのメタデータなどを取得して表示する。

  2. プロキシサーバーを作成する
    要するにCORSの問題をなんとかしたいので、自前でプロキシサーバーを作成してそこを経由する。

  3. 自分でAPIを用意する
    1を自作する。すでにAPIがある場合は一番簡単。

ServerActionsのサンプルコード

上記のいずれの方法もめんどくさいといった場合は、Next.jsのServerActionsで取得してしまう方法があります。ServerActionsはクライアントサイドではなく、文字通りサーバーで実行されるのでCORSの心配がありません。
以下はMUIを使用したサンプルコードです。

サーバーで実行するコード(HTML解析にはcheerioを使う)

"use server";
import * as cheerio from "cheerio";

type MetaData = {
  title: string;
  description: string;
  image: string;
  url: string;
};

/**
 * 指定されたURLのメタデータを取得する
 * @param url
 */
export const fetchMetaData = async (url: string): Promise<MetaData | null> => {
  try {
    const res = await fetch(url);
    if (!res.ok) {
      console.warn(
        "メタデータの取得に失敗しました",
        res.status,
        res.statusText,
      );
      return null;
    }
    const html = await res.text();
    const $ = cheerio.load(html);

    const title = $("title").text();
    const description = $('meta[name="description"]').attr("content");
    const image = getImage($);

    return {
      title: title || "",
      description: description || "",
      image: image || "",
      url,
    };
  } catch (error) {
    console.warn("メタデータの取得に失敗しました", error);
    return null;
  }
};

/**
 * メタデータから画像を取得する
 * @param $ cheerio
 * @returns 画像URL
 */
const getImage = ($: cheerio.CheerioAPI) => {
  const ogImg = $('meta[property="og:image"]').attr("content");
  if (ogImg) {
    return ogImg;
  }
  const imgRelLink = $('link[rel="image_src"]').attr("href");
  if (imgRelLink) {
    return imgRelLink;
  }
  const twitterImg = $('meta[name="twitter:image"]').attr("content");
  if (twitterImg) {
    return twitterImg;
  }
  const imgs = $("img");
  if (imgs.length > 0) {
    const img = imgs.first();
    return img.attr("src");
  }
  return null;
};

ポイント

  • $('meta[property="og:image"]').attr("content");<meta property="og:image" content="...">の...部分を取得している。
  • 画像取得の優先順位
    1. Open Graphプロトコル(ウェブページがソーシャルメディアサイト(FacebookやTwitterなど)に共有されたときにどのように表示されるかを制御するためのもの)
    2. <link rel="image_src" href="...">
    3. <meta name="twitter:image" content="...">
    4. 最初のimgタグ

コンポーネント

type Props = {
  link?: string;
};

export const LinkPreview: React.FC<Props> = ({ link }) => {
  const [metaData, setMetaData] = useState<MetaData | null>(null);
  const theme = useTheme();
  useEffect(() => {
    if (!link) {
      return;
    }
    const fetchLinkMetaData = async () => {
      const data = await fetchMetaData(link);
      setMetaData(data);
    };

    fetchLinkMetaData();
  }, [link]);

  if (!metaData) {
    return (
      link && (
        <Link href={link} style={{ textDecoration: "none", display: "block" }} target="_blank">
          {link}
        </Link>
      )
    );
  }

  const isSmallScreen = theme.breakpoints.down("sm");
  return (
    link && (
      <Box sx={{ width: "100%" }}>
        <Link href={link} style={{ textDecoration: "none" }} target="_blank">
          <Card sx={{ display: "flex", overflow: "hidden" }}>
            <CardMedia
              component="img"
              sx={{
                width: isSmallScreen ? "5rem" : "7rem",
                objectFit: "contain",
              }}
              src={metaData.image}
              alt={metaData.title}
            />
            <CardContent
              sx={{
                textAlign: "left",
                width: isSmallScreen
                  ? "calc(100% - 5rem)"
                  : "calc(100% - 7rem)",
              }}
            >
              <Typography
                overflow="hidden"
                fontSize="1.0rem"
                fontWeight="bold"
                textOverflow="ellipsis"
                whiteSpace="nowrap"
                width="100%"
              >
                {metaData.title}
              </Typography>
              <Typography
                height={isSmallScreen ? "2.8rem" : "3rem"}
                overflow="hidden"
                fontSize="0.6rem"
                textOverflow="ellipsis"
                whiteSpace="wrap"
                width="100%"
              >
                {metaData.description}
              </Typography>
            </CardContent>
          </Card>
        </Link>
      </Box>
    )
  );
};

上記を実装するとこんな感じのコンポーネントができます。(Zennのリンクを渡してみた)

終わりに

個人開発やServerActionsをサクッと試してみたい方にはいいかもしれません。

Discussion