- Make a copy of this repo as a template with the Use this template button, please note that the repo name must be the same as the plugin name, the default branch must be
main - Clone your repo to a local development folder. For convenience, you can place this folder in your
{workspace}/data/plugins/folder - Install NodeJS and pnpm, then run
pnpm iin the command line under your repo folder - Execute
pnpm run devfor real-time compilation - Open SiYuan marketplace and enable plugin in downloaded tab
- i18n/*
- icon.png (optional default icon, 160*160)
- index.css
- index.js
- plugin.json
- preview.png (optional default preview, 1024*768)
- README*.md
- Frontend API
- Backend API
In terms of internationalization, our main consideration is to support multiple languages. Specifically, we need to complete the following tasks:
- Meta information about the plugin itself, such as plugin description and readme
displayName,descriptionandreadmefields in plugin.json, and the corresponding README*.md file
- Text used in the plugin, such as button text and tooltips
- src/i18n/*.json language configuration files
- Use
this.i18n.keyto get the text in the code
It is recommended that the plugin supports at least English and Simplified Chinese, so that more people can use it more conveniently. Unsupported languages do not need to be declared in the displayName, description and readme fields in plugin.json.
A typical example is as follows:
{
"name": "plugin-sample",
"author": "Vanessa",
"url": "https://github.com/siyuan-note/plugin-sample",
"version": "0.5.1",
"minAppVersion": "3.8.4",
"kernels": ["all"],
"backends": ["all"],
"frontends": ["all"],
"disabledInPublish": false,
"publish": {
"resources": [],
"data": ["readonlyText"]
},
"displayName": {
"default": "Plugin Sample",
"zh-CN": "插件示例"
},
"description": {
"default": "This is a plugin development sample",
"zh-CN": "这是一个插件开发示例"
},
"readme": {
"default": "README.md",
"zh-CN": "README.zh-CN.md"
},
"icon": "icon.png",
"preview": "preview.png",
"funding": {
"custom": ["https://ld246.com/sponsor"]
},
"keywords": [
"开发者参考",
"developer reference",
"示例插件"
]
}name: Plugin package name, must be the same as the GitHub repository name, and cannot be duplicated with other plugins in the marketplaceauthor: Plugin author nameurl: Plugin repo URLversion: Plugin version number, needs to follow the semver specificationminAppVersion: Minimum SiYuan version required to use this plugindisabledInPublish: Whether to disable the plugin when using the publish service, defaults to false, i.e., not disabledpublish.resources: Extra frontend files available to publishing-service visitors, using exact paths relative to the plugin directory, such asimages/logo.png; directories and wildcards are not supportedpublish.data: Public snapshot field names; each value must be a string, number, boolean ornull, and access requires a separate administrator grantbackends: Backend environment required by the plugin, optional values arewindows,linux,darwin,docker,android,ios,harmonyandallwindows: Windows desktoplinux: Linux desktopdarwin: macOS desktopdocker: Dockerandroid: Android APPios: iOS APPharmony: HarmonyOS APPall: All environments
kernels: Backend environment supported by the plugin's kernel plugin (kernel.js), optional values are the same asbackends(windows,linux,darwin,docker,android,ios,harmonyandall)- Only needed when the plugin includes a kernel plugin; if this field is missing or empty, the kernel plugin will not be started, but the plugin can still be installed and used on the frontend
frontends: Frontend environment required by the plugin, optional values aredesktop,desktop-window,mobile,browser-desktop,browser-mobileandalldesktop: Desktopdesktop-window: Desktop window converted from tabmobile: Mobile APPbrowser-desktop: Desktop browserbrowser-mobile: Mobile browserall: All environments
displayName: Plugin name (plain text), displayed in the marketplace listdefault: Default language, must exist. If the plugin supports English, English should be used herezh-CN,enand other languages: optional, must be BCP 47 tags (e.g.zh-CN,zh-TW,en,ja,pt-BR)
description: Plugin description (plain text), displayed in the marketplace listdefault: Default language, must exist. If the plugin supports English, English should be used herezh-CN,enand other languages: optional, must be BCP 47 tags
readme: Readme file name, displayed in the marketplace details pagedefault: Default language, must exist. If the plugin supports English, English should be used herezh-CN,enand other languages: optional, must be BCP 47 tags- Relative images are loaded from
package.zipwhen present; otherwise the online marketplace falls back to the matching GitHub Release. Include them inpackage.zipfor offline use
icon: Optional marketplace icon filename at the package root. Supports PNG, JPEG, WebP, and AVIF up to 64 KiB; the recommended size is 160*160preview: Optional marketplace preview filename at the package root. Supports PNG, JPEG, WebP, and AVIF up to 512 KiB; the recommended size is 1024*768- SVG is unsupported. To omit an image, remove its field and the legacy
icon.pngorpreview.png; an empty field value is invalid
- SVG is unsupported. To omit an image, remove its field and the legacy
funding: Plugin sponsorship informationopenCollective: Open Collective namepatreon: Patreon namegithub: GitHub login namecustom: Custom sponsorship link listlinks: Labeled custom sponsorship links, for example{"label": "Sponsor", "url": "https://example.com"}
keywords: Search keyword list, used for marketplace search function, supplements search keywords beyond the values ofname,author,displayName, anddescriptionfields
This sample requires SiYuan 3.8.4 or later. It keeps private settings accessed through loadData / saveData separate from public snapshots accessed through loadPublishData / savePublishData. Visitors and other code on the published page can read the public snapshot, so publish only content suitable for disclosure.
- In the administrator interface, enable the plugin and allow it in the publishing service, then open Plugin published data on its downloaded-plugin card and grant access to
readonlyText - Open this plugin's settings, edit Readonly text, and click Generate published snapshot to publish the current text
- Open the published page and use the plugin's top bar menu to view the snapshot; use Refresh published snapshot to load updates
The administrator selects the public field explicitly:
await this.savePublishData({readonlyText: textareaElement.value});The published frontend reads only the public snapshot:
const label = document.createElement("span");
try {
const data = await this.loadPublishData();
label.textContent = typeof data.readonlyText === "string" ? data.readonlyText : this.i18n.readonlyText;
} catch {
label.textContent = this.i18n.readonlyText;
}See src/index.ts for the complete example. The published frontend also handles read-only mode, skips private storage and kernel RPC, and clears its previous snapshot before each read. Unavailable data (403), a missing snapshot (404), and other failures use defaults and never fall back to private settings. Configuration and snapshot text are escaped before being used as HTML menu labels.
- Saving or deleting private settings does not update the public snapshot; use Generate published snapshot again to publish changes
savePublishDatareplaces the entire snapshot; omitted fields are removed, and{}publishes an empty snapshot- Data access is off by default; adding declared fields requires a new grant, and granting or revoking access clears the previous snapshot
- After a grant, generate a new snapshot; to stop sharing, revoke data access on the downloaded-plugin card
- Select public scalar values individually; never pass the whole private settings object or serialize it into a string
index.js,index.css, and JSON files directly underi18n/are available as standard entry resources; this sample uses no extra resources, sopublish.resourcesis empty- List each additional script, image, font or HTML file in
publish.resourcesand include it in the package;plugin.json,kernel.js, links, and directory traversal are prohibited data/storage/petalremains private, and/api/file/readDirremains administrator-only
The published siyuan 1.2.7 SDK does not yet declare the snapshot methods. src/siyuan-publish.d.ts supplies temporary declarations matching petal; remove this file after upgrading to an SDK that includes them. The declarations provide types only; the runtime methods require SiYuan 3.8.4 or later. Run pnpm test to check the publishing example and pnpm exec tsc --noEmit to check types.
For the complete permission model and HTTP APIs, see Plugin publishing.
A plugin can provide one or more startup appearances without running plugin code during startup. SiYuan scans the resources declared by installed plugins, and the user makes the final selection in Settings - Appearance - Startup appearance. The selection applies only to the current device after restart; a plugin should not modify it proactively.
Declare the appearance IDs in plugin.json:
{
"bootAppearances": [
"sunrise",
"night-sky"
]
}Place each appearance in its own directory:
boot-appearances/
└── sunrise/
├── boot.json
├── style.css
└── assets/
├── background.mp4
├── poster.webp
└── logo.webp
boot.json uses the following format:
{
"schemaVersion": 1,
"id": "sunrise",
"displayName": {
"default": "Sunrise",
"zh_CN": "日出"
},
"frontends": [
"desktop",
"mobile"
],
"backgroundColor": "#1e1e1e",
"style": "style.css",
"layers": [
{
"id": "background",
"type": "video",
"src": "assets/background.mp4",
"poster": "assets/poster.webp",
"fit": "cover",
"position": "center"
},
{
"id": "logo",
"type": "image",
"src": "assets/logo.webp",
"fit": "contain",
"position": "center"
}
],
"officialUI": {
"showLogo": false,
"showDetails": true,
"textColor": "#ffffff",
"progressColor": "#d23f31",
"trackColor": "#ffffff33"
}
}The layer array order is the visual stacking order. style.css runs only inside a non-interactive sandboxed frame and can address generated elements with [data-layer="<id>"]; relative url() values are resolved from the stylesheet directory. CSS subresources remain limited to the selected appearance by CSP and the resource route; an unavailable indirect resource fails on its own without necessarily disabling the whole appearance. JavaScript, arbitrary HTML, audio, custom fonts, and external URLs are not supported.
The format is validated before an appearance is listed:
- Appearance and layer IDs contain only lowercase letters, digits, and hyphens, are at most 64 characters, and hyphens cannot be consecutive or appear at either end; layer IDs must be unique
displayName.defaultis required;frontendsaccepts onlydesktopandmobile, and when omitted it inherits the compatible native frontends fromplugin.json- Colors use 3, 4, 6, or 8 digit hexadecimal notation; omitted background and official UI colors use the built-in startup page colors, while
showLogoandshowDetailsdefault totrue fitacceptscover,contain,fill,none, orscale-downand defaults tocover;positionacceptscenter,top,right,bottom,left,top-left,top-right,bottom-right, orbottom-leftand defaults tocenter- Images use PNG, JPEG, or WebP and are at most 5 MB each; videos use MP4, are at most 20 MB each, require an image poster, and are forced to muted, autoplay, loop, and inline playback
boot.jsonandstyle.cssare each at most 200 KB, an appearance has at most 8 layers, and its directory is at most 50 MB with at most 256 files and directories; relative paths are at most 512 UTF-8 bytes and 16 levels deep- Paths declared by
boot.jsonare relative to the appearance directory; absolute paths,.., backslashes, and symbolic links are rejected; unsupported declared resource types or MIME mismatches make the appearance unavailable, and unsupported files are never served
The appearance resources live under workspace data and can be synchronized. The active selection is device-local and automatically falls back to SiYuan's built-in startup page if the provider is uninstalled or any validation or loading step fails.
No matter which method is used to compile and package, we finally need to generate a package.zip, which contains at least the following files:
- i18n/* (If the plugin supports multiple languages, language files need to be packaged to this directory, otherwise this directory is not needed)
- Image files declared by
iconandpreview(optional) - index.css
- index.js
- plugin.json
- README*.md
- boot-appearances/* (optional startup appearance resources)
- Execute
pnpm run buildto generate package.zip - Create a new GitHub release using your new version number as the "Tag version". See here for an example: https://github.com/siyuan-note/plugin-sample/releases
- Upload the file package.zip as binary attachments
- Publish the release
For the first release, fork the community bazaar repository, add one owner/repo line to plugins.txt in its root, and open a PR against main. Use one repository per line without commas or empty lines, and add only one new package per PR. See Submitting a bazaar package for the full process and review rules.
After the PR is merged, the bazaar updates its index automatically. For subsequent updates, increase version in the package manifest and publish a regular GitHub Release containing package.zip; no additional listing PR is needed. See Updating a bazaar package for update timing and troubleshooting, and check deployment status in the Stage workflow.
Developers need to pay attention to the following specifications.
If plugins or external extensions require direct reading or writing of files under the data directory, please use the kernel API to achieve this. Do not call fs or other electron or nodejs APIs directly, as it may result in data loss during synchronization and cause damage to cloud data.
Related APIs can be found at: /api/file/* (e.g., /api/file/getFile).
When creating a daily note in SiYuan, a custom-dailynote-yyyymmdd attribute will be automatically added to the document to distinguish it from regular documents.
For more details, please refer to Github Issue #9807.
Developers should pay attention to the following when developing the functionality to manually create Daily Notes:
- If
/api/filetree/createDailyNoteis called to create a daily note, the attribute will be automatically added to the document, and developers do not need to handle it separately - If a document is created manually by developer's code (e.g., using the
createDocWithMdAPI to create a daily note), please manually add this attribute to the document
Each frontend process owns its own plugin instance. Under normal conditions, SiYuan runs onload, onLayoutReady, onDataChanged, onunload, and uninstall for the same plugin strictly in sequence and waits for a returned Promise before entering the next phase. onload and onunload describe whether the plugin is running in the current frontend, uninstall runs only when the plugin is removed from the workspace, and onLayoutReady runs at most once after onload and kernel initialization complete.
onDataChanged runs only after the plugin reaches the Ready state and mounting completes. Pending notifications are coalesced. If the plugin leaves the base implementation unchanged, SiYuan reloads the whole plugin instead of invoking the empty callback. A pending notification that has not started is discarded when the plugin is disabled or uninstalled.
Disabling, reloading, or uninstalling a plugin starts one shared five-second removal budget when the first removal request is received. If onload, kernel initialization, onLayoutReady, or an active onDataChanged is still pending, waiting for it consumes the same budget. The remaining time is shared by onunload and, only for an actual uninstall, uninstall; the budget is not restarted for each hook.
Before the deadline, lifecycle phases remain strictly serial. Once the deadline expires, SiYuan stops waiting. JavaScript promises cannot be forcibly canceled, so a timed-out hook may continue during or after teardown. The five-second budget limits only how long SiYuan waits for Promises; it cannot interrupt synchronous JavaScript. SiYuan still invokes each remaining teardown hook exactly once on a best-effort basis without waiting for it, then removes host-managed resources and destroys the kernel connection.
Closing a standalone window or exiting SiYuan does not trigger frontend plugin lifecycle hooks as part of that action.
Plugin lifecycle hooks should follow these guidelines:
- Keep hooks short and avoid unbounded waits
- Make
onunloadanduninstallidempotent and safe when only part of the plugin state has been initialized - Cancel pending work with a plugin-owned mechanism such as
AbortController, and check cancellation after each asynchronous boundary before changing the DOM or using plugin APIs - Persist essential data when the corresponding operation occurs instead of relying on a teardown hook to finish
Plugins can register custom block renderers through customBlockRenders. This sample registers the counter type and adds an Insert custom block button to the editor breadcrumb bar. Clicking the button inserts a counter custom block at the current caret. See src/index.ts for the complete implementation.
The corresponding Markdown is shown below. plugin-sample is the plugin package name and should be replaced with the name from plugin.json in another plugin. The plugin package name and block type must be encoded separately as URI components.
;;;plugin-sample/counter
0
;;;A renderer should modify only the provided element mount. content is the custom block's persisted raw content. To change it, call setContent after render returns. setContent returns false in read-only mode or when the content contains a standalone ;;; closing-fence line. A renderer can return a cleanup function to remove event listeners, timers, and other external resources.
SiYuan displays the raw content as a fallback when the plugin is unavailable or the block type is not registered. Rendered DOM is transient; persisted data belongs in content, block attributes, or plugin-owned storage. Nested Protyle editors are not supported inside the mount. See the SiYuan .sy file JSON structure specification for the underlying format.