[grade=A] Add DashCaddy blog and LLM install map

This commit is contained in:
Krystie committed 2026-10-05 20:16:33 -07:00
1 parent 22c3409c75
commit 5c71f251ca
10 files changed
+476 -1

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
import fs from "fs";
import path from "path";
import Link from "next/link";
import { notFound } from "next/navigation";
import { marked } from "marked";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { POSTS } from "@/content/blog/manifest";
export const dynamicParams = false;
const BLOG_DIR = path.join(process.cwd(), "src", "content", "blog");
export function generateStaticParams() {
// Fail the build loudly if a manifest entry has no matching markdown file —
// prevents a future publisher from registering a post it never wrote.
for (const p of POSTS) {
if (!fs.existsSync(path.join(BLOG_DIR, `${p.slug}.md`))) {
throw new Error(
`blog: manifest slug "${p.slug}" has no src/content/blog/${p.slug}.md`
);
}
}
// Reverse guard: reject orphaned markdown files not represented in POSTS, so a
// stray/forgotten draft can't silently ship without an index entry.
const slugs = new Set(POSTS.map((p) => p.slug));
for (const f of fs.readdirSync(BLOG_DIR)) {
if (f.endsWith(".md") && !slugs.has(f.replace(/\.md$/, ""))) {
throw new Error(
`blog: orphaned src/content/blog/${f} has no POSTS manifest entry`
);
}
}
return POSTS.map((p) => ({ slug: p.slug }));
}
function getPost(slug: string) {
return POSTS.find((p) => p.slug === slug) ?? null;
}
function readBody(slug: string): string | null {
const file = path.join(BLOG_DIR, `${slug}.md`);
if (!fs.existsSync(file)) return null;
return fs.readFileSync(file, "utf8");
}
function formatDate(iso: string): string {
return new Date(iso + "T00:00:00Z").toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
});
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getPost(slug);
if (!post) return { title: "Post not found — DashCaddy Blog" };
return {
title: `${post.title} — DashCaddy Blog`,
description: post.description,
alternates: { canonical: `https://dashcaddy.net/blog/${post.slug}/` },
openGraph: {
title: post.title,
description: post.description,
type: "article",
url: `https://dashcaddy.net/blog/${post.slug}/`,
},
};
}
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getPost(slug);
const body = post ? readBody(slug) : null;
if (!post || body === null) notFound();
const html = marked.parse(body, { async: false }) as string;
return (
<div className="flex min-h-screen flex-col bg-surface-950 text-surface-50">
<Navbar />
<article className="relative py-16 sm:py-20">
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<Link
href="/blog"
className="mb-8 inline-block text-sm text-brand-400 hover:text-brand-300"
>
← All posts
</Link>
<header className="mb-10 border-b border-surface-800 pb-8">
<div className="mb-3 flex flex-wrap items-center gap-3 text-sm text-surface-400">
<time dateTime={post.date}>{formatDate(post.date)}</time>
<span aria-hidden>·</span>
<span>{post.readingMinutes} min read</span>
<span aria-hidden>·</span>
<span>{post.author}</span>
</div>
<h1 className="text-3xl font-bold sm:text-4xl">{post.title}</h1>
<div className="mt-4 flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-surface-800 px-3 py-1 text-xs font-medium text-surface-300"
>
{tag}
</span>
))}
</div>
</header>
<div
className="blog-prose"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
</article>
<Footer />
</div>
);
}
+85
View File
@@ -0,0 +1,85 @@
import Link from "next/link";
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { POSTS } from "@/content/blog/manifest";
export const metadata = {
title: "Blog — DashCaddy",
description:
"Guides, product updates, and self-hosting deep-dives from the DashCaddy team: AI-powered deployment, the MCP server, security, and fleet management.",
alternates: { canonical: "https://dashcaddy.net/blog/" },
};
function formatDate(iso: string): string {
return new Date(iso + "T00:00:00Z").toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
});
}
export default function BlogIndexPage() {
const posts = [...POSTS].sort((a, b) => (a.date < b.date ? 1 : -1));
return (
<div className="flex min-h-screen flex-col bg-surface-950 text-surface-50">
<Navbar />
<section className="relative py-16 sm:py-20 lg:py-24">
<div className="absolute inset-0 -z-10">
<div className="absolute left-1/2 top-1/2 h-96 w-96 -translate-x-1/2 -translate-y-1/2 animate-pulse rounded-full bg-brand-500/20 opacity-30 blur-3xl" />
</div>
<div className="mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
<h1 className="mb-6 text-4xl font-bold sm:text-5xl">
The DashCaddy <span className="text-brand-400">Blog</span>
</h1>
<p className="mx-auto max-w-2xl text-xl text-surface-300">
Self-hosting deep-dives, product updates, and practical guides to
running your own infrastructure.
</p>
</div>
</section>
<section className="relative pb-20">
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
{posts.length === 0 ? (
<p className="text-center text-surface-400">No posts yet — check back soon.</p>
) : (
<ul className="space-y-6">
{posts.map((post) => (
<li key={post.slug}>
<Link
href={`/blog/${post.slug}`}
className="glass-card block rounded-xl p-6 transition-colors hover:border-brand-500/30"
>
<div className="mb-2 flex flex-wrap items-center gap-3 text-xs text-surface-400">
<time dateTime={post.date}>{formatDate(post.date)}</time>
<span aria-hidden>·</span>
<span>{post.readingMinutes} min read</span>
</div>
<h2 className="mb-2 text-2xl font-semibold text-white">
{post.title}
</h2>
<p className="mb-4 text-surface-300">{post.description}</p>
<div className="flex flex-wrap gap-2">
{post.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-surface-800 px-3 py-1 text-xs font-medium text-surface-300"
>
{tag}
</span>
))}
</div>
</Link>
</li>
))}
</ul>
)}
</div>
</section>
<Footer />
</div>
);
}
+1 -1
View File
@@ -99,7 +99,7 @@ curl -fsSL https://get.dashcaddy.net | bash -s -- quick`}</code></pre>
<li><strong>Detect your environment</strong> — operating system, package manager, free disk space, available memory, public and LAN IP addresses</li>
<li><strong>Install prerequisites</strong> — Docker Engine and the Docker Compose plugin if missing</li>
<li><strong>Write configuration</strong> — generates <code>/etc/dashcaddy/</code>, <code>/opt/dockerdata/</code>, and the DashCaddy config file with sensible defaults</li>
<li><strong>Pull the DashCaddy container image</strong> — fetches the latest release from the public registry</li>
<li><strong>Build the DashCaddy container image</strong> — fetches the source (release tarball or git) and runs <code>docker-compose build</code></li>
<li><strong>Start the DashCaddy container</strong> — launches <code>dashcaddy-api</code> on port 3001, mounts persistent data volumes</li>
<li><strong>Health check</strong> — waits for the API to respond on <code>/healthz</code> before continuing</li>
<li><strong>Print the access URL</strong> — shows the dashboard address and any follow-up commands</li>
+75
View File
@@ -133,3 +133,78 @@ body {
.theme-toggle:hover {
background-color: var(--color-surface-800);
}
/* Blog post typography (rendered markdown via .blog-prose) */
.blog-prose {
color: var(--color-surface-200);
line-height: 1.75;
font-size: 1.05rem;
}
.blog-prose h2 {
color: #fff;
font-size: 1.6rem;
font-weight: 700;
margin-top: 2.25rem;
margin-bottom: 0.9rem;
line-height: 1.3;
}
.blog-prose h3 {
color: #fff;
font-size: 1.25rem;
font-weight: 600;
margin-top: 1.75rem;
margin-bottom: 0.6rem;
}
.blog-prose p {
margin-bottom: 1.25rem;
}
.blog-prose a {
color: var(--color-brand-400);
text-decoration: underline;
text-underline-offset: 2px;
}
.blog-prose a:hover {
color: var(--color-brand-300);
}
.blog-prose strong {
color: #fff;
font-weight: 600;
}
.blog-prose ul,
.blog-prose ol {
margin: 0 0 1.25rem 1.5rem;
}
.blog-prose ul {
list-style: disc;
}
.blog-prose ol {
list-style: decimal;
}
.blog-prose li {
margin-bottom: 0.5rem;
}
.blog-prose code {
background-color: var(--color-surface-800);
padding: 0.15rem 0.4rem;
border-radius: 6px;
font-size: 0.9em;
}
.blog-prose pre {
background-color: var(--color-surface-900);
border: 1px solid var(--color-surface-800);
border-radius: 10px;
padding: 1rem 1.25rem;
overflow-x: auto;
margin-bottom: 1.5rem;
}
.blog-prose pre code {
background: none;
padding: 0;
}
.blog-prose blockquote {
border-left: 3px solid var(--color-brand-500);
padding-left: 1rem;
color: var(--color-surface-300);
font-style: italic;
margin-bottom: 1.5rem;
}
+1
View File
@@ -28,6 +28,7 @@ export default function Navbar() {
{ href: '/features', label: 'Features' },
{ href: '/pricing', label: 'Pricing' },
{ href: '/docs', label: 'Docs' },
{ href: '/blog', label: 'Blog' },
{ href: '/about', label: 'About' },
];
@@ -0,0 +1,83 @@
Self-hosting has always carried a tax: every app you want to run means a new
container to configure, a reverse proxy to wire up, a certificate to issue, and a
dashboard to babysit. DashCaddy was built to erase that tax — and the clearest
example is the **AI Intent Router**, which lets you describe what you want in
plain English and have it deployed, secured, and running.
## From a sentence to a running service
Traditional self-hosting asks you to think like an operator: pick an image, map
the ports, mount the volumes, set the environment, then hand-write a reverse-proxy
block and chase down a TLS certificate. DashCaddy collapses that into intent.
Instead of memorizing flags, you tell DashCaddy what you're trying to accomplish.
The AI Intent Router turns that plain-English request into a structured deploy
intent matched to one of DashCaddy's **77 pre-configured application templates**;
the deploy flow then provisions the service with sane defaults — container,
networking, and TLS via DashCaddy's built-in CA and Caddy. The result is a
working, HTTPS-secured app instead of a half-finished `docker run` command.
## 77 one-click templates, not a blank canvas
A blank Docker host is powerful and unforgiving. DashCaddy ships **77 one-click
templates** spanning media servers, productivity tools, developer platforms,
networking and DNS utilities, and more. Each template encodes the knowledge that
usually lives in a dozen browser tabs: a working default port and a health check,
wired and ready to run.
Because the templates are curated rather than improvised, "install Nextcloud" or
"set up a Git server" becomes a single action — deployable from the dashboard or
the REST API.
## Built for AI assistants: the MCP server
DashCaddy doesn't just use AI internally — it exposes itself *to* AI. The built-in
**MCP (Model Context Protocol) server** gives an AI assistant a set of named tools
— among them listing your services, checking service and system health, searching
the app catalog, and running container actions — over a standard protocol. Instead
of translating intent into API calls by hand, you can let an assistant do the
work. Self-hosting becomes something you can delegate.
## A real platform underneath: 400+ registered REST route handlers
The friendly front end sits on top of a genuine platform. DashCaddy registers
**400+ REST route handlers** covering deployment, lifecycle, health, security,
backups, and fleet operations — with a WebSocket channel for live updates.
Scripts and CI pipelines get a first-class path too. The AI layer is a
convenience, not a lock-in: the API is always there for the operators who want
it.
## Security that isn't an afterthought
Self-hosting is only worth it if it's safe to expose. DashCaddy includes a
**Security Center** — a dashboard for security events and the hosts it tracks —
so security visibility isn't a separate project you keep meaning to set up. TLS
is handled by the built-in CA and Caddy. And when you do want to let someone in,
**Secure Share Links** issue time-boxed public links (with an optional status
subscription) or single-use Tailscale invitations, so sharing a service doesn't
mean opening it to the world.
## Scales from one box to a fleet
What starts as a single-host homelab rarely stays that way. DashCaddy includes a
deploy bridge (its shipdeck integration) with live health re-probes and fleet
credentials for reaching other hosts, so growing beyond one machine doesn't mean
starting over with a new tool.
## Speaks your language — literally
DashCaddy's interface is localized into **31 languages**, including full
right-to-left support for Arabic. Self-hosting shouldn't be gated behind English
fluency, and the dashboard reflects that.
## Why this matters
The promise of self-hosting is control: your data, your infrastructure, your
rules. The cost has always been complexity. By turning intent into deployment,
curating 77 templates instead of handing you a blank canvas, and exposing 400+
registered REST route handlers plus an MCP server, DashCaddy keeps the control
while removing most of the complexity.
Ready to try it? Browse the [full feature set](/features), see the
[77-app catalog](/docs/catalog), or jump straight to the
[installation guide](/docs/installation).
+27
View File
@@ -0,0 +1,27 @@
// Blog post manifest — single source of truth for the index and the
// [slug] static-params generator. To publish, insert a new entry into the
// POSTS array (newest first) and add a matching markdown file at
// src/content/blog/<slug>.md. The build fails if a slug has no matching file.
export interface PostMeta {
slug: string;
title: string;
description: string;
date: string; // ISO yyyy-mm-dd
author: string;
tags: string[];
readingMinutes: number;
}
export const POSTS: PostMeta[] = [
{
slug: "deploy-self-hosted-apps-with-ai",
title: "Deploy Self-Hosted Apps From Plain English: DashCaddy's AI Intent Router",
description:
"How DashCaddy turns a plain-English request into a running, SSL-secured container — 77 one-click app templates, an MCP server for AI assistants, and 400+ registered REST route handlers.",
date: "2026-10-05",
author: "The DashCaddy Team",
tags: ["self-hosting", "ai", "docker", "homelab"],
readingMinutes: 6,
},
];