@tomako/tools-runtime 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/features/tools/app-icon-resizer/app-icon-resizer.spec.ts +1 -1
- package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.container.tsx +6 -8
- package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.spec.ts +1 -1
- package/src/features/tools/cold-start-channel-selector/cold-start-channel-selector.spec.ts +1 -1
- package/src/features/tools/competitor-analysis/competitor-analysis.container.tsx +5 -6
- package/src/features/tools/competitor-analysis/competitor-analysis.spec.ts +1 -1
- package/src/features/tools/disclaimer-generator/disclaimer-generator.package.ts +1 -1
- package/src/features/tools/email-template-generator/email-template-generator.container.tsx +45 -173
- package/src/features/tools/email-template-generator/email-template-generator.spec.ts +1 -1
- package/src/features/tools/pitch-deck-generator/pitch-deck-library.tsx +12 -5
- package/src/features/tools/product-name-generator/product-name-generator.spec.ts +1 -1
- package/src/features/tools/product-poster-generator/product-poster-generator.spec.ts +1 -1
- package/src/features/tools/shared/tool-guide-section.tsx +5 -1
- package/src/features/tools/shared/tool-page-shell.tsx +10 -4
- package/src/features/tools/shared/tool-standard-landing-sections.tsx +4 -1
- package/src/features/tools/slogan-generator/slogan-generator.package.ts +73 -1
- package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.container.tsx +2 -3
- package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.spec.ts +1 -1
- package/src/i18n/messages/en/tools/app-store-screenshot-generator.ts +1 -1
- package/src/i18n/messages/en/tools/cold-start-channel-selector.ts +1 -1
- package/src/i18n/messages/en/tools/competitor-analysis.ts +5 -2
- package/src/i18n/messages/en/tools/email-template-generator.ts +3 -3
- package/src/i18n/messages/en/tools/product-poster-generator.ts +2 -2
- package/src/i18n/messages/en/tools/twitter-gif-downloader.ts +1 -1
- package/src/i18n/messages/zh/tools/app-store-screenshot-generator.ts +1 -1
- package/src/i18n/messages/zh/tools/competitor-analysis.ts +4 -1
- package/src/i18n/messages/zh/tools/email-template-generator.ts +1 -1
- package/src/i18n/messages/zh/tools/product-poster-generator.ts +1 -1
- package/src/i18n/messages/zh/tools/twitter-gif-downloader.ts +1 -1
- package/src/i18n/messages/zh-tw/tools/app-store-screenshot-generator.ts +1 -1
- package/src/i18n/messages/zh-tw/tools/competitor-analysis.ts +4 -1
- package/src/i18n/messages/zh-tw/tools/email-template-generator.ts +1 -1
- package/src/i18n/messages/zh-tw/tools/product-poster-generator.ts +1 -1
- package/src/i18n/messages/zh-tw/tools/twitter-gif-downloader.ts +1 -1
- package/src/lib/static-asset.ts +52 -0
- package/src/lib/tools/ops-catalog.ts +10 -1
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ export const appIconResizerSpec: ToolSpec = {
|
|
|
6
6
|
riskLevel: "low",
|
|
7
7
|
seo: {
|
|
8
8
|
"publishedAt": "2026-06-30",
|
|
9
|
-
"updatedAt": "2026-08-
|
|
9
|
+
"updatedAt": "2026-08-30",
|
|
10
10
|
"readingTime": "5 min",
|
|
11
11
|
"featured": false,
|
|
12
12
|
"ogImage": "/tools/app-icon-resizer-section-primary-8bit-v3.webp"
|
|
@@ -15,7 +15,6 @@ import { ToolCta } from "../shared/tool-cta";
|
|
|
15
15
|
import { ToolGuideSection, ToolImageVisual, ToolWorkspaceSection } from "../shared/tool-guide-section";
|
|
16
16
|
import { ToolPageShell } from "../shared/tool-page-shell";
|
|
17
17
|
import type { ToolPageProps } from "../types";
|
|
18
|
-
import { ImgConfig } from "./config";
|
|
19
18
|
|
|
20
19
|
export async function AppStoreScreenshotGeneratorContainer({
|
|
21
20
|
locale,
|
|
@@ -30,14 +29,13 @@ export async function AppStoreScreenshotGeneratorContainer({
|
|
|
30
29
|
<ToolPageShell
|
|
31
30
|
locale={locale}
|
|
32
31
|
spec={spec}
|
|
33
|
-
|
|
34
|
-
heroBackgroundImage={ImgConfig.heroBgImg}
|
|
35
|
-
heroBackgroundAlt={t("heroVisualAlt")}
|
|
36
|
-
heroBackgroundColor="#F7F6F2"
|
|
37
|
-
heroBackgroundImageFrameClassName="inset-y-16 left-1/2 w-[calc(100%-2.5rem)] max-w-[1472px] -translate-x-1/2 sm:w-[calc(100%-4rem)] md:inset-y-24"
|
|
38
|
-
heroBackgroundImageSizes="(min-width: 1536px) 1472px, (min-width: 640px) calc(100vw - 4rem), calc(100vw - 2.5rem)"
|
|
32
|
+
showDefaultHeader={false}
|
|
39
33
|
>
|
|
40
|
-
<ToolWorkspaceSection
|
|
34
|
+
<ToolWorkspaceSection
|
|
35
|
+
heading={t("workspaceHeading")}
|
|
36
|
+
hint={t("workspaceHint")}
|
|
37
|
+
headingLevel="h1"
|
|
38
|
+
>
|
|
41
39
|
<AppStoreScreenshotGenerator />
|
|
42
40
|
</ToolWorkspaceSection>
|
|
43
41
|
|
package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.spec.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const appStoreScreenshotGeneratorSpec: ToolSpec = {
|
|
|
5
5
|
toolType: "generator",
|
|
6
6
|
seo: {
|
|
7
7
|
"publishedAt": "2026-06-28",
|
|
8
|
-
"updatedAt": "2026-
|
|
8
|
+
"updatedAt": "2026-08-30",
|
|
9
9
|
"readingTime": "5 min",
|
|
10
10
|
"featured": false,
|
|
11
11
|
"ogImage": "/tools/app-store-screenshot-generator-value.webp"
|
|
@@ -6,7 +6,7 @@ export const coldStartChannelSelectorSpec: ToolSpec = {
|
|
|
6
6
|
riskLevel: "medium",
|
|
7
7
|
seo: {
|
|
8
8
|
publishedAt: "2026-06-29",
|
|
9
|
-
updatedAt: "2026-08-
|
|
9
|
+
updatedAt: "2026-08-30",
|
|
10
10
|
readingTime: "7 min",
|
|
11
11
|
featured: false,
|
|
12
12
|
ogImage: "/tools/cold-start-channel-selector-hero.webp",
|
|
@@ -46,7 +46,6 @@ function ImagePlaceholder({
|
|
|
46
46
|
|
|
47
47
|
export async function CompetitorAnalysisContainer({ locale, spec }: ToolPageProps) {
|
|
48
48
|
const t = await createToolGuideTranslator(locale, "toolsPages.competitorAnalysis.guide");
|
|
49
|
-
const sharedT = await createToolGuideTranslator(locale, "toolsPages.shared");
|
|
50
49
|
|
|
51
50
|
const steps = t.raw("howToUseSteps") as readonly string[];
|
|
52
51
|
const guidance = t.raw("guidanceBullets") as readonly string[];
|
|
@@ -58,13 +57,13 @@ export async function CompetitorAnalysisContainer({ locale, spec }: ToolPageProp
|
|
|
58
57
|
<ToolPageShell
|
|
59
58
|
locale={locale}
|
|
60
59
|
spec={spec}
|
|
61
|
-
|
|
62
|
-
heroVisual={<ImagePlaceholder label={t("visualAlt.hero")} className="max-w-4xl" />}
|
|
60
|
+
showDefaultHeader={false}
|
|
63
61
|
>
|
|
64
62
|
<ToolWorkspaceSection
|
|
65
|
-
heading={
|
|
66
|
-
hint={
|
|
67
|
-
badge={
|
|
63
|
+
heading={t("workspaceHeading")}
|
|
64
|
+
hint={t("workspaceHint")}
|
|
65
|
+
badge={t("workspaceBadge")}
|
|
66
|
+
headingLevel="h1"
|
|
68
67
|
>
|
|
69
68
|
<CompetitorAnalysis />
|
|
70
69
|
</ToolWorkspaceSection>
|
|
@@ -6,7 +6,7 @@ export const competitorAnalysisSpec: ToolSpec = {
|
|
|
6
6
|
riskLevel: "medium",
|
|
7
7
|
seo: {
|
|
8
8
|
"publishedAt": "2026-06-29",
|
|
9
|
-
"updatedAt": "2026-
|
|
9
|
+
"updatedAt": "2026-08-30",
|
|
10
10
|
"readingTime": "6 min",
|
|
11
11
|
"featured": false,
|
|
12
12
|
"ogImage": "/tools/competitor-radar-hero.webp"
|
|
@@ -34,7 +34,7 @@ export const disclaimerGeneratorPackage: ToolPackage = {
|
|
|
34
34
|
riskLevel: "high",
|
|
35
35
|
keywordEvidence: "docs/seo-keyword-briefs/disclaimer-generator.md",
|
|
36
36
|
seo: {
|
|
37
|
-
updatedAt: "2026-
|
|
37
|
+
updatedAt: "2026-08-30",
|
|
38
38
|
readingTime: "6 min",
|
|
39
39
|
ogImage: "/tools/disclaimer-generator-section-primary.webp",
|
|
40
40
|
},
|
|
@@ -3,216 +3,88 @@ import { createToolGuideTranslator } from "../shared/tool-message-translator";
|
|
|
3
3
|
import { EmailTemplateGenerator } from "./widget";
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
ToolBoundarySection,
|
|
7
|
+
ToolFaqSection,
|
|
8
|
+
ToolSection,
|
|
9
|
+
ToolStandardsSection,
|
|
10
|
+
ToolTextRows,
|
|
11
|
+
ToolUseCasesSection,
|
|
12
|
+
ToolValueSection,
|
|
13
|
+
type ToolBoundaryGroup,
|
|
14
|
+
type ToolFaqItem,
|
|
15
|
+
type ToolTextCard,
|
|
8
16
|
} from "../shared/tool-content-sections";
|
|
9
17
|
import { ToolCta } from "../shared/tool-cta";
|
|
10
18
|
import { ToolImageVisual, ToolWorkspaceSection } from "../shared/tool-guide-section";
|
|
11
19
|
import { ToolPageShell } from "../shared/tool-page-shell";
|
|
12
20
|
import type { ToolPageProps } from "../types";
|
|
13
21
|
|
|
14
|
-
type TextCard = {
|
|
15
|
-
title: string;
|
|
16
|
-
body: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type BoundaryGroup = {
|
|
20
|
-
title: string;
|
|
21
|
-
items: readonly string[];
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type FaqItem = {
|
|
25
|
-
question: string;
|
|
26
|
-
answer: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function OutputSection({
|
|
30
|
-
heading,
|
|
31
|
-
intro,
|
|
32
|
-
items,
|
|
33
|
-
visualAlt,
|
|
34
|
-
}: {
|
|
35
|
-
heading: string;
|
|
36
|
-
intro: string;
|
|
37
|
-
items: readonly TextCard[];
|
|
38
|
-
visualAlt: string;
|
|
39
|
-
}) {
|
|
40
|
-
return (
|
|
41
|
-
<section className="border-t border-[#E1DDD3] py-14 md:py-16">
|
|
42
|
-
<div className="grid gap-8 md:grid-cols-[0.92fr_1.08fr] md:items-center">
|
|
43
|
-
<div>
|
|
44
|
-
<h2 className="text-3xl font-semibold leading-tight text-[#111111] md:text-4xl">
|
|
45
|
-
{heading}
|
|
46
|
-
</h2>
|
|
47
|
-
<p className="mt-4 text-base leading-7 text-[#555555]">{intro}</p>
|
|
48
|
-
</div>
|
|
49
|
-
<ToolImageVisual src="/tools/email-template-generator-value.webp" alt={visualAlt} wide />
|
|
50
|
-
</div>
|
|
51
|
-
<div className="mt-8">
|
|
52
|
-
<ToolCardGrid items={items} />
|
|
53
|
-
</div>
|
|
54
|
-
</section>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function StandardsSection({
|
|
59
|
-
heading,
|
|
60
|
-
intro,
|
|
61
|
-
items,
|
|
62
|
-
}: {
|
|
63
|
-
heading: string;
|
|
64
|
-
intro: string;
|
|
65
|
-
items: readonly TextCard[];
|
|
66
|
-
}) {
|
|
67
|
-
return (
|
|
68
|
-
<section className="border-t border-[#E1DDD3] py-14 md:py-16">
|
|
69
|
-
<div className="max-w-3xl">
|
|
70
|
-
<h2 className="text-3xl font-semibold leading-tight text-[#111111] md:text-4xl">
|
|
71
|
-
{heading}
|
|
72
|
-
</h2>
|
|
73
|
-
<p className="mt-4 text-base leading-7 text-[#555555]">{intro}</p>
|
|
74
|
-
</div>
|
|
75
|
-
<div className="mt-7">
|
|
76
|
-
<ToolCardGrid items={items} />
|
|
77
|
-
</div>
|
|
78
|
-
</section>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function BoundarySection({
|
|
83
|
-
heading,
|
|
84
|
-
intro,
|
|
85
|
-
groups,
|
|
86
|
-
visualAlt,
|
|
87
|
-
}: {
|
|
88
|
-
heading: string;
|
|
89
|
-
intro: string;
|
|
90
|
-
groups: readonly BoundaryGroup[];
|
|
91
|
-
visualAlt: string;
|
|
92
|
-
}) {
|
|
93
|
-
return (
|
|
94
|
-
<section className="grid gap-8 border-t border-[#E1DDD3] py-14 md:grid-cols-[0.98fr_1.02fr] md:items-center md:py-16">
|
|
95
|
-
<ToolImageVisual src="/tools/email-template-generator-support.webp" alt={visualAlt} />
|
|
96
|
-
<div>
|
|
97
|
-
<h2 className="text-3xl font-semibold leading-tight text-[#111111] md:text-4xl">
|
|
98
|
-
{heading}
|
|
99
|
-
</h2>
|
|
100
|
-
<p className="mt-4 text-base leading-7 text-[#555555]">{intro}</p>
|
|
101
|
-
<div className="mt-6 grid gap-3">
|
|
102
|
-
{groups.map((group) => (
|
|
103
|
-
<article
|
|
104
|
-
key={group.title}
|
|
105
|
-
className="rounded-[1.25rem] border border-[#D8D4CA] bg-white p-5 shadow-[0_12px_28px_rgba(17,17,17,0.05)]"
|
|
106
|
-
>
|
|
107
|
-
<h3 className="text-base font-semibold leading-tight text-[#111111]">
|
|
108
|
-
{group.title}
|
|
109
|
-
</h3>
|
|
110
|
-
<ul className="mt-3 list-disc space-y-2 pl-5 text-sm leading-6 text-[#555555]">
|
|
111
|
-
{group.items.map((item) => (
|
|
112
|
-
<li key={item}>{item}</li>
|
|
113
|
-
))}
|
|
114
|
-
</ul>
|
|
115
|
-
</article>
|
|
116
|
-
))}
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</section>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function GuidanceSection({
|
|
124
|
-
heading,
|
|
125
|
-
intro,
|
|
126
|
-
items,
|
|
127
|
-
visualAlt,
|
|
128
|
-
}: {
|
|
129
|
-
heading: string;
|
|
130
|
-
intro: string;
|
|
131
|
-
items: readonly string[];
|
|
132
|
-
visualAlt: string;
|
|
133
|
-
}) {
|
|
134
|
-
return (
|
|
135
|
-
<section className="grid gap-8 border-t border-[#E1DDD3] py-14 md:grid-cols-[1fr_1fr] md:items-center md:py-16">
|
|
136
|
-
<div>
|
|
137
|
-
<h2 className="text-3xl font-semibold leading-tight text-[#111111] md:text-4xl">
|
|
138
|
-
{heading}
|
|
139
|
-
</h2>
|
|
140
|
-
<p className="mt-4 text-base leading-7 text-[#555555]">{intro}</p>
|
|
141
|
-
<div className="mt-6 space-y-4">
|
|
142
|
-
{items.map((item) => (
|
|
143
|
-
<p key={item} className="text-base leading-7 text-[#3D3D3D]">
|
|
144
|
-
{item}
|
|
145
|
-
</p>
|
|
146
|
-
))}
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
<ToolImageVisual src="/tools/email-template-generator-export.webp" alt={visualAlt} />
|
|
150
|
-
</section>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
22
|
export async function EmailTemplateGeneratorContainer({ locale, spec }: ToolPageProps) {
|
|
155
23
|
const t = await createToolGuideTranslator(locale, "toolsPages.emailTemplateGenerator.guide");
|
|
156
|
-
const outputItems = t.raw("outputItems") as readonly
|
|
157
|
-
const standardsItems = t.raw("standardsItems") as readonly
|
|
158
|
-
const boundaryGroups = t.raw("boundaryGroups") as readonly
|
|
24
|
+
const outputItems = t.raw("outputItems") as readonly ToolTextCard[];
|
|
25
|
+
const standardsItems = t.raw("standardsItems") as readonly ToolTextCard[];
|
|
26
|
+
const boundaryGroups = t.raw("boundaryGroups") as readonly ToolBoundaryGroup[];
|
|
159
27
|
const guidance = t.raw("guidanceItems") as readonly string[];
|
|
160
|
-
const useCases = t.raw("useCaseItems") as readonly
|
|
161
|
-
const faqItems = t.raw("faqItems") as readonly
|
|
28
|
+
const useCases = t.raw("useCaseItems") as readonly ToolTextCard[];
|
|
29
|
+
const faqItems = t.raw("faqItems") as readonly ToolFaqItem[];
|
|
162
30
|
|
|
163
31
|
return (
|
|
164
|
-
<ToolPageShell
|
|
165
|
-
locale={locale}
|
|
166
|
-
spec={spec}
|
|
167
|
-
heroVariant="immersive"
|
|
168
|
-
heroBackgroundImage="/tools/email-template-generator-hero.webp"
|
|
169
|
-
heroBackgroundAlt={t("heroImageAlt")}
|
|
170
|
-
>
|
|
32
|
+
<ToolPageShell locale={locale} spec={spec} showDefaultHeader={false}>
|
|
171
33
|
<ToolWorkspaceSection
|
|
172
34
|
heading={t("workspaceHeading")}
|
|
173
35
|
hint={t("workspaceHint")}
|
|
36
|
+
headingLevel="h1"
|
|
174
37
|
>
|
|
175
38
|
<EmailTemplateGenerator />
|
|
176
39
|
</ToolWorkspaceSection>
|
|
177
40
|
|
|
178
|
-
<
|
|
41
|
+
<ToolValueSection
|
|
179
42
|
heading={t("outputs")}
|
|
180
43
|
intro={t("outputsIntro")}
|
|
181
44
|
items={outputItems}
|
|
182
|
-
|
|
45
|
+
visual={
|
|
46
|
+
<ToolImageVisual
|
|
47
|
+
src="/tools/email-template-generator-value.webp"
|
|
48
|
+
alt={t("valueVisualAlt")}
|
|
49
|
+
wide
|
|
50
|
+
/>
|
|
51
|
+
}
|
|
183
52
|
/>
|
|
184
53
|
|
|
185
|
-
<
|
|
54
|
+
<ToolStandardsSection
|
|
186
55
|
heading={t("standards")}
|
|
187
56
|
intro={t("standardsIntro")}
|
|
188
57
|
items={standardsItems}
|
|
189
58
|
/>
|
|
190
59
|
|
|
191
|
-
<
|
|
60
|
+
<ToolBoundarySection
|
|
192
61
|
heading={t("boundary")}
|
|
193
62
|
intro={t("boundaryIntro")}
|
|
194
63
|
groups={boundaryGroups}
|
|
195
|
-
|
|
64
|
+
reverse
|
|
65
|
+
visual={
|
|
66
|
+
<ToolImageVisual
|
|
67
|
+
src="/tools/email-template-generator-support.webp"
|
|
68
|
+
alt={t("supportVisualAlt")}
|
|
69
|
+
/>
|
|
70
|
+
}
|
|
196
71
|
/>
|
|
197
72
|
|
|
198
|
-
<
|
|
73
|
+
<ToolSection
|
|
199
74
|
heading={t("guidance")}
|
|
200
75
|
intro={t("guidanceIntro")}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<ToolCardGrid items={useCases} />
|
|
211
|
-
</div>
|
|
212
|
-
</section>
|
|
76
|
+
visual={
|
|
77
|
+
<ToolImageVisual
|
|
78
|
+
src="/tools/email-template-generator-export.webp"
|
|
79
|
+
alt={t("exportVisualAlt")}
|
|
80
|
+
/>
|
|
81
|
+
}
|
|
82
|
+
>
|
|
83
|
+
<ToolTextRows items={guidance} />
|
|
84
|
+
</ToolSection>
|
|
213
85
|
|
|
86
|
+
<ToolUseCasesSection heading={t("useCases")} items={useCases} />
|
|
214
87
|
<ToolFaqSection heading={t("faq")} items={faqItems} />
|
|
215
|
-
|
|
216
88
|
<ToolCta
|
|
217
89
|
heading={t("ctaTitle")}
|
|
218
90
|
hint={t("ctaHint")}
|
|
@@ -5,7 +5,7 @@ export const emailTemplateGeneratorSpec: ToolSpec = {
|
|
|
5
5
|
toolType: "generator",
|
|
6
6
|
seo: {
|
|
7
7
|
"publishedAt": "2026-06-29",
|
|
8
|
-
"updatedAt": "2026-
|
|
8
|
+
"updatedAt": "2026-08-30",
|
|
9
9
|
"readingTime": "6 min",
|
|
10
10
|
"featured": false,
|
|
11
11
|
"ogImage": "/tools/email-template-generator-hero.webp"
|
|
@@ -20,20 +20,23 @@ import {
|
|
|
20
20
|
import { useLocale, useTranslations } from "../../../i18n/client";
|
|
21
21
|
import { isZhRouteLocale } from "../../../i18n/locale-fallback";
|
|
22
22
|
import type { AppLocale } from "../../../i18n/locale";
|
|
23
|
+
import { isRemoteStaticAsset, staticAsset } from "../../../lib/static-asset";
|
|
23
24
|
|
|
24
25
|
function deckTitle(entry: PitchDeckLibraryEntry, roundLabel: string): string {
|
|
25
26
|
return `${entry.company} ${roundLabel} Pitch Deck (${entry.year})`;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function DeckCover({ entry, alt }: { entry: PitchDeckLibraryEntry; alt: string }) {
|
|
30
|
+
const src = staticAsset(`${PITCH_DECK_LIBRARY_COVER_BASE}/${entry.slug}-cover.webp`);
|
|
29
31
|
return (
|
|
30
32
|
<div className="relative aspect-video w-full overflow-hidden bg-[#EEEAE0]">
|
|
31
33
|
<Image
|
|
32
|
-
src={
|
|
34
|
+
src={src}
|
|
33
35
|
alt={alt}
|
|
34
36
|
fill
|
|
35
37
|
sizes="(min-width: 1280px) 25vw, (min-width: 768px) 33vw, 100vw"
|
|
36
38
|
className="object-cover"
|
|
39
|
+
unoptimized={isRemoteStaticAsset(src)}
|
|
37
40
|
/>
|
|
38
41
|
<span className="absolute left-3 top-3 rounded-md bg-[#111111] px-2 py-1 text-[11px] font-semibold tracking-wide text-white">
|
|
39
42
|
BP
|
|
@@ -98,21 +101,25 @@ export function PitchDeckLibrary() {
|
|
|
98
101
|
<p className="text-sm leading-6 text-[#555555]">{activeCopy.takeaway}</p>
|
|
99
102
|
{active.pages.length ? (
|
|
100
103
|
<div className="mt-4 grid gap-4">
|
|
101
|
-
{active.pages.map((page, index) =>
|
|
104
|
+
{active.pages.map((page, index) => {
|
|
105
|
+
const src = staticAsset(page);
|
|
106
|
+
return (
|
|
102
107
|
<div
|
|
103
108
|
key={page}
|
|
104
109
|
className="overflow-hidden rounded-[0.75rem] border border-[#E1DDD3] bg-[#EEEAE0]"
|
|
105
110
|
>
|
|
106
111
|
<Image
|
|
107
|
-
src={
|
|
112
|
+
src={src}
|
|
108
113
|
alt={t("pageAlt", { company: active.company, page: index + 1 })}
|
|
109
114
|
width={1440}
|
|
110
115
|
height={1080}
|
|
111
116
|
sizes="(min-width: 1024px) 960px, 100vw"
|
|
112
117
|
className="h-auto w-full"
|
|
118
|
+
unoptimized={isRemoteStaticAsset(src)}
|
|
113
119
|
/>
|
|
114
120
|
</div>
|
|
115
|
-
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
116
123
|
</div>
|
|
117
124
|
) : (
|
|
118
125
|
<div className="mt-4 grid gap-4">
|
|
@@ -144,7 +151,7 @@ export function PitchDeckLibrary() {
|
|
|
144
151
|
type="button"
|
|
145
152
|
className="min-h-9 bg-[#111111] text-white hover:bg-[#2A2A2A]"
|
|
146
153
|
>
|
|
147
|
-
<a href={active.downloadUrl} download>
|
|
154
|
+
<a href={staticAsset(active.downloadUrl)} download>
|
|
148
155
|
<Download className="size-4" aria-hidden />
|
|
149
156
|
{t("download")}
|
|
150
157
|
</a>
|
|
@@ -5,7 +5,7 @@ export const productNameGeneratorSpec: ToolSpec = {
|
|
|
5
5
|
toolType: "generator",
|
|
6
6
|
seo: {
|
|
7
7
|
"publishedAt": "2026-06-25",
|
|
8
|
-
"updatedAt": "2026-08-
|
|
8
|
+
"updatedAt": "2026-08-30",
|
|
9
9
|
"readingTime": "6 min",
|
|
10
10
|
"featured": true,
|
|
11
11
|
"ogImage": "/tools/product-name-generator-section-primary-8bit-v2.webp",
|
|
@@ -6,7 +6,7 @@ export const productPosterGeneratorSpec: ToolSpec = {
|
|
|
6
6
|
riskLevel: "medium",
|
|
7
7
|
seo: {
|
|
8
8
|
"publishedAt": "2026-06-29",
|
|
9
|
-
"updatedAt": "2026-
|
|
9
|
+
"updatedAt": "2026-08-30",
|
|
10
10
|
"readingTime": "6 min",
|
|
11
11
|
"featured": false,
|
|
12
12
|
"ogImage": "/tools/product-poster-generator-section-primary-8bit-v2.webp"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import Image from "next/image";
|
|
3
3
|
|
|
4
|
+
import { isRemoteStaticAsset, staticAsset } from "../../../lib/static-asset";
|
|
5
|
+
|
|
4
6
|
type VisualTone = "lime" | "blue" | "coral" | "wine";
|
|
5
7
|
|
|
6
8
|
const toneStyles: Record<
|
|
@@ -171,14 +173,16 @@ export function ToolImageVisual({
|
|
|
171
173
|
priority?: boolean;
|
|
172
174
|
wide?: boolean;
|
|
173
175
|
}) {
|
|
176
|
+
const resolved = staticAsset(src);
|
|
174
177
|
return (
|
|
175
178
|
<figure className="overflow-hidden rounded-[1.5rem]">
|
|
176
179
|
<Image
|
|
177
|
-
src={
|
|
180
|
+
src={resolved}
|
|
178
181
|
alt={alt}
|
|
179
182
|
width={1672}
|
|
180
183
|
height={941}
|
|
181
184
|
priority={priority}
|
|
185
|
+
unoptimized={isRemoteStaticAsset(resolved)}
|
|
182
186
|
sizes={
|
|
183
187
|
wide
|
|
184
188
|
? "(min-width: 1024px) 1120px, 100vw"
|
|
@@ -5,6 +5,7 @@ import type { ReactNode } from "react";
|
|
|
5
5
|
import { relatedToolSlugsByTool } from "../../../constants/related-tools";
|
|
6
6
|
import { RelatedTools } from "../../../host/server";
|
|
7
7
|
import type { AppLocale } from "../../../i18n/locale";
|
|
8
|
+
import { isRemoteStaticAsset, staticAsset } from "../../../lib/static-asset";
|
|
8
9
|
import { resolveToolLocaleCopy } from "../../../lib/tools/resolve-tool-locale-copy";
|
|
9
10
|
|
|
10
11
|
import type { ToolSpec } from "../types";
|
|
@@ -63,6 +64,9 @@ export async function ToolPageShell({
|
|
|
63
64
|
// Full messages: public getRequestConfig omits toolsPages; client tools layout alone is not enough for RSC.
|
|
64
65
|
const messages = await loadToolMessages(locale);
|
|
65
66
|
const t = createToolMessageTranslator(locale, messages, "toolsPages.shared");
|
|
67
|
+
const resolvedHeroBackground = heroBackgroundImage
|
|
68
|
+
? staticAsset(heroBackgroundImage)
|
|
69
|
+
: undefined;
|
|
66
70
|
|
|
67
71
|
return (
|
|
68
72
|
<div className="min-h-screen overflow-x-clip bg-white text-[#111111]">
|
|
@@ -73,28 +77,30 @@ export async function ToolPageShell({
|
|
|
73
77
|
className="relative w-full overflow-hidden text-[#2B211A]"
|
|
74
78
|
style={{
|
|
75
79
|
minHeight: "min(72svh, 680px)",
|
|
76
|
-
background:
|
|
80
|
+
background: resolvedHeroBackground ? heroBackgroundColor : "#F4E6D8",
|
|
77
81
|
}}
|
|
78
82
|
>
|
|
79
|
-
{
|
|
83
|
+
{resolvedHeroBackground ? (
|
|
80
84
|
<>
|
|
81
85
|
{heroBackgroundImageFrameClassName ? (
|
|
82
86
|
<div className={`absolute ${heroBackgroundImageFrameClassName}`}>
|
|
83
87
|
<Image
|
|
84
|
-
src={
|
|
88
|
+
src={resolvedHeroBackground}
|
|
85
89
|
alt={heroBackgroundAlt}
|
|
86
90
|
fill
|
|
87
91
|
priority
|
|
92
|
+
unoptimized={isRemoteStaticAsset(resolvedHeroBackground)}
|
|
88
93
|
sizes={heroBackgroundImageSizes}
|
|
89
94
|
className={`pointer-events-none ${heroBackgroundImageClassName}`}
|
|
90
95
|
/>
|
|
91
96
|
</div>
|
|
92
97
|
) : (
|
|
93
98
|
<Image
|
|
94
|
-
src={
|
|
99
|
+
src={resolvedHeroBackground}
|
|
95
100
|
alt={heroBackgroundAlt}
|
|
96
101
|
fill
|
|
97
102
|
priority
|
|
103
|
+
unoptimized={isRemoteStaticAsset(resolvedHeroBackground)}
|
|
98
104
|
sizes={heroBackgroundImageSizes}
|
|
99
105
|
className={`pointer-events-none ${heroBackgroundImageClassName}`}
|
|
100
106
|
/>
|
|
@@ -5,6 +5,7 @@ import Image from "next/image";
|
|
|
5
5
|
|
|
6
6
|
import { Button } from "@tomako/ui/button";
|
|
7
7
|
|
|
8
|
+
import { isRemoteStaticAsset, staticAsset } from "../../../lib/static-asset";
|
|
8
9
|
import { ToolStandardGenerationSteps } from "./tool-standard-generation-steps";
|
|
9
10
|
|
|
10
11
|
export type ToolStandardBenefitCard = {
|
|
@@ -641,6 +642,7 @@ function ToolStandardImageFrame({
|
|
|
641
642
|
priority?: boolean;
|
|
642
643
|
className?: string;
|
|
643
644
|
}) {
|
|
645
|
+
const resolved = staticAsset(src);
|
|
644
646
|
return (
|
|
645
647
|
<figure className={`mx-auto w-full min-w-0 max-w-[760px] overflow-hidden rounded-2xl border border-[#40576D]/[0.07] bg-white xl:max-w-none ${className ?? ""}`}>
|
|
646
648
|
<Image
|
|
@@ -649,7 +651,8 @@ function ToolStandardImageFrame({
|
|
|
649
651
|
height={840}
|
|
650
652
|
priority={priority}
|
|
651
653
|
sizes="(min-width: 1536px) 630px, (min-width: 1280px) 45vw, (min-width: 768px) 760px, calc(100vw - 40px)"
|
|
652
|
-
src={
|
|
654
|
+
src={resolved}
|
|
655
|
+
unoptimized={isRemoteStaticAsset(resolved)}
|
|
653
656
|
width={1260}
|
|
654
657
|
/>
|
|
655
658
|
</figure>
|
|
@@ -35,7 +35,7 @@ export const sloganGeneratorPackage: ToolPackage = {
|
|
|
35
35
|
toolType: "generator",
|
|
36
36
|
keywordEvidence: "docs/seo-keyword-briefs/slogan-generator.md",
|
|
37
37
|
seo: {
|
|
38
|
-
updatedAt: "2026-
|
|
38
|
+
updatedAt: "2026-08-30",
|
|
39
39
|
readingTime: "6 min",
|
|
40
40
|
ogImage: "/tools/slogan-generator-hero.webp",
|
|
41
41
|
},
|
|
@@ -247,6 +247,78 @@ Quality requirements:
|
|
|
247
247
|
},
|
|
248
248
|
badge: { en: "Free, no sign-up", zh: "免费免注册", "zh-tw": "免費免註冊" },
|
|
249
249
|
},
|
|
250
|
+
standard: {
|
|
251
|
+
benefits: {
|
|
252
|
+
title: {
|
|
253
|
+
en: "Compare distinct slogan directions, not filler variations",
|
|
254
|
+
zh: "比较真正不同的 Slogan 方向,而不是凑数变体",
|
|
255
|
+
"zh-tw": "比較真正不同的 Slogan 方向,而不是湊數變體",
|
|
256
|
+
},
|
|
257
|
+
cards: [
|
|
258
|
+
{
|
|
259
|
+
title: { en: "10 varied candidates", zh: "10 条多风格候选", "zh-tw": "10 條多風格候選" },
|
|
260
|
+
body: { en: "Compare bold, minimal, playful, premium, and benefit-led lines side by side.", zh: "并排比较大胆、极简、俏皮、高端和利益导向的表达。", "zh-tw": "並排比較大膽、極簡、俏皮、高端和利益導向的表達。" },
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
title: { en: "Reasons you can judge", zh: "每条都有判断依据", "zh-tw": "每條都有判斷依據" },
|
|
264
|
+
body: { en: "Each line explains the rhythm, contrast, specificity, or promise that makes it work.", zh: "每条都说明节奏、对比、具体性或承诺为什么成立。", "zh-tw": "每條都說明節奏、對比、具體性或承諾為什麼成立。" },
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
title: { en: "A shortlist with next steps", zh: "带下一步的候选清单", "zh-tw": "帶下一步的候選清單" },
|
|
268
|
+
body: { en: "Use read-aloud, recall, trademark, and domain checks before choosing a final line.", zh: "定稿前继续做朗读、记忆、商标和域名检查。", "zh-tw": "定稿前繼續做朗讀、記憶、商標和網域檢查。" },
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
imageTextSections: [
|
|
273
|
+
{
|
|
274
|
+
id: "section-primary",
|
|
275
|
+
title: { en: "Start with a brief the generator can understand", zh: "先给生成器一份能读懂的业务简介", "zh-tw": "先給產生器一份能讀懂的業務簡介" },
|
|
276
|
+
body: {
|
|
277
|
+
en: ["State who the business serves and what changes for them.", "Add the brand name only when you want name-based wordplay; vague one-word prompts produce generic lines."],
|
|
278
|
+
zh: ["写清业务服务谁,以及为他们带来什么变化。", "只有需要名字相关的文字游戏时才填写品牌名;单个空泛词只会得到通用句子。"],
|
|
279
|
+
"zh-tw": ["寫清業務服務誰,以及為他們帶來什麼變化。", "只有需要名字相關的文字遊戲時才填寫品牌名;單一空泛詞只會得到通用句子。"],
|
|
280
|
+
},
|
|
281
|
+
image: { src: "/tools/slogan-generator-value.webp", alt: { en: "A concise slogan standing out from vague drafts.", zh: "一句清晰口号从空泛草案中脱颖而出。", "zh-tw": "一句清晰口號從空泛草案中脫穎而出。" }, side: "left" },
|
|
282
|
+
imageFirst: true,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: "section-variation",
|
|
286
|
+
title: { en: "Compare styles before polishing words", zh: "先比较风格方向,再打磨具体词句", "zh-tw": "先比較風格方向,再打磨具體詞句" },
|
|
287
|
+
body: {
|
|
288
|
+
en: ["Shortlist candidates from different styles rather than near-duplicates.", "Read them aloud and test recall beside the logo with people who do not already know the brand."],
|
|
289
|
+
zh: ["从不同风格中挑选候选,不要只留同一个想法的近似变体。", "逐条朗读,并把它们和 Logo 放在一起,让不了解品牌的人测试记忆度。"],
|
|
290
|
+
"zh-tw": ["從不同風格中挑選候選,不要只留同一個想法的近似變體。", "逐條朗讀,並把它們和 Logo 放在一起,讓不了解品牌的人測試記憶度。"],
|
|
291
|
+
},
|
|
292
|
+
image: { src: "/tools/slogan-generator-standard.webp", alt: { en: "Slogan candidates compared for fit and memorability.", zh: "按贴合度和记忆度比较口号候选。", "zh-tw": "按貼合度和記憶度比較口號候選。" }, side: "right" },
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
id: "section-support",
|
|
296
|
+
title: { en: "Treat every result as a draft to clear", zh: "把每条结果当成仍需检查的草案", "zh-tw": "把每條結果當成仍需檢查的草案" },
|
|
297
|
+
body: {
|
|
298
|
+
en: ["The tool does not check trademarks, domains, or social handles.", "Search the exact phrase in your market and reject lines that overpromise what the product can deliver."],
|
|
299
|
+
zh: ["工具不会检查商标、域名或社媒账号占用。", "请在目标市场搜索候选原文,并淘汰任何超出产品真实能力的承诺。"],
|
|
300
|
+
"zh-tw": ["工具不會檢查商標、網域或社群帳號佔用。", "請在目標市場搜尋候選原文,並淘汰任何超出產品真實能力的承諾。"],
|
|
301
|
+
},
|
|
302
|
+
image: { src: "/tools/slogan-generator-support.webp", alt: { en: "A slogan shortlist passing brand and trademark checks.", zh: "口号候选清单经过品牌和商标检查。", "zh-tw": "口號候選清單經過品牌和商標檢查。" }, side: "left" },
|
|
303
|
+
imageFirst: true,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
generation: {
|
|
307
|
+
title: { en: "Move from candidate list to a tested slogan", zh: "从候选清单走到经过测试的最终口号", "zh-tw": "從候選清單走到經過測試的最終口號" },
|
|
308
|
+
image: { src: "/tools/slogan-generator-result-use.webp", alt: { en: "A shortlisted slogan tested beside a logo and landing page.", zh: "入围口号与 Logo 和落地页一起测试。", "zh-tw": "入圍口號與 Logo 和網站首頁一起測試。" }, side: "left" },
|
|
309
|
+
steps: [
|
|
310
|
+
{ title: { en: "Shortlist three directions", zh: "选出三个不同方向", "zh-tw": "選出三個不同方向" }, body: { en: "Keep candidates that make one clear, honest promise.", zh: "保留只表达一个清晰、诚实承诺的候选。", "zh-tw": "保留只表達一個清晰、誠實承諾的候選。" } },
|
|
311
|
+
{ title: { en: "Test sound and recall", zh: "测试读感和记忆度", "zh-tw": "測試讀感和記憶度" }, body: { en: "Read each aloud and ask unfamiliar readers what they remember later.", zh: "逐条朗读,并询问陌生读者过一段时间后还记得哪条。", "zh-tw": "逐條朗讀,並詢問陌生讀者過一段時間後還記得哪條。" } },
|
|
312
|
+
{ title: { en: "Clear conflicts before launch", zh: "上线前排查冲突", "zh-tw": "上線前排查衝突" }, body: { en: "Check exact-phrase search, trademarks, domains, and handles before committing.", zh: "定稿前检查原文搜索、商标、域名和账号占用。", "zh-tw": "定稿前檢查原文搜尋、商標、網域和帳號佔用。" } },
|
|
313
|
+
],
|
|
314
|
+
ctaLabel: { en: "Generate slogan candidates", zh: "生成 Slogan 候选", "zh-tw": "產生 Slogan 候選" },
|
|
315
|
+
},
|
|
316
|
+
faq: {
|
|
317
|
+
title: { en: "Slogan generator FAQs", zh: "Slogan 生成器常见问题", "zh-tw": "Slogan 產生器常見問題" },
|
|
318
|
+
intro: { en: "Generate a broad shortlist, then test and clear the final phrase before using it.", zh: "先生成覆盖不同方向的候选,再测试并检查最终短语。", "zh-tw": "先產生涵蓋不同方向的候選,再測試並檢查最終短語。" },
|
|
319
|
+
ctaLabel: { en: "Create slogan candidates", zh: "生成 Slogan 候选", "zh-tw": "產生 Slogan 候選" },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
250
322
|
sections: [
|
|
251
323
|
{
|
|
252
324
|
kind: "cards",
|
|
@@ -40,14 +40,13 @@ export async function TwitterGifDownloaderContainer({ locale, spec }: ToolPagePr
|
|
|
40
40
|
<ToolPageShell
|
|
41
41
|
locale={locale}
|
|
42
42
|
spec={spec}
|
|
43
|
-
|
|
44
|
-
heroBackgroundImage="/tools/twitter-gif-downloader-hero.webp"
|
|
45
|
-
heroBackgroundAlt={t("heroVisualAlt")}
|
|
43
|
+
showDefaultHeader={false}
|
|
46
44
|
>
|
|
47
45
|
<ToolWorkspaceSection
|
|
48
46
|
heading={t("workspaceHeading")}
|
|
49
47
|
hint={t("workspaceHint")}
|
|
50
48
|
badge={t("workspaceBadge")}
|
|
49
|
+
headingLevel="h1"
|
|
51
50
|
>
|
|
52
51
|
<TwitterGifDownloader />
|
|
53
52
|
</ToolWorkspaceSection>
|
|
@@ -5,7 +5,7 @@ export const twitterGifDownloaderSpec: ToolSpec = {
|
|
|
5
5
|
toolType: "downloader",
|
|
6
6
|
seo: {
|
|
7
7
|
"publishedAt": "2026-06-29",
|
|
8
|
-
"updatedAt": "2026-08-
|
|
8
|
+
"updatedAt": "2026-08-30",
|
|
9
9
|
"readingTime": "5 min",
|
|
10
10
|
"featured": false,
|
|
11
11
|
"ogImage": "/tools/twitter-gif-downloader-hero.webp"
|
|
@@ -16,7 +16,7 @@ const appStoreScreenshotGenerator = {
|
|
|
16
16
|
guide: {
|
|
17
17
|
heroVisualAlt:
|
|
18
18
|
"Hero visual for an app store listing image generator, showing a complete app store screenshot set.",
|
|
19
|
-
workspaceHeading: "
|
|
19
|
+
workspaceHeading: "App Store Screenshot Generator",
|
|
20
20
|
workspaceHint:
|
|
21
21
|
"Upload real UI screenshots. A cloud Agent uses Skills-OL to fetch supporting context, plan the complete set, and submit live image tasks.",
|
|
22
22
|
whatYouGetHeading: "What you get after generation",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const coldStartChannelSelector = {
|
|
2
2
|
meta: {
|
|
3
3
|
title: "Growth Channel Strategy Generator",
|
|
4
|
-
description: "Build
|
|
4
|
+
description: "Build an early-product growth channel strategy. Add your product, audience, time, and budget to choose the first test and the signal that guides the next step.",
|
|
5
5
|
intro: "Choose the first growth channel to test, then use real signals to decide what comes next.",
|
|
6
6
|
category: "Growth strategy tools",
|
|
7
7
|
keywords: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const competitorAnalysis = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "Competitor Analysis Tool",
|
|
4
|
-
description: "Enter a competitor
|
|
3
|
+
title: "AI Competitor Analysis Tool: Sources",
|
|
4
|
+
description: "Enter a competitor URL and get a sourced profile of its product, traffic, growth, business scale, team, and funding, with confidence shown for each finding.",
|
|
5
5
|
intro: "Turn a single competitor URL into a one-page profile. Built from public web and search signals, every dimension shows its confidence so you can act on positioning, growth, and competitive decisions.",
|
|
6
6
|
category: "Market research tools",
|
|
7
7
|
keywords: [
|
|
@@ -126,6 +126,9 @@ const competitorAnalysis = {
|
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
128
|
guide: {
|
|
129
|
+
workspaceHeading: "AI Competitor Analysis Tool",
|
|
130
|
+
workspaceHint: "Enter a public competitor URL to build a sourced profile with confidence and unknowns shown for every major finding.",
|
|
131
|
+
workspaceBadge: "Public sources only",
|
|
129
132
|
visualAlt: {
|
|
130
133
|
hero: "Competitor profile report preview (visual pending)",
|
|
131
134
|
howToUse: "Competitor analysis workflow illustration (visual pending)",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const emailTemplateGenerator = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "Email Template Generator",
|
|
4
|
-
description: "Turn marketing or
|
|
3
|
+
title: "MJML Email Template Generator",
|
|
4
|
+
description: "Turn a marketing or transactional email brief into an MJML-first package with HTML preview, plain text, variables, review notes, and developer handoff.",
|
|
5
5
|
intro: "Convert a rough email brief into a previewable, revisable, export-ready MJML template package for campaigns, verification codes, receipts, subscription confirmations, and account notifications.",
|
|
6
6
|
category: "Marketing tools",
|
|
7
7
|
keywords: [
|
|
@@ -16,7 +16,7 @@ const emailTemplateGenerator = {
|
|
|
16
16
|
},
|
|
17
17
|
guide: {
|
|
18
18
|
heroImageAlt: "Visual of a rough email brief becoming MJML, HTML, plain text, variables, and an export bundle.",
|
|
19
|
-
workspaceHeading: "
|
|
19
|
+
workspaceHeading: "MJML Email Template Generator",
|
|
20
20
|
workspaceHint:
|
|
21
21
|
"Describe the email goal, audience, scenario, and brand tone. The Agent returns a structured MJML-first package you can preview, revise, copy, or export.",
|
|
22
22
|
workspaceBadge: "MJML package",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const productPosterGenerator = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "AI Product Poster Generator
|
|
3
|
+
title: "AI Product Poster Generator for SaaS",
|
|
4
4
|
displayTitle: "AI Product Poster Generator",
|
|
5
5
|
description:
|
|
6
|
-
"
|
|
6
|
+
"Add a product name, benefits, audience, CTA, and optional screenshots to generate a review-ready promo poster for ads, social posts, and websites.",
|
|
7
7
|
intro: "Turn product context into a downloadable promo poster.",
|
|
8
8
|
category: "SEO marketing tools",
|
|
9
9
|
keywords: [
|
|
@@ -44,7 +44,7 @@ const twitterGifDownloader = {
|
|
|
44
44
|
},
|
|
45
45
|
guide: {
|
|
46
46
|
heroVisualAlt: "Hero background showing a public X post link becoming GIF and MP4 downloads",
|
|
47
|
-
workspaceHeading: "
|
|
47
|
+
workspaceHeading: "Twitter GIF Downloader for X Posts",
|
|
48
48
|
workspaceHint: "Paste a public /status/ link to preview convertible media and save a GIF or MP4. Private, restricted, deleted, or media-free posts will not work.",
|
|
49
49
|
workspaceBadge: "Public link → GIF or MP4",
|
|
50
50
|
resultHeading: "What you get",
|
|
@@ -16,7 +16,7 @@ const appStoreScreenshotGenerator = {
|
|
|
16
16
|
guide: {
|
|
17
17
|
heroVisualAlt:
|
|
18
18
|
"应用商店上架图生成器 Hero 视觉图,展示一套应用商店截图。",
|
|
19
|
-
workspaceHeading: "
|
|
19
|
+
workspaceHeading: "App Store / Google Play 上架截图生成器",
|
|
20
20
|
workspaceHint:
|
|
21
21
|
"上传真实 UI 截图后,云端 Agent 会通过 Skills-OL 抓取补充上下文、规划完整套图并提交真实生图任务。",
|
|
22
22
|
whatYouGetHeading: "生成后会得到什么",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const competitorAnalysis = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "竞品分析工具",
|
|
3
|
+
title: "AI 竞品分析工具",
|
|
4
4
|
description: "输入一个竞品的网址,云端 Agent 调研公开信息,输出结构化的竞品档案:产品概览、流量与增长、业务规模、团队与融资,并附来源和可信度。",
|
|
5
5
|
intro: "把一个竞品 URL 变成一页看懂的竞品档案。基于公开网页和检索的估算,每个维度都标注可信度,帮助你做定位、增长和竞争决策。",
|
|
6
6
|
category: "市场调研工具",
|
|
@@ -124,6 +124,9 @@ const competitorAnalysis = {
|
|
|
124
124
|
},
|
|
125
125
|
},
|
|
126
126
|
guide: {
|
|
127
|
+
workspaceHeading: "AI 竞品分析工具",
|
|
128
|
+
workspaceHint: "输入竞品公开网址,生成带来源的结构化档案,并为主要发现标注可信度与未知项。",
|
|
129
|
+
workspaceBadge: "仅使用公开来源",
|
|
127
130
|
visualAlt: {
|
|
128
131
|
hero: "竞品档案报告预览(配图待补)",
|
|
129
132
|
howToUse: "竞品分析使用流程示意(配图待补)",
|
|
@@ -16,7 +16,7 @@ const emailTemplateGenerator = {
|
|
|
16
16
|
},
|
|
17
17
|
guide: {
|
|
18
18
|
heroImageAlt: "粗略邮件需求转成 MJML、HTML、纯文本、变量和导出包的视觉图。",
|
|
19
|
-
workspaceHeading: "
|
|
19
|
+
workspaceHeading: "MJML 邮件模板生成器",
|
|
20
20
|
workspaceHint:
|
|
21
21
|
"描述邮件目标、受众、场景和品牌语气,Agent 会返回结构化的 MJML-first 邮件模板包,可预览、修改、复制和导出。",
|
|
22
22
|
workspaceBadge: "MJML 模板包",
|
|
@@ -40,7 +40,7 @@ const twitterGifDownloader = {
|
|
|
40
40
|
},
|
|
41
41
|
guide: {
|
|
42
42
|
heroVisualAlt: "公开 X 帖子链接转换成 GIF 和 MP4 下载文件的 Hero 背景图",
|
|
43
|
-
workspaceHeading: "
|
|
43
|
+
workspaceHeading: "Twitter / X GIF 下载器",
|
|
44
44
|
workspaceHint: "粘贴带 /status/ 的公开帖子链接,预览可转换媒体后保存 GIF 或 MP4。私密、受限、已删除或没有媒体的帖子无法使用。",
|
|
45
45
|
workspaceBadge: "公开链接 → GIF 或 MP4",
|
|
46
46
|
resultHeading: "你会得到什么",
|
|
@@ -16,7 +16,7 @@ const appStoreScreenshotGenerator = {
|
|
|
16
16
|
guide: {
|
|
17
17
|
heroVisualAlt:
|
|
18
18
|
"應用商店上架圖生成器 Hero 視覺圖,展示一套應用商店截圖。",
|
|
19
|
-
workspaceHeading: "
|
|
19
|
+
workspaceHeading: "App Store / Google Play 上架截圖生成器",
|
|
20
20
|
workspaceHint:
|
|
21
21
|
"上傳真實 UI 截圖後,雲端 Agent 會通過 Skills-OL 抓取補充上下文、規劃完整套圖並提交真實生圖任務。",
|
|
22
22
|
whatYouGetHeading: "生成後會得到什麼",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const competitorAnalysis = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "競品分析工具",
|
|
3
|
+
title: "AI 競品分析工具",
|
|
4
4
|
description: "輸入一個競品的網址,雲端 Agent 調研公開信息,輸出結構化的競品檔案:產品概覽、流量與增長、業務規模、團隊與融資,並附來源和可信度。",
|
|
5
5
|
intro: "把一個競品 URL 變成一頁看懂的競品檔案。基於公開網頁和檢索的估算,每個維度都標註可信度,幫助你做定位、增長和競爭決策。",
|
|
6
6
|
category: "市場調研工具",
|
|
@@ -124,6 +124,9 @@ const competitorAnalysis = {
|
|
|
124
124
|
},
|
|
125
125
|
},
|
|
126
126
|
guide: {
|
|
127
|
+
workspaceHeading: "AI 競品分析工具",
|
|
128
|
+
workspaceHint: "輸入競品公開網址,產生帶來源的結構化檔案,並為主要發現標註可信度與未知項。",
|
|
129
|
+
workspaceBadge: "僅使用公開來源",
|
|
127
130
|
visualAlt: {
|
|
128
131
|
hero: "競品檔案報告預覽(配圖待補)",
|
|
129
132
|
howToUse: "競品分析使用流程示意(配圖待補)",
|
|
@@ -16,7 +16,7 @@ const emailTemplateGenerator = {
|
|
|
16
16
|
},
|
|
17
17
|
guide: {
|
|
18
18
|
heroImageAlt: "粗略郵件需求轉成 MJML、HTML、純文本、變量和導出包的視覺圖。",
|
|
19
|
-
workspaceHeading: "
|
|
19
|
+
workspaceHeading: "MJML 郵件模板產生器",
|
|
20
20
|
workspaceHint:
|
|
21
21
|
"描述郵件目標、受眾、場景和品牌語氣,Agent 會返回結構化的 MJML-first 郵件模板包,可預覽、修改、複製和導出。",
|
|
22
22
|
workspaceBadge: "MJML 模板包",
|
|
@@ -40,7 +40,7 @@ const twitterGifDownloader = {
|
|
|
40
40
|
},
|
|
41
41
|
guide: {
|
|
42
42
|
heroVisualAlt: "公開 X 帖子鏈接轉換成 GIF 和 MP4 下載文件的 Hero 背景圖",
|
|
43
|
-
workspaceHeading: "
|
|
43
|
+
workspaceHeading: "Twitter / X GIF 下載器",
|
|
44
44
|
workspaceHint: "貼上帶 /status/ 的公開帖子連結,預覽可轉換媒體後保存 GIF 或 MP4。私密、受限、已刪除或沒有媒體的帖子無法使用。",
|
|
45
45
|
workspaceBadge: "公開連結 → GIF 或 MP4",
|
|
46
46
|
resultHeading: "你會得到什麼",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve root-relative marketing asset paths to the static CDN.
|
|
3
|
+
* Mirrors `@tomako/public-chrome` staticAsset so tools-runtime stays free of
|
|
4
|
+
* a chrome peer dependency.
|
|
5
|
+
*
|
|
6
|
+
* Image styles (`!960` etc.) follow Tomako OSS image guidelines — append only
|
|
7
|
+
* when serving from STATIC_ORIGIN; do not rename object keys to `.webp`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const STYLE_SUFFIX_PATTERN = /!(default|256|640|960|1600|2560)$/;
|
|
11
|
+
const RASTER_EXT = /\.(png|jpe?g|webp|gif|bmp|tiff?)$/i;
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_STATIC_ORIGIN = "https://static.tomako.ai";
|
|
14
|
+
|
|
15
|
+
export type OssImageStyle =
|
|
16
|
+
| "default"
|
|
17
|
+
| "256"
|
|
18
|
+
| "640"
|
|
19
|
+
| "960"
|
|
20
|
+
| "1600"
|
|
21
|
+
| "2560";
|
|
22
|
+
|
|
23
|
+
function readStaticOrigin(): string {
|
|
24
|
+
const raw = process.env.NEXT_PUBLIC_STATIC_ORIGIN;
|
|
25
|
+
if (raw !== undefined) {
|
|
26
|
+
const value = raw.trim().replace(/\/$/, "");
|
|
27
|
+
if (value.length > 0) return value;
|
|
28
|
+
}
|
|
29
|
+
return DEFAULT_STATIC_ORIGIN;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function staticAsset(path: string, style?: OssImageStyle): string {
|
|
33
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
34
|
+
throw new Error("staticAsset(path): path is required");
|
|
35
|
+
}
|
|
36
|
+
if (/^https?:\/\//i.test(path) || path.startsWith("//")) {
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
41
|
+
const origin = readStaticOrigin();
|
|
42
|
+
const absolute = `${origin}/static${normalized}`;
|
|
43
|
+
if (!style || !RASTER_EXT.test(normalized.replace(STYLE_SUFFIX_PATTERN, ""))) {
|
|
44
|
+
return absolute;
|
|
45
|
+
}
|
|
46
|
+
return `${absolute.replace(STYLE_SUFFIX_PATTERN, "")}!${style}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** True when src is an absolute CDN/http URL (skip Next image optimizer). */
|
|
50
|
+
export function isRemoteStaticAsset(src: string): boolean {
|
|
51
|
+
return /^https?:\/\//i.test(src) || src.startsWith("//");
|
|
52
|
+
}
|
|
@@ -47,6 +47,15 @@ export function getEffectiveStatus(
|
|
|
47
47
|
return getOpsRow(catalog, slug)?.status ?? "draft";
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Search-index eligibility is stricter than public detail access. */
|
|
51
|
+
export function isToolIndexable(
|
|
52
|
+
slug: string,
|
|
53
|
+
catalog: ToolOpsRecord[],
|
|
54
|
+
): boolean {
|
|
55
|
+
const row = getOpsRow(catalog, slug);
|
|
56
|
+
return row?.status === "published" && row.showOnIndex;
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
export async function getEffectiveStatusForSlug(slug: string): Promise<ToolStatus> {
|
|
51
60
|
if (!getToolSpec(slug)) {
|
|
52
61
|
return "draft";
|
|
@@ -64,7 +73,7 @@ export async function listIndexedToolSpecs(): Promise<ToolSpec[]> {
|
|
|
64
73
|
const specsBySlug = new Map(listToolSpecs().map((spec) => [spec.slug, spec]));
|
|
65
74
|
|
|
66
75
|
return catalog
|
|
67
|
-
.filter((row) => row.
|
|
76
|
+
.filter((row) => isToolIndexable(row.slug, catalog))
|
|
68
77
|
.sort(
|
|
69
78
|
(left, right) =>
|
|
70
79
|
left.indexSort - right.indexSort ||
|