Add SiYuan Blogging support

This commit is contained in:
MassiveBox 2025-04-20 12:26:23 +02:00
parent bf3e635a1c
commit a3fb0634f6
Signed by: massivebox
GPG key ID: 9B74D3A59181947D
6 changed files with 188 additions and 14 deletions

6
.idea/jsLibraryMappings.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

View file

@ -18,10 +18,37 @@ npm run build
``` ```
The website will be built to the `./dist` folder. The website will be built to the `./dist` folder.
## Blogging with SiYuan
You can use SiYuan to write and manage your blog posts, instead of just placing the Markdown files in the
`src/content/blog` folder.
1. Install [SiYuan](https://b3log.org/siyuan/en/)
2. Start the application and keep it running in the background
3. Create a notebook for your blog posts, then right-click on it in the document tree, select `Settings`, then `Copy ID`
4. Set the environment variable `BLOG_NOTEBOOK_ID` to the notebook ID you copied earlier, like so:
`BLOG_NOTEBOOK_ID=your_notebook_id`
5. Run `npm run siyuan`
The documents in your SiYuan notebook must include the frontmatter inside a code block with `yaml` as the language.
Every line before the frontmatter will be deleted. The notes' title is entirely irrelevant, only the frontmatter
determines the slug and title.
You can embed attachments in your notes, and they will work normally in the blog.
You might need to restart the development server after running the script.
In order, to have an ogImage (also known as article cover):
1. Insert the image in your note before the YAML frontmatter block, so that it gets added to the Assets folder
2. Copy the asset link (Right-click on the image > copy `image URL`)
3. You can now remove the image from the note, or you can keep it there. It will not be shown twice in the blog post as
long as it's before the frontmatter.
- Generally, it's best to keep the image referenced somewhere, so that it's not suggested as an "unused asset".
- A simple and reasonable way to do this is to set the image as the document's cover image.
- Hover on the cover image, click `Assets` (photograph icon), and select your cover image.
## Blogging with Joplin ## Blogging with Joplin
You can use Joplin to write and manage your blog posts, instead of just placing the Markdown files in the If you prefer Joplin over SiYuan, that can also be used to generate the blog posts.
`src/content/blog` folder.
1. Install [Joplin](https://joplinapp.org/) 1. Install [Joplin](https://joplinapp.org/)
2. Start the Web Clipper Service: Tools > Options > Web Clipper > Start Web Clipper Service 2. Start the Web Clipper Service: Tools > Options > Web Clipper > Start Web Clipper Service
@ -32,9 +59,6 @@ You can use Joplin to write and manage your blog posts, instead of just placing
The script will look for a notebook named `Blog`. It will then download all notes from that notebook, alongside their The script will look for a notebook named `Blog`. It will then download all notes from that notebook, alongside their
attachments, and place them in the `src/content/blog` folder. attachments, and place them in the `src/content/blog` folder.
You can embed attachments in your notes, and they will work normally in the blog.
You might need to restart the development server after running the Joplin script.
In order, to have an ogImage (also known as article cover): In order, to have an ogImage (also known as article cover):
1. Add the image to the note, and change the alt text to `ogImage`. It will look like this: 1. Add the image to the note, and change the alt text to `ogImage`. It will look like this:
``` ```

9
package-lock.json generated
View file

@ -33,7 +33,7 @@
"@astrojs/sitemap": "^3.0.5", "@astrojs/sitemap": "^3.0.5",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@divriots/jampack": "^0.23.2", "@divriots/jampack": "^0.23.2",
"@iconify-json/fa6-solid": "^1.1.21", "@iconify-json/fa6-solid": "^1.2.3",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/github-slugger": "^1.3.0", "@types/github-slugger": "^1.3.0",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
@ -1682,10 +1682,11 @@
} }
}, },
"node_modules/@iconify-json/fa6-solid": { "node_modules/@iconify-json/fa6-solid": {
"version": "1.1.21", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@iconify-json/fa6-solid/-/fa6-solid-1.1.21.tgz", "resolved": "https://registry.npmjs.org/@iconify-json/fa6-solid/-/fa6-solid-1.2.3.tgz",
"integrity": "sha512-+C6F3dFjNdqj8S83ggIXZFsX2CwecFErJKk9dVEZNd0XpNx2iNCNS+K/fl1HLFhYRhS4lb2Rgfi8yVV4MQU7yA==", "integrity": "sha512-C5o8YJF+ekrS4wRb/6/0SE2KjRyJlCg++IOVC/fineiRinITivsmzFRNW1MQX2xfDZ1T7bxeKxLN6lcaTG3jGA==",
"dev": true, "dev": true,
"license": "CC-BY-4.0",
"dependencies": { "dependencies": {
"@iconify/types": "*" "@iconify/types": "*"
} }

View file

@ -13,6 +13,7 @@
"cz": "cz", "cz": "cz",
"prepare": "husky install", "prepare": "husky install",
"lint": "eslint .", "lint": "eslint .",
"siyuan": "node src/utils/siyuan.mjs",
"joplin": "node src/utils/joplin.mjs", "joplin": "node src/utils/joplin.mjs",
"upload": "node src/utils/upload.mjs" "upload": "node src/utils/upload.mjs"
}, },
@ -42,7 +43,7 @@
"@astrojs/sitemap": "^3.0.5", "@astrojs/sitemap": "^3.0.5",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@divriots/jampack": "^0.23.2", "@divriots/jampack": "^0.23.2",
"@iconify-json/fa6-solid": "^1.1.21", "@iconify-json/fa6-solid": "^1.2.3",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@types/github-slugger": "^1.3.0", "@types/github-slugger": "^1.3.0",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",

View file

@ -19,7 +19,7 @@ async function fetch(url, method = "GET") {
} }
async function saveImage(url, outputPath) { export async function saveFile(url, outputPath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = http.get(url, (response) => { const request = http.get(url, (response) => {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
@ -164,12 +164,12 @@ async function saveResources(resources, apiToken, host, slug) {
const resID = resource[0]; const resID = resource[0];
const extension = resource[1]; const extension = resource[1];
const filePath = path.join(__dirname, '../content/blog', slug, resID + "." + extension); const filePath = path.join(__dirname, '../content/blog', slug, resID + "." + extension);
await saveImage(`${host}/resources/${resID}/file?token=${apiToken}&fields=data`, filePath) await saveFile(`${host}/resources/${resID}/file?token=${apiToken}&fields=data`, filePath)
console.log(`Resource saved to ${filePath}`); console.log(`Resource saved to ${filePath}`);
} }
} }
async function clearBlogFolder() { export async function clearBlogFolder() {
const blogFolderPath = path.join(__dirname, '../content/blog'); const blogFolderPath = path.join(__dirname, '../content/blog');
if (fs.existsSync(blogFolderPath)) { if (fs.existsSync(blogFolderPath)) {
fs.rmSync(blogFolderPath, { recursive: true, force: true }); fs.rmSync(blogFolderPath, { recursive: true, force: true });
@ -204,4 +204,6 @@ async function main() {
} }
} }
main().then(); if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(console.error);
}

140
src/utils/siyuan.mjs Normal file
View file

@ -0,0 +1,140 @@
import dotenv from 'dotenv';
import { clearBlogFolder, saveFile } from "./joplin.mjs";
import path from "path";
import { fileURLToPath } from "url";
import fs from "fs";
import extract from 'extract-zip';
dotenv.config();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const blogDir = path.join(__dirname,'../content/blog/');
const zipPath = path.join(blogDir,'/siyuan.zip');
let apiBase = process.env.SIYUAN_API_BASE ? process.env.SIYUAN_API_BASE : await findSiYuanPort();
async function checkPort(base, port) {
const response = await fetch(base + port + '/api/notebook/lsNotebooks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if(response.status !== 200) { return false; }
const respJson = await response.json();
if(respJson.code === 0) {
console.log(`Found SiYuan port: ${port}`);
return true;
}
}
async function findSiYuanPort() {
let port;
const base = 'http://127.0.0.1:';
if(await checkPort(base, 6806)) { // default port in most cases
return base + 6806;
}
console.log("Starting to look for SiYuan port (long scan)...")
for(port = 1024; port <= 65535; port++) {
try {
if(await checkPort(base, port)) {
return base + port;
}
}catch(e){}
}
console.log("Couldn't find SiYuan port, make sure it's running!");
return null;
}
async function getNotebookDownload(notebookID) {
const response = await fetch(apiBase + '/api/export/exportNotebookMd', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
'notebook': notebookID
})
});
if(response.status !== 200) {
throw new Error(`Failed to download notebook. Status code: ${response.status}`);
}
const respJson = await response.json();
return apiBase + respJson.data.zip;
}
async function findMarkdownFiles(dirPath) {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
return entries
.filter(entry => entry.isFile())
.filter(entry => entry.name.endsWith('.md'))
.map(entry => path.join(dirPath, entry.name));
}
async function transformBlogPost(filePath) {
// Read the file content
const content = fs.readFileSync(filePath, 'utf-8');
// Split into lines and process
const lines = content.split('\n');
let newContent = [];
let inFrontmatter = false;
let frontmatterFound = false;
for (let line of lines) {
// Handle frontmatter code fences
if (!frontmatterFound && !inFrontmatter && line.startsWith('\`\`\`yaml')) {
inFrontmatter = true;
frontmatterFound = true;
continue; // Remove the opening fence
}
if(inFrontmatter && line.startsWith('ogImage: ')) {
line = line.replace('ogImage: assets/', 'ogImage: ./assets/');
}else{
line = line.replace('](assets/', '](./assets/'); // asset in markdown
}
if (inFrontmatter && line.startsWith('\`\`\`')) {
inFrontmatter = false;
continue; // Remove the closing fence
}
// Remove all lines before frontmatter
if (!frontmatterFound) {
continue;
}
if(!inFrontmatter) {
line += ' ' // add spaces before newline
}
newContent.push(line);
}
// Join lines and write back to file
fs.writeFileSync(filePath, newContent.join('\n'));
}
async function main() {
console.log('Preparing for SiYuan download...')
if(!process.env.SIYUAN_NOTEBOOK_ID) {
console.error('required env variable is not set');
return;
}
let downloadLink = await getNotebookDownload(process.env.SIYUAN_NOTEBOOK_ID);
await clearBlogFolder();
fs.mkdirSync(blogDir, { recursive: true });
await saveFile(downloadLink, zipPath);
await extract(zipPath, { dir: blogDir });
for (const filePath of await findMarkdownFiles(blogDir)) {
console.log("Processing...", filePath);
void transformBlogPost(filePath);
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(console.error);
}