awesome-gpt-image-2/scripts/utils/markdown-generator.ts
Jared Liu 3bea8cd20f feat: add OG hero image at README top + fix English URL to have no locale prefix
- Embed clickable OG hero image at the very top of each locale README, linking
  to the prompts gallery (respecting the locale — en → no prefix, zh → zh-CN,
  others → their full code, matching youhome's next-intl `as-needed`).
- Introduce `buildPromptsUrl()` helper; consolidate all gallery URL construction
  through it. Fixes the previous English URLs which wrongly emitted `/en-US/`.
2026-04-20 20:19:37 +08:00

467 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Prompt, FilterCategory } from './cms-client.js';
import { t } from './i18n.js';
interface SortedPrompts {
all: Prompt[];
featured: Prompt[];
regular: Prompt[];
stats: {
total: number;
featured: number;
};
categories?: FilterCategory[];
}
export interface LanguageConfig {
code: string;
name: string; // Display name
readmeFileName: string;
}
export const SUPPORTED_LANGUAGES: LanguageConfig[] = [
{ code: 'en', name: 'English', readmeFileName: 'README.md' },
{ code: 'zh', name: '简体中文', readmeFileName: 'README_zh.md' },
{ code: 'zh-TW', name: '繁體中文', readmeFileName: 'README_zh-TW.md' },
{ code: 'ja-JP', name: '日本語', readmeFileName: 'README_ja-JP.md' },
{ code: 'ko-KR', name: '한국어', readmeFileName: 'README_ko-KR.md' },
{ code: 'th-TH', name: 'ไทย', readmeFileName: 'README_th-TH.md' },
{ code: 'vi-VN', name: 'Tiếng Việt', readmeFileName: 'README_vi-VN.md' },
{ code: 'hi-IN', name: 'हिन्दी', readmeFileName: 'README_hi-IN.md' },
{ code: 'es-ES', name: 'Español', readmeFileName: 'README_es-ES.md' },
{ code: 'es-419', name: 'Español (Latinoamérica)', readmeFileName: 'README_es-419.md' },
{ code: 'de-DE', name: 'Deutsch', readmeFileName: 'README_de-DE.md' },
{ code: 'fr-FR', name: 'Français', readmeFileName: 'README_fr-FR.md' },
{ code: 'it-IT', name: 'Italiano', readmeFileName: 'README_it-IT.md' },
{ code: 'pt-BR', name: 'Português (Brasil)', readmeFileName: 'README_pt-BR.md' },
{ code: 'pt-PT', name: 'Português', readmeFileName: 'README_pt-PT.md' },
{ code: 'tr-TR', name: 'Türkçe', readmeFileName: 'README_tr-TR.md' },
];
const MAX_REGULAR_PROMPTS_TO_DISPLAY = 120;
/**
* Convert locale to URL language prefix.
* youhome uses next-intl `as-needed`: default (en-US) has no prefix;
* all other locales carry their full code.
* en -> '' (no prefix), zh -> 'zh-CN', others remain unchanged
*/
function getLocalePrefix(locale: string): string {
if (locale === 'en') {
return '';
}
if (locale === 'zh') {
return 'zh-CN';
}
// Other language codes (e.g., zh-TW, ja-JP) remain unchanged
return locale;
}
/**
* Build an absolute URL to the GPT Image 2 prompts gallery with the correct
* locale prefix. Pass a suffix like `?id=123` or `?categories=foo` via `suffix`.
*/
function buildPromptsUrl(locale: string, suffix = ''): string {
const prefix = getLocalePrefix(locale);
const prefixSegment = prefix ? `/${prefix}` : '';
return `https://youmind.com${prefixSegment}/gpt-image-2-prompts${suffix}`;
}
/**
* 清理提示词内容中的代码块标记
* 移除 ``` 或 ```json 等格式的代码块标记
*
* 处理的情况:
* - ``` 提示词 ```
* - ```json 提示词 ```
* - ```python 提示词 ``` 等任意语言标识符
* - 多行内容中的代码块标记
*/
function cleanPromptContent(content: string): string {
if (!content) return content;
let cleaned = content;
// 匹配代码块标记:``` 或 ```language如 ```json, ```python 等)
// 语言标识符可能包含字母、数字、连字符等(如 json, python, typescript
// 1. 移除开头的代码块标记
// 匹配:``` + 可选语言标识符 + 可选空白字符 + 可选换行
cleaned = cleaned.replace(/^```[\w-]*\s*\n?/im, '');
// 2. 移除结尾的代码块标记
// 匹配:可选换行 + ``` + 可选空白字符
cleaned = cleaned.replace(/\n?```\s*$/im, '');
// 3. 移除中间可能存在的代码块标记(处理嵌套或错误格式的情况)
// 匹配:换行 + ``` + 可选语言标识符 + 可选空白 + 换行
cleaned = cleaned.replace(/\n```[\w-]*\s*\n/g, '\n');
// 4. 清理首尾空白字符(包括换行)
cleaned = cleaned.trim();
return cleaned;
}
export function generateMarkdown(data: SortedPrompts, total: number, locale: string = 'en'): string {
const { featured, regular, stats, categories } = data;
const displayedRegular = regular.slice(0, MAX_REGULAR_PROMPTS_TO_DISPLAY);
const hiddenCount = total - displayedRegular.length;
let md = generateHeader(locale);
md += generateLanguageNavigation(locale);
md += generateGalleryCTA(categories || [], locale);
md += generateTOC(locale);
md += generateWhatIs(locale);
md += generateStats(stats, locale);
md += generateFeaturedSection(featured, locale);
md += generateAllPromptsSection(displayedRegular, hiddenCount, locale);
md += generateContribute(locale);
md += generateFooter(locale);
return md;
}
function generateHeader(locale: string): string {
const galleryUrl = buildPromptsUrl(locale);
return `
<a href="${galleryUrl}">
<img src="https://marketing-assets.youmind.com/campaigns/gpt-image-2/og.png" alt="GPT Image 2 Prompts" width="100%" />
</a>
> 💡 ${t('seedancePromo', locale)}
# 🚀 ${t('title', locale)}
[![Awesome](https://awesome.re/badge.svg)](https://github.com/sindresorhus/awesome)
[![GitHub stars](https://img.shields.io/github/stars/YouMind-OpenLab/awesome-gpt-image-2?style=social)](https://github.com/YouMind-OpenLab/awesome-gpt-image-2)
[![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/)
[![Update README](https://github.com/YouMind-OpenLab/awesome-gpt-image-2/actions/workflows/update-readme.yml/badge.svg)](https://github.com/YouMind-OpenLab/awesome-gpt-image-2/actions)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](docs/CONTRIBUTING.md)
> 🎨 ${t('subtitle', locale)}
> ⚠️ ${t('copyright', locale)}
---
`;
}
function generateLanguageNavigation(currentLocale: string): string {
let md = '';
// Sort languages so current one is first or en is first?
// Keeping the array order is usually best, but we want a clean list.
const badges = SUPPORTED_LANGUAGES.map(lang => {
const isCurrent = lang.code === currentLocale || (currentLocale.startsWith(lang.code) && !SUPPORTED_LANGUAGES.some(l => l.code === currentLocale && l.code !== lang.code));
// Color logic: green for current, blue for others, or grey?
// Using the style from the image: "Click to View"
const color = isCurrent ? 'brightgreen' : 'lightgrey';
const text = isCurrent ? 'Current' : 'Click%20to%20View';
const link = lang.readmeFileName;
// If current, maybe no link or link to self?
// Using shields.io badge format: label-message-color
// Label = Native Name, Message = Click to View (or Ver Traducción etc)
const safeName = encodeURIComponent(lang.name);
return `[![${lang.name}](https://img.shields.io/badge/${safeName}-${text}-${color})](${link})`;
});
md += badges.join(' ') + '\n\n---\n\n';
return md;
}
function generateGalleryCTA(categories: FilterCategory[], locale: string): string {
// 根据语言选择图片zh 和 zh-TW 使用 zh其他使用 en
const imageLang = locale === 'zh' || locale === 'zh-TW' ? 'zh' : 'en';
const coverImage = `public/images/gpt-image-2-prompts-cover-${imageLang}.png`;
let md = `## 🌐 ${t('viewInGallery', locale)}
<div align="center">
![Cover](${coverImage})
</div>
**[${t('browseGallery', locale)}](${buildPromptsUrl(locale)})**
${t('galleryFeatures', locale)}
| Feature | ${t('githubReadme', locale)} | ${t('youmindGallery', locale)} |
|---------|--------------|---------------------|
| 🎨 ${t('visualLayout', locale)} | ${t('linearList', locale)} | ${t('masonryGrid', locale)} |
| 🔍 ${t('search', locale)} | ${t('ctrlFOnly', locale)} | ${t('fullTextSearch', locale)} |
| 🤖 ${t('aiGenerate', locale)} | - | ${t('aiOneClickGen', locale)} |
| 📱 ${t('mobile', locale)} | ${t('basic', locale)} | ${t('fullyResponsive', locale)} |
| 🏷️ ${t('categories', locale)} | - | ${t('categoryBrowsing', locale)} |
`;
// Add categories section if available
if (categories.length > 0) {
md += generateCategoriesSection(categories, locale);
}
md += `---
`;
return md;
}
function generateCategoriesSection(categories: FilterCategory[], locale: string): string {
// Get parent categories (no parentId)
const parentCategories = categories.filter(c => c.parentId === null);
let md = `\n### 🏷️ ${t('browseByCategory', locale)}\n\n`;
for (const parent of parentCategories) {
// Parent category - no link
md += `- **${parent.title}**\n`;
// Get children of this parent
const children = categories.filter(c => c.parentId === parent.id);
for (const child of children) {
// Child category - with link
const categoryUrl = buildPromptsUrl(locale, `?categories=${child.slug}`);
md += ` - [${child.title}](${categoryUrl})\n`;
}
}
md += `\n`;
return md;
}
function generatePromptSection(prompt: Prompt, index: number, locale: string): string {
const authorLink = prompt.author.link || '#';
const publishedDate = new Date(prompt.sourcePublishedAt).toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
// Use translatedContent if available, otherwise fallback to content
// Clean code block markers (``` or ```json etc.) from the content
const rawContent = prompt.translatedContent || prompt.content;
const promptContent = cleanPromptContent(rawContent);
const hasArguments = promptContent.includes('{argument');
let md = `### No. ${index + 1}: ${prompt.title}\n\n`;
// Language badge
md += `![Language-${prompt.language.toUpperCase()}](https://img.shields.io/badge/Language-${prompt.language.toUpperCase()}-blue)\n`;
if (prompt.featured) {
md += `![Featured](https://img.shields.io/badge/⭐-Featured-gold)\n`;
}
if (hasArguments) {
md += `![Raycast](https://img.shields.io/badge/🚀-Raycast_Friendly-purple)\n`;
}
md += `\n#### 📖 ${t('description', locale)}\n\n${prompt.description}\n\n`;
md += `#### 📝 ${t('prompt', locale)}\n\n\`\`\`\n${promptContent}\n\`\`\`\n\n`;
if (prompt.sourceMedia && prompt.sourceMedia.length > 0) {
md += `#### 🖼️ ${t('generatedImages', locale)}\n\n`;
prompt.sourceMedia.forEach((imageUrl, imgIndex) => {
md += `##### Image ${imgIndex + 1}\n\n`;
md += `<div align="center">\n`;
md += `<img src="${imageUrl}" width="${prompt.featured ? '700' : '600'}" alt="${prompt.title} - Image ${imgIndex + 1}">\n`;
md += `</div>\n\n`;
});
}
md += `#### 📌 ${t('details', locale)}\n\n`;
md += `- **${t('author', locale)}:** [${prompt.author.name}](${authorLink})\n`;
md += `- **${t('source', locale)}:** [Twitter Post](${prompt.sourceLink})\n`;
md += `- **${t('published', locale)}:** ${publishedDate}\n`;
md += `- **${t('languages', locale)}:** ${prompt.language}\n\n`;
md += `**[${t('tryItNow', locale)}](${buildPromptsUrl(locale, `?id=${prompt.id}`)})**\n\n`;
md += `---\n\n`;
return md;
}
function generateFeaturedSection(featured: Prompt[], locale: string): string {
if (featured.length === 0) return '';
let md = `## 🔥 ${t('featuredPrompts', locale)}\n\n`;
md += `> ⭐ ${t('handPicked', locale)}\n\n`;
featured.forEach((prompt, index) => {
md += generatePromptSection(prompt, index, locale);
});
return md;
}
function generateAllPromptsSection(regular: Prompt[], hiddenCount: number, locale: string): string {
if (regular.length === 0 && hiddenCount === 0) return '';
let md = `## 📋 ${t('allPrompts', locale)}\n\n`;
md += `> 📝 ${t('sortedByDate', locale)}\n\n`;
regular.forEach((prompt, index) => {
md += generatePromptSection(prompt, index, locale);
});
if (hiddenCount > 0) {
md += `---\n\n`;
md += `## 📚 ${t('morePrompts', locale)}\n\n`;
md += `<div align="center">\n\n`;
md += `### 🎯 ${hiddenCount} ${t('morePromptsDesc', locale)}\n\n`;
md += `Due to GitHub's content length limitations, we can only display the first ${MAX_REGULAR_PROMPTS_TO_DISPLAY} regular prompts in this README.\n\n`;
md += `**[${t('viewAll', locale)}](${buildPromptsUrl(locale)})**\n\n`;
md += `The gallery features:\n\n`;
md += `${t('galleryFeature1', locale)}\n\n`;
md += `${t('galleryFeature2', locale)}\n\n`;
md += `${t('galleryFeature3', locale)}\n\n`;
md += `${t('galleryFeature4', locale)}\n\n`;
md += `</div>\n\n`;
md += `---\n\n`;
}
return md;
}
function generateStats(stats: { total: number; featured: number }, locale: string): string {
const now = new Date().toLocaleString(locale, {
timeZone: 'UTC',
dateStyle: 'full',
timeStyle: 'long',
});
return `## 📊 ${t('stats', locale)}
<div align="center">
| ${t('metric', locale)} | ${t('count', locale)} |
|--------|-------|
| 📝 ${t('totalPrompts', locale)} | **${stats.total}** |
| ⭐ ${t('featured', locale)} | **${stats.featured}** |
| 🔄 ${t('lastUpdated', locale)} | **${now}** |
</div>
---
`;
}
function generateTOC(locale: string): string {
// Generating anchors is tricky with i18n, but GitHub usually slugifies the headers.
// For now we assume English anchors or standard GitHub behavior.
// Ideally we should use the exact translation for the link text, and the slugified translation for the href.
// But simple manual mapping for now.
return `## 📖 ${t('toc', locale)}
- [🌐 ${t('viewInGallery', locale)}](#-view-in-web-gallery)
- [🤔 ${t('whatIs', locale)}](#-what-is-gpt-image-2)
- [📊 ${t('stats', locale)}](#-statistics)
- [🔥 ${t('featuredPrompts', locale)}](#-featured-prompts)
- [📋 ${t('allPrompts', locale)}](#-all-prompts)
- [🤝 ${t('howToContribute', locale)}](#-how-to-contribute)
- [📄 ${t('license', locale)}](#-license)
- [🙏 ${t('acknowledgements', locale)}](#-acknowledgements)
- [⭐ ${t('starHistory', locale)}](#-star-history)
---
`;
}
function generateWhatIs(locale: string): string {
return `## 🤔 ${t('whatIs', locale)}
${t('whatIsIntro', locale)}
- 🎯 ${t('multimodalUnderstanding', locale)}
- 🎨 ${t('highQualityGeneration', locale)}
- ⚡ ${t('fastIteration', locale)}
- 🌈 ${t('diverseStyles', locale)}
- 🔧 ${t('preciseControl', locale)}
- 📐 ${t('complexScenes', locale)}
📚 ${t('learnMore', locale)}
### 🚀 ${t('raycastIntegration', locale)}
${t('raycastDescription', locale)}
**${t('example', locale)}**
\`\`\`
${t('raycastExample', locale)}
\`\`\`
${t('raycastUsage', locale)}
---
`;
}
function generateContribute(locale: string): string {
return `## 🤝 ${t('howToContribute', locale)}
${t('welcomeContributions', locale)}
### 🐛 ${t('githubIssue', locale)}
1. Click [**${t('submitNewPrompt', locale)}**](https://github.com/YouMind-OpenLab/awesome-gpt-image-2/issues/new?template=submit-prompt.yml)
2. ${t('fillForm', locale)}
3. ${t('submitWait', locale)}
4. ${t('approvedSync', locale)}
5. ${t('appearInReadme', locale)}
**${t('note', locale)}** ${t('noteContent', locale)}
${t('seeContributing', locale)}
---
`;
}
function generateFooter(locale: string): string {
const timestamp = new Date().toISOString();
return `## 📄 ${t('license', locale)}
${t('licensedUnder', locale)}
---
## 🙏 ${t('acknowledgements', locale)}
- [Payload CMS](https://payloadcms.com/)
- [youmind.com](https://youmind.com)
---
## ⭐ ${t('starHistory', locale)}
[![Star History Chart](https://api.star-history.com/svg?repos=YouMind-OpenLab/awesome-gpt-image-2&type=Date)](https://star-history.com/#YouMind-OpenLab/awesome-gpt-image-2&Date)
---
<div align="center">
**[🌐 ${t('viewInGallery', locale)}](${buildPromptsUrl(locale)})** •
**[📝 ${t('submitPrompt', locale)}](https://github.com/YouMind-OpenLab/awesome-gpt-image-2/issues/new?template=submit-prompt.yml)** •
**[⭐ ${t('starRepo', locale)}](https://github.com/YouMind-OpenLab/awesome-gpt-image-2)**
<sub>🤖 ${t('autoGenerated', locale)} ${timestamp}</sub>
</div>
`;
}