Skip to content

Commit bb36251

Browse files
committed
feat(admin): sidebar dashboard redesign + read-next card fixes
- Admin: fixed left sidebar nav on desktop (glyph icons, nested sub-tabs, tools pinned to the foot), brand editorial-yellow accent replacing the cyan, marketing-matched cream light theme, serif stat numerals, self-hosted fonts (Google Fonts removed) - Admin: tab badges with [hidden] no longer render as stray dots - Post pages: "Read next" cards fall back to /cover/<slug>.svg when a post has no stored hero (no more imageless cards) and excerpts truncate on word boundaries with an ellipsis
1 parent 3ebcbc6 commit bb36251

3 files changed

Lines changed: 134 additions & 44 deletions

File tree

functions/_lib/page_render.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,19 +167,31 @@ export function renderContentPage({ env, request, post, kind, related = [], sett
167167

168168
// "Read next" — only on /blog/<slug> pages and only when we have at
169169
// least one sibling post to link to.
170+
// Word-safe excerpt: cut at the last space before the limit and add
171+
// an ellipsis, so cards never end mid-word ("cost-effecti").
172+
const excerpt = (s, n) => {
173+
const t = String(s || '');
174+
if (t.length <= n) return t;
175+
const cut = t.slice(0, n);
176+
return cut.slice(0, Math.max(cut.lastIndexOf(' '), 40)).replace(/[,;:.\s]+$/, '') + '…';
177+
};
178+
170179
const relatedHTML = (kind === 'blog' && related.length) ? `
171180
<aside class="read-next">
172181
<h2 class="read-next-title">Read next</h2>
173182
<ul class="read-next-list">
174183
${related.map((r) => {
175-
const rSrc = r.hero_image_key ? `/image/${esc(r.hero_image_key)}` : '';
184+
// Same fallback as the blog index: stored R2 hero, else the live
185+
// cover template — so every card gets an image and the row
186+
// doesn't look half-finished.
187+
const rSrc = r.hero_image_key ? `/image/${esc(r.hero_image_key)}` : `/cover/${esc(r.slug)}.svg`;
176188
return `
177189
<li>
178190
<a href="/blog/${esc(r.slug)}">
179-
${rSrc ? `<img src="${rSrc}" alt="${esc(r.hero_image_alt || r.title)}" width="640" height="336" loading="lazy" decoding="async" />` : ''}
191+
<img src="${rSrc}" alt="${esc(r.hero_image_alt || r.title)}" width="640" height="336" loading="lazy" decoding="async" />
180192
<div class="read-next-meta">
181193
<h3>${esc(r.title)}</h3>
182-
${r.meta_description ? `<p>${esc(r.meta_description.slice(0, 140))}</p>` : ''}
194+
${r.meta_description ? `<p>${esc(excerpt(r.meta_description, 140))}</p>` : ''}
183195
</div>
184196
</a>
185197
</li>`;

public/admin.css

Lines changed: 116 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,53 @@
11
/* pages-seo · admin dashboard.
22
* Standalone styling — doesn't share the public site's CSS so it can
3-
* evolve independently. Dark UI, dense tables, focused on operators
4-
* who'll spend hours staring at it. */
3+
* evolve independently. Dark operator UI with the brand's editorial
4+
* yellow as the accent; fixed sidebar nav on desktop, dense tables. */
5+
6+
@font-face { font-family: "Instrument Serif"; font-style: normal; font-weight: 400; font-display: swap; src: url("/_fonts/instrument-serif-400.woff2") format("woff2"); }
7+
@font-face { font-family: "Instrument Serif"; font-style: italic; font-weight: 400; font-display: swap; src: url("/_fonts/instrument-serif-400-italic.woff2") format("woff2"); }
8+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 400; font-display: swap; src: url("/_fonts/inter-400.woff2") format("woff2"); }
9+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 500; font-display: swap; src: url("/_fonts/inter-500.woff2") format("woff2"); }
10+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 600; font-display: swap; src: url("/_fonts/inter-600.woff2") format("woff2"); }
11+
@font-face { font-family: "Inter"; font-style: normal; font-weight: 700; font-display: swap; src: url("/_fonts/inter-700.woff2") format("woff2"); }
12+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("/_fonts/jetbrains-mono-400.woff2") format("woff2"); }
13+
@font-face { font-family: "JetBrains Mono"; font-style: normal; font-weight: 500; font-display: swap; src: url("/_fonts/jetbrains-mono-500.woff2") format("woff2"); }
514

615
:root{
7-
--bg: #06080d;
8-
--bg-soft: #0d1117;
9-
--bg-card: #0f1520;
10-
--ink: #e8edf2;
11-
--ink-dim: #a3aec0;
12-
--ink-faint: #6a7484;
13-
--accent: #7afff0;
14-
--accent-soft: rgba(122,255,240,0.10);
16+
--bg: #0f0e0b;
17+
--bg-soft: #16140f;
18+
--bg-card: #1a1813;
19+
--ink: #f0eee8;
20+
--ink-dim: #a8a298;
21+
--ink-faint: #6f6a60;
22+
--accent: #f5cf3e;
23+
--accent-ink: #131210; /* text colour ON accent surfaces */
24+
--accent-soft: rgba(245,207,62,0.10);
1525
--line: rgba(255,255,255,0.08);
1626
--line-2: rgba(255,255,255,0.18);
1727
--good: #7afca6;
1828
--warn: #e0b76a;
1929
--bad: #ff8aa1;
30+
--serif: 'Instrument Serif', 'Iowan Old Style', Palatino, Georgia, serif;
2031
--sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
2132
--mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
2233
}
2334

2435
/* Light theme — opt-in via data-theme="light" on <html>. Triggered
25-
by the topbar theme toggle and persisted in localStorage. Keeps
26-
the same accent (Workers AI cyan) and the same status colours
27-
so muscle memory transfers between modes. */
36+
by the theme toggle and persisted in localStorage. Light mode uses
37+
the marketing site's cream + black-CTA palette; status colours stay
38+
put so muscle memory transfers between modes. */
2839
:root[data-theme="light"]{
29-
--bg: #f7f5ee;
30-
--bg-soft: #efece2;
40+
--bg: #faf8f3;
41+
--bg-soft: #f2efe7;
3142
--bg-card: #ffffff;
32-
--ink: #0a0c10;
33-
--ink-dim: #4b525e;
34-
--ink-faint: #8a93a3;
35-
--accent: #0a6e62;
36-
--accent-soft: rgba(10,110,98,0.10);
37-
--line: rgba(10,12,16,0.10);
38-
--line-2: rgba(10,12,16,0.18);
43+
--ink: #131210;
44+
--ink-dim: #57534b;
45+
--ink-faint: #8f897d;
46+
--accent: #131210;
47+
--accent-ink: #faf8f3;
48+
--accent-soft: rgba(245,207,62,0.28);
49+
--line: rgba(19,18,16,0.10);
50+
--line-2: rgba(19,18,16,0.20);
3951
--good: #137a3a;
4052
--warn: #8a5a00;
4153
--bad: #b3203d;
@@ -179,12 +191,15 @@ a:hover{text-decoration:underline}
179191
.gate-card .err{margin-top:12px;color:var(--bad);font-size:13px;min-height:18px}
180192

181193
/* ── Dash shell ──────────────────────────────────────────────────── */
194+
/* Base (mobile/tablet): horizontal top bar. Desktop (≥1001px):
195+
fixed left sidebar — see the media query below. Markup is shared;
196+
only CSS changes between the two. */
182197
.dash[hidden]{display:none}
183198
.topbar{
184199
position:sticky;top:0;z-index:5;
185200
display:flex;align-items:center;gap:18px;
186201
padding:14px 24px;
187-
background:rgba(6,8,13,0.85);backdrop-filter:blur(8px);
202+
background:color-mix(in srgb, var(--bg) 88%, transparent);backdrop-filter:blur(8px);
188203
border-bottom:1px solid var(--line);
189204
}
190205
.topbar .brand{font-family:var(--mono);font-weight:600;color:var(--accent);letter-spacing:.04em;font-size:14px}
@@ -197,9 +212,71 @@ a:hover{text-decoration:underline}
197212
.topbar .tab:hover{color:var(--ink)}
198213
.topbar .tab[aria-current="page"]{color:var(--ink);background:var(--bg-soft);border-color:var(--line-2)}
199214

215+
200216
.pages{max-width:1200px;margin:0 auto;padding:28px 24px 80px}
201217
.page > .card + .card{margin-top:18px}
202218

219+
/* Desktop: proper dashboard chrome — fixed sidebar, content column. */
220+
/* Geometric glyph per section — keyed off data-tab so admin.js's
221+
markup stays untouched. Sidebar-only: inline glyphs wrap badly in
222+
the narrow horizontal top bar. */
223+
@media (min-width:1001px){
224+
.topbar .tab::before{
225+
font-family:var(--mono);
226+
display:inline-block;width:17px;text-align:center;
227+
margin-right:7px;color:var(--ink-faint);
228+
transition:color .15s;
229+
}
230+
.topbar .tab[data-tab="overview"]::before{content:"▤"}
231+
.topbar .tab[data-tab="blog"]::before{content:"✎"}
232+
.topbar .tab[data-tab="brand"]::before{content:"◈"}
233+
.topbar .tab[data-tab="covers"]::before{content:"▣"}
234+
.topbar .tab[data-tab="seo"]::before{content:"↗"}
235+
.topbar .tab[data-tab="status"]::before{content:"◉"}
236+
.topbar .tab[data-tab="settings"]::before{content:"⚙"}
237+
.topbar .tab:hover::before,
238+
.topbar .tab[aria-current="page"]::before{color:var(--accent)}
239+
}
240+
241+
@media (min-width:1001px){
242+
.topbar{
243+
position:fixed;top:0;left:0;bottom:0;width:238px;
244+
flex-direction:column;align-items:stretch;gap:6px;
245+
padding:22px 14px 16px;
246+
background:var(--bg-soft);backdrop-filter:none;
247+
border-bottom:none;border-right:1px solid var(--line);
248+
overflow-y:auto;
249+
}
250+
.topbar .brand{
251+
font-size:14px;padding:0 10px 16px;margin-bottom:8px;
252+
border-bottom:1px solid var(--line);
253+
}
254+
.topbar .tabs{flex-direction:column;gap:2px;margin-left:0;flex:0 0 auto}
255+
.topbar .tab{
256+
display:flex;align-items:center;width:100%;
257+
text-align:left;padding:9px 12px;border-radius:8px;font-size:13.5px;
258+
}
259+
.topbar .tab[aria-current="page"]{
260+
background:var(--accent-soft);border-color:transparent;
261+
box-shadow:inset 2px 0 0 var(--accent);
262+
}
263+
/* Sub-nav nests under its parent tab as an indented column. */
264+
.topbar .subtabs{
265+
flex-direction:column;gap:1px;
266+
background:transparent;border-bottom:none;
267+
padding:2px 0 8px 34px;overflow:visible;
268+
}
269+
.topbar .subtab{text-align:left;padding:6px 10px;font-size:12.5px;border-radius:6px}
270+
/* Tools pin to the sidebar foot. */
271+
.topbar .topbar-tools{
272+
margin-top:auto;flex-direction:column;align-items:stretch;gap:8px;
273+
padding-top:14px;border-top:1px solid var(--line);
274+
}
275+
.topbar .topbar-tools .btn{justify-content:center}
276+
.topbar .version-badge{justify-content:center;margin-right:0}
277+
.pages{margin-left:238px;max-width:1140px;padding:34px 40px 90px}
278+
}
279+
203280
.grid-3{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:18px}
204281

205282
/* ── Cards ───────────────────────────────────────────────────────── */
@@ -213,15 +290,15 @@ a:hover{text-decoration:underline}
213290
}
214291
.card h2{margin:0 0 6px;font-size:18px;font-weight:600;letter-spacing:-0.01em}
215292
.card > p{margin:0 0 16px;color:var(--ink-dim);font-size:14px}
216-
.card-stat{font-size:36px;font-weight:600;color:var(--ink);letter-spacing:-0.02em}
293+
.card-stat{font-family:var(--serif);font-size:42px;font-weight:400;color:var(--ink);letter-spacing:-0.01em;line-height:1.05}
217294
.card-sub{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-faint);margin-top:4px}
218295
/* "Stuck? Hand off to an AI" card — slightly tinted so it reads as an
219296
escape hatch rather than a primary control. */
220297
.ai-help-card{
221-
background:linear-gradient(135deg, rgba(122,255,240,0.04), rgba(212,175,98,0.04));
222-
border-color:rgba(122,255,240,0.32);
298+
background:linear-gradient(135deg, rgba(245,207,62,0.04), rgba(212,175,98,0.04));
299+
border-color:rgba(245,207,62,0.32);
223300
}
224-
.ai-help-card .card-eyebrow{color:#0a6e62}
301+
.ai-help-card .card-eyebrow{color:var(--accent)}
225302

226303
.row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
227304
.status{font-family:var(--mono);font-size:11px;letter-spacing:.04em;color:var(--ink-faint)}
@@ -247,8 +324,8 @@ textarea{display:block;width:100%;min-height:140px;font-family:var(--mono);font-
247324
}
248325
.btn:hover{transform:translateY(-1px);border-color:var(--accent)}
249326
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
250-
.btn-primary{background:var(--accent);color:#06080d;border-color:var(--accent)}
251-
.btn-primary:hover{background:#9affec}
327+
.btn-primary{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
328+
.btn-primary:hover{background:color-mix(in srgb, var(--accent) 85%, #fff)}
252329
.btn-ghost{background:transparent}
253330
.btn-sm{padding:6px 12px;font-size:12px}
254331
.btn-danger{color:var(--bad);border-color:rgba(255,138,161,0.4)}
@@ -415,7 +492,7 @@ textarea{display:block;width:100%;min-height:140px;font-family:var(--mono);font-
415492
cursor:pointer;
416493
}
417494
.cover-layer:hover{background:var(--bg-card)}
418-
.cover-layer.selected{background:rgba(58,167,255,0.15);border:1px solid rgba(58,167,255,0.4)}
495+
.cover-layer.selected{background:rgba(245,207,62,0.15);border:1px solid rgba(245,207,62,0.4)}
419496
.cover-layer-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
420497
.cover-layer-actions{display:flex;gap:2px}
421498
.cover-add-row{display:flex;gap:4px;margin-top:8px;flex-wrap:wrap}
@@ -654,13 +731,13 @@ em.optional{font-style:normal;color:var(--ink-faint);font-size:11px;letter-spaci
654731
.cal-slot{
655732
display:flex;gap:6px;align-items:flex-start;
656733
padding:6px 8px;border-radius:7px;
657-
background:rgba(122,255,240,0.05);
658-
border:1px solid rgba(122,255,240,0.12);
734+
background:rgba(245,207,62,0.05);
735+
border:1px solid rgba(245,207,62,0.12);
659736
font-size:12px;line-height:1.35;color:var(--ink);
660737
cursor:pointer;transition:background .15s,border-color .15s;
661738
text-align:left;
662739
}
663-
.cal-slot:hover{background:rgba(122,255,240,0.1);border-color:rgba(122,255,240,0.3)}
740+
.cal-slot:hover{background:rgba(245,207,62,0.1);border-color:rgba(245,207,62,0.3)}
664741
.cal-slot-icon{flex-shrink:0;font-size:11px;line-height:1.4;opacity:0.85}
665742
.cal-slot-title{
666743
flex:1;min-width:0;
@@ -729,7 +806,7 @@ em.optional{font-style:normal;color:var(--ink-faint);font-size:11px;letter-spaci
729806
.wiz{
730807
position:fixed;inset:0;z-index:100;
731808
background:
732-
radial-gradient(1200px 600px at 50% -10%, rgba(122,255,240,0.10), transparent 65%),
809+
radial-gradient(1200px 600px at 50% -10%, rgba(245,207,62,0.10), transparent 65%),
733810
radial-gradient(800px 800px at 110% 110%, rgba(245,207,62,0.06), transparent 60%),
734811
var(--bg);
735812
display:flex;align-items:flex-start;justify-content:center;
@@ -765,7 +842,7 @@ em.optional{font-style:normal;color:var(--ink-faint);font-size:11px;letter-spaci
765842
}
766843
.wiz-dot{
767844
width:8px;height:8px;border-radius:50%;background:var(--accent);
768-
box-shadow:0 0 0 4px rgba(122,255,240,0.15);
845+
box-shadow:0 0 0 4px rgba(245,207,62,0.15);
769846
}
770847
.wiz-brand-text{color:var(--ink)}
771848

@@ -782,7 +859,7 @@ em.optional{font-style:normal;color:var(--ink-faint);font-size:11px;letter-spaci
782859
}
783860
.wiz-step b{font-weight:500;font-size:10px;letter-spacing:.1em}
784861
.wiz-step.is-current{
785-
color:var(--accent);background:var(--accent-soft);border-color:rgba(122,255,240,0.25);
862+
color:var(--accent);background:var(--accent-soft);border-color:rgba(245,207,62,0.25);
786863
}
787864
.wiz-step.is-done{color:var(--good)}
788865
.wiz-step.is-done::before{content:"✓ ";font-weight:700}
@@ -972,7 +1049,7 @@ em.optional{font-style:normal;color:var(--ink-faint);font-size:11px;letter-spaci
9721049
}
9731050
.wiz-tick-big{
9741051
font-size:32px;color:var(--accent);
975-
background:rgba(122,255,240,0.1);border-color:rgba(122,255,240,0.4);
1052+
background:rgba(245,207,62,0.1);border-color:rgba(245,207,62,0.4);
9761053
}
9771054
@keyframes wiz-pop{0%{transform:scale(0.6);opacity:0}100%{transform:scale(1);opacity:1}}
9781055
.wiz-pill{
@@ -1075,9 +1152,10 @@ em.optional{font-style:normal;color:var(--ink-faint);font-size:11px;letter-spaci
10751152
.upd-commit-list .upd-author { font-size:11px; color:var(--ink-faint); flex-shrink:0; }
10761153
.upd-commit-list a { color:var(--accent); }
10771154

1155+
.tab-badge[hidden] { display:none; }
10781156
.tab-badge {
10791157
display:inline-block; margin-left:6px; min-width:18px; height:18px; padding:0 5px;
1080-
border-radius:9px; background:var(--accent); color:#06080d;
1158+
border-radius:9px; background:var(--accent); color:var(--accent-ink);
10811159
font-family:var(--mono); font-size:10px; font-weight:600; line-height:18px;
10821160
text-align:center; vertical-align:middle;
10831161
}

public/admin.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
<meta name="viewport" content="width=device-width,initial-scale=1" />
66
<meta name="robots" content="noindex,nofollow" />
77
<title>Admin · pages-seo</title>
8-
<link rel="preconnect" href="https://fonts.googleapis.com" />
9-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap" />
8+
<link rel="preload" href="/_fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin />
9+
<link rel="preload" href="/_fonts/inter-500.woff2" as="font" type="font/woff2" crossorigin />
10+
<link rel="preload" href="/_fonts/jetbrains-mono-400.woff2" as="font" type="font/woff2" crossorigin />
1111
<link rel="stylesheet" href="/admin.css" />
1212
<link rel="stylesheet" href="/cover-editor.css" />
1313
<script>

0 commit comments

Comments
 (0)