docs/blog index page (#20188)
* docs: add card-based blog index page for mobile navigation Fixes #20100 - the blog landing page showed post content directly instead of an index, with no way to navigate between posts on mobile. - Swizzle BlogListPage with card-based grid layout - Featured latest post spans full width with badge - Responsive 2-column grid with orphan handling - Pagination, SEO metadata, accessibility (aria-label, dateTime, heading hierarchy) - Add description frontmatter to existing blog posts * docs: add deterministic fallback colors for unknown blog tags * docs: rename blog heading to The LiteLLM Blog
This commit is contained in:
parent
a92b938926
commit
82383cde74
@ -15,6 +15,7 @@ authors:
|
||||
title: "CTO, LiteLLM"
|
||||
url: https://www.linkedin.com/in/reffajnaahsi/
|
||||
image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg
|
||||
description: "Guide to Claude Opus 4.5 and advanced features in LiteLLM: Tool Search, Programmatic Tool Calling, and Effort Parameter."
|
||||
tags: [anthropic, claude, tool search, programmatic tool calling, effort, advanced features]
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
@ -15,6 +15,7 @@ authors:
|
||||
title: "CTO, LiteLLM"
|
||||
url: https://www.linkedin.com/in/reffajnaahsi/
|
||||
image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg
|
||||
description: "Common questions and best practices for using gemini-3-pro-preview with LiteLLM Proxy and SDK."
|
||||
tags: [gemini, day 0 support, llms]
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
@ -15,6 +15,7 @@ authors:
|
||||
title: "CTO, LiteLLM"
|
||||
url: https://www.linkedin.com/in/reffajnaahsi/
|
||||
image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg
|
||||
description: "Guide to using Gemini 3 Flash on LiteLLM Proxy and SDK with day 0 support."
|
||||
tags: [gemini, day 0 support, llms]
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||
123
docs/my-website/src/theme/BlogListPage/index.js
Normal file
123
docs/my-website/src/theme/BlogListPage/index.js
Normal file
@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import Layout from '@theme/Layout';
|
||||
import Link from '@docusaurus/Link';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
const TAG_COLORS = {
|
||||
gemini: {bg: '#d2e3fc', text: '#174ea6', darkBg: '#1a3a5c', darkText: '#8ab4f8'},
|
||||
anthropic: {bg: '#fde0c4', text: '#b33d00', darkBg: '#4a2800', darkText: '#ffb74d'},
|
||||
claude: {bg: '#fde0c4', text: '#b33d00', darkBg: '#4a2800', darkText: '#ffb74d'},
|
||||
llms: {bg: '#c8e6c9', text: '#1b5e20', darkBg: '#1b3d1f', darkText: '#81c784'},
|
||||
};
|
||||
|
||||
function hashHue(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return Math.abs(hash) % 360;
|
||||
}
|
||||
|
||||
function getTagColor(label) {
|
||||
const key = label.toLowerCase();
|
||||
for (const [k, v] of Object.entries(TAG_COLORS)) {
|
||||
if (key === k) return v;
|
||||
}
|
||||
const hue = hashHue(key);
|
||||
return {
|
||||
bg: `hsl(${hue}, 40%, 90%)`,
|
||||
text: `hsl(${hue}, 60%, 25%)`,
|
||||
darkBg: `hsl(${hue}, 40%, 20%)`,
|
||||
darkText: `hsl(${hue}, 50%, 75%)`,
|
||||
};
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffDays = Math.floor((now - d) / (1000 * 60 * 60 * 24));
|
||||
if (diffDays <= 0) return 'Today';
|
||||
if (diffDays === 1) return '1d ago';
|
||||
if (diffDays < 30) return `${diffDays}d ago`;
|
||||
return d.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric'});
|
||||
}
|
||||
|
||||
function BlogCard({post, featured}) {
|
||||
const {title, permalink, date, description, tags} = post;
|
||||
const visibleTags = (tags || []).slice(0, 3);
|
||||
|
||||
return (
|
||||
<Link to={permalink} className={styles.cardLink} aria-label={title}>
|
||||
<article className={featured ? styles.cardFeatured : styles.card}>
|
||||
<div className={styles.meta}>
|
||||
<time className={styles.time} dateTime={date}>{formatDate(date)}</time>
|
||||
{featured && <span className={styles.badge}>Latest</span>}
|
||||
</div>
|
||||
<h2 className={styles.title}>{title}</h2>
|
||||
{description && <p className={styles.desc}>{description}</p>}
|
||||
{visibleTags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{visibleTags.map(tag => {
|
||||
const c = getTagColor(tag.label);
|
||||
return (
|
||||
<span key={tag.label} className={styles.tag} style={{
|
||||
'--tag-bg': c.bg, '--tag-text': c.text,
|
||||
'--tag-bg-dark': c.darkBg, '--tag-text-dark': c.darkText,
|
||||
}}>{tag.label}</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.arrow} aria-hidden="true">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M6 3l5 5-5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function Pagination({metadata}) {
|
||||
const {previousPage, nextPage} = metadata;
|
||||
if (!previousPage && !nextPage) return null;
|
||||
return (
|
||||
<nav className={styles.pagination} aria-label="Blog list pagination">
|
||||
{previousPage ? (
|
||||
<Link to={previousPage} className={styles.paginationLink}>← Newer posts</Link>
|
||||
) : <span />}
|
||||
{nextPage ? (
|
||||
<Link to={nextPage} className={styles.paginationLink}>Older posts →</Link>
|
||||
) : <span />}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BlogListPage(props) {
|
||||
const items = props.items || [];
|
||||
const metadata = props.metadata || {};
|
||||
const [first, ...rest] = items;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={metadata.blogTitle || 'Blog'}
|
||||
description={metadata.blogDescription || 'Guides, announcements, and best practices from the LiteLLM team.'}
|
||||
>
|
||||
<header className={styles.hero}>
|
||||
<h1 className={styles.heroTitle}>The LiteLLM Blog</h1>
|
||||
<p className={styles.heroSubtitle}>Guides, announcements, and best practices from the LiteLLM team.</p>
|
||||
</header>
|
||||
|
||||
<main className={styles.grid}>
|
||||
{first && (
|
||||
<BlogCard post={first.content.metadata} featured />
|
||||
)}
|
||||
{rest.map(({content}) => (
|
||||
<BlogCard key={content.metadata.permalink} post={content.metadata} />
|
||||
))}
|
||||
</main>
|
||||
|
||||
<Pagination metadata={metadata} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
163
docs/my-website/src/theme/BlogListPage/styles.module.css
Normal file
163
docs/my-website/src/theme/BlogListPage/styles.module.css
Normal file
@ -0,0 +1,163 @@
|
||||
.hero {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 1.5rem 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.heroSubtitle {
|
||||
color: var(--ifm-color-emphasis-600);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cardLink {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
border: 1px solid var(--ifm-color-emphasis-200);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
padding-right: 2.5rem;
|
||||
height: 100%;
|
||||
transition: border-color 0.15s, transform 0.15s, background 0.15s;
|
||||
background: var(--ifm-background-surface-color, var(--ifm-background-color));
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--ifm-color-primary);
|
||||
transform: translateY(-2px);
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.cardFeatured {
|
||||
composes: card;
|
||||
border-color: var(--ifm-color-primary-lighter);
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--ifm-color-emphasis-600);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 2px 8px;
|
||||
border-radius: 99px;
|
||||
background: var(--ifm-color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.4rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 0.88rem;
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
line-height: 1.5;
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
padding: 2px 10px;
|
||||
border-radius: 99px;
|
||||
background: var(--tag-bg);
|
||||
color: var(--tag-text);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .tag {
|
||||
background: var(--tag-bg-dark);
|
||||
color: var(--tag-text-dark);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--ifm-color-emphasis-400);
|
||||
transition: color 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.card:hover .arrow {
|
||||
color: var(--ifm-color-primary);
|
||||
transform: translateY(-50%) translateX(3px);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1.5rem 3rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.paginationLink {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.paginationLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid .cardLink:first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.grid .cardLink:last-child:nth-child(even) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user