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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@comneed/textby

v0.0.3

Published

Transform JSON data into LLM-friendly markdown with LiquidJS templates. Supports .md, .mdx, .liquid formats with powerful template composition.

Downloads

33

Readme

@comneed/textby

npm version License: MIT

JSON 데이터를 LLM-friendly 마크다운으로 변환하는 템플릿 기반 생성기

TextBy는 구조화된 JSON 데이터를 LLM이 이해하기 쉬운 마크다운 형식으로 변환하는 라이브러리입니다. llmstxt.org 표준을 따르며, Next.js, React, Vue, NestJS 등 모든 JavaScript/TypeScript 환경에서 사용할 수 있습니다.

LiquidJS 템플릿 엔진을 기반으로 하여 강력한 템플릿 문법을 제공합니다.

✨ 핵심 기능

  • 📝 다양한 템플릿 형식: .md, .liquid 파일 지원
  • 🔧 간단한 API: 직관적이고 사용하기 쉬운 인터페이스
  • 🚀 TypeScript 완벽 지원: 완전한 타입 안전성
  • 📦 범용적: 브라우저(SPA), Node.js, Next.js, Express 등 모든 환경
  • 🎨 LiquidJS 템플릿 문법: 변수, 반복문, 조건문, 필터, 커스텀 필터/태그
  • 📂 JSON 파일 자동 로드: 파일 경로만 전달하면 자동으로 데이터 로드 (Node.js)

📦 설치

pnpm add @comneed/textby
# or
npm install @comneed/textby
# or
yarn add @comneed/textby

🚀 빠른 시작

SPA (React, Vue, Svelte 등)에서 사용하기

브라우저 환경에서는 인라인 템플릿 문자열과 객체 데이터를 사용합니다:

import { TextBy } from '@comneed/textby';

const textby = new TextBy();

// 인라인 템플릿 + 객체 데이터
const markdown = await textby.render({
  template: `
# {{ name }}

> {{ description }}

## Skills
{% for skill in skills %}
- {{ skill }}
{% endfor %}
  `,
  data: {
    name: "John Doe",
    description: "Full Stack Developer",
    skills: ["React", "Next.js", "TypeScript"]
  }
});

console.log(markdown);

서버사이드 (Node.js, Next.js, Express)에서 사용하기

서버 환경에서는 파일 경로를 사용할 수 있습니다:

1. 템플릿 파일 작성 (templates/portfolio.md)

# {{ name }} ({{ fullName }})

> {{ description }}

## Skills

{% for category in skillCategories %}
### {{ category.category }}
{{ category.skills | join: ", " }}
{% endfor %}

2. 데이터 파일 작성 (data/portfolio.json)

{
  "name": "John Doe",
  "fullName": "John Doe",
  "description": "Full Stack Developer",
  "skillCategories": [
    {
      "category": "Frontend",
      "skills": ["React", "Next.js", "TypeScript"]
    }
  ]
}

3. 마크다운 생성

import { TextBy } from '@comneed/textby';

const textby = new TextBy();

// 방법 1: 파일 경로 모두 사용 (가장 간단!)
const markdown = await textby.render({
  template: './templates/portfolio.md',
  data: './data/portfolio.json'  // JSON 파일 자동 로드
});

// 방법 2: 템플릿 파일 + 객체 데이터
const markdown2 = await textby.render({
  template: './templates/portfolio.md',
  data: {
    name: "John Doe",
    // ... 데이터
  }
});

// 방법 3: 파일로 저장
await textby.renderToFile({
  template: './templates/portfolio.md',
  data: './data/portfolio.json',
  output: './public/portfolio.txt'
});

📚 API 문서

TextBy 생성자

new TextBy(options?: TextByOptions)

TextBy 인스턴스를 생성합니다.

Options:

interface TextByOptions {
  // LiquidJS 템플릿 조합 설정
  root?: string | string[];      // 메인 템플릿 디렉토리 (Node.js)
  partials?: string | string[];  // Partials 디렉토리 (Node.js)
  layouts?: string | string[];   // Layouts 디렉토리 (Node.js)
  templates?: Record<string, string>; // 인메모리 템플릿 (브라우저)
  globals?: Record<string, any>; // 전역 변수
  extname?: string;              // 파일 확장자 (기본값: '')
}

예시:

// Node.js: 파일 기반
const textby = new TextBy({
  root: './templates',
  partials: './templates/partials',
  extname: '.liquid'
});

// 브라우저: 인메모리
const textby = new TextBy({
  templates: {
    'header': '# {{ title }}',
    'footer': '---'
  }
});

render(options: RenderOptions): Promise<string>

템플릿과 데이터를 받아 문자열을 생성합니다.

Parameters:

interface RenderOptions {
  template: string;                    // 템플릿 문자열 또는 파일 경로
  data: Record<string, any> | string;  // 데이터 객체 또는 JSON 파일 경로
  isFilePath?: boolean;                // 파일 경로 여부 (기본값: 자동 감지)
}

자동 감지 규칙:

  • 브라우저: 항상 인라인 템플릿으로 처리 (isFilePath: true면 에러)
  • Node.js: .md, .liquid 확장자 또는 ./, ../, / 시작 시 파일로 인식

예시:

// 인라인 템플릿 (브라우저/Node.js 모두)
const result = await textby.render({
  template: '# {{ name }}',
  data: { name: 'John' }
});

// 파일 템플릿 (Node.js만)
const result = await textby.render({
  template: './templates/portfolio.md',
  data: { name: 'John' }
});

// JSON 파일 자동 로드 (Node.js만)
const result = await textby.render({
  template: './templates/portfolio.md',
  data: './data/portfolio.json'
});

renderToFile(options: RenderToFileOptions): Promise<void>

템플릿을 렌더링하고 파일로 저장합니다. (Node.js 전용)

Parameters:

interface RenderToFileOptions extends RenderOptions {
  output: string;  // 출력 파일 경로 (디렉토리 자동 생성)
}

예시:

await textby.renderToFile({
  template: './templates/portfolio.md',
  data: portfolioData,
  output: './public/llms.txt'
});

registerFilter(name: string, filter: Function): void

커스텀 Liquid 필터를 등록합니다.

예시:

textby.registerFilter('uppercase', (value: string) => {
  return value.toUpperCase();
});

// 템플릿에서 사용: {{ name | uppercase }}

registerTag(name: string, tag: any): void

커스텀 Liquid 태그를 등록합니다.

예시:

textby.registerTag('highlight', {
  parse(token) {
    this.content = token.args;
  },
  render(ctx) {
    return `**${this.content}**`;
  }
});

// 템플릿에서 사용: {% highlight Important %}

🎯 사용 사례

1. 서버사이드: 포트폴리오 llms.txt 생성

import { TextBy } from '@comneed/textby';

const textby = new TextBy();

// JSON 파일 사용 (권장)
await textby.renderToFile({
  template: './templates/portfolio.md',
  data: './data/portfolio.json',  // 자동 로드
  output: './public/llms.txt'
});

// 또는 객체 직접 전달
await textby.renderToFile({
  template: './templates/portfolio.md',
  data: {
    name: 'Your Name',
    skills: ['JavaScript', 'TypeScript', 'React'],
    projects: [/* ... */]
  },
  output: './public/llms.txt'
});

2. SPA: 동적 마크다운 생성

import { TextBy } from '@comneed/textby';

function generateReadme(userData) {
  const textby = new TextBy();

  // 인라인 템플릿 사용
  const markdown = await textby.render({
    template: `
# {{ name }}

## 프로필
- Email: {{ email }}
- GitHub: {{ github }}

## 스킬
{% for skill in skills %}
- {{ skill }}
{% endfor %}
    `,
    data: userData
  });

  return markdown;
}

3. API 문서 생성

await textby.renderToFile({
  template: './templates/api-docs.md',
  data: './data/api-spec.json',  // OpenAPI 스펙 등
  output: './docs/api.txt'
});

📖 템플릿 문법

TextBy는 강력한 템플릿 문법을 지원합니다. 주요 문법:

변수 출력

{{ variableName }}
{{ object.property }}
{{ array[0] }}

반복문

{% for item in items %}
  {{ item.name }}
{% endfor %}

{% for item in items limit:3 %}
  제한된 반복
{% endfor %}

조건문

{% if condition %}
  내용
{% elsif otherCondition %}
  다른 내용
{% else %}
  기본 내용
{% endif %}

필터

{{ text | upcase }}
{{ text | downcase }}
{{ array | join: ", " }}
{{ text | append: "suffix" }}
{{ text | prepend: "prefix" }}
{{ number | plus: 5 }}
{{ text | replace: "old", "new" }}

더 많은 필터와 문법은 LiquidJS 공식 문서를 참고하세요.

🧩 템플릿 조합 (LiquidJS 기능)

TextBy는 LiquidJS 템플릿 엔진을 사용하므로, LiquidJS의 모든 템플릿 조합 기능을 그대로 사용할 수 있습니다:

  • {% include %} - 부모 스코프를 공유하는 partial 포함
  • {% render %} - 격리된 스코프의 partial 렌더링
  • {% layout %} - 레이아웃 상속 및 블록 오버라이드

템플릿 조합 설정

Node.js 환경 (파일 기반)

const textby = new TextBy({
  root: './templates',              // 메인 템플릿 디렉토리
  partials: './templates/partials', // partial 템플릿 디렉토리
  layouts: './templates/layouts',   // layout 템플릿 디렉토리
  extname: '.liquid'                // 자동 확장자 추가
});

브라우저 환경 (인메모리)

const textby = new TextBy({
  templates: {
    'header': `# {{ name }}`,
    'footer': `---\n*Generated with TextBy*`
  }
});

템플릿 조합 간단 예제

// Partial 템플릿 사용
const result = await textby.render({
  template: `
{% include 'header' %}

Content goes here

{% include 'footer' %}
  `,
  data: { name: 'John Doe' }
});

더 자세한 템플릿 조합 문법LiquidJS 공식 문서를 참고하세요.

🔧 개발

# 의존성 설치
pnpm install

# 빌드
pnpm build

# 개발 모드
pnpm dev

# 테스트
pnpm test

# 린트
pnpm lint

📄 라이선스

MIT

🌐 Node.js 서버 통합

Express, Fastify 등 Node.js 서버에서도 사용 가능합니다.

import express from 'express';
import { TextBy } from '@comneed/textby';

const app = express();
const textby = new TextBy();

app.get('/llms.txt', async (req, res) => {
  const markdown = await textby.render({
    template: './templates/profile.md',
    data: profileData
  });

  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.send(markdown);
});

app.listen(3000);

📦 실제 사용 예시

모노레포의 apps/docs 앱에서 TextBy를 실제로 사용하는 완전한 예시를 확인할 수 있습니다:

cd apps/docs
pnpm dev
# http://localhost:3002/llms.txt 접속

⚡ Next.js 통합

App Router (Next.js 13+)

// app/llms.txt/route.ts
import { TextBy } from '@comneed/textby';

export const dynamic = 'force-static'; // 정적 생성

export async function GET() {
  const textby = new TextBy();

  const markdown = await textby.render({
    template: './templates/profile.md', // 또는 .mdx
    data: profileData
  });

  return new Response(markdown, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Cache-Control': 'public, s-maxage=3600',
    },
  });
}

Pages Router

// pages/api/llms.txt.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { TextBy } from '@comneed/textby';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const textby = new TextBy();

  const markdown = await textby.render({
    template: './templates/profile.mdx',
    data: profileData
  });

  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.status(200).send(markdown);
}

실제 작동 예시: apps/docs 앱에서 완전한 Next.js 통합을 확인할 수 있습니다.

📋 지원하는 템플릿 형식

  • .md - 마크다운 파일 (Liquid 문법 포함)
  • .liquid - Liquid 템플릿 파일
  • 인라인 템플릿 - 파일 없이 문자열로 직접 전달

모든 형식에서 동일한 LiquidJS 템플릿 문법을 사용할 수 있습니다.

🔍 Troubleshooting

템플릿 파일을 찾을 수 없어요

증상: Error: ENOENT: no such file or directory

해결방법:

// 절대 경로 사용
import { join } from 'path';

const templatePath = join(process.cwd(), 'templates', 'docs.md');
const result = await textby.render({
  template: templatePath,
  data: myData
});

브라우저에서 파일 경로가 작동하지 않아요

원인: 브라우저 환경에서는 파일 시스템에 접근할 수 없습니다.

해결방법: 인라인 템플릿이나 인메모리 템플릿을 사용하세요.

// ❌ 브라우저에서는 작동하지 않음
const result = await textby.render({
  template: './templates/docs.md',
  data: myData
});

// ✅ 인라인 템플릿 사용
const result = await textby.render({
  template: `# {{ title }}\n{{ content }}`,
  data: myData
});

Liquid 문법 에러가 발생해요

증상: LiquidError: unexpected token

해결방법:

  • 태그는 {% %}, 변수는 {{ }}를 사용하세요
  • 태그는 반드시 닫아야 합니다 ({% for %}...{% endfor %})
  • 주석은 {% comment %}...{% endcomment %} 또는 {% # 주석 %}를 사용하세요

Next.js에서 빌드 시 에러가 나요

증상: Module not found: Can't resolve 'fs'

원인: 클라이언트 컴포넌트에서 파일 시스템을 사용하려고 시도

해결방법:

// app/llms.txt/route.ts (Server Component)
import { TextBy } from '@comneed/textby';

export async function GET() {
  const textby = new TextBy();
  // 서버에서만 파일 시스템 사용 가능
  const result = await textby.render({
    template: './templates/docs.md',
    data: './data/info.json'
  });
  return new Response(result);
}

한글이 깨져서 나와요

해결방법: 올바른 Content-Type 헤더를 설정하세요.

// Express
res.setHeader('Content-Type', 'text/plain; charset=utf-8');

// Next.js
return new Response(markdown, {
  headers: {
    'Content-Type': 'text/plain; charset=utf-8'
  }
});

⚡ 성능 최적화

1. 템플릿 캐싱

자주 사용하는 템플릿은 미리 컴파일하여 재사용하세요:

// 싱글톤 패턴으로 TextBy 인스턴스 재사용
class MarkdownGenerator {
  private static textby: TextBy;

  static getInstance() {
    if (!this.textby) {
      this.textby = new TextBy({
        // 파일 기반 템플릿은 자동으로 캐싱됨
        root: './templates',
        partials: './templates/partials',
      });
    }
    return this.textby;
  }
}

// 사용
const textby = MarkdownGenerator.getInstance();
const result = await textby.render({
  template: './templates/docs.md',
  data: myData
});

2. 데이터 최적화

큰 JSON 파일은 필요한 부분만 로드하세요:

// ❌ 전체 데이터 로드 (느림)
const allData = await loadEntireDatabase();

// ✅ 필요한 데이터만 로드
const pageData = await loadPageData(pageId);

const result = await textby.render({
  template: templateString,
  data: pageData // 작은 데이터셋
});

3. Next.js 정적 생성

빌드 시 한 번만 생성하도록 설정:

// app/llms.txt/route.ts
export const dynamic = 'force-static';
export const revalidate = 3600; // 1시간마다 재생성 (ISR)

export async function GET() {
  // 빌드 시 한 번만 실행됨
  const result = await textby.render({
    template: './templates/docs.md',
    data: './data/info.json'
  });
  
  return new Response(result, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Cache-Control': 'public, max-age=3600, s-maxage=3600'
    }
  });
}

4. 스트리밍 (대용량 데이터)

대용량 마크다운 생성 시 스트리밍 사용:

import { Readable } from 'stream';

// Express에서 스트리밍
app.get('/large-doc', async (req, res) => {
  const markdown = await textby.render({
    template: './templates/large-doc.md',
    data: hugeDataset
  });
  
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  const stream = Readable.from([markdown]);
  stream.pipe(res);
});

🎓 고급 사용법

동적 템플릿 선택

조건에 따라 다른 템플릿 사용:

const getTemplate = (type: string) => {
  const templates = {
    blog: './templates/blog-post.md',
    doc: './templates/documentation.md',
    api: './templates/api-reference.md'
  };
  return templates[type] || templates.doc;
};

const markdown = await textby.render({
  template: getTemplate(contentType),
  data: contentData
});

다국어 지원

언어별 템플릿 관리:

const textby = new TextBy({
  root: './templates',
  partials: './templates/partials'
});

const locale = 'ko'; // 또는 'en', 'ja' 등

const markdown = await textby.render({
  template: `./templates/${locale}/docs.md`,
  data: {
    ...contentData,
    locale: locale
  }
});

조건부 섹션

특정 조건에서만 섹션 표시:

{% if premium %}
## 프리미엄 기능

{% for feature in premiumFeatures %}
- {{ feature.name }}: {{ feature.description }}
{% endfor %}
{% endif %}

{% if hasProjects and projects.size > 0 %}
## 프로젝트
{% for project in projects %}
### {{ project.title }}
{{ project.description }}
{% endfor %}
{% endif %}

중첩 데이터 처리

깊게 중첩된 객체 다루기:

const complexData = {
  user: {
    profile: {
      personal: {
        name: "John Doe",
        contact: {
          email: "[email protected]"
        }
      }
    }
  }
};

const template = `
# {{ user.profile.personal.name }}

Email: {{ user.profile.personal.contact.email }}
`;

📊 실전 예제 모음

1. 블로그 포스트 생성

const blogPost = await textby.render({
  template: `
# {{ title }}

**작성자**: {{ author.name }} | **날짜**: {{ publishedAt }}

{{ content }}

## 태그
{% for tag in tags %}
- #{{ tag }}
{% endfor %}

## 관련 글
{% for related in relatedPosts %}
- [{{ related.title }}]({{ related.url }})
{% endfor %}
  `,
  data: {
    title: "TextBy 시작하기",
    author: { name: "Developer" },
    publishedAt: "2024-10-23",
    content: "TextBy는 강력한 템플릿 엔진입니다...",
    tags: ["tutorial", "javascript", "markdown"],
    relatedPosts: [
      { title: "고급 기능", url: "/advanced" }
    ]
  }
});

2. 팀 소개 페이지

const teamPage = await textby.render({
  template: `
# {{ teamName }}

> {{ tagline }}

## 팀원

{% for member in members %}
### {{ member.name }} - {{ member.role }}

{{ member.bio }}

**전문 분야**: {{ member.skills | join: ", " }}

{% if member.github %}🔗 [GitHub]({{ member.github }}){% endif %}
{% if member.twitter %}🐦 [Twitter]({{ member.twitter }}){% endif %}

---
{% endfor %}
  `,
  data: {
    teamName: "Development Team",
    tagline: "Building the future, together",
    members: [
      {
        name: "Alice",
        role: "Lead Developer",
        bio: "10년 경력의 풀스택 개발자",
        skills: ["React", "Node.js", "AWS"],
        github: "https://github.com/alice"
      }
    ]
  }
});

3. 제품 카탈로그

const catalog = await textby.render({
  template: `
# {{ storeName }} 제품 카탈로그

{% for category in categories %}
## {{ category.name }}

{{ category.description }}

{% for product in category.products %}
### {{ product.name }}

**가격**: {{ product.price | append: '원' }}
{% if product.discount %}
~~원가: {{ product.originalPrice }}원~~ **{{ product.discount }}% 할인!**
{% endif %}

{{ product.description }}

**재고**: {% if product.inStock %}✅ 있음{% else %}❌ 없음{% endif %}

---
{% endfor %}
{% endfor %}
  `,
  data: {
    storeName: "Tech Store",
    categories: [
      {
        name: "노트북",
        description: "최신 노트북 제품",
        products: [
          {
            name: "MacBook Pro",
            price: 2500000,
            originalPrice: 3000000,
            discount: 16,
            description: "강력한 M3 칩 탑재",
            inStock: true
          }
        ]
      }
    ]
  }
});

4. 이력서 생성

const resume = await textby.render({
  template: `
# {{ name }}

{{ title }} | {{ location }}

📧 {{ email }} | 📱 {{ phone }}
{% if linkedin %}🔗 [LinkedIn]({{ linkedin }}){% endif %}
{% if github %}💻 [GitHub]({{ github }}){% endif %}

## 소개

{{ summary }}

## 경력

{% for job in experience %}
### {{ job.company }} - {{ job.position }}
*{{ job.startDate }} - {{ job.endDate | default: "현재" }}*

{{ job.description }}

**주요 성과**:
{% for achievement in job.achievements %}
- {{ achievement }}
{% endfor %}
{% endfor %}

## 기술 스택

{% for category in skills %}
### {{ category.category }}
{{ category.items | join: " • " }}
{% endfor %}

## 학력

{% for edu in education %}
- **{{ edu.degree }}**, {{ edu.school }} ({{ edu.year }})
{% endfor %}
  `,
  data: {
    name: "김개발",
    title: "Senior Full Stack Developer",
    location: "서울, 대한민국",
    email: "[email protected]",
    phone: "010-1234-5678",
    linkedin: "https://linkedin.com/in/developer",
    github: "https://github.com/developer",
    summary: "10년 이상의 웹 개발 경험을 가진 풀스택 개발자입니다.",
    experience: [
      {
        company: "Tech Corp",
        position: "Senior Developer",
        startDate: "2020.01",
        endDate: null,
        description: "대규모 웹 애플리케이션 개발 리드",
        achievements: [
          "트래픽 50% 증가 처리",
          "API 응답 속도 40% 개선"
        ]
      }
    ],
    skills: [
      {
        category: "Frontend",
        items: ["React", "Vue", "TypeScript", "Next.js"]
      },
      {
        category: "Backend",
        items: ["Node.js", "Python", "PostgreSQL", "Redis"]
      }
    ],
    education: [
      {
        degree: "컴퓨터공학 학사",
        school: "한국대학교",
        year: "2014"
      }
    ]
  }
});

🤝 커뮤니티 및 지원

문제 제보

버그를 발견하셨나요? GitHub Issues에 제보해주세요.

기능 제안

새로운 기능을 제안하고 싶으신가요? GitHub Discussions에서 논의해주세요.

📚 추가 자료

🙏 크레딧

이 라이브러리는 다음 오픈소스 프로젝트를 기반으로 합니다:


Made with ❤️ by comneed