Upload markdown blobs as text/plain so browsers render them inline; keep the kind 1063 m tag as text/markdown via a mimeType override

This commit is contained in:
Laan Tungir
2026-08-21 10:32:56 -04:00
parent 7db85abb6a
commit 8da7527abf
4 changed files with 45 additions and 4 deletions
+2
View File
@@ -204,6 +204,8 @@ Blossom is a protocol for storing blobs (files) on media servers using your Nost
2. Click **Publish to Blossom (with header)** (or run **Nostr: Publish to Blossom (with header)**). 2. Click **Publish to Blossom (with header)** (or run **Nostr: Publish to Blossom (with header)**).
3. The front-matter block is stripped, the markdown body is uploaded as a blob, and a kind 1063 event is built from the descriptor + front-matter (title, author, language, year, cover, tags) and published to your relays. 3. The front-matter block is stripped, the markdown body is uploaded as a blob, and a kind 1063 event is built from the descriptor + front-matter (title, author, language, year, cover, tags) and published to your relays.
> **Markdown MIME handling:** markdown blobs are uploaded as `text/plain` so browsers render them inline (no mainstream browser displays `text/markdown`, and servers send `X-Content-Type-Options: nosniff` which forbids guessing). The kind 1063 event's `m` tag still advertises `text/markdown`, so Nostr clients that render markdown can detect the file's true type.
**Blossom servers**: **Blossom servers**:
- The **Blossom Servers** section shows your servers with checkboxes — exactly like the Relays section. Every checked server receives the upload. - The **Blossom Servers** section shows your servers with checkboxes — exactly like the Relays section. Every checked server receives the upload.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "nostr-publish", "name": "nostr-publish",
"displayName": "Nostr Publish", "displayName": "Nostr Publish",
"description": "Publish files to Nostr from Codium/VSCode. Phase 0: raw unsigned event JSON. Phase 1: long-form notes (.md). Phase 2: signer (Rust n_signer port) signing backend.", "description": "Publish files to Nostr from Codium/VSCode. Phase 0: raw unsigned event JSON. Phase 1: long-form notes (.md). Phase 2: signer (Rust n_signer port) signing backend.",
"version": "0.1.1", "version": "0.1.2",
"publisher": "laantungir", "publisher": "laantungir",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
+19 -2
View File
@@ -1869,6 +1869,12 @@ function resolveBlossomServers(): string[] {
* 3. Uploads to all checked Blossom servers concurrently. * 3. Uploads to all checked Blossom servers concurrently.
* 4. Builds a kind 1063 NIP-94 event from the first successful descriptor. * 4. Builds a kind 1063 NIP-94 event from the first successful descriptor.
* 5. Signs and publishes the 1063 event to the enabled relays. * 5. Signs and publishes the 1063 event to the enabled relays.
*
* Markdown bodies are uploaded as `text/plain` (browsers render text/plain
* inline; no mainstream browser displays text/markdown, and servers send
* `X-Content-Type-Options: nosniff` which forbids guessing). The kind 1063
* event's `m` tag still advertises `text/markdown` so Nostr clients that
* render markdown can detect the file's true semantic type.
*/ */
async function uploadAndPublishMetadata( async function uploadAndPublishMetadata(
state: StateService, state: StateService,
@@ -1877,6 +1883,12 @@ async function uploadAndPublishMetadata(
body: Buffer, body: Buffer,
mime: string, mime: string,
): Promise<void> { ): Promise<void> {
// Hybrid MIME handling: markdown uploads go out as text/plain for
// browser-renderability; the semantic type is preserved for the m tag.
const isMarkdown = mime === "text/markdown";
const uploadMime = isMarkdown ? "text/plain" : mime;
const semanticMime = isMarkdown ? "text/markdown" : undefined;
// Require sign-in. // Require sign-in.
let signer = getSigner(state, config); let signer = getSigner(state, config);
if (!signer) { if (!signer) {
@@ -1938,7 +1950,7 @@ async function uploadAndPublishMetadata(
results = await uploadBlobToMany( results = await uploadBlobToMany(
{ {
body, body,
contentType: mime, contentType: uploadMime,
sha256, sha256,
signer, signer,
authTokenTtlSec: config.blossomAuthTokenTtlSec, authTokenTtlSec: config.blossomAuthTokenTtlSec,
@@ -1966,7 +1978,12 @@ async function uploadAndPublishMetadata(
// Build the kind 1063 event from the first successful descriptor. // Build the kind 1063 event from the first successful descriptor.
const descriptor: BlobDescriptor = ok[0].descriptor!; const descriptor: BlobDescriptor = ok[0].descriptor!;
const template = buildFileMetadataEvent({ descriptor, meta, publishedAt }); const template = buildFileMetadataEvent({
descriptor,
meta,
publishedAt,
mimeType: semanticMime,
});
// Validate. // Validate.
const validation = validateEventTemplate(template); const validation = validateEventTemplate(template);
+23 -1
View File
@@ -13,6 +13,17 @@ export interface FileMetadataInput {
meta: BlossomFrontMatter; meta: BlossomFrontMatter;
/** First-publish timestamp (unix seconds). Reused on updates. */ /** First-publish timestamp (unix seconds). Reused on updates. */
publishedAt: number; publishedAt: number;
/**
* Override for the `m` tag (the file's semantic MIME type).
*
* Markdown blobs are uploaded as `text/plain` so browsers render them
* inline (no mainstream browser displays `text/markdown`, and
* `X-Content-Type-Options: nosniff` forbids guessing). The server then
* reports `type: text/plain` in the descriptor. This override keeps the
* Nostr `m` tag advertising the true semantic type (`text/markdown`) so
* clients that render markdown can still detect it.
*/
mimeType?: string;
} }
/** /**
@@ -47,10 +58,21 @@ export function buildFileMetadataEvent(input: FileMetadataInput): EventTemplate
// Ensure the core tags exist even if nip94 omitted them. // Ensure the core tags exist even if nip94 omitted them.
ensureTag(tags, "url", descriptor.url); ensureTag(tags, "url", descriptor.url);
ensureTag(tags, "m", descriptor.type); ensureTag(tags, "m", input.mimeType ?? descriptor.type);
ensureTag(tags, "x", descriptor.sha256); ensureTag(tags, "x", descriptor.sha256);
ensureTag(tags, "size", String(descriptor.size)); ensureTag(tags, "size", String(descriptor.size));
// When an explicit mimeType override is given, replace any server-provided
// `m` tag (BUD-08 nip94 tags carry the upload Content-Type, which for
// markdown is deliberately text/plain — see FileMetadataInput.mimeType).
if (input.mimeType) {
for (const t of tags) {
if (t[0] === "m") {
t[1] = input.mimeType;
}
}
}
// Descriptive metadata from front-matter. // Descriptive metadata from front-matter.
tags.push(["title", meta.title]); tags.push(["title", meta.title]);
if (meta.summary) { if (meta.summary) {