feat: resolve [[WikiLinks]] to blog URLs on publish

- Converts [[Note Name]] and [[Note Name|Alias]] in the body to
  Markdown links using the linked note's mp-url frontmatter field.
  Falls back to plain display text if the note is not published yet.
- Resolves the `related:` frontmatter field (Obsidian wikilinks) to
  blog URLs and sends them as a `related` Micropub property, enabling
  "See Also" display on the blog.
- Image embeds (![[...]]) are excluded via negative lookbehind.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-23 18:13:47 +01:00
parent 8b68846b5b
commit 0623c3ad0b
2 changed files with 72 additions and 9 deletions
+8 -8
View File
File diff suppressed because one or more lines are too long
+64 -1
View File
@@ -49,8 +49,11 @@ export class Publisher {
const { content: processedBody, uploadedUrls } = const { content: processedBody, uploadedUrls } =
await this.processImages(body); await this.processImages(body);
// Resolve [[WikiLinks]] in body to blog URLs
const linkedBody = this.resolveWikilinks(processedBody, file.path);
// Build Micropub properties // Build Micropub properties
const properties = this.buildProperties(frontmatter, processedBody, uploadedUrls, file.basename); const properties = this.buildProperties(frontmatter, linkedBody, uploadedUrls, file.basename, file.path);
let result: PublishResult; let result: PublishResult;
@@ -81,6 +84,7 @@ export class Publisher {
body: string, body: string,
uploadedUrls: string[], uploadedUrls: string[],
basename: string, basename: string,
filePath: string,
): Record<string, unknown> { ): Record<string, unknown> {
const props: Record<string, unknown> = {}; const props: Record<string, unknown> = {};
@@ -214,6 +218,17 @@ export class Publisher {
props["photo"] = uploadedUrls.map((url) => ({ value: url })); props["photo"] = uploadedUrls.map((url) => ({ value: url }));
} }
// Related posts — resolve [[WikiLink]] wikilinks to published blog URLs
const relatedRaw = this.resolveArray(fm["related"]);
if (relatedRaw.length > 0) {
const relatedUrls = relatedRaw
.map((ref) => this.resolveWikilinkToUrl(ref, filePath))
.filter((url): url is string => url !== null);
if (relatedUrls.length > 0) {
props["related"] = relatedUrls;
}
}
// Pass through any `mp-*` properties from frontmatter verbatim // Pass through any `mp-*` properties from frontmatter verbatim
for (const [k, v] of Object.entries(fm)) { for (const [k, v] of Object.entries(fm)) {
if (k.startsWith("mp-") && k !== "mp-url" && k !== "mp-syndicate-to") { if (k.startsWith("mp-") && k !== "mp-url" && k !== "mp-syndicate-to") {
@@ -435,6 +450,54 @@ export class Publisher {
return fmBlock.replace(/(\r?\n---\r?\n)$/, `\n${key}: ${value}$1`); return fmBlock.replace(/(\r?\n---\r?\n)$/, `\n${key}: ${value}$1`);
} }
// ── Wikilink resolution ──────────────────────────────────────────────────
/**
* Replace Obsidian [[WikiLinks]] in body text with Markdown hyperlinks.
* Uses mp-url from the linked note's frontmatter. Falls back to plain
* display text if the note is not found or not yet published.
* Image embeds (![[...]]) are left untouched via negative lookbehind.
*/
private resolveWikilinks(body: string, sourcePath: string): string {
return body.replace(
/(?<!!)\[\[([^\]|#]+)(#[^\]|]*)?\|?([^\]]*)\]\]/g,
(_match, noteName: string, anchor: string | undefined, alias: string) => {
const cleanName = noteName.trim();
const displayText =
alias?.trim() || cleanName.split("/").pop() || cleanName;
const url = this.resolveWikilinkToUrl(`[[${cleanName}]]`, sourcePath);
if (!url) return displayText;
const anchorSuffix = anchor
? anchor.toLowerCase().replace(/\s+/g, "-")
: "";
return `[${displayText}](${url}${anchorSuffix})`;
},
);
}
/**
* Resolve a single [[WikiLink]] or plain URL string to a published mp-url.
* Returns null if the note is not found or has no mp-url.
*/
private resolveWikilinkToUrl(
ref: string,
sourcePath: string,
): string | null {
if (ref.startsWith("http")) return ref;
const m = ref.match(/^\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]$/);
if (!m) return null;
const file = this.app.metadataCache.getFirstLinkpathDest(
m[1].trim(),
sourcePath,
);
if (!file) return null;
return (
(this.app.metadataCache.getFileCache(file)?.frontmatter?.[
"mp-url"
] as string | undefined) ?? null
);
}
private resolveArray(value: unknown): string[] { private resolveArray(value: unknown): string[] {
if (!value) return []; if (!value) return [];
if (Array.isArray(value)) return value.map(String); if (Array.isArray(value)) return value.map(String);