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
17 changes: 17 additions & 0 deletions .github/workflows/publish-website-next.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ jobs:
run: yarn playwright install --with-deps chromium
working-directory: website-next

- name: Cache Optimized Images
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
website-next/public/_optimized
key: ${{ runner.os }}-optimized-images-${{ hashFiles('website-next/public/images/**', 'website-next/src/image-optimization/**', 'website-next/scripts/optimize-images.mjs') }}
restore-keys: |
${{ runner.os }}-optimized-images-

- name: Optimize Images
run: yarn optimize-images
working-directory: website-next

- name: Generate Git Metadata
run: yarn generate-git-metadata
working-directory: website-next

- name: Build Website
run: yarn build
working-directory: website-next
Expand Down
6 changes: 6 additions & 0 deletions website-next/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
/.next/
/out/

# build-time image optimization output
/public/_optimized/

# generated git metadata (produced by the release workflow)
/git-metadata.generated.json

# storybook
/storybook-static/

Expand Down
4 changes: 2 additions & 2 deletions website-next/app/(content)/platform/analytics/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Image from "next/image";
import { Picture } from "@/src/design-system/Picture";

import { ContentSection } from "@/src/components/ContentSection";
import { NextStepsSection } from "@/src/components/NextStepsSection";
Expand All @@ -19,7 +19,7 @@ export default function AnalyticsPage() {
</SolidButton>
</div>
<div className="mt-20 flex justify-center">
<Image
<Picture
src="/images/analytics/banner.png"
alt="Instant Insights. Enhanced Performance."
width={1200}
Expand Down
30 changes: 14 additions & 16 deletions website-next/app/blog/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BlogTags } from "@/src/components/BlogTags";
import { BlogTeaserGrid } from "@/src/components/BlogTeaserGrid";
import { NotFoundContent } from "@/src/components/NotFoundContent";
import { Pagination } from "@/src/design-system/Pagination";
import { Picture } from "@/src/design-system/Picture";
import { SimilarPosts } from "@/src/components/SimilarPosts";
import { Typography } from "@/src/design-system/Typography";
import { paginate, POSTS_PER_PAGE } from "@/src/helpers/blogPaging";
Expand All @@ -15,7 +16,10 @@ import {
listBlogPosts,
resolveBlogFile,
} from "@/src/helpers/blogPaths";
import { findSimilarPosts, listBlogPostSummaries } from "@/src/helpers/blogPosts";
import {
findSimilarPosts,
listBlogPostSummaries,
} from "@/src/helpers/blogPosts";
import { compileDoc } from "@/src/helpers/compileDoc";
import { readFrontmatter } from "@/src/helpers/readFrontmatter";
import { estimateReadingTime } from "@/src/helpers/readingTime";
Expand Down Expand Up @@ -48,20 +52,13 @@ export function generateStaticParams(): Params[] {
}));

const summaries = listBlogPostSummaries();
const totalPages = Math.max(
1,
Math.ceil(summaries.length / POSTS_PER_PAGE),
);
const totalPages = Math.max(1, Math.ceil(summaries.length / POSTS_PER_PAGE));
const pageParams: Params[] = [];
for (let p = 2; p <= totalPages; p++) {
pageParams.push({ slug: [String(p)] });
}

const params = [
...postParams,
...pageParams,
{ slug: [NOT_FOUND_SEGMENT] },
];
const params = [...postParams, ...pageParams, { slug: [NOT_FOUND_SEGMENT] }];
// output: export requires at least one prerendered path; placeholder
// renders 404 via notFound() when no content is present.
return params.length > 0 ? params : [{ slug: ["__empty__"] }];
Expand Down Expand Up @@ -118,7 +115,9 @@ export default async function BlogSlugPage({ params }: PageProps) {

if (isNotFoundSlug(slug)) {
return (
<NotFoundContent secondary={{ href: "/blog", label: "Browse the blog" }} />
<NotFoundContent
secondary={{ href: "/blog", label: "Browse the blog" }}
/>
);
}

Expand Down Expand Up @@ -148,13 +147,12 @@ export default async function BlogSlugPage({ params }: PageProps) {
<main className="px-5 py-8 sm:px-12">
<article className="mx-auto max-w-5xl">
{featuredImage ? (
// eslint-disable-next-line @next/next/no-img-element
<img
<Picture
src={featuredImage}
alt=""
loading="eager"
decoding="async"
className="mb-6 aspect-[16/9] w-full rounded-lg object-cover"
priority
sizes="(max-width: 1024px) 100vw, 1024px"
className="mb-6 aspect-video w-full rounded-lg object-cover"
/>
) : null}
{frontmatter.title ? (
Expand Down
10 changes: 8 additions & 2 deletions website-next/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Divider } from "@/src/design-system/Divider";
import { InlineCode } from "@/src/design-system/InlineCode";
import { Link } from "@/src/design-system/Link";
import { List, ListItem } from "@/src/design-system/List";
import { Image } from "@/src/design-system/Image";
import { Picture } from "@/src/design-system/Picture";
import { Quote } from "@/src/design-system/Quote";
import { Tab, Tabs } from "@/src/design-system/Tabs";
import { ApiChoiceTabs } from "@/src/design-system/ApiChoiceTabs";
Expand Down Expand Up @@ -69,7 +69,13 @@ const components: MDXComponents = {
th: TableHeaderCell,
td: TableCell,

img: Image,
img: (props) => (
<Picture
{...props}
className={`my-6 max-w-full rounded-md ${props.className ?? ""}`.trim()}
sizes="(max-width: 768px) 100vw, 1024px"
/>
),

CodeStep,
Tabs,
Expand Down
5 changes: 3 additions & 2 deletions website-next/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const exportToc = path.resolve(process.cwd(), "src/recma/exportToc.mjs");
const nextConfig: NextConfig = {
output: "export",
images: {
// TODO: Optimize images during build
unoptimized: true,
},
pageExtensions: ["ts", "tsx", "md", "mdx"],
Expand All @@ -30,4 +29,6 @@ const withMDX = createMDX({
},
});

export default withMDX(nextConfig);
const config = withMDX(nextConfig);

export default config;
4 changes: 4 additions & 0 deletions website-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start -p 3001",
"optimize-images": "node scripts/optimize-images.mjs",
"generate-git-metadata": "node scripts/generate-git-metadata.mjs",
"ci-local": "bash scripts/ci-local.sh",
"lint": "eslint",
"storybook": "storybook dev -p 6006",
Comment on lines 8 to 13
"build-storybook": "storybook build"
Expand All @@ -26,6 +29,7 @@
"react": "19.2.4",
"react-dom": "19.2.4",
"remark-gfm": "^4.0.1",
"sharp": "^0.34.5",
"shiki": "^4.0.2"
},
"devDependencies": {
Expand Down
121 changes: 121 additions & 0 deletions website-next/scripts/generate-git-metadata.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env node
// Standalone git metadata step, run as part of the release workflow (and the
// local `ci-local.sh` script). Walks the git history of the tracked content
// directories and writes `git-metadata.generated.json` mapping each doc/blog
// file to its last commit date and author. The app reads this file at build
// time; when it is absent it falls back to static placeholder metadata.
//
// A shallow checkout cannot produce accurate attribution, so this fails hard
// instead of silently degrading. Use `actions/checkout` with `fetch-depth: 0`.
import { execFileSync, spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";

const WEBSITE_ROOT = process.cwd();
const OUTPUT_PATH = path.join(WEBSITE_ROOT, "git-metadata.generated.json");
const HEADER = "@@COMMIT@@";

const TRACKED_DIRS = [
path.join(WEBSITE_ROOT, "content", "docs"),
path.join(WEBSITE_ROOT, "content", "blog"),
path.join(WEBSITE_ROOT, "app", "(content)"),
];

function gitRoot() {
const res = spawnSync("git", ["rev-parse", "--show-toplevel"], {
cwd: WEBSITE_ROOT,
encoding: "utf-8",
});
if (res.status !== 0) {
throw new Error("not a git repository, cannot generate git metadata");
}
return res.stdout.trim();
}

function assertNotShallow(repoRoot) {
const res = spawnSync("git", ["rev-parse", "--is-shallow-repository"], {
cwd: repoRoot,
encoding: "utf-8",
});
if (res.status === 0 && res.stdout.trim() === "true") {
throw new Error(
"shallow clone detected, doc attribution requires full history. " +
"Use `actions/checkout` with `fetch-depth: 0` in CI.",
);
}
}

function isTrackedFile(repoRelativeFile) {
if (/\.mdx?$/.test(repoRelativeFile)) {
return true;
}
return path.basename(repoRelativeFile) === "page.tsx";
}

function buildManifest(repoRoot) {
const trackedPaths = TRACKED_DIRS.map((p) => path.relative(repoRoot, p));

const stdout = execFileSync(
"git",
[
"-c",
"core.quotePath=false",
"log",
"--name-only",
"--no-merges",
"--diff-filter=AMR",
`--format=${HEADER}%aI%x09%an`,
"--",
...trackedPaths,
],
{
cwd: repoRoot,
encoding: "utf-8",
maxBuffer: 256 * 1024 * 1024,
stdio: ["ignore", "pipe", "ignore"],
},
);

const out = {};
let currentDate = null;
let currentAuthor = null;

for (const line of stdout.split("\n")) {
if (line === "") {
continue;
}
if (line.startsWith(HEADER)) {
const rest = line.slice(HEADER.length);
const tab = rest.indexOf("\t");
currentDate = tab === -1 ? rest : rest.slice(0, tab);
currentAuthor = tab === -1 ? "" : rest.slice(tab + 1);
continue;
}
if (currentDate === null || !isTrackedFile(line)) {
continue;
}
const abs = path.join(repoRoot, line);
const key = path.relative(WEBSITE_ROOT, abs);
if (key.startsWith("..") || out[key]) {
continue;
}
out[key] = { date: currentDate, author: currentAuthor ?? "" };
}

return out;
}

try {
const repoRoot = gitRoot();
assertNotShallow(repoRoot);
const manifest = buildManifest(repoRoot);
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(manifest, null, 2));
console.log(
`[git-metadata] wrote ${Object.keys(manifest).length} entries to ` +
path.relative(WEBSITE_ROOT, OUTPUT_PATH),
);
} catch (err) {
console.error(`[git-metadata] ${err?.message ?? err}`);
process.exit(1);
}
60 changes: 60 additions & 0 deletions website-next/scripts/optimize-images.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env node
// Standalone image optimization step, run as part of the release workflow (and
// the local `ci-local.sh` script). Generates AVIF/WebP variants, blur
// placeholders and self-hosted copies of remote images into
// `public/_optimized/`, then writes the manifest the app reads at build time.
// Renders a progress indicator: a live bar on a TTY, decile milestones in CI.
import process from "node:process";
import optimizeImages from "../src/image-optimization/generate.mjs";

const config = {
quality: 90,
widths: [640, 1080, 1920],
formats: ["avif", "webp"],
sourceDir: "public/images",
outputDir: "public/_optimized/images",
manifestPath: "public/_optimized/manifest.json",
};

const LABELS = { images: "Images", remote: "Remote" };
const isTTY = Boolean(process.stdout.isTTY);
let currentPhase = null;
let lastPct = -1;

function render({ phase, done, total }) {
if (total === 0) {
return;
}
if (phase !== currentPhase) {
currentPhase = phase;
lastPct = -1;
}
const label = (LABELS[phase] ?? phase).padEnd(6);
const pct = Math.floor((done / total) * 100);

if (isTTY) {
const width = 30;
const filled = Math.round((done / total) * width);
const bar = "█".repeat(filled) + "░".repeat(width - filled);
process.stdout.write(
`\r[image-opt] ${label} ${bar} ${done}/${total} (${pct}%)`,
);
if (done === total) {
process.stdout.write("\n");
}
return;
}

// Non-TTY (CI): one line per 10% to keep the log readable.
if (pct !== lastPct && (pct % 10 === 0 || done === total)) {
lastPct = pct;
console.log(`[image-opt] ${label.trim()} ${done}/${total} (${pct}%)`);
}
}

try {
await optimizeImages({ ...config, onProgress: render });
} catch (err) {
console.error(`[image-opt] failed: ${err?.stack ?? err}`);
process.exit(1);
}
8 changes: 4 additions & 4 deletions website-next/src/components/BlogMetadata.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Picture } from "@/src/design-system/Picture";

type BlogMetadataProps = {
author?: string;
authorUrl?: string;
Expand Down Expand Up @@ -35,14 +37,12 @@ export function BlogMetadata({
}
>
{authorImageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
<Picture
src={authorImageUrl}
alt={`${author}'s avatar`}
width={30}
height={30}
loading="lazy"
decoding="async"
sizes="30px"
className="mr-2 h-[30px] w-[30px] rounded-full object-cover"
/>
) : null}
Expand Down
Loading
Loading