Added blog drafts and ui for posting them.

This commit is contained in:
scadmin
2025-12-14 22:18:10 +00:00
parent 5357cfae0e
commit 7e5e17bde1
6 changed files with 147 additions and 104 deletions

26
public/admin/config.yml Normal file
View File

@@ -0,0 +1,26 @@
backend:
name: gitea
repo: scadmin/seans-cloud # <owner>/<repo>
branch: main
app_id: "607244b1-2313-478b-bd25-40b224c66617"
base_url: "https://git.seans.cloud" # or https://<tailscale-name>:3000
api_root: "https://git.seans.cloud/api/v1"
auth_endpoint: "https://git.seans.cloud/login/oauth/authorize"
site_url: "https://seans.cloud"
media_folder: "src/assets/images/uploads"
public_folder: "/src/assets/images/uploads"
collections:
- name: "blog"
label: "Blog"
folder: "src/content/blog"
create: true
slug: "{{slug}}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Publish Date", name: "date", widget: "datetime" }
- { label: "Description", name: "description", widget: "text", required: false }
- { label: "Draft", name: "draft", widget: "boolean", default: false, required: false }
- { label: "Body", name: "body", widget: "markdown" }

21
src/content/config.ts Normal file
View File

@@ -0,0 +1,21 @@
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
draft: z.boolean().optional().default(false),
// Optional if you use hero images in your layout:
heroImage: z
.object({
src: z.string(),
alt: z.string().optional(),
})
.optional(),
}),
});
export const collections = { blog };

13
src/pages/admin.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex" />
<link href="/admin/config.yml" type="text/yaml" rel="cms-config-url" />
<title>Content Manager</title>
</head>
<body>
<script src="https://unpkg.com/decap-cms@^3.1.2/dist/decap-cms.js"></script>
</body>
</html>

View File

@@ -4,14 +4,18 @@ import BlogPost from '../../layouts/BlogPost.astro';
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection('blog'); const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id }, return posts
props: post, .filter((post) => !(post.data as { draft?: boolean }).draft)
})); .map((post) => ({
params: { slug: post.id },
props: post,
}));
} }
type Props = CollectionEntry<'blog'>; type Props = CollectionEntry<'blog'>;
const post = Astro.props; const post = Astro.props as Props;
const { Content } = await render(post); const { Content } = await render(post);
--- ---

View File

@@ -1,113 +1,59 @@
--- ---
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
import BaseHead from '../../components/BaseHead.astro'; import BaseHead from '../../components/BaseHead.astro';
import Footer from '../../components/Footer.astro'; import Footer from '../../components/Footer.astro';
import FormattedDate from '../../components/FormattedDate.astro';
import Header from '../../components/Header.astro'; import Header from '../../components/Header.astro';
import { SITE_DESCRIPTION, SITE_TITLE } from '../../consts'; import { SITE_DESCRIPTION, SITE_TITLE } from '../../consts';
const posts = (await getCollection('blog')).sort( import { getCollection } from 'astro:content';
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
); const posts = (await getCollection('blog'))
.filter((p) => !(p.data as { draft?: boolean }).draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} /> <BaseHead title={`${SITE_TITLE} | Blog`} description={SITE_DESCRIPTION} />
<style>
main {
width: 960px;
}
ul {
display: flex;
flex-wrap: wrap;
gap: 2rem;
list-style-type: none;
margin: 0;
padding: 0;
}
ul li {
width: calc(50% - 1rem);
}
ul li * {
text-decoration: none;
transition: 0.2s ease;
}
ul li:first-child {
width: 100%;
margin-bottom: 1rem;
text-align: center;
}
ul li:first-child img {
width: 100%;
}
ul li:first-child .title {
font-size: 2.369rem;
}
ul li img {
margin-bottom: 0.5rem;
border-radius: 12px;
}
ul li a {
display: block;
}
.title {
margin: 0;
color: rgb(var(--black));
line-height: 1;
}
.date {
margin: 0;
color: rgb(var(--gray));
}
ul li a:hover h4,
ul li a:hover .date {
color: rgb(var(--accent));
}
ul a:hover img {
box-shadow: var(--box-shadow);
}
@media (max-width: 720px) {
ul {
gap: 0.5em;
}
ul li {
width: 100%;
text-align: center;
}
ul li:first-child {
margin-bottom: 0;
}
ul li:first-child .title {
font-size: 1.563em;
}
}
</style>
</head> </head>
<body> <body>
<Header /> <Header />
<main> <main>
<section> <h1>Sean's Cloud!</h1>
<h3>Infrastructure • Cloud • Systems Thinking • Leadership</h3>
<p>
seans.cloud is an evolving space focused on building, testing, and refining modern IT and cloud practices with an eye toward whats next. It serves as a foundation for future projects in cloud architecture, automation, security, and operational excellence—along with the leadership thinking that supports sustainable systems.
</p>
<p>
As this site grows, it will become a place to share practical insights, real-world experiments, and lessons learned from working with infrastructure at scale. Future content may include technical guides, architecture patterns, tooling evaluations, and perspectives on how technology, process, and people intersect to create resilient organizations.
</p>
<p>On this site youll find:</p>
<ul>
<li>Technical experiments and lab notes</li>
<li>Cloud and infrastructure architecture thinking</li>
<li>Reflections on reliability, security, and process</li>
<li>Lessons learned from real-world systems</li>
</ul>
<hr />
<h2>Latest Posts</h2>
{posts.length === 0 ? (
<p>No posts yet. Check back soon.</p>
) : (
<ul> <ul>
{ {posts.map((post) => (
posts.map((post) => ( <li>
<li> <a href={`/blog/${post.id}`}>{post.data.title}</a>
<a href={`/blog/${post.id}/`}> <small> — {post.data.pubDate.toLocaleDateString()}</small>
{post.data.heroImage && ( {post.data.description ? <p>{post.data.description}</p> : null}
<Image width={720} height={360} src={post.data.heroImage} alt="" /> </li>
)} ))}
<h4 class="title">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
</a>
</li>
))
}
</ul> </ul>
</section> )}
</main> </main>
<Footer /> <Footer />
</body> </body>

View File

@@ -3,6 +3,13 @@ import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import Header from '../components/Header.astro'; import Header from '../components/Header.astro';
import { SITE_DESCRIPTION, SITE_TITLE } from '../consts'; import { SITE_DESCRIPTION, SITE_TITLE } from '../consts';
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog'))
.filter((p) => !(p.data as { draft?: boolean }).draft)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
.slice(0, 5);
--- ---
<!doctype html> <!doctype html>
@@ -15,19 +22,45 @@ import { SITE_DESCRIPTION, SITE_TITLE } from '../consts';
<main> <main>
<h1>Sean's Cloud!</h1> <h1>Sean's Cloud!</h1>
<h3>Infrastructure • Cloud • Systems Thinking • Leadership</h3> <h3>Infrastructure • Cloud • Systems Thinking • Leadership</h3>
<p> <p>
seans.cloud is an evolving space focused on building, testing, and refining modern IT and cloud practices with an eye toward whats next. It serves as a foundation for future projects in cloud architecture, automation, security, and operational excellence—along with the leadership thinking that supports sustainable systems. seans.cloud is an evolving space focused on building, testing, and refining modern IT and cloud practices with an eye toward whats next. It serves as a foundation for future projects in cloud architecture, automation, security, and operational excellence—along with the leadership thinking that supports sustainable systems.
</p> </p>
<p> <p>
As this site grows, it will become a place to share practical insights, real-world experiments, and lessons learned from working with infrastructure at scale. Future content may include technical guides, architecture patterns, tooling evaluations, and perspectives on how technology, process, and people intersect to create resilient organizations. As this site grows, it will become a place to share practical insights, real-world experiments, and lessons learned from working with infrastructure at scale. Future content may include technical guides, architecture patterns, tooling evaluations, and perspectives on how technology, process, and people intersect to create resilient organizations.
</p> </p>
<p>On this site youll find:</p> <p>On this site youll find:</p>
<ul> <ul>
<li>Technical experiments and lab notes</li> <li>Technical experiments and lab notes</li>
<li>Cloud and infrastructure architecture thinking</li> <li>Cloud and infrastructure architecture thinking</li>
<li>Reflections on reliability, security, and process</li> <li>Reflections on reliability, security, and process</li>
<li>Lessons learned from real-world systems</li> <li>Lessons learned from real-world systems</li>
</ul> </ul>
<hr />
<h2>Latest Posts</h2>
{posts.length === 0 ? (
<p>No posts yet. Check back soon.</p>
) : (
<>
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
<small> — {post.data.pubDate.toLocaleDateString()}</small>
{post.data.description ? <p>{post.data.description}</p> : null}
</li>
))}
</ul>
<p>
<a href="/blog">View all posts →</a>
</p>
</>
)}
</main> </main>
<Footer /> <Footer />
</body> </body>