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

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}/integrations URL 의 {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.configremotePatterns 에 등록해야 합니다. 등록하지 않으면 런타임 에러가 납니다.

// 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 를 던집니다. statusbody 로 분기할 수 있습니다.

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?)BlogArticleDetail
  • getSiteMeta()BlogSiteMeta
  • getSitemap()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.