diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..350e329
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,35 @@
+name: Release Obsidian plugin
+
+on:
+ push:
+ tags:
+ - "*"
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Use Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: "18.x"
+
+ - name: Build plugin
+ run: |
+ npm install
+ npm run build
+
+ - name: Create release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ tag="${GITHUB_REF#refs/tags/}"
+
+ gh release create "$tag" \
+ --title="$tag" \
+ --draft \
+ main.js manifest.json styles.css
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 8aa2645..e172ea7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) [year] [fullname]
+Copyright (c) 2025 Owain Williams
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index d845a33..2ed4f31 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
-# Umbracidian - a plugin for Obsidian
+
+# umbPublisher - an Obsidian plugin.
-This plugin allows you to push your [Obsidian](https://obsidian.md/) notes to [Umbraco 15+](https://umbraco.com) as a blog post.
+This plugin allows you to push your [Obsidian](https://obsidian.md/) notes to [Umbraco 15+](https://umbraco.com) as a content item.
-To see how to use this plugin with Obsidian, check out the [Wiki pages](https://github.com/OwainWilliams/Umbracidian/wiki)
+To see how to use this plugin with Obsidian, check out the [Wiki pages](https://github.com/OwainWilliams/umbpublisher/wiki)
diff --git a/assets/UmbracidianLogo.png b/assets/umbPublisher-Logo.png
similarity index 100%
rename from assets/UmbracidianLogo.png
rename to assets/umbPublisher-Logo.png
diff --git a/assets/umbracidianSettings.png b/assets/umbPublisherSettings.png
similarity index 100%
rename from assets/umbracidianSettings.png
rename to assets/umbPublisherSettings.png
diff --git a/getLatest.ps1 b/getLatest.ps1
index 66443c4..3424b76 100644
--- a/getLatest.ps1
+++ b/getLatest.ps1
@@ -1,4 +1,4 @@
-$Repo = "OwainWilliams/Umbracidian" # Replace with your actual repository name
+$Repo = "OwainWilliams/umbpublisher" # Replace with your actual repository name
$ManifestFile = "manifest.json"
$JSFile = "main.js"
diff --git a/icons/icons.ts b/icons/icons.ts
index fdd14a1..c91266b 100644
--- a/icons/icons.ts
+++ b/icons/icons.ts
@@ -1,9 +1,9 @@
import { addIcon } from "obsidian";
-import umbracoLogo from "./img/umbracidian-logo.svg";
+import umbPublisherLogo from "./img/umbpublisher-logo.svg";
-export class UmbracidianIcons {
- private icons = [{ iconId: "umbracidian-logo", svg: umbracoLogo }];
+export class umbpublisherIcons {
+ private icons = [{ iconId: "umbpublisher-logo", svg: umbPublisherLogo }];
registerIcons = () => {
this.icons.forEach(({ iconId, svg }) => {
diff --git a/icons/img/umbracidian-logo.svg b/icons/img/umbpublisher-logo.svg
similarity index 100%
rename from icons/img/umbracidian-logo.svg
rename to icons/img/umbpublisher-logo.svg
diff --git a/main.ts b/main.ts
index f5d31c0..ac45a2b 100644
--- a/main.ts
+++ b/main.ts
@@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Editor, MarkdownView, Notice, Plugin, requestUrl } from 'obsidian';
-import { DEFAULT_SETTINGS, UmbracidianSettings } from "./types/index";
+import { DEFAULT_SETTINGS, umbpublisherSettings } from "./types/index";
import { SettingTab } from "./settings";
-import { UmbracidianIcons } from "./icons/icons";
+import { umbpublisherIcons } from "./icons/icons";
import { GetUmbracoDocType } from "./methods/getUmbracoDocType";
import { CallUmbracoApi } from "./methods/callUmbracoApi";
import { GenerateGuid } from 'methods/generateGuid';
-const matter = require("gray-matter");
+
interface Frontmatter {
@@ -20,10 +20,10 @@ interface Frontmatter {
content: string;
}
-export default class Umbracidian extends Plugin {
- settings: UmbracidianSettings;
+export default class umbpublisher extends Plugin {
+ settings: umbpublisherSettings;
- private icons = new UmbracidianIcons();
+ private icons = new umbpublisherIcons();
private bearerToken: null | string = null; // Initialize bearerToken to null
async onload() {
@@ -31,7 +31,7 @@ export default class Umbracidian extends Plugin {
this.icons.registerIcons();
// This creates an icon in the left ribbon.
- this.addRibbonIcon('umbracidian-logo', 'Umbracidian', async (evt: MouseEvent) => {
+ this.addRibbonIcon('umbpublisher-logo', 'umbpublisher', async (evt: MouseEvent) => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) {
new Notice('No active Markdown view found.');
@@ -48,18 +48,24 @@ export default class Umbracidian extends Plugin {
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'push-to-umbraco',
- name: 'Push to Umbraco command',
- editorCallback: async (editor: Editor) => {
- const view = this.app.workspace.getActiveViewOfType(MarkdownView);
- if (!view) {
+ name: 'Push to Umbraco',
+ editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) =>
+ {
+ if(checking) return true;
+
+ const value = this.app.workspace.getActiveViewOfType(MarkdownView);
+
+ if(!value){
new Notice('No active Markdown view found.');
return;
}
+ (async () => {
this.bearerToken = await this.getBearerToken();
-
const umbracoDocType = await GetUmbracoDocType(this.settings.blogDocTypeAlias, this.settings.websiteUrl, this.bearerToken);
await this.createObsidianNode(view, umbracoDocType, this.settings.websiteUrl);
+ })();
}
+
});
// This adds a settings tab so the user can configure various aspects of the plugin
@@ -105,18 +111,16 @@ export default class Umbracidian extends Plugin {
body: body.toString(),
});
- // Check if the response contains valid JSON
+
if (response.json) {
const data = response.json as { access_token: string };
- // console.log('Bearer token response:', data);
- return data.access_token; // Assuming the token is in the "access_token" field
+
+ return data.access_token;
} else {
- // console.error('Empty or invalid JSON response:', response);
new Notice('Failed to fetch bearer token.');
return null;
}
} catch (error) {
- // console.error('Error fetching bearer token:', error);
new Notice(`Error fetching bearer token: ${error}`);
return null;
}
@@ -130,9 +134,17 @@ export default class Umbracidian extends Plugin {
return null;
}
- const metaMatter = this.app.metadataCache.getFileCache(noteFile)?.frontmatter;
- const pageContent = matter(view.getViewData());
-
+ const fileCache = this.app.metadataCache.getFileCache(noteFile);
+ const metaMatter = fileCache?.frontmatter;
+ const fileContent = await this.app.vault.read(noteFile);
+
+ let content = fileContent;
+ if (fileCache?.frontmatterPosition) {
+ const { start, end } = fileCache.frontmatterPosition;
+ const lines = fileContent.split('\n');
+ content = lines.slice(end.line + 1).join('\n');
+ }
+
const frontmatter = {
title: metaMatter?.title || view.file?.basename,
tags: metaMatter?.tags || [],
@@ -140,14 +152,13 @@ export default class Umbracidian extends Plugin {
status: metaMatter?.published ? "published" : "draft",
excerpt: metaMatter?.excerpt || undefined,
feature_image: metaMatter?.feature_image || undefined,
- content: pageContent.content,
+ content: content,
};
if (!frontmatter) {
new Notice('No frontmatter found.');
return null;
}
else {
- // console.log('Meta matter:', frontmatter);
return frontmatter;
}
diff --git a/manifest.json b/manifest.json
index b5ca102..f51dcec 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,9 +1,9 @@
{
- "id": "umbracidian",
- "name": "Umbracidian",
- "version": "1.0.0",
- "minAppVersion": "1.8.10",
- "description": "Push notes to Umbraco CMS as blog posts.",
+ "id": "umbpublisher",
+ "name": "umbPublisher",
+ "version": "1.1.0",
+ "minAppVersion": "1.9.12",
+ "description": "Push notes to Umbraco CMS as content.",
"author": "Owain Williams",
"authorUrl": "https://owain.codes",
"fundingUrl": "https://buymeacoffee.com/owaincodes",
diff --git a/package-lock.json b/package-lock.json
index f89b55b..91963a2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "Umbracidian",
+ "name": "umbpublisher",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "Umbracidian",
+ "name": "umbpublisher",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 4c98f99..8e8ffc4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "Umbracidian",
- "version": "0.1.0",
- "description": "Send Blog Posts to Umbraco CMS via Management API",
+ "name": "umbpublisher",
+ "version": "1.1.0",
+ "description": "Send Obsidian Notes to Umbraco CMS via Management API",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@@ -9,7 +9,7 @@
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
- "author": "",
+ "author": "Owain Williams",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.18.126",
@@ -32,10 +32,10 @@
},
"repository": {
"type": "git",
- "url": "git+https://github.com/obsidianmd/obsidian-sample-plugin.git"
+ "url": "git+https://github.com/OwainWilliams/umbPublisher.git"
},
"bugs": {
- "url": "https://github.com/obsidianmd/obsidian-sample-plugin/issues"
+ "url": "https://github.com/OwainWilliams/umbPublisher/issues"
},
- "homepage": "https://github.com/obsidianmd/obsidian-sample-plugin#readme"
+ "homepage": "https://github.com/OwainWilliams/umbPublisher/"
}
diff --git a/settings/index.ts b/settings/index.ts
index c17e0f5..06e7c1c 100644
--- a/settings/index.ts
+++ b/settings/index.ts
@@ -1,27 +1,93 @@
-import Umbracidian from "main";
-import { App, PluginSettingTab, Setting } from "obsidian";
+import umbpublisher from "main";
+import { App, PluginSettingTab, Setting, requestUrl, Notice } from "obsidian";
+
+async function getBearerToken(websiteUrl: string, clientId: string, clientSecret: string): Promise {
+ const tokenEndpoint = `${websiteUrl}/umbraco/management/api/v1/security/back-office/token`;
+ const body = new URLSearchParams({
+ grant_type: 'client_credentials',
+ client_id: clientId,
+ client_secret: clientSecret,
+ });
+ try {
+ const response = await requestUrl({
+ url: tokenEndpoint,
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: body.toString(),
+ });
+ return (response.json as any).access_token;
+ } catch (e) {
+ new Notice('Failed to fetch bearer token');
+ return null;
+ }
+}
+
+async function fetchContentTree(websiteUrl: string, token: string): Promise {
+ const endpoint = `${websiteUrl}/umbraco/management/api/v1/tree/document-type/root`;
+ const response = await requestUrl({
+ url: endpoint,
+ method: 'GET',
+ headers: { 'Authorization': `Bearer ${token}` },
+ });
+ return (response.json as any).items || [];
+}
+
+// Recursively fetch all nodes and their children
+async function fetchAllContentNodes(
+ websiteUrl: string,
+ token: string,
+ parentId: string | null = null,
+ depth: number = 0
+): Promise {
+ const endpoint = parentId
+ ? `${websiteUrl}/umbraco/management/api/v1/tree/document/children?parentId=${parentId}`
+ : `${websiteUrl}/umbraco/management/api/v1/tree/document/root?skip=0&take=100&foldersOnly=false`;
+
+ const response = await requestUrl({
+ url: endpoint,
+ method: 'GET',
+ headers: { 'Authorization': `Bearer ${token}` },
+ });
+
+ const items = (response.json as any).items || [];
+ let allNodes: any[] = [];
+
+ for (const item of items) {
+ // Add current node with depth for indentation
+ allNodes.push({ ...item, depth });
+ // Recursively fetch children
+ const children = await fetchAllContentNodes(websiteUrl, token, item.id, depth + 1);
+ allNodes = allNodes.concat(children);
+ }
+
+ return allNodes;
+}
export class SettingTab extends PluginSettingTab {
- plugin: Umbracidian;
+ plugin: umbpublisher;
+ private cachedNodes: any[] = []; // Store fetched nodes
- constructor(app: App, plugin: Umbracidian) {
- super(app, plugin);
- this.plugin = plugin;
- }
+ constructor(app: App, plugin: umbpublisher) {
+ super(app, plugin);
+ this.plugin = plugin;
+ }
- display(): void {
- const { containerEl } = this;
+ display(): void {
+ let parentNodeDropdown: HTMLSelectElement | null = null;
+ let fetchButton: HTMLButtonElement | null = null;
+ const { containerEl } = this;
+ containerEl.empty();
- containerEl.empty();
- containerEl.createEl('h2', { text: 'Umbracidian' });
- new Setting(containerEl)
- .setName('Website URL')
- .setDesc('The URL of the Umbraco website e.g. https://example.com')
- .addText(text => text
- .setPlaceholder('Enter the website URL')
- .setValue(this.plugin.settings.websiteUrl)
- .onChange(async (value) => {
- this.plugin.settings.websiteUrl = value;
+ new Setting(containerEl)
+ .setName('Website URL')
+ .setDesc('The URL of the Umbraco website e.g. https://example.com')
+ .addText(text => text
+ .setPlaceholder('Enter the website URL')
+ .setValue(this.plugin.settings.websiteUrl)
+ .onChange(async (value) => {
+ const match = value.match(/^(https?:\/\/[^\/]+)/i);
+ const sanitized = match ? match[1] : value.replace(/\/.*$/, '');
+ this.plugin.settings.websiteUrl = sanitized;
await this.plugin.saveSettings();
})),
new Setting(containerEl)
@@ -35,25 +101,80 @@ export class SettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})),
new Setting(containerEl)
- .setName('Client Secret')
+ .setName('Client secret')
.setDesc('The client secret for the Umbraco API')
.addText(text => text
- .setPlaceholder('Client Secret from Umbraco')
+ .setPlaceholder('Client secret from Umbraco')
.setValue(this.plugin.settings.clientSecret)
.onChange(async (value) => {
this.plugin.settings.clientSecret = value;
await this.plugin.saveSettings();
}).inputEl.setAttribute('type', 'password')),
- new Setting(containerEl)
- .setName('Blog Parent Node UUID')
- .setDesc('The UUID of the parent node for blog posts e.g. 00000000-0000-0000-0000-00000000000, leave empty for root')
+ new Setting(containerEl)
+ .setName('Pick content parent node')
+ .setDesc('Fetch and select a parent node from Umbraco where content will be saved under')
+ .addButton(button => {
+ fetchButton = button.buttonEl;
+ button.setButtonText('Fetch nodes').onClick(async () => {
+ const { websiteUrl, clientId, clientSecret } = this.plugin.settings;
+ if (!websiteUrl || !clientId || !clientSecret) {
+ new Notice('Please enter Website URL, Client Id, and Client Secret first.');
+ return;
+ }
+ const token = await getBearerToken(websiteUrl, clientId, clientSecret);
+ if (!token) return;
+ // Fetch all nodes recursively and cache them
+ this.cachedNodes = await fetchAllContentNodes(websiteUrl, token);
+ if (parentNodeDropdown) {
+ parentNodeDropdown.innerHTML = '';
+ const rootOption = document.createElement('option');
+ rootOption.value = '';
+ rootOption.text = '[Select Node]';
+ parentNodeDropdown.appendChild(rootOption);
+ this.cachedNodes.forEach(node => {
+ const option = document.createElement('option');
+ option.value = node.id;
+ option.text = `${'—'.repeat(node.depth)} ${node.variants[0].name}`;
+ parentNodeDropdown?.appendChild(option);
+ });
+ parentNodeDropdown.value = this.plugin.settings.blogParentNodeId || '';
+ }
+ });
+ })
+ .addDropdown(dropdown => {
+ parentNodeDropdown = dropdown.selectEl;
+ // Populate dropdown from cache if available
+ parentNodeDropdown.innerHTML = '';
+ const rootOption = document.createElement('option');
+ rootOption.value = '';
+ rootOption.text = '[Select Node]';
+ parentNodeDropdown.appendChild(rootOption);
+ if (this.cachedNodes.length > 0) {
+ this.cachedNodes.forEach(node => {
+ const option = document.createElement('option');
+ option.value = node.id;
+ option.text = `${'—'.repeat(node.depth)} ${node.variants[0].name}`;
+ parentNodeDropdown?.appendChild(option);
+ });
+ }
+ parentNodeDropdown.value = this.plugin.settings.blogParentNodeId || '';
+
+ dropdown.onChange(async (value) => {
+ this.plugin.settings.blogParentNodeId = value;
+ this.display();
+ await this.plugin.saveSettings();
+
+ });
+
+ }),
+ new Setting(containerEl)
+ .setName('Blog parent node UUID')
+ .setDesc('For reference, this is fetched from the node picker above')
.addText(text => text
- .setPlaceholder('Enter the parent node UUID')
+ .setPlaceholder('Fetched from node picker above')
.setValue(this.plugin.settings.blogParentNodeId)
- .onChange(async (value) => {
- this.plugin.settings.blogParentNodeId = value;
- await this.plugin.saveSettings();
- })),
+ .setDisabled(true)
+ ),
new Setting(containerEl)
.setName('DocType alias')
.setDesc('This is the alias of the DocType you want to use for your blog posts')
@@ -71,11 +192,11 @@ export class SettingTab extends PluginSettingTab {
.setPlaceholder('Enter the Title alias')
.setValue(this.plugin.settings.titleAlias)
.onChange(async (value) => {
- this.plugin.settings.titleAlias = value;
+ this.plugin.settings.titleAlias = value;
await this.plugin.saveSettings();
})),
new Setting(containerEl)
- .setName('Blog Content Editor alias')
+ .setName('Blog content editor alias')
.setDesc('This should be an Umbraco.MarkdownEditor property on your page')
.addText(text => text
.setPlaceholder('Enter the Property alias')
@@ -83,6 +204,6 @@ export class SettingTab extends PluginSettingTab {
.onChange(async (value) => {
this.plugin.settings.blogContentAlias = value;
await this.plugin.saveSettings();
- }))
+ }));
}
}
diff --git a/types/index.ts b/types/index.ts
index 4d7baf9..fd3d746 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -1,4 +1,4 @@
-export interface UmbracidianSettings {
+export interface umbpublisherSettings {
mySetting: string;
websiteUrl: string;
blogParentNodeId: string;
@@ -9,7 +9,7 @@ export interface UmbracidianSettings {
blogContentAlias: string;
}
-export const DEFAULT_SETTINGS: UmbracidianSettings = {
+export const DEFAULT_SETTINGS: umbpublisherSettings = {
mySetting: 'default',
blogParentNodeId: 'null',
blogDocTypeAlias: 'BlogPost',
diff --git a/versions.json b/versions.json
index 26382a1..df3d877 100644
--- a/versions.json
+++ b/versions.json
@@ -1,3 +1,3 @@
{
- "1.0.0": "0.15.0"
+ "1.1.0": "0.15.0"
}