feat: fallback to unsplash cover when article contains no image

This commit is contained in:
Max Richter
2025-11-09 23:52:53 +01:00
parent 6c6b69a46a
commit 655fc648e6
27 changed files with 687 additions and 224 deletions

View File

@@ -31,19 +31,54 @@ export const fixRenderedMarkdown = (content: string) => {
});
};
export async function fetchStream(url: string, cb: (chunk: string) => void) {
const response = await fetch(url);
const reader = response?.body?.getReader();
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) return;
const data = new TextDecoder().decode(value);
data
.split("$")
.filter((d) => d && d.length)
.map((d) => cb(Array.isArray(d) ? d[0] : d));
}
type StreamMessage = {
type: "info";
message: string;
} | {
type: "error";
message: string;
} | {
type: "warning";
message: string;
} | {
type: "finished";
url: string;
};
export async function fetchStream(
url: string,
cb: (chunk: StreamMessage) => void,
init?: RequestInit,
) {
const res = await fetch(url, init);
if (!res.body) return;
let buffer = "";
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(
new TransformStream<string, string>({
transform(chunk, controller) {
buffer += chunk;
let idx;
while ((idx = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, idx).trim();
buffer = buffer.slice(idx + 1);
if (line) controller.enqueue(line);
}
},
flush(controller) {
const line = buffer.trim();
if (line) controller.enqueue(line);
},
}),
)
.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
cb(JSON.parse(value));
}
}
@@ -58,32 +93,53 @@ export function hashString(message: string) {
}
export const createStreamResponse = () => {
let controller: ReadableStreamController<ArrayBufferView>;
const body = new ReadableStream({
start(cont) {
controller = cont;
const encoder = new TextEncoder();
let controller: ReadableStreamDefaultController<Uint8Array>;
const body = new ReadableStream<Uint8Array>({
start(c) {
controller = c;
},
});
const response = new Response(body, {
headers: {
"content-type": "text/plain",
// newline-delimited JSON
"content-type": "application/x-ndjson; charset=utf-8",
// prevent intermediaries from buffering/transforming
"cache-control": "no-cache, no-transform",
"x-content-type-options": "nosniff",
// nginx hint to disable proxy buffering
"x-accel-buffering": "no",
// if you control compression, keep it off for streams
// "content-encoding": "identity",
},
});
function cancel() {
controller.close();
const send = (obj: unknown) => {
controller.enqueue(encoder.encode(JSON.stringify(obj) + "\n")); // ← delimiter
};
const cancel = () => controller.close();
function info(message: string) {
return send({ type: "info", message });
}
function enqueue(chunk: string) {
controller?.enqueue(new TextEncoder().encode("$" + chunk));
function error(message: string) {
return send({ type: "error", message });
}
function warning(message: string) {
return send({ type: "warning", message });
}
return {
response,
cancel,
enqueue,
send,
info,
error,
warning,
};
};