Skip to content

Commit ce72079

Browse files
authored
Merge pull request #2573 from appwrite/feat/blog-mid-cta-automation
Add automatic mid-article CTAs with category-aware copy
2 parents f938bf5 + fbab5e7 commit ce72079

File tree

7 files changed

+532
-20
lines changed

7 files changed

+532
-20
lines changed

src/lib/components/BlogCta.svelte

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,30 @@
22
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
33
import { Button } from '$lib/components/ui';
44
5-
export let heading: string = 'Start building with Appwrite today';
6-
export let label: string = 'Get started';
5+
let {
6+
heading = 'Start building with Appwrite today',
7+
label = 'Get started',
8+
description = undefined,
9+
href = getAppwriteDashboardUrl(),
10+
event = 'blog-cta-get_started_btn-click'
11+
}: {
12+
heading?: string;
13+
label?: string;
14+
description?: string;
15+
href?: string;
16+
event?: string;
17+
} = $props();
718
</script>
819

920
<div
10-
class="bg relative mt-12 -mb-6 flex min-h-[12rem] items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12"
21+
class="bg relative mt-12 -mb-6 flex min-h-48 items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12"
1122
>
1223
<div class="flex max-w-3xs flex-col items-center justify-center gap-5 text-center">
1324
<h2 class="text-label text-primary font-aeonik-pro">{heading}</h2>
14-
<Button href={getAppwriteDashboardUrl()} event="blog-cta-get_started_btn-click"
15-
>{label}</Button
16-
>
25+
{#if description}
26+
<p class="text-main-body text-muted-foreground">{description}</p>
27+
{/if}
28+
<Button {href} {event}>{label}</Button>
1729
</div>
1830
</div>
1931

src/lib/utils/blog-cta.ts

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
2+
3+
export type BlogCtaConfig = {
4+
heading: string;
5+
label: string;
6+
href: string;
7+
description: string;
8+
event?: string;
9+
points: [string, string, string, string];
10+
};
11+
12+
type ResolverParams = {
13+
slug: string;
14+
rawContent?: string | null;
15+
};
16+
17+
type CategoryResolver = (params: ResolverParams) => BlogCtaConfig;
18+
19+
export const DEFAULT_CTA_POINTS: [string, string, string, string] = [
20+
'Start for free',
21+
'Open source',
22+
'Support for over 13 SDKs',
23+
'Managed cloud solution'
24+
] as const;
25+
26+
const DEVELOPERS_CLOUD_CTA: BlogCtaConfig = {
27+
heading: 'Build fast, scale faster',
28+
description: 'Backend infrastructure and web hosting built for developers who ship.',
29+
label: 'Start building for free',
30+
href: getAppwriteDashboardUrl(),
31+
event: 'blog-cta-cloud-btn-click',
32+
points: DEFAULT_CTA_POINTS
33+
};
34+
35+
const STARTUPS_CTA: BlogCtaConfig = {
36+
heading: 'Build your startup with Appwrite',
37+
description: 'Backend infrastructure and hosting that grows with your startup.',
38+
label: 'Apply for the program',
39+
href: '/startups',
40+
event: 'blog-cta-startups-btn-click',
41+
points: ['Cloud credits', 'Priority support', 'Ship faster', 'Built-in security and compliance']
42+
};
43+
44+
const AUTH_CTA: BlogCtaConfig = {
45+
heading: 'Customer identity without the hassle',
46+
description: 'Add secure authentication in minutes, not weeks.',
47+
label: 'Request a demo',
48+
href: 'https://appwrite.io/contact-us/enterprise',
49+
event: 'blog-cta-auth-demo-btn-click',
50+
points: [
51+
'Built-in security and compliance',
52+
'Multiple login methods',
53+
'Custom authentication flows',
54+
'Multi-factor authentication'
55+
]
56+
};
57+
58+
const SITES_CTA: BlogCtaConfig = {
59+
heading: 'Deploy in seconds, scale globally',
60+
description: 'Host your websites and web apps with zero infrastructure headaches.',
61+
label: 'Get started for free',
62+
href: getAppwriteDashboardUrl(),
63+
event: 'blog-cta-sites-btn-click',
64+
points: [
65+
'Open source and no vendor lock-in',
66+
'Built-in security and DDoS protection',
67+
'Fully managed cloud solution',
68+
'Global CDN for improved performance'
69+
]
70+
};
71+
72+
const CUSTOMER_STORIES_CTA: BlogCtaConfig = {
73+
heading: 'Join thousands of developers shipping faster',
74+
description: 'See how Appwrite can accelerate your development.',
75+
label: 'Start your project',
76+
href: getAppwriteDashboardUrl(),
77+
event: 'blog-cta-customer-stories-btn-click',
78+
points: DEFAULT_CTA_POINTS
79+
};
80+
81+
const OPEN_SOURCE_CTA: BlogCtaConfig = {
82+
heading: 'Open source, built for developers',
83+
description: 'Self-host or use Appwrite Cloud. No vendor lock-in, ever.',
84+
label: 'Get started',
85+
href: getAppwriteDashboardUrl(),
86+
event: 'blog-cta-open-source-btn-click',
87+
points: [
88+
'Start for free',
89+
'Open source under BSD 3-Clause',
90+
'Self-hosting available',
91+
'Active community support'
92+
]
93+
};
94+
95+
const INTEGRATIONS_CTA: BlogCtaConfig = {
96+
heading: 'Build with your favorite tools',
97+
description: 'Appwrite works with the frameworks and platforms you already use.',
98+
label: 'Explore integrations',
99+
href: '/integrations',
100+
event: 'blog-cta-integrations-btn-click',
101+
points: [
102+
'Support for over 13 SDKs',
103+
'Works with any framework',
104+
'Pre-built integrations',
105+
'Extensible with Functions'
106+
]
107+
};
108+
109+
const CONTRIBUTORS_CTA: BlogCtaConfig = {
110+
heading: 'Join the Appwrite community',
111+
description: 'Contribute to open source and help shape the future of backend development.',
112+
label: 'Start contributing',
113+
href: 'https://github.com/appwrite/appwrite',
114+
event: 'blog-cta-contributors-btn-click',
115+
points: [
116+
'Open source and welcoming',
117+
'Active Discord community',
118+
'Contributor recognition',
119+
'Good first issues available'
120+
]
121+
};
122+
123+
const HACKATHON_CTA: BlogCtaConfig = {
124+
heading: 'Build something amazing',
125+
description: 'Ship your hackathon project faster with Appwrite.',
126+
label: 'Start building',
127+
href: getAppwriteDashboardUrl(),
128+
event: 'blog-cta-hackathon-btn-click',
129+
points: [
130+
'Free to start',
131+
'Deploy in minutes',
132+
'Backend ready out of the box',
133+
'Focus on your idea, not infrastructure'
134+
]
135+
};
136+
137+
const INIT_CTA: BlogCtaConfig = {
138+
heading: 'Experience Appwrite yourself',
139+
description: 'Join our community and stay updated on the latest releases.',
140+
label: 'Join the community',
141+
href: '/discord',
142+
event: 'blog-cta-init-btn-click',
143+
points: [
144+
'Active Discord server',
145+
'Product announcements',
146+
'Community events',
147+
'Get help from maintainers'
148+
]
149+
};
150+
151+
const DEBUGGING_CTA: BlogCtaConfig = {
152+
heading: 'Debug with confidence',
153+
description: 'Track executions, view logs, and identify issues in the Console.',
154+
label: 'Try Appwrite',
155+
href: getAppwriteDashboardUrl(),
156+
event: 'blog-cta-debugging-btn-click',
157+
points: [
158+
'Execution tracking and status',
159+
'Request and response logs',
160+
'Console output for SSR sites',
161+
'Custom function logging'
162+
]
163+
};
164+
165+
const DESIGN_CTA: BlogCtaConfig = {
166+
heading: 'Build beautiful apps faster',
167+
description: 'Focus on design while Appwrite handles your backend.',
168+
label: 'Start building',
169+
href: getAppwriteDashboardUrl(),
170+
event: 'blog-cta-design-btn-click',
171+
points: DEFAULT_CTA_POINTS
172+
};
173+
174+
const categoryResolvers = new Map<string, BlogCtaConfig | CategoryResolver>([
175+
['startup', STARTUPS_CTA],
176+
['tutorial', DEVELOPERS_CLOUD_CTA],
177+
['customer-stories', CUSTOMER_STORIES_CTA],
178+
['security', AUTH_CTA],
179+
['open-source', OPEN_SOURCE_CTA],
180+
['integrations', INTEGRATIONS_CTA],
181+
['devrel', DEVELOPERS_CLOUD_CTA],
182+
['culture', DEVELOPERS_CLOUD_CTA],
183+
['company', DEVELOPERS_CLOUD_CTA],
184+
['contributors', CONTRIBUTORS_CTA],
185+
['hackathon', HACKATHON_CTA],
186+
['init', INIT_CTA],
187+
['news', DEVELOPERS_CLOUD_CTA],
188+
['gdpr', AUTH_CTA],
189+
['accessibility', DEVELOPERS_CLOUD_CTA],
190+
['design', DESIGN_CTA],
191+
['debugging', DEBUGGING_CTA],
192+
['sites', SITES_CTA]
193+
]);
194+
195+
const PRODUCT_RESOLVER: CategoryResolver = ({ slug, rawContent }) => {
196+
const lowerSlug = slug.toLowerCase();
197+
const content = rawContent?.toLowerCase() ?? '';
198+
const mentionsSites =
199+
lowerSlug.includes('site') ||
200+
lowerSlug.includes('hosting') ||
201+
lowerSlug.includes('deploy') ||
202+
content.includes('appwrite sites');
203+
const mentionsAuth =
204+
content.includes('ciam') ||
205+
content.includes('auth ') ||
206+
content.includes('authentication') ||
207+
content.includes('sso') ||
208+
content.includes('/products/auth') ||
209+
content.includes('oauth');
210+
211+
if (mentionsSites) {
212+
// Reuse canonical Sites CTA copy used across hosting posts
213+
return SITES_CTA;
214+
}
215+
if (mentionsAuth) {
216+
return AUTH_CTA;
217+
}
218+
219+
return DEVELOPERS_CLOUD_CTA;
220+
};
221+
222+
const FALLBACK_CTA = DEVELOPERS_CLOUD_CTA;
223+
224+
const ANNOUNCEMENT_PREFIXES = ['announcing-', 'announcement-', 'product-update-'];
225+
226+
export const parseCategories = (category: string | undefined): string[] =>
227+
(category ?? '')
228+
.split(',')
229+
.map((cat) => cat.trim().toLowerCase())
230+
.filter(Boolean);
231+
232+
export const isAnnouncement = (slug: string, categories: string[]): boolean => {
233+
if (categories.includes('announcement')) {
234+
return true;
235+
}
236+
return ANNOUNCEMENT_PREFIXES.some((prefix) => slug.startsWith(prefix));
237+
};
238+
239+
const CATEGORY_PRIORITY = [
240+
'startup',
241+
'sites',
242+
'product',
243+
'tutorial',
244+
'integrations',
245+
'customer-stories',
246+
'security',
247+
'open-source',
248+
'devrel',
249+
'culture',
250+
'company',
251+
'contributors',
252+
'hackathon',
253+
'init',
254+
'news',
255+
'gdpr',
256+
'accessibility',
257+
'design',
258+
'debugging'
259+
] satisfies string[];
260+
261+
export const resolveBlogCta = (
262+
rawCategory: string | undefined,
263+
slug: string,
264+
rawContent?: string | null
265+
): BlogCtaConfig => {
266+
const categories = parseCategories(rawCategory);
267+
const categoriesSet = new Set(categories);
268+
269+
for (const category of CATEGORY_PRIORITY) {
270+
if (!categoriesSet.has(category)) {
271+
continue;
272+
}
273+
274+
if (category === 'product') {
275+
return PRODUCT_RESOLVER({ slug, rawContent: rawContent ?? undefined });
276+
}
277+
278+
const value = categoryResolvers.get(category);
279+
if (!value) {
280+
continue;
281+
}
282+
283+
if (typeof value === 'function') {
284+
return value({ slug, rawContent: rawContent ?? undefined });
285+
}
286+
287+
return value;
288+
}
289+
290+
if (categories.length) {
291+
for (const category of categories) {
292+
const value = categoryResolvers.get(category);
293+
if (value) {
294+
if (typeof value === 'function') {
295+
return value({ slug, rawContent: rawContent ?? undefined });
296+
}
297+
return value;
298+
}
299+
}
300+
}
301+
302+
return FALLBACK_CTA;
303+
};

0 commit comments

Comments
 (0)