서버리스로 동적 소셜 공유 이미지 생성하기

이번 장에서는 서버리스를 사용해 동적으로 소셜 공유 이미지 또는 오픈 그래프(OG) 이미지를 생성하는 방법을 살펴보겠습니다.

소셜 카드, 소셜 공유 이미지 또는 오픈 그래프 이미지는 링크가 페이스북, 트위터 등 소셜 미디어 사이트에 게시될 때 표시되는 미리보기 이미지입니다. 이러한 이미지는 사용자에게 링크의 내용을 더 잘 이해할 수 있게 해줍니다. 또한 로고나 파비콘보다 더 멋지게 보입니다.

그러나 블로그 게시물이나 웹사이트 페이지마다 고유한 이미지를 만드는 것은 시간이 많이 들고 비현실적일 수 있습니다. 따라서 블로그 게시물이나 페이지의 제목과 기타 정보를 기반으로 이러한 이미지를 동적으로 생성할 수 있으면 좋을 것입니다.

우리는 SST에서 이런 기능을 구현하고 싶었습니다. 그리고 이는 서버리스를 사용하기에 완벽한 사례였습니다. 이러한 이미지는 웹사이트가 공유될 때 생성되며, 이를 제공하기 위해 서버를 운영하는 것은 의미가 없습니다. 그래서 우리는 SST를 사용해 자체 소셜 카드 서비스를 구축했습니다! 또한 Seed를 통해 배포하고 관리합니다.

예를 들어, 우리의 한 장에 대한 소셜 카드는 다음과 같습니다.

SST 장에 대한 소셜 카드

우리는 이러한 소셜 카드를 생성하기 위해 여러 템플릿을 가지고 있습니다. 다음은 블로그 게시물에 대한 예시입니다.

샘플 블로그 게시물에 대한 소셜 카드

이러한 이미지는 우리의 소셜 카드 서비스에서 제공됩니다. 이 서비스는 SST를 사용해 구축되었으며 AWS에 호스팅됩니다:

https://social-cards.sst.dev

이번 장에서는 이 서비스를 어떻게 만들었는지, 그리고 여러분도 같은 작업을 할 수 있는 방법을 살펴보겠습니다!

전체 소셜 카드 서비스는 오픈 소스이며 GitHub에서 확인할 수 있습니다. 따라서 이 장에서 다룰 모든 내용을 포크하여 직접 실험해 볼 수 있습니다.

  GitHub의 SST 소셜 카드 서비스

목차

이번 장에서는 다음 내용을 살펴볼 것입니다:

  1. 소셜 카드 서비스의 아키텍처
  2. SST로 서버리스 앱 만들기
  3. 브라우저에서 소셜 카드 템플릿 디자인하기
  4. Puppeteer로 템플릿 스크린샷 찍기
  5. Lambda에서 비라틴 폰트 지원하기
  6. S3 버킷에 이미지 캐싱하기
  7. CDN으로 CloudFront 사용하여 이미지 제공하기
  8. 소셜 카드 서비스에 커스텀 도메인 추가하기
  9. 정적 사이트 생성기와 통합하기

먼저 소셜 카드 서비스의 아키텍처를 전체적으로 살펴보는 것부터 시작해 보겠습니다.

아키텍처

우리의 소셜 카드 서비스는 AWS에 배포된 서버리스 앱입니다.

소셜 카드 서비스 아키텍처 다이어그램

여기에는 몇 가지 주요 부분이 있습니다. 자세히 살펴보겠습니다.

  1. 이미지를 제공하기 위해 CloudFront를 CDN으로 사용합니다.
  2. CloudFront는 서버리스 API에 연결됩니다.
  3. 이 API는 Lambda 함수로 구동됩니다.
  4. 이미지를 생성하는 Lambda 함수는 다음과 같은 작업을 수행합니다:
    • 사용할 템플릿을 포함합니다. 이 템플릿은 Lambda 함수에 포함된 HTML 파일입니다.
    • Puppeteer를 사용하여 헤드리스 Chrome 인스턴스를 실행하고 템플릿에 대한 매개변수를 전달합니다.
    • 이 템플릿을 로드하고 스크린샷을 찍습니다.
    • 생성된 이미지를 S3 버킷에 저장합니다.
    • 이전에 이미지를 생성했는지 확인하기 위해 먼저 S3 버킷을 확인합니다.

SST 앱 만들기

새로운 SST 앱을 만들어 보겠습니다.

$ npx create-sst@one --template=minimal/javascript-starter social-cards
$ cd social-cards

앱의 인프라는 CDK를 사용해 정의됩니다. 현재는 단순히 Lambda 함수를 호출하는 간단한 API가 있습니다.

이 내용은 stacks/MyStack.js에서 확인할 수 있습니다.

// HTTP API 생성
const api = new Api(stack, "Api", {
  routes: {
    "GET /": "functions/lambda.handler",
  },
});

현재 functions/lambda.js에 있는 Lambda 함수는 단순히 _“Hello World”_를 출력합니다.

소셜 카드 템플릿 디자인하기

첫 번째 단계는 소셜 공유 이미지를 위한 템플릿을 만드는 것입니다. 이 HTML 파일들은 로컬에서 로드되며, 쿼리 문자열을 통해 템플릿에 필요한 파라미터를 전달합니다.

SST에서 사용하는 블로그 템플릿을 예시로 살펴보겠습니다.

로컬에서 실행 중인 소셜 카드 템플릿

이 페이지를 생성하는 HTML은 다음과 같습니다:

<html>
  <head>
    <link rel="stylesheet" href="assets/css/reset.css" />
    <link rel="stylesheet" href="assets/css/fonts.css" />
    <link rel="stylesheet" href="assets/css/main.css" />
    <link rel="stylesheet" href="assets/css/blog.css" />
  </head>
  <body>
    <img class="logo" height="55" src="assets/images/logo.svg" />
    <span class="section">Blog</span>
    <div class="spacer">
      <h1 id="title"></h1>
      <div class="profile">
        <img id="avatar" width="64" src="" />
        <span id="author"></span>
      </div>
    </div>
    <a>Read Post</a>
    <script>
      const urlSearchParams = new URLSearchParams(window.location.search);
      const params = Object.fromEntries(urlSearchParams.entries());

      document.getElementById("title").innerHTML = params.title;
      document.getElementById("author").innerHTML = params.author;
      document.getElementById(
        "avatar"
      ).src = `assets/images/profiles/${params.avatar}.png`;
    </script>
  </body>
</html>

맨 아래에 있는 <script> 태그를 주목하세요. 이 스크립트는 쿼리 문자열 파라미터를 가져와 HTML에 적용합니다.

소셜 공유 이미지의 권장 크기는 1200x630입니다. 따라서 페이지 스타일을 이에 맞게 조정해야 합니다.

이 파일들을 프로젝트의 templates/ 디렉토리에 추가합니다.

로컬에서 이 파일들을 다음과 같은 URL로 열 수 있습니다:

file:///Users/jayair/Desktop/social-cards-service/templates/serverless-stack-blog.html?title=This%20is%20a%20sample%20blog%20post%20on%20Serverless%20Stack&author=Jay&avatar=jay

형식은 다음과 같습니다:

file:///Users/jayair/Desktop/social-cards-service/templates/serverless-stack-blog.html?title={title}&author={name}&avatar={filename}

레포지토리로 이동하여 이 템플릿에 포함된 나머지 파일들을 확인하세요. 여기에는 템플릿을 스타일링하는 데 사용하는 CSS 파일도 포함되어 있습니다.

레포지토리에는 우리가 사용하는 몇 가지 다른 템플릿들도 있습니다. 여러분도 비슷한 방식으로 작업할 수 있습니다. 각 템플릿이 쿼리 문자열을 읽고 파라미터를 적용할 수 있도록 하면 됩니다.

Puppeteer로 스크린샷 찍기

이제 템플릿을 브라우저에서 로컬로 불러올 수 있으니, 이 템플릿의 스크린샷을 찍고 Lambda 함수에서 이미지를 반환해 보겠습니다.

API를 업데이트하여 템플릿과 나머지 옵션을 받아오도록 하겠습니다. 사용할 형식은 다음과 같습니다.

https://api-endpoint.com/{template}/{encoded_title}.png?author={author}&avatar={avatar}

위 예시를 따라 templateserverless-stack-blog이고, authoravatar는 각각 Jayjay입니다.

encoded_title은 제목의 Base64 인코딩된 문자열입니다. AWS API Gateway가 특정 URL 인코딩 문자를 파싱하는 데 문제가 있기 때문에 Base64로 인코딩합니다.

스크린샷을 찍기 위해 Puppeteer를 사용할 것입니다. 이를 위해 공개된 Lambda Layer를 사용합니다. 이 레이어를 사용하면 AWS Lambda에 맞게 Puppeteer를 별도로 컴파일할 필요가 없습니다.

이제 stacks/MyStack.js의 API 정의는 다음과 같습니다.

const api = new Api(stack, "Api", {
  routes: {
    "GET /{template}/{file}": {
      function: {
        handler: "functions/lambda.handler",
        // 스크린샷 생성 시간을 늘림
        timeout: 15,
        // Layer에서 Chrome 로드
        layers: [layer],
        bundle: {
          // 템플릿 복사
          copyFiles: [
            {
              from: "templates",
              to: "templates",
            },
          ],
          // Lambda 함수에서 번들링 제외
          externalModules: ["chrome-aws-lambda"],
        },
      },
    },
  },
});

여기서 layer는 다음과 같습니다.

const layerArn =
  "arn:aws:lambda:us-east-1:764866452798:layer:chrome-aws-lambda:22";
const layer = LayerVersion.fromLayerVersionArn(this, "Layer", layerArn);

또한 copyFiles 옵션을 사용해 템플릿 파일을 Lambda 함수로 복사하고 있습니다.

이제 Lambda 함수를 위해 몇 가지 NPM 패키지를 설치해야 합니다.

$ npm install puppeteer puppeteer-core chrome-aws-lambda

Lambda 함수의 주요 부분은 다음과 같습니다.

import path from "path";
import chrome from "chrome-aws-lambda";

const ext = "png";
const ContentType = `image/${ext}`;

// chrome-aws-lambda는 로컬에서 로드할지 Layer에서 로드할지 처리
const puppeteer = chrome.puppeteer;

export async function handler(event) {
  const { file, template } = event.pathParameters;

  const title = parseTitle(file);

  // 유효한 요청인지 확인
  if (file === null) {
    return createErrorResponse();
  }

  const options = event.rawQueryString;

  const browser = await puppeteer.launch({
    args: chrome.args,
    executablePath: await chrome.executablePath,
  });

  const page = await browser.newPage();

  await page.setViewport({
    width: 1200,
    height: 630,
  });

  // URL로 이동
  await page.goto(
    `file:${path.join(
      process.cwd(),
      `templates/${template}.html`
    )}?title=${title}&${options}`
  );

  // 페이지 로딩 완료 대기
  await page.evaluate("document.fonts.ready");

  // 스크린샷 찍기
  const buffer = await page.screenshot();

  return createResponse(buffer);
}

/**
 * Base64로 인코딩된 URL 문자열 파싱
 *
 * $title.png
 *
 */
function parseTitle(file) {
  const extension = `.${ext}`;

  if (!file.endsWith(extension)) {
    return null;
  }

  // .png 확장자 제거
  const encodedTitle = file.slice(0, -1 * extension.length);

  const buffer = Buffer.from(encodedTitle, "base64");

  return decodeURIComponent(buffer.toString("ascii"));
}

function createResponse(buffer) {
  return {
    statusCode: 200,
    // 바이너리 데이터로 반환
    isBase64Encoded: true,
    body: buffer.toString("base64"),
    headers: { "Content-Type": ContentType },
  };
}

function createErrorResponse() {
  return {
    statusCode: 500,
    body: "Invalid request",
  };
}

대부분의 코드는 직관적입니다. 하지만 몇 가지 주요 포인트를 살펴보겠습니다.

  • Base64로 인코딩된 제목을 파싱할 때 URL 디코딩도 함께 수행합니다. 이는 제목을 Base64로 인코딩하기 전에 먼저 ASCII로 변환해야 하기 때문입니다.

  • 브라우저를 소셜 카드 이미지의 적절한 크기인 1200x630으로 설정합니다.

  • page.goto 호출에서 브라우저를 Lambda 함수에 로컬로 저장된 템플릿으로 이동시킵니다. 그리고 위에서 설명한 것과 동일한 쿼리 문자열 매개변수를 사용합니다.

  • document.fonts.ready를 사용해 템플릿의 폰트가 로드될 때까지 기다립니다. 이렇게 하면 적절한 시점에 스크린샷을 찍을 수 있습니다.

  • 마지막으로 스크린샷을 찍고 적절한 헤더와 함께 바이너리 데이터로 반환합니다.

이제 다음과 같은 URL로 테스트할 수 있습니다.

https://l36xnnxdw6.execute-api.us-east-1.amazonaws.com/serverless-stack-blog/VGhpcyUyMGlzJTIwYSUyMHNhbXBsZSUyMGJsb2clMjBwb3N0JTIwb24lMjBTZXJ2ZXJsZXNzJTIwU3RhY2s=.png?author=Jay&avatar=jay

여기서 긴 인코딩된 문자열은 다음 문자열의 Base64 인코딩 버전입니다.

This%20is%20a%20sample%20blog%20post%20on%20Serverless%20Stack

이 페이지를 방문하면 템플릿의 스크린샷을 얻을 수 있습니다.

API로 생성된 소셜 카드 이미지

Lambda에서 비라틴 폰트 지원하기

SST를 로컬에서 실행할 때는 Puppeteer가 시스템에 설치된 폰트를 사용합니다. 하지만 Lambda에 배포하면 일부 폰트가 누락될 수 있습니다. 이 경우 작은 상자로 렌더링될 수 있습니다.

토푸 폰트로 생성된 소셜 카드 이미지

이 문제를 해결하려면 Lambda가 실행되는 OS에 해당 폰트를 설정해야 합니다.

먼저 프로젝트 루트에 .fonts/ 디렉토리를 생성합니다.

$ mkdir .fonts

Google의 Noto 프로젝트에서 필요한 폰트를 다운로드하고 복사합니다. 이 예제에서는 NotoSansCJKsc-Regular.otf를 복사합니다.

.fonts
└── NotoSansCJKsc-Regular.otf

Lambda 함수가 이 디렉토리를 복사하도록 설정합니다. 그리고 Lambda 함수의 OS가 폰트를 인식할 수 있도록 $HOME 환경 변수를 /var/task로 설정합니다.

// HTTP API 생성
const api = new Api(stack, "Api", {
  routes: {
    "GET /{template}/{file}": {
      function: {
        handler: "functions/lambda.handler",

        //...

        environment: {
          // OS가 .fonts/ 디렉토리에서 비라틴 폰트를 인식하도록 $HOME 설정
          HOME: "/var/task",
        },

        bundle: {
          // 템플릿과 비라틴 폰트 복사
          copyFiles: [
            {
              from: "templates",
              to: "templates",
            },
            {
              from: ".fonts",
              to: ".fonts",
            },
          ],
        },

        //...
      },
    },
  },
});

이제 문자가 올바르게 표시되는 것을 확인할 수 있습니다.

비라틴 폰트로 생성된 소셜 카드 이미지

S3에 이미지 캐싱하기

API를 여러 번 방문할 때마다 스크린샷을 찍으면 느리고 비효율적입니다. 따라서 이미지를 S3에 캐싱해 보겠습니다.

먼저 S3 버킷을 생성합니다.

stacks/MyStack.js 파일에서 API 정의 위에 다음 코드를 추가합니다.

// 생성된 이미지를 저장할 S3 버킷 생성
const bucket = new sst.Bucket(this, "WebsiteBucket", {
  s3Bucket: {
    // 삭제 시 모든 객체 제거
    autoDeleteObjects: true,
    removalPolicy: RemovalPolicy.DESTROY,
  },
});

API 정의도 업데이트하여 이 버킷의 이름을 환경 변수로 전달합니다.

// HTTP API 생성
const api = new Api(stack, "Api", {
  routes: {
    "GET /{template}/{file}": {
      function: {
        handler: "functions/lambda.handler",

        // ...

        environment: {
          HOME: "/var/task",
          BucketName: bucket.bucketName,
        },

        // ...
      },
    },
  },
});

API가 S3 버킷에 접근할 수 있도록 허용합니다.

// API가 버킷에 접근할 수 있도록 허용
api.attachPermissions([bucket]);

Lambda 함수 측에서는 환경 변수를 참조합니다.

import { S3 } from "aws-sdk";

const Bucket = process.env.BucketName;
const s3 = new S3({ apiVersion: "2006-03-01" });

functions/lambda.js 파일에서 const options... 줄 다음에 다음 코드를 추가합니다.

const key = generateS3Key(template, title, options);

// S3 버킷 확인
const fromBucket = await get(key);

// 버킷에서 반환
if (fromBucket) {
  return createResponse(fromBucket);
}

generateS3Key 함수는 다음과 같습니다.

/**
 * 경로 매개변수와 쿼리 문자열 옵션을 사용해 S3 안전 키 생성
 */
function generateS3Key(template, title, options) {
  const parts = [
    template,
    ...(options !== "" ? [encodeURIComponent(options)] : []),
    `${encodeURIComponent(title)}.${ext}`,
  ];

  return parts.join("/");
}

이렇게 하면 입력 매개변수를 사용해 디렉토리 경로처럼 보이는 S3 안전 키가 생성됩니다. 필요할 때 S3 버킷을 쉽게 탐색할 수 있습니다.

get(key) 함수는 이 키가 이미 S3에 존재하는지 확인합니다.

async function get(Key) {
  const params = { Key, Bucket };

  try {
    const { Body } = await s3.getObject(params).promise();
    return Body;
  } catch (e) {
    return null;
  }
}

존재한다면 createResponse(fromBucket)를 호출해 바로 반환합니다.

page.screenshot()으로 스크린샷을 찍은 후 S3에 저장합니다.

// 버킷에 업로드
await upload(key, buffer);

upload 함수는 다음과 같습니다.

async function upload(Key, Body) {
  const params = {
    Key,
    Body,
    Bucket,
    ContentType,
  };

  await s3.putObject(params).promise();
}

전체 functions/lambda.js 소스 코드는 여기에서 확인할 수 있습니다 — github.com/sst/social-cards/blob/main/functions/lambda.js

이제 API 엔드포인트를 여러 번 로드하면 두 번째부터 훨씬 빠르게 동작하는 것을 확인할 수 있습니다.

CloudFront를 CDN으로 사용하기

API 요청 속도를 더 빠르게 만들기 위해 API 앞에 CDN을 추가할 것입니다. 이를 위해 CloudFront를 사용합니다.

stacks/MyStack.js에서 CloudFront 배포를 정의합니다.

// CloudFront 배포 생성
const distribution = new cf.Distribution(this, "WebsiteCdn", {
  defaultBehavior: {
    origin: new HttpOrigin(Fn.parseDomainName(api.url)),
    viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    cachePolicy: new cf.CachePolicy(this, "WebsiteCachePolicy", {
      // 캐시 지속 시간을 1년으로 설정
      minTtl: Duration.seconds(31536000),
      // 쿼리 문자열을 오리진으로 전달
      queryStringBehavior: cf.CacheQueryStringBehavior.all(),
    }),
  },
});

여기서 몇 가지 작업을 수행합니다:

  1. CDN의 오리진을 API로 설정합니다.
  2. CDN의 http 방문자를 https로 리디렉션합니다.
  3. 요청을 최대 1년 동안 캐시합니다.
  4. 쿼리 문자열 매개변수를 캐시된 요청의 일부로 만듭니다.

이제 이전에 사용한 URL 스키마에서 CloudFront 도메인을 교체하면 이미지가 매우 빠르게 로드될 것입니다.

https://d12c5yrsx1d0su.cloudfront.net/serverless-stack-blog/VGhpcyUyMGlzJTIwYSUyMHNhbXBsZSUyMGJsb2clMjBwb3N0JTIwb24lMjBTZXJ2ZXJsZXNzJTIwU3RhY2s=.png?author=Jay&avatar=jay

CloudFront에서 생성된 소셜 카드 이미지

커스텀 도메인 추가하기

우리는 소셜 카드 서비스를 우리만의 커스텀 도메인에서 호스팅하고 싶습니다. 이 예제에서는 여러분이 도메인을 Route 53에 설정했다고 가정합니다.

새 도메인을 만들고 싶다면, Route 53에서 도메인을 구매하는 가이드를 따라보세요.

다른 프로바이더에서 호스팅 중인 도메인이 있다면, Route 53으로 마이그레이션하는 방법을 읽어보세요.

이 작업은 stacks/MyStack.js에서 CloudFront 정의 위에 다음 블록을 추가하여 수행할 수 있습니다.

const rootDomain = "sst.dev";
const domainName = `social-cards.${rootDomain}`;

const useCustomDomain = app.stage === "prod";

if (useCustomDomain) {
  // 도메인 호스팅 영역 조회
  hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
    domainName: rootDomain,
  });

  // ACM 인증서 생성
  const certificate = new DnsValidatedCertificate(this, "Certificate", {
    domainName,
    hostedZone,
    region: "us-east-1",
  });

  domainProps = {
    ...domainProps,
    certificate,
    domainNames: [domainName],
  };
}

이 코드는 도메인에 대한 인증서를 생성합니다. 이 인증서는 us-east-1 리전에 있어야 합니다.

그런 다음 이 domainProps를 CloudFront Distribution에 전달합니다.

// CloudFront Distribution 생성
const distribution = new cf.Distribution(this, "WebsiteCdn", {
  ...domainProps,
  defaultBehavior: {
    origin: new HttpOrigin(Fn.parseDomainName(api.url)),
    // ...

마지막으로, Route 53에서 도메인을 설정합니다.

if (useCustomDomain) {
  // DNS 레코드 생성
  new route53.ARecord(this, "AliasRecord", {
    zone: hostedZone,
    recordName: domainName,
    target: route53.RecordTarget.fromAlias(new CloudFrontTarget(distribution)),
  });
}

전체 stacks/MyStack.js 소스 코드는 여기에서 확인할 수 있습니다 — github.com/sst/social-cards/blob/main/stacks/MyStack.js

이제 커스텀 도메인 URL을 로드할 수 있습니다!

https://social-cards.sst.dev/serverless-stack-blog/VGhpcyUyMGlzJTIwYSUyMHNhbXBsZSUyMGJsb2clMjBwb3N0JTIwb24lMjBTZXJ2ZXJsZXNzJTIwU3RhY2s=.png?author=Jay&avatar=jay

커스텀 도메인에서 생성된 소셜 카드 이미지

정적 사이트 생성기와 통합하기

이제 우리의 소셜 카드 서비스가 준비되었고 프로덕션 환경에 최적화되었습니다. Jekyll부터 시작하여 정적 웹사이트에서 이를 어떻게 사용할지 살펴보겠습니다.

Jekyll과 통합하기

제목을 Base64로 인코딩해야 합니다. 이를 쉽게 하기 위해 간단한 플러그인을 만들겠습니다. Jekyll 사이트의 _plugins/base64_filter.rb에 다음 내용을 추가하세요.

require "base64"

module Base64Filter
  def base64_encode (input)
    Base64.encode64(input)
  end
end

Liquid::Template.register_filter(Base64Filter) # 필터를 전역으로 등록

<head> 태그가 있는 레이아웃에 다음을 추가하세요.

{% if page.id %} {% assign encoded_title=title | truncate: 700 | url_encode |
base64_encode | url_encode %}
<meta
  content="https://social-cards.sst.dev/serverless-stack-blog/{{ encoded_title }}.png?author={{ site.data.authors[page.author].name | url_encode }}&avatar={{ page.author }}"
  property="og:image"
/>
{% endif %}

여기서는 현재 페이지가 블로그 포스트인 경우 og:image 태그를 추가합니다. 또한 제목에 대해 몇 가지 작업을 수행합니다.

  • 700자로 제한합니다. 길이를 관리하기 쉽게 하기 위함이며, S3에 파일을 캐시할 때 사용하는 키가 1024자로 제한되기 때문입니다.
  • 그런 다음 URL 인코딩을 통해 ASCII로 변환합니다. 그리고 Base64로 인코딩합니다.
  • 마지막으로 Base64 문자 집합에 URL에 안전하지 않은 몇 가지 문자가 있기 때문에 한 번 더 URL 인코딩합니다.

위 코드 스니펫에서는 블로그 포스트의 프론트 매터에 author가 설정되어 있다고 가정합니다.

author: jay

또한 사이트의 모든 작성자를 저장하는 데이터 파일 _data/authors.yml이 있습니다.

jay:
  name: Jay

Docusaurus와 통합하기

Docusaurus와 통합하려면, og:image 태그를 추가하기 위해 테마를 감싸야 합니다.

theme-original을 사용 중이라면, src/theme/DocItem/index.js에 다음 코드를 추가할 수 있습니다.

import React from "react";
import { Base64 } from "js-base64";
import Head from "@docusaurus/Head";
import OriginalDocItem from "@theme-original/DocItem";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";

export default function DocItem(props) {
  const { siteConfig } = useDocusaurusContext();
  const title = props.content.metadata.title;
  const author = props.content.frontMatter.author;
  const { authors, socialCardsUrl } = siteConfig.customFields;

  const encodedTitle = encodeURIComponent(
    Base64.encode(encodeURIComponent(title.substring(0, 700)))
  );
  const encodedName = encodeURIComponent(authors[author].name);

  const metaImageUrl = `${socialCardsUrl}/serverless-stack-blog/${encodedTitle}.png?author=${encodedName}&avatar=${author}`;

  return (
    <>
      <OriginalDocItem {...props} />
      <Head>
        <meta property="og:image" content={metaImageUrl} />
      </Head>
    </>
  );
}

여기서는 원본 테마 컴포넌트를 감싸는 방식을 사용합니다. 클라이언트와 서버 모두에서 실행될 수 있도록 Base64 npm 패키지를 사용합니다.

Jekyll 예제와 마찬가지로, 제목의 크기를 제한하고 docusaurus.config.js에는 소셜 카드 서비스의 URL과 작성자 정보를 포함하는 커스텀 필드를 추가합니다.

customFields: {
  // "src/theme/DocItem/index.js"에서 동적으로 og:image 태그를 추가하기 위해 사용
  socialCardsUrl: "https://social-cards.sst.dev",
  authors: {
    jay: {
      name: "Jay"
    }
  },
},

이제 모든 페이지에 대해 동적으로 소셜 카드가 생성됩니다.

마무리

이미지가 어떻게 보이는지 확인하려면 Serverless-Stack.com문서 사이트에서 몇 페이지를 공유해 보세요.

또한, 소셜 카드 서비스를 구동하는 저장소도 확인해 보세요 — github.com/sst/social-cards

이 저장소는 Seed로 설정되어 있어, main 브랜치에 git push를 하면 프로덕션에 배포됩니다. 또한 스크린샷 생성에 문제가 있을 때 실시간으로 알림을 보냅니다.

Seed를 통해 배포된 소셜 카드 서비스

이 서비스를 구축하는 동안 몇 가지 SST 구문을 사용했습니다. 자세한 내용은 아래 링크에서 확인할 수 있습니다:

이번 챕터가 도움이 되었길 바랍니다. 질문이나 피드백이 있다면 아래에 댓글을 남겨주세요!