Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
111 changes: 67 additions & 44 deletions website-next/app/blog/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { BlogMetadata } from "@/src/components/BlogMetadata";
import { BlogShareBar } from "@/src/components/BlogShareBar";
import { BlogSidebar } from "@/src/components/BlogSidebar";
import { BlogTags } from "@/src/components/BlogTags";
import { BlogTeaserGrid } from "@/src/components/BlogTeaserGrid";
import { DocsToolbar } from "@/src/components/DocsToolbar";
import { NotFoundContent } from "@/src/components/NotFoundContent";
import { SidebarDrawer } from "@/src/components/SidebarDrawer";
import { TableOfContents } from "@/src/components/TableOfContents";
import { Pagination } from "@/src/design-system/Pagination";
import { Picture } from "@/src/design-system/Picture";
import { SimilarPosts } from "@/src/components/SimilarPosts";
Expand Down Expand Up @@ -147,7 +151,7 @@ export default async function BlogSlugPage({ params }: PageProps) {
}

const absPath = path.join(BLOG_ROOT, rel);
const [{ content, frontmatter }, raw] = await Promise.all([
const [{ content, frontmatter, toc }, raw] = await Promise.all([
compileDoc<BlogFrontmatter>(absPath),
fs.readFile(absPath, "utf-8"),
]);
Expand All @@ -159,6 +163,11 @@ export default async function BlogSlugPage({ params }: PageProps) {
const similar = current ? findSimilarPosts(current, summaries) : [];
const featuredImage = current?.featuredImage ?? null;

// Left rail: the 10 most recent posts (summaries come newest-first),
// including the current post, which is highlighted as active.
const sidebarPosts = summaries.slice(0, 10);
const currentHref = current?.href ?? `/blog/${stem}`;

const jsonLd = current
? {
"@context": "https://schema.org",
Expand All @@ -183,50 +192,64 @@ export default async function BlogSlugPage({ params }: PageProps) {
: null;

return (
<main className="px-5 py-8 sm:px-12">
{jsonLd ? (
<script
type="application/ld+json"
// Escape `<` so content text can never close the script tag (XSS).
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
) : null}
<article className="mx-auto max-w-5xl">
{featuredImage ? (
<Picture
src={featuredImage}
alt=""
priority
// Mirrors the layout: a max-w-5xl (1024px) column inside px-5
// (sm:px-12) page padding, so the browser picks the smallest
// sufficient variant instead of rounding the slot up to 100vw.
sizes="(max-width: 639px) calc(100vw - 2.5rem), (max-width: 1119px) calc(100vw - 6rem), 1024px"
className="mb-6 aspect-video w-full rounded-lg object-cover"
/>
) : null}
{frontmatter.title ? (
<Typography variant="h1">{frontmatter.title}</Typography>
) : null}
<div className="flex flex-wrap items-center justify-between gap-4">
<BlogMetadata
author={frontmatter.author}
authorUrl={frontmatter.authorUrl}
authorImageUrl={frontmatter.authorImageUrl}
date={frontmatter.date}
readingTime={readingTime}
/>
<BlogShareBar
url={toAbsoluteUrl(current?.href ?? `/blog/${stem}`)}
title={frontmatter.title ?? ""}
/>
<div
data-docs-layout
className="cc-content-dark grid grid-cols-1 lg:grid-cols-[20rem_1fr]"
>
<SidebarDrawer>
<BlogSidebar posts={sidebarPosts} currentHref={currentHref} />
</SidebarDrawer>
<div className="min-w-0">
<DocsToolbar />
<div className="grid grid-cols-1 2xl:grid-cols-[1fr_20rem]">
Comment thread
tobias-tengler marked this conversation as resolved.
Resolved
<main className="min-w-0 px-5 pb-8 pt-16 sm:px-12 2xl:pt-8">
{jsonLd ? (
<script
type="application/ld+json"
// Escape `<` so content text can never close the script tag (XSS).
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
) : null}
<article className="mx-auto max-w-5xl">
{featuredImage ? (
<Picture
src={featuredImage}
alt=""
priority
// Mirrors the layout: a max-w-5xl (1024px) column inside px-5
// (sm:px-12) page padding, so the browser picks the smallest
// sufficient variant instead of rounding the slot up to 100vw.
sizes="(max-width: 639px) calc(100vw - 2.5rem), (max-width: 1119px) calc(100vw - 6rem), 1024px"
className="mb-6 aspect-video w-full rounded-lg object-cover"
/>
) : null}
{frontmatter.title ? (
<Typography variant="h1">{frontmatter.title}</Typography>
) : null}
<div className="flex flex-wrap items-center justify-between gap-4">
<BlogMetadata
author={frontmatter.author}
authorUrl={frontmatter.authorUrl}
authorImageUrl={frontmatter.authorImageUrl}
date={frontmatter.date}
readingTime={readingTime}
/>
<BlogShareBar
url={toAbsoluteUrl(current?.href ?? `/blog/${stem}`)}
title={frontmatter.title ?? ""}
/>
</div>
<BlogTags tags={frontmatter.tags} />
{content}
<SimilarPosts posts={similar} />
</article>
</main>
<TableOfContents items={toc} />
</div>
<BlogTags tags={frontmatter.tags} />
{content}
<SimilarPosts posts={similar} />
</article>
</main>
</div>
</div>
);
}

Expand Down
81 changes: 68 additions & 13 deletions website-next/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -246,38 +246,93 @@ html[data-smooth-scroll="true"]:has(:target) {
variables are declared by DocSearch on `:root` (its stylesheet loads
after this one, so a `:root` override here loses the cascade). Scoping
to `.DocSearch-Container` beats that with higher specificity and lets
descendants inherit via `var()`. Full token list:
descendants inherit via `var()`.

NOTE: @docsearch/css is v4, which renamed/added tokens. v4-only tokens left
unset fall back to the light `:root` defaults (bright-blue selected row,
white footer keys, light borders), so every token the modal reads must be
overridden here. Full token list:
https://docsearch.algolia.com/docs/styling */
.DocSearch-Container {
--docsearch-primary-color: var(--color-cc-accent);
--docsearch-highlight-color: var(--color-cc-accent);
--docsearch-focus-color: var(--color-cc-accent);
--docsearch-soft-primary-color: rgb(94 234 212 / 0.12);
--docsearch-text-color: var(--color-cc-ink);
--docsearch-secondary-text-color: var(--color-cc-ink-dim);
--docsearch-muted-color: var(--color-cc-ink-dim);
--docsearch-icon-color: var(--color-cc-ink-dim);
--docsearch-subtle-color: var(--color-cc-ink-faint);
--docsearch-container-background: rgb(5 8 15 / 0.7);
--docsearch-modal-background: #0c1322;
--docsearch-modal-background: var(--color-cc-surface);
--docsearch-modal-shadow:
inset 1px 1px 0 0 rgb(245 241 234 / 0.06), 0 12px 32px 0 rgb(0 0 0 / 0.5);
--docsearch-searchbox-background: rgb(245 241 234 / 0.05);
--docsearch-searchbox-focus-background: rgb(245 241 234 / 0.08);
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--color-cc-accent);
--docsearch-hit-color: rgb(245 241 234 / 0.8);
--docsearch-hit-active-color: #ffffff;
--docsearch-hit-background: rgb(245 241 234 / 0.04);
--docsearch-hit-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3);
--docsearch-key-gradient: linear-gradient(
-225deg,
rgb(245 241 234 / 0.08),
rgb(245 241 234 / 0.04)
);
--docsearch-key-shadow:
inset 0 -2px 0 0 rgb(0 0 0 / 0.3),
inset 0 0 1px 1px rgb(245 241 234 / 0.06), 0 1px 2px 1px rgb(0 0 0 / 0.3);
--docsearch-footer-background: #0b0f1a;
--docsearch-hit-highlight-color: rgb(94 234 212 / 0.12);
--docsearch-background-color: rgb(245 241 234 / 0.06);
--docsearch-key-background: rgb(245 241 234 / 0.06);
--docsearch-key-color: var(--color-cc-ink-dim);
--docsearch-key-pressed-shadow: inset 0 2px 4px rgb(0 0 0 / 0.4);
--docsearch-footer-background: var(--color-cc-bg);
--docsearch-footer-shadow:
0 -1px 0 0 rgb(245 241 234 / 0.1), 0 -3px 6px 0 rgb(0 0 0 / 0.2);
--docsearch-logo-color: var(--color-cc-ink-dim);
}

/* Give the modal a defined edge so it reads as a distinct surface over the
blurred page behind it. */
.DocSearch-Container .DocSearch-Modal {
border: 1px solid rgb(245 241 234 / 0.12);
}

/* The hit title is the primary line; the breadcrumb/path beneath it is
supporting context, so dim it down. */
.DocSearch-Container .DocSearch-Hit-path {
color: var(--color-cc-ink-dim);
}

/* Footer command hints ("Navigate", "Select", "Close") are chrome the user
reads, keep them at full ink rather than the muted token. */
.DocSearch-Container .DocSearch-Commands,
.DocSearch-Container .DocSearch-Commands li {
color: var(--color-cc-ink);
}

/* Hovering/selecting a hit highlights the row background; DocSearch also
recolors the leading icon and the enter-arrow to the accent, which reads as
noisy. Keep those icons at their resting dim ink instead. */
.DocSearch-Container .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-icon,
.DocSearch-Container .DocSearch-Hit[aria-selected="true"] .DocSearch-Hit-action {
color: var(--color-cc-ink-dim);
}

/* Search-bar chrome (magnifier, Clear, Close) defaults to the accent. Reserve
the accent for matched terms only and keep these controls as dim ink. The
magnifier svg flips to the accent on `:focus-within` and the Close button
(also a .DocSearch-Action) flips to a tinted accent box on hover, so those
states need overriding at higher specificity too. */
.DocSearch-Container .DocSearch-SearchBar-Magnifier,
.DocSearch-Container .DocSearch-MagnifierLabel,
.DocSearch-Container .DocSearch-MagnifierLabel svg,
.DocSearch-Container .DocSearch-Form:focus-within .DocSearch-MagnifierLabel svg,
.DocSearch-Container .DocSearch-Clear,
.DocSearch-Container .DocSearch-Close {
color: var(--color-cc-ink-dim);
}

.DocSearch-Container .DocSearch-Action.DocSearch-Close:hover {
background: rgb(245 241 234 / 0.08);
color: var(--color-cc-ink);
}

.DocSearch-Container .DocSearch-Clear:hover {
color: var(--color-cc-ink);
}

:where(h1, h2, h3, h4, h5, h6)[id] {
scroll-margin-top: 7.5rem;
}
Expand Down
46 changes: 46 additions & 0 deletions website-next/src/components/BlogSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Link from "next/link";
import type { BlogPostSummary } from "@/src/helpers/blogPosts";

/**
* Left rail for blog posts mirroring the docs sidebar, listing the most recent
* posts (newest first) so readers can jump between articles. The currently open
* post is excluded by the caller, which also caps the list length.
*/
Comment thread
tobias-tengler marked this conversation as resolved.
Resolved
export function BlogSidebar({
posts,
currentHref,
}: {
posts: BlogPostSummary[];
currentHref: string;
}) {
return (
<div className="flex min-h-0 flex-1 flex-col gap-2 px-5 py-6 text-sm">
<p className="px-3 text-xs font-semibold uppercase tracking-widest text-cc-ink-dim">
Latest posts
</p>
<nav className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
<ul className="flex flex-col gap-1">
{posts.map((post) => {
const isActive = post.href === currentHref;
return (
<li key={post.stem}>
<Link
href={post.href}
prefetch={false}
aria-current={isActive ? "page" : undefined}
className={`block rounded px-3 py-1.5 transition-colors ${
isActive
? "bg-cc-white/10 font-medium text-cc-white"
: "text-cc-nav-text hover:bg-cc-white/5 hover:text-cc-white"
}`}
>
{post.title}
</Link>
</li>
);
})}
</ul>
</nav>
</div>
);
}
Loading