FreshでSupabase上の画像を取得する

作成日
更新日

前提

  • FreshでSupabase上の記事を取得する

管理者API Key を使わずにやりたかったけど、anonキーのやり方がむずい...

import { supabase } from "./supabase.ts";

const bucketName = "";

export async function getSignedUrl(fileName: string): Promise<string | null> {
  const safeFileName = slugifyFileName(fileName);
  const { data, error } = await supabase.storage.from(bucketName)
    .createSignedUrl(safeFileName, 60 * 60);

  if (error) {
    console.error(
      `Error generating signed URL for ${safeFileName}:`,
      (error as Error).message,
    );
    return null;
  }

  return data.signedUrl;
}

// ファイル名を slugify して ASCII のみの形式に変換
function slugifyFileName(fileName: string): string {
  return fileName
    .normalize("NFKD") // Unicode 正規化(濁点・半濁点を分離)
    .replace(/[\u0300-\u036f]/g, "") // ダイアクリティカルマーク除去
    .replace(/[^\w.-]/g, "_") // 許可されていない文字を `_` に変換
    .replace(/_{2,}/g, "_") // 連続する `_` を1つに
    .toLowerCase();
}
export async function getPostWithImages(slug: string) {
  // 記事データを取得
  const { data: post, error } = await supabase
    .from("articles")
    .select("title, content, created_at, cover_image")
    .eq("slug", slug)
    .eq("private", false)
    .single();

  if (error || !post) {
    console.error("Error fetching post:", error?.message);
    return null;
  }
  console.log("🐈image:", post.cover_image);

  // `cover_image` の署名付きURLを取得
  const coverImageUrl = post.cover_image
    ? await getSignedUrl(post.cover_image)
    : null;

  return { ...post, cover_image: coverImageUrl };
}

これでカバー画像が取得できることは確認した。

api を使う

Freshでapiフォルダとutilsフォルダの使い分けより、画像はapiで取得したらいいんじゃないか説。

routes/api/getImage/[fileName].ts に配置

routes/api/getImage/[fileName].ts
import { Handlers } from "$fresh/server.ts";
import { supabase } from "../../../utils/supabase.ts";

export const handler: Handlers = {
  async GET(_req, ctx) {
    const { fileName } = ctx.params;
    // デコードしてからさらにASCIIにする
    const encodeFileName = decodeURIComponent(fileName);
    const safeFileName = slugifyFileName(encodeFileName);
    const bucketName = Deno.env.get("SUPABASE_IMAGE_BUCKET") ?? "";
    const { data, error } = await supabase.storage.from(bucketName)
      .createSignedUrl(safeFileName, 60 * 60);

    if (error || !data) {
      console.error(
        `Error generating signed URL for ${safeFileName}:`,
        (error as Error).message,
      );
      return new Response("Error generating signed URL", { status: 500 });
    }

    return new Response(JSON.stringify({ url: data.signedUrl }), {
      headers: {
        "Content-Type": "application/json"
      },
    });
  },
};

// ファイル名を slugify して ASCII のみの形式に変換
function slugifyFileName(fileName: string): string {
  return fileName
    .normalize("NFKD") // Unicode 正規化(濁点・半濁点を分離)
    .replace(/[\u0300-\u036f]/g, "") // ダイアクリティカルマーク除去
    .replace(/[^\w.-]/g, "_") // 許可されていない文字を `_` に変換
    .replace(/_{2,}/g, "_") // 連続する `_` を1つに
    .toLowerCase();
}

slugifyFileName をする理由は GitHub Acitonsを使ってSupabase Storage に画像をアップロードするの時に日本語が使えずASCIIに変換してるため。

考えていた方法

routes/images/[fileName].tsx から取得するようにしようとしていたが、そっちの方が重いっぽく、api を利用した形にはできないみたいなのでやめた。

signed urlが公開されないようにする

urlが公開されてしまうので、signed urlを公開しない形に修正する。 signedURL自体を使わなくても単に download でよかった。

import { Handlers } from "$fresh/server.ts";
import { supabase } from "../../../utils/supabase.ts";

export const handler: Handlers = {
  async GET(_req, ctx) {
    const { fileName } = ctx.params;
    // デコードしてからさらにASCIIにする
    const encodeFileName = decodeURIComponent(fileName);
    const safeFileName = slugifyFileName(encodeFileName);
    const bucketName = Deno.env.get("SUPABASE_IMAGE_BUCKET") ?? "";
    const { data, error } = await supabase.storage.from(bucketName).download(safeFileName);
    if (error || !data) {
      console.error(`Error fetching image ${safeFileName}:`, error);
      return new Response("Error fetching image", { status: 500 });
    }

    // 画像データをそのままレスポンスとして返す
    return new Response(data, {
      headers: {
        "Content-Type": data.type, // 画像のMIMEタイプを維持
        "Cache-Control": "public, max-age=3600", // クライアント側キャッシュを1時間有効
      },
    });
  },
};

// ファイル名を slugify して ASCII のみの形式に変換
function slugifyFileName(fileName: string): string {
  return fileName
    .normalize("NFKD") // Unicode 正規化(濁点・半濁点を分離)
    .replace(/[\u0300-\u036f]/g, "") // ダイアクリティカルマーク除去
    .replace(/[^\w.-]/g, "_") // 許可されていない文字を `_` に変換
    .replace(/_{2,}/g, "_") // 連続する `_` を1つに
    .toLowerCase();
}

islandで表示する

api を使うので islandで表示する。

routes/islands/CoverImage.tsx
import { useState, useEffect } from "preact/hooks";

export default function CoverImage({ imageName }: { imageName: string }) {
  const [imageSrc, setImageSrc] = useState<string | null>(null);

  useEffect(() => {
    async function fetchImageUrl() {
      try {
        const res = await fetch(`/api/getImage/${imageName}`);
        if (!res.ok) throw new Error(`HTTP error! Status: ${res.status}`);
        setImageSrc(res.url);
      } catch (error) {
        console.error("Failed to fetch image:", error);
      }
    }

    fetchImageUrl();
  }, [imageName]);

  return imageSrc ? <img src={imageSrc} alt="Cover Image" /> : <p>Loading...</p>;
}

オケ

サイトアイコン
公開日
更新日