From 058655f23d597ddea569b3f2c1d262b29cc55ee1 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Mon, 8 Dec 2025 03:58:22 -0700 Subject: [PATCH] Add blog components and enhance blog functionality - Introduced new BlogCard and BlogFilters components for improved blog post presentation and filtering capabilities. - Updated content configuration to include fields for featured posts, categories, and tags. - Enhanced the blog index page to display a featured post and editor's picks, along with a filterable grid for latest posts. - Added a new blog entry on the G-Star Raw Olympics campaign with associated metadata for better categorization and tagging. --- dev/blog-example.html | 1 + src/components/BlogCard.astro | 148 ++++++++++++++ src/components/BlogFilters.astro | 202 ++++++++++++++++++ src/content.config.ts | 4 + src/content/blog/gstar-raw-olympics.mdx | 3 + src/pages/blog/index.astro | 260 ++++++++++++++++++++---- 6 files changed, 577 insertions(+), 41 deletions(-) create mode 100644 dev/blog-example.html create mode 100644 src/components/BlogCard.astro create mode 100644 src/components/BlogFilters.astro diff --git a/dev/blog-example.html b/dev/blog-example.html new file mode 100644 index 0000000..050b765 --- /dev/null +++ b/dev/blog-example.html @@ -0,0 +1 @@ +

Become an AI-Powered Marketer

The Averi Blog

Don't Feed the Algorithm

By signing up you agree to our privacy policy

Latest

Search

Don't Feed the Algorithm

“Top 3 tech + AI newsletters in the country. Always sharp, always actionable.”

"Genuinely my favorite newsletter in tech. No fluff, no cheesy ads, just great content."

“Clear, practical, and on-point. Helps me keep up without drowning in noise.”

\ No newline at end of file diff --git a/src/components/BlogCard.astro b/src/components/BlogCard.astro new file mode 100644 index 0000000..b5cdf09 --- /dev/null +++ b/src/components/BlogCard.astro @@ -0,0 +1,148 @@ +--- +import { Image } from 'astro:assets'; +import type { ImageMetadata } from 'astro'; +import FormattedDate from './FormattedDate.astro'; + +interface Props { + title: string; + description: string; + pubDate: Date; + heroImage?: ImageMetadata; + category?: string; + tags?: string[]; + href: string; + variant?: 'default' | 'compact' | 'featured'; + class?: string; +} + +const { + title, + description, + pubDate, + heroImage, + category, + tags, + href, + variant = 'default', + class: className = '', +} = Astro.props; + +// Compute estimated read time (rough estimate: 200 words per minute) +// For now, we'll show a placeholder since we don't have word count in frontmatter +const readTime = '5 min read'; + +const isCompact = variant === 'compact'; +const isFeatured = variant === 'featured'; +--- + +
+ +
+ + + + {heroImage && ( + + )} +
+
+ + + {category && ( +
+ + {category} + +
+ )} +
+ + +
+ +
+ + + + + + {readTime} + +
+ + + +

+ {title} +

+
+ + +

+ {description} +

+ + + {tags && tags.length > 0 && !isCompact && ( +
+ {tags.slice(0, 4).map((tag) => ( + + {tag} + + ))} +
+ )} + + +
+ + Read Article + + + + + + +
+
+
+ diff --git a/src/components/BlogFilters.astro b/src/components/BlogFilters.astro new file mode 100644 index 0000000..415c2eb --- /dev/null +++ b/src/components/BlogFilters.astro @@ -0,0 +1,202 @@ +--- +interface Props { + categories: string[]; + class?: string; +} + +const { categories, class: className = '' } = Astro.props; +--- + +
+ +
+ +
+ + /// FILTER BY + + + {categories.map((category) => ( + + ))} +
+ + +
+
+ + + + +
+ + +
+
+ + +
+ + 0 ARTICLES + + +
+
+ + + + + diff --git a/src/content.config.ts b/src/content.config.ts index 2ec7509..d7bafff 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -13,6 +13,10 @@ const blog = defineCollection({ pubDate: z.coerce.date(), updatedDate: z.coerce.date().optional(), heroImage: image().optional(), + // Blog hub fields + featured: z.boolean().optional().default(false), + category: z.string().optional(), + tags: z.array(z.string()).optional(), }), }); diff --git a/src/content/blog/gstar-raw-olympics.mdx b/src/content/blog/gstar-raw-olympics.mdx index 63e0d33..0275147 100644 --- a/src/content/blog/gstar-raw-olympics.mdx +++ b/src/content/blog/gstar-raw-olympics.mdx @@ -3,6 +3,9 @@ title: 'G-Star Raw Olympics Campaign' description: 'A deep dive into the VFX supervision and technical pipeline behind the G-Star Raw Olympics brand film, created in collaboration with Stinkfilms and director Felix Brady.' pubDate: 'Aug 15 2024' heroImage: '../../assets/g-star-image.jpg' +featured: true +category: 'Case Study' +tags: ['VFX', 'Houdini', 'Nuke', 'AI/ML', 'Brand Film'] --- In summer 2024, Biohazard VFX partnered with Stinkfilms and director Felix Brady to create a visually striking brand film for G-Star Raw's Olympics campaign. diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 6fb776d..3200942 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -3,14 +3,31 @@ import { Image } from 'astro:assets'; import { getCollection } from 'astro:content'; import BaseLayout from '../../layouts/BaseLayout.astro'; import FormattedDate from '../../components/FormattedDate.astro'; +import BlogCard from '../../components/BlogCard.astro'; +import BlogFilters from '../../components/BlogFilters.astro'; import { SITE_DESCRIPTION, SITE_TITLE } from '../../consts'; -const posts = (await getCollection('blog')).sort( +// Fetch all posts sorted by date (newest first) +const allPosts = (await getCollection('blog')).sort( (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(), ); + +// Derive featured post (first post with featured: true, or fallback to latest) +const featuredPost = allPosts.find((post) => post.data.featured) || allPosts[0]; + +// Editor's picks: next 3 posts after featured (excluding the featured one) +const editorPicks = allPosts + .filter((post) => post.id !== featuredPost?.id) + .slice(0, 3); + +// Latest posts: all posts for the filterable grid +const latestPosts = allPosts; + +// Extract unique categories for filters +const categories = [...new Set(allPosts.map((post) => post.data.category).filter(Boolean))] as string[]; --- - +
@@ -18,49 +35,210 @@ const posts = (await getCollection('blog')).sort( Back to Home -
-

Blog

-

/// THOUGHTS & PROCESS

+ +
+
+

+ BLOG + ARCHIVE +

+
+
+
/// THOUGHTS & PROCESS
+

+ Deep dives into VFX production, technical pipelines, and creative process. Sharing lessons from the front lines of visual effects. +

+
- + + + +

+ {featuredPost.data.title} +

+
+ + +

+ {featuredPost.data.description} +

+ + + {featuredPost.data.tags && featuredPost.data.tags.length > 0 && ( +
+ {featuredPost.data.tags.slice(0, 5).map((tag: string) => ( + + {tag} + + ))} +
+ )} + + + +
+ + + + )} + + + {editorPicks.length > 0 && ( +
+
+ + /// EDITOR'S PICKS + + +
+ +
+ {editorPicks.map((post, index) => ( +
+ +
+ ))} +
+
+ )} + + +
+ + +
+
+ + /// LATEST TRANSMISSIONS + + +
+ + + + + +
+ {latestPosts.map((post, index) => ( +
+ +
+ ))} +
+ + + +