Skip to content

Commit 22f9df2

Browse files
committed
feat: separate post prefix/suffix per network with tabbed preview
- ActivityPub and Bluesky get independent prefix/suffix fields - Bluesky fields fall back to AP values if left empty - Tabbed UI: switch between AP (500 char) and Bluesky (300 char) view - Live preview updates per tab with char counter - Budget indicator per network
1 parent 361e5c3 commit 22f9df2

4 files changed

Lines changed: 95 additions & 54 deletions

File tree

src/lib/server/bot-profile.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface BotProfile {
1919
fields: BotProfileField[];
2020
postPrefix: string;
2121
postSuffix: string;
22+
bskyPostPrefix: string;
23+
bskyPostSuffix: string;
2224
}
2325

2426
const DEFAULT_PROFILE: BotProfile = {
@@ -32,7 +34,9 @@ const DEFAULT_PROFILE: BotProfile = {
3234
{ name: 'Source', value: 'https://github.com/rmdes/newsdiff' }
3335
],
3436
postPrefix: '',
35-
postSuffix: ''
37+
postSuffix: '',
38+
bskyPostPrefix: '',
39+
bskyPostSuffix: ''
3640
};
3741

3842
export async function loadBotProfile(): Promise<BotProfile> {

src/lib/server/workers/syndicator.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ async function syndicate(job: Job<SyndicateJobData>) {
2929
if (!diff || diff.isBoring) return;
3030

3131
const botProfile = await loadBotProfile();
32-
const postPrefix = botProfile.postPrefix || '';
33-
const postSuffix = botProfile.postSuffix || '';
32+
const apPrefix = botProfile.postPrefix || '';
33+
const apSuffix = botProfile.postSuffix || '';
34+
const bskyPrefix = botProfile.bskyPostPrefix || apPrefix;
35+
const bskySuffix = botProfile.bskyPostSuffix || apSuffix;
3436

3537
const cardData = {
3638
feedName: diff.article.feed.name,
@@ -111,7 +113,7 @@ async function syndicate(job: Job<SyndicateJobData>) {
111113
articleTitle: cardData.articleTitle, feedName: diff.article.feed.name,
112114
titleChanged: diff.titleChanged, contentChanged: diff.contentChanged,
113115
charsAdded: diff.charsAdded, charsRemoved: diff.charsRemoved,
114-
diffPageUrl, archiveUrl, prefix: postPrefix, suffix: postSuffix
116+
diffPageUrl, archiveUrl, prefix: bskyPrefix, suffix: bskySuffix
115117
});
116118

117119
const embedType = (process.env.BLUESKY_EMBED_TYPE === 'card') ? 'external' as const : 'image' as const;
@@ -167,8 +169,8 @@ async function syndicate(job: Job<SyndicateJobData>) {
167169
imageUrl,
168170
diffPageUrl: `${origin}/diff/${diff.id}`,
169171
archiveUrl,
170-
prefix: postPrefix,
171-
suffix: postSuffix,
172+
prefix: apPrefix,
173+
suffix: apSuffix,
172174
replyToId: latest?.social_posts.postUri || undefined
173175
});
174176

src/routes/bot/profile/+page.server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,18 @@ export const actions = {
6969
if (displayName !== undefined) profile.displayName = displayName || profile.displayName;
7070
if (summary !== undefined) profile.summary = summary || profile.summary;
7171

72-
// Post template
72+
// Post template — ActivityPub
7373
const postPrefix = formData.get('postPrefix')?.toString().trim().slice(0, 200);
7474
const postSuffix = formData.get('postSuffix')?.toString().trim().slice(0, 500);
7575
if (postPrefix !== undefined) profile.postPrefix = postPrefix;
7676
if (postSuffix !== undefined) profile.postSuffix = postSuffix;
7777

78+
// Post template — Bluesky
79+
const bskyPostPrefix = formData.get('bskyPostPrefix')?.toString().trim().slice(0, 100);
80+
const bskyPostSuffix = formData.get('bskyPostSuffix')?.toString().trim().slice(0, 200);
81+
if (bskyPostPrefix !== undefined) profile.bskyPostPrefix = bskyPostPrefix;
82+
if (bskyPostSuffix !== undefined) profile.bskyPostSuffix = bskyPostSuffix;
83+
7884
// Handle avatar upload
7985
const avatar = formData.get('avatar') as File | null;
8086
if (avatar && avatar.size > 0) {

src/routes/bot/profile/+page.svelte

Lines changed: 76 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,34 @@
55
66
// Bluesky budget: 300 chars total. ~80 chars for change desc + stats + feed name + links
77
const BSKY_LIMIT = 300;
8-
const BSKY_OVERHEAD = 120; // approx: change desc, stats, feed name, URLs
9-
let prefixInput = $state(data.profile.postPrefix || '');
10-
let suffixInput = $state(data.profile.postSuffix || '');
11-
let bskyBudget = $derived(BSKY_LIMIT - BSKY_OVERHEAD - prefixInput.length - suffixInput.length);
8+
const AP_LIMIT = 500;
9+
const OVERHEAD = 120;
10+
11+
let apPrefixInput = $state(data.profile.postPrefix || '');
12+
let apSuffixInput = $state(data.profile.postSuffix || '');
13+
let bskyPrefixInput = $state(data.profile.bskyPostPrefix || '');
14+
let bskySuffixInput = $state(data.profile.bskyPostSuffix || '');
15+
16+
let previewTab = $state('ap');
1217
13-
// Live preview using a real diff
1418
const preview = data.previewData;
15-
let previewText = $derived(() => {
19+
20+
function buildPreview(prefix: string, suffix: string): string {
1621
if (!preview) return '';
1722
const changes = [
1823
preview.titleChanged ? 'Headline changed' : '',
1924
preview.contentChanged ? 'Content changed' : ''
2025
].filter(Boolean).join(' & ') || 'Article updated';
2126
const stats = `+${preview.charsAdded} / -${preview.charsRemoved} chars`;
22-
const prefix = prefixInput ? `${prefixInput} ` : '';
23-
const suffix = suffixInput ? `\n\n${suffixInput}` : '';
24-
return `${prefix}${changes} in "${preview.title}" (${preview.feedName})\n${stats}\n\nhttps://diff.example.com/diff/${preview.diffId}\nhttps://example.com/article${suffix}`;
25-
});
26-
let previewCharCount = $derived(previewText().length);
27+
const pfx = prefix ? `${prefix} ` : '';
28+
const sfx = suffix ? `\n\n${suffix}` : '';
29+
return `${pfx}${changes} in "${preview.title}" (${preview.feedName})\n${stats}\n\nhttps://diff.example.com/diff/${preview.diffId}\nhttps://example.com/article${sfx}`;
30+
}
31+
32+
let apPreview = $derived(buildPreview(apPrefixInput, apSuffixInput));
33+
let bskyPreview = $derived(buildPreview(bskyPrefixInput || apPrefixInput, bskySuffixInput || apSuffixInput));
34+
let apBudget = $derived(AP_LIMIT - apPreview.length);
35+
let bskyBudget = $derived(BSKY_LIMIT - bskyPreview.length);
2736
2837
let fields = $derived((() => {
2938
const f = [...(profile.fields || [])];
@@ -116,46 +125,61 @@
116125

117126
<section>
118127
<h2>Post template</h2>
119-
<p class="hint">Customize how syndicated posts are constructed on ActivityPub and Bluesky. The default format is: <code>{'{change}'} in "{'{title}'}" ({'{feed}'})</code></p>
120-
<div class="field">
121-
<label for="postPrefix">Prefix</label>
122-
<input type="text" id="postPrefix" name="postPrefix" bind:value={prefixInput} placeholder="e.g. 📝 Edit detected:" />
123-
<p class="hint">Added before the change description. Leave empty for default.</p>
124-
</div>
125-
<div class="field">
126-
<label for="postSuffix">Suffix</label>
127-
<input type="text" id="postSuffix" name="postSuffix" bind:value={suffixInput} placeholder="e.g. #newsdiff #transparency" />
128-
<p class="hint">Added at the end of every post. Useful for hashtags.</p>
129-
</div>
130-
<div class="budget" class:budget-warn={bskyBudget < 30} class:budget-over={bskyBudget < 0}>
131-
Bluesky budget: ~{bskyBudget} chars remaining for title
132-
{#if bskyBudget < 0}
133-
— title will be truncated
134-
{:else if bskyBudget < 30}
135-
— title may be truncated
136-
{/if}
128+
<p class="hint">Customize syndicated posts per network. Bluesky fields fall back to ActivityPub values if left empty.</p>
129+
130+
<div class="template-tabs">
131+
<button class:active={previewTab === 'ap'} onclick={() => previewTab = 'ap'}>
132+
<span class="tab-icon">🐘</span> ActivityPub
133+
</button>
134+
<button class:active={previewTab === 'bsky'} onclick={() => previewTab = 'bsky'}>
135+
<span class="tab-icon">🦋</span> Bluesky
136+
</button>
137137
</div>
138138

139-
{#if preview}
140-
<div class="preview-cards">
141-
<div class="preview-card preview-mastodon">
142-
<div class="preview-header">
143-
<span class="preview-icon">🐘</span>
144-
<span class="preview-label">ActivityPub preview</span>
145-
</div>
146-
<div class="preview-body">{previewText()}</div>
147-
<div class="preview-meta">{previewCharCount} chars (no limit)</div>
139+
{#if previewTab === 'ap'}
140+
<div class="template-fields">
141+
<div class="field">
142+
<label for="postPrefix">Prefix</label>
143+
<input type="text" id="postPrefix" name="postPrefix" bind:value={apPrefixInput} placeholder="e.g. 📝 Edit detected:" />
148144
</div>
149-
<div class="preview-card preview-bluesky">
150-
<div class="preview-header">
151-
<span class="preview-icon">🦋</span>
152-
<span class="preview-label">Bluesky preview</span>
153-
</div>
154-
<div class="preview-body" class:preview-truncated={previewCharCount > 300}>{previewText().slice(0, 300)}{#if previewCharCount > 300}...{/if}</div>
155-
<div class="preview-meta" class:budget-over={previewCharCount > 300}>{Math.min(previewCharCount, 300)}/300 chars</div>
145+
<div class="field">
146+
<label for="postSuffix">Suffix</label>
147+
<input type="text" id="postSuffix" name="postSuffix" bind:value={apSuffixInput} placeholder="e.g. #newsdiff #transparency" />
148+
</div>
149+
<div class="budget" class:budget-warn={apBudget < 50} class:budget-over={apBudget < 0}>
150+
{apPreview.length}/500 chars
151+
</div>
152+
</div>
153+
{:else}
154+
<div class="template-fields">
155+
<div class="field">
156+
<label for="bskyPostPrefix">Prefix</label>
157+
<input type="text" id="bskyPostPrefix" name="bskyPostPrefix" bind:value={bskyPrefixInput} placeholder={apPrefixInput || 'Same as ActivityPub'} />
158+
</div>
159+
<div class="field">
160+
<label for="bskyPostSuffix">Suffix</label>
161+
<input type="text" id="bskyPostSuffix" name="bskyPostSuffix" bind:value={bskySuffixInput} placeholder={apSuffixInput || 'Same as ActivityPub'} />
162+
</div>
163+
<div class="budget" class:budget-warn={bskyBudget < 30} class:budget-over={bskyBudget < 0}>
164+
{bskyPreview.length}/300 chars
165+
{#if bskyBudget < 0} — title will be truncated{/if}
156166
</div>
157167
</div>
158168
{/if}
169+
170+
{#if preview}
171+
<div class="preview-card" class:preview-mastodon={previewTab === 'ap'} class:preview-bluesky={previewTab === 'bsky'}>
172+
<div class="preview-header">
173+
<span class="preview-icon">{previewTab === 'ap' ? '🐘' : '🦋'}</span>
174+
<span class="preview-label">{previewTab === 'ap' ? 'ActivityPub' : 'Bluesky'} preview</span>
175+
</div>
176+
{#if previewTab === 'ap'}
177+
<div class="preview-body">{apPreview}</div>
178+
{:else}
179+
<div class="preview-body" class:preview-truncated={bskyPreview.length > 300}>{bskyPreview.slice(0, 300)}{#if bskyPreview.length > 300}...{/if}</div>
180+
{/if}
181+
</div>
182+
{/if}
159183
</section>
160184

161185
<div class="actions">
@@ -226,8 +250,13 @@
226250
.budget-warn { color: #92400e; background: #fef3c7; }
227251
.budget-over { color: var(--color-del-text); background: var(--color-del-bg); }
228252
229-
.preview-cards { display: flex; gap: 1rem; margin-top: 1rem; flex-wrap: wrap; }
230-
.preview-card { flex: 1; min-width: 260px; border: 1px solid var(--color-border); border-radius: 0.5rem; overflow: hidden; background: white; }
253+
.template-tabs { display: flex; gap: 0; margin-bottom: 1rem; border-bottom: 2px solid var(--color-border); }
254+
.template-tabs button { padding: 0.5rem 1rem; border: none; background: none; cursor: pointer; font-size: 0.85rem; color: var(--color-muted); border-bottom: 2px solid transparent; margin-bottom: -2px; display: flex; align-items: center; gap: 0.3rem; }
255+
.template-tabs button.active { color: var(--color-text); border-bottom-color: var(--color-primary); font-weight: 600; }
256+
.template-tabs button:hover { color: var(--color-text); }
257+
.template-fields { margin-bottom: 0.75rem; }
258+
259+
.preview-card { border: 1px solid var(--color-border); border-radius: 0.5rem; overflow: hidden; background: white; margin-top: 1rem; }
231260
.preview-header { display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.75rem; background: #f8f8f8; border-bottom: 1px solid var(--color-border); font-size: 0.8rem; font-weight: 600; color: var(--color-muted); }
232261
.preview-icon { font-size: 1rem; }
233262
.preview-body { padding: 0.75rem; font-size: 0.85rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; color: var(--color-text); }

0 commit comments

Comments
 (0)