npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@villion-inc/blog-sdk

v2.1.0

Published

Villion Blog public read API SDK — fetch articles, sites, and feeds from your Next.js / Node / browser app.

Readme

@villion-inc/blog-sdk

Villion 블로그 글을 내 사이트에서 불러오는 공식 TypeScript SDK입니다. 글 작성과 발행은 Villion 대시보드에서 하고, 화면은 내 사이트가 그립니다. SDK는 읽기만 하므로 비밀 키를 담지 않습니다.

npm install @villion-inc/blog-sdk

화면을 직접 만들지 않고 reader.villion.io 와 같은 디자인을 쓰려면 @villion-inc/blog-ui 를 씁니다. Next.js App Router 사이트에서 npx @villion-inc/blog-ui init 한 줄로 /blog 를 설치합니다. Villion 고객사 전용입니다.

v1(0.1.x)을 쓰고 있다면 v1에서 옮기기를 먼저 읽어 주세요. 글 목록의 기본 언어와 getArticle 인자 순서가 바뀌었습니다.

v2 핵심

| 항목 | v2 동작 | |---|---| | 글 목록 | locale을 비우면 사이트 기본 언어 글만 옵니다. 모든 언어는 locale: "all"입니다. | | 글 하나 | getArticle(slug, { locale }, init)로 언어를 고릅니다. | | 글 주소 | siteUrl과 주소 규칙으로 hreflang, 사이트맵 주소를 만들어 줍니다. | | 캐시 | requestInit에 한 번 넣으면 모든 요청에 붙습니다. | | 2.1 추가 | 카테고리, 관련 글, 댓글 목록, CMS 폼 조회. 방문자 브라우저용 createPublicClient(댓글 작성, 구독, 폼 제출) |

준비

대시보드의 블로그 설치 화면에서 개발자 연동을 고릅니다. Villion 호스팅과 개발자 연동은 함께 쓸 수 없습니다. 이 SDK는 개발자 연동용입니다.

| 값 | 찾는 곳 | 형태 | |---|---|---| | 사이트 ID | 블로그 설치 화면 주소 /publish/{siteId}/integrations의 {siteId} | UUID | | API 키 | 블로그 설치 > 개발자 연동 > API 키 탭 | vk_pub_... | | Webhook 시크릿 | 블로그 설치 > 개발자 연동 > Webhook 탭 | whsec_... | | 사이트 주소 | 블로그를 설치할 내 사이트 | https://www.example.com |

사이트 ID 자리에 사이트 slug를 넣으면 안 됩니다. slug는 사이트끼리 겹칠 수 있어 식별자로 쓰지 않습니다.

# .env.local
SITE_URL=https://www.example.com
VILLION_SITE_ID=00000000-0000-0000-0000-000000000000
VILLION_API_KEY=vk_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
VILLION_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxx   # webhook 을 받을 때만

빠른 시작

import { createClient } from "@villion-inc/blog-sdk";

const villion = createClient({
  siteId: process.env.VILLION_SITE_ID!,
  apiKey: process.env.VILLION_API_KEY!,
  siteUrl: process.env.SITE_URL, // hreflang, 사이트맵 주소를 만들 때 씁니다
});

const { articles, total } = await villion.listArticles({ limit: 20 }); // 기본 언어 글
const { articles: en } = await villion.listArticles({ locale: "en" }); // 영어 글
const post = await villion.getArticle("hello-world"); // 기본 언어 글 하나
const enPost = await villion.getArticle("hello-world", { locale: "en" }); // 영어 글 하나

언어 처리

글마다 언어(locale)가 있습니다. 언어가 비어 있는 글은 사이트 기본 언어 글로 봅니다. 대시보드에서 번역한 글은 같은 번역 묶음으로 이어지고, 글 상세의 translations에 실립니다.

| 호출 | 받는 글 | |---|---| | listArticles() | 사이트 기본 언어 글 | | listArticles({ locale: "en" }) | 영어 글 | | listArticles({ locale: "all" }) | 모든 언어 글 | | getArticle(slug) | 기본 언어 글. 그 slug의 기본 언어 글이 없으면 404 | | getArticle(slug, { locale: "en" }) | 영어 글 |

사이트 기본 언어는 대시보드 설정값을 한 번 조회해 씁니다. defaultLocale 옵션을 주면 조회하지 않고 그 값을 씁니다.

글 주소 규칙

기본 규칙은 아래와 같습니다. getArticleUrl, getAlternates, sitemapEntries가 이 규칙으로 주소를 만듭니다.

| 글 | 주소 | |---|---| | 기본 언어 글 | {siteUrl}/blog/{slug} | | 다른 언어 글 | {siteUrl}/{locale}/blog/{slug} |

사이트 라우트가 다르면 articlePath로 바꿉니다.

// 모든 언어에 접두사를 붙이는 사이트: /ko/blog/a, /en/blog/a
createClient({
  siteId, apiKey, siteUrl,
  articlePath: ({ slug, locale }) => `/${locale}/blog/${encodeURIComponent(slug)}`,
});

// 블로그 경로가 /news 인 사이트: /news/a, /en/news/a
createClient({
  siteId, apiKey, siteUrl,
  articlePath: ({ slug, locale, isDefaultLocale }) =>
    isDefaultLocale ? `/news/${slug}` : `/${locale}/news/${slug}`,
});

siteUrl을 비우면 /blog/a 같은 경로만 돌려줍니다. 사이트맵은 전체 주소가 필요하니 siteUrl을 꼭 넣어 주세요.

Next.js App Router

기본 언어는 /blog, 영어는 /en/blog에 두는 예시입니다.

| 파일 | 역할 | |---|---| | lib/villion.ts | 클라이언트와 글 조회 함수 | | app/blog/page.tsx | 기본 언어 글 목록 | | app/blog/[slug]/page.tsx | 기본 언어 글 화면 | | app/en/blog/page.tsx | 영어 글 목록 | | app/en/blog/[slug]/page.tsx | 영어 글 화면 | | app/sitemap.ts | 사이트맵 | | app/api/villion-webhook/route.ts | 발행 알림을 받아 화면 새로 만들기 |

클라이언트

// lib/villion.ts
import { cache } from "react";
import { createClient } from "@villion-inc/blog-sdk";

export const villion = createClient({
  siteId: process.env.VILLION_SITE_ID!,
  apiKey: process.env.VILLION_API_KEY!,
  siteUrl: process.env.SITE_URL,
  // 모든 요청을 5분 동안 캐시합니다(ISR). 발행 알림이 오면 바로 새로 만듭니다.
  requestInit: { next: { revalidate: 300 } },
});

// generateMetadata 와 화면이 같은 글을 두 번 부르지 않게 한 요청 안에서 묶습니다.
export const getPost = cache((slug: string, locale?: string) =>
  villion.getArticleOrNull(slug, { locale }),
);

글 목록

// app/blog/page.tsx
import Link from "next/link";
import { villion } from "@/lib/villion";

export default async function BlogPage() {
  const { articles } = await villion.listArticles({ limit: 20 });
  return (
    <ul>
      {articles.map((a) => (
        <li key={a.id}>
          <Link href={`/blog/${a.slug}`}>{a.title}</Link>
        </li>
      ))}
    </ul>
  );
}

영어 목록(app/en/blog/page.tsx)은 listArticles({ limit: 20, locale: "en" })로 부르고, 링크를 /en/blog/${a.slug}로 바꿉니다.

글 화면과 hreflang

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { villion, getPost } from "@/lib/villion";

type Props = { params: Promise<{ slug: string }> };

export async function generateStaticParams() {
  const articles = await villion.listAllArticles();
  return articles.filter((a) => a.slug).map((a) => ({ slug: a.slug! }));
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) return {};
  const alternates = await villion.getAlternates(post);
  return {
    title: post.seo_title || post.title,
    description: post.meta_description ?? undefined,
    alternates: {
      canonical: post.canonical_url || (await villion.getArticleUrl(post)),
      languages: Object.fromEntries(alternates.map((a) => [a.hreflang, a.url])),
    },
  };
}

export default async function PostPage({ params }: Props) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) notFound();
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.body_html }} />
    </article>
  );
}

getAlternates는 번역본마다 한 줄씩 주고, 마지막에 x-default를 붙입니다. x-default는 기본 언어 번역본을 가리킵니다. 기본 언어 번역본이 없으면 지금 글을 가리킵니다.

다른 언어 글 화면

app/en/blog/[slug]/page.tsx는 위 파일과 같습니다. 두 곳만 영어로 바꿉니다.

// app/en/blog/[slug]/page.tsx
const LOCALE = "en";

export async function generateStaticParams() {
  const articles = await villion.listAllArticles({ locale: LOCALE });
  return articles.filter((a) => a.slug).map((a) => ({ slug: a.slug! }));
}

// generateMetadata 와 PostPage 안에서
const post = await getPost(slug, LOCALE);

언어가 여럿이면 app/[lang]/blog/[slug]/page.tsx 하나로 묶을 수 있습니다. 이때는 export const dynamicParams = false를 두고, generateStaticParams에서 쓰는 언어만 돌려줍니다. 그래야 다른 주소가 이 라우트에 걸리지 않습니다.

사이트맵

// app/sitemap.ts
import type { MetadataRoute } from "next";
import { villion } from "@/lib/villion";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await villion.sitemapEntries(); // 모든 언어의 글 주소
  return [
    // 내 사이트의 다른 주소는 여기에 그대로 둡니다
    ...posts.map((p) => ({ url: p.url, lastModified: p.lastModified ?? undefined })),
  ];
}

getSitemap()이 주는 XML은 Villion 호스팅 블로그 주소 기준입니다. 설치형 사이트에서는 sitemapEntries()를 씁니다.

캐시와 즉시 갱신

| 방법 | 쓰는 곳 | 설명 | |---|---|---| | 공통 캐시 | createClient({ requestInit }) | 모든 요청에 붙습니다. | | 호출별 캐시 | 각 메서드의 마지막 인자 init | 공통 값보다 우선합니다. | | 즉시 갱신 | Webhook | 글을 발행하면 알림을 받아 해당 화면을 새로 만듭니다. |

// 목록만 10분 캐시하고 태그를 붙이는 예
const { articles } = await villion.listArticles(
  { limit: 20 },
  { next: { revalidate: 600, tags: ["blog-index"] } },
);

Webhook

Webhook 탭에 https://www.example.com/api/villion-webhook을 등록합니다. Villion은 요청 본문에 HMAC-SHA256 서명을 붙여 X-Villion-Signature 헤더로 보냅니다.

// app/api/villion-webhook/route.ts
import { revalidatePath } from "next/cache";
import { verifyWebhookSignature, type WebhookArticlePayload } from "@villion-inc/blog-sdk";

// 사이트가 쓰는 언어 접두사. 기본 언어는 접두사가 없습니다.
const PREFIXES = ["", "/en"];

export async function POST(req: Request) {
  const body = await req.text();
  const ok = await verifyWebhookSignature(
    process.env.VILLION_WEBHOOK_SECRET!,
    body,
    req.headers.get("x-villion-signature") || "",
  );
  if (!ok) return new Response("invalid signature", { status: 401 });

  const { data } = JSON.parse(body) as WebhookArticlePayload;
  const slug = data.article?.slug;
  for (const p of PREFIXES) {
    revalidatePath(`${p}/blog`);
    if (slug) revalidatePath(`${p}/blog/${slug}`);
  }
  revalidatePath("/sitemap.xml");
  return new Response("ok");
}

알림에는 아직 글 언어가 없습니다. 그래서 위 예시처럼 모든 언어 경로를 새로 만듭니다.

| 이벤트 | 보내는 때 | |---|---| | article.published | 발행, 변경사항 발행, 예약 발행, 검토 승인 | | article.unpublished | 발행 취소 | | article.deleted | 삭제 |

article.updated와 site.updated는 등록할 수 있지만 지금은 보내지 않습니다.

에러 처리

요청이 실패하면 VillionApiError를 던집니다.

import { VillionApiError } from "@villion-inc/blog-sdk";

try {
  const post = await villion.getArticle(slug);
} catch (e) {
  if (e instanceof VillionApiError) {
    if (e.isNotFound) { /* 없는 글 */ }
    if (e.isAuth) { /* 401, 403 */ }
    if (e.isRateLimit) { /* 재시도를 다 쓴 레이트리밋 */ }
  }
  throw e;
}

| 상황 | SDK 동작 | |---|---| | 429, 500, 502, 503, 504, 네트워크 오류 | maxRetries(기본 2)만큼 다시 시도합니다. 429는 Retry-After를 따릅니다. | | 그 밖의 4xx | 다시 시도하지 않고 바로 던집니다. | | 응답이 늦음 | timeoutMs(기본 10초)가 지나면 끊고 다시 시도합니다. | | 호출한 쪽에서 취소 | 다시 시도하지 않습니다. |

없는 글을 빈 화면으로 다룰 때는 getArticleOrNull이 편합니다. 404면 null을 돌려주고, 다른 오류는 그대로 던집니다.

작성자 표시

resolveAuthor는 글 작성자가 없으면 사이트 기본 작성자를 씁니다. 둘 다 없거나 작성자 표시를 끈 사이트면 show가 false입니다.

import { resolveAuthor } from "@villion-inc/blog-sdk";

const author = resolveAuthor(post, await villion.getSiteMeta());
if (author.show) {
  // author.name, author.avatarUrl, author.bio 로 표시
}

Next.js Pages Router

// pages/blog/index.tsx
import { villion } from "@/lib/villion";

export async function getStaticProps() {
  const { articles } = await villion.listArticles({ limit: 20 });
  return { props: { articles }, revalidate: 300 };
}

Pages Router에서는 lib/villion.ts의 requestInit과 cache를 빼고 씁니다.

이미지

hero_image_url과 og_image_url은 외부 도메인을 가리킬 수 있습니다. next/image로 그리려면 그 호스트를 remotePatterns에 넣어야 합니다. 넣지 않으면 실행 중에 오류가 납니다. 일반 <img>를 쓰면 설정이 필요 없습니다.

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "**.villion.io" },
      // 이미지가 다른 CDN 도메인에 있으면 그 호스트도 넣습니다
    ],
  },
};

API 참조

createClient(options)

| 옵션 | 타입 | 기본값 | 설명 | |---|---|---|---| | siteId | string | 필수 | 사이트 UUID | | apiKey | string | 필수 | vk_pub_... 읽기 전용 키 | | siteUrl | string | 없음 | 내 사이트 주소. https://로 시작해야 합니다. | | defaultLocale | string | 사이트 설정값 | 언어를 비웠을 때 쓸 언어 | | articlePath | 함수 | defaultArticlePath | 글 주소 규칙 | | requestInit | RequestInit | 없음 | 모든 요청에 붙일 fetch 옵션 | | maxRetries | number | 2 | 일시 오류 재시도 횟수. 0이면 끕니다. | | timeoutMs | number | 10000 | 요청 한 번의 제한 시간(ms) | | baseUrl | string | https://api.villion.io | API 주소. 바꿀 일이 거의 없습니다. | | fetch | 함수 | globalThis.fetch | 테스트나 특수 런타임용 fetch | | userAgent | string | villion-blog-sdk/<버전> | 요청 식별용 |

메서드

init은 선택 인자이고, fetch 옵션(RequestInit)을 받습니다. Next.js의 { next: { revalidate, tags } }, cache도 여기에 넣습니다.

| 메서드 | 돌려주는 값 | 설명 | |---|---|---| | listArticles(params?, init?) | { articles, total } | 글 목록 한 페이지. total은 조건에 맞는 전체 글 수입니다. | | listAllArticles(params?, opts?, init?) | BlogArticleSummary[] | 모든 페이지를 이어 받습니다. opts.pageSize 기본 100, 최대 200. | | getArticle(slug, params?, init?) | BlogArticleDetail | 글 하나. 없으면 404 오류를 던집니다. | | getArticleOrNull(slug, params?, init?) | BlogArticleDetail \| null | 글 하나. 없으면 null입니다. | | getArticleUrl(article) | string | 글 주소 | | getAlternates(article) | ArticleAlternate[] | hreflang 목록 (hreflang, url) | | sitemapEntries(init?) | SitemapEntry[] | 모든 언어 글의 주소와 수정일 | | getSiteMeta() | BlogSiteMeta | 사이트 정보. 한 번 받은 뒤 다시 씁니다. | | getSitemap() | string | Villion 호스팅 기준 sitemap XML | | getFeed() | string | Villion 호스팅 기준 RSS XML | | listCategories(init?) | BlogCategory[] | 카테고리 목록 (2.1) | | listCategoryArticles(slug, params?, init?) | { articles, total } | 카테고리 하나의 글. locale 규칙은 listArticles와 같습니다. 없는 카테고리는 404 (2.1) | | getRelatedArticles(slug, params?, init?) | BlogArticleSummary[] | 관련 글. 태그가 겹치는 글 먼저, 모자라면 최신 글. 같은 언어 글만 옵니다 (2.1) | | listComments(slug, params?, init?) | BlogComment[] | 발행된 댓글 (2.1) | | getForm(formId, init?) | BlogForm | CMS 폼 정의. 활성 폼만 옵니다 (2.1) |

listArticles의 params는 아래 값을 받습니다.

| 값 | 설명 | |---|---| | limit | 한 페이지 글 수. 1~200, 기본 50 | | offset | 건너뛸 글 수 | | locale | 글 언어. 비우면 기본 언어, "all"이면 모든 언어 | | tag | 이 태그가 붙은 글만. 태그 이름과 정확히 같아야 합니다. | | sort | published_at(기본), updated_at, title | | order | desc(기본), asc |

createPublicClient(options) (2.1)

방문자 브라우저에서 쓰는 클라이언트입니다. API 키를 쓰지 않습니다. 댓글 작성, 구독, 폼 제출은 이 클라이언트로 브라우저에서 보냅니다. Villion은 요청한 사람의 IP로 호출 한도와 스팸을 판단하는데, 사이트 서버를 거치면 모든 방문자가 서버 IP 하나로 보이기 때문입니다.

"use client";
import { createPublicClient } from "@villion-inc/blog-sdk";

const pub = createPublicClient({ siteId: process.env.NEXT_PUBLIC_VILLION_SITE_ID! });
await pub.subscribe({ email, source: "blog_banner" });

사이트 ID는 공개돼도 되는 값이라 NEXT_PUBLIC_으로 브라우저에 넘겨도 됩니다. API 키는 넘기지 않습니다.

| 메서드 | 설명 | |---|---| | listComments(slug, params?) | 발행된 댓글 | | createComment(slug, input, params?) | 댓글 작성. input.website는 봇 걸러내기용 숨긴 칸 값입니다 | | subscribe(input) | 구독. 이미 구독 중이면 조용히 성공합니다 | | getForm(formId) | CMS 폼 정의 | | submitForm(formId, data, { articleId? }) | CMS 폼 제출 |

쓰기 요청은 중복 저장을 막으려고 재시도하지 않습니다. 댓글과 구독 폼은 사이트 정보의 comments_enabled, subscribe_enabled가 켜진 사이트에서만 그립니다(기본 꺼짐).

함수와 상수

| 이름 | 설명 | |---|---| | verifyWebhookSignature(secret, body, signature) | Webhook 서명 검증. Web Crypto와 Node 모두 지원 | | resolveAuthor(article, site) | 표시할 작성자 정하기 | | defaultArticlePath(input) | 기본 글 주소 규칙 | | ALL_LOCALES | "all" | | FORM_TOKEN_RE | 글 본문의 CMS 폼 자리 표시 [[form:ID]]를 찾는 정규식 (2.1) | | SDK_VERSION | SDK 버전 문자열 |

타입은 모두 export합니다.

import type {
  BlogArticleSummary, BlogArticleDetail, BlogArticleTranslation, BlogSiteMeta,
  ArticleAlternate, SitemapEntry, ListArticlesParams, GetArticleParams,
  ListArticlesResponse, VillionClient, VillionClientOptions, WebhookArticlePayload,
  BlogCategory, BlogComment, BlogForm, BlogFormField, CreateCommentInput, SubscribeInput,
  VillionPublicClient, VillionPublicClientOptions,
} from "@villion-inc/blog-sdk";

v1에서 옮기기

v1 코드는 v2에서 그대로 돌지 않을 수 있습니다. 아래 표대로 고칩니다.

| v1 | v2 | 할 일 | |---|---|---| | listArticles()가 모든 언어 글을 줌 | 기본 언어 글만 줌 | 모든 언어가 필요하면 { locale: "all" } | | listAllArticles()가 모든 언어 글을 줌 | 기본 언어 글만 줌 | 같음 | | getArticle(slug, init, locale) | getArticle(slug, { locale }, init) | 인자 순서를 바꿉니다. | | getArticleOrNull(slug, init, locale) | getArticleOrNull(slug, { locale }, init) | 같음 | | 기본 API 주소 api-dev.villion.io | api.villion.io | 할 일 없음. 같은 서버입니다. | | 호출마다 캐시 옵션 | requestInit 한 번 | 선택 | | hreflang, 사이트맵을 직접 만듦 | getAlternates, sitemapEntries | 선택 |

// v1
await client.getArticle(slug, { next: { revalidate: 300 } }, "en");

// v2
await client.getArticle(slug, { locale: "en" }, { next: { revalidate: 300 } });

TypeScript는 v1 순서로 부르면 빌드에서 오류를 냅니다. JavaScript는 오류 없이 언어와 캐시 옵션을 놓치니 직접 찾아 고쳐야 합니다.

자주 겪는 문제

| 증상 | 원인 | 해결 | |---|---|---| | 영어 글이 /en/blog에 안 보임 | 영어 목록과 글 화면이 없거나, 언어 없이 조회함 | /en/blog 라우트를 만들고 locale: "en"으로 조회합니다. | | 영어 글 주소가 404 | getArticle(slug)는 기본 언어 글을 찾음 | getArticle(slug, { locale: "en" }) | | 사이트맵, hreflang 주소가 /blog/...로만 나옴 | siteUrl이 없음 | createClient에 siteUrl을 넣습니다. | | 발행했는데 화면이 그대로 | 캐시가 남아 있음 | Webhook을 등록하거나 캐시 시간을 줄입니다. | | 429 오류가 자주 남 | 키가 없거나 틀려서 익명 한도(분당 30회)가 적용됨 | 응답 헤더 x-ratelimit-scope가 anon이면 API 키 탭에서 키를 확인합니다. | | 몇 초 걸린 뒤 500 오류 | siteId에 UUID가 아닌 값(slug 등)을 넣음 | 블로그 설치 화면 주소의 UUID를 넣습니다. | | next/image 오류 | 이미지 호스트가 remotePatterns에 없음 | 이미지를 봅니다. |

런타임

| 환경 | 지원 | |---|---| | Next.js App Router, Pages Router, SSG, ISR | 지원 | | Node 18 이상 | 지원 | | Edge 런타임 | 지원 | | 브라우저 | 지원. 아래 보안 안내를 봅니다. |

표준 Web API(fetch, AbortController, URLSearchParams, Headers)만 씁니다. 다른 fetch가 필요하면 createClient({ fetch })로 넣습니다.

보안

API 키(vk_pub_...)는 읽기 전용입니다. 글을 쓰거나 지울 수 없습니다. 대시보드에서 언제든 폐기할 수 있습니다.

브라우저에서 직접 부르면 키가 번들에 드러납니다. 콘텐츠가 새지는 않지만, 다른 사람이 키 사용량을 쓸 수 있습니다. 서버 컴포넌트나 라우트 핸들러에서 부르기를 권합니다.

읽기 API는 키 없이도 답합니다. 키가 틀렸거나 폐기된 경우도 오류 없이 익명 요청으로 받습니다. 대신 요청 한도가 다릅니다.

| 요청 | 분당 | 시간당 | 한도 기준 | |---|---|---|---| | 맞는 키 | 120회 | 3,000회 | 키 | | 키 없음, 틀린 키 | 30회 | 600회 | IP |

한도를 넘으면 429가 납니다. SDK는 Retry-After만큼 기다렸다 다시 시도합니다. 글이 많은 사이트를 한 번에 빌드할 때 이 한도에 걸릴 수 있으니, 운영 환경에서는 항상 apiKey를 넣어 주세요.

Webhook 시크릿(whsec_...)으로 서명을 꼭 검증하세요. 검증하지 않으면 누구나 갱신 요청을 보낼 수 있습니다.

라이선스

MIT © Villion Inc.