Skip to content

Commit c5d3dbb

Browse files
markcclaude
andcommitted
Refine blog UI with horizontal cards, pagination, and navigation
Blog Index: - Horizontal card layout (image left, content right) - Compose with card-hover for DRY styling - Remove redundant header, use ctx->perp for pagination - Longer excerpts with italic metadata Single Post: - Prev/next navigation at top and bottom - Featured image inline (33% width, right-aligned) - Simplified nav labels (just « Title and Title ») CSS (base.css): - Add .blog-nav, .blog-list, .blog-item styles - Add .blog-item-meta/.blog-single-meta with italic - Mobile responsive (stack on small screens) Root Router: - Serve static files from chapter public/ directories - Correct content-type headers for images Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b0e916c commit c5d3dbb

File tree

8 files changed

+154
-60
lines changed

8 files changed

+154
-60
lines changed

09-Blog/src/Core/Ctx.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ final class Ctx
1717
public Db $db;
1818

1919
// Centralized config
20-
public int $perp = 10; // Items per page
20+
public int $perp = 6; // Items per page
2121

2222
public function __construct(
2323
public string $email = 'noreply@localhost',

09-Blog/src/Plugins/Blog/BlogModel.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313

1414
final class BlogModel extends Plugin
1515
{
16-
private const int DEFAULT_PER_PAGE = 9;
17-
1816
private ?Db $dbh = null;
1917
private array $in = ['id' => 0];
2018

@@ -32,7 +30,7 @@ public function __construct(
3230
public function list(): array
3331
{
3432
$page = filter_var($_REQUEST['page'] ?? 1, FILTER_VALIDATE_INT) ?: 1;
35-
$perPage = self::DEFAULT_PER_PAGE;
33+
$perPage = $this->ctx->perp;
3634
$offset = ($page - 1) * $perPage;
3735

3836
$where = 'type = :type';

09-Blog/src/Plugins/Blog/BlogView.php

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,48 +25,45 @@ public function list(): string
2525
$a = $this->a;
2626
$t = $this->t();
2727

28-
$html = <<<HTML
29-
<div class="blog-header">
30-
<h1>Blog</h1>
31-
<p class="text-muted">Latest posts and updates</p>
32-
</div>
33-
<div class="blog-grid">
34-
HTML;
28+
$html = '<div class="blog-list">';
3529

3630
foreach ($a['items'] as $post) {
3731
$title = htmlspecialchars($post['title']);
3832
$excerpt = htmlspecialchars($post['excerpt']);
3933
$author = htmlspecialchars($post['author']);
4034
$date = date('M j, Y', strtotime($post['created']));
4135
$image = $post['featured_image'] ?: 'https://picsum.photos/seed/' . $post['id'] . '/400/200';
36+
$url = "?o=Blog&m=read&id={$post['id']}$t";
4237

4338
$html .= <<<HTML
44-
<article class="blog-card">
45-
<a href="?o=Blog&m=read&id={$post['id']}$t" class="blog-card-image">
39+
<article class="card-hover blog-item">
40+
<a href="$url" class="blog-item-image">
4641
<img src="$image" alt="$title" loading="lazy">
4742
</a>
48-
<div class="blog-card-content">
49-
<h3><a href="?o=Blog&m=read&id={$post['id']}$t">$title</a></h3>
50-
<p class="blog-card-excerpt">$excerpt</p>
51-
<p class="blog-card-meta">
52-
<span>$author</span> &bull; <span>$date</span>
53-
</p>
43+
<div class="blog-item-content">
44+
<h3><a href="$url">$title</a></h3>
45+
<p class="blog-item-meta">$author &bull; $date</p>
46+
<p class="blog-item-excerpt">$excerpt</p>
5447
</div>
5548
</article>
5649
HTML;
5750
}
5851

5952
$html .= '</div>';
6053

61-
// Pagination
54+
// Pagination (similar style to single page prev/next)
6255
$p = $a['pagination'];
6356
if ($p['pages'] > 1) {
64-
$html .= '<div class="blog-pagination">';
65-
if ($p['page'] > 1)
66-
$html .= "<a href=\"?o=Blog&page=" . ($p['page'] - 1) . "$t\" class=\"btn\">« Newer</a>";
67-
$html .= "<span>Page {$p['page']} of {$p['pages']}</span>";
68-
if ($p['page'] < $p['pages'])
69-
$html .= "<a href=\"?o=Blog&page=" . ($p['page'] + 1) . "$t\" class=\"btn\">Older »</a>";
57+
$html .= '<div class="blog-nav">';
58+
if ($p['page'] > 1) {
59+
$html .= "<a href=\"?o=Blog&page=" . ($p['page'] - 1) . "$t\" class=\"blog-nav-prev\">« Newer Posts</a>";
60+
} else {
61+
$html .= '<span></span>';
62+
}
63+
$html .= "<span class=\"blog-nav-page\">Page {$p['page']} of {$p['pages']}</span>";
64+
if ($p['page'] < $p['pages']) {
65+
$html .= "<a href=\"?o=Blog&page=" . ($p['page'] + 1) . "$t\" class=\"blog-nav-next\">Older Posts »</a>";
66+
}
7067
$html .= '</div>';
7168
}
7269

@@ -90,7 +87,7 @@ public function read(): string
9087
$author = htmlspecialchars($a['author']);
9188
$date = date('F j, Y', strtotime($a['created']));
9289
$image = $a['featured_image']
93-
? "<img src=\"{$a['featured_image']}\" alt=\"$title\" class=\"blog-featured-image\">"
90+
? "<img src=\"{$a['featured_image']}\" alt=\"$title\" class=\"blog-featured-image\" style=\"float:right;width:33%;margin:0 0 1rem 1.5rem;border-radius:var(--radius-md);\">"
9491
: '';
9592

9693
// Build category tags
@@ -109,25 +106,25 @@ public function read(): string
109106
$prevNext = '<div class="blog-nav">';
110107
if ($a['prev']) {
111108
$prevTitle = htmlspecialchars($a['prev']['title']);
112-
$prevNext .= "<a href=\"?o=Blog&m=read&id={$a['prev']['id']}$t\" class=\"blog-nav-prev\"><span>« Previous</span><strong>$prevTitle</strong></a>";
109+
$prevNext .= "<a href=\"?o=Blog&m=read&id={$a['prev']['id']}$t\" class=\"blog-nav-prev\">« $prevTitle</a>";
113110
} else {
114111
$prevNext .= '<span></span>';
115112
}
116113
if ($a['next']) {
117114
$nextTitle = htmlspecialchars($a['next']['title']);
118-
$prevNext .= "<a href=\"?o=Blog&m=read&id={$a['next']['id']}$t\" class=\"blog-nav-next\"><span>Next »</span><strong>$nextTitle</strong></a>";
115+
$prevNext .= "<a href=\"?o=Blog&m=read&id={$a['next']['id']}$t\" class=\"blog-nav-next\">$nextTitle »</a>";
119116
}
120117
$prevNext .= '</div>';
121118

122119
return <<<HTML
120+
$prevNext
123121
<article class="blog-single">
124122
<header class="blog-single-header">
125123
<h1><a href="?o=Blog$t" class="back-arrow">«</a> $title</h1>
126124
<p class="blog-single-meta">By $author &bull; $date</p>
127125
$catTags
128126
</header>
129-
$image
130-
<div class="prose">$content</div>
127+
<div class="prose">$image$content</div>
131128
</article>
132129
$prevNext
133130
HTML;

10-Htmx/src/Core/Ctx.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ final class Ctx
1717
public Db $db;
1818

1919
// Centralized config
20-
public int $perp = 10; // Items per page
20+
public int $perp = 6; // Items per page
2121

2222
public function __construct(
2323
public string $email = 'noreply@localhost',

10-Htmx/src/Plugins/Blog/BlogModel.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313

1414
final class BlogModel extends Plugin
1515
{
16-
private const int DEFAULT_PER_PAGE = 9;
17-
1816
private ?Db $dbh = null;
1917
private array $in = ['id' => 0];
2018

@@ -32,7 +30,7 @@ public function __construct(
3230
public function list(): array
3331
{
3432
$page = filter_var($_REQUEST['page'] ?? 1, FILTER_VALIDATE_INT) ?: 1;
35-
$perPage = self::DEFAULT_PER_PAGE;
33+
$perPage = $this->ctx->perp;
3634
$offset = ($page - 1) * $perPage;
3735

3836
$where = 'type = :type';

10-Htmx/src/Plugins/Blog/BlogView.php

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,7 @@ public function list(): string
2525
$a = $this->a;
2626
$t = $this->t();
2727

28-
$html = <<<HTML
29-
<div class="blog-header">
30-
<h1>Blog</h1>
31-
<p class="text-muted">Latest posts and updates</p>
32-
</div>
33-
<div class="blog-grid">
34-
HTML;
28+
$html = '<div class="blog-list">';
3529

3630
foreach ($a['items'] as $post) {
3731
$title = htmlspecialchars($post['title']);
@@ -42,35 +36,35 @@ public function list(): string
4236
$url = "?o=Blog&m=read&id={$post['id']}$t";
4337

4438
$html .= <<<HTML
45-
<article class="blog-card">
46-
<a href="$url" hx-get="$url" hx-target="#main" hx-push-url="true" class="blog-card-image">
39+
<article class="card-hover blog-item">
40+
<a href="$url" hx-get="$url" hx-target="#main" hx-push-url="true" class="blog-item-image">
4741
<img src="$image" alt="$title" loading="lazy">
4842
</a>
49-
<div class="blog-card-content">
43+
<div class="blog-item-content">
5044
<h3><a href="$url" hx-get="$url" hx-target="#main" hx-push-url="true">$title</a></h3>
51-
<p class="blog-card-excerpt">$excerpt</p>
52-
<p class="blog-card-meta">
53-
<span>$author</span> &bull; <span>$date</span>
54-
</p>
45+
<p class="blog-item-meta">$author &bull; $date</p>
46+
<p class="blog-item-excerpt">$excerpt</p>
5547
</div>
5648
</article>
5749
HTML;
5850
}
5951

6052
$html .= '</div>';
6153

62-
// Pagination with htmx
54+
// Pagination (similar style to single page prev/next)
6355
$p = $a['pagination'];
6456
if ($p['pages'] > 1) {
65-
$html .= '<div class="blog-pagination">';
57+
$html .= '<div class="blog-nav">';
6658
if ($p['page'] > 1) {
6759
$prevUrl = "?o=Blog&page=" . ($p['page'] - 1) . $t;
68-
$html .= "<a href=\"$prevUrl\" hx-get=\"$prevUrl\" hx-target=\"#main\" hx-push-url=\"true\" class=\"btn\">« Newer</a>";
60+
$html .= "<a href=\"$prevUrl\" hx-get=\"$prevUrl\" hx-target=\"#main\" hx-push-url=\"true\" class=\"blog-nav-prev\">« Newer Posts</a>";
61+
} else {
62+
$html .= '<span></span>';
6963
}
70-
$html .= "<span>Page {$p['page']} of {$p['pages']}</span>";
64+
$html .= "<span class=\"blog-nav-page\">Page {$p['page']} of {$p['pages']}</span>";
7165
if ($p['page'] < $p['pages']) {
7266
$nextUrl = "?o=Blog&page=" . ($p['page'] + 1) . $t;
73-
$html .= "<a href=\"$nextUrl\" hx-get=\"$nextUrl\" hx-target=\"#main\" hx-push-url=\"true\" class=\"btn\">Older »</a>";
67+
$html .= "<a href=\"$nextUrl\" hx-get=\"$nextUrl\" hx-target=\"#main\" hx-push-url=\"true\" class=\"blog-nav-next\">Older Posts »</a>";
7468
}
7569
$html .= '</div>';
7670
}
@@ -95,7 +89,7 @@ public function read(): string
9589
$author = htmlspecialchars($a['author']);
9690
$date = date('F j, Y', strtotime($a['created']));
9791
$image = $a['featured_image']
98-
? "<img src=\"{$a['featured_image']}\" alt=\"$title\" class=\"blog-featured-image\">"
92+
? "<img src=\"{$a['featured_image']}\" alt=\"$title\" class=\"blog-featured-image\" style=\"float:right;width:33%;margin:0 0 1rem 1.5rem;border-radius:var(--radius-md);\">"
9993
: '';
10094

10195
// Build category tags
@@ -115,27 +109,27 @@ public function read(): string
115109
if ($a['prev']) {
116110
$prevTitle = htmlspecialchars($a['prev']['title']);
117111
$prevUrl = "?o=Blog&m=read&id={$a['prev']['id']}$t";
118-
$prevNext .= "<a href=\"$prevUrl\" hx-get=\"$prevUrl\" hx-target=\"#main\" hx-push-url=\"true\" class=\"blog-nav-prev\"><span>« Previous</span><strong>$prevTitle</strong></a>";
112+
$prevNext .= "<a href=\"$prevUrl\" hx-get=\"$prevUrl\" hx-target=\"#main\" hx-push-url=\"true\" class=\"blog-nav-prev\">« $prevTitle</a>";
119113
} else {
120114
$prevNext .= '<span></span>';
121115
}
122116
if ($a['next']) {
123117
$nextTitle = htmlspecialchars($a['next']['title']);
124118
$nextUrl = "?o=Blog&m=read&id={$a['next']['id']}$t";
125-
$prevNext .= "<a href=\"$nextUrl\" hx-get=\"$nextUrl\" hx-target=\"#main\" hx-push-url=\"true\" class=\"blog-nav-next\"><span>Next »</span><strong>$nextTitle</strong></a>";
119+
$prevNext .= "<a href=\"$nextUrl\" hx-get=\"$nextUrl\" hx-target=\"#main\" hx-push-url=\"true\" class=\"blog-nav-next\">$nextTitle »</a>";
126120
}
127121
$prevNext .= '</div>';
128122

129123
$backUrl = "?o=Blog$t";
130124
return <<<HTML
125+
$prevNext
131126
<article class="blog-single">
132127
<header class="blog-single-header">
133128
<h1><a href="$backUrl" hx-get="$backUrl" hx-target="#main" hx-push-url="true" class="back-arrow">«</a> $title</h1>
134129
<p class="blog-single-meta">By $author &bull; $date</p>
135130
$catTags
136131
</header>
137-
$image
138-
<div class="prose">$content</div>
132+
<div class="prose">$image$content</div>
139133
</article>
140134
$prevNext
141135
HTML;

docs/base.css

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,106 @@
14821482
opacity: 1;
14831483
text-decoration: none;
14841484
}
1485+
1486+
/* === Blog Navigation (Prev/Next) === */
1487+
.blog-nav {
1488+
display: flex;
1489+
justify-content: space-between;
1490+
align-items: center;
1491+
gap: var(--space-4);
1492+
margin-block-end: var(--space-2);
1493+
}
1494+
1495+
.blog-nav a {
1496+
font-weight: 500;
1497+
}
1498+
1499+
.blog-nav-page {
1500+
color: var(--fg-muted);
1501+
font-size: var(--text-sm);
1502+
}
1503+
1504+
.blog-nav-next {
1505+
margin-inline-start: auto;
1506+
}
1507+
1508+
/* === Blog List (Index) === */
1509+
.blog-header {
1510+
margin-block-end: var(--space-6);
1511+
}
1512+
1513+
.blog-header h1 {
1514+
margin: 0;
1515+
}
1516+
1517+
.blog-list {
1518+
display: flex;
1519+
flex-direction: column;
1520+
gap: var(--space-6);
1521+
}
1522+
1523+
.blog-item {
1524+
display: flex;
1525+
gap: var(--space-6);
1526+
}
1527+
1528+
.blog-item-image {
1529+
flex-shrink: 0;
1530+
width: 200px;
1531+
}
1532+
1533+
.blog-item-image img {
1534+
width: 100%;
1535+
height: 150px;
1536+
object-fit: cover;
1537+
border-radius: var(--radius-md);
1538+
}
1539+
1540+
.blog-item-content {
1541+
flex: 1;
1542+
display: flex;
1543+
flex-direction: column;
1544+
}
1545+
1546+
.blog-item-content h3 {
1547+
margin: 0 0 var(--space-2);
1548+
}
1549+
1550+
.blog-item-meta,
1551+
.blog-single-meta {
1552+
font-size: var(--text-sm);
1553+
font-style: italic;
1554+
color: var(--fg-muted);
1555+
margin: 0 0 var(--space-3);
1556+
}
1557+
1558+
.blog-item-excerpt {
1559+
margin: 0;
1560+
color: var(--fg-muted);
1561+
}
1562+
1563+
.blog-pagination {
1564+
display: flex;
1565+
justify-content: center;
1566+
align-items: center;
1567+
gap: var(--space-4);
1568+
margin-block-start: var(--space-8);
1569+
}
1570+
1571+
/* Mobile: stack image on top */
1572+
@media (max-width: 599px) {
1573+
.blog-item {
1574+
flex-direction: column;
1575+
}
1576+
1577+
.blog-item-image {
1578+
width: 100%;
1579+
}
1580+
1581+
.blog-item-image img {
1582+
height: 200px;
1583+
}
1584+
}
14851585
}
14861586

14871587
/* === Utilities === */

index.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010

1111
// Chapter pattern: /XX-Name/... -> /XX-Name/public/...
1212
if (preg_match('#^/(\d{2}-[^/]+)(/.*)?$#', $uri, $m) && is_dir($pub = __DIR__ . "/{$m[1]}/public")) {
13-
if (is_file($f = $pub . ($m[2] ?? '/'))) return false;
13+
if (is_file($f = $pub . ($m[2] ?? '/'))) {
14+
// Serve static file with correct content type
15+
$ext = pathinfo($f, PATHINFO_EXTENSION);
16+
$types = ['css' => 'text/css', 'js' => 'text/javascript', 'webp' => 'image/webp',
17+
'png' => 'image/png', 'jpg' => 'image/jpeg', 'gif' => 'image/gif', 'svg' => 'image/svg+xml'];
18+
header('Content-Type: ' . ($types[$ext] ?? mime_content_type($f)));
19+
return readfile($f);
20+
}
1421
$_SERVER['SCRIPT_NAME'] = "/{$m[1]}/public/index.php";
1522
return require "$pub/index.php";
1623
}

0 commit comments

Comments
 (0)