Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 0.1.3 – 2026-10-19

The homepage shows Image Update Monitor between the facts and the project list.

- **Product preview:** a real screenshot, a short explanation about keeping container updates in
sight and a link to the project, in English and German.
- **Phones and themes:** a narrow-screen capture shows a complete container card on phones; the
desktop view shows five rows. Both follow the device's light or dark setting and use optimised WebP.
- **Build checks on Windows:** resolve the output directory as a filesystem path and normalise
directory separators before comparing page routes.

## 0.1.2 – 2026-10-19

Stellwerk's page explains Codex wherever it explains Claude Code.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azentrius-website",
"version": "0.1.2",
"version": "0.1.3",
"private": true,
"description": "The website azentrius.com: project pages for the Azentrius tools, in English and German.",
"license": "Apache-2.0",
Expand Down
7 changes: 4 additions & 3 deletions scripts/check-dist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
// executable script, links only to pages and files that exist, and names nothing unpublished.
import { createHash } from "node:crypto";
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import { join, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { NOINDEX_PATHS, ROUTES, SITE } from "../src/data/routes.mjs";

const DIST = new URL("../dist/", import.meta.url).pathname;
const DIST = fileURLToPath(new URL("../dist/", import.meta.url));
const errors = [];
const fail = (where, message) => errors.push(`${where}: ${message}`);

Expand Down Expand Up @@ -66,7 +67,7 @@ for (const file of ["404.html", "robots.txt", "sitemap-index.xml", "favicon.svg"
// 2. Each HTML page.
const files = walk(DIST);
const html = files.filter((f) => f.endsWith(".html"));
const pathOf = (file) => "/" + relative(DIST, file).replace(/index\.html$/, "");
const pathOf = (file) => "/" + relative(DIST, file).split(sep).join("/").replace(/index\.html$/, "");

for (const file of html) {
const where = pathOf(file);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions src/components/HomePage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { getCollection } from "astro:content";
import Base from "../layouts/Base.astro";
import ToolCard from "./ToolCard.astro";
import CopyBlock from "./CopyBlock.astro";
import ProductName from "./ProductName.astro";
import Shot from "./Shot.astro";
import updates from "../assets/screenshots/cockpit-image-update-monitor/updates.png";
import updatesDark from "../assets/screenshots/cockpit-image-update-monitor/updates-dark.png";
import updatesMobile from "../assets/screenshots/cockpit-image-update-monitor/mobile.png";
import updatesMobileDark from "../assets/screenshots/cockpit-image-update-monitor/mobile-dark.png";
import { TOOLS } from "../data/tools";
import { SITE } from "../data/routes.mjs";
import type { Lang } from "../i18n/ui";
Expand All @@ -15,6 +21,7 @@ interface Props {
const { lang } = Astro.props;
const entries = await getCollection("tools", (e) => e.data.lang === lang);
const bySlug = Object.fromEntries(entries.map((e) => [e.data.slug, e]));
const previewTool = TOOLS.find((tool) => tool.slug === "cockpit-image-update-monitor")!;

const text = {
en: {
Expand All @@ -31,6 +38,10 @@ const text = {
["EU", "Our servers and analytics"],
["Transparent", "Dependencies named per project"],
],
previewTitle: "Keep container updates in sight.",
previewText: "See which version is running and which is available. Update your containers directly in Cockpit, with a backup before the update.",
previewLink: "View Image Update Monitor",
previewAlt: "Image Update Monitor in Cockpit: an excerpt of the container overview with installed and available versions, update status and update actions.",
projectsEyebrow: "Projects",
projectsTitle: "Current projects",
allProjects: "All projects compared",
Expand Down Expand Up @@ -65,6 +76,10 @@ const text = {
["EU", "Eigene Server und Statistik"],
["Transparent", "Abhängigkeiten je Projekt benannt"],
],
previewTitle: "Container-Updates im Blick.",
previewText: "Sieh, welche Version läuft und welche verfügbar ist. Aktualisiere deine Container direkt in Cockpit – mit Sicherung vor dem Update.",
previewLink: "Image Update Monitor ansehen",
previewAlt: "Image Update Monitor in Cockpit: Ausschnitt der Containerübersicht mit installierten und verfügbaren Versionen, Update-Status und Aktionen zum Aktualisieren.",
projectsEyebrow: "Projekte",
projectsTitle: "Aktuelle Projekte",
allProjects: "Alle Projekte im Vergleich",
Expand Down Expand Up @@ -138,6 +153,34 @@ const prefix = lang === "en" ? "" : "/de";
</div>
</section>

<section class="product-preview" aria-labelledby="update-preview-title">
<div class="wrap">
<figure class="product-preview__figure">
<Shot
image={updates}
dark={updatesDark}
mobile={{ image: updatesMobile, dark: updatesMobileDark }}
widths={[390, 640, 960, 1280, 1440]}
sizes="(max-width: 430px) calc(100vw - 40px), (max-width: 760px) 390px, (max-width: 1168px) calc(100vw - 48px), 1120px"
alt={text.previewAlt}
class="product-preview__shot"
/>
<figcaption class="product-preview__caption">
<div>
<p class="product-preview__name"><ProductName name={previewTool.name} /></p>
<h2 class="section-title" id="update-preview-title">{text.previewTitle}</h2>
</div>
<div>
<p>{text.previewText}</p>
<p class="more-link">
<a href={`${prefix}/${previewTool.slug}/`}>{text.previewLink} <span aria-hidden="true">→</span></a>
</p>
</div>
</figcaption>
</figure>
</div>
</section>

<section class="section">
<div class="wrap">
<p class="az-eyebrow">{text.projectsEyebrow}</p>
Expand Down
12 changes: 10 additions & 2 deletions src/components/Shot.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { ImageMetadata } from "astro";
interface Props {
image: ImageMetadata;
dark?: ImageMetadata;
/** A genuine narrow-screen capture, used below the site's 760 px layout breakpoint. */
mobile?: { image: ImageMetadata; dark?: ImageMetadata };
alt: string;
/** Widths to offer; the browser picks by the space the picture gets. */
widths?: number[];
Expand All @@ -16,13 +18,15 @@ interface Props {
class?: string;
}

const { image, dark, alt, widths = [640, 960, 1280], sizes = "(max-width: 1168px) 100vw, 1120px", eager = false } = Astro.props;
const { image, dark, mobile, alt, widths = [640, 960, 1280], sizes = "(max-width: 1168px) 100vw, 1120px", eager = false } = Astro.props;
const usable = widths.filter((w) => w <= image.width);
if (usable.length === 0) usable.push(image.width);

async function variants(source: ImageMetadata) {
const list = [];
for (const width of usable) {
const sourceWidths = widths.filter((w) => w <= source.width);
if (sourceWidths.length === 0) sourceWidths.push(source.width);
for (const width of sourceWidths) {
const result = await getImage({ src: source, width, format: "webp", quality: 82 });
list.push(`${result.src} ${width}w`);
}
Expand All @@ -31,11 +35,15 @@ async function variants(source: ImageMetadata) {

const lightSet = await variants(image);
const darkSet = dark ? await variants(dark) : undefined;
const mobileSet = mobile ? await variants(mobile.image) : undefined;
const mobileDarkSet = mobile?.dark ? await variants(mobile.dark) : undefined;
const fallback = await getImage({ src: image, width: usable[usable.length - 1], format: "webp", quality: 82 });
const height = Math.round((image.height / image.width) * usable[usable.length - 1]);
---

<picture class={Astro.props.class}>
{mobileDarkSet && <source media="(max-width: 760px) and (prefers-color-scheme: dark)" srcset={mobileDarkSet} sizes={sizes} type="image/webp" />}
{mobileSet && <source media="(max-width: 760px)" srcset={mobileSet} sizes={sizes} type="image/webp" />}
{darkSet && <source media="(prefers-color-scheme: dark)" srcset={darkSet} sizes={sizes} type="image/webp" />}
<source srcset={lightSet} sizes={sizes} type="image/webp" />
<img
Expand Down
22 changes: 22 additions & 0 deletions src/styles/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ figcaption { margin-top: var(--space-sm); font-size: 14px; color: var(--text-sub
.fact__label { color: var(--text-subtle); margin: var(--space-sm) 0 0; }

/* ---------- Sections ---------- */
/* A real product view between the homepage facts and the project list. The crop ends after five
desktop rows, or one complete mobile card; the picture element selects the matching capture. */
.product-preview { padding-bottom: var(--space-section); }
.product-preview__figure { margin: 0; }
.product-preview__shot {
display: block; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius-medium);
background: var(--bg-page);
}
.product-preview__shot img { width: 100%; aspect-ratio: 1440 / 632; object-fit: cover; object-position: top; }
.product-preview__caption {
display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-columns); align-items: center;
margin-top: var(--space-xl); font-size: 17px;
}
.product-preview__caption > * { min-width: 0; }
.product-preview__name { font-size: 22px; color: var(--text); }
.product-preview__caption .more-link { margin-top: var(--space-md); }
.product-preview__caption a { display: inline-flex; align-items: center; gap: var(--space-sm); min-height: 44px; }

.section { padding-block: var(--space-section); border-top: 1px solid var(--line); }
.section--alt { background: var(--bg-card); }
.section--alt .az-label { background: var(--tint-neutral); }
Expand Down Expand Up @@ -293,12 +311,16 @@ pre code { font-size: inherit; }
.split, .split--wide { grid-template-columns: 1fr; gap: 40px; }
}
@media (max-width: 760px) {
.product-preview__shot { max-width: 390px; margin-inline: auto; }
.product-preview__shot img { aspect-ratio: 390 / 581; }
.product-preview__caption { grid-template-columns: 1fr; gap: var(--space-lg); }
.card-grid, .gallery, .values, .hero__pair, .hero__split { grid-template-columns: 1fr; }
.hero__split { gap: var(--space-lg); }
.hero__split .hero__icon { order: -1; }
.hero__icon img { width: 96px; height: 96px; }
}
@media (max-width: 639px) {
.product-preview { padding-bottom: 72px; }
.wrap { padding-inline: 20px; }
.hero { padding-top: 56px; padding-bottom: 64px; }
.section { padding-block: 72px; }
Expand Down
Loading