├── .eslintrc.json
├── .npmrc
├── .env.production
├── public
├── logo.png
├── fonts
│ └── SimSun.ttf
├── uploads
│ └── .DS_Store
├── vercel.svg
└── next.svg
├── src
├── app
│ ├── favicon.ico
│ ├── docs
│ │ ├── page.tsx
│ │ └── editProps.tsx
│ ├── page.tsx
│ ├── layout.tsx
│ └── globals.css
├── lib
│ ├── utils.ts
│ └── useMounted.tsx
├── mdx-components.tsx
├── pages
│ ├── api
│ │ ├── hello.js
│ │ ├── getImage.js
│ │ ├── images
│ │ │ └── [filename].ts
│ │ ├── generatePoster.ts
│ │ └── generatePosterImage.ts
│ └── poster
│ │ ├── layout.tsx
│ │ └── index.tsx
├── components
│ ├── Section.tsx
│ ├── Footer.tsx
│ ├── ui
│ │ ├── label.tsx
│ │ ├── textarea.tsx
│ │ ├── switch.tsx
│ │ ├── radio-group.tsx
│ │ ├── scroll-area.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── select.tsx
│ ├── PosterView.tsx
│ ├── Editor.tsx
│ └── Header.tsx
├── styles
│ └── header.css
└── markdown
│ ├── docs.mdx
│ └── home.mdx
├── .env
├── example
├── output
│ ├── news_poster.png
│ ├── poster-1754641688124.png
│ ├── poster-1754647673038.png
│ ├── poster-1754647681361.png
│ ├── poster-1754647689189.png
│ └── poster-1754647978853.png
├── news_poster.md
├── api_buffer_2_image.js
├── api_request.json
└── api_buffer_2_image.py
├── postcss.config.mjs
├── .gitignore
├── .dockerignore
├── next-env.d.ts
├── Dockerfile
├── .whitesource
├── .env.shadow
├── railway.json
├── components.json
├── docker-compose.prod.yml
├── render.yaml
├── Dockerfile.base
├── docker-compose.yml
├── patches
└── markdown-to-poster+0.0.9.patch
├── tsconfig.json
├── next.config.mjs
├── fly.toml
├── LICENSE
├── package.json
├── dev.md
├── Dockerfile.backup
├── tailwind.config.ts
├── .github
└── workflows
│ ├── docker-image-no-cache.yml
│ └── docker-image.yml
├── DOCKER_SETUP.md
├── DEPLOYMENT.md
├── TROUBLESHOOTING.md
├── README_EN.md
└── README.md
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # 设置阿里云镜像源
2 | registry=https://registry.npmmirror.com/
3 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | CHROME_PATH=/opt/bin/chromium
2 | NEXT_PUBLIC_BASE_URL=http://localhost:3000
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/public/logo.png
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/public/fonts/SimSun.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/public/fonts/SimSun.ttf
--------------------------------------------------------------------------------
/public/uploads/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/public/uploads/.DS_Store
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_BASE_URL=http://localhost:3000
2 | CHROME_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
--------------------------------------------------------------------------------
/example/output/news_poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/example/output/news_poster.png
--------------------------------------------------------------------------------
/example/output/poster-1754641688124.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/example/output/poster-1754641688124.png
--------------------------------------------------------------------------------
/example/output/poster-1754647673038.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/example/output/poster-1754647673038.png
--------------------------------------------------------------------------------
/example/output/poster-1754647681361.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/example/output/poster-1754647681361.png
--------------------------------------------------------------------------------
/example/output/poster-1754647689189.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/example/output/poster-1754647689189.png
--------------------------------------------------------------------------------
/example/output/poster-1754647978853.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxingheng/markdown-to-image-serve/HEAD/example/output/poster-1754647978853.png
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import type { MDXComponents } from 'mdx/types'
2 |
3 | export function useMDXComponents(components: MDXComponents): MDXComponents {
4 | return {
5 | ...components,
6 | }
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # *.mdx
2 | # *.mdx.js
3 | # *.md.js
4 | node_modules
5 | .next
6 | # .env
7 | .env.development.local
8 | .env.test.local
9 | .env.production.local
10 | public/uploads/posters/*
11 | .env.local
12 | .vercel
13 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
3 | npm-debug.log
4 | yarn-debug.log
5 | yarn-error.log
6 | .git
7 | .gitignore
8 | .env
9 | .DS_Store
10 | README.md
11 | README_EN.md
12 | example
13 | dist
14 | build
15 | Dockerfile*
16 | .env*
--------------------------------------------------------------------------------
/src/lib/useMounted.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState, useEffect } from 'react'
4 |
5 | export function useMounted() {
6 | const [mounted, setMounted] = useState(false)
7 |
8 | useEffect(() => {
9 | setMounted(true)
10 | }, [])
11 |
12 | return mounted
13 | }
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # 文件:Dockerfile
2 | FROM wxingheng/node-chrome-base:latest
3 |
4 | # 设置工作目录
5 | WORKDIR /app
6 |
7 | # 复制依赖文件并安装
8 | COPY package*.json ./
9 | COPY .env* ./
10 |
11 | RUN npm install
12 |
13 | # 复制应用代码并构建
14 | COPY . .
15 |
16 | RUN npm run build
17 |
18 | EXPOSE 3000
19 |
20 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/.whitesource:
--------------------------------------------------------------------------------
1 | {
2 | "scanSettings": {
3 | "baseBranches": []
4 | },
5 | "checkRunSettings": {
6 | "vulnerableCheckRunConclusionLevel": "failure",
7 | "displayMode": "diff",
8 | "useMendCheckNames": true
9 | },
10 | "issueSettings": {
11 | "minSeverityLevel": "LOW",
12 | "issueType": "DEPENDENCY"
13 | }
14 | }
--------------------------------------------------------------------------------
/.env.shadow:
--------------------------------------------------------------------------------
1 | ###
2 | # @Author: wxingheng
3 | # @Date: 2024-11-28 10:57:38
4 | # @LastEditTime: 2024-11-28 10:57:39
5 | # @LastEditors: wxingheng
6 | # @Description:
7 | # @FilePath: /markdown-to-image-serve/.env copy.local
8 | ###
9 | NEXT_PUBLIC_BASE_URL=http://localhost:3000
10 | CHROME_PATH=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome
--------------------------------------------------------------------------------
/src/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: wxingheng
3 | * @Date: 2024-11-27 16:52:33
4 | * @LastEditTime: 2024-11-27 16:52:35
5 | * @LastEditors: wxingheng
6 | * @Description:
7 | * @FilePath: /example/src/pages/api/hello.js
8 | */
9 | export default function handler(req, res) {
10 | res.status(200).json({ message: 'Hello, Next.js API!' });
11 | }
--------------------------------------------------------------------------------
/src/pages/api/getImage.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: wxingheng
3 | * @Date: 2024-11-27 16:55:05
4 | * @LastEditTime: 2024-11-27 16:55:06
5 | * @LastEditors: wxingheng
6 | * @Description:
7 | * @FilePath: /example/src/pages/api/getImage.js
8 | */
9 |
10 | export default function handler(req, res) {
11 | res.status(200).json({ message: "Hello, Next.js API! getImage" });
12 | }
13 |
--------------------------------------------------------------------------------
/railway.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://railway.app/railway.schema.json",
3 | "build": {
4 | "builder": "DOCKERFILE",
5 | "dockerfilePath": "Dockerfile"
6 | },
7 | "deploy": {
8 | "startCommand": "yarn start",
9 | "healthcheckPath": "/api/hello",
10 | "healthcheckTimeout": 300,
11 | "restartPolicyType": "ON_FAILURE",
12 | "restartPolicyMaxRetries": 10
13 | }
14 | }
--------------------------------------------------------------------------------
/src/components/Section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | /**
3 | *
4 | * @param param0
5 | * @returns
6 | */
7 | export default function Section({ children, className }: { children?: React.ReactNode | string; className?: string }) {
8 | return (
9 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/header.css:
--------------------------------------------------------------------------------
1 | .friend-link-btn {
2 | display: flex;
3 | align-items: center;
4 | font-size: 16px;
5 | font-weight: 500;
6 | color: #1890ff;
7 | padding: 4px 12px;
8 | border-radius: 4px;
9 | transition: all 0.3s;
10 | }
11 |
12 | .friend-link-btn:hover {
13 | color: #40a9ff;
14 | background: rgba(24, 144, 255, 0.1);
15 | }
16 |
17 | .friend-link-btn span {
18 | margin-left: 4px;
19 | }
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/src/pages/poster/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Inter } from "next/font/google";
2 | const inter = Inter({ subsets: ["latin"] });
3 | import 'markdown-to-poster/dist/style.css'
4 |
5 |
6 | export default function PosterLayout({
7 | children,
8 | }: Readonly<{
9 | children: React.ReactNode;
10 | }>) {
11 | return (
12 |
13 |
14 | {children}
15 |
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/src/app/docs/page.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic'
2 | import MdDocs from '@/markdown/docs.mdx'
3 | import Section from '@/components/Section'
4 |
5 | const EditProps = dynamic(() => import('./editProps'), {
6 | ssr: false,
7 | })
8 |
9 | export default function Docs() {
10 | return (
11 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | version: '0.1'
2 |
3 | services:
4 | app:
5 | image: wxingheng/markdown-to-image-serve:0.0.1
6 | # 指定平台 amd64 架构 (Mac Apple Silicon 平台)
7 | platform: linux/amd64
8 | ports:
9 | - "3000:3000"
10 | environment:
11 | - NODE_ENV=production
12 | - NEXT_PUBLIC_BASE_URL=http://localhost:3000
13 | - CHROME_PATH=/usr/bin/google-chrome-unstable
14 | volumes:
15 | # 只挂载必要的目录,不挂载源代码
16 | - ./public/uploads/posters:/tmp/uploads/posters
--------------------------------------------------------------------------------
/render.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | - type: web
3 | name: markdown-to-image-serve
4 | env: docker
5 | region: oregon
6 | plan: free
7 | dockerfilePath: ./Dockerfile
8 | dockerCommand: yarn start
9 | envVars:
10 | - key: NODE_ENV
11 | value: production
12 | - key: NEXT_PUBLIC_BASE_URL
13 | value: https://markdown-to-image-serve.onrender.com
14 | - key: CHROME_PATH
15 | value: /usr/bin/google-chrome-unstable
16 | healthCheckPath: /api/hello
17 | autoDeploy: true
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import MdHome from '@/markdown/home.mdx'
3 | import Section from '@/components/Section'
4 | import dynamic from 'next/dynamic'
5 |
6 | const Editor = dynamic(() => import('@/components/Editor'), {
7 | ssr: false,
8 | })
9 | export default function Home() {
10 | return (
11 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/api/images/[filename].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | export default function handler(req: NextApiRequest, res: NextApiResponse) {
6 | const { filename } = req.query;
7 | const filePath = path.join('/tmp', 'uploads', 'posters', filename as string);
8 |
9 | try {
10 | const imageBuffer = fs.readFileSync(filePath);
11 | res.setHeader('Content-Type', 'image/png');
12 | res.send(imageBuffer);
13 | } catch (error) {
14 | res.status(404).json({ error: '图片未找到' });
15 | }
16 | }
--------------------------------------------------------------------------------
/src/pages/poster/index.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: wxingheng
3 | * @Date: 2024-11-27 17:50:50
4 | * @LastEditTime: 2025-07-09 15:48:21
5 | * @LastEditors: wxingheng
6 | * @Description:
7 | * @FilePath: /markdown-to-image-serve/src/pages/poster/index.tsx
8 | */
9 | "use client";
10 | import dynamic from "next/dynamic";
11 | import "markdown-to-poster/dist/style.css";
12 |
13 | // .bg-spring-gradient-wave 需要设置这个css 的样式
14 |
15 | const PosterView = dynamic(() => import("@/components/PosterView"), {
16 | ssr: false,
17 | });
18 | export default function Home() {
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/Dockerfile.base:
--------------------------------------------------------------------------------
1 | FROM node:20-slim
2 |
3 | # 设置国内 npm 镜像源
4 | RUN npm config set registry https://registry.npmmirror.com
5 |
6 | # 安装 Chromium 和中文字体(适用于 Puppeteer / Playwright)
7 | RUN apt-get update \
8 | && apt-get install -y \
9 | chromium \
10 | fonts-ipafont-gothic \
11 | fonts-wqy-zenhei \
12 | fonts-thai-tlwg \
13 | fonts-kacst \
14 | fonts-freefont-ttf \
15 | --no-install-recommends \
16 | && rm -rf /var/lib/apt/lists/*
17 |
18 | # 设置 Puppeteer 的环境变量
19 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
20 | ENV CHROME_PATH=/usr/bin/chromium
21 |
22 | # 设置工作目录
23 | WORKDIR /app
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '0.1'
2 |
3 | services:
4 | app:
5 | # 使用本地构建
6 | # build:
7 | # context: .
8 | # 使用已有的远程镜像
9 | image: wxingheng/markdown-to-image-serve:0.0.1
10 | platform: linux/amd64
11 | ports:
12 | - "3000:3000"
13 | environment:
14 | - NODE_ENV=production
15 | - NEXT_PUBLIC_BASE_URL=http://localhost:3000
16 | - CHROME_PATH=/usr/bin/google-chrome-unstable
17 | volumes:
18 | - .:/app
19 | - /app/node_modules
20 | # 指定缓存目录, 避免每次重新构建
21 | - /app/.next
22 | # 需要将容器中的文件挂载到宿主机
23 | - ./public/uploads/posters:/tmp/uploads/posters
24 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | export default function Footer() {
4 | return (
5 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/patches/markdown-to-poster+0.0.9.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/markdown-to-poster/dist/markdown-to-poster.js b/node_modules/markdown-to-poster/dist/markdown-to-poster.js
2 | index 136280b..c9634bc 100644
3 | --- a/node_modules/markdown-to-poster/dist/markdown-to-poster.js
4 | +++ b/node_modules/markdown-to-poster/dist/markdown-to-poster.js
5 | @@ -17938,7 +17938,7 @@ const ig = ({
6 | {
7 | components: {
8 | img(a) {
9 | - const { node: u, src: o, ...l } = a, f = o && `https://api.allorigins.win/raw?url=${encodeURIComponent(o)}`;
10 | + const { node: u, src: o, ...l } = a, f = o && `${o}`;
11 | return /* @__PURE__ */ Se("img", { ...l, src: f });
12 | }
13 | },
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "noImplicitAny": false,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | import createMDX from '@next/mdx'
3 |
4 | const nextConfig = {
5 | // Configure `pageExtensions` to include markdown and MDX files
6 | pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
7 | images: {
8 | remotePatterns: [
9 | {
10 | protocol: 'https',
11 | hostname: '**',
12 | },
13 | {
14 | protocol: 'http',
15 | hostname: '**',
16 | },
17 | ],
18 | },
19 | };
20 |
21 | const withMDX = createMDX({
22 | // Add markdown plugins here, as desired
23 | extension: /\.mdx?$/,
24 | options: {
25 | remarkPlugins: [],
26 | rehypePlugins: [],
27 | },
28 | })
29 |
30 | export default withMDX(nextConfig)
31 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml app configuration file generated for markdown-to-image-serve on 2024-01-01T00:00:00Z
2 | #
3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4 |
5 | app = "markdown-to-image-serve"
6 | primary_region = "hkg"
7 |
8 | [build]
9 | dockerfile = "Dockerfile"
10 |
11 | [env]
12 | NODE_ENV = "production"
13 | NEXT_PUBLIC_BASE_URL = "https://markdown-to-image-serve.fly.dev"
14 |
15 | [http_service]
16 | internal_port = 3000
17 | force_https = true
18 | auto_stop_machines = true
19 | auto_start_machines = true
20 | min_machines_running = 0
21 | processes = ["app"]
22 |
23 | [[http_service.checks]]
24 | grace_period = "10s"
25 | interval = "30s"
26 | method = "GET"
27 | timeout = "5s"
28 | path = "/api/hello"
29 |
30 | [[vm]]
31 | cpu_kind = "shared"
32 | cpus = 1
33 | memory_mb = 1024
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/example/news_poster.md:
--------------------------------------------------------------------------------
1 | # 新闻快报
2 |
3 | ## 无人机公司任命小唐纳德·特朗普后股价飙升
4 | **类别**: 商业新闻
5 |
6 | 
7 |
8 | **摘要**: 总部位于奥兰多的 Unusual Machines 公司表示,其业务依赖中国进口,并警告特朗普关税可能带来的影响。
9 |
10 | **时间**: 1天前
11 |
12 | ---
13 |
14 | ## 达美航空乘务员在新奥尔良涉嫌持刀袭击同事
15 | **类别**: 美国新闻
16 |
17 | 
18 |
19 | **摘要**: 36 岁的约书亚·史密斯(Joshua Smith)因涉嫌二级谋杀被捕,他的律师称其在深夜和凌晨时分饮酒。
20 |
21 | **时间**: 2天前
22 |
23 | ---
24 |
25 | ## 报告发现租房者难以积累财富,他们可以这样改善财务状况
26 | **类别**: 个人理财
27 |
28 | 
29 |
30 | **摘要**: 各收入水平的租房者可以通过减少债务、专注储蓄和投资等关键步骤来积累财富。
31 |
32 | **时间**: 2天前
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 wxingheng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import Header from "@/components/Header";
5 | import Footer from "@/components/Footer";
6 | const inter = Inter({ subsets: ["latin"] });
7 | import 'markdown-to-poster/dist/style.css'
8 | import { Analytics } from "@vercel/analytics/react"
9 |
10 | export const metadata: Metadata = {
11 | title: "markdown to poster image",
12 | description: "Render Markdown into a beautiful poster image, with support for copying as an image. Md to Poster/Image/Quote/Card/Instagram/Twitter/Facebook...",
13 | keywords: ["poster image", "海报", "卡片", "图片", "markdown", "ai", "markdown to poster", "markdown to image", "markdown to card", "markdown to quote", "instagram", "twitter", "facebook"],
14 | creator: "gcui.ai",
15 | };
16 |
17 | export default function RootLayout({
18 | children,
19 | }: Readonly<{
20 | children: React.ReactNode;
21 | }>) {
22 | return (
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "markdown-to-image-serve",
3 | "author": {
4 | "name": "wxingheng",
5 | "email": "wxingheng@outlook.com"
6 | },
7 | "description": "A service that converts Markdown content to beautiful images with API support, offering customizable themes and layouts",
8 | "version": "0.0.6",
9 | "private": true,
10 | "scripts": {
11 | "dev": "next dev -p 3000",
12 | "build": "next build",
13 | "start": "next start",
14 | "lint": "next lint",
15 | "postinstall": "patch-package"
16 | },
17 | "dependencies": {
18 | "@radix-ui/react-label": "^2.0.2",
19 | "@radix-ui/react-radio-group": "^1.1.3",
20 | "@radix-ui/react-scroll-area": "^1.0.5",
21 | "@radix-ui/react-select": "^2.0.0",
22 | "@radix-ui/react-slot": "^1.0.2",
23 | "@radix-ui/react-switch": "^1.0.3",
24 | "@sparticuz/chromium-min": "^131.0.1",
25 | "@uiw/react-md-editor": "^4.0.4",
26 | "@vercel/analytics": "^1.2.2",
27 | "class-variance-authority": "^0.7.0",
28 | "clsx": "^2.1.1",
29 | "lucide-react": "^0.378.0",
30 | "markdown-to-poster": "0.0.9",
31 | "next": "14.2.3",
32 | "puppeteer-core": "^23.9.0",
33 | "react": "^18",
34 | "react-dom": "^18",
35 | "tailwind-merge": "^2.3.0"
36 | },
37 | "devDependencies": {
38 | "@mdx-js/loader": "^3.0.1",
39 | "@mdx-js/react": "^3.0.1",
40 | "@next/mdx": "^14.2.3",
41 | "@types/mdx": "^2.0.13",
42 | "@tailwindcss/typography": "^0.5.13",
43 | "@types/node": "^20",
44 | "@types/react": "^18",
45 | "@types/react-dom": "^18",
46 | "eslint": "^8",
47 | "eslint-config-next": "14.2.3",
48 | "postcss": "^8",
49 | "tailwindcss": "^3.4.1",
50 | "patch-package": "^8.0.0",
51 | "typescript": "^5",
52 | "buffer": "^6.0.3",
53 | "tailwindcss-animate": "^1.0.7",
54 | "axios": "^1.7.9"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 224 71.4% 4.1%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 224 71.4% 4.1%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 224 71.4% 4.1%;
15 |
16 | --primary: 220.9 39.3% 11%;
17 | --primary-foreground: 210 20% 98%;
18 |
19 | --secondary: 220 14.3% 95.9%;
20 | --secondary-foreground: 220.9 39.3% 11%;
21 |
22 | --muted: 220 14.3% 95.9%;
23 | --muted-foreground: 220 8.9% 46.1%;
24 |
25 | --accent: 220 14.3% 95.9%;
26 | --accent-foreground: 220.9 39.3% 11%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 20% 98%;
30 |
31 | --border: 220 13% 91%;
32 | --input: 220 13% 91%;
33 | --ring: 224 71.4% 4.1%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 224 71.4% 4.1%;
40 | --foreground: 210 20% 98%;
41 |
42 | --card: 224 71.4% 4.1%;
43 | --card-foreground: 210 20% 98%;
44 |
45 | --popover: 224 71.4% 4.1%;
46 | --popover-foreground: 210 20% 98%;
47 |
48 | --primary: 210 20% 98%;
49 | --primary-foreground: 220.9 39.3% 11%;
50 |
51 | --secondary: 215 27.9% 16.9%;
52 | --secondary-foreground: 210 20% 98%;
53 |
54 | --muted: 215 27.9% 16.9%;
55 | --muted-foreground: 217.9 10.6% 64.9%;
56 |
57 | --accent: 215 27.9% 16.9%;
58 | --accent-foreground: 210 20% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 20% 98%;
62 |
63 | --border: 215 27.9% 16.9%;
64 | --input: 215 27.9% 16.9%;
65 | --ring: 216 12.2% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/dev.md:
--------------------------------------------------------------------------------
1 |
9 |
10 | # Docker 镜像推送与使用说明
11 |
12 | ## 1. 推送镜像到 Docker Hub
13 |
14 | 1. 登录 Docker Hub:
15 | ```bash
16 | docker login
17 | ```
18 | 2. 给镜像打标签(假设本地镜像名为 markdown-to-image-serve):
19 | ```bash
20 | docker tag markdown-to-image-serve wxingheng/markdown-to-image-serve:0.0.1
21 | ```
22 | 3. 推送到 Docker Hub:
23 | ```bash
24 | docker push wxingheng/markdown-to-image-serve:0.0.1
25 | ```
26 |
27 | ---
28 |
29 | ## 2. 拉取和使用镜像
30 |
31 | 1. 拉取镜像:
32 | ```bash
33 | docker pull wxingheng/markdown-to-image-serve:0.0.1
34 | ```
35 | 2. 运行容器:
36 | ```bash
37 | docker run -p 3000:3000 wxingheng/markdown-to-image-serve:0.0.1
38 | ```
39 |
40 | ---
41 |
42 | ## 3. 镜像优化建议
43 |
44 | - 镜像太大?
45 | - 可用 `docker images` 查看镜像大小。
46 | - 优化建议:多阶段构建、精简依赖、清理缓存等。
47 |
48 |
49 | # 自行构建
50 |
51 |
52 | ## 1. 使用 Docker Compose
53 |
54 | ```bash
55 | docker-compose up -d
56 | # 或
57 | docker compose build --no-cache
58 | ```
59 |
60 | ## 2. 直接使用 Docker
61 |
62 | ```bash
63 | docker build -f Dockerfile -t markdown-to-image-serve .
64 |
65 | docker tag markdown-to-image-serve wxingheng/markdown-to-image-serve:0.0.4
66 | docker tag markdown-to-image-serve wxingheng/markdown-to-image-serve:latest
67 |
68 | docker push wxingheng/markdown-to-image-serve:0.0.4
69 | docker push wxingheng/markdown-to-image-serve:latest
70 |
71 | docker run -p 3000:3000 markdown-to-image-serve
72 | ```
73 |
74 | > **注意:** 如果你在 Docker 构建过程中遇到报错,可以尝试先执行以下命令关闭 BuildKit:
75 | > ```bash
76 | > export DOCKER_BUILDKIT=0
77 | > export COMPOSE_DOCKER_CLI_BUILD=0
78 | > ```
79 |
80 |
81 |
82 | docker build -f Dockerfile.base --platform=linux/amd64 -t node-chrome-base .
83 |
84 | docker tag node-chrome-base wxingheng/node-chrome-base:0.0.2
85 | docker tag node-chrome-base wxingheng/node-chrome-base:latest
86 |
87 | docker push wxingheng/node-chrome-base:0.0.2
88 | docker push wxingheng/node-chrome-base:latest
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/Dockerfile.backup:
--------------------------------------------------------------------------------
1 | # 使用官方 node 镜像,结合国内加速器
2 | FROM node:20-slim
3 | # FROM registry.cn-hangzhou.aliyuncs.com/google_containers/node:20-slim
4 | # FROM ccr.ccs.tencentyun.com/dockerhub-mirror/library/node:20-slim
5 |
6 | # 推荐:使用 Docker 国内加速器(如阿里云、DaoCloud、腾讯云等)
7 | # 你可以在 Docker Desktop 设置里配置加速器,或在 /etc/docker/daemon.json 添加:
8 | # {
9 | # "registry-mirrors": ["https://<你的加速器ID>.mirror.aliyuncs.com"]
10 | # }
11 | # 然后重启 Docker 服务
12 |
13 | # 设置 npm 镜像为淘宝源,加速依赖安装
14 | RUN npm config set registry https://registry.npmmirror.com
15 |
16 | # 添加Google Chrome的密钥
17 | ADD https://dl-ssl.google.com/linux/linux_signing_key.pub /tmp/linux_signing_key.pub
18 | RUN apt-get update \
19 | && apt-get install -y gnupg \
20 | && cat /tmp/linux_signing_key.pub | apt-key add - \
21 | && rm /tmp/linux_signing_key.pub \
22 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
23 | && apt-get update \
24 | && apt-get install -y \
25 | google-chrome-unstable \
26 | fonts-ipafont-gothic \
27 | fonts-wqy-zenhei \
28 | fonts-thai-tlwg \
29 | fonts-kacst \
30 | fonts-freefont-ttf \
31 | --no-install-recommends \
32 | && rm -rf /var/lib/apt/lists/*
33 |
34 | # Install necessary dependencies for Puppeteer and Chromium
35 | # RUN apt-get update && apt-get install -y --no-install-recommends \
36 | # chromium \
37 | # fonts-liberation \
38 | # libasound2 \
39 | # libatk-bridge2.0-0 \
40 | # libatk1.0-0 \
41 | # libcups2 \
42 | # libdrm2 \
43 | # libgbm1 \
44 | # libglu1-mesa \
45 | # libgtk-3-0 \
46 | # libnspr4 \
47 | # libnss3 \
48 | # libu2f-udev \
49 | # libx11-xcb1 \
50 | # libxcomposite1 \
51 | # libxdamage1 \
52 | # libxrandr2 \
53 | # libxshmfence1 \
54 | # xdg-utils \
55 | # && apt-get clean \
56 | # && rm -rf /var/lib/apt/lists/*
57 |
58 | # 设置工作目录
59 | WORKDIR /app
60 |
61 | # 首先复制依赖相关文件
62 | COPY package*.json ./
63 | COPY .env* ./
64 |
65 | # 然后再复制其他源代码
66 | COPY ./ .
67 |
68 | # 安装依赖并构建
69 | RUN npm install && \
70 | npm run build
71 |
72 |
73 | # 暴露端口
74 | EXPOSE 3000
75 |
76 |
77 | # Puppeteer setup: Skip Chromium download and use the installed Chrome
78 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
79 | ENV CHROME_PATH=/usr/bin/google-chrome-unstable
80 | # ENV NODE_ENV=production
81 | # ENV NEXT_PUBLIC_BASE_URL=http://localhost:3000
82 |
83 | # 启动命令
84 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/src/components/PosterView.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react'
3 | import { Md2PosterContent, Md2Poster, Md2PosterHeader, Md2PosterFooter } from 'markdown-to-poster'
4 | import { useSearchParams } from 'next/navigation';
5 | import Image from 'next/image'
6 |
7 | type IThemeType = 'blue' | 'pink' | 'purple' | 'green' | 'yellow' | 'gray' | 'red' | 'indigo' | 'SpringGradientWave';
8 |
9 |
10 | const defaultContentMd = `# AI的发展
11 |
12 |
13 | 人工智能(AI)正在以前所未有的速度发展,深刻改变着我们的生活方式。从ChatGPT到DALL-E,从自动驾驶到智能医疗,AI技术正在各个领域展现其强大潜力。
14 |
15 | ## 主要突破
16 | 1. **大语言模型**: GPT系列模型带来了自然语言处理的重大突破
17 | 2. **计算机视觉**: 在图像识别和生成领域取得显著进展
18 | 3. **智能决策**: 在游戏和复杂决策系统中超越人类表现
19 |
20 | ## 未来展望
21 | - 更强大的多模态模型
22 | - AI与各行业深度融合
23 | - 负责任的AI发展和伦理规范
24 |
25 | 
26 | `
27 |
28 | export default function PosterView() {
29 | // 需要根据url参数,作为mdString 的默认值
30 | const searchParams = useSearchParams()
31 |
32 | function safeDecodeURIComponent(val: string | null | undefined, fallback: string) {
33 | if (typeof val !== 'string') return fallback;
34 | try {
35 | // 防止重复 decode
36 | return decodeURIComponent(val);
37 | } catch {
38 | return val;
39 | }
40 | }
41 |
42 | const mdString = safeDecodeURIComponent(searchParams?.get('content'), defaultContentMd);
43 | const headerString = safeDecodeURIComponent(searchParams?.get('header'), '');
44 | const footerString = safeDecodeURIComponent(searchParams?.get('footer'), 'Powered by markdown-to-image-serve.jcommon.top')
45 | const logo = ('/logo.png')
46 | const logoString = safeDecodeURIComponent(searchParams?.get('logo'), logo);
47 | const theme = safeDecodeURIComponent(searchParams?.get('theme'), 'SpringGradientWave');
48 |
49 | return (
50 |
51 | {/* Preview */}
52 |
53 |
54 | {headerString || new Date().toISOString().slice(0, 10)}
55 |
56 | {mdString}
57 |
58 |
59 | {footerString}
60 |
61 |
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/example/api_buffer_2_image.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs').promises;
2 | const path = require('path');
3 | const axios = require('axios'); // 需要先安装: npm install axios
4 | const Buffer = require('buffer').Buffer;
5 |
6 | /**
7 | * 读取 Markdown 文件内容
8 | * @param {string} filePath - Markdown 文件路径
9 | * @returns {Promise} Markdown 文件内容
10 | */
11 | async function readMarkdownFile(filePath) {
12 | return await fs.readFile(filePath, 'utf-8');
13 | }
14 |
15 | /**
16 | * 将 Markdown 内容转换为图像
17 | * @param {string} markdownContent - Markdown 内容
18 | * @returns {Promise