@villion-inc/blog-sdk
v0.1.3
Published
Villion Blog public read API SDK — fetch articles, sites, and feeds from your Next.js / Node / browser app.
Downloads
293
Readme
@villion-inc/blog-sdk
Villion 블로그의 공개 read API 호출용 공식 TypeScript SDK. 글 목록, 본문, 사이트 메타, sitemap, RSS feed 를 내 사이트(Next.js / Node / 브라우저)에서 불러옵니다.
npm install @villion-inc/blog-sdk콘텐츠 작성과 발행은 Villion 대시보드에서 하고, 화면 렌더는 내 사이트가 담당하는 headless 구조입니다. SDK 는 읽기 전용이라 비밀 키를 담지 않습니다.
빠른 시작
import { createClient } from "@villion-inc/blog-sdk";
const client = createClient({
siteId: process.env.VILLION_SITE_ID!, // blog_sites.id (UUID, slug 아님)
apiKey: process.env.VILLION_API_KEY!, // vk_pub_... (대시보드 → 연동 → API 키)
});
const { articles, total } = await client.listArticles({ limit: 20 });
const detail = await client.getArticle("hello-world");
const meta = await client.getSiteMeta();
siteId는 사이트의 UUID 입니다 (대시보드/publish/{siteId}/integrationsURL 의{siteId}). slug 는 사이트마다 중복될 수 있어 식별자로 쓰지 않습니다.
환경 변수
.env (또는 .env.local) 에 다음을 둡니다. 키 발급은 대시보드 연동센터에서 합니다.
VILLION_SITE_ID=00000000-0000-0000-0000-000000000000
VILLION_API_KEY=vk_pub_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
VILLION_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxx # webhook 수신 시에만Next.js App Router (SSG / ISR)
// app/blog/page.tsx
import { createClient } from "@villion-inc/blog-sdk";
const client = createClient({
siteId: process.env.VILLION_SITE_ID!,
apiKey: process.env.VILLION_API_KEY!,
});
export const revalidate = 3600; // 1h ISR. webhook 으로 즉시 무효화 권장
export default async function Page() {
const { articles } = await client.listArticles({ limit: 20 });
return (
<ul>
{articles.map((a) => (
<li key={a.id}>
<a href={`/blog/${a.slug}`}>{a.title}</a>
</li>
))}
</ul>
);
}글 상세 (동적 라우트 + 정적 생성)
// app/blog/[slug]/page.tsx
import { createClient } from "@villion-inc/blog-sdk";
import { notFound } from "next/navigation";
const client = createClient({
siteId: process.env.VILLION_SITE_ID!,
apiKey: process.env.VILLION_API_KEY!,
});
export async function generateStaticParams() {
const { articles } = await client.listArticles({ limit: 100 });
return articles.map((a) => ({ slug: a.slug! }));
}
export default async function ArticlePage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
let detail;
try {
detail = await client.getArticle(slug);
} catch {
notFound();
}
return (
<article>
<h1>{detail.title}</h1>
{/* body_markdown 을 ReactMarkdown 등으로 렌더하거나, body_html 을 직접 사용 */}
<div dangerouslySetInnerHTML={{ __html: detail.body_html }} />
</article>
);
}캐시 제어 (ISR 무효화 옵션)
listArticles / getArticle 는 두 번째 인자로 Next 의 fetch 옵션을 받습니다.
// 글마다 다른 재검증 주기 / 태그 무효화
const { articles } = await client.listArticles(
{ limit: 20 },
{ next: { revalidate: 600, tags: ["blog-index"] } },
);Next.js Pages Router
// pages/blog/index.tsx
import { createClient } from "@villion-inc/blog-sdk";
const client = createClient({
siteId: process.env.VILLION_SITE_ID!,
apiKey: process.env.VILLION_API_KEY!,
});
export async function getStaticProps() {
const { articles } = await client.listArticles({ limit: 20 });
return { props: { articles }, revalidate: 3600 };
}이미지 렌더 (next/image 사용 시 필수)
hero_image_url / og_image_url 은 외부 도메인을 가리킬 수 있습니다. next/image 로 렌더하려면 그 호스트를 next.config 의 remotePatterns 에 등록해야 합니다. 등록하지 않으면 런타임 에러가 납니다.
// next.config.js
module.exports = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "**.villion.io" },
// hero/og 이미지가 다른 CDN 도메인이면 그 호스트도 추가
],
},
};next/image 대신 일반 <img> 를 쓰면 이 설정은 필요 없습니다.
Webhook 수신 + 검증
글이 발행/수정되면 등록한 webhook 으로 알림이 옵니다. 서명을 검증한 뒤 해당 경로를 재검증하면 ISR 을 즉시 갱신할 수 있습니다.
// app/api/villion-webhook/route.ts
import { verifyWebhookSignature, type WebhookArticlePayload } from "@villion-inc/blog-sdk";
import { revalidatePath } from "next/cache";
export async function POST(req: Request) {
const sig = req.headers.get("x-villion-signature") || "";
const body = await req.text();
const ok = await verifyWebhookSignature(
process.env.VILLION_WEBHOOK_SECRET!,
body,
sig,
);
if (!ok) return new Response("invalid signature", { status: 401 });
const payload = JSON.parse(body) as WebhookArticlePayload;
revalidatePath("/blog");
if (payload.data.article?.slug) {
revalidatePath(`/blog/${payload.data.article.slug}`);
}
return new Response("ok");
}에러 처리
실패한 요청은 VillionApiError 를 던집니다. status 와 body 로 분기할 수 있습니다.
import { VillionApiError } from "@villion-inc/blog-sdk";
try {
const detail = await client.getArticle(slug);
} catch (e) {
if (e instanceof VillionApiError) {
if (e.status === 404) {
// 없는 글 — notFound() 등으로 처리
} else if (e.status === 429) {
// 레이트리밋 — 잠시 후 재시도
}
}
throw e;
}sitemap / RSS feed
const sitemapXml = await client.getSitemap(); // string (XML)
const feedXml = await client.getFeed(); // string (RSS XML)Next.js 라우트 핸들러로 그대로 내보낼 수 있습니다.
// app/sitemap.xml/route.ts
export async function GET() {
const xml = await client.getSitemap();
return new Response(xml, { headers: { "content-type": "application/xml" } });
}API 참조
createClient(options)
| Option | Type | Default |
|--------------|---------|----------------------------------|
| siteId | string | — (필수) |
| apiKey | string | — (필수) |
| baseUrl | string | https://api-dev.villion.io |
| fetch | fn | globalThis.fetch |
| timeoutMs | number | 10000 |
| userAgent | string | villion-blog-sdk/<version> |
Methods
listArticles({ limit?, offset?, tag? }, init?)→{ articles, total }getArticle(slug, init?)→BlogArticleDetailgetSiteMeta()→BlogSiteMetagetSitemap()→string(XML)getFeed()→string(RSS XML)
init 은 선택 인자로 RequestInit(Next 의 { next: { revalidate, tags }, cache } 포함)을 받습니다.
타입
BlogArticleSummary, BlogArticleDetail, BlogSiteMeta, ListArticlesParams, ListArticlesResponse, VillionClient, VillionClientOptions, WebhookArticlePayload, VillionApiError 가 export 됩니다.
import type { BlogArticleDetail, BlogSiteMeta } from "@villion-inc/blog-sdk";Webhook helpers
verifyWebhookSignature(secret, body, signature)→Promise<boolean>(Web Crypto / Node 양쪽 호환, timing-safe 비교)
런타임 호환
| 환경 | 지원 | |------|------| | Next.js App Router (server) / Pages Router / SSG / ISR | 지원 | | Node 서버 (Node ≥ 18) | 지원 | | Edge runtime (read 경로) | 지원 | | 브라우저 / CSR | 지원 (아래 키 노출 주의) |
read 경로는 표준 Web API(fetch, AbortController, URLSearchParams)만 사용합니다. 커스텀 fetch 가 필요하면 createClient({ fetch }) 로 주입할 수 있습니다.
보안
- API 키는 vk_pub_… prefix 의 public read 전용. write 권한 없음. 대시보드에서 언제든 revoke 가능(revoke 즉시 401).
- 클라이언트(CSR)에서 직접 호출하면 키가 번들에 노출됩니다. read 전용 키라 콘텐츠 유출 위험은 없지만 사용량/레이트리밋 등급이 도용될 수 있습니다. 가능하면 서버 컴포넌트나 라우트 핸들러에서 호출해 키를 서버에 두는 것을 권장합니다.
- Webhook 시크릿은 whsec_….
verifyWebhookSignature로 HMAC-SHA256 서명 검증을 반드시 거치세요.
블로그 콘텐츠는 공개 자원이라 read 엔드포인트는 키 없이도 응답할 수 있지만, 키를 전달하면 더 높은 레이트리밋 등급이 적용되고 사용량이 키 단위로 집계됩니다. 운영 환경에서는 항상
apiKey를 함께 전달하세요.
라이선스
MIT © Villion Inc.
