Skip to content

Commit 5aaabc6

Browse files
ten9876claude
andcommitted
Add a Blog page with card index and full-post view
New /blog.html: a card grid (hero image, date, title, 3-4 line summary) that swaps to the full post when a card is clicked. Routing is driven by location.hash in assets/js/blog.js, so post URLs are shareable and Back/Forward work; an unknown slug falls back to the index. Posts live inline in blog.html as hidden <article>s — adding one is a card plus an article sharing a slug, no build step and no fetch. Nav gets a Blog link on both pages. Since .nav-links is display:none under 940px and there's no hamburger to replace it, Blog also appears in .nav-cta at mobile widths so it stays reachable on phones. Note for future styling: .blog-post deliberately sets no `display`, as a class rule out-specifies the UA's [hidden] { display: none } and every post would then render stacked on the index. Seed content is three placeholder posts drawn from existing site copy — replace before this is announced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a120915 commit 5aaabc6

6 files changed

Lines changed: 641 additions & 2 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
if: ${{ env.CF_TOKEN != '' }}
3535
run: |
3636
mkdir -p _site
37-
cp -R index.html 404.html styles.css assets _headers _site/
37+
cp -R index.html blog.html 404.html styles.css assets _headers _site/
3838
[ -f _redirects ] && cp _redirects _site/ || true
3939
echo "Staged:" && find _site -maxdepth 2 -type f | sort
4040

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,35 @@ feature-forward, using AetherSDR's own palette (deep navy + electric cyan→teal
1111

1212
```
1313
index.html The landing page (single page, no build step required)
14+
blog.html The blog — card index + every post, routed client-side
1415
styles.css All styling (design tokens at the top)
1516
assets/img/ Optimized screenshots, logo, and 3D-spectrum visuals
17+
assets/js/blog.js Blog card-index <-> post routing
1618
serve.py Tiny local static server (python3 serve.py → :4321)
1719
scripts/build.py Bundles everything into dist/index.html (self-contained)
1820
```
1921

22+
## Adding a blog post
23+
24+
Everything lives in `blog.html` — no build step, no separate post files. A post
25+
is two blocks that share a slug:
26+
27+
1. **A card** in the `.blog-grid`, linking to `#your-slug`. The description is
28+
clamped to four lines, so write 3–4 lines and don't worry about matching
29+
neighbouring card heights.
30+
2. **An `<article class="blog-post" data-post="your-slug" ... hidden>`** further
31+
down, holding the hero image and full body. Keep the `hidden` attribute — it
32+
is what keeps the post off the index until it's routed to.
33+
34+
`assets/js/blog.js` matches the two by slug: clicking a card sets the hash, which
35+
swaps the grid out for that post. Post URLs (`/blog.html#your-slug`) are
36+
shareable, and Back/Forward work. Body copy goes inside `.blog-body`, which
37+
styles headings, lists, links, quotes, and code blocks for you.
38+
39+
> Note: don't add a `display` value to `.blog-post` in CSS — a class rule
40+
> out-specifies the browser's `[hidden] { display: none }` and every post would
41+
> render on the index at once.
42+
2043
## Local preview
2144

2245
```bash

assets/js/blog.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/* AetherSDR blog — card index <-> full post routing.
2+
*
3+
* The index grid and every post live in blog.html; posts start `hidden`.
4+
* Clicking a card routes to #<slug>, which swaps the grid out for the
5+
* matching <article data-post="<slug>">. Routing is driven entirely by
6+
* location.hash, so a post URL is shareable and Back/Forward work.
7+
*
8+
* No JS -> the <noscript> rule in blog.html reveals every post stacked
9+
* under the index, so the content is still readable.
10+
*/
11+
(function () {
12+
var index = document.getElementById('blog-index');
13+
var posts = document.querySelectorAll('.blog-post[data-post]');
14+
var baseTitle = 'AetherSDR';
15+
if (!index || !posts.length) return;
16+
17+
// Slug of the post we're leaving, so we can restore focus to its card.
18+
var lastSlug = '';
19+
20+
// Build slug -> article map once.
21+
var bySlug = {};
22+
for (var i = 0; i < posts.length; i++) {
23+
bySlug[posts[i].getAttribute('data-post')] = posts[i];
24+
}
25+
26+
function currentSlug() {
27+
// decodeURIComponent guards against slugs that arrive percent-encoded.
28+
var raw = (location.hash || '').replace(/^#/, '');
29+
try { raw = decodeURIComponent(raw); } catch (e) {}
30+
return raw;
31+
}
32+
33+
// Idempotent: safe to call from hashchange and popstate both.
34+
function route(opts) {
35+
var slug = currentSlug();
36+
var post = Object.prototype.hasOwnProperty.call(bySlug, slug) ? bySlug[slug] : null;
37+
38+
for (var i = 0; i < posts.length; i++) {
39+
posts[i].hidden = posts[i] !== post;
40+
}
41+
index.hidden = !!post;
42+
document.body.classList.toggle('is-reading', !!post);
43+
44+
if (post) {
45+
var h1 = post.querySelector('h1');
46+
document.title = (h1 ? h1.textContent.trim() + ' — ' : '') + baseTitle + ' Blog';
47+
// Move focus to the article so keyboard and screen-reader users land
48+
// on the post rather than back at the top of the document.
49+
if (opts && opts.focus) post.focus({ preventScroll: true });
50+
} else {
51+
document.title = 'Blog — ' + baseTitle;
52+
// Returning to the index: put focus back on the card we came from,
53+
// otherwise it would be stranded on the article we just hid.
54+
if (lastSlug) {
55+
var card = index.querySelector('.blog-card[href="#' + lastSlug + '"]');
56+
if (card) card.focus({ preventScroll: true });
57+
}
58+
}
59+
lastSlug = post ? slug : '';
60+
61+
if (!opts || opts.scroll !== false) scrollToContent();
62+
}
63+
64+
// Land just below the sticky nav rather than at the very top, and skip the
65+
// page's smooth-scroll so view swaps feel instant instead of animated.
66+
function scrollToContent() {
67+
var behavior = 'auto';
68+
var root = document.documentElement;
69+
var prev = root.style.scrollBehavior;
70+
root.style.scrollBehavior = behavior;
71+
window.scrollTo(0, 0);
72+
// Restore on the next frame so in-page anchor links keep smooth scrolling.
73+
requestAnimationFrame(function () { root.style.scrollBehavior = prev; });
74+
}
75+
76+
// "Back to blog" — clear the hash without leaving a bare "#" in the URL.
77+
document.addEventListener('click', function (e) {
78+
var back = e.target.closest ? e.target.closest('[data-blog-back]') : null;
79+
if (!back) return;
80+
e.preventDefault();
81+
if (history.pushState) {
82+
history.pushState(null, '', location.pathname + location.search);
83+
route({ focus: false });
84+
} else {
85+
location.hash = '';
86+
}
87+
});
88+
89+
window.addEventListener('hashchange', function () { route({ focus: true }); });
90+
window.addEventListener('popstate', function () { route({ focus: false }); });
91+
92+
// Initial render: don't steal focus or scroll on a plain page load, but do
93+
// honour a deep link like blog.html#signal-chain.
94+
route({ focus: false, scroll: !!currentSlug() });
95+
})();

0 commit comments

Comments
 (0)