- NewsletterIssue gains an optional 'image' (per-issue cover); IssueArticle renders
it above the header and buildIssueArticle uses it as the NIP-23 'image' tag
(falling back to the spot-of-the-week image).
- NEWSLETTER_ISSUES now leads with #006 (2026-09-27): the Nostr-Twitter-Clone
rant, the Directions fixes (route never drew + identical per-mode numbers),
follow mode, choose-your-start + offline saved routes, Sasquatch Pro
(sats-paid, streamed, personal tools, one signature per session), the
Marketplace hub + 1-10 deck ratings, Mints (directory + the native JSON-error
fix), Amber scoping, the DM inbox-relay bug, and the polish list.
- Cover: https://skate-spots.org/images/drop-006-cover.png (hosted in
public/images, added in e075ee8).
Verified: the archive renders the cover + #006 copy, and the 'Drop #006 is out'
pill shows on other pages for 7 days. tsc/eslint clean, 101 tests, smoke PASS.
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import { NEWSLETTER_KIND, NEWSLETTER_TAG } from '@/lib/types';
|
|
|
|
/**
|
|
* The subset of a static newsletter issue we need to turn it into a NIP-23
|
|
* long-form article (kind 30023). Kept structural (not importing the page) to
|
|
* avoid a page → lib → page import cycle.
|
|
*/
|
|
export interface StaticIssueLike {
|
|
number: number;
|
|
date: string; // ISO "YYYY-MM-DD"
|
|
vibe: string;
|
|
/** Optional cover image URL (used as the NIP-23 `image` tag). */
|
|
image?: string;
|
|
updates: { title: string; tag: string; body: string }[];
|
|
spotOfTheWeek?: {
|
|
title: string;
|
|
location: string;
|
|
why: string;
|
|
mapperName?: string;
|
|
image?: string;
|
|
href?: string;
|
|
};
|
|
communityNote: string;
|
|
}
|
|
|
|
/** Stable addressable identifier for a backfilled archive issue. */
|
|
export function issueSlug(number: number): string {
|
|
return `issue-${number}`;
|
|
}
|
|
|
|
/** Human title for an issue, e.g. "SkateSpots Drop #005". */
|
|
export function issueTitle(number: number): string {
|
|
return `SkateSpots Drop #${String(number).padStart(3, '0')}`;
|
|
}
|
|
|
|
/** Render a static issue's structured sections into a Markdown article body. */
|
|
export function issueToMarkdown(issue: StaticIssueLike): string {
|
|
const lines: string[] = [];
|
|
|
|
lines.push(`_${issue.vibe}_`);
|
|
lines.push('');
|
|
|
|
lines.push('## What shipped');
|
|
lines.push('');
|
|
for (const u of issue.updates) {
|
|
lines.push(`### ${u.title}`);
|
|
lines.push(`**${u.tag}**`);
|
|
lines.push('');
|
|
lines.push(u.body);
|
|
lines.push('');
|
|
}
|
|
|
|
if (issue.spotOfTheWeek) {
|
|
const s = issue.spotOfTheWeek;
|
|
lines.push('## Spot of the Week');
|
|
lines.push('');
|
|
lines.push(`**${s.title}** — ${s.location}`);
|
|
lines.push('');
|
|
lines.push(s.why);
|
|
if (s.mapperName) {
|
|
lines.push('');
|
|
lines.push(`_Mapped by ${s.mapperName}_`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
lines.push('## Community');
|
|
lines.push('');
|
|
lines.push(issue.communityNote);
|
|
lines.push('');
|
|
lines.push('---');
|
|
lines.push('');
|
|
lines.push('Read every drop at https://skate-spots.org/#/newsletter');
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/** A one-line summary for the article's NIP-23 `summary` tag. */
|
|
export function issueSummary(issue: StaticIssueLike): string {
|
|
return issue.vibe.length > 180 ? `${issue.vibe.slice(0, 177)}…` : issue.vibe;
|
|
}
|
|
|
|
/** Build the full (unsigned) kind-30023 event template for a static issue. */
|
|
export function buildIssueArticle(issue: StaticIssueLike) {
|
|
const publishedAt = Math.floor(new Date(issue.date + 'T12:00:00Z').getTime() / 1000);
|
|
// A per-issue cover wins; fall back to the spot-of-the-week image.
|
|
const image = issue.image ?? issue.spotOfTheWeek?.image;
|
|
return {
|
|
kind: NEWSLETTER_KIND,
|
|
content: issueToMarkdown(issue),
|
|
tags: [
|
|
['d', issueSlug(issue.number)],
|
|
['title', issueTitle(issue.number)],
|
|
['summary', issueSummary(issue)],
|
|
...(image ? [['image', image]] : []),
|
|
['published_at', String(Number.isFinite(publishedAt) ? publishedAt : Math.floor(Date.now() / 1000))],
|
|
['t', NEWSLETTER_TAG],
|
|
['t', 'skatespots'],
|
|
['t', 'newsletter'],
|
|
['t', 'skateboarding'],
|
|
['alt', `SkateSpots newsletter issue: ${issueTitle(issue.number)}`],
|
|
] as string[][],
|
|
};
|
|
}
|