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