Created: March 20, 2026 Status: β COMPLETE Version: 1.0.0
The journal section displays blog posts/articles with featured images from Unsplash. Each post card links to a dedicated single post page.
File: /data/mock/journal-posts.ts
Structure:
export interface JournalPost {
id: string;
slug: string;
title: string;
excerpt: string;
category: string;
featuredImage: string; // Unsplash URL
publishedAt: string;
}
```text
**Total Posts:** 6 posts across different categories:
- Essay (2 posts)
- Video (1 post)
- Podcast (1 post)
- Travel (1 post)
- Field Notes (1 post)
### Routes
**Archive Page:** `/journal` β `JournalPage` component
**Single Post:** `/journal/:slug` β `JournalSinglePage` component
**Route Configuration** (`/routes.ts`):
```typescript
{ path: "journal", Component: JournalPage },
{ path: "journal/:slug", Component: JournalSinglePage }
```text
β οΈ **Order Matters:** The `/journal/:slug` route MUST come AFTER the `/journal`
route to prevent routing conflicts.
---
## Components
### 1. JournalPage (Archive)
**File:** `/components/pages/book-site/JournalPage.tsx`
**Features:**
- Displays all journal posts in a grid
- Featured images (240px height, cropped with `objectFit: cover`)
- Category-based color coding
- Click-through to single posts
- ES5-compatible syntax (no arrow functions)
**Category Colors:**
- Essay β Pink (`card__category--pink`)
- Video β Green (`card__category--green`)
- Podcast β Violet (`card__category--violet`)
- Travel β Yellow (`card__category--yellow`)
- Field Notes β Pink (`card__category--pink`)
**Click Handlers:**
```typescript
function handlePostClick(slug: string) {
return function() {
navigate('/journal/' + slug);
};
}
```text
**Card Structure:**
```tsx
<article className="card" onClick={handlePostClick(slug)}>
<div className="card__image-wrapper">
<img src={featuredImage} alt={title} />
</div>
<span className="card_*category card*_category--{color}">{category}</span>
<h3 className="heading-card">{title}</h3>
<p className="text-body">{excerpt}</p>
<a href="/journal/{slug}" className="card__link">Read more β</a>
</article>
```text
### 2. JournalSinglePage (Single Post)
**File:** `/components/pages/book-site/JournalSinglePage.tsx`
**Features:**
- Displays individual journal post
- Large featured image (400px height)
- Post metadata (category, publish date)
- Full content area
- Back to Journal button (top and bottom)
- 404 handling for invalid slugs
- Dynamic SEO metadata with featured image
**Layout:**
1. Back button (top)
1. Featured image (full width, 400px height)
1. Post header (category badge, title, date)
1. Content area
1. Back to Journal CTA (bottom)
## URL Parameter Extraction
```typescript
var params = useParams();
var slug = params.slug;
// Find post
var post = null;
for (var i = 0; i < journalPosts.length; i++) {
if (journalPosts[i].slug === slug) {
post = journalPosts[i];
break;
}
}
```text
**SEO Integration:**
```typescript
React.useEffect(function() {
if (post) {
setSEO({
title: post.title + ' | Nova News',
description: post.excerpt,
ogTitle: post.title,
ogDescription: post.excerpt,
ogImage: post.featuredImage,
twitterCard: 'summary*large*image'
});
}
}, [post]);
```text
---
## Unsplash Images
All featured images are sourced from Unsplash using the `unsplash_tool`. Images
are selected to match the post content:
| Post | Query | Image URL |
|---|---|---|
| Dancefloor belonging | "dancefloor techno party neon lights" | images.unsplash.com/photo-1666682115302... |
| UV paint visible | "UV paint festival art glow" | images.unsplash.com/photo-1771167213926... |
| Living in full colour | "podcast microphone studio setup" | images.unsplash.com/photo-1709846485906... |
| Berlin bicycles | "Berlin bicycle urban cycling" | images.unsplash.com/photo-1628603998251... |
| Rough draft notes | "notebook writing manuscript desk" | images.unsplash.com/photo-1756993263826... |
| Standing out skill | "colorful neon soul expression art" | images.unsplash.com/photo-1663023943477... |
**Image Parameters:**
- `crop=entropy` - Smart cropping
- `cs=tinysrgb` - Color space
- `fit=max` - Fit mode
- `fm=jpg` - Format
- `q=80` - Quality
- `w=1080` - Width
---
## Styling
### Featured Images
## Archive Cards (JournalPage)
```css
.card__image-wrapper {
width: 100%;
height: 240px;
overflow: hidden;
border-radius: 8px;
margin-bottom: 16px;
}
.card__image {
width: 100%;
height: 100%;
object-fit: cover;
}
```text
## Single Post (JournalSinglePage)
```css
{
width: 100%;
max-width: 1200px;
height: 400px;
margin: 0 auto;
overflow: hidden;
border-radius: 12px;
margin-bottom: 32px;
}
```text
### Category Badges
Defined in `/styles/globals.css`:
```css
.card__category { color: var(--color-uv-violet); }
.card__category--yellow { color: var(--color-neon-yellow); }
.card__category--green { color: #39FF14; }
.card__category--pink { color: var(--color-neon-pink); }
.card__category--violet { color: var(--color-uv-violet); }
```text
---
## ES5 Bundler Compatibility
### Forbidden Patterns
β Arrow functions β β
Classic `function()` syntax
β Template literals β β
String concatenation
β Optional chaining β β
Explicit null checks
β `const`/`let` β β
`var` declarations
### Safe Patterns
## Loop with Classic For
```typescript
for (var i = 0; i < journalPosts.length; i++) {
if (journalPosts[i].slug === slug) {
post = journalPosts[i];
break;
}
}
```text
## Closure Pattern for Click Handlers
```typescript
function handlePostClick(slug: string) {
return function() {
navigate('/journal/' + slug);
};
}
```text
## Null Check with Conditional
```typescript
if (e && e.preventDefault) e.preventDefault();
```text
---
## Navigation Flow
### User Journey
1. **Homepage** β User sees "Journal" in navigation
1. **Click Journal** β Navigate to `/journal`
1. **Journal Archive** β Grid of 6 blog post cards with images
1. **Click Card** β Navigate to `/journal/{slug}`
1. **Single Post** β Read full post with featured image
1. **Back Button** β Return to `/journal`
### Internal Links
**From Homepage:**
```tsx
<a href="/journal">Journal</a>
```text
## From Journal Archive to Single Post
```tsx
onClick={handlePostClick('dancefloor-belonging')}
// Navigates to: /journal/dancefloor-belonging
```text
## From Single Post back to Archive
```tsx
<button onClick={() => navigate('/journal')}>
Back to Journal
</button>
```text
---
## SEO & Metadata
### Journal Archive Page
```typescript
setSEO(pageSEO.journal);
```text
**Values:**
- Title: "Journal | Nova News"
- Description: "Notes, stories, videos, and companion media from the world of the book."
- OG Image: Default site image
- Twitter Card: summary
### Single Post Page
```typescript
setSEO({
title: post.title + ' | Nova News',
description: post.excerpt,
ogTitle: post.title,
ogDescription: post.excerpt,
ogImage: post.featuredImage, // Unsplash image
twitterCard: 'summary*large*image'
});
```text
**Dynamic Values:**
- Title: Post title + site name
- Description: Post excerpt
- OG Image: **Post's featured image** (Unsplash URL)
- Twitter Card: Large image format
---
## Accessibility
β
**WCAG 2.2 AA Compliant**
**Semantic HTML:**
- `<article>` for blog post cards
- `<h1>`, `<h2>`, `<h3>` proper heading hierarchy
- `<img>` with descriptive `alt` text
## Keyboard Navigation
- Tab to focus on cards
- Enter/Space to click
- Back button keyboard accessible
**Focus Indicators:**
- 3px neon pink outline
- Visible on all interactive elements
**Color Contrast:**
- Text on dark backgrounds: 7:1+ (AAA)
- Category badges: 4.5:1+ (AA)
**Reduced Motion:**
- No animations on journal cards
- Hover effects use simple transforms
- `prefers-reduced-motion` respected
---
## Content Expansion
### Adding New Posts
**Step 1:** Add to `/data/mock/journal-posts.ts`
```typescript
{
id: 'unique-id',
slug: 'url-friendly-slug',
title: 'Post Title',
excerpt: 'Brief description...',
category: 'Essay', // or Video, Podcast, Travel, Field Notes
featuredImage: 'https://images.unsplash.com/photo-...',
publishedAt: '2026-03-20'
}
```text
**Step 2:** Get Unsplash Image
```text
Use unsplash_tool with descriptive query
Copy URL to featuredImage property
```text
**Step 3:** Test
```text
Visit /journal β Should see new card
Click card β Should navigate to /journal/{slug}
```text
### Category Guidelines
**Essay** - Written reflections, personal stories
**Video** - Visual content, video embeds
**Podcast** - Audio content, podcast episodes
**Travel** - Journey stories, location-based posts
**Field Notes** - Behind-the-scenes, process updates
---
## Testing Checklist
### Journal Archive Page (1)
- [ ] `/journal` loads without errors
- [ ] All 6 posts display in grid
- [ ] Featured images load correctly
- [ ] Category colors match expected values
- [ ] Click on card navigates to single post
- [ ] Click on "Read more" link navigates to single post
- [ ] Responsive grid works on mobile
- [ ] Tag filters visible (decorative only for now)
### Single Post Page (1)
- [ ] `/journal/dancefloor-belonging` loads
- [ ] Featured image displays (400px height)
- [ ] Title, category, date visible
- [ ] Content area renders
- [ ] Back button (top) navigates to /journal
- [ ] Back button (bottom) navigates to /journal
- [ ] Invalid slug shows 404 message
- [ ] SEO metadata includes featured image
### Navigation
- [ ] Header "Journal" link works
- [ ] Footer "Journal" link works (if exists)
- [ ] Browser back button works
- [ ] Direct URL entry works
### Mobile
- [ ] Grid collapses to 1 column
- [ ] Images scale correctly
- [ ] Cards remain clickable
- [ ] Touch targets adequate size
---
## Known Limitations
1. **Static Content:** Full post content is placeholder text (not from data)
1. **No Rich Text:** Content doesn't support markdown/HTML yet
1. **No Pagination:** All posts load at once (fine for 6 posts)
1. **No Filtering:** Tag filters are decorative only
1. **No Search:** No search functionality within journal
1. **No Comments:** No comment system
---
## Future Enhancements
### Phase 1 (Short Term)
- [ ] Add rich text content to data layer (markdown support)
- [ ] Wire up category filter tags
- [ ] Add "Related Posts" section to single post
- [ ] Add social sharing buttons
- [ ] Add reading progress indicator
### Phase 2 (Medium Term)
- [ ] Pagination (10 posts per page)
- [ ] Search within journal
- [ ] Author profiles (multi-author support)
- [ ] Post series/collections
- [ ] Featured post carousels
### Phase 3 (Long Term)
- [ ] Comments system (Disqus or custom)
- [ ] Newsletter signup integration
- [ ] Post bookmarking/favorites
- [ ] Content recommendations algorithm
- [ ] RSS feed generation
---
## Troubleshooting
### Images Not Loading
**Problem:** Unsplash images return 403 or don't load
**Solution:** Check Unsplash URL parameters are correct. Re-fetch with
unsplash_tool if needed.
### Route Not Working
**Problem:** `/journal/:slug` returns 404
**Solution:** Verify route order in `/routes.ts`. Single route MUST come after
archive route.
### Click Not Navigating
**Problem:** Card click doesn't navigate
**Solution:** Check `handlePostClick` closure returns function. Verify
`navigate()` is imported.
### Category Color Wrong
**Problem:** Badge shows wrong color
**Solution:** Check `getCategoryColor()` function. Verify CSS classes exist in
`globals.css`.
### SEO Image Not Showing
**Problem:** Featured image doesn't appear in social shares
**Solution:** Verify `ogImage` is set to `post.featuredImage`. Test with
Facebook Debugger.
---
## Dependencies
**React Router:**
- `useParams()` - Extract slug from URL
- `useNavigate()` - Programmatic navigation
**Phosphor Icons:**
- `ArrowLeft` - Back button icon
**Utils:**
- `setSEO()` - Dynamic SEO metadata
**Data:**
- `journalPosts` - Post data array
- `pageSEO.journal` - Archive page SEO
---
## Performance Notes
**Image Loading:**
- All images loaded from Unsplash CDN
- No lazy loading implemented (6 images is acceptable)
- Consider lazy loading if expanding to 20+ posts
**Rendering:**
- 6 cards render immediately on page load
- No virtualization needed for small dataset
- Single post renders single image + content
**Bundle Size:**
- Data file: ~2KB
- Components: ~8KB total
- No heavy dependencies added
---
**Documentation Version:** 1.0.0
**Last Updated:** March 20, 2026
**Maintained By:** Nova News Development Team
π **Journal is fully functional and ready for content!**