概要
Azureの静的ページを使ってSPAを公開している。
ユーザの個別ページに動的OGPを設定したいが、LambdaのEdgeFunctionsのようなものがないので、OGPの設定をしたページをStorageAccountに保存し、SPAにリダイレクトするように設定した。
ソースコード
OGP用のファイル作成
meta タグによるリダイレクトを採用している。
twitter-cardの設定を、Xに合わせたものにしている。
app/src/domain/fallMagia/services/persistent/saveCharacterTwitterCard.ts
const SERVICE_NAME = '魔法少女フォールマギア';
interface Args {
url: string;
title: string;
description: string;
src: string;
}
export const createCharacterTwitterCard = ({
url,
title,
description,
src,
}: Args) =>
`
<!doctype html>
<html lang="ja">
<head prefix="og: http://ogp.me/ns#">
<meta charset="UTF-8" />
<title>${title}|${SERVICE_NAME}</title>
<meta name="description" content="${description}" />
<meta name="author" content="hibohiboo">
<meta name="keywords" content="TRPG,${SERVICE_NAME},キャラクターシート" />
<meta property="og:type" content="article" />
<meta property="og:locale" content="ja_JP" />
<meta property="og:site_name" content="${SERVICE_NAME}">
<meta property="og:title" content="${title}|${SERVICE_NAME}" />
<meta property="og:description" content="${description}" />
<meta property="og:image" content="${src}" />
<meta property="og:url" content="${url}" />
<meta name="x:card" content="summary_large_image" />
<meta name="x:site" content="@hibohiboo" />
<meta name="x:creator" content="@hibohiboo" />
<meta name="x:image:alt" content="${title}|${SERVICE_NAME}">
<meta http-equiv="Refresh" content="0; URL=${url}" />
</head>
<body></body>
</html>
`;
アップロード
app/src/domain/fallMagia/store/slices/fallMagiaCharacterPageSlice.ts
const directory = `upload/${userPage.id}`;
// ストレージアカウントにTwitterCardを保存
await uploadToStorageAccount(
new File(
[
new Blob(
[
createCharacterTwitterCard({
url: `https://${location.hostname}/app/spa/userpage/${userPage.id}`,
title: userPage.title,
src: userPage.src,
description: userPage.description,
}),
],
{ type: 'text/html' },
),
],
'twitter-card.html',
{ type: 'text/html' },
),
'twitter-card.html',
directory,
{
blobHTTPHeaders: {
blobContentType: 'text/html',
},
},
);
app/src/domain/storageAccount/uploadToStorageAccount.ts
import {
BlockBlobClient,
BlockBlobParallelUploadOptions,
} from '@azure/storage-blob';
import { postSASToken } from '../api/crud';
import { convertFileToArrayBuffer } from './convert-file-to-arraybuffer';
const containerName = `$web`;
const permission = 'w'; //write
const timerange = 5; //minutes
const minFileSize = 1; //bytes
const maxFileSize = 5 * 1024 * 1024; // 5M bytes
/**
* Uploads a file to a storage account web container
* @param file - The file to upload
* @param fileName - The name of the file
* @param directory - The directory to upload the file to
*/
export const uploadToStorageAccount = async (
file: File,
fileName: string,
directory = 'uploaded',
option?: BlockBlobParallelUploadOptions,
) => {
const filePath = `${directory}/${fileName}`;
const sasToken = await getSasTokenUrl(filePath);
if (!sasToken) {
console.warn('Failed to get sas token');
return false;
}
try {
const fileArrayBuffer = await convertFileToArrayBuffer(file as File);
if (
fileArrayBuffer === null ||
fileArrayBuffer.byteLength < minFileSize ||
fileArrayBuffer.byteLength > maxFileSize
) {
console.warn('Failed to convert file to array buffer');
return;
}
const blockBlobClient = new BlockBlobClient(sasToken);
await blockBlobClient.uploadData(fileArrayBuffer, option);
return true;
} catch (error) {
if (error instanceof Error) {
const { message, stack } = error;
console.warn(
`Failed to finish upload with error : ${message} ${stack || ''}`,
);
} else {
console.warn(error as string);
}
return false;
}
};
async function getSasTokenUrl(filePath: string) {
try {
const response = await postSASToken(
filePath,
permission,
containerName,
timerange,
);
const data = await response.json();
const { url } = data;
return url;
} catch (error) {
console.warn(error);
if (error instanceof Error) {
const { message, stack } = error;
console.warn(`Error getting sas token: ${message} ${stack || ''}`);
} else {
console.warn(error as string);
}
}
}
app/src/domain/api/crud.ts
const baseUrl = `${import.meta.env.VITE_API_SERVER ?? ''}/api`;
const apiBaseUrl = '/data-api/rest';
export const postSASToken = async (
filePath: string,
permission: string,
containerName: string,
timerange: number,
) => {
const requestUrl = `${baseUrl}/credentials?file=${encodeURIComponent(
filePath,
)}&permission=${permission}&container=${containerName}&timerange=${timerange}`;
const response = await fetch(requestUrl, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
});
return response;
};
SASトークン取得のサーバ側の処理は静的 Web アプリから Azure Blob Storage に画像をアップロードするメモと同様。