Magazine-Style Layout Implementation for Content Collections

Summary

Implemented a flexible, reusable magazine-style layout system for displaying content collections, featuring a prominent feature card and a responsive grid of standard cards with consistent styling and data handling.

Why Care

This implementation provides a powerful, reusable pattern for presenting content collections in an engaging magazine-style format across the site. The modular design separates content loading from presentation, making it easy to apply this layout to any content collection while maintaining consistent styling and behavior. The responsive design ensures optimal viewing across all devices.

Implementation

Changes Made

  • Created new UI components for the magazine layout:
    • /site/src/components/articles/PostCard.astro
    • /site/src/components/articles/PostCardFeature.astro
    • /site/src/layouts/PostCardContentLayout.astro
  • Added support for prompts collection in content config:
    • Updated /site/src/content.config.ts to include prompts collection
    • Added path mapping for prompts collection
  • Created new pages for content display:
    • /site/src/pages/prompts/
    • /site/src/pages/thread/
  • Updated data files:
    • Modified /site/src/content/citations/citation-registry.json
    • Modified /site/src/content/messages/featureSideImage.json

Technical Details

Content Collection Configuration

typescript
// In /site/src/content.config.ts
const promptsCollection = defineCollection({
  loader: glob({pattern: "**/*.md", base: "../content/lost-in-public/prompts"}),
  schema: z.object({}).passthrough().transform((data) => ({
    ...data,
    // Ensure tags is always an array, even if null/undefined in frontmatter
    tags: Array.isArray(data.tags) ? data.tags
      : data.tags ? [data.tags]
      : []
  }))
});

// Added to paths object
'prompts': '../content/lost-in-public/prompts'

// Added to collections export
'prompts': promptsCollection

PostCard Component

typescript
// In /site/src/components/articles/PostCard.astro
---
// PostCard.astro
// Accepts a generic object as props and deconstructs only the needed fields.
// This follows project rules: NO type safety, NO explicit interfaces, passthrough pattern only.

// Accept both Astro collection entry and flat frontmatter object
const { data, ...rest } = Astro.props;
const props = data ? data : Astro.props;
const { title, lede, authors, tags, date_authored_initial_draft, url, summary, image } = props;

// Format date if it exists
let formattedDate = '';
if (date_authored_initial_draft) {
  // Date formatting logic...
}
---

<article class="post-card card-hover-effect">
  {image && <img src={image} alt={title} class="post-card__image" />}
  <div class="content-wrapper">
    <div class="post-card__header">
      <h2 class="post-card__title"><a href={url}><span class="text-wrapper">{title}</span></a></h2>
      <p class="post-card__lede"><span class="text-wrapper">{lede || summary}</span></p>
    </div>
    <div class="post-card__meta">
      <span class="post-card__authors">{authors?.join(', ')}</span>
      <span class="post-card__date">{formattedDate}</span>
      {tags && tags.length > 0 && (
        <div class="post-card__tags">
          {tags.map(tag => <span class="post-card__tag">{tag}</span>)}
        </div>
      )}
    </div>
  </div>
</article>

PostCardFeature Component

typescript
// In /site/src/components/articles/PostCardFeature.astro
---
// PostCardFeature.astro
// Accepts a generic prompt object (Astro content entry) as props and destructures to get frontmatter fields.
// This follows project rules: NO type safety, NO explicit interfaces, passthrough pattern only.

// Accept both Astro collection entry and flat frontmatter object
const { data, ...rest } = Astro.props;
const props = data ? data : Astro.props;
const { title, lede, authors, tags, date_authored_initial_draft, url, summary, image } = props;

// Format date if it exists
let formattedDate = '';
if (date_authored_initial_draft) {
  // Date formatting logic...
}
---

<article class="post-card-feature card-hover-effect">
  {image && <img src={image} alt={title} class="post-card-feature__image" />}
  <h1 class="post-card-feature__title"><a href={url}><span class="text-wrapper">{title}</span></a></h1>
  <p class="post-card-feature__lede"><span class="text-wrapper">{lede || summary}</span></p>
  <div class="post-card-feature__meta">
    <span class="post-card-feature__authors">{authors?.join(', ')}</span>
    <span class="post-card-feature__date">{formattedDate}</span>
    <div class="post-card-feature__tags">
      {tags?.map(tag => <span class="post-card-feature__tag">{tag}</span>)}
    </div>
  </div>
</article>

PostCardContentLayout Component

typescript
// In /site/src/layouts/PostCardContentLayout.astro
---
// PostCardContentLayout.astro
// Receives an array of objects as the 'contentThreads' prop.
// Renders the first as a feature card, the rest as standard cards.
// Follows passthrough pattern: NO type safety, NO explicit interfaces.
import { getCollection } from "astro:content";

import PostCardFeature from '../components/articles/PostCardFeature.astro';
import PostCard from '../components/articles/PostCard.astro';

const { contentThreads = [] } = Astro.props;
---

{contentThreads.length === 0 ? (
  <div class="magazine-empty">No content available.</div>
) : (
  <div class="magazine-container">
    {/* Feature card displayed separately, outside the grid */}
    {contentThreads.length > 0 && (
      <div class="feature-card-container">
        <PostCardFeature {...contentThreads[0]} />
      </div>
    )}
    
    {/* Regular cards in a grid layout */}
    {contentThreads.length > 1 && (
      <div class="magazine-grid">
        {contentThreads.slice(1).map((item) => (
          <PostCard {...item} />
        ))}
      </div>
    )}
  </div>
)}

Responsive Grid Layout

css
/* In /site/src/layouts/PostCardContentLayout.astro */
.magazine-container {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-8, 2rem);
  margin: 2rem auto;
  max-width: 92vw;
}

.feature-card-container {
  width: 100%;
  margin-bottom: var(--spacing-4, 1rem);
}

.magazine-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--spacing-6, 1.5rem);
}

/* Medium screens: 2 columns */
@media (min-width: 768px) {
  .magazine-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Large screens: 3 columns */
@media (min-width: 1024px) {
  .magazine-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

Card Styling

Both card components include consistent styling with:
  • Responsive design that adapts to different screen sizes
  • Hover effects using the site's animation system
  • Proper text wrapping for all content
  • Consistent typography following the site's design system
  • Accessible color contrast and interactive elements
css
/* Example from /site/src/components/articles/PostCard.astro */
.post-card {
  background: color-mix(
    in oklab,
    var(--clr-lossless-primary-glass),
    var(--clr-lossless-primary-dark) 90%
  );
  border: 1px solid var(--clr-lossless-ui-btn-border);
  color: var(--clr-lossless-primary-light);
  padding: 1rem;
  border-radius: 1em;
  display: flex;
  flex-direction: column;
  height: 100%;
  box-sizing: border-box;
}

Integration Points

  • The magazine layout system integrates with Astro's content collections
  • The components follow the project's passthrough pattern for props
  • The layout is designed to work with any content collection that provides the necessary fields
  • The styling is consistent with the site's design system
  • The implementation respects the project's established patterns for content rendering

Documentation

  • All components include comprehensive comments explaining their purpose and usage
  • The implementation follows the project's established patterns for content rendering
  • The styling is consistent with the project's design system
  • Debug information is included for development but can be removed for production

Future Enhancements

  • Add filtering and sorting capabilities to the magazine layout
  • Implement pagination for large collections
  • Add category-based grouping for more organized browsing
  • Create variations of the layout for different content types
  • Add animation effects for card interactions