diff --git a/README.md b/README.md
index c44110b..d0d982c 100644
--- a/README.md
+++ b/README.md
@@ -258,7 +258,8 @@ curl -X POST https://rssamplifier.com/api/discover \
Every author page carries `` to `/authors/{slug}/openprofile.md`, the
person as one portable file: identity block, Accounts (their `rel="me"` links), Topics (their feeds'
subjects), and a Broadcast section ([OpenBroadcast](https://logicsrc.com/openbroadcast)) for every
-podcast or show they publish, with only the facts their own feed states. The file never fills in
+podcast or show they publish, with only the facts their own feed states (not `Since`: a feed is a
+window on a show, and the oldest episode the directory holds is not when the show began). The file never fills in
what the person did not say: no `Seeking`, `Pays`, `Charges`, no Guest section, and no email even
when the API republishes one.
diff --git a/apps/web/src/lib/authorProfile.js b/apps/web/src/lib/authorProfile.js
index e1e5791..c8d22c0 100644
--- a/apps/web/src/lib/authorProfile.js
+++ b/apps/web/src/lib/authorProfile.js
@@ -25,10 +25,9 @@ export async function loadAuthorProfile(slug) {
const feeds = person.feeds ?? [];
const feedIds = feeds.map((f) => String(f.id));
- const [profile, topicsByFeed, firstByFeed] = await Promise.all([
+ const [profile, topicsByFeed] = await Promise.all([
profiles.profileForAuthor(client, String(person.id)),
profiles.keywordsForFeeds(client, feedIds),
- profiles.firstPublishedAt(client, feedIds),
]);
const base = siteUrl();
@@ -36,7 +35,6 @@ export async function loadAuthorProfile(slug) {
person,
feeds,
topicsByFeed,
- firstByFeed,
base,
overrides: profile?.overrides ?? null,
});
diff --git a/apps/web/src/lib/openprofile.js b/apps/web/src/lib/openprofile.js
index 96d6bbb..054dd94 100644
--- a/apps/web/src/lib/openprofile.js
+++ b/apps/web/src/lib/openprofile.js
@@ -20,6 +20,12 @@ import {
* Guest section: those are the person's to write, through the overlay, and
* absent means unstated.
*
+ * `Since` is deliberately not written either. A feed is a window on a show,
+ * the last fifty or hundred episodes, and the oldest item the directory holds
+ * says when that window starts, not when the show did. A podcast running since
+ * 2019 would read "Since: 2026-06", which is exactly the false claim the spec
+ * forbids; the owner can write the year themselves.
+ *
* Email is deliberately not in the generated file even when the table has it.
* The API republishes an address the author published; the profile is a
* document other directories will copy, and the owner adds `Email` to it
@@ -68,18 +74,6 @@ function label(network) {
return names[network] ?? network.charAt(0).toUpperCase() + network.slice(1);
}
-/**
- * `YYYY-MM` from an ISO stamp, or null when it does not parse.
- *
- * @param {string|null|undefined} iso
- * @returns {string|null}
- */
-export function yearMonth(iso) {
- if (!iso) return null;
- const m = /^(\d{4})-(\d{2})/.exec(String(iso));
- return m ? `${m[1]}-${m[2]}` : null;
-}
-
/**
* The generated document, before the owner's corrections.
*
@@ -89,12 +83,11 @@ export function yearMonth(iso) {
* feeds: Array<{ id: string, slug: string, title: string, kind?: string|null, role?: string|null,
* feed_url?: string|null, site_url?: string|null, language?: string|null }>,
* topicsByFeed?: Map,
- * firstByFeed?: Map,
* base: string,
* }} input
* @returns {import('@profullstack/openprofile').OpenProfileDoc}
*/
-export function generateAuthorProfile({ person, feeds, topicsByFeed, firstByFeed, base }) {
+export function generateAuthorProfile({ person, feeds, topicsByFeed, base }) {
const links = (person.links ?? []).filter((l) => l.network !== 'email');
const handleLink = HANDLE_NETWORKS.map((n) => links.find((l) => l.network === n && l.handle)).find(
Boolean,
@@ -136,7 +129,6 @@ export function generateAuthorProfile({ person, feeds, topicsByFeed, firstByFeed
Show: f.title,
Kind: BROADCAST_KINDS.get(String(f.kind)) ?? null,
Language: f.language ?? null,
- Since: yearMonth(firstByFeed?.get(String(f.id))),
Feed: f.feed_url ?? null,
Listen: f.site_url ?? `${base}/${encodeURIComponent(String(f.slug))}`,
Topics: (topicsByFeed?.get(String(f.id)) ?? []).slice(0, 8).join(', ') || null,
@@ -150,7 +142,6 @@ export function generateAuthorProfile({ person, feeds, topicsByFeed, firstByFeed
Show: shows[0].title,
Kind: BROADCAST_KINDS.get(String(shows[0].kind)) ?? null,
Language: shows[0].language ?? null,
- Since: yearMonth(firstByFeed?.get(String(shows[0].id))),
Feed: shows[0].feed_url ?? null,
Listen: shows[0].site_url ?? `${base}/${encodeURIComponent(String(shows[0].slug))}`,
Topics: (topicsByFeed?.get(String(shows[0].id)) ?? []).slice(0, 8).join(', ') || null,
diff --git a/apps/web/test/openprofile.test.js b/apps/web/test/openprofile.test.js
index d38e09d..26bed62 100644
--- a/apps/web/test/openprofile.test.js
+++ b/apps/web/test/openprofile.test.js
@@ -9,7 +9,6 @@ import {
overridesFromBody,
overridesFromForm,
profileUrl,
- yearMonth,
} from '../src/lib/openprofile.js';
import { claimVerdict, isOwner, linksBack } from '../src/lib/profileAuth.js';
import { principalFromToken } from '../src/lib/openaccess.js';
@@ -57,10 +56,9 @@ const TOPICS = new Map([
['f1', ['history', 'mathematics']],
['f2', ['mathematics', 'writing']],
]);
-const FIRST = new Map([['f1', '2024-11-15T00:00:00.000Z']]);
test('the generated file says what the author published, and nothing they did not', () => {
- const doc = generateAuthorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, firstByFeed: FIRST, base: BASE });
+ const doc = generateAuthorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, base: BASE });
assert.equal(doc.name, 'Ada Lovelace');
const identity = Object.fromEntries(doc.identity.map((e) => [e.key, e.value]));
@@ -84,12 +82,12 @@ test('the generated file says what the author published, and nothing they did no
Show: 'The Analytical Engine',
Kind: 'podcast',
Language: 'en',
- Since: '2024-11',
Feed: 'https://ada.example/podcast/feed.xml',
Listen: 'https://ada.example/podcast',
Topics: 'history, mathematics',
});
assert.equal('Seeking' in shows[0], false);
+ assert.equal('Since' in shows[0], false, 'the feed window is not when the show started');
assert.equal(doc.sections.some((s) => s.name === 'guest'), false, 'no Guest section unless the person wrote one');
const links = doc.sections.find((s) => s.name === 'links');
@@ -98,7 +96,7 @@ test('the generated file says what the author published, and nothing they did no
test('two owned shows become ### groups in one Broadcast section', () => {
const feeds = [FEEDS[0], { ...FEEDS[2], role: 'owner' }];
- const doc = generateAuthorProfile({ person: ADA, feeds, topicsByFeed: TOPICS, firstByFeed: FIRST, base: BASE });
+ const doc = generateAuthorProfile({ person: ADA, feeds, topicsByFeed: TOPICS, base: BASE });
const shows = broadcasts(doc);
assert.deepEqual(shows.map((s) => s.Show), ['The Analytical Engine', 'Somebody Elses Show']);
assert.equal(shows[1].Listen, `${BASE}/other-show`, 'a show with no site listens on its directory page');
@@ -110,7 +108,7 @@ test('the overlay wins per part, the rest stays generated, and the file round-tr
identity: { Email: 'ada@example.com', Location: 'London', Avatar: null },
sections: { guest: '- **Available**: yes\n- **Expertise**: early computing', links: 'none' },
};
- const { markdown, doc } = authorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, firstByFeed: FIRST, base: BASE, overrides });
+ const { markdown, doc } = authorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, base: BASE, overrides });
assert.equal(doc.headline, 'Countess, programmer.');
const identity = Object.fromEntries(doc.identity.map((e) => [e.key, e.value]));
assert.equal(identity.Email, 'ada@example.com', 'the owner chose to publish it');
@@ -125,7 +123,7 @@ test('the overlay wins per part, the rest stays generated, and the file round-tr
});
test('a PUT body as Markdown becomes the whole overlay; as JSON it is a patch', () => {
- const generated = generateAuthorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, firstByFeed: FIRST, base: BASE });
+ const generated = generateAuthorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, base: BASE });
const edited = '# Ada Lovelace\n\n- Kind: person\n- Web: https://ada.example\n\nCountess.\n\n## Guest\n\n- **Available**: yes\n';
const fromMd = overridesFromBody({ contentType: 'text/markdown; charset=utf-8', text: edited, existing: null, generated });
@@ -235,7 +233,5 @@ test('an OpenAccess token yields a principal with scopes and an email when the h
});
test('small helpers', () => {
- assert.equal(yearMonth('2024-11-15T00:00:00Z'), '2024-11');
- assert.equal(yearMonth(null), null);
assert.equal(profileUrl(BASE, 'ada lovelace'), `${BASE}/authors/ada%20lovelace/openprofile.md`);
});
diff --git a/packages/db/src/profiles.js b/packages/db/src/profiles.js
index 74a73c7..f33aa73 100644
--- a/packages/db/src/profiles.js
+++ b/packages/db/src/profiles.js
@@ -143,31 +143,6 @@ export async function profilesForUser(db, userId) {
return /** @type {any} */ (rows);
}
-/**
- * When each feed first published, as `feed_id -> ISO date`, for the `Since`
- * key of a Broadcast section. Feeds with no dated item are absent.
- *
- * @param {Client} db
- * @param {string[]} feedIds
- * @returns {Promise