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= 의 값을 변경하세요
```
[](https://velog.io/@eungyeole)
```
[](https://velog.io/@eungyeole)
# 최신 글 가져오기
> ?name= 의 값을 변경하세요
```
[](https://github.com/eungyeole/velog-readme-stats)
```
[](https://github.com/eungyeole/velog-readme-stats)
## 특정태그를 가진 최신글 가져오기
> Option : `&tag`
```
[](https://github.com/eungyeole/velog-readme-stats)
```
[](https://github.com/eungyeole/velog-readme-stats)
## 특정 제목을 가진 글
> Option : `&slug`
```
[](https://github.com/eungyeole/velog-readme-stats)
```
[](https://github.com/eungyeole/velog-readme-stats)
## 상태카드와 최신글 연결하기
> Option : name, tag
> 카드클릭시 최신글로 리다이렉트 합니다.
```
https://velog-readme-stats.vercel.app/api/redirect?name=eungyeole&tag=github
```
[](https://velog-readme-stats.vercel.app/api/redirect?name=eungyeole&tag=github)
# 최신 글 목록 가져오기 (Beta)
> ?name= 의 값을 변경하세요
```
[](https://velog.io/@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"
}
]
}
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
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.