Full Code of eungyeole/velog-readme-stats for AI

main 1a587d66efb2 cached
31 files
21.1 KB
7.2k tokens
17 symbols
1 requests
Download .txt
Repository: eungyeole/velog-readme-stats
Branch: main
Commit: 1a587d66efb2
Files: 31
Total size: 21.1 KB

Directory structure:
gitextract_ok3hzzs_/

├── .github/
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   └── dependabot.yml
├── .gitignore
├── .nvmrc
├── .vscode/
│   └── settings.json
├── README.md
├── api/
│   └── serverless.ts
├── package.json
├── src/
│   ├── app.ts
│   ├── errors/
│   │   └── ApiError.ts
│   ├── index.ts
│   ├── router.ts
│   ├── routes/
│   │   ├── badge.ts
│   │   ├── ping.ts
│   │   ├── post-list.ts
│   │   ├── post.ts
│   │   └── redirect.ts
│   └── utils/
│       ├── get-recently-post.ts
│       ├── get-text-width.ts
│       └── velog-client/
│           ├── client.ts
│           ├── gql/
│           │   ├── posts.ts
│           │   └── read-post.ts
│           ├── index.ts
│           ├── types.ts
│           └── velog-client.ts
├── templates/
│   ├── badge.ejs
│   ├── post-list.ejs
│   └── post.ejs
├── tsconfig.json
└── vercel.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/CODEOWNERS
================================================
* @eungyeole

================================================
FILE: .github/FUNDING.yml
================================================
github: eungyeole


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: "npm" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"


================================================
FILE: .gitignore
================================================
.vercel
node_modules


================================================
FILE: .nvmrc
================================================
v18.12.1

================================================
FILE: .vscode/settings.json
================================================
{
  "editor.tabSize": 2,
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}


================================================
FILE: README.md
================================================
# velog-readme-stats란?

> markdown에서 나의 velog의 정보를 가져올수 있는 도구입니다.

# Velog 뱃지

> ?name= 의 값을 변경하세요

```
[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api/badge?name=eungyeole)](https://velog.io/@eungyeole)
```

[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api/badge?name=eungyeole)](https://velog.io/@eungyeole)

# 최신 글 가져오기

> ?name= 의 값을 변경하세요

```
[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api?name=eungyeole)](https://github.com/eungyeole/velog-readme-stats)
```

[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api?name=eungyeole)](https://github.com/eungyeole/velog-readme-stats)

## 특정태그를 가진 최신글 가져오기

> Option : `&tag`

```
[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api?name=eungyeole&tag=github)](https://github.com/eungyeole/velog-readme-stats)
```

[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api?name=eungyeole&tag=github)](https://github.com/eungyeole/velog-readme-stats)

## 특정 제목을 가진 글

> Option : `&slug`

```
[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api?name=eungyeole&slug=Velog-포스트로-Github를-꾸며보자)](https://github.com/eungyeole/velog-readme-stats)
```

[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api?name=eungyeole&slug=Velog-포스트로-Github를-꾸며보자)](https://github.com/eungyeole/velog-readme-stats)

## 상태카드와 최신글 연결하기

> Option : name, tag
> 카드클릭시 최신글로 리다이렉트 합니다.

```
https://velog-readme-stats.vercel.app/api/redirect?name=eungyeole&tag=github
```

[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api?name=eungyeole&tag=github)](https://velog-readme-stats.vercel.app/api/redirect?name=eungyeole&tag=github)

# 최신 글 목록 가져오기 (Beta)

> ?name= 의 값을 변경하세요

```
[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api/list?name=eungyeole)](https://velog.io/@eungyeole)
```

[![Velog's GitHub stats](https://velog-readme-stats.vercel.app/api/list?name=eungyeole)](https://velog.io/@eungyeole)


================================================
FILE: api/serverless.ts
================================================
import app from "../src/app";

import { VercelRequest, VercelResponse } from "@vercel/node";

const serverless = async (req: VercelRequest, res: VercelResponse) => {
  await app.ready();

  return app.server.emit("request", req, res);
};

export default serverless;


================================================
FILE: package.json
================================================
{
  "name": "velog-readme-stats",
  "version": "1.0.0",
  "description": "당신의 Velog 상태를 깃허브에서 확인하세요.",
  "main": "./api/index.js",
  "scripts": {
    "dev": "ts-node-dev --respawn --transpile-only ./src/index.ts",
    "dev:vercel": "vercel dev --debug",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",
  "dependencies": {
    "@fastify/view": "^9.1.0",
    "@napi-rs/canvas": "^0.1.44",
    "ejs": "^3.1.10",
    "fastify": "^4.28.1",
    "graphql-request": "^6.1.0"
  },
  "devDependencies": {
    "@types/ejs": "^3.1.5",
    "@types/node": "^20.14.9",
    "@vercel/node": "^3.2.1",
    "ts-node-dev": "^2.0.0",
    "typescript": "^5.5.3",
    "vercel": "^34.3.0"
  }
}


================================================
FILE: src/app.ts
================================================
import Fastify from "fastify";
import { router } from "./router";
import fastifyView from "@fastify/view";
import ejs from "ejs";
import path from "path";
import { cwd } from "process";

const app = Fastify({
  logger: false,
});

app.register(router, {
  prefix: "/api",
});

app.register(fastifyView, {
  engine: {
    ejs,
  },
  templates: path.resolve(cwd(), "templates"),
});

export default app;


================================================
FILE: src/errors/ApiError.ts
================================================
export class ApiError extends Error {
  statusCode: number;

  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
    Error.captureStackTrace(this, this.constructor);
  }
}

export class NotFoundError extends ApiError {
  constructor(message = "Resource not found") {
    super(message, 404);
  }
}

export class InternalServerError extends ApiError {
  constructor(message = "Internal Server Error") {
    super(message, 500);
  }
}


================================================
FILE: src/index.ts
================================================
import app from "./app";

app.listen(
  {
    port: 3000,
  },
  (err, address) => {
    if (err) {
      throw err;
    }
    console.log(`Server listening on ${address}`);
  }
);


================================================
FILE: src/router.ts
================================================
import { FastifyInstance } from "fastify";
import { ping } from "./routes/ping";
import { redirect } from "./routes/redirect";
import { postList } from "./routes/post-list";
import { post } from "./routes/post";
import { badge } from "./routes/badge";

export const router = async (fastify: FastifyInstance) => {
  fastify.register(ping);
  fastify.register(redirect);
  fastify.register(postList);
  fastify.register(post);
  fastify.register(badge);
};


================================================
FILE: src/routes/badge.ts
================================================
import { FastifyInstance, FastifyRequest } from "fastify";
import { NotFoundError } from "../errors/ApiError";
import { getTextWidth } from "../utils/get-text-width";

export const badge = async (fastify: FastifyInstance) => {
  fastify.get(
    "/badge",
    async (
      req: FastifyRequest<{
        Querystring: { name: string };
      }>,
      res
    ) => {
      const { name } = req.query;

      if (!name) {
        throw new NotFoundError("name is required");
      }

      return res.type("image/svg+xml").view("badge.ejs", {
        name,
        getTextWidth,
      });
    }
  );
};


================================================
FILE: src/routes/ping.ts
================================================
import { FastifyInstance } from "fastify";

export const ping = async (fastify: FastifyInstance) => {
  fastify.get("/ping", async (_, res) => {
    res.send("pong");
  });
};


================================================
FILE: src/routes/post-list.ts
================================================
import { FastifyInstance, FastifyRequest } from "fastify";
import { velogClient } from "../utils/velog-client";

export const postList = async (fastify: FastifyInstance) => {
  fastify.get(
    "/list",
    async (
      req: FastifyRequest<{
        Querystring: { name: string };
      }>,
      res
    ) => {
      const { name } = req.query;

      try {
        const { posts } = await velogClient.getPosts({
          username: name,
          limit: 4,
        });

        const data = {
          username: name,
          posts: posts.map((post) => ({
            user: { username: name },
            url_slug: post.url_slug,
            title: post.title,
          })),
        };

        return res.type("image/svg+xml").view("post-list.ejs", data);
      } catch (e) {
        return res.send(e);
      }
    }
  );
};


================================================
FILE: src/routes/post.ts
================================================
import { FastifyInstance, FastifyRequest } from "fastify";
import { getTextWidth } from "../utils/get-text-width";
import { NotFoundError } from "../errors/ApiError";
import { getRecentlyPost } from "../utils/get-recently-post";

export const post = async (fastify: FastifyInstance) => {
  fastify.get(
    "/",
    async (
      req: FastifyRequest<{
        Querystring: { name: string; tag?: string; slug?: string };
      }>,
      res
    ) => {
      const { name, slug, tag } = req.query;

      if (!name) {
        throw new NotFoundError("name is required");
      }

      try {
        const post = await getRecentlyPost(name, { tag, slug });

        return res.type("image/svg+xml").view("post.ejs", {
          ...post,
          getTextWidth,
        });
      } catch (e) {
        return res.send(e);
      }
    }
  );
};


================================================
FILE: src/routes/redirect.ts
================================================
import { FastifyInstance, FastifyRequest } from "fastify";
import { NotFoundError } from "../errors/ApiError";
import { velogClient } from "../utils/velog-client";

export const redirect = async (fastify: FastifyInstance) => {
  fastify.get(
    "/redirect",
    async (
      req: FastifyRequest<{
        Querystring: { name: string; tag: string };
      }>,
      res
    ) => {
      const { name, tag } = req.query;

      try {
        const { posts } = await velogClient.getPosts({
          username: name,
          tag,
          limit: 1,
        });

        const [post] = posts;

        if (!post.url_slug) {
          throw new NotFoundError("Post not found");
        }

        const url = new URL(`https://velog.io/@${name}/${post.url_slug}`);

        res.redirect(url.toString(), 301);
      } catch (e) {
        return res.send(e);
      }
    }
  );
};


================================================
FILE: src/utils/get-recently-post.ts
================================================
import { velogClient } from "./velog-client";

interface GetRecentlyPostOptions {
  slug?: string;
  tag?: string;
}

interface RecentlyPost {
  title: string | null;
  likes: number | null;
  short_description: string | null;
  user: { username: string };
  tags: Array<string>;
}

export const getRecentlyPost = async (
  username: string,
  options?: GetRecentlyPostOptions
) => {
  const { slug, tag } = options || {};

  if (slug) {
    const { post } = await velogClient.getPostByUrlSlug({
      username,
      url_slug: slug,
    });
    return post as RecentlyPost;
  }

  const { posts } = await velogClient.getPosts({
    username,
    limit: 1,
    tag,
  });

  return posts[0] as RecentlyPost;
};


================================================
FILE: src/utils/get-text-width.ts
================================================
import canvas from "@napi-rs/canvas";

export function getTextWidth(text: string, font: number) {
  const ctx = canvas.createCanvas(1, 1).getContext("2d");
  ctx.font = `${font}px -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji`;
  const textMetrics = ctx.measureText(text);

  return textMetrics.width;
}


================================================
FILE: src/utils/velog-client/client.ts
================================================
import { GraphQLClient } from 'graphql-request'

export const client = new GraphQLClient('https://v3.velog.io/graphql')

================================================
FILE: src/utils/velog-client/gql/posts.ts
================================================
import { gql } from "graphql-request";

export const postsGql = gql`
  query velogPosts($input: GetPostsInput!) {
    posts(input: $input) {
      id
      title
      short_description
      thumbnail
      user {
        username
        profile {
          thumbnail
        }
      }
      url_slug
      released_at
      updated_at
      comments_count
      tags
      likes
    }
  }
`;


================================================
FILE: src/utils/velog-client/gql/read-post.ts
================================================
import { gql } from "graphql-request";

export const readPostGql = gql`
  query readPost($input: ReadPostInput!) {
    post(input: $input) {
      id
      title
      released_at
      updated_at
      body
      short_description
      is_markdown
      is_private
      is_temp
      thumbnail
      comments_count
      url_slug
      likes
      is_liked
      is_followed
      tags
      user {
        id
        username
        profile {
          id
          display_name
          thumbnail
          short_bio
          profile_links
        }
        velog_config {
          title
        }
      }
      comments {
        id
        user {
          id
          username
          profile {
            id
            thumbnail
          }
        }
        text
        replies_count
        level
        created_at
        level
        deleted
      }
      series {
        id
        name
        url_slug
        series_posts {
          id
          post {
            id
            title
            url_slug
            user {
              id
              username
            }
          }
        }
      }
      linked_posts {
        previous {
          id
          title
          url_slug
          user {
            id
            username
          }
        }
        next {
          id
          title
          url_slug
          user {
            id
            username
          }
        }
      }
    }
  }
`;


================================================
FILE: src/utils/velog-client/index.ts
================================================
export * from "./velog-client";
export * from "./types";


================================================
FILE: src/utils/velog-client/types.ts
================================================
export interface GetPostsParams {
  cursor?: number;
  limit?: number;
  username: string;
  tag?: string;
}

export type GetPostsResponse = {
  posts: Array<{
    id: string;
    title: string | null;
    short_description: string | null;
    thumbnail: string | null;
    url_slug: string | null;
    released_at: any | null;
    updated_at: any;
    comments_count: number | null;
    tags: Array<string>;
    is_private: boolean;
    likes: number | null;
    user: {
      id: string;
      username: string;
      profile: { id: string; thumbnail: string | null; display_name: string };
    } | null;
  }>;
};

export interface GetPostByUrlSlugParams {
  username: string;
  url_slug: string;
}

export interface GetPostByUrlSlugResponse {
  post: {
    id: string;
    title: string | null;
    released_at: any | null;
    updated_at: any;
    body: string | null;
    short_description: string | null;
    is_markdown: boolean | null;
    is_private: boolean;
    is_temp: boolean | null;
    thumbnail: string | null;
    comments_count: number | null;
    url_slug: string | null;
    likes: number | null;
    is_liked: boolean | null;
    is_followed: boolean | null;
    tags: Array<string>;
    user: {
      id: string;
      username: string;
      profile: {
        id: string;
        display_name: string;
        thumbnail: string | null;
        short_bio: string;
        profile_links: Record<string, any>;
      };
      velog_config: { title: string | null } | null;
    } | null;
    comments: Array<{
      id: string;
      text: string | null;
      replies_count: number | null;
      level: number | null;
      created_at: any | null;
      deleted: boolean | null;
      user: {
        id: string;
        username: string;
        profile: { id: string; thumbnail: string | null };
      } | null;
    }>;
    series: {
      id: string;
      name: string | null;
      url_slug: string | null;
      series_posts: Array<{
        id: string;
        post: {
          id: string;
          title: string | null;
          url_slug: string | null;
          user: { id: string; username: string } | null;
        } | null;
      }> | null;
    } | null;
    linked_posts: {
      previous: {
        id: string;
        title: string | null;
        url_slug: string | null;
        user: { id: string; username: string } | null;
      } | null;
      next: {
        id: string;
        title: string | null;
        url_slug: string | null;
        user: { id: string; username: string } | null;
      } | null;
    } | null;
  } | null;
}


================================================
FILE: src/utils/velog-client/velog-client.ts
================================================
import { client } from "./client";
import { postsGql } from "./gql/posts";
import {
  GetPostByUrlSlugParams,
  GetPostByUrlSlugResponse,
  GetPostsParams,
  GetPostsResponse,
} from "./types";
import { readPostGql } from "./gql/read-post";

class VelogClient {
  constructor() {}

  public async getPosts(params: GetPostsParams) {
    return await client.request<GetPostsResponse>(postsGql, {
      input: params,
    });
  }

  public async getPostByUrlSlug(params: GetPostByUrlSlugParams) {
    return await client.request<GetPostByUrlSlugResponse>(readPostGql, {
      input: params,
    });
  }
}

export const velogClient = new VelogClient();


================================================
FILE: templates/badge.ejs
================================================
<% const size = getTextWidth(name, 11); const rectWidth = size + 28; %>
<svg
  width="<%= rectWidth %>"
  height="20"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 <%= rectWidth %> 20"
>
  <style>
    .name {
      fill: #ffffff;
      font-weight: 500;
      font-size: 11px;
    }
    .background {
      width: 100%;
      height: 100%;
      fill: #20c997;
      rx: 3;
    }
    .logo {
      fill: #ffffff;
      width: 14px;
      height: 14px;
      transform: translateY(-2px);
    }
  </style>
  <rect class="background" />

  <g>
    <path
      class="logo"
      d="M18.6199 8.526V7.54163C17.9949 7.3385 17.2605 7.11975 16.4167 6.88538C15.573 6.63538 15.0027 6.51038 14.7058 6.51038C14.0496 6.51038 13.6589 6.82288 13.5339 7.44788L12.0105 16.0963C11.5261 15.4557 11.1277 14.9166 10.8152 14.4791C10.3308 13.7916 9.8855 13.0026 9.47925 12.1119C9.05737 11.2213 8.84644 10.4244 8.84644 9.72131C8.84644 9.29944 8.96362 8.9635 9.198 8.7135C9.41675 8.44788 9.83081 8.11194 10.4402 7.70569C9.81519 6.90881 9.03393 6.51038 8.09643 6.51038C7.59644 6.51038 7.18237 6.65881 6.85425 6.95569C6.5105 7.25256 6.33862 7.69006 6.33862 8.26819C6.33862 9.23694 6.74487 10.4479 7.55737 11.901C8.35425 13.3385 9.89331 15.5026 12.1746 18.3932L14.4949 18.5573L16.2761 8.526H18.6199Z"
    />

    <text
      textLength="<%= size %>"
      text-anchor="start"
      x="22"
      y="14"
      class="name"
    >
      <%= name %>
    </text>
  </g>
</svg>


================================================
FILE: templates/post-list.ejs
================================================
<svg xmlns="http://www.w3.org/2000/svg" width="450" height="160">
  <style>
    @keyframes fadeInAnimation {
      from {
        opacity: 0;
      }
      to {
        opacity: 1;
      }
    }

    svg {
      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
        sans-serif, Apple Color Emoji, Segoe UI Emoji;
      animation: fadeInAnimation 0.8s ease-in-out forwards;
    }
    .background {
      fill: #00000000;
      width: calc(100% - 1px);
      height: calc(100% - 1px);
      stroke: #8b8b8b22;
      stroke-opacity: 1;
      rx: 4.5px;
      x: 0.5px;
      y: 0.5px;
    }
    .header {
      font-size: 14px;
      font-weight: 600;
      fill: #8d96a0;
    }
    .log-title {
      font-size: 14px;
      font-weight: 400;
      fill: #8d96a0;
    }
    .log-description {
      font-size: 12px;
      fill: #8d96a0;
    }
    .tag-item {
      font-size: 12px;
      fill: #0ca678;
    }
    .heart-count {
      font-size: 12px;
      fill: #8d96a0;
    }
    .log-title:hover {
      fill: #0ca678;
      text-decoration: underline;
    }
    .list-style {
      font-size: 14px;
      fill: #8d96a0;
    }
  </style>
  <rect class="background" />

  <g transform="translate(25, 35)">
    <g transform="translate(0, 0)">
      <text x="0" y="0" class="header"><%= username %>.log's latest posts</text>
    </g>
  </g>
  <g transform="translate(0, 45)">
    <svg x="25" width="400" height="400" viewBox="0 0 400 400">
      <g transform="translate(0, 0)">
        <% posts.forEach((post, index) => { %>
        <text class="list-style" x="5" y="<%= 20 + index * 23 %>">•</text>
        <a
          href="https://velog.io/@<%= post.user.username %>/<%= post.url_slug %>"
        >
          <text x="20" y="<%= 20 + index * 23 %>" class="log-title">
            <%= post.title || "-" %>
          </text>
        </a>
        <% }); %>
      </g>
    </svg>
  </g>
</svg>


================================================
FILE: templates/post.ejs
================================================
<svg xmlns="http://www.w3.org/2000/svg" width="450" height="130">
  <style>
    @keyframes fadeInAnimation {
      from {
        opacity: 0;
      }
      to {
        opacity: 1;
      }
    }

    svg {
      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
        sans-serif, Apple Color Emoji, Segoe UI Emoji;
      animation: fadeInAnimation 0.8s ease-in-out forwards;
    }
    text {
      fill: #8d96a0;
    }
    .background {
      fill: #00000000;
      width: calc(100% - 1px);
      height: calc(100% - 1px);
      stroke: #8b8b8b22;
      stroke-opacity: 1;
      rx: 4.5px;
      x: 0.5px;
      y: 0.5px;
    }
    .header {
      font-weight: 600;
      font-size: 14px;
    }
    .log-title {
      font-weight: 700;
      font-size: 14px;
    }
    .log-description {
      font-size: 12px;
      font-weight: 400;
    }
    .likes {
      font-size: 12px;
    }
    .tag__text {
      font-size: 12px;
      font-weight: 400;
      fill: #0ca678;
      text-anchor: middle;
    }
    .tag {
      fill: #00000000;
      stroke: #8b8b8b22;
      stroke-opacity: 1;
    }
  </style>
  <rect class="background" />
  <g transform="translate(25, 35)">
    <g transform="translate(0, 0)">
      <text x="0" y="0" class="header"><%= user.username %>.log</text>
      <svg
        width="30"
        x="390"
        y="-10"
        height="13"
        viewBox="0 0 30 13"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M11.25 0L7.5 2.26044L3.75 0L0 2.82555V6.78133L7.5 12.4324L15 6.78133V2.82555L11.25 0Z"
          fill="#8d96a0"
        />
      </svg>
      <text x="<%= likes > 99 ? 365 : likes > 9 ? 370 : 380 %>" class="likes">
        <%= likes %>
      </text>
    </g>
  </g>
  <g data-testid="main-card-body" transform="translate(0, 45)">
    <svg x="25" width="400" height="40" viewBox="0 0 400 40">
      <g transform="translate(0, 0)">
        <text x="2" y="15" class="log-title"><%= title %></text>
        <text x="2" y="35" class="log-description">
          <%= short_description %>
        </text>
      </g>
    </svg>
  </g>
  <g transform="translate(0, 40)">
    <% let prev = 25; %> <% tags.forEach((tag, index) => { const size =
    getTextWidth(tag, 12) + 20; const pos = prev; if (prev + size > 400) return;
    prev += size + 6; %>
    <svg x="<%= pos %>" width="<%= size %>" viewBox="0 0 <%= size %> 19">
      <g style="position: relative">
        <rect width="<%= size %>" height="19.5367" rx="9.76834" class="tag" />
        <text x="<%= size / 2 %>" y="13" class="tag__text"><%= tag %></text>
      </g>
    </svg>
    <% }); %>
  </g>
</svg>


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "./dist",
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true
  },
  "exclude": ["node_modules"],
  "include": ["./src/**/*.ts"]
}


================================================
FILE: vercel.json
================================================
{
  "redirects": [
    {
      "source": "/",
      "destination": "https://github.com/eungyeole/velog-readme-stats"
    }
  ],
  "rewrites": [
    {
      "source": "/api",
      "destination": "/api/serverless.ts"
    },
    {
      "source": "/api/(.*)",
      "destination": "/api/serverless.ts"
    }
  ]
}
Download .txt
gitextract_ok3hzzs_/

├── .github/
│   ├── CODEOWNERS
│   ├── FUNDING.yml
│   └── dependabot.yml
├── .gitignore
├── .nvmrc
├── .vscode/
│   └── settings.json
├── README.md
├── api/
│   └── serverless.ts
├── package.json
├── src/
│   ├── app.ts
│   ├── errors/
│   │   └── ApiError.ts
│   ├── index.ts
│   ├── router.ts
│   ├── routes/
│   │   ├── badge.ts
│   │   ├── ping.ts
│   │   ├── post-list.ts
│   │   ├── post.ts
│   │   └── redirect.ts
│   └── utils/
│       ├── get-recently-post.ts
│       ├── get-text-width.ts
│       └── velog-client/
│           ├── client.ts
│           ├── gql/
│           │   ├── posts.ts
│           │   └── read-post.ts
│           ├── index.ts
│           ├── types.ts
│           └── velog-client.ts
├── templates/
│   ├── badge.ejs
│   ├── post-list.ejs
│   └── post.ejs
├── tsconfig.json
└── vercel.json
Download .txt
SYMBOL INDEX (17 symbols across 5 files)

FILE: src/errors/ApiError.ts
  class ApiError (line 1) | class ApiError extends Error {
    method constructor (line 4) | constructor(message: string, statusCode: number) {
  class NotFoundError (line 11) | class NotFoundError extends ApiError {
    method constructor (line 12) | constructor(message = "Resource not found") {
  class InternalServerError (line 17) | class InternalServerError extends ApiError {
    method constructor (line 18) | constructor(message = "Internal Server Error") {

FILE: src/utils/get-recently-post.ts
  type GetRecentlyPostOptions (line 3) | interface GetRecentlyPostOptions {
  type RecentlyPost (line 8) | interface RecentlyPost {

FILE: src/utils/get-text-width.ts
  function getTextWidth (line 3) | function getTextWidth(text: string, font: number) {

FILE: src/utils/velog-client/types.ts
  type GetPostsParams (line 1) | interface GetPostsParams {
  type GetPostsResponse (line 8) | type GetPostsResponse = {
  type GetPostByUrlSlugParams (line 29) | interface GetPostByUrlSlugParams {
  type GetPostByUrlSlugResponse (line 34) | interface GetPostByUrlSlugResponse {

FILE: src/utils/velog-client/velog-client.ts
  class VelogClient (line 11) | class VelogClient {
    method constructor (line 12) | constructor() {}
    method getPosts (line 14) | public async getPosts(params: GetPostsParams) {
    method getPostByUrlSlug (line 20) | public async getPostByUrlSlug(params: GetPostByUrlSlugParams) {
Condensed preview — 31 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (25K chars).
[
  {
    "path": ".github/CODEOWNERS",
    "chars": 12,
    "preview": "* @eungyeole"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 18,
    "preview": "github: eungyeole\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 525,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".gitignore",
    "chars": 21,
    "preview": ".vercel\nnode_modules\n"
  },
  {
    "path": ".nvmrc",
    "chars": 8,
    "preview": "v18.12.1"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 133,
    "preview": "{\n  \"editor.tabSize\": 2,\n  \"editor.formatOnSave\": true,\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"exp"
  },
  {
    "path": "README.md",
    "chars": 1981,
    "preview": "# velog-readme-stats란?\n\n> markdown에서 나의 velog의 정보를 가져올수 있는 도구입니다.\n\n# Velog 뱃지\n\n> ?name= 의 값을 변경하세요\n\n```\n[![Velog's GitHu"
  },
  {
    "path": "api/serverless.ts",
    "chars": 266,
    "preview": "import app from \"../src/app\";\n\nimport { VercelRequest, VercelResponse } from \"@vercel/node\";\n\nconst serverless = async ("
  },
  {
    "path": "package.json",
    "chars": 722,
    "preview": "{\n  \"name\": \"velog-readme-stats\",\n  \"version\": \"1.0.0\",\n  \"description\": \"당신의 Velog 상태를 깃허브에서 확인하세요.\",\n  \"main\": \"./api/"
  },
  {
    "path": "src/app.ts",
    "chars": 403,
    "preview": "import Fastify from \"fastify\";\nimport { router } from \"./router\";\nimport fastifyView from \"@fastify/view\";\nimport ejs fr"
  },
  {
    "path": "src/errors/ApiError.ts",
    "chars": 488,
    "preview": "export class ApiError extends Error {\n  statusCode: number;\n\n  constructor(message: string, statusCode: number) {\n    su"
  },
  {
    "path": "src/index.ts",
    "chars": 181,
    "preview": "import app from \"./app\";\n\napp.listen(\n  {\n    port: 3000,\n  },\n  (err, address) => {\n    if (err) {\n      throw err;\n   "
  },
  {
    "path": "src/router.ts",
    "chars": 455,
    "preview": "import { FastifyInstance } from \"fastify\";\nimport { ping } from \"./routes/ping\";\nimport { redirect } from \"./routes/redi"
  },
  {
    "path": "src/routes/badge.ts",
    "chars": 601,
    "preview": "import { FastifyInstance, FastifyRequest } from \"fastify\";\nimport { NotFoundError } from \"../errors/ApiError\";\nimport { "
  },
  {
    "path": "src/routes/ping.ts",
    "chars": 176,
    "preview": "import { FastifyInstance } from \"fastify\";\n\nexport const ping = async (fastify: FastifyInstance) => {\n  fastify.get(\"/pi"
  },
  {
    "path": "src/routes/post-list.ts",
    "chars": 836,
    "preview": "import { FastifyInstance, FastifyRequest } from \"fastify\";\nimport { velogClient } from \"../utils/velog-client\";\n\nexport "
  },
  {
    "path": "src/routes/post.ts",
    "chars": 841,
    "preview": "import { FastifyInstance, FastifyRequest } from \"fastify\";\nimport { getTextWidth } from \"../utils/get-text-width\";\nimpor"
  },
  {
    "path": "src/routes/redirect.ts",
    "chars": 877,
    "preview": "import { FastifyInstance, FastifyRequest } from \"fastify\";\nimport { NotFoundError } from \"../errors/ApiError\";\nimport { "
  },
  {
    "path": "src/utils/get-recently-post.ts",
    "chars": 711,
    "preview": "import { velogClient } from \"./velog-client\";\n\ninterface GetRecentlyPostOptions {\n  slug?: string;\n  tag?: string;\n}\n\nin"
  },
  {
    "path": "src/utils/get-text-width.ts",
    "chars": 367,
    "preview": "import canvas from \"@napi-rs/canvas\";\n\nexport function getTextWidth(text: string, font: number) {\n  const ctx = canvas.c"
  },
  {
    "path": "src/utils/velog-client/client.ts",
    "chars": 119,
    "preview": "import { GraphQLClient } from 'graphql-request'\n\nexport const client = new GraphQLClient('https://v3.velog.io/graphql')"
  },
  {
    "path": "src/utils/velog-client/gql/posts.ts",
    "chars": 395,
    "preview": "import { gql } from \"graphql-request\";\n\nexport const postsGql = gql`\n  query velogPosts($input: GetPostsInput!) {\n    po"
  },
  {
    "path": "src/utils/velog-client/gql/read-post.ts",
    "chars": 1463,
    "preview": "import { gql } from \"graphql-request\";\n\nexport const readPostGql = gql`\n  query readPost($input: ReadPostInput!) {\n    p"
  },
  {
    "path": "src/utils/velog-client/index.ts",
    "chars": 57,
    "preview": "export * from \"./velog-client\";\nexport * from \"./types\";\n"
  },
  {
    "path": "src/utils/velog-client/types.ts",
    "chars": 2580,
    "preview": "export interface GetPostsParams {\n  cursor?: number;\n  limit?: number;\n  username: string;\n  tag?: string;\n}\n\nexport typ"
  },
  {
    "path": "src/utils/velog-client/velog-client.ts",
    "chars": 649,
    "preview": "import { client } from \"./client\";\nimport { postsGql } from \"./gql/posts\";\nimport {\n  GetPostByUrlSlugParams,\n  GetPostB"
  },
  {
    "path": "templates/badge.ejs",
    "chars": 1463,
    "preview": "<% const size = getTextWidth(name, 11); const rectWidth = size + 28; %>\n<svg\n  width=\"<%= rectWidth %>\"\n  height=\"20\"\n  "
  },
  {
    "path": "templates/post-list.ejs",
    "chars": 1921,
    "preview": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"450\" height=\"160\">\n  <style>\n    @keyframes fadeInAnimation {\n      from "
  },
  {
    "path": "templates/post.ejs",
    "chars": 2668,
    "preview": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"450\" height=\"130\">\n  <style>\n    @keyframes fadeInAnimation {\n      from "
  },
  {
    "path": "tsconfig.json",
    "chars": 392,
    "preview": "{\n  \"compilerOptions\": {\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"noImplicitAny\": true,\n    \"strictNullChecks"
  },
  {
    "path": "vercel.json",
    "chars": 311,
    "preview": "{\n  \"redirects\": [\n    {\n      \"source\": \"/\",\n      \"destination\": \"https://github.com/eungyeole/velog-readme-stats\"\n   "
  }
]

About this extraction

This page contains the full source code of the eungyeole/velog-readme-stats GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 31 files (21.1 KB), approximately 7.2k tokens, and a symbol index with 17 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!