facelift: blog index - featured security-series opener + image-led grid
Breadcrumb eyebrow (~/jwright/blog), 'Notes from the build.' H1, featured card for the series opener (text left / hero art scrimmed right, series badge, eager-loaded), remaining posts in the image-led responsive grid with series badges + reading time. Post CONTENT untouched (index only).
This commit is contained in:
+164
-3
@@ -2,14 +2,22 @@
|
|||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import Section from "../../components/Section.astro";
|
import Section from "../../components/Section.astro";
|
||||||
import PostList from "../../components/PostList.astro";
|
import PostList from "../../components/PostList.astro";
|
||||||
import { getPosts, allTags } from "../../lib/blog";
|
import { getPosts, allTags, formatDate } from "../../lib/blog";
|
||||||
|
import { readingTime } from "../../lib/reading";
|
||||||
|
import { series } from "../../data/series";
|
||||||
|
import { site } from "../../data/site";
|
||||||
|
|
||||||
const posts = await getPosts();
|
const posts = await getPosts();
|
||||||
const tags = allTags(posts);
|
const tags = allTags(posts);
|
||||||
|
|
||||||
|
// Featured = the security-series opener when present, else the newest post.
|
||||||
|
const featured = posts.find((p) => p.id === "secret-zero") ?? posts[0];
|
||||||
|
const rest = posts.filter((p) => p.id !== featured.id);
|
||||||
|
const featuredSeries = series[featured.id];
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Blog" path="/blog" description="Notes on platform engineering, edge Kubernetes, GPUs and homelab infrastructure.">
|
<Layout title="Blog" path="/blog" description="Notes on platform engineering, edge Kubernetes, GPUs and homelab infrastructure.">
|
||||||
<Section id="blog" eyebrow="Writing" index="*" title="Notes from the platform.">
|
<Section id="blog" eyebrow={`~/${site.handle}/blog`} index="*" title="Notes from the build.">
|
||||||
<p class="lead blog__intro" data-reveal>
|
<p class="lead blog__intro" data-reveal>
|
||||||
Lessons from edge Kubernetes, GPUs, and running infrastructure like it matters.
|
Lessons from edge Kubernetes, GPUs, and running infrastructure like it matters.
|
||||||
</p>
|
</p>
|
||||||
@@ -23,7 +31,44 @@ const tags = allTags(posts);
|
|||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PostList posts={posts} />
|
{featured && (
|
||||||
|
<a class="featured" href={`/blog/${featured.id}/`} data-reveal>
|
||||||
|
<div class="featured__body">
|
||||||
|
<div class="featured__meta mono">
|
||||||
|
{featuredSeries && (
|
||||||
|
<span class="featured__series">
|
||||||
|
{featuredSeries.name} · {String(featuredSeries.number).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<time datetime={featured.data.date.toISOString()}>{formatDate(featured.data.date)}</time>
|
||||||
|
<span class="featured__sep">·</span>
|
||||||
|
<span>{readingTime(featured.body)}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="featured__title">{featured.data.title}</h3>
|
||||||
|
<p class="featured__summary">{featured.data.summary}</p>
|
||||||
|
<ul class="featured__tags" role="list">
|
||||||
|
{featured.data.tags.map((t) => (
|
||||||
|
<li class="tag">{t}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<span class="featured__read mono">Read the post →</span>
|
||||||
|
</div>
|
||||||
|
{featured.data.hero && (
|
||||||
|
<div class="featured__art" aria-hidden="true">
|
||||||
|
<img
|
||||||
|
src={`${featured.data.hero}?v=2`}
|
||||||
|
alt=""
|
||||||
|
width="1600"
|
||||||
|
height="1073"
|
||||||
|
loading="eager"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PostList posts={rest} />
|
||||||
</Section>
|
</Section>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
@@ -49,4 +94,120 @@ const tags = allTags(posts);
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* featured card — text left, hero art right, scrimmed into the panel */
|
||||||
|
.featured {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border-accent);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
backdrop-filter: var(--panel-blur);
|
||||||
|
-webkit-backdrop-filter: var(--panel-blur);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
transition:
|
||||||
|
border-color 180ms var(--ease),
|
||||||
|
box-shadow 180ms var(--ease);
|
||||||
|
}
|
||||||
|
.featured:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
border-color: var(--border-accent-hover);
|
||||||
|
box-shadow: var(--glow-card);
|
||||||
|
}
|
||||||
|
.featured__body {
|
||||||
|
padding: var(--space-6);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.featured__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
.featured__series {
|
||||||
|
color: var(--accent-2);
|
||||||
|
border: 1px solid var(--border-magenta);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
font-size: 0.66rem;
|
||||||
|
}
|
||||||
|
.featured__sep {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.featured__title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--step-2);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-strong);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.featured__summary {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: var(--step-0);
|
||||||
|
line-height: 1.65;
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
.featured__tags {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: var(--space-2) 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.featured__read {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.featured__art {
|
||||||
|
position: relative;
|
||||||
|
min-height: 280px;
|
||||||
|
}
|
||||||
|
.featured__art img {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
/* scrim the art into the panel from the text side */
|
||||||
|
.featured__art::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: linear-gradient(90deg, var(--panel-solid) 0%, transparent 45%);
|
||||||
|
}
|
||||||
|
[data-theme="light"] .featured__art::before {
|
||||||
|
background: linear-gradient(90deg, var(--panel-solid) 0%, transparent 45%);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.featured {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.featured__art {
|
||||||
|
order: -1;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.featured__art::before {
|
||||||
|
background: linear-gradient(180deg, transparent 55%, var(--panel-solid) 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user