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:
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user