news behavior split: /news leads with Latest, Highlights via ?view=highlights

Base-aware so the frozen `/` is untouched (it's still the live, indexed site
until cutover); the new behavior applies only to /news. At cutover `/` becomes
the hub and only /news's behavior remains.

- defaultView(base): /news bare → Latest (the live firehose); `/` bare → Highlights.
- Brief is canonically /news?view=highlights, with ?view=today kept as an alias.
- Latest is pure chronological on /news — stop passing `home` into it (geo scope
  belongs to Highlights). The Closer-to-Home card/dial is hidden on /news Latest;
  Highlights keeps the scope dial. `/`'s Latest keeps geo (frozen).
- Back fixed: on /news it shows only for genuine drill-ins (tag/source/search),
  not the top-level lanes (Latest/Highlights/Following); `/` keeps its old rule.
- goBack's app-safe fallback lands on the base's default view.

feednav.js gains defaultView + def-aware parse/build; 36 frontend tests (9 new),
build clean. /news stays noindex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-28 16:21:42 -04:00
parent 39b38f0cf1
commit 54761f5083
3 changed files with 78 additions and 42 deletions
+39 -21
View File
@@ -1,12 +1,12 @@
import { describe, it, expect } from 'vitest';
import { feedBase, parseView, viewUrl } from './feednav.js';
import { feedBase, defaultView, parseView, viewUrl } from './feednav.js';
const view = (path) => parseView(new URL('https://x' + path));
const view = (path, def) => parseView(new URL('https://x' + path), def);
describe('feedBase', () => {
it('is /news only under the /news path; everything else is /', () => {
expect(feedBase('/news')).toBe('/news');
expect(feedBase('/news?view=latest')).toBe('/news');
expect(feedBase('/news?view=highlights')).toBe('/news');
expect(feedBase('/')).toBe('/');
expect(feedBase('/?view=latest')).toBe('/');
expect(feedBase('')).toBe('/');
@@ -14,37 +14,55 @@ describe('feedBase', () => {
});
});
describe('defaultView', () => {
it('/news leads with Latest; / (frozen) leads with Highlights', () => {
expect(defaultView('/news')).toBe('latest');
expect(defaultView('/')).toBe('today');
});
});
describe('parseView', () => {
it('derives the view from query params, path-agnostic', () => {
expect(view('/')).toBe('today');
expect(view('/news')).toBe('today');
expect(view('/?view=latest')).toBe('latest');
expect(view('/news?view=following')).toBe('following');
it('bare path resolves to the base default', () => {
expect(view('/')).toBe('today'); // / default (frozen)
expect(view('/news', 'latest')).toBe('latest'); // /news default = Latest
});
it('Brief is reachable via ?view=highlights, with ?view=today as an alias', () => {
expect(view('/news?view=highlights', 'latest')).toBe('today');
expect(view('/news?view=today', 'latest')).toBe('today'); // alias kept
expect(view('/?view=today')).toBe('today');
});
it('reads the standard views regardless of base', () => {
expect(view('/news?view=latest', 'latest')).toBe('latest');
expect(view('/?view=following')).toBe('following');
expect(view('/?tag=clean-energy')).toBe('tag:clean-energy');
expect(view('/?source=7')).toBe('source:7');
expect(view('/?q=solar')).toBe('search');
});
it('prioritizes search > source > tag > view', () => {
it('prioritizes search > source > tag > view, ignores whitespace q', () => {
expect(view('/?q=a&source=7&tag=b&view=latest')).toBe('search');
expect(view('/?source=7&tag=b&view=latest')).toBe('source:7');
expect(view('/?tag=b&view=latest')).toBe('tag:b');
expect(view('/?q=%20')).toBe('today'); // whitespace-only q is not a search
expect(view('/?q=%20')).toBe('today');
});
});
describe('viewUrl', () => {
it('builds base-relative links that parseView reads back identically', () => {
for (const base of ['/', '/news']) {
expect(viewUrl(base, 'today')).toBe(base);
expect(viewUrl(base, 'latest')).toBe(base + '?view=latest');
expect(viewUrl(base, 'tag:clean-energy')).toBe(base + '?tag=clean-energy');
expect(viewUrl(base, 'source:7')).toBe(base + '?source=7');
// round-trip: the generated URL resolves back to the same view key
expect(view(viewUrl(base, 'latest').replace(base, '/'))).toBe('latest');
}
it('the base default is the bare path; the Brief is ?view=highlights', () => {
// /news: Latest is the default (bare), Highlights is explicit
expect(viewUrl('/news', 'latest', 'latest')).toBe('/news');
expect(viewUrl('/news', 'today', 'latest')).toBe('/news?view=highlights');
expect(viewUrl('/news', 'following', 'latest')).toBe('/news?view=following');
// / (frozen): Highlights is the default (bare), Latest is explicit
expect(viewUrl('/', 'today', 'today')).toBe('/');
expect(viewUrl('/', 'latest', 'today')).toBe('/?view=latest');
});
it('round-trips: a generated link parses back to the same view', () => {
expect(view(viewUrl('/news', 'today', 'latest'), 'latest')).toBe('today');
expect(view(viewUrl('/news', 'latest', 'latest'), 'latest')).toBe('latest');
expect(view(viewUrl('/', 'latest', 'today').replace('/', '/'), 'today')).toBe('latest');
});
it('encodes tag/source/view values', () => {
expect(viewUrl('/news', 'tag:good news')).toBe('/news?tag=good%20news');
expect(viewUrl('/', 'view=a&b')).toBe('/?view=view%3Da%26b');
expect(viewUrl('/news', 'tag:good news', 'latest')).toBe('/news?tag=good%20news');
expect(viewUrl('/', 'source:42', 'today')).toBe('/?source=42');
});
});