Teilen gibt einen fertigen Discord-Beitrag -- und die zwei Seiten bekommen ein Gesicht
Filipe: "beim teilen wird auch nur der link geteilt, und ich will dass
wenn ich es teile im discord der agentur, dass ein banner und ein
kompletter fertiger text fuer die creator entsteht und nicht einfach
der link kopiert wird damit die leute drauf gehen. das ist kake."
Und: "perfektionier die zwei events kategorien. ich will es noch viel
krasser und spezieller."
EIN LINK IN EINEM DISCORD-KANAL IST EINE AUFFORDERUNG. Wer ihn nicht
anklickt -- und das sind die meisten -- weiss nicht, dass es die
Kampagne gibt. Was in Discord steht, muss die Kampagne SEIN. Der
Teilen-Knopf legt deshalb einen fertigen Beitrag in die Zwischenablage:
Titel als Ueberschrift, Zeitraum, Einleitung, die Aufgaben als Liste,
die Geschenke mit Coinpreis, "Gut zu wissen" aus Rangliste, Zeitplan
und Teilnahme. Er steht zusaetzlich sichtbar unter der Kachel, mit
Zeichenzaehler und zweitem Kopierknopf -- was man nicht gesehen hat,
fuegt man blind ein.
ZWEI DINGE SIND DABEI NICHT BELIEBIG:
- Der Link steht GANZ AM ENDE. Discord setzt die Bannervorschau
UNTER die Nachricht; steht er oben, schiebt das Banner den Text
aus dem Blick.
- Der Link steht NACKT da. Spitze Klammern unterdruecken die
Vorschau -- und damit genau das Banner, um das es geht.
2000 ZEICHEN, UND NICHT EINS MEHR. Gekuerzt wird auf dem Server, in
fester Reihenfolge und an Abschnittsgrenzen: erst "Gut zu wissen",
dann die Einleitung auf zwei Saetze, dann die Geschenke. Titel,
Zeitraum, Aufgaben und Link bleiben IMMER. Gemessen gegen sieben
Laengen und einen 1500-Zeichen-Titel als Haertefall.
Gebaut wird der Beitrag aus `teilenDaten()` -- derselben Funktion, aus
der auch die offene Seite entsteht. Zwei Aufbauten fuer dasselbe Event
wuerden irgendwann Verschiedenes sagen, und niemand koennte entscheiden,
welcher recht hat.
DIE ZWEI SEITEN: ein gezeichnetes Wappen in der Farbe der Spalte, ein
Kopf mit Lichtkante und Verlauf, der beim Scrollen oben stehen bleibt,
und eine Lagezeile ("2 laufen - 1 kommt - 3 vorbei"). Gezaehlt mit
derselben Regel, die die Gruppen darunter bildet; was null ist,
erscheint nicht. Augenschonend bleibt es ueber Form und Tiefe, nicht
ueber Leuchtkraft.
NEBENBEFUND, BEHOBEN: Der Einlesekasten war durch Seitenwahl und
Bildweg auf 521 px gewachsen -- auf 412 px mehr als ein halber
Telefonbildschirm, bevor man die erste Kachel sieht. Er klappt jetzt zu
(82 px) und merkt sich die Wahl; ein Druck, und er bleibt offen.
BERICHTIGT, WEIL GEMESSEN:
- Der Bindestrich wurde entwertet: "4\-woechige", "Ahorn\-Welt". In
Discord ist "-" nur am Zeilenanfang eine Aufzaehlung.
- Das Klappschild hatte nur eine Handy-Regel und sonst kein
Aussehen -- auf 1400 px ein nackter Knopf mit 27 px statt 48.
- bild-kampagne mass elementFromPoint ausserhalb des
Sichtfensters und meldete "etwas liegt auf dem Knopf", wo nichts
lag; und r(null) liess den ganzen Lauf abstuerzen, statt die
Zusicherung rot zu machen.
- EINE GEGENPROBE WAR BLIND: DISCORD_MAX von 2000 auf 99999 zu
setzen liess die Pruefung gruen -- der Testbeitrag ist 1230
Zeichen lang, die Grenze wurde also nie geprueft. Jetzt mit einem
Beitrag, der ohne Kuerzung sicher zu lang waere.
GEPRUEFT: 190 (Route und Teilen, +8), 183 (Kachel, +13), 69 Messungen
(Bild, +2), 67 (Agentur), 43 (Bildleser), struktur, css-klassen,
zeichen, deutsche-texte, tippziele. Sechs Gegenproben: Link nur noch
Link, Link oben statt unten, Grenze weg, Lagezeile weg, Kopf klebt
nicht, Kasten wieder offen -- alle sechs werden bemerkt, alle Dateien
byte-gleich wiederhergestellt.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+256
-16
@@ -157,7 +157,33 @@ teilenRouter.post("/workspace/api/eintrag/:id(\\d+)/teilen", (req, res) => {
|
||||
detail: `Eintrag ${e.id} "${String(e.titel).slice(0, 60)}"`,
|
||||
});
|
||||
}
|
||||
return res.json({ weg: `${TEILEN_BASIS}/e/${schluessel}`, schon: !!e.teilen_token });
|
||||
/* ---- DER FERTIGE BEITRAG KOMMT MIT (08.10.2026) ----------
|
||||
|
||||
Filipe: „beim teilen wird auch nur der link geteilt ... das
|
||||
ist kake.“ Er hat recht: Ein Link ist eine Aufforderung,
|
||||
und wer ihn nicht anklickt, weiß nicht, dass es die
|
||||
Kampagne gibt.
|
||||
|
||||
GEBAUT WIRD HIER, NICHT IM BROWSER. Der Beitrag enthält
|
||||
Aufgaben, Geschenke und Regeln -- dieselben Daten, die die
|
||||
offene Seite zeigt, aus derselben Funktion
|
||||
(`teilenDaten`). Im Browser gebaut wäre es die zweite
|
||||
Fassung derselben Sache, und die läuft auseinander.
|
||||
|
||||
FÄLLT ER AUS, BLEIBT DER LINK. Ein Teilen, das wegen eines
|
||||
Textbaufehlers gar nicht geht, wäre schlimmer als eines
|
||||
ohne Text -- deshalb steht der Bau in einem eigenen
|
||||
`try`. */
|
||||
const basis = `${TEILEN_BASIS}/e/${schluessel}`;
|
||||
let beitrag = null;
|
||||
try {
|
||||
const voll = eventMitNummer(e.id);
|
||||
if (voll) beitrag = discordText(teilenDaten(voll, basis));
|
||||
} catch (f) {
|
||||
console.error("[teilen] Beitrag:", f?.message);
|
||||
beitrag = null;
|
||||
}
|
||||
return res.json({ weg: basis, text: beitrag, schon: !!e.teilen_token });
|
||||
} catch (fehler) {
|
||||
console.error("[teilen] anlegen:", fehler?.message);
|
||||
return res.status(503).json({ fehler: "Das Teilen hat gerade nicht geklappt." });
|
||||
@@ -196,6 +222,31 @@ function tagText(iso) {
|
||||
return m ? `${m[3]}.${m[2]}.${m[1]}` : null;
|
||||
}
|
||||
|
||||
/* DIE SPALTEN STEHEN EINMAL DA (08.10.2026).
|
||||
|
||||
Seit es zwei Abfragen gibt -- eine ueber den Schluessel (die
|
||||
offene Seite) und eine ueber die Nummer (der Discord-Text beim
|
||||
Teilen) -- waere eine zweite Liste die Sorte Abschrift, die in
|
||||
diesem Haus schon einmal drei Spalten gekostet hat. Wer hier
|
||||
etwas ergaenzt, ergaenzt es fuer beide.
|
||||
|
||||
`erstellt_von` und `haus` stehen ABSICHTLICH nicht dabei: Was
|
||||
die Kampagne sagt, geht hinaus -- wer wir sind, nicht. */
|
||||
const TEILEN_SPALTEN = `id, titel, text, datum, event_ende, kampagne_id,
|
||||
teilen_seit, teilen_token, event_aufgaben, event_regeln,
|
||||
kampagne_daten`;
|
||||
|
||||
/** Das Event zu einer Nummer -- fuer den Weg ueber den Teilen-Knopf. */
|
||||
function eventMitNummer(id) {
|
||||
try {
|
||||
return db().prepare(
|
||||
`SELECT ${TEILEN_SPALTEN}
|
||||
FROM eintraege
|
||||
WHERE id = ? AND bereich = 'agentur' AND art = 'kampagne'`)
|
||||
.get(Number(id)) || null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** Das Event zu einem Schlüssel -- oder null. */
|
||||
function eventZu(token) {
|
||||
if (!/^[A-Za-z0-9_-]{10,64}$/.test(String(token || ""))) return null;
|
||||
@@ -207,8 +258,7 @@ function eventZu(token) {
|
||||
hinaus; was ueber UNS etwas sagt, nicht. Deshalb steht
|
||||
`erstellt_von` hier weiterhin nicht, und `haus` auch
|
||||
nicht. */
|
||||
`SELECT id, titel, text, datum, event_ende, kampagne_id, teilen_seit,
|
||||
event_aufgaben, event_regeln, kampagne_daten
|
||||
`SELECT ${TEILEN_SPALTEN}
|
||||
FROM eintraege
|
||||
WHERE teilen_token = ? AND bereich = 'agentur' AND art = 'kampagne'`)
|
||||
.get(String(token)) || null;
|
||||
@@ -242,15 +292,21 @@ teilenRouter.get("/e/:token/bild", (req, res) => {
|
||||
return liefereDatei(req, res, pfad);
|
||||
});
|
||||
|
||||
teilenRouter.get("/e/:token", (req, res) => {
|
||||
nichtIndizieren(res);
|
||||
const e = eventZu(req.params.token);
|
||||
if (!e) {
|
||||
/* ZURUECKGEZOGEN SIEHT AUS WIE NIE DAGEWESEN. Ein eigener Satz
|
||||
(„war mal da") würde bestätigen, dass es diesen Schlüssel gab --
|
||||
und damit Durchprobieren belohnen. */
|
||||
return res.status(404).type("text/html; charset=utf-8").send(seiteLeer());
|
||||
}
|
||||
/* =====================================================================
|
||||
WAS VON EINEM EVENT NACH DRAUSSEN GEHT -- AN EINER STELLE
|
||||
(08.10.2026)
|
||||
|
||||
Gerufen von ZWEI Wegen: der offenen Seite (`GET /e/:token`) und
|
||||
dem Discord-Text (`POST .../teilen`). Beide zeigen dasselbe
|
||||
Event; haetten sie je einen eigenen Aufbau, wuerde die Seite
|
||||
irgendwann etwas anderes sagen als der Beitrag daneben -- und
|
||||
niemand koennte sagen, welcher von beiden recht hat.
|
||||
|
||||
Die Grenze ist dieselbe wie vorher: Was die KAMPAGNE sagt, geht
|
||||
hinaus; keine Hinweiszeile an DogFather, kein Steckbrief, kein
|
||||
Name, keine Rolle, kein Haus.
|
||||
===================================================================== */
|
||||
export function teilenDaten(e, basis) {
|
||||
const von = tagText(e.datum);
|
||||
const bis = tagText(e.event_ende);
|
||||
const zeitraum = von && bis && bis !== von ? `${von} – ${bis}` : (von || "");
|
||||
@@ -326,15 +382,13 @@ teilenRouter.get("/e/:token", (req, res) => {
|
||||
const aufgabenKurz = aufgaben.slice(0, 3).join(" · ");
|
||||
const beschreibung = [zeitraum, kurz || aufgabenKurz]
|
||||
.filter(Boolean).join(" · ");
|
||||
const basis = `${TEILEN_BASIS}/e/${encodeURIComponent(req.params.token)}`;
|
||||
const hatBild = (() => {
|
||||
try {
|
||||
return !!db().prepare("SELECT 1 FROM dateien WHERE eintrag_id = ? LIMIT 1").get(e.id);
|
||||
} catch { return false; }
|
||||
})();
|
||||
|
||||
res.type("text/html; charset=utf-8");
|
||||
return res.send(seitePlakat({
|
||||
return {
|
||||
titel: String(e.titel || "Event"),
|
||||
zeitraum,
|
||||
text: kurz,
|
||||
@@ -347,7 +401,193 @@ teilenRouter.get("/e/:token", (req, res) => {
|
||||
gabenTitel: gabenAb?.titel || null,
|
||||
gabenUnter: gabenAb?.unter || null,
|
||||
abschnitte: rest,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* =====================================================================
|
||||
EIN FERTIGER BEITRAG FÜR DISCORD (08.10.2026)
|
||||
|
||||
Filipe: „beim teilen wird auch nur der link geteilt, und ich will
|
||||
dass wenn ich es teile im discord der agentur, dass ein banner und
|
||||
ein kompletter fertiger text für die creator entsteht und nicht
|
||||
einfach der link kopiert wird damit die leute drauf gehen. das ist
|
||||
kake."
|
||||
|
||||
Er hat recht, und zwar aus einem Grund, den man im Workspace nicht
|
||||
sieht: Ein Link in einem Discord-Kanal ist eine AUFFORDERUNG. Wer
|
||||
ihn nicht anklickt — und das sind die meisten — weiß nicht, dass es
|
||||
die Kampagne gibt. Was in Discord steht, muss die Kampagne SEIN.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
DER BANNER KOMMT VON SELBST, UND ZWAR ÜBER DEN LINK
|
||||
|
||||
Discord baut aus den Open-Graph-Angaben der offenen Seite eine
|
||||
Vorschau mit großem Bild. Die steht UNTER der Nachricht. Deshalb:
|
||||
|
||||
- Der Link steht GANZ AM ENDE. Steht er oben, schiebt die Vorschau
|
||||
den Text nach unten aus dem Blick.
|
||||
- Der Link steht NACKT da, nicht in spitzen Klammern. `<…>`
|
||||
unterdrückt die Vorschau — und damit genau das Banner, um das es
|
||||
geht.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
2000 ZEICHEN, UND NICHT EINS MEHR
|
||||
|
||||
Discord nimmt pro Nachricht 2000 Zeichen. Eine Nachricht, die beim
|
||||
Einfügen abgeschnitten wird, endet mitten im Wort — und niemand
|
||||
merkt es, bis ein Creator nachfragt. Gekürzt wird deshalb HIER, in
|
||||
fester Reihenfolge und an Abschnittsgrenzen:
|
||||
|
||||
1. „Gut zu wissen" fällt weg (steht vollständig auf der Seite)
|
||||
2. die Einleitung wird kürzer (steht vollständig auf der Seite)
|
||||
3. die Geschenke fallen weg (stehen vollständig auf der Seite)
|
||||
|
||||
Titel, Zeitraum, Aufgaben und der Link bleiben IMMER. Das sind die
|
||||
vier Dinge, wegen denen man den Beitrag überhaupt schreibt.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
KEINE NAMEN, KEINE ROLLEN
|
||||
|
||||
Derselbe Satz wie bei der offenen Seite: Was die Kampagne sagt, geht
|
||||
hinaus. Wer sie eingelesen hat, steht nirgends — der Beitrag kommt
|
||||
vom Team, nicht von einer Person.
|
||||
===================================================================== */
|
||||
|
||||
const DISCORD_MAX = 2000;
|
||||
|
||||
/** Discord-Auszeichnung in fremdem Text entwerten.
|
||||
*
|
||||
* EIN TITEL MIT EINEM STERN DARIN würde sonst die halbe Nachricht
|
||||
* kursiv setzen. Entwertet wird nur dort, wo FREMDER Text steht
|
||||
* (Titel, Aufgaben, Geschenke) -- nicht in den Überschriften, die wir
|
||||
* selbst schreiben.
|
||||
*
|
||||
* ZWEI SORTEN ZEICHEN, UND SIE BRAUCHEN ZWEI REGELN (berichtigt
|
||||
* 08.10.2026, beim ersten Blick auf den fertigen Beitrag):
|
||||
*
|
||||
* ÜBERALL \ * _ ~ ` | die wirken mitten im Wort
|
||||
* NUR AM ANFANG # - > die wirken nur am Zeilenanfang
|
||||
*
|
||||
* Zuerst stand der Bindestrich in der ersten Gruppe. Heraus kam
|
||||
* „4\-wöchige Wachstum Challenge" und „Ahorn\-Welt" -- die
|
||||
* Rückstriche standen sichtbar im Beitrag. Ein Bindestrich mitten im
|
||||
* Wort ist in Discord keine Aufzählung; ihn zu entwerten macht den
|
||||
* Text kaputt, statt ihn zu retten. Dasselbe gilt für `#` und `>`.
|
||||
*
|
||||
* Der Rückstrich ist in Discord das Entwertungszeichen, genau wie in
|
||||
* Markdown -- und steht deshalb selbst mit in der ersten Gruppe. */
|
||||
function discordRoh(s) {
|
||||
return String(s ?? "")
|
||||
.replace(/([\\*_~`|])/g, "\\$1")
|
||||
.replace(/^([#\->])/, "\\$1");
|
||||
}
|
||||
|
||||
/** Ein fertiger Beitrag für den Agentur-Kanal.
|
||||
*
|
||||
* @param {object} d was `teilenDaten()` geliefert hat
|
||||
* @returns {string} höchstens 2000 Zeichen, Link am Ende
|
||||
*/
|
||||
export function discordText(d) {
|
||||
const kopf = [];
|
||||
kopf.push(`# ${discordRoh(d.titel)}`);
|
||||
const zeile2 = [];
|
||||
if (d.zeitraum) zeile2.push(`**${discordRoh(d.zeitraum)}**`);
|
||||
zeile2.push(d.vonTikTok ? "TikTok-Kampagne" : "Event der Agentur");
|
||||
kopf.push(zeile2.join(" · "));
|
||||
|
||||
/* Die Einleitung. Sie darf lang sein -- gekürzt wird sie erst, wenn
|
||||
es wirklich eng wird (siehe unten). */
|
||||
const einleitung = d.text ? [``, discordRoh(d.text)] : [];
|
||||
|
||||
const aufgaben = (d.aufgaben || []).length
|
||||
? [``, `## 🎯 ${d.aufgaben.length === 1 ? "Deine Aufgabe" : "Deine Aufgaben"}`,
|
||||
...d.aufgaben.map((a) => `- ${discordRoh(a)}`)]
|
||||
: [];
|
||||
|
||||
const gaben = (d.gaben || []).length
|
||||
? [``, "## 🎁 Diese Geschenke zählen",
|
||||
...d.gaben.map((g) => {
|
||||
const c = coinText(g.coins);
|
||||
const zusatz = c || g.zusatz;
|
||||
return `- **${discordRoh(g.text)}**${zusatz ? ` · ${discordRoh(zusatz)}` : ""}`;
|
||||
})]
|
||||
: [];
|
||||
|
||||
/* „Gut zu wissen": die Punkte aus Rangliste, Zeitplan und Teilnahme
|
||||
-- aber nur die kurzen. Ein ganzer Teilnahmebedingungs-Absatz in
|
||||
einem Discord-Beitrag liest niemand; dafür gibt es die Seite. */
|
||||
const merken = [];
|
||||
for (const a of d.abschnitte || []) {
|
||||
if (!/rangliste|zeitplan|teilnahme/i.test(a.art || "")) continue;
|
||||
for (const z of a.zeilen || []) {
|
||||
if (z.art !== "punkt" && z.art !== "text") continue;
|
||||
if (z.text.length > 120) continue;
|
||||
merken.push(`- ${discordRoh(z.text)}`);
|
||||
if (merken.length >= 5) break;
|
||||
}
|
||||
if (merken.length >= 5) break;
|
||||
}
|
||||
const wissen = merken.length ? [``, "## ℹ️ Gut zu wissen", ...merken] : [];
|
||||
|
||||
/* DER LINK ZULETZT UND NACKT -- Begründung oben. */
|
||||
const fuss = [``, "-# Alle Regeln, Preise und Ligen im Detail:", d.basis];
|
||||
|
||||
/* ZUSAMMENSETZEN UND NOTFALLS KÜRZEN. Gebaut wird von der
|
||||
vollständigen Fassung abwärts, nicht von der kurzen aufwärts: So
|
||||
steht immer die beste Fassung da, die noch passt. */
|
||||
const bauen = (teile) => teile.flat().join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
||||
|
||||
let aus = bauen([kopf, einleitung, aufgaben, gaben, wissen, fuss]);
|
||||
if (aus.length <= DISCORD_MAX) return aus;
|
||||
|
||||
aus = bauen([kopf, einleitung, aufgaben, gaben, fuss]);
|
||||
if (aus.length <= DISCORD_MAX) return aus;
|
||||
|
||||
/* Die Einleitung auf zwei Sätze. An einer Satzgrenze, nicht mitten
|
||||
im Wort -- ein abgeschnittenes Wort sieht nach einem Fehler aus. */
|
||||
if (d.text) {
|
||||
const saetze = String(d.text).split(/(?<=[.!?])\s+/);
|
||||
const kurz = saetze.slice(0, 2).join(" ");
|
||||
aus = bauen([kopf, [``, discordRoh(kurz)], aufgaben, gaben, fuss]);
|
||||
if (aus.length <= DISCORD_MAX) return aus;
|
||||
}
|
||||
|
||||
aus = bauen([kopf, aufgaben, gaben, fuss]);
|
||||
if (aus.length <= DISCORD_MAX) return aus;
|
||||
|
||||
aus = bauen([kopf, aufgaben, fuss]);
|
||||
if (aus.length <= DISCORD_MAX) return aus;
|
||||
|
||||
/* Selbst Titel, Aufgaben und Link passen nicht -- dann werden die
|
||||
Aufgaben beschnitten. Der Link bleibt in jedem Fall: Ohne ihn ist
|
||||
der Beitrag eine Sackgasse. */
|
||||
const ohneAufgaben = bauen([kopf, fuss]);
|
||||
const platz = DISCORD_MAX - ohneAufgaben.length - 40;
|
||||
const gekuerzt = [];
|
||||
let laenge = 0;
|
||||
for (const a of aufgaben.slice(2)) {
|
||||
if (laenge + a.length + 1 > platz) break;
|
||||
gekuerzt.push(a);
|
||||
laenge += a.length + 1;
|
||||
}
|
||||
return bauen([kopf,
|
||||
gekuerzt.length ? [``, aufgaben[1], ...gekuerzt, "- …"] : [],
|
||||
fuss]);
|
||||
}
|
||||
|
||||
teilenRouter.get("/e/:token", (req, res) => {
|
||||
nichtIndizieren(res);
|
||||
const e = eventZu(req.params.token);
|
||||
if (!e) {
|
||||
/* ZURUECKGEZOGEN SIEHT AUS WIE NIE DAGEWESEN. Ein eigener Satz
|
||||
(„war mal da") würde bestätigen, dass es diesen Schlüssel gab --
|
||||
und damit Durchprobieren belohnen. */
|
||||
return res.status(404).type("text/html; charset=utf-8").send(seiteLeer());
|
||||
}
|
||||
const basis = `${TEILEN_BASIS}/e/${encodeURIComponent(req.params.token)}`;
|
||||
res.type("text/html; charset=utf-8");
|
||||
return res.send(seitePlakat(teilenDaten(e, basis)));
|
||||
});
|
||||
|
||||
/* ---------- Die beiden Seiten ----------------------------------------- */
|
||||
|
||||
Reference in New Issue
Block a user