Add SEO structured data, comparison pages, and metadata improvements
- Add Heroku and Railway alternative comparison pages with detailed content and FAQ - Add StructuredData component for injecting JSON-LD schema - Add structured data to homepage, blog, posts, KTI, and services pages - Refactor SEO utilities: add absoluteUrl, improve genPageMetadata, support more metadata fields - Update siteMetadata with new description, keywords, and contact info - Improve sitemap to include static, dynamic, and KTI routes with priorities - Update contact info across docs and content to support@smbcloud.xyz - Add public/llms.txt with product and editorial info - Minor copy and metadata tweaks for clarity and SEO - Add ESLint ignores for build and generated files
keypair34 committed
May 8, 2026 at 21:37 UTC
ceb23d4847ac5d3c982740d9a14c2b4d03bd8e1c
24 files changed
+1825
-241
app/[...slug]/page.tsx
+54
-11
@@ -1,9 +1,11 @@
1
import { notFound } from "next/navigation";
2
-import { Metadata } from "next";
2
+import type { Metadata } from "next";
3
import { allPages } from "contentlayer/generated";
4
import { Mdx } from "@/components/mdx-components";
5
import AboutMedia from "@/components/about-media";
6
import Link from "next/link";
7
+import { StructuredData } from "@/components/structured-data";
8
+import { absoluteUrl, genPageMetadata } from "@/app/seo";
9
10
interface PageProps {
11
params: Promise<{
@@ -19,11 +21,16 @@ async function getPageFromParams(params: Awaited<PageProps["params"]>) {
21
export async function generateMetadata(props: PageProps): Promise<Metadata> {
22
const params = await props.params;
23
const page = await getPageFromParams(params);
22
- if (!page) return {};
23
- return {
24
+
25
+ if (!page) {
26
+ return {};
27
+ }
28
+
29
+ return genPageMetadata({
30
title: page.title,
31
description: page.description,
26
- };
32
+ path: page.slug,
33
+ });
34
}
35
36
export async function generateStaticParams() {
@@ -36,11 +43,51 @@ export default async function PagePage(props: PageProps) {
43
const params = await props.params;
44
const page = await getPageFromParams(params);
45
39
- if (!page) notFound();
46
+ if (!page) {
47
+ notFound();
48
+ }
49
+
50
+ const pageStructuredData = {
51
+ "@context": "https://schema.org",
52
+ "@graph": [
53
+ {
54
+ "@type": "WebPage",
55
+ "@id": absoluteUrl(page.slug),
56
+ url: absoluteUrl(page.slug),
57
+ name: page.title,
58
+ description: page.description,
59
+ isPartOf: {
60
+ "@id": absoluteUrl("/#website"),
61
+ },
62
+ breadcrumb: {
63
+ "@id": `${absoluteUrl(page.slug)}#breadcrumb`,
64
+ },
65
+ },
66
+ {
67
+ "@type": "BreadcrumbList",
68
+ "@id": `${absoluteUrl(page.slug)}#breadcrumb`,
69
+ itemListElement: [
70
+ {
71
+ "@type": "ListItem",
72
+ position: 1,
73
+ name: "Home",
74
+ item: absoluteUrl("/"),
75
+ },
76
+ {
77
+ "@type": "ListItem",
78
+ position: 2,
79
+ name: page.title,
80
+ item: absoluteUrl(page.slug),
81
+ },
82
+ ],
83
+ },
84
+ ],
85
+ };
86
87
return (
88
<div className="mx-auto max-w-3xl px-6 py-16 lg:px-8">
43
- {/* ── Breadcrumb ──────────────────────────────────── */}
89
+ <StructuredData data={pageStructuredData} />
90
+
91
<nav className="mb-10 flex items-center gap-2 text-xs text-slate-400 dark:text-slate-600">
92
<Link
93
href="/"
@@ -52,7 +99,6 @@ export default async function PagePage(props: PageProps) {
99
<span className="text-slate-600 dark:text-slate-400">{page.title}</span>
100
</nav>
101
55
- {/* ── Page header ─────────────────────────────────── */}
102
<div className="mb-12">
103
<h1 className="text-4xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-5xl">
104
{page.title}
@@ -64,21 +110,18 @@ export default async function PagePage(props: PageProps) {
110
)}
111
</div>
112
67
- {/* ── About media block ───────────────────────────── */}
113
{page.slugAsParams === "about" && (
114
<div className="mb-12">
115
<AboutMedia />
116
</div>
117
)}
118
74
- {/* ── MDX body ────────────────────────────────────── */}
119
<article className="prose dark:prose-invert prose-slate max-w-none prose-headings:font-semibold prose-headings:tracking-tight prose-a:text-indigo-600 dark:prose-a:text-indigo-400 prose-a:no-underline hover:prose-a:underline prose-code:rounded prose-code:bg-slate-100 dark:prose-code:bg-white/[0.06] prose-code:px-1 prose-code:py-0.5 prose-code:text-slate-800 dark:prose-code:text-slate-200 prose-code:before:content-none prose-code:after:content-none prose-pre:rounded-xl prose-pre:border prose-pre:border-slate-200 dark:prose-pre:border-white/[0.06] prose-pre:bg-[#0D1117] prose-hr:border-slate-200 dark:prose-hr:border-white/[0.06] prose-strong:text-slate-900 dark:prose-strong:text-white">
120
<Mdx code={page.body.code} />
121
</article>
122
79
- {/* ── Last updated ────────────────────────────────── */}
123
<p className="mt-16 border-t border-slate-200 dark:border-white/[0.06] pt-6 text-xs text-slate-400 dark:text-slate-600">
81
- Last updated:{" "}
124
+ Last updated{" "}
125
{new Date(page.lastmod).toLocaleDateString("en-US", {
126
year: "numeric",
127
month: "long",
app/heroku-alternative/page.tsx
new
+376
@@ -0,0 +1,376 @@
1
+import type { Metadata } from "next";
2
+import Link from "next/link";
3
+import { StructuredData } from "@/components/structured-data";
4
+import { absoluteUrl, genPageMetadata } from "../seo";
5
+
6
+const comparisonRows = [
7
+ {
8
+ label: "Deploy workflow",
9
+ smbcloud:
10
+ "Run `smb`, deploy over SSH, and watch the logs while it happens.",
11
+ heroku: "Managed platform workflow with its own conventions and habits.",
12
+ },
13
+ {
14
+ label: "Best fit",
15
+ smbcloud:
16
+ "Small teams that want a simple deploy flow and fewer moving parts.",
17
+ heroku:
18
+ "Teams already comfortable with Heroku and happy to keep working that way.",
19
+ },
20
+ {
21
+ label: "Runtime focus",
22
+ smbcloud: "Node.js, Ruby on Rails, and Swift/Vapor.",
23
+ heroku: "A broader app platform with a longer-established ecosystem.",
24
+ },
25
+ {
26
+ label: "Why teams pick it",
27
+ smbcloud: "Because it feels easier to follow and easier to operate.",
28
+ heroku: "Because it is familiar and already part of the team's workflow.",
29
+ },
30
+];
31
+
32
+const smbcloudFits = [
33
+ "You want Heroku-like simplicity, but with less platform in the middle.",
34
+ "You mostly ship Node.js, Rails, or Vapor apps.",
35
+ "You do not want a big platform story around a straightforward deploy.",
36
+ "You want to understand what is happening when the app goes live.",
37
+];
38
+
39
+const herokuStillFits = [
40
+ "Your team is already deep into Heroku and happy there.",
41
+ "You depend on Heroku-specific workflows or add-ons.",
42
+ "You are not looking to change the way you deploy right now.",
43
+];
44
+
45
+const bestFor = [
46
+ "Small SaaS teams",
47
+ "Client projects that need a repeatable deploy path",
48
+ "Rails apps",
49
+ "Node.js services and dashboards",
50
+ "Vapor backends",
51
+];
52
+
53
+const faqs = [
54
+ {
55
+ question: "Is smbCloud a Heroku replacement?",
56
+ answer:
57
+ "For some teams, yes. If you mostly ship Node.js, Rails, or Vapor apps and want a simpler SSH-first deploy flow, smbCloud can make more sense.",
58
+ },
59
+ {
60
+ question: "Who should switch from Heroku to smbCloud?",
61
+ answer:
62
+ "Teams that still want simplicity, but want less platform in the middle.",
63
+ },
64
+ {
65
+ question: "Is smbCloud trying to match every Heroku feature?",
66
+ answer: "No. That is not the point. smbCloud is narrower on purpose.",
67
+ },
68
+];
69
+
70
+export const metadata: Metadata = {
71
+ ...genPageMetadata({
72
+ title: "Heroku alternative for small teams",
73
+ description:
74
+ "Looking for a Heroku alternative? smbCloud is an SSH-first deploy platform for small teams shipping Node.js, Ruby on Rails, and Swift/Vapor apps.",
75
+ path: "/heroku-alternative",
76
+ keywords: [
77
+ "Heroku alternative",
78
+ "Heroku replacement",
79
+ "SSH-first deploy platform",
80
+ "deploy Node.js app",
81
+ "deploy Ruby on Rails app",
82
+ "deploy Vapor app",
83
+ "small team hosting platform",
84
+ ],
85
+ }),
86
+};
87
+
88
+const structuredData = {
89
+ "@context": "https://schema.org",
90
+ "@graph": [
91
+ {
92
+ "@type": "WebPage",
93
+ "@id": absoluteUrl("/heroku-alternative#webpage"),
94
+ url: absoluteUrl("/heroku-alternative"),
95
+ name: "Heroku alternative for small teams",
96
+ description:
97
+ "Compare smbCloud and Heroku for small-team app deployment workflows.",
98
+ isPartOf: {
99
+ "@id": absoluteUrl("/#website"),
100
+ },
101
+ },
102
+ {
103
+ "@type": "BreadcrumbList",
104
+ itemListElement: [
105
+ {
106
+ "@type": "ListItem",
107
+ position: 1,
108
+ name: "Home",
109
+ item: absoluteUrl("/"),
110
+ },
111
+ {
112
+ "@type": "ListItem",
113
+ position: 2,
114
+ name: "Heroku alternative",
115
+ item: absoluteUrl("/heroku-alternative"),
116
+ },
117
+ ],
118
+ },
119
+ {
120
+ "@type": "FAQPage",
121
+ mainEntity: faqs.map((faq) => ({
122
+ "@type": "Question",
123
+ name: faq.question,
124
+ acceptedAnswer: {
125
+ "@type": "Answer",
126
+ text: faq.answer,
127
+ },
128
+ })),
129
+ },
130
+ ],
131
+};
132
+
133
+export default function HerokuAlternativePage() {
134
+ return (
135
+ <div>
136
+ <StructuredData data={structuredData} />
137
+
138
+ <section className="relative overflow-hidden px-6 pt-24 pb-16 sm:pt-32 sm:pb-20 lg:px-8">
139
+ <div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[520px] bg-[radial-gradient(ellipse_75%_50%_at_50%_0%,rgba(99,102,241,0.18),transparent)] dark:bg-[radial-gradient(ellipse_75%_50%_at_50%_0%,rgba(99,102,241,0.12),transparent)]" />
140
+
141
+ <div className="mx-auto max-w-4xl">
142
+ <div className="mb-6 inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-4 py-1.5 dark:border-indigo-500/30 dark:bg-indigo-500/10">
143
+ <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
144
+ <span className="text-xs font-semibold uppercase tracking-widest text-indigo-700 dark:text-indigo-400">
145
+ Heroku alternative
146
+ </span>
147
+ </div>
148
+
149
+ <h1 className="max-w-3xl text-4xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-6xl leading-[1.05]">
150
+ Looking for a Heroku alternative?
151
+ </h1>
152
+
153
+ <p className="mt-6 max-w-3xl text-lg leading-8 text-slate-600 dark:text-slate-400">
154
+ We built smbCloud for teams that liked what Heroku got right but got
155
+ tired of how much platform ended up sitting between the code and the
156
+ deploy. SSH-first, one command, easy to follow.
157
+ </p>
158
+
159
+ <div className="mt-8 flex flex-col gap-4 sm:flex-row">
160
+ <Link
161
+ href="https://console.smbcloud.xyz"
162
+ target="_blank"
163
+ rel="noopener noreferrer"
164
+ className="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-8 py-3.5 text-sm font-semibold text-white shadow-lg shadow-indigo-500/20 transition-all hover:bg-indigo-500 hover:shadow-indigo-500/30 active:scale-[0.98]"
165
+ >
166
+ Try smbCloud
167
+ </Link>
168
+ <Link
169
+ href="/posts/cli"
170
+ className="inline-flex items-center justify-center rounded-lg border border-slate-300 px-8 py-3.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 dark:border-white/[0.12] dark:text-slate-200 dark:hover:bg-white/[0.04]"
171
+ >
172
+ Install the CLI
173
+ </Link>
174
+ </div>
175
+ </div>
176
+ </section>
177
+
178
+ <section className="px-6 pb-24 lg:px-8">
179
+ <div className="mx-auto max-w-5xl grid gap-8 lg:grid-cols-[0.95fr_1.05fr]">
180
+ <div>
181
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
182
+ Why people leave Heroku
183
+ </p>
184
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
185
+ Usually they still want simplicity. They just want less platform
186
+ around it.
187
+ </h2>
188
+ <p className="mt-5 text-base leading-8 text-slate-600 dark:text-slate-400">
189
+ That is basically the whole argument. Heroku got a lot right. I
190
+ just think a lot of teams eventually want something they can
191
+ explain in plain English.
192
+ </p>
193
+ </div>
194
+
195
+ <div className="rounded-2xl border border-slate-200 bg-slate-50 p-8 dark:border-white/[0.06] dark:bg-white/[0.02]">
196
+ <p className="text-sm font-semibold text-slate-900 dark:text-white">
197
+ What smbCloud does well
198
+ </p>
199
+ <ul className="mt-5 space-y-4 text-sm leading-7 text-slate-600 dark:text-slate-400">
200
+ <li className="flex gap-3">
201
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
202
+ <span>Deploy over SSH with one command.</span>
203
+ </li>
204
+ <li className="flex gap-3">
205
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
206
+ <span>Support for Node.js, Rails, and Swift/Vapor.</span>
207
+ </li>
208
+ <li className="flex gap-3">
209
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
210
+ <span>A setup that feels closer to normal app operations.</span>
211
+ </li>
212
+ </ul>
213
+ </div>
214
+ </div>
215
+ </section>
216
+
217
+ <section className="border-t border-slate-200 px-6 py-24 dark:border-white/[0.06] lg:px-8">
218
+ <div className="mx-auto max-w-5xl">
219
+ <div className="mb-12 max-w-3xl">
220
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
221
+ smbCloud vs Heroku
222
+ </p>
223
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
224
+ Here is the trade, without the usual sales fog.
225
+ </h2>
226
+ </div>
227
+
228
+ <div className="overflow-hidden rounded-2xl border border-slate-200 dark:border-white/[0.06]">
229
+ <div className="grid grid-cols-[0.8fr_1fr_1fr] bg-slate-50 text-sm font-semibold text-slate-900 dark:bg-white/[0.03] dark:text-white">
230
+ <div className="border-b border-slate-200 px-5 py-4 dark:border-white/[0.06]">
231
+ Area
232
+ </div>
233
+ <div className="border-b border-l border-slate-200 px-5 py-4 dark:border-white/[0.06]">
234
+ smbCloud
235
+ </div>
236
+ <div className="border-b border-l border-slate-200 px-5 py-4 dark:border-white/[0.06]">
237
+ Heroku
238
+ </div>
239
+ </div>
240
+ {comparisonRows.map((row) => (
241
+ <div
242
+ key={row.label}
243
+ className="grid grid-cols-[0.8fr_1fr_1fr] text-sm leading-7 text-slate-600 dark:text-slate-400"
244
+ >
245
+ <div className="border-b border-slate-200 bg-white px-5 py-5 font-semibold text-slate-900 dark:border-white/[0.06] dark:bg-white/[0.02] dark:text-white">
246
+ {row.label}
247
+ </div>
248
+ <div className="border-b border-l border-slate-200 bg-white px-5 py-5 dark:border-white/[0.06] dark:bg-white/[0.02]">
249
+ {row.smbcloud}
250
+ </div>
251
+ <div className="border-b border-l border-slate-200 bg-white px-5 py-5 dark:border-white/[0.06] dark:bg-white/[0.02]">
252
+ {row.heroku}
253
+ </div>
254
+ </div>
255
+ ))}
256
+ </div>
257
+ </div>
258
+ </section>
259
+
260
+ <section className="px-6 py-24 lg:px-8">
261
+ <div className="mx-auto grid max-w-5xl gap-6 lg:grid-cols-2">
262
+ <div className="rounded-2xl border border-slate-200 bg-white p-8 dark:border-white/[0.06] dark:bg-white/[0.02]">
263
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
264
+ Pick smbCloud if
265
+ </p>
266
+ <ul className="mt-6 space-y-4 text-sm leading-7 text-slate-600 dark:text-slate-400">
267
+ {smbcloudFits.map((item) => (
268
+ <li key={item} className="flex gap-3">
269
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
270
+ <span>{item}</span>
271
+ </li>
272
+ ))}
273
+ </ul>
274
+ </div>
275
+
276
+ <div className="rounded-2xl border border-slate-200 bg-slate-50 p-8 dark:border-white/[0.06] dark:bg-white/[0.02]">
277
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
278
+ Stay on Heroku if
279
+ </p>
280
+ <ul className="mt-6 space-y-4 text-sm leading-7 text-slate-600 dark:text-slate-400">
281
+ {herokuStillFits.map((item) => (
282
+ <li key={item} className="flex gap-3">
283
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-slate-400" />
284
+ <span>{item}</span>
285
+ </li>
286
+ ))}
287
+ </ul>
288
+ </div>
289
+ </div>
290
+ </section>
291
+
292
+ <section className="border-t border-slate-200 px-6 py-24 dark:border-white/[0.06] lg:px-8">
293
+ <div className="mx-auto max-w-5xl grid gap-8 lg:grid-cols-[0.95fr_1.05fr]">
294
+ <div>
295
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
296
+ Best fit
297
+ </p>
298
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
299
+ I would look at smbCloud if you still want ease, just with less
300
+ ceremony.
301
+ </h2>
302
+ </div>
303
+ <ul className="rounded-2xl border border-slate-200 bg-white p-8 text-sm leading-7 text-slate-600 dark:border-white/[0.06] dark:bg-white/[0.02] dark:text-slate-400">
304
+ {bestFor.map((item) => (
305
+ <li
306
+ key={item}
307
+ className="flex gap-3 border-b border-slate-200 py-3 last:border-0 last:pb-0 first:pt-0 dark:border-white/[0.06]"
308
+ >
309
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
310
+ <span>{item}</span>
311
+ </li>
312
+ ))}
313
+ </ul>
314
+ </div>
315
+ </section>
316
+
317
+ <section className="px-6 pb-24 lg:px-8">
318
+ <div className="mx-auto max-w-3xl">
319
+ <div className="mb-10">
320
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
321
+ FAQ
322
+ </p>
323
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
324
+ Common questions
325
+ </h2>
326
+ </div>
327
+ <div className="space-y-4">
328
+ {faqs.map((faq) => (
329
+ <div
330
+ key={faq.question}
331
+ className="rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/[0.06] dark:bg-white/[0.02]"
332
+ >
333
+ <h3 className="text-lg font-semibold text-slate-900 dark:text-white">
334
+ {faq.question}
335
+ </h3>
336
+ <p className="mt-3 text-sm leading-7 text-slate-600 dark:text-slate-400">
337
+ {faq.answer}
338
+ </p>
339
+ </div>
340
+ ))}
341
+ </div>
342
+ </div>
343
+ </section>
344
+
345
+ <section className="px-6 pb-20 lg:px-8">
346
+ <div className="mx-auto max-w-3xl">
347
+ <div className="relative overflow-hidden rounded-2xl bg-indigo-600 px-8 py-14 text-center shadow-2xl shadow-indigo-500/20">
348
+ <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.1),transparent)]" />
349
+ <h2 className="relative text-3xl font-bold tracking-tight text-white">
350
+ If Heroku feels like more platform than you need, try smbCloud.
351
+ </h2>
352
+ <p className="relative mt-4 text-indigo-200">
353
+ Run it on one app. That is usually enough to know.
354
+ </p>
355
+ <div className="relative mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
356
+ <Link
357
+ href="https://console.smbcloud.xyz"
358
+ target="_blank"
359
+ rel="noopener noreferrer"
360
+ className="inline-flex rounded-lg bg-white px-8 py-3.5 text-sm font-semibold text-indigo-700 shadow-sm transition-colors hover:bg-indigo-50"
361
+ >
362
+ Start with smbCloud
363
+ </Link>
364
+ <Link
365
+ href="/railway-alternative"
366
+ className="inline-flex rounded-lg border border-white/20 px-8 py-3.5 text-sm font-semibold text-white transition-colors hover:bg-white/10"
367
+ >
368
+ Compare Railway alternative
369
+ </Link>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ </section>
374
+ </div>
375
+ );
376
+}
app/kti/[term]/page.tsx
+71
-24
@@ -1,9 +1,11 @@
1
+import type { Metadata } from "next";
2
import { allKtis } from "@/.contentlayer/generated";
3
import { notFound } from "next/navigation";
4
import { Mdx } from "@/components/mdx-components";
5
import Link from "next/link";
5
-import { genPageMetadata } from "../../seo";
6
+import { StructuredData } from "@/components/structured-data";
7
import siteMetadata from "@/content/siteMetadata";
8
+import { absoluteUrl, genPageMetadata } from "../../seo";
9
10
export async function generateStaticParams() {
11
return allKtis.map((kti) => ({
@@ -15,40 +17,45 @@ export async function generateMetadata({
17
params,
18
}: {
19
params: Promise<{ term: string }>;
18
-}) {
20
+}): Promise<Metadata> {
21
const { term } = await params;
22
const kti = allKtis.find((kti) => kti.term === term);
23
24
if (!kti) return {};
25
26
+ const keywords = [
27
+ kti.term,
28
+ kti.category,
29
+ ...(kti.tags || []),
30
+ "kamus teknologi informasi",
31
+ "istilah IT",
32
+ "bahasa Indonesia",
33
+ ];
34
+
35
+ if (kti.englishTerm) {
36
+ keywords.splice(1, 0, kti.englishTerm);
37
+ }
38
+
39
return {
40
...genPageMetadata({
41
title: `${kti.title} - Kamus Teknologi Informasi`,
42
description: kti.description,
28
- }),
29
- keywords: [
30
- kti.term,
31
- kti.englishTerm,
32
- kti.category,
33
- ...(kti.tags || []),
34
- "kamus teknologi informasi",
35
- "istilah IT",
36
- "bahasa Indonesia",
37
- ].filter(Boolean),
38
- openGraph: {
39
- title: `${kti.title} - KTI`,
40
- description: kti.description,
41
- url: `${siteMetadata.siteUrl}/kti/${kti.term}`,
42
- siteName: siteMetadata.title,
43
+ path: kti.slug,
44
locale: "id_ID",
45
type: "article",
45
- publishedTime: kti.lastmod,
46
- modifiedTime: kti.lastmod,
47
- authors: [siteMetadata.author],
48
- },
49
- alternates: {
50
- canonical: `${siteMetadata.siteUrl}/kti/${kti.term}`,
51
- },
46
+ openGraph: {
47
+ title: `${kti.title} - KTI | ${siteMetadata.title}`,
48
+ description: kti.description,
49
+ url: absoluteUrl(kti.slug),
50
+ siteName: siteMetadata.title,
51
+ locale: "id_ID",
52
+ type: "article",
53
+ publishedTime: kti.lastmod,
54
+ modifiedTime: kti.lastmod,
55
+ authors: [siteMetadata.author],
56
+ },
57
+ }),
58
+ keywords,
59
};
60
}
61
@@ -107,8 +114,48 @@ export default async function KtiTermPage({
114
const difficulty = kti.difficulty as Difficulty;
115
const config = difficultyConfig[difficulty] ?? difficultyConfig.pemula;
116
117
+ const termStructuredData = {
118
+ "@context": "https://schema.org",
119
+ "@graph": [
120
+ {
121
+ "@type": "DefinedTerm",
122
+ name: kti.title,
123
+ description: kti.description,
124
+ url: absoluteUrl(kti.slug),
125
+ inLanguage: "id-ID",
126
+ termCode: kti.term,
127
+ alternateName: [kti.englishTerm, kti.acronym].filter(Boolean),
128
+ inDefinedTermSet: absoluteUrl("/kti"),
129
+ },
130
+ {
131
+ "@type": "BreadcrumbList",
132
+ itemListElement: [
133
+ {
134
+ "@type": "ListItem",
135
+ position: 1,
136
+ name: "Home",
137
+ item: absoluteUrl("/"),
138
+ },
139
+ {
140
+ "@type": "ListItem",
141
+ position: 2,
142
+ name: "KTI",
143
+ item: absoluteUrl("/kti"),
144
+ },
145
+ {
146
+ "@type": "ListItem",
147
+ position: 3,
148
+ name: kti.title,
149
+ item: absoluteUrl(kti.slug),
150
+ },
151
+ ],
152
+ },
153
+ ],
154
+ };
155
+
156
return (
157
<div className="mx-auto max-w-3xl px-6 py-16 lg:px-8">
158
+ <StructuredData data={termStructuredData} />
159
{/* ── Breadcrumb ──────────────────────────────────── */}
160
<nav className="mb-10 flex items-center gap-2 text-xs text-slate-400 dark:text-slate-600">
161
<Link
app/kti/page.tsx
+60
-3
@@ -1,13 +1,17 @@
1
+import type { Metadata } from "next";
2
import { allKtis } from "@/.contentlayer/generated";
3
import Link from "next/link";
3
-import { genPageMetadata } from "../seo";
4
+import { StructuredData } from "@/components/structured-data";
5
import siteMetadata from "@/content/siteMetadata";
6
+import { absoluteUrl, genPageMetadata } from "../seo";
7
6
-export const metadata = {
8
+export const metadata: Metadata = {
9
...genPageMetadata({
10
title: "KTI - Kamus Teknologi Informasi",
11
description:
12
"Kamus Teknologi Informasi lengkap untuk tenaga kerja muda Indonesia. Pelajari istilah-istilah IT, pemrograman, cloud computing, dan teknologi terkini dengan penjelasan dalam Bahasa Indonesia.",
13
+ path: "/kti",
14
+ locale: "id_ID",
15
}),
16
keywords: [
17
"kamus teknologi informasi",
@@ -75,8 +79,59 @@ export default function KtiPage() {
79
lanjut: publishedKtis.filter((kti) => kti.difficulty === "lanjut"),
80
};
81
82
+ const ktiStructuredData = {
83
+ "@context": "https://schema.org",
84
+ "@graph": [
85
+ {
86
+ "@type": "CollectionPage",
87
+ "@id": absoluteUrl("/kti#webpage"),
88
+ url: absoluteUrl("/kti"),
89
+ name: "KTI - Kamus Teknologi Informasi",
90
+ description:
91
+ "Kamus Teknologi Informasi lengkap untuk tenaga kerja muda Indonesia.",
92
+ inLanguage: "id-ID",
93
+ isPartOf: {
94
+ "@id": absoluteUrl("/#website"),
95
+ },
96
+ },
97
+ {
98
+ "@type": "BreadcrumbList",
99
+ itemListElement: [
100
+ {
101
+ "@type": "ListItem",
102
+ position: 1,
103
+ name: "Home",
104
+ item: absoluteUrl("/"),
105
+ },
106
+ {
107
+ "@type": "ListItem",
108
+ position: 2,
109
+ name: "KTI",
110
+ item: absoluteUrl("/kti"),
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ "@type": "DefinedTermSet",
116
+ name: "Kamus Teknologi Informasi",
117
+ url: absoluteUrl("/kti"),
118
+ description:
119
+ "Glosarium istilah teknologi informasi dalam Bahasa Indonesia.",
120
+ inLanguage: "id-ID",
121
+ hasDefinedTerm: publishedKtis.map((kti) => ({
122
+ "@type": "DefinedTerm",
123
+ name: kti.title,
124
+ description: kti.description,
125
+ url: absoluteUrl(kti.slug),
126
+ inLanguage: "id-ID",
127
+ })),
128
+ },
129
+ ],
130
+ };
131
+
132
return (
133
<div className="mx-auto max-w-5xl px-6 py-16 lg:px-8">
134
+ <StructuredData data={ktiStructuredData} />
135
{/* ── Page header ─────────────────────────────────── */}
136
<div className="mb-16">
137
<p className="mb-4 text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
@@ -252,11 +307,13 @@ export default function KtiPage() {
307
Siap membangun dengan smbCloud?
308
</h2>
309
<p className="relative mt-3 text-indigo-200">
255
- Deploy aplikasi NodeJS, Swift, dan Ruby dengan satu perintah.
310
+ Deploy aplikasi Node.js, Ruby on Rails, dan Swift/Vapor dengan satu
311
+ perintah.
312
</p>
313
<Link
314
href="https://console.smbcloud.xyz/login"
315
target="_blank"
316
+ rel="noopener noreferrer"
317
className="relative mt-8 inline-block rounded-lg bg-white px-8 py-3.5 text-sm font-semibold text-indigo-700 shadow-sm transition-colors hover:bg-indigo-50"
318
>
319
Mulai dengan smbCloud →
app/layout.tsx
+91
-4
@@ -4,6 +4,9 @@ import { Inter } from "next/font/google";
4
import { ThemeProvider } from "@/components/theme-provider";
5
import Footer from "@/components/footer";
6
import Header from "@/components/header";
7
+import { StructuredData } from "@/components/structured-data";
8
+import siteMetadata from "@/content/siteMetadata";
9
+import { absoluteUrl } from "./seo";
10
11
const inter = Inter({
12
subsets: ["latin"],
@@ -12,10 +15,93 @@ const inter = Inter({
15
});
16
17
export const metadata: Metadata = {
15
- title: "smbCloud — The unified cloud platform",
16
- description:
17
- "Deploy NodeJS, Swift, and Ruby apps with one command. SSH-based. Zero complexity.",
18
- metadataBase: new URL("https://smbcloud.xyz"),
18
+ metadataBase: new URL(siteMetadata.siteUrl),
19
+ applicationName: siteMetadata.title,
20
+ title: {
21
+ default: siteMetadata.title,
22
+ template: `%s | ${siteMetadata.title}`,
23
+ },
24
+ description: siteMetadata.description,
25
+ keywords: siteMetadata.keywords,
26
+ authors: [{ name: siteMetadata.author }],
27
+ creator: siteMetadata.author,
28
+ publisher: siteMetadata.companyName,
29
+ category: "Developer tools",
30
+ alternates: {
31
+ canonical: "/",
32
+ },
33
+ openGraph: {
34
+ title: siteMetadata.title,
35
+ description: siteMetadata.description,
36
+ url: siteMetadata.siteUrl,
37
+ siteName: siteMetadata.title,
38
+ locale: "en_US",
39
+ type: "website",
40
+ images: [
41
+ {
42
+ url: siteMetadata.socialBanner || "/twitter-card.png",
43
+ alt: siteMetadata.tagline,
44
+ },
45
+ ],
46
+ },
47
+ twitter: {
48
+ card: "summary_large_image",
49
+ title: siteMetadata.title,
50
+ description: siteMetadata.description,
51
+ images: [siteMetadata.socialBanner || "/twitter-card.png"],
52
+ },
53
+ robots: {
54
+ index: true,
55
+ follow: true,
56
+ googleBot: {
57
+ index: true,
58
+ follow: true,
59
+ "max-image-preview": "large",
60
+ "max-snippet": -1,
61
+ "max-video-preview": -1,
62
+ },
63
+ },
64
+ icons: {
65
+ icon: [{ url: "/favicon.ico" }],
66
+ shortcut: [{ url: "/favicon.ico" }],
67
+ },
68
+};
69
+
70
+const globalStructuredData = {
71
+ "@context": "https://schema.org",
72
+ "@graph": [
73
+ {
74
+ "@type": "Organization",
75
+ "@id": absoluteUrl("/#organization"),
76
+ name: siteMetadata.organizationName,
77
+ legalName: siteMetadata.companyName,
78
+ url: siteMetadata.siteUrl,
79
+ logo: absoluteUrl(siteMetadata.siteLogo),
80
+ email: siteMetadata.email,
81
+ sameAs: [
82
+ siteMetadata.github,
83
+ siteMetadata.facebook,
84
+ siteMetadata.instagram,
85
+ siteMetadata.youtube,
86
+ ].filter(Boolean),
87
+ address: {
88
+ "@type": "PostalAddress",
89
+ addressLocality: "Stockholm",
90
+ addressCountry: "SE",
91
+ },
92
+ },
93
+ {
94
+ "@type": "WebSite",
95
+ "@id": absoluteUrl("/#website"),
96
+ url: siteMetadata.siteUrl,
97
+ name: siteMetadata.title,
98
+ description: siteMetadata.description,
99
+ inLanguage: siteMetadata.language,
100
+ publisher: {
101
+ "@id": absoluteUrl("/#organization"),
102
+ },
103
+ },
104
+ ],
105
};
106
107
interface RootLayoutProps {
@@ -28,6 +114,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
114
<body
115
className={`${inter.variable} font-sans antialiased min-h-screen bg-white dark:bg-[#0A0E14] text-slate-900 dark:text-slate-50`}
116
>
117
+ <StructuredData data={globalStructuredData} />
118
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
119
<div className="flex min-h-screen flex-col">
120
<Header />
app/page.tsx
+361
-108
@@ -1,46 +1,135 @@
1
+import type { Metadata } from "next";
2
import { allPosts } from "@/.contentlayer/generated";
3
import Link from "next/link";
4
import TabbedRunner from "./components/TabbedRunner";
5
import { FaApple, FaGooglePlay, FaWindows } from "react-icons/fa";
6
+import { StructuredData } from "@/components/structured-data";
7
+import siteMetadata from "@/content/siteMetadata";
8
+import { absoluteUrl, genPageMetadata } from "./seo";
9
6
-const features = [
10
+const proofPoints = [
11
{
8
- icon: "⚡",
9
- title: "One-command deploys",
12
+ title: "One command",
13
+ description: "Deploy from your project with the `smb` CLI.",
14
+ },
15
+ {
16
+ title: "SSH-first",
17
+ description: "Keep the deploy flow closer to how developers already work.",
18
+ },
19
+ {
20
+ title: "Live logs",
21
description:
11
- "Run `smb` in your project directory. That’s it. Your app is live in seconds.",
22
+ "Watch deploy progress, app checks, and runtime feedback in real time.",
23
+ },
24
+ {
25
+ title: "Fast rollbacks",
26
+ description: "Go back quickly when a release needs to be reversed.",
27
},
28
+];
29
+
30
+const reasonsToChoose = [
31
{
14
- icon: "🔐",
15
- title: "SSH-based security",
32
+ title: "No platform maze",
33
description:
17
- "No agents, no tokens. Pure SSH key auth for every deployment. Zero attack surface.",
34
+ "We built smbCloud so deploys would feel boring again. Run `smb`, watch the release, move on.",
35
},
36
{
20
- icon: "🔄",
21
- title: "Roll back fast",
22
- description: "Each deploy is versioned, so going back is one command too.",
37
+ title: "Built for real stacks",
38
+ description:
39
+ "Node.js, Rails, and Vapor are the stacks we actually care about. This product is shaped around them on purpose.",
40
},
41
{
25
- icon: "📊",
26
- title: "Watch deploys live",
42
+ title: "Closer to normal ops",
43
description:
28
- "See logs, health checks, and deploy status while the release runs.",
44
+ "If your team already likes SSH and straightforward deploys, smbCloud will make sense fast.",
45
},
46
+];
47
+
48
+const bestFit = [
49
+ "Small product teams shipping web apps and APIs",
50
+ "Agencies and consultants managing client deployments",
51
+ "Founders who miss when deploys felt simple",
52
+ "Teams that already like SSH and fewer moving parts",
53
+];
54
+
55
+const comparisonPages = [
56
{
31
- icon: "🌐",
32
- title: "Built for multiple runtimes",
57
+ href: "/heroku-alternative",
58
+ eyebrow: "Comparison page",
59
+ title: "Heroku alternative",
60
description:
34
- "Deploy Rust, Node.js, Swift/Vapor, and Ruby on Rails apps on one platform.",
61
+ "If you still want simplicity, but with less platform in the middle.",
62
+ bullets: [
63
+ "Why some Rails, Node.js, and Vapor teams leave Heroku",
64
+ "Where smbCloud fits, and where it does not",
65
+ ],
66
},
67
{
37
- icon: "⚙️",
38
- title: "Less setup",
68
+ href: "/railway-alternative",
69
+ eyebrow: "Comparison page",
70
+ title: "Railway alternative",
71
description:
40
- "We detect the runtime for you. No YAML maze. No Dockerfiles unless you want them.",
72
+ "If Railway feels a bit too broad and you want a calmer deploy flow.",
73
+ bullets: [
74
+ "What you gain from a narrower platform",
75
+ "Why some teams prefer less surface area",
76
+ ],
77
},
78
];
79
80
+export const metadata: Metadata = {
81
+ ...genPageMetadata({
82
+ title: "Deploy Node.js, Rails, and Vapor apps with one command",
83
+ description:
84
+ "smbCloud is an SSH-first deploy platform for small teams shipping Node.js, Ruby on Rails, and Swift/Vapor apps. Deploy with one command, live logs, fast rollbacks, and a clean console.",
85
+ path: "/",
86
+ keywords: [
87
+ "SSH-first deploy platform",
88
+ "deploy Node.js app",
89
+ "deploy Ruby on Rails app",
90
+ "deploy Vapor app",
91
+ "Heroku alternative",
92
+ "Railway alternative",
93
+ "small team deployment platform",
94
+ ],
95
+ }),
96
+};
97
+
98
+const homepageStructuredData = {
99
+ "@context": "https://schema.org",
100
+ "@graph": [
101
+ {
102
+ "@type": "WebPage",
103
+ "@id": absoluteUrl("/#webpage"),
104
+ url: absoluteUrl("/"),
105
+ name: "smbCloud homepage",
106
+ description: siteMetadata.description,
107
+ isPartOf: {
108
+ "@id": absoluteUrl("/#website"),
109
+ },
110
+ about: {
111
+ "@id": absoluteUrl("/#organization"),
112
+ },
113
+ },
114
+ {
115
+ "@type": "Service",
116
+ name: "smbCloud Deploy",
117
+ serviceType: "SSH-first application deployment platform",
118
+ provider: {
119
+ "@id": absoluteUrl("/#organization"),
120
+ },
121
+ areaServed: "Worldwide",
122
+ url: absoluteUrl("/"),
123
+ description:
124
+ "Deploy Node.js, Ruby on Rails, and Swift/Vapor apps with one command, live logs, and fast rollbacks.",
125
+ audience: {
126
+ "@type": "Audience",
127
+ audienceType: "Developers and small software teams",
128
+ },
129
+ },
130
+ ],
131
+};
132
+
133
export default function Home() {
134
let publishedPosts = allPosts.filter((post) => post.published);
135
publishedPosts = publishedPosts.sort(
@@ -49,66 +138,112 @@ export default function Home() {
138
139
return (
140
<div>
52
- {/* ── Hero ───────────────────────────────────────────────────────── */}
141
+ <StructuredData data={homepageStructuredData} />
142
+
143
<section className="relative overflow-hidden px-6 pt-24 pb-20 sm:pt-36 sm:pb-28 lg:px-8">
54
- {/* Soft radial glow */}
144
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[600px] bg-[radial-gradient(ellipse_80%_50%_at_50%_0%,rgba(99,102,241,0.18),transparent)] dark:bg-[radial-gradient(ellipse_80%_50%_at_50%_0%,rgba(99,102,241,0.12),transparent)]" />
145
146
<div className="mx-auto max-w-4xl text-center">
58
- {/* Status badge */}
59
- <div className="mb-8 inline-flex items-center gap-2 rounded-full border border-indigo-200 dark:border-indigo-500/30 bg-indigo-50 dark:bg-indigo-500/10 px-4 py-1.5">
60
- <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-indigo-500" />
61
- <span className="text-xs font-semibold uppercase tracking-widest text-indigo-700 dark:text-indigo-400">
62
- Cloud infrastructure for developers
63
- </span>
64
- </div>
147
+ <p className="mb-8 text-sm font-semibold uppercase tracking-[0.2em] text-slate-500 dark:text-slate-500">
148
+ Enterprise Power at Startup Velocity
149
+ </p>
150
151
<h1 className="text-5xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-7xl leading-[1.05]">
67
- Deploy your app with{" "}
152
+ Deploy your app without the{" "}
153
<span className="text-indigo-600 dark:text-indigo-400">
69
- one command.
154
+ platform maze.
155
</span>
156
</h1>
157
73
- <p className="mx-auto mt-8 max-w-2xl text-lg leading-8 text-slate-600 dark:text-slate-400">
74
- The unified cloud platform for{" "}
158
+ <p className="mx-auto mt-8 max-w-3xl text-lg leading-8 text-slate-600 dark:text-slate-400">
159
+ smbCloud is an SSH-first deploy platform for small teams shipping{" "}
160
<span className="font-semibold text-slate-800 dark:text-slate-200">
76
- Rust
161
+ Node.js
162
</span>
163
,{" "}
164
<span className="font-semibold text-slate-800 dark:text-slate-200">
80
- NodeJS
81
- </span>
82
- ,{" "}
83
- <span className="font-semibold text-slate-800 dark:text-slate-200">
84
- Swift
165
+ Ruby on Rails
166
</span>
167
, and{" "}
168
<span className="font-semibold text-slate-800 dark:text-slate-200">
88
- Ruby
169
+ Swift/Vapor
170
</span>{" "}
90
- apps.
171
+ apps. Run one command, watch the logs, and roll back fast when you
172
+ need to.
173
</p>
174
175
<div className="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
176
<Link
177
href="https://console.smbcloud.xyz"
178
target="_blank"
179
+ rel="noopener noreferrer"
180
className="w-full rounded-lg bg-indigo-600 px-8 py-3.5 text-sm font-semibold text-white shadow-lg shadow-indigo-500/20 transition-all hover:bg-indigo-500 hover:shadow-indigo-500/30 active:scale-[0.98] sm:w-auto"
181
>
99
- Open the console →
182
+ Start deploying →
183
+ </Link>
184
+ <Link
185
+ href="/posts/cli"
186
+ className="inline-flex w-full items-center justify-center rounded-lg border border-slate-300 dark:border-white/[0.12] px-8 py-3.5 text-sm font-semibold text-slate-700 dark:text-slate-200 transition-colors hover:bg-slate-50 dark:hover:bg-white/[0.04] sm:w-auto"
187
+ >
188
+ Install the CLI
189
+ </Link>
190
+ </div>
191
+
192
+ <div className="mt-5 flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-sm text-slate-500 dark:text-slate-400">
193
+ <Link
194
+ href="/heroku-alternative"
195
+ className="font-medium text-indigo-600 transition-colors hover:text-indigo-500 dark:text-indigo-400"
196
+ >
197
+ Heroku alternative
198
</Link>
199
+ <span className="text-slate-300 dark:text-slate-700">•</span>
200
+ <Link
201
+ href="/railway-alternative"
202
+ className="font-medium text-indigo-600 transition-colors hover:text-indigo-500 dark:text-indigo-400"
203
+ >
204
+ Railway alternative
205
+ </Link>
206
+ </div>
207
+
208
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs font-medium text-slate-500 dark:text-slate-500">
209
+ <span>Built by Splitfire AB</span>
210
+ <span className="hidden sm:inline text-slate-300 dark:text-slate-700">
211
+ •
212
+ </span>
213
<Link
102
- href="/posts"
103
- className="flex w-full items-center justify-center gap-1.5 text-sm font-semibold text-slate-600 dark:text-slate-400 transition-colors hover:text-slate-900 dark:hover:text-white sm:w-auto"
214
+ href="https://github.com/smbcloudXYZ"
215
+ target="_blank"
216
+ rel="noopener noreferrer"
217
+ className="transition-colors hover:text-slate-800 dark:hover:text-slate-300"
218
>
105
- Read docs
219
+ Open source on GitHub
220
</Link>
221
+ <span className="hidden sm:inline text-slate-300 dark:text-slate-700">
222
+ •
223
+ </span>
224
+ <span>Stockholm, Sweden</span>
225
</div>
226
</div>
227
</section>
228
111
- {/* ── Terminal Demo ─────────────────────────────────────────────── */}
229
+ <section className="px-6 pb-12 lg:px-8">
230
+ <div className="mx-auto grid max-w-5xl gap-4 sm:grid-cols-2 lg:grid-cols-4">
231
+ {proofPoints.map((point) => (
232
+ <div
233
+ key={point.title}
234
+ className="rounded-xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] p-5"
235
+ >
236
+ <p className="text-sm font-semibold text-slate-900 dark:text-white">
237
+ {point.title}
238
+ </p>
239
+ <p className="mt-2 text-sm leading-relaxed text-slate-500 dark:text-slate-400">
240
+ {point.description}
241
+ </p>
242
+ </div>
243
+ ))}
244
+ </div>
245
+ </section>
246
+
247
<section className="px-6 pb-28 lg:px-8">
248
<div className="mx-auto max-w-3xl">
249
<p className="mb-8 text-center text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
@@ -118,31 +253,32 @@ export default function Home() {
253
</div>
254
</section>
255
121
- {/* ── Features ──────────────────────────────────────────────────────────── */}
256
<section className="border-t border-slate-200 dark:border-white/[0.06] px-6 py-24 lg:px-8">
257
<div className="mx-auto max-w-5xl">
124
- <div className="mb-16 text-center">
125
- <h2 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
126
- The deploy flow, without the usual mess.
258
+ <div className="mb-16 max-w-3xl">
259
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
260
+ Why smbCloud
261
+ </p>
262
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
263
+ Most teams do not need more platform.
264
</h2>
128
- <p className="mx-auto mt-4 max-w-xl text-slate-500 dark:text-slate-400">
129
- Deploy, roll back, and watch logs without wiring up half your
130
- stack first.
265
+ <p className="mt-5 text-base leading-8 text-slate-600 dark:text-slate-400">
266
+ We kept coming back to the same idea: deploys should be easy to
267
+ explain and hard to mess up. That is where smbCloud fits.
268
</p>
269
</div>
270
134
- <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
135
- {features.map((feature) => (
271
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
272
+ {reasonsToChoose.map((reason) => (
273
<div
137
- key={feature.title}
138
- className="rounded-xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] p-6 transition-colors hover:border-indigo-300 dark:hover:border-indigo-700/50 hover:bg-slate-50 dark:hover:bg-white/[0.04]"
274
+ key={reason.title}
275
+ className="rounded-2xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] p-6"
276
>
140
- <div className="mb-4 text-2xl">{feature.icon}</div>
141
- <h3 className="mb-2 font-semibold text-slate-900 dark:text-white">
142
- {feature.title}
277
+ <h3 className="text-lg font-semibold text-slate-900 dark:text-white">
278
+ {reason.title}
279
</h3>
144
- <p className="text-sm leading-relaxed text-slate-500 dark:text-slate-400">
145
- {feature.description}
280
+ <p className="mt-3 text-sm leading-7 text-slate-500 dark:text-slate-400">
281
+ {reason.description}
282
</p>
283
</div>
284
))}
@@ -150,35 +286,184 @@ export default function Home() {
286
</div>
287
</section>
288
153
- {/* ── CTA banner ──────────────────────────────────────────────────── */}
289
+ <section className="px-6 pb-24 lg:px-8">
290
+ <div className="mx-auto grid max-w-5xl gap-8 lg:grid-cols-[1.1fr_0.9fr]">
291
+ <div className="rounded-2xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] p-8">
292
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
293
+ Who it fits best
294
+ </p>
295
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
296
+ This is for teams like ours.
297
+ </h2>
298
+ <ul className="mt-6 space-y-4 text-sm leading-7 text-slate-600 dark:text-slate-400">
299
+ {bestFit.map((item) => (
300
+ <li key={item} className="flex gap-3">
301
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
302
+ <span>{item}</span>
303
+ </li>
304
+ ))}
305
+ </ul>
306
+ </div>
307
+
308
+ <div className="rounded-2xl border border-slate-200 dark:border-white/[0.06] bg-slate-50 dark:bg-white/[0.02] p-8">
309
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
310
+ Stack focus
311
+ </p>
312
+ <h2 className="mt-4 text-2xl font-bold tracking-tight text-slate-900 dark:text-white">
313
+ The stacks we care about.
314
+ </h2>
315
+ <div className="mt-6 space-y-4 text-sm text-slate-600 dark:text-slate-400">
316
+ <div className="rounded-xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.03] p-4">
317
+ <p className="font-semibold text-slate-900 dark:text-white">
318
+ Node.js
319
+ </p>
320
+ <p className="mt-1 leading-7">
321
+ A strong fit for web apps, APIs, dashboards, and backend
322
+ services.
323
+ </p>
324
+ </div>
325
+ <div className="rounded-xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.03] p-4">
326
+ <p className="font-semibold text-slate-900 dark:text-white">
327
+ Ruby on Rails
328
+ </p>
329
+ <p className="mt-1 leading-7">
330
+ Still one of my favorite stacks when I want to ship fast
331
+ without arguing with the tooling.
332
+ </p>
333
+ </div>
334
+ <div className="rounded-xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.03] p-4">
335
+ <p className="font-semibold text-slate-900 dark:text-white">
336
+ Swift / Vapor
337
+ </p>
338
+ <p className="mt-1 leading-7">
339
+ A good fit if you like Vapor and do not want to be treated
340
+ like an afterthought.
341
+ </p>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </section>
347
+
348
+ <section className="border-t border-slate-200 dark:border-white/[0.06] px-6 py-24 lg:px-8">
349
+ <div className="mx-auto max-w-5xl">
350
+ <div className="mb-14 max-w-3xl">
351
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
352
+ Compare platforms
353
+ </p>
354
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
355
+ Looking at Heroku or Railway?
356
+ </h2>
357
+ <p className="mt-5 text-base leading-8 text-slate-600 dark:text-slate-400">
358
+ Fair enough. We wrote the comparison pages I would actually want
359
+ to read before switching: direct, specific, and honest about fit.
360
+ </p>
361
+ </div>
362
+
363
+ <div className="grid gap-6 lg:grid-cols-2">
364
+ {comparisonPages.map((page) => (
365
+ <Link
366
+ key={page.href}
367
+ href={page.href}
368
+ className="group rounded-2xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-white/[0.02] p-8 transition-colors hover:border-indigo-300 dark:hover:border-indigo-700/50 hover:bg-slate-50 dark:hover:bg-white/[0.04]"
369
+ >
370
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
371
+ {page.eyebrow}
372
+ </p>
373
+ <h3 className="mt-4 text-2xl font-semibold tracking-tight text-slate-900 transition-colors group-hover:text-indigo-600 dark:text-white dark:group-hover:text-indigo-400">
374
+ {page.title}
375
+ </h3>
376
+ <p className="mt-4 text-sm leading-7 text-slate-600 dark:text-slate-400">
377
+ {page.description}
378
+ </p>
379
+ <ul className="mt-5 space-y-3 text-sm leading-6 text-slate-500 dark:text-slate-400">
380
+ {page.bullets.map((bullet) => (
381
+ <li key={bullet} className="flex gap-3">
382
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
383
+ <span>{bullet}</span>
384
+ </li>
385
+ ))}
386
+ </ul>
387
+ <p className="mt-6 text-sm font-semibold text-indigo-600 dark:text-indigo-400">
388
+ Read comparison →
389
+ </p>
390
+ </Link>
391
+ ))}
392
+ </div>
393
+ </div>
394
+ </section>
395
+
396
<section className="px-6 py-16 lg:px-8">
397
<div className="mx-auto max-w-2xl">
398
<div className="relative overflow-hidden rounded-2xl bg-indigo-600 px-8 py-14 text-center shadow-2xl shadow-indigo-500/20">
157
- {/* Inner glow */}
399
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.1),transparent)]" />
400
<h2 className="relative text-3xl font-bold tracking-tight text-white">
160
- Try smbCloud with a real app.
401
+ Ready to ship?
402
</h2>
403
<p className="relative mt-4 text-indigo-200">
163
- Start free, deploy something small, and see if it fits your
164
- workflow. No credit card required.
404
+ Create an account, install the CLI, and see if it feels better on
405
+ a real project.
406
</p>
166
- <Link
167
- href="https://console.smbcloud.xyz"
168
- target="_blank"
169
- className="relative mt-8 inline-block rounded-lg bg-white px-8 py-3.5 text-sm font-semibold text-indigo-700 shadow-sm transition-colors hover:bg-indigo-50"
170
- >
171
- Create your free account →
172
- </Link>
407
+ <div className="relative mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
408
+ <Link
409
+ href="https://console.smbcloud.xyz"
410
+ target="_blank"
411
+ rel="noopener noreferrer"
412
+ className="inline-flex rounded-lg bg-white px-8 py-3.5 text-sm font-semibold text-indigo-700 shadow-sm transition-colors hover:bg-indigo-50"
413
+ >
414
+ Create free account →
415
+ </Link>
416
+ <Link
417
+ href="/services"
418
+ className="inline-flex rounded-lg border border-white/20 px-8 py-3.5 text-sm font-semibold text-white transition-colors hover:bg-white/10"
419
+ >
420
+ Explore services
421
+ </Link>
422
+ </div>
423
</div>
424
</div>
425
</section>
426
177
- {/* ── siGit CTA ───────────────────────────────────────────────────────── */}
427
+ {publishedPosts.length > 0 && (
428
+ <section className="border-t border-slate-200 dark:border-white/[0.06] px-6 py-24 lg:px-8">
429
+ <div className="mx-auto max-w-5xl">
430
+ <div className="mb-10 flex items-end justify-between gap-4">
431
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
432
+ From the blog
433
+ </p>
434
+ <Link
435
+ href="/posts"
436
+ className="text-sm font-semibold text-indigo-600 transition-colors hover:text-indigo-500 dark:text-indigo-400"
437
+ >
438
+ View all posts
439
+ </Link>
440
+ </div>
441
+ <div className="space-y-6">
442
+ {publishedPosts.slice(0, 3).map((post) => (
443
+ <article
444
+ key={post._id}
445
+ className="group border-b border-slate-200 dark:border-white/[0.06] pb-6 last:border-0"
446
+ >
447
+ <Link href={post.slug} className="block">
448
+ <h3 className="font-semibold text-slate-900 dark:text-white transition-colors group-hover:text-indigo-600 dark:group-hover:text-indigo-400">
449
+ {post.title}
450
+ </h3>
451
+ {post.description && (
452
+ <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
453
+ {post.description}
454
+ </p>
455
+ )}
456
+ </Link>
457
+ </article>
458
+ ))}
459
+ </div>
460
+ </div>
461
+ </section>
462
+ )}
463
+
464
<section className="px-6 pb-16 lg:px-8">
465
<div className="mx-auto max-w-2xl">
180
- <div className="relative overflow-hidden rounded-2xl bg-slate-900 dark:bg-white/[0.04] border border-slate-800 dark:border-white/[0.08] px-8 py-12">
181
- {/* Subtle top highlight */}
466
+ <div className="relative overflow-hidden rounded-2xl border border-slate-800 bg-slate-900 px-8 py-12 dark:border-white/[0.08] dark:bg-white/[0.04]">
467
<div className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/20 to-transparent" />
468
469
<p className="mb-5 text-xs font-bold uppercase tracking-[0.2em] text-slate-500">
@@ -186,7 +471,6 @@ export default function Home() {
471
</p>
472
473
<div className="mb-4 flex items-center gap-3">
189
- {/* siGit mark */}
474
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white text-slate-900">
475
<span className="text-xs font-black tracking-tighter leading-none">
476
si
@@ -206,7 +490,6 @@ export default function Home() {
490
Available on iPhone, Android, and Windows.
491
</p>
492
209
- {/* Platform buttons */}
493
<div className="flex flex-wrap gap-3">
494
<a
495
href="https://apps.apple.com/se/app/sigit-git-client-code-editor/id6753018849"
@@ -239,36 +522,6 @@ export default function Home() {
522
</div>
523
</div>
524
</section>
242
-
243
- {/* ── Blog Posts ───────────────────────────────────────────────────────── */}
244
- {publishedPosts.length > 0 && (
245
- <section className="border-t border-slate-200 dark:border-white/[0.06] px-6 py-24 lg:px-8">
246
- <div className="mx-auto max-w-5xl">
247
- <p className="mb-10 text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
248
- From the blog
249
- </p>
250
- <div className="space-y-6">
251
- {publishedPosts.slice(0, 3).map((post) => (
252
- <article
253
- key={post._id}
254
- className="group border-b border-slate-200 dark:border-white/[0.06] pb-6 last:border-0"
255
- >
256
- <Link href={post.slug} className="block">
257
- <h3 className="font-semibold text-slate-900 dark:text-white transition-colors group-hover:text-indigo-600 dark:group-hover:text-indigo-400">
258
- {post.title}
259
- </h3>
260
- {post.description && (
261
- <p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
262
- {post.description}
263
- </p>
264
- )}
265
- </Link>
266
- </article>
267
- ))}
268
- </div>
269
- </div>
270
- </section>
271
- )}
525
</div>
526
);
527
}
app/posts/[...slug]/page.tsx
+109
-18
@@ -1,8 +1,11 @@
1
import { notFound } from "next/navigation";
2
import { allPosts } from "contentlayer/generated";
3
-
4
-import { Metadata } from "next";
3
+import type { Metadata } from "next";
4
+import Link from "next/link";
5
import { Mdx } from "@/components/mdx-components";
6
+import { StructuredData } from "@/components/structured-data";
7
+import siteMetadata from "@/content/siteMetadata";
8
+import { absoluteUrl, genPageMetadata } from "@/app/seo";
9
10
interface PostProps {
11
params: Promise<{
@@ -16,11 +19,7 @@ async function getPostFromParams(params: Awaited<PostProps["params"]>) {
19
.filter((post) => post.published)
20
.find((post) => post.slugAsParams === slug);
21
19
- if (!post) {
20
- null;
21
- }
22
-
23
- return post;
22
+ return post ?? null;
23
}
24
25
export async function generateMetadata(props: PostProps): Promise<Metadata> {
@@ -31,17 +30,26 @@ export async function generateMetadata(props: PostProps): Promise<Metadata> {
30
return {};
31
}
32
34
- return {
33
+ return genPageMetadata({
34
title: post.title,
35
description: post.description,
37
- metadataBase: new URL("https://smbcloud.xyz"),
38
- };
36
+ path: post.slug,
37
+ type: "article",
38
+ openGraph: {
39
+ type: "article",
40
+ publishedTime: post.date,
41
+ modifiedTime: post.lastmod,
42
+ authors: [siteMetadata.author],
43
+ },
44
+ });
45
}
46
47
export async function generateStaticParams() {
42
- return allPosts.map((post) => ({
43
- slug: post.slugAsParams.split("/"),
44
- }));
48
+ return allPosts
49
+ .filter((post) => post.published)
50
+ .map((post) => ({
51
+ slug: post.slugAsParams.split("/"),
52
+ }));
53
}
54
55
export default async function PostPage(props: PostProps) {
@@ -52,20 +60,103 @@ export default async function PostPage(props: PostProps) {
60
notFound();
61
}
62
63
+ const postStructuredData = {
64
+ "@context": "https://schema.org",
65
+ "@graph": [
66
+ {
67
+ "@type": "BlogPosting",
68
+ headline: post.title,
69
+ description: post.description,
70
+ url: absoluteUrl(post.slug),
71
+ datePublished: post.date,
72
+ dateModified: post.lastmod,
73
+ author: {
74
+ "@type": "Organization",
75
+ name: siteMetadata.author,
76
+ },
77
+ publisher: {
78
+ "@id": absoluteUrl("/#organization"),
79
+ },
80
+ mainEntityOfPage: {
81
+ "@type": "WebPage",
82
+ "@id": absoluteUrl(post.slug),
83
+ },
84
+ },
85
+ {
86
+ "@type": "BreadcrumbList",
87
+ itemListElement: [
88
+ {
89
+ "@type": "ListItem",
90
+ position: 1,
91
+ name: "Home",
92
+ item: absoluteUrl("/"),
93
+ },
94
+ {
95
+ "@type": "ListItem",
96
+ position: 2,
97
+ name: "Blog",
98
+ item: absoluteUrl("/posts"),
99
+ },
100
+ {
101
+ "@type": "ListItem",
102
+ position: 3,
103
+ name: post.title,
104
+ item: absoluteUrl(post.slug),
105
+ },
106
+ ],
107
+ },
108
+ ],
109
+ };
110
+
111
return (
112
<div className="mx-auto max-w-3xl px-6 py-12 lg:px-8">
113
+ <StructuredData data={postStructuredData} />
114
+
115
+ <nav className="mb-8 flex items-center gap-2 text-xs text-slate-400 dark:text-slate-600">
116
+ <Link
117
+ href="/"
118
+ className="transition-colors hover:text-slate-700 dark:hover:text-slate-300"
119
+ >
120
+ Home
121
+ </Link>
122
+ <span>/</span>
123
+ <Link
124
+ href="/posts"
125
+ className="transition-colors hover:text-slate-700 dark:hover:text-slate-300"
126
+ >
127
+ Blog
128
+ </Link>
129
+ <span>/</span>
130
+ <span className="text-slate-600 dark:text-slate-400">{post.title}</span>
131
+ </nav>
132
+
133
<article className="prose dark:prose-invert max-w-none">
134
<h1 className="mb-2">{post.title}</h1>
135
+ <div className="mb-4 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-slate-500 dark:text-slate-400">
136
+ <time dateTime={post.date}>
137
+ Published{" "}
138
+ {new Date(post.date).toLocaleDateString("en-US", {
139
+ year: "numeric",
140
+ month: "long",
141
+ day: "numeric",
142
+ })}
143
+ </time>
144
+ {post.lastmod && (
145
+ <time dateTime={post.lastmod}>
146
+ Updated{" "}
147
+ {new Date(post.lastmod).toLocaleDateString("en-US", {
148
+ year: "numeric",
149
+ month: "long",
150
+ day: "numeric",
151
+ })}
152
+ </time>
153
+ )}
154
+ </div>
155
{post.description && (
156
<p className="text-xl mt-0 text-slate-700 dark:text-slate-200">
157
{post.description}
158
</p>
159
)}
64
- {post.lastmod && (
65
- <p className="italic text-base text-gray-500 mt-2 mb-0">
66
- Last updated: {new Date(post.lastmod).toLocaleDateString()}
67
- </p>
68
- )}
160
<hr className="my-4" />
161
<Mdx code={post.body.code} />
162
</article>
app/posts/page.tsx
+69
-9
@@ -1,13 +1,72 @@
1
+import type { Metadata } from "next";
2
import { allPosts } from "@/.contentlayer/generated";
3
import Link from "next/link";
3
-import { genPageMetadata } from "../seo";
4
+import { StructuredData } from "@/components/structured-data";
5
+import siteMetadata from "@/content/siteMetadata";
6
+import { absoluteUrl, genPageMetadata } from "../seo";
7
5
-export const metadata = {
8
+export const metadata: Metadata = {
9
...genPageMetadata({
7
- title: "Posts",
8
- description: "Updates, guides, and deep dives from the smbCloud team.",
10
+ title: "Blog",
11
+ description:
12
+ "Product updates, deployment guides, and infrastructure notes from the smbCloud team.",
13
+ path: "/posts",
14
+ keywords: [
15
+ "smbCloud blog",
16
+ "deployment guides",
17
+ "Node.js deployment guide",
18
+ "Rails deployment guide",
19
+ "Vapor deployment guide",
20
+ ],
21
}),
10
- metadataBase: new URL("https://smbcloud.xyz"),
22
+};
23
+
24
+const blogStructuredData = {
25
+ "@context": "https://schema.org",
26
+ "@graph": [
27
+ {
28
+ "@type": "CollectionPage",
29
+ "@id": absoluteUrl("/posts#webpage"),
30
+ url: absoluteUrl("/posts"),
31
+ name: "smbCloud Blog",
32
+ description:
33
+ "Product updates, deployment guides, and infrastructure notes from the smbCloud team.",
34
+ isPartOf: {
35
+ "@id": absoluteUrl("/#website"),
36
+ },
37
+ about: {
38
+ "@id": absoluteUrl("/#organization"),
39
+ },
40
+ },
41
+ {
42
+ "@type": "BreadcrumbList",
43
+ itemListElement: [
44
+ {
45
+ "@type": "ListItem",
46
+ position: 1,
47
+ name: "Home",
48
+ item: absoluteUrl("/"),
49
+ },
50
+ {
51
+ "@type": "ListItem",
52
+ position: 2,
53
+ name: "Blog",
54
+ item: absoluteUrl("/posts"),
55
+ },
56
+ ],
57
+ },
58
+ {
59
+ "@type": "Blog",
60
+ name: "smbCloud Blog",
61
+ url: absoluteUrl("/posts"),
62
+ description:
63
+ "Product updates, deployment guides, and infrastructure notes from the smbCloud team.",
64
+ publisher: {
65
+ "@id": absoluteUrl("/#organization"),
66
+ },
67
+ inLanguage: siteMetadata.language,
68
+ },
69
+ ],
70
};
71
72
export default function PostsPage() {
@@ -18,20 +77,21 @@ export default function PostsPage() {
77
78
return (
79
<div className="mx-auto max-w-3xl px-6 py-16 lg:px-8">
21
- {/* Page header */}
80
+ <StructuredData data={blogStructuredData} />
81
+
82
<div className="mb-14">
83
<p className="mb-3 text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
84
Blog
85
</p>
86
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
27
- Posts
87
+ smbCloud Blog
88
</h1>
89
<p className="mt-3 text-slate-500 dark:text-slate-400">
30
- Updates, guides, and deep dives from the smbCloud team.
90
+ Product updates, deployment guides, and infrastructure notes from the
91
+ smbCloud team.
92
</p>
93
</div>
94
34
- {/* Post list */}
95
{publishedPosts.length === 0 ? (
96
<p className="text-sm text-slate-400 dark:text-slate-600">
97
No posts published yet. Check back soon.
app/railway-alternative/page.tsx
new
+376
@@ -0,0 +1,376 @@
1
+import type { Metadata } from "next";
2
+import Link from "next/link";
3
+import { StructuredData } from "@/components/structured-data";
4
+import { absoluteUrl, genPageMetadata } from "../seo";
5
+
6
+const comparisonRows = [
7
+ {
8
+ label: "Deploy workflow",
9
+ smbcloud: "Run `smb`, deploy over SSH, and watch the logs.",
10
+ railway:
11
+ "Quick platform workflow for apps and services, with more platform around it.",
12
+ },
13
+ {
14
+ label: "Best fit",
15
+ smbcloud:
16
+ "Small teams that want a narrower product and a simpler deploy story.",
17
+ railway: "Teams that like Railway's broader approach and flexibility.",
18
+ },
19
+ {
20
+ label: "Runtime focus",
21
+ smbcloud: "Node.js, Ruby on Rails, and Swift/Vapor.",
22
+ railway: "General app platform across more service shapes.",
23
+ },
24
+ {
25
+ label: "Why teams pick it",
26
+ smbcloud: "Because the smaller surface area feels easier to work with.",
27
+ railway: "Because they want more flexibility under one roof.",
28
+ },
29
+];
30
+
31
+const smbcloudFits = [
32
+ "You want a calmer, more focused deploy flow.",
33
+ "You are mostly shipping Node.js, Rails, or Vapor.",
34
+ "You prefer SSH and straightforward app operations.",
35
+ "You would rather have less platform to think about.",
36
+];
37
+
38
+const railwayStillFits = [
39
+ "You want one platform for lots of service types.",
40
+ "Your team already likes how Railway works.",
41
+ "You need flexibility beyond smbCloud's current focus.",
42
+];
43
+
44
+const bestFor = [
45
+ "Founders and small product teams",
46
+ "Agencies that want a repeatable deploy workflow",
47
+ "Node.js backends and dashboards",
48
+ "Rails apps",
49
+ "Vapor apps",
50
+];
51
+
52
+const faqs = [
53
+ {
54
+ question: "Is smbCloud better than Railway?",
55
+ answer:
56
+ "Depends what you want. If you want a simpler SSH-first deploy flow for Node.js, Rails, or Vapor, maybe yes. If you want a broader platform, probably not.",
57
+ },
58
+ {
59
+ question: "What makes smbCloud different from Railway?",
60
+ answer: "It is narrower. That is intentional.",
61
+ },
62
+ {
63
+ question: "Who should consider switching from Railway?",
64
+ answer:
65
+ "Teams that want less platform in the middle and a deploy flow that feels easier to follow.",
66
+ },
67
+];
68
+
69
+export const metadata: Metadata = {
70
+ ...genPageMetadata({
71
+ title: "Railway alternative for Node.js, Rails, and Vapor",
72
+ description:
73
+ "smbCloud is a Railway alternative for small teams that want an SSH-first deploy workflow for Node.js, Ruby on Rails, and Swift/Vapor apps.",
74
+ path: "/railway-alternative",
75
+ keywords: [
76
+ "Railway alternative",
77
+ "Railway replacement",
78
+ "SSH-first deploy platform",
79
+ "deploy Node.js app",
80
+ "deploy Ruby on Rails app",
81
+ "deploy Vapor app",
82
+ "small team hosting platform",
83
+ ],
84
+ }),
85
+};
86
+
87
+const structuredData = {
88
+ "@context": "https://schema.org",
89
+ "@graph": [
90
+ {
91
+ "@type": "WebPage",
92
+ "@id": absoluteUrl("/railway-alternative#webpage"),
93
+ url: absoluteUrl("/railway-alternative"),
94
+ name: "Railway alternative for small teams",
95
+ description:
96
+ "Compare smbCloud and Railway for small-team app deployment workflows.",
97
+ isPartOf: {
98
+ "@id": absoluteUrl("/#website"),
99
+ },
100
+ },
101
+ {
102
+ "@type": "BreadcrumbList",
103
+ itemListElement: [
104
+ {
105
+ "@type": "ListItem",
106
+ position: 1,
107
+ name: "Home",
108
+ item: absoluteUrl("/"),
109
+ },
110
+ {
111
+ "@type": "ListItem",
112
+ position: 2,
113
+ name: "Railway alternative",
114
+ item: absoluteUrl("/railway-alternative"),
115
+ },
116
+ ],
117
+ },
118
+ {
119
+ "@type": "FAQPage",
120
+ mainEntity: faqs.map((faq) => ({
121
+ "@type": "Question",
122
+ name: faq.question,
123
+ acceptedAnswer: {
124
+ "@type": "Answer",
125
+ text: faq.answer,
126
+ },
127
+ })),
128
+ },
129
+ ],
130
+};
131
+
132
+export default function RailwayAlternativePage() {
133
+ return (
134
+ <div>
135
+ <StructuredData data={structuredData} />
136
+
137
+ <section className="relative overflow-hidden px-6 pt-24 pb-16 sm:pt-32 sm:pb-20 lg:px-8">
138
+ <div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[520px] bg-[radial-gradient(ellipse_75%_50%_at_50%_0%,rgba(99,102,241,0.18),transparent)] dark:bg-[radial-gradient(ellipse_75%_50%_at_50%_0%,rgba(99,102,241,0.12),transparent)]" />
139
+
140
+ <div className="mx-auto max-w-4xl">
141
+ <div className="mb-6 inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-4 py-1.5 dark:border-indigo-500/30 dark:bg-indigo-500/10">
142
+ <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
143
+ <span className="text-xs font-semibold uppercase tracking-widest text-indigo-700 dark:text-indigo-400">
144
+ Railway alternative
145
+ </span>
146
+ </div>
147
+
148
+ <h1 className="max-w-3xl text-4xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-6xl leading-[1.05]">
149
+ Looking for a Railway alternative?
150
+ </h1>
151
+
152
+ <p className="mt-6 max-w-3xl text-lg leading-8 text-slate-600 dark:text-slate-400">
153
+ We built smbCloud for teams that like fast deploys but do not want
154
+ the platform to keep getting bigger around the work. SSH-first, one
155
+ command, not much ceremony.
156
+ </p>
157
+
158
+ <div className="mt-8 flex flex-col gap-4 sm:flex-row">
159
+ <Link
160
+ href="https://console.smbcloud.xyz"
161
+ target="_blank"
162
+ rel="noopener noreferrer"
163
+ className="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-8 py-3.5 text-sm font-semibold text-white shadow-lg shadow-indigo-500/20 transition-all hover:bg-indigo-500 hover:shadow-indigo-500/30 active:scale-[0.98]"
164
+ >
165
+ Start deploying
166
+ </Link>
167
+ <Link
168
+ href="/posts/cli"
169
+ className="inline-flex items-center justify-center rounded-lg border border-slate-300 px-8 py-3.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 dark:border-white/[0.12] dark:text-slate-200 dark:hover:bg-white/[0.04]"
170
+ >
171
+ Install the CLI
172
+ </Link>
173
+ </div>
174
+ </div>
175
+ </section>
176
+
177
+ <section className="px-6 pb-24 lg:px-8">
178
+ <div className="mx-auto max-w-5xl grid gap-8 lg:grid-cols-[0.95fr_1.05fr]">
179
+ <div>
180
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
181
+ Why people leave Railway
182
+ </p>
183
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
184
+ Usually they are not asking for more features. They want less to
185
+ think about.
186
+ </h2>
187
+ <p className="mt-5 text-base leading-8 text-slate-600 dark:text-slate-400">
188
+ Railway is fast to get started with. I get the appeal. But some
189
+ teams eventually decide the broader platform is not actually the
190
+ part they wanted.
191
+ </p>
192
+ </div>
193
+
194
+ <div className="rounded-2xl border border-slate-200 bg-slate-50 p-8 dark:border-white/[0.06] dark:bg-white/[0.02]">
195
+ <p className="text-sm font-semibold text-slate-900 dark:text-white">
196
+ What smbCloud does well
197
+ </p>
198
+ <ul className="mt-5 space-y-4 text-sm leading-7 text-slate-600 dark:text-slate-400">
199
+ <li className="flex gap-3">
200
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
201
+ <span>Deploy over SSH with one command.</span>
202
+ </li>
203
+ <li className="flex gap-3">
204
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
205
+ <span>Support for Node.js, Rails, and Swift/Vapor.</span>
206
+ </li>
207
+ <li className="flex gap-3">
208
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
209
+ <span>A smaller surface area, which is kind of the point.</span>
210
+ </li>
211
+ </ul>
212
+ </div>
213
+ </div>
214
+ </section>
215
+
216
+ <section className="border-t border-slate-200 px-6 py-24 dark:border-white/[0.06] lg:px-8">
217
+ <div className="mx-auto max-w-5xl">
218
+ <div className="mb-12 max-w-3xl">
219
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
220
+ smbCloud vs Railway
221
+ </p>
222
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
223
+ If you want a focused platform, this is the trade.
224
+ </h2>
225
+ </div>
226
+
227
+ <div className="overflow-hidden rounded-2xl border border-slate-200 dark:border-white/[0.06]">
228
+ <div className="grid grid-cols-[0.8fr_1fr_1fr] bg-slate-50 text-sm font-semibold text-slate-900 dark:bg-white/[0.03] dark:text-white">
229
+ <div className="border-b border-slate-200 px-5 py-4 dark:border-white/[0.06]">
230
+ Area
231
+ </div>
232
+ <div className="border-b border-l border-slate-200 px-5 py-4 dark:border-white/[0.06]">
233
+ smbCloud
234
+ </div>
235
+ <div className="border-b border-l border-slate-200 px-5 py-4 dark:border-white/[0.06]">
236
+ Railway
237
+ </div>
238
+ </div>
239
+ {comparisonRows.map((row) => (
240
+ <div
241
+ key={row.label}
242
+ className="grid grid-cols-[0.8fr_1fr_1fr] text-sm leading-7 text-slate-600 dark:text-slate-400"
243
+ >
244
+ <div className="border-b border-slate-200 bg-white px-5 py-5 font-semibold text-slate-900 dark:border-white/[0.06] dark:bg-white/[0.02] dark:text-white">
245
+ {row.label}
246
+ </div>
247
+ <div className="border-b border-l border-slate-200 bg-white px-5 py-5 dark:border-white/[0.06] dark:bg-white/[0.02]">
248
+ {row.smbcloud}
249
+ </div>
250
+ <div className="border-b border-l border-slate-200 bg-white px-5 py-5 dark:border-white/[0.06] dark:bg-white/[0.02]">
251
+ {row.railway}
252
+ </div>
253
+ </div>
254
+ ))}
255
+ </div>
256
+ </div>
257
+ </section>
258
+
259
+ <section className="px-6 py-24 lg:px-8">
260
+ <div className="mx-auto grid max-w-5xl gap-6 lg:grid-cols-2">
261
+ <div className="rounded-2xl border border-slate-200 bg-white p-8 dark:border-white/[0.06] dark:bg-white/[0.02]">
262
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
263
+ Pick smbCloud if
264
+ </p>
265
+ <ul className="mt-6 space-y-4 text-sm leading-7 text-slate-600 dark:text-slate-400">
266
+ {smbcloudFits.map((item) => (
267
+ <li key={item} className="flex gap-3">
268
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
269
+ <span>{item}</span>
270
+ </li>
271
+ ))}
272
+ </ul>
273
+ </div>
274
+
275
+ <div className="rounded-2xl border border-slate-200 bg-slate-50 p-8 dark:border-white/[0.06] dark:bg-white/[0.02]">
276
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
277
+ Stay on Railway if
278
+ </p>
279
+ <ul className="mt-6 space-y-4 text-sm leading-7 text-slate-600 dark:text-slate-400">
280
+ {railwayStillFits.map((item) => (
281
+ <li key={item} className="flex gap-3">
282
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-slate-400" />
283
+ <span>{item}</span>
284
+ </li>
285
+ ))}
286
+ </ul>
287
+ </div>
288
+ </div>
289
+ </section>
290
+
291
+ <section className="border-t border-slate-200 px-6 py-24 dark:border-white/[0.06] lg:px-8">
292
+ <div className="mx-auto max-w-5xl grid gap-8 lg:grid-cols-[0.95fr_1.05fr]">
293
+ <div>
294
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
295
+ Best fit
296
+ </p>
297
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
298
+ I would look at smbCloud if you keep thinking, "I just want to
299
+ deploy the app."
300
+ </h2>
301
+ </div>
302
+ <ul className="rounded-2xl border border-slate-200 bg-white p-8 text-sm leading-7 text-slate-600 dark:border-white/[0.06] dark:bg-white/[0.02] dark:text-slate-400">
303
+ {bestFor.map((item) => (
304
+ <li
305
+ key={item}
306
+ className="flex gap-3 border-b border-slate-200 py-3 last:border-0 last:pb-0 first:pt-0 dark:border-white/[0.06]"
307
+ >
308
+ <span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-indigo-500" />
309
+ <span>{item}</span>
310
+ </li>
311
+ ))}
312
+ </ul>
313
+ </div>
314
+ </section>
315
+
316
+ <section className="px-6 pb-24 lg:px-8">
317
+ <div className="mx-auto max-w-3xl">
318
+ <div className="mb-10">
319
+ <p className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-600">
320
+ FAQ
321
+ </p>
322
+ <h2 className="mt-4 text-3xl font-bold tracking-tight text-slate-900 dark:text-white">
323
+ Common questions
324
+ </h2>
325
+ </div>
326
+ <div className="space-y-4">
327
+ {faqs.map((faq) => (
328
+ <div
329
+ key={faq.question}
330
+ className="rounded-2xl border border-slate-200 bg-white p-6 dark:border-white/[0.06] dark:bg-white/[0.02]"
331
+ >
332
+ <h3 className="text-lg font-semibold text-slate-900 dark:text-white">
333
+ {faq.question}
334
+ </h3>
335
+ <p className="mt-3 text-sm leading-7 text-slate-600 dark:text-slate-400">
336
+ {faq.answer}
337
+ </p>
338
+ </div>
339
+ ))}
340
+ </div>
341
+ </div>
342
+ </section>
343
+
344
+ <section className="px-6 pb-20 lg:px-8">
345
+ <div className="mx-auto max-w-3xl">
346
+ <div className="relative overflow-hidden rounded-2xl bg-indigo-600 px-8 py-14 text-center shadow-2xl shadow-indigo-500/20">
347
+ <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.1),transparent)]" />
348
+ <h2 className="relative text-3xl font-bold tracking-tight text-white">
349
+ If Railway feels a bit too broad, try smbCloud.
350
+ </h2>
351
+ <p className="relative mt-4 text-indigo-200">
352
+ Try it on one app. You will know pretty quickly if the smaller
353
+ workflow is what you wanted.
354
+ </p>
355
+ <div className="relative mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
356
+ <Link
357
+ href="https://console.smbcloud.xyz"
358
+ target="_blank"
359
+ rel="noopener noreferrer"
360
+ className="inline-flex rounded-lg bg-white px-8 py-3.5 text-sm font-semibold text-indigo-700 shadow-sm transition-colors hover:bg-indigo-50"
361
+ >
362
+ Try smbCloud
363
+ </Link>
364
+ <Link
365
+ href="/heroku-alternative"
366
+ className="inline-flex rounded-lg border border-white/20 px-8 py-3.5 text-sm font-semibold text-white transition-colors hover:bg-white/10"
367
+ >
368
+ Compare Heroku alternative
369
+ </Link>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ </section>
374
+ </div>
375
+ );
376
+}
app/seo.tsx
+47
-12
@@ -1,35 +1,70 @@
1
-import { Metadata } from "next";
1
+import type { Metadata } from "next";
2
import siteMetadata from "@/content/siteMetadata";
3
4
-interface PageSEOProps {
4
+interface PageSEOProps extends Omit<
5
+ Metadata,
6
+ "title" | "description" | "alternates" | "openGraph" | "twitter"
7
+> {
8
title: string;
9
description?: string;
10
image?: string;
8
- [key: string]: any;
11
+ path?: string;
12
+ locale?: string;
13
+ type?: "website" | "article";
14
+ alternates?: Metadata["alternates"];
15
+ openGraph?: NonNullable<Metadata["openGraph"]>;
16
+ twitter?: NonNullable<Metadata["twitter"]>;
17
+}
18
+
19
+export function absoluteUrl(path = "/") {
20
+ return new URL(path, siteMetadata.siteUrl).toString();
21
+}
22
+
23
+function formatTitle(title: string) {
24
+ return title === siteMetadata.title
25
+ ? title
26
+ : `${title} | ${siteMetadata.title}`;
27
}
28
29
export function genPageMetadata({
30
title,
31
description,
32
image,
33
+ path = "/",
34
+ locale = "en_US",
35
+ type = "website",
36
+ alternates,
37
+ openGraph,
38
+ twitter,
39
...rest
40
}: PageSEOProps): Metadata {
41
+ const pageDescription = description || siteMetadata.description;
42
+ const pageUrl = absoluteUrl(path);
43
+ const socialImage = absoluteUrl(image || siteMetadata.socialBanner);
44
+
45
return {
46
title,
19
- description: description || siteMetadata.description,
47
+ description: pageDescription,
48
+ alternates: {
49
+ canonical: pageUrl,
50
+ ...alternates,
51
+ },
52
openGraph: {
21
- title: `${title} | ${siteMetadata.title}`,
22
- description: description || siteMetadata.description,
23
- url: "./",
53
+ title: formatTitle(title),
54
+ description: pageDescription,
55
+ url: pageUrl,
56
siteName: siteMetadata.title,
25
- images: image ? [image] : [siteMetadata.socialBanner],
26
- locale: "en_US",
27
- type: "website",
57
+ images: [socialImage],
58
+ locale,
59
+ type,
60
+ ...openGraph,
61
},
62
twitter: {
30
- title: `${title} | ${siteMetadata.title}`,
63
+ title: formatTitle(title),
64
+ description: pageDescription,
65
card: "summary_large_image",
32
- images: image ? [image] : [siteMetadata.socialBanner],
66
+ images: [socialImage],
67
+ ...twitter,
68
},
69
...rest,
70
};
app/services/page.tsx
+51
-4
@@ -1,7 +1,8 @@
1
import type { Metadata } from "next";
2
import Link from "next/link";
3
-import { genPageMetadata } from "../seo";
3
+import { StructuredData } from "@/components/structured-data";
4
import siteMetadata from "@/content/siteMetadata";
5
+import { absoluteUrl, genPageMetadata } from "../seo";
6
7
const services = [
8
{
@@ -55,6 +56,7 @@ export const metadata: Metadata = {
56
title: "Services",
57
description:
58
"Explore smbCloud services for deploys, auth, mail, and GresIQ. One platform for shipping apps, handling accounts, routing email, and managing the data layer.",
59
+ path: "/services",
60
}),
61
keywords: [
62
"smbCloud services",
@@ -84,8 +86,53 @@ export const metadata: Metadata = {
86
};
87
88
export default function ServicesPage() {
89
+ const servicesStructuredData = {
90
+ "@context": "https://schema.org",
91
+ "@graph": [
92
+ {
93
+ "@type": "CollectionPage",
94
+ "@id": absoluteUrl("/services#webpage"),
95
+ url: absoluteUrl("/services"),
96
+ name: "smbCloud Services",
97
+ description:
98
+ "Deploy, Auth, Mail, and GresIQ live under one smbCloud platform instead of four disconnected tools.",
99
+ isPartOf: {
100
+ "@id": absoluteUrl("/#website"),
101
+ },
102
+ },
103
+ {
104
+ "@type": "BreadcrumbList",
105
+ itemListElement: [
106
+ {
107
+ "@type": "ListItem",
108
+ position: 1,
109
+ name: "Home",
110
+ item: absoluteUrl("/"),
111
+ },
112
+ {
113
+ "@type": "ListItem",
114
+ position: 2,
115
+ name: "Services",
116
+ item: absoluteUrl("/services"),
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ "@type": "ItemList",
122
+ name: "smbCloud services",
123
+ itemListElement: services.map((service, index) => ({
124
+ "@type": "ListItem",
125
+ position: index + 1,
126
+ name: service.name,
127
+ description: service.description,
128
+ })),
129
+ },
130
+ ],
131
+ };
132
+
133
return (
134
<div>
135
+ <StructuredData data={servicesStructuredData} />
136
<section className="relative overflow-hidden px-6 pt-24 pb-16 sm:pt-32 sm:pb-20 lg:px-8">
137
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[520px] bg-[radial-gradient(ellipse_75%_50%_at_50%_0%,rgba(99,102,241,0.18),transparent)] dark:bg-[radial-gradient(ellipse_75%_50%_at_50%_0%,rgba(99,102,241,0.12),transparent)]" />
138
@@ -102,9 +149,9 @@ export default function ServicesPage() {
149
</h1>
150
151
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-600 dark:text-slate-400">
105
- smbCloud is built around four core services: Deploy, Auth, Mail, and
106
- GresIQ. They are separate products, but they belong together. That
107
- is the point.
152
+ Deploy apps, handle auth, route domain email, and keep the data
153
+ layer close to the rest of your stack without stitching together
154
+ four disconnected vendors.
155
</p>
156
157
<div className="mt-8 flex flex-col gap-4 sm:flex-row">
app/sitemap.ts
+57
-14
@@ -1,24 +1,67 @@
1
-import { MetadataRoute } from 'next'
2
-import { allPages, allPosts } from 'contentlayer/generated'
3
-import siteMetadata from '@/content/siteMetadata'
1
+import { MetadataRoute } from "next";
2
+import { allKtis, allPages, allPosts } from "contentlayer/generated";
3
+import { absoluteUrl } from "./seo";
4
5
-export const dynamic = 'force-static'
5
+export const dynamic = "force-static";
6
7
export default function sitemap(): MetadataRoute.Sitemap {
8
- const siteUrl = siteMetadata.siteUrl
8
+ const staticRoutes: MetadataRoute.Sitemap = [
9
+ {
10
+ url: absoluteUrl("/"),
11
+ changeFrequency: "weekly",
12
+ priority: 1,
13
+ },
14
+ {
15
+ url: absoluteUrl("/services"),
16
+ changeFrequency: "weekly",
17
+ priority: 0.9,
18
+ },
19
+ {
20
+ url: absoluteUrl("/posts"),
21
+ changeFrequency: "weekly",
22
+ priority: 0.8,
23
+ },
24
+ {
25
+ url: absoluteUrl("/kti"),
26
+ changeFrequency: "weekly",
27
+ priority: 0.8,
28
+ },
29
+ {
30
+ url: absoluteUrl("/heroku-alternative"),
31
+ changeFrequency: "weekly",
32
+ priority: 0.8,
33
+ },
34
+ {
35
+ url: absoluteUrl("/railway-alternative"),
36
+ changeFrequency: "weekly",
37
+ priority: 0.8,
38
+ },
39
+ ];
40
10
- const postRoutes = allPosts
41
+ const postRoutes: MetadataRoute.Sitemap = allPosts
42
.filter((post) => post.published)
43
.map((post) => ({
13
- url: `${siteUrl}/${post.slug}`,
44
+ url: absoluteUrl(post.slug),
45
lastModified: post.lastmod || post.date,
15
- }))
46
+ changeFrequency: "monthly",
47
+ priority: 0.7,
48
+ }));
49
17
- const pageRoutes = allPages
18
- .map((page) => ({
19
- url: `${siteUrl}/${page.slug}`,
20
- lastModified: page.lastmod,
21
- }))
50
+ const pageRoutes: MetadataRoute.Sitemap = allPages.map((page) => ({
51
+ url: absoluteUrl(page.slug),
52
+ lastModified: page.lastmod,
53
+ changeFrequency: "yearly",
54
+ priority: 0.5,
55
+ }));
56
23
- return [...pageRoutes, ...postRoutes]
57
+ const ktiRoutes: MetadataRoute.Sitemap = allKtis
58
+ .filter((kti) => kti.published)
59
+ .map((kti) => ({
60
+ url: absoluteUrl(kti.slug),
61
+ lastModified: kti.lastmod,
62
+ changeFrequency: "monthly",
63
+ priority: 0.6,
64
+ }));
65
+
66
+ return [...staticRoutes, ...pageRoutes, ...postRoutes, ...ktiRoutes];
67
}
components/footer.tsx
+2
-10
@@ -6,7 +6,6 @@ export default function Footer() {
6
<footer className="border-t border-slate-200 dark:border-white/[0.06] px-6 py-12 lg:px-8">
7
<div className="mx-auto max-w-5xl">
8
<div className="flex flex-col gap-8 sm:flex-row sm:items-start sm:justify-between">
9
- {/* Brand */}
9
<div>
10
<Link href="/" className="mb-3 flex items-center gap-2">
11
<div className="flex h-6 w-[30px] items-center justify-center rounded bg-indigo-600 text-white">
@@ -21,7 +20,6 @@ export default function Footer() {
20
</p>
21
</div>
22
24
- {/* Links */}
23
<div className="flex flex-wrap gap-x-8 gap-y-3 text-sm text-slate-500 dark:text-slate-500">
24
<Link
25
href="/about"
@@ -50,6 +48,7 @@ export default function Footer() {
48
<Link
49
href="https://github.com/smbcloudXYZ"
50
target="_blank"
51
+ rel="noopener noreferrer"
52
className="flex items-center gap-1.5 hover:text-slate-900 dark:hover:text-slate-300 transition-colors"
53
>
54
<FaGithub className="h-3.5 w-3.5" />
@@ -63,14 +62,7 @@ export default function Footer() {
62
© {new Date().getFullYear()} smbCloud. All rights reserved.
63
</p>
64
<p className="text-xs text-slate-400 dark:text-slate-600">
66
- Powered by{" "}
67
- <a
68
- href="https://debian.org"
69
- target="_blank"
70
- className="font-medium hover:text-slate-600 dark:hover:text-slate-400 transition-colors"
71
- >
72
- Debian
73
- </a>
65
+ support@smbcloud.xyz
66
</p>
67
</div>
68
</div>
components/header.tsx
+4
-6
@@ -18,7 +18,6 @@ export default function Header() {
18
</div>
19
<div className="mx-auto max-w-5xl px-6 lg:px-8">
20
<div className="flex h-14 items-center justify-between">
21
- {/* Logo wordmark */}
21
<Link href="/" className="flex items-center gap-2.5">
22
<div className="flex h-7 w-[38px] items-center justify-center rounded-md bg-indigo-600 text-white">
23
<span className="text-[10px] font-bold tracking-tight leading-none">
@@ -30,7 +29,6 @@ export default function Header() {
29
</span>
30
</Link>
31
33
- {/* Center nav */}
32
<nav className="hidden sm:flex items-center gap-6 text-sm font-medium text-slate-500 dark:text-slate-400">
33
<Link
34
href="/"
@@ -48,25 +46,25 @@ export default function Header() {
46
href="/kti"
47
className="hover:text-slate-900 dark:hover:text-white transition-colors"
48
>
51
- KTI
49
+ KTI (ID)
50
</Link>
51
<Link
52
href="/posts"
53
className="hover:text-slate-900 dark:hover:text-white transition-colors"
54
>
57
- Posts
55
+ Blog
56
</Link>
57
</nav>
58
61
- {/* Right side */}
59
<div className="flex items-center gap-3">
60
<ModeToggle />
61
<Link
62
href="https://console.smbcloud.xyz"
63
target="_blank"
64
+ rel="noopener noreferrer"
65
className="hidden sm:inline-flex items-center rounded-md bg-slate-900 dark:bg-white px-3.5 py-1.5 text-xs font-semibold text-white dark:text-slate-900 hover:bg-slate-700 dark:hover:bg-slate-100 transition-colors"
66
>
69
- Console →
67
+ Create account →
68
</Link>
69
</div>
70
</div>
components/structured-data.tsx
new
+12
@@ -0,0 +1,12 @@
1
+interface StructuredDataProps {
2
+ data: Record<string, unknown> | Array<Record<string, unknown>>;
3
+}
4
+
5
+export function StructuredData({ data }: StructuredDataProps) {
6
+ return (
7
+ <script
8
+ type="application/ld+json"
9
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
10
+ />
11
+ );
12
+}
content/pages/about.mdx
+3
-3
@@ -1,10 +1,10 @@
1
---
2
title: About
3
description: The unified cloud platform for modern app deployment.
4
-lastmod: "2025-06-29"
4
+lastmod: "2026-05-07"
5
---
6
7
-[smbCloud](https://smbcloud.xyz) offers small and medium-sized businesses easy, on-demand access to powerful, open-source software solutions in the cloud—without the hassle of installation or expensive licenses.
7
+[smbCloud](https://smbcloud.xyz) offers small and medium-sized businesses easy, on-demand access to powerful, open-source software solutions in the cloud, without the hassle of installation or expensive licenses.
8
9
## Our Mission
10
@@ -20,7 +20,7 @@ At smbCloud, our mission is to democratize access to modern cloud infrastructure
20
21
## Why Choose smbCloud?
22
23
-- **Simplicity:** Get started in minutes—no complex setup or infrastructure management required.
23
+- **Simplicity:** Get started in minutes with no complex setup or infrastructure management required.
24
- **Flexibility:** Scale from a single project to multiple teams and environments effortlessly.
25
- **Transparency:** Open source at its core, with a commitment to community-driven development.
26
- **Support:** Dedicated to helping small and medium businesses succeed in the cloud era.
content/pages/contact.mdx
+8
-6
@@ -4,11 +4,11 @@ description: Get in touch with smbCloud and Splitfire AB.
4
lastmod: "2025-10-28"
5
---
6
7
-<b>Splitfire AB</b>
7
+**Splitfire AB**
8
9
-We develop smbCloud and other innovative mobile and web applications.
9
+We build smbCloud and other developer-focused mobile and web applications.
10
11
-**Email:** smbcloud[at]setoelkahfi[dot]se
11
+**Email:** [support@smbcloud.xyz](mailto:support@smbcloud.xyz)
12
13
**Phone:** (+46) 72 853 82 88
14
@@ -16,12 +16,14 @@ We develop smbCloud and other innovative mobile and web applications.
16
17
**Location:** Stockholm, Sweden
18
19
+**GitHub:** [github.com/smbcloudXYZ](https://github.com/smbcloudXYZ)
20
+
21
---
22
23
## About Us
24
23
-Splitfire AB is a vertically integrated technology company specializing in cloud deployment platforms and mobile applications. We build professional developer tools like **siGit Code & Deploy** and educational apps like **MooVibe: Vibe Coding**.
25
+Splitfire AB is a vertically integrated technology company focused on cloud deployment platforms and modern developer tools. We build products like **smbCloud**, **siGit Code & Deploy**, and other software that helps teams ship faster with less operational overhead.
26
25
-Our technology stack includes Rust, Ruby on Rails, and TypeScript, enabling us to deliver fast, reliable, and scalable solutions.
27
+Our core stack includes Rust, Ruby on Rails, and TypeScript, which we use to deliver fast, reliable, and maintainable systems.
28
27
-For business inquiries, partnership opportunities, or technical support, please reach out via email or phone.
\ No newline at end of file
29
+For business inquiries, partnerships, technical support, or press, reach out by email or phone.
content/siteMetadata.js
+19
-4
@@ -1,20 +1,35 @@
1
/** @type {import("pliny/config").PlinyConfig } */
2
const siteMetadata = {
3
- title: "smbCloud | One-command deploys for NodeJS, Swift, and Ruby",
3
+ title: "smbCloud",
4
author: "smbCloud Team",
5
headerTitle: "smbCloud",
6
+ tagline: "Enterprise Power at Startup Velocity",
7
description:
7
- "Deploy NodeJS, Swift, and Ruby apps with one command. No Docker. No config files. Then browse KTI, our Indonesian IT glossary.",
8
+ "Enterprise Power at Startup Velocity. smbCloud is an SSH-first cloud deploy platform for small teams shipping Node.js, Ruby on Rails, and Swift/Vapor apps with one-command deploys, live logs, and fast rollbacks.",
9
+ keywords: [
10
+ "smbCloud",
11
+ "SSH deploy platform",
12
+ "Node.js deployment",
13
+ "Ruby on Rails deployment",
14
+ "Swift Vapor deployment",
15
+ "developer platform",
16
+ "cloud deployment",
17
+ "app hosting for developers",
18
+ ],
19
language: "en-us",
20
+ locale: "en-US",
21
theme: "dark", // system, dark or light
22
siteUrl: "https://smbcloud.xyz",
23
siteLogo: `${process.env.BASE_PATH || ""}/logo.png`,
24
socialBanner: `${process.env.BASE_PATH || ""}/twitter-card.png`,
13
- email: "hello@smbcloud.xyz",
25
+ email: "support@smbcloud.xyz",
26
+ organizationName: "smbCloud",
27
+ companyName: "Splitfire AB",
28
+ companyLocation: "Stockholm, Sweden",
29
+ github: "https://github.com/smbcloudXYZ",
30
facebook: "https://facebook.com/smbCloudXYZ",
31
youtube: "https://youtube.com/@smbCloudXYZ",
32
instagram: "https://www.instagram.com/smbCloudXYZ",
17
- locale: "en-US",
33
// set to true if you want a navbar fixed to the top
34
stickyNav: false,
35
analytics: {
eslint.config.mjs
+11
-1
@@ -1 +1,11 @@
1
-export default [{}];
1
+export default [
2
+ {
3
+ ignores: [
4
+ ".next/**",
5
+ ".contentlayer/**",
6
+ "node_modules/**",
7
+ "public/**",
8
+ "tsconfig.tsbuildinfo",
9
+ ],
10
+ },
11
+];
llm/KTI_DEPLOYMENT_CHECKLIST.md
+1
-1
@@ -219,7 +219,7 @@ If critical issues are found:
219
- **Technical Docs**: `KTI_SUMMARY.md`
220
- **SEO Strategy**: `APA-ITU-SEO-STRATEGY.md`
221
- **Content Guidelines**: `content/kti/README.md`
222
-- **Contact**: hello@smbcloud.xyz
222
+- **Contact**: support@smbcloud.xyz
223
224
## Notes
225
llm/KTI_QUICK_REFERENCE.md
+1
-1
@@ -252,7 +252,7 @@ app/kti/
252
- **SEO Questions**: See `APA-ITU-SEO-STRATEGY.md`
253
- **Content Guidelines**: Read `content/kti/README.md`
254
- **Deployment**: Use `KTI_DEPLOYMENT_CHECKLIST.md`
255
-- **Contact**: hello@smbcloud.xyz
255
+- **Contact**: support@smbcloud.xyz
256
257
## 🎯 Success Metrics
258
llm/KTI_SUMMARY.md
+1
-1
@@ -334,4 +334,4 @@ KTI is now a fully functional, SEO-optimized content marketing platform that pos
334
**Build Status**: Passing ✅
335
**Deployed**: Ready for deployment
336
337
-For questions or contributions: hello@smbcloud.xyz
\ No newline at end of file
337
+For questions or contributions: support@smbcloud.xyz
\ No newline at end of file
llm/README-KIT.md
+1
-1
@@ -247,4 +247,4 @@ Code examples are licensed under MIT
247
248
**Maintained by**: smbCloud Team
249
**Last Updated**: January 2024
250
-**Contact**: hello@smbcloud.xyz
\ No newline at end of file
250
+**Contact**: support@smbcloud.xyz
\ No newline at end of file
public/llms.txt
new
+40
@@ -0,0 +1,40 @@
1
+# smbCloud
2
+
3
+> smbCloud is an SSH-first cloud deploy platform for small teams. It helps developers deploy Node.js, Ruby on Rails, and Swift/Vapor apps with one command, live logs, fast rollbacks, and a clean web console.
4
+
5
+Website: https://smbcloud.xyz/
6
+Organization: smbCloud
7
+Company: Splitfire AB
8
+Location: Stockholm, Sweden
9
+Contact: support@smbcloud.xyz
10
+GitHub: https://github.com/smbcloudXYZ
11
+
12
+## Primary pages
13
+- Home: https://smbcloud.xyz/
14
+- Services: https://smbcloud.xyz/services
15
+- Blog: https://smbcloud.xyz/posts
16
+- CLI install guide: https://smbcloud.xyz/posts/cli
17
+- Contact: https://smbcloud.xyz/contact
18
+- About: https://smbcloud.xyz/about
19
+- Heroku alternative: https://smbcloud.xyz/heroku-alternative
20
+- Railway alternative: https://smbcloud.xyz/railway-alternative
21
+- KTI glossary: https://smbcloud.xyz/kti
22
+
23
+## Core topics
24
+- SSH-first application deployment
25
+- Node.js deployment
26
+- Ruby on Rails deployment
27
+- Swift Vapor deployment
28
+- Developer infrastructure for small teams
29
+- Hosted deploy workflows, logs, and rollbacks
30
+
31
+## Products and services
32
+- smbCloud Deploy: application deployment over SSH
33
+- smbCloud Auth: hosted authentication flows
34
+- smbCloud Mail: domain mail routing and inbox workflows
35
+- GresIQ: data layer service within the smbCloud platform
36
+- siGit Code & Deploy: mobile and desktop companion app for code review and deployment workflows
37
+
38
+## Editorial resources
39
+- Product updates and deployment guides live on the blog: https://smbcloud.xyz/posts
40
+- Indonesian technology glossary (KTI) lives here: https://smbcloud.xyz/kti