├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── rss-parser.d.ts ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── badge │ ├── badge.controller.ts │ ├── badge.module.ts │ ├── badge.service.ts │ ├── svg.service.ts │ └── velog-api.service.ts ├── constants │ └── graphql-queries.ts ├── interfaces │ ├── feed-item.interface.ts │ └── velog-api.interface.ts ├── main.ts └── utils │ ├── rss-parser.ts │ └── svg-generator.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── vercel.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | .vercel 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Velog GitHub Badge 🏷️

4 | 5 |

6 | Velog 활동을 GitHub 프로필에 표시해 보자 7 |

8 | 9 | 15 | 16 |

17 | 주요 기능 • 18 | 사용법 • 19 | 커스터마이징 20 |

21 | 22 | | Light Mode | Dark Mode | 23 | |-------------|-----------| 24 | | ![Light Mode](https://velog-github-badge.vercel.app/badge/velog?username=velog&theme=light&posts=3) | ![Dark Mode](https://velog-github-badge.vercel.app/badge/velog?username=velog&theme=dark&posts=2) | 25 | 26 |
27 | 28 | ## 주요 기능 29 | 30 | - Velog 최신 포스트 표시 31 | - 인기 태그 노출 32 | - 총 좋아요 수 집계 33 | - 다크/라이트 테마 지원 34 | - 손쉬운 커스터마이징 35 | - 실시간 업데이트 36 | 37 | 38 |
39 | 40 | ## 사용법 41 | GitHub 프로필 README.md 파일에 다음 코드를 추가하세요 42 | ``` 43 | ![Velog GitHub stats](https://velog-github-badge.vercel.app/badge/{YourUserName}) 44 | ``` 45 | **YourUsername을 반드시 여러분의 Velog 사용자 이름으로 변경해주세요!** 46 | 47 |
48 | 49 | ## 커스터마이징 50 | 배지의 모양을 다음 옵션으로 설정할 수 있습니다 51 | 52 | - theme: 배지의 테마 설정 (light 또는 dark, 기본값: light) 53 | - posts: 표시할 최근 게시물의 수 설정 (기본값: 1, 최대: 3) 54 | 55 | ``` 56 | ![Velog GitHub stats](https://velog-github-badge.vercel.app/badge/{YourUserName}?theme=light&posts=3) 57 | ``` 58 | 59 |
60 | 61 | --- 62 | 63 |
64 | 65 | 66 |

67 | 68 | Give this repo a star! 69 | 70 |

71 | 72 |

73 | 이 프로젝트가 마음에 드셨다면 ⭐️을 눌러주세요!
74 | 여러분의 ⭐️ 하나하나가 프로젝트의 발전에 큰 힘이 됩니다!
75 |

76 | 77 | 78 |

79 | Created with ❤️ by Antraxmin 80 |

81 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "velog-github-badge", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "vercel-build": "npm run build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^10.0.0", 25 | "@nestjs/core": "^10.0.0", 26 | "@nestjs/platform-express": "^10.0.0", 27 | "axios": "^1.7.2", 28 | "reflect-metadata": "^0.1.13", 29 | "rss-parser": "^3.13.0", 30 | "rxjs": "^7.8.1" 31 | }, 32 | "devDependencies": { 33 | "@nestjs/cli": "^10.0.0", 34 | "@nestjs/schematics": "^10.0.0", 35 | "@nestjs/testing": "^10.0.0", 36 | "@types/express": "^4.17.17", 37 | "@types/jest": "^29.5.12", 38 | "@types/node": "^20.3.1", 39 | "@types/supertest": "^2.0.12", 40 | "@typescript-eslint/eslint-plugin": "^6.0.0", 41 | "@typescript-eslint/parser": "^6.0.0", 42 | "eslint": "^8.42.0", 43 | "eslint-config-prettier": "^9.0.0", 44 | "eslint-plugin-prettier": "^5.0.0", 45 | "jest": "^29.7.0", 46 | "prettier": "^3.0.0", 47 | "source-map-support": "^0.5.21", 48 | "supertest": "^6.3.3", 49 | "ts-jest": "^29.2.3", 50 | "ts-loader": "^9.4.3", 51 | "ts-node": "^10.9.1", 52 | "tsconfig-paths": "^4.2.0", 53 | "typescript": "^5.1.3" 54 | }, 55 | "jest": { 56 | "moduleFileExtensions": [ 57 | "js", 58 | "json", 59 | "ts" 60 | ], 61 | "rootDir": "src", 62 | "testRegex": ".*\\.spec\\.ts$", 63 | "transform": { 64 | "^.+\\.(t|j)s$": "ts-jest" 65 | }, 66 | "collectCoverageFrom": [ 67 | "**/*.(t|j)s" 68 | ], 69 | "coverageDirectory": "../coverage", 70 | "testEnvironment": "node" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rss-parser.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'rss-parser' { 2 | class Parser { 3 | constructor(options?: any); 4 | parseURL(url: string): Promise; 5 | } 6 | export = Parser; 7 | } -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BadgeModule } from './badge/badge.module'; 3 | 4 | @Module({ 5 | imports: [BadgeModule], 6 | }) 7 | export class AppModule {} -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/badge/badge.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Res, Query } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { BadgeService } from './badge.service'; 4 | 5 | @Controller('badge') 6 | export class BadgeController { 7 | constructor(private readonly badgeService: BadgeService) {} 8 | 9 | @Get(':username') 10 | async getBadge( 11 | @Param('username') username: string, 12 | @Query('theme') theme: string = 'light', 13 | @Query('posts') posts: number = 5, 14 | @Res() res: Response, 15 | ) { 16 | const svg = await this.badgeService.generateBadge(username, theme, posts); 17 | res.setHeader('Content-Type', 'image/svg+xml'); 18 | res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate'); 19 | res.send(svg); 20 | } 21 | } -------------------------------------------------------------------------------- /src/badge/badge.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BadgeController } from './badge.controller'; 3 | import { BadgeService } from './badge.service'; 4 | import { RSSParserService } from '../utils/rss-parser'; 5 | import { VelogAPIService } from './velog-api.service'; 6 | import { SVGService } from './svg.service'; 7 | 8 | @Module({ 9 | controllers: [BadgeController], 10 | providers: [BadgeService, 11 | RSSParserService, 12 | VelogAPIService, 13 | SVGService], 14 | }) 15 | export class BadgeModule {} -------------------------------------------------------------------------------- /src/badge/badge.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { SVGService } from './svg.service'; 3 | import { VelogAPIService } from './velog-api.service'; 4 | import { RSSParserService } from '../utils/rss-parser'; 5 | import { Feed, FeedItem } from '../interfaces/feed-item.interface'; 6 | 7 | @Injectable() 8 | export class BadgeService { 9 | private readonly logger = new Logger(BadgeService.name); 10 | 11 | constructor( 12 | private readonly rssParserService: RSSParserService, 13 | private readonly velogAPIService: VelogAPIService, 14 | private readonly svgService: SVGService, 15 | ) {} 16 | 17 | async generateBadge(username: string, theme: string, posts: number): Promise { 18 | try { 19 | const [feed, totalLikes, tags] = await Promise.all([ 20 | this.getFeed(username), 21 | this.velogAPIService.getTotalLikes(username), 22 | this.velogAPIService.getPopularTags(username), 23 | ]); 24 | 25 | const recentPosts = this.getRecentPosts(feed.items, posts); 26 | console.log(recentPosts) 27 | 28 | return this.svgService.generateSVG(username, recentPosts, theme, totalLikes, tags); 29 | } catch (error) { 30 | this.logger.error(`Error generating badge for ${username}: ${error.message}`); 31 | throw error; 32 | } 33 | } 34 | 35 | private async getFeed(username: string): Promise { 36 | try { 37 | return await this.rssParserService.parseRSS(`https://v2.velog.io/rss/${username}`); 38 | } catch (error) { 39 | this.logger.error(`Error fetching RSS feed for ${username}: ${error.message}`); 40 | return { items: [] }; 41 | } 42 | } 43 | 44 | private getRecentPosts(items: FeedItem[], count: number): FeedItem[] { 45 | return items.slice(0, count); 46 | } 47 | } -------------------------------------------------------------------------------- /src/badge/svg.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { generateSVG as generateSVGUtil } from '../utils/svg-generator'; 3 | import { FeedItem } from '../interfaces/feed-item.interface'; 4 | 5 | @Injectable() 6 | export class SVGService { 7 | generateSVG(username: string, items: FeedItem[], theme: string, totalLikes: number, tags: string[]): string { 8 | return generateSVGUtil(username, items, theme, totalLikes, tags); 9 | } 10 | } -------------------------------------------------------------------------------- /src/badge/velog-api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import axios from 'axios'; 3 | import { GET_USER_POSTS, GET_POPULAR_TAGS } from '../constants/graphql-queries'; 4 | import { Post, Tag } from '../interfaces/velog-api.interface'; 5 | 6 | interface VelogPost { 7 | id: string; 8 | title: string; 9 | likes: number; 10 | } 11 | 12 | @Injectable() 13 | export class VelogAPIService { 14 | private readonly logger = new Logger(VelogAPIService.name); 15 | private readonly VELOG_API_ENDPOINT = 'https://v2.velog.io/graphql'; 16 | 17 | async getPostsWithLikes(username: string): Promise { 18 | const query = ` 19 | query { 20 | posts(username: "${username}") { 21 | id 22 | title 23 | likes 24 | } 25 | } 26 | `; 27 | 28 | try { 29 | const response = await axios.post(this.VELOG_API_ENDPOINT, { query }); 30 | return response.data.data.posts; 31 | } catch (error) { 32 | this.logger.error(`Error fetching posts for ${username}: ${error.message}`); 33 | throw error; 34 | } 35 | } 36 | 37 | async getTotalLikes(username: string): Promise { 38 | try { 39 | const posts = await this.getPostsWithLikes(username); 40 | return posts.reduce((sum, post) => sum + post.likes, 0); 41 | } catch (error) { 42 | this.logger.error(`Error calculating total likes for ${username}: ${error.message}`); 43 | throw error; 44 | } 45 | } 46 | 47 | async getPopularTags(username: string): Promise { 48 | try { 49 | const tags = await this.fetchUserTags(username); 50 | return this.getTopTags(tags); 51 | } catch (error) { 52 | this.logger.error(`Error fetching popular tags for ${username}: ${error.message}`); 53 | return []; 54 | } 55 | } 56 | 57 | private async fetchAllPosts(username: string): Promise { 58 | let allPosts: Post[] = []; 59 | let hasNextPage = true; 60 | let cursor = null; 61 | 62 | while (hasNextPage) { 63 | const response = await this.queryGraphQL(GET_USER_POSTS, { username, cursor }); 64 | console.log('Full GraphQL Response:', JSON.stringify(response.data, null, 2)); 65 | 66 | const { posts, pageInfo } = response.data.data.userPosts; 67 | console.log('Extracted Posts:', JSON.stringify(posts, null, 2)); 68 | 69 | allPosts = [...allPosts, ...posts]; 70 | hasNextPage = pageInfo.hasNextPage; 71 | cursor = pageInfo.endCursor; 72 | } 73 | 74 | return allPosts; 75 | } 76 | 77 | private calculateTotalLikes(posts: Post[]): number { 78 | this.logger.debug(`Calculating total likes for ${posts.length} posts`); 79 | 80 | return posts.reduce((sum, post, index) => { 81 | if (typeof post.likes !== 'number') { 82 | this.logger.warn(`Post at index ${index} has invalid 'likes' value: ${post.likes}`); 83 | return sum; 84 | } 85 | 86 | console.log(`Post ${index + 1}: ID=${post.id}, Likes=${post.likes}`); 87 | return sum + post.likes; 88 | }, 0); 89 | } 90 | 91 | private async fetchUserTags(username: string): Promise { 92 | const response = await this.queryGraphQL(GET_POPULAR_TAGS, { username }); 93 | return response.data.data.userTags.tags; 94 | } 95 | 96 | private getTopTags(tags: Tag[]): string[] { 97 | return tags 98 | .sort((a, b) => b.posts_count - a.posts_count) 99 | .slice(0, 3) 100 | .map(tag => tag.name); 101 | } 102 | 103 | private async queryGraphQL(query: string, variables: any): Promise { 104 | try { 105 | return await axios.post(this.VELOG_API_ENDPOINT, { query, variables }); 106 | } catch (error) { 107 | this.logger.error(`GraphQL query failed: ${error.message}`); 108 | throw error; 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/constants/graphql-queries.ts: -------------------------------------------------------------------------------- 1 | export const GET_USER_POSTS = ` 2 | query GetUserPosts($username: String!, $cursor: ID) { 3 | userPosts(username: $username, cursor: $cursor) { 4 | posts { 5 | id 6 | likes 7 | } 8 | pageInfo { 9 | endCursor 10 | hasNextPage 11 | } 12 | } 13 | } 14 | `; 15 | 16 | export const GET_POPULAR_TAGS = ` 17 | query GetPopularTags($username: String!) { 18 | userTags(username: $username) { 19 | tags { 20 | name 21 | posts_count 22 | } 23 | } 24 | } 25 | `; -------------------------------------------------------------------------------- /src/interfaces/feed-item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Feed { 2 | items: FeedItem[]; 3 | } 4 | 5 | export interface FeedItem { 6 | title: string; 7 | link: string; 8 | pubDate: string; 9 | likes: number; 10 | } -------------------------------------------------------------------------------- /src/interfaces/velog-api.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Post { 2 | id: string; 3 | likes: number; 4 | } 5 | 6 | export interface Tag { 7 | name: string; 8 | posts_count: number; 9 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | app.enableCors(); 7 | await app.listen(3000); 8 | } 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /src/utils/rss-parser.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import Parser from 'rss-parser'; 3 | import { Feed, FeedItem } from '../interfaces/feed-item.interface'; 4 | 5 | @Injectable() 6 | export class RSSParserService { 7 | private readonly parser = new Parser(); 8 | private readonly logger = new Logger(RSSParserService.name); 9 | 10 | async parseRSS(url: string): Promise { 11 | try { 12 | const feed = await this.parser.parseURL(url); 13 | return { 14 | items: feed.items.map((item: any): FeedItem => ({ 15 | title: item.title || '', 16 | link: item.link || '', 17 | pubDate: item.pubDate || '', 18 | likes: item.likes || '', 19 | })), 20 | }; 21 | } catch (error) { 22 | this.logger.error(`Failed to parse RSS feed from ${url}: ${error.message}`); 23 | throw error; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/utils/svg-generator.ts: -------------------------------------------------------------------------------- 1 | import { FeedItem } from '../interfaces/feed-item.interface'; 2 | 3 | const WIDTH = 480; 4 | const HEIGHT = 240; 5 | const VELOG_LOGO_INLINE = ` 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | `; 15 | 16 | 17 | function getThemeColors(darkMode: boolean) { 18 | return { 19 | backgroundColor: darkMode ? '#0F172A' : '#F8FAFC', 20 | textColor: darkMode ? '#E2E8F0' : '#334155', 21 | accentColor: '#1EC997', 22 | secondaryColor: darkMode ? '#64748B' : '#94A3B8', 23 | cardColor: darkMode ? '#1E293B' : '#FFFFFF', 24 | }; 25 | } 26 | 27 | function createBackground(colors: ReturnType, darkMode: boolean) { 28 | return ` 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | `; 37 | } 38 | 39 | function createHeader(username: string, userProfileUrl: string, totalLikes: number, colors: ReturnType) { 40 | function getHeartPosition(totalLikes: number, width: number) { 41 | const textWidth = totalLikes.toLocaleString().length * 10; 42 | return width - textWidth - 40; 43 | } 44 | 45 | const heartX = getHeartPosition(totalLikes, WIDTH); 46 | 47 | return ` 48 | 49 | 50 | ${VELOG_LOGO_INLINE} 51 | 52 | 53 | ${username} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ${totalLikes.toLocaleString()} 63 | 64 | `; 65 | } 66 | 67 | function createLatestPosts(items: FeedItem[], colors: ReturnType) { 68 | let content = ` 69 | 70 | Latest Posts 71 | 72 | `; 73 | 74 | items.slice(0, 3).forEach((item, index) => { 75 | const yPos = 100 + index * 30; 76 | const title = item.title.length > 40 ? item.title.substring(0, 37) + '...' : item.title; 77 | const date = new Date(item.pubDate).toLocaleDateString('ko-KR', { year: 'numeric', month: 'short', day: 'numeric' }); 78 | content += ` 79 | 80 | 81 | 82 | ${title} 83 | 84 | 85 | ${date} 86 | 87 | 88 | `; 89 | }); 90 | 91 | return content; 92 | } 93 | 94 | function createTags(tags: string[], colors: ReturnType) { 95 | let content = `Top Tags`; 96 | 97 | const tagWidth = 90; 98 | const tagSpacing = 10; 99 | tags.slice(0, 5).forEach((tag, index) => { 100 | const xPos = 20 + index * (tagWidth + tagSpacing); 101 | const tagUrl = `https://velog.io/search?q=${encodeURIComponent(tag)}`; 102 | content += ` 103 | 104 | 105 | 106 | ${tag} 107 | 108 | 109 | `; 110 | }); 111 | 112 | return content; 113 | } 114 | 115 | export function generateSVG(username: string, items: FeedItem[], theme: string, totalLikes: number, tags: string[]): string { 116 | const darkMode = theme === 'dark'; 117 | const colors = getThemeColors(darkMode); 118 | const userProfileUrl = `https://velog.io/@${username}`; 119 | 120 | let svgContent = ` 121 | 122 | ${createBackground(colors, darkMode)} 123 | ${createHeader(username, userProfileUrl, totalLikes, colors)} 124 | ${createLatestPosts(items, colors)} 125 | ${createTags(tags, colors)} 126 | 127 | `; 128 | 129 | return svgContent; 130 | } -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "ES2021", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "src/main.ts", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "src/main.ts", 13 | "methods": [ 14 | "GET", 15 | "POST", 16 | "PUT", 17 | "DELETE" 18 | ] 19 | } 20 | ] 21 | } --------------------------------------------------------------------------------