notes-to-strapi-export-article-ai 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.editorconfig ADDED
@@ -0,0 +1,10 @@
1
+ # top-most EditorConfig file
2
+ root = true
3
+
4
+ [*]
5
+ charset = utf-8
6
+ end_of_line = lf
7
+ insert_final_newline = true
8
+ indent_style = tab
9
+ indent_size = 4
10
+ tab_width = 4
package/.eslintignore ADDED
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+
3
+ main.js
package/.eslintrc ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "root": true,
3
+ "parser": "@typescript-eslint/parser",
4
+ "env": { "node": true },
5
+ "plugins": [
6
+ "@typescript-eslint"
7
+ ],
8
+ "extends": [
9
+ "eslint:recommended",
10
+ "plugin:@typescript-eslint/eslint-recommended",
11
+ "plugin:@typescript-eslint/recommended"
12
+ ],
13
+ "parserOptions": {
14
+ "sourceType": "module"
15
+ },
16
+ "rules": {
17
+ "no-unused-vars": "off",
18
+ "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
19
+ "@typescript-eslint/ban-ts-comment": "off",
20
+ "no-prototype-builtins": "off",
21
+ "@typescript-eslint/no-empty-function": "off",
22
+ "@typescript-eslint/no-explicit-any": "off"
23
+ }
24
+ }
@@ -0,0 +1,58 @@
1
+ name: Auto Deploy, install dependencies, increment version and push tag
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ main ]
6
+ types: [ closed ]
7
+
8
+ jobs:
9
+ publish:
10
+ if: github.event.pull_request.merged == true
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+ token: ${{ secrets.GIT_TOKEN }}
18
+ ref: 'main'
19
+
20
+ - name: Configure Git
21
+ run: |
22
+ git config --local user.email "cinquin.andy@gmail.com"
23
+ git config --local user.name "Cinquin Andy"
24
+
25
+ - name: Use Node.js
26
+ uses: actions/setup-node@v4
27
+ with:
28
+ node-version: '20'
29
+ registry-url: 'https://registry.npmjs.org'
30
+
31
+ - name: Install Dependencies
32
+ run: npm ci
33
+
34
+ - name: Increment Package Version and Update Tag
35
+ id: version-bump
36
+ run: |
37
+ git fetch --tags
38
+ current_version=$(npm pkg get version | sed 's/"//g')
39
+ echo "Current version: $current_version"
40
+
41
+ new_version=$(npx semver -i patch $current_version)
42
+ echo "New version: $new_version"
43
+
44
+ while git rev-parse v$new_version >/dev/null 2>&1; do
45
+ new_version=$(npx semver -i patch $new_version)
46
+ echo "Tag v$new_version already exists. Trying next version..."
47
+ done
48
+
49
+ npm version $new_version --no-git-tag-version
50
+ echo "::set-output name=VERSION::$new_version"
51
+
52
+ - name: Commit and Push Version Update
53
+ run: |
54
+ git add package.json
55
+ git commit -m "chore(release): ${{ steps.version-bump.outputs.VERSION }}"
56
+ git tag v${{ steps.version-bump.outputs.VERSION }}
57
+ git push origin main --follow-tags
58
+ git push origin refs/tags/v${{ steps.version-bump.outputs.VERSION }}
@@ -0,0 +1,30 @@
1
+ name: Publish to npm when a new tag is pushed
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+ token: ${{ secrets.GIT_TOKEN }}
17
+
18
+ - name: Use Node.js
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '20'
22
+ registry-url: 'https://registry.npmjs.org'
23
+
24
+ - name: Install Dependencies
25
+ run: npm ci
26
+
27
+ - name: Publish to npm
28
+ run: npm publish
29
+ env:
30
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.prettierrc ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "singleQuote": true,
3
+ "semi": false,
4
+ "trailingComma": "es5",
5
+ "tabWidth": 2,
6
+ "useTabs": true,
7
+ "printWidth": 80,
8
+ "arrowParens": "avoid",
9
+ "bracketSpacing": true,
10
+ "jsxSingleQuote": false,
11
+ "proseWrap": "preserve",
12
+ "quoteProps": "as-needed",
13
+ "requirePragma": false,
14
+ "insertPragma": false,
15
+ "endOfLine": "lf",
16
+ "htmlWhitespaceSensitivity": "css",
17
+ "vueIndentScriptAndStyle": false,
18
+ "embeddedLanguageFormatting": "auto"
19
+ }
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2024> <CINQUIN Andy>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # 🚀 Strapi Exporter: Supercharge Your Obsidian-to-Strapi Workflow
2
+
3
+ [![Version](https://img.shields.io/github/package-json/v/CinquinAndy/notes-to-strapi-export-article-ai)](https://github.com/CinquinAndy/notes-to-strapi-export-article-ai/releases)
4
+ [![License](https://img.shields.io/github/license/CinquinAndy/notes-to-strapi-export-article-ai)](https://github.com/CinquinAndy/notes-to-strapi-export-article-ai/blob/main/LICENSE)
5
+ [![Sponsor](https://img.shields.io/badge/sponsor-CinquinAndy-purple)](https://github.com/sponsors/CinquinAndy)
6
+
7
+ Strapi Exporter is a game-changing Obsidian plugin that streamlines your content creation process by seamlessly exporting your notes to Strapi CMS. With its AI-powered image handling and SEO optimization features, you can take your content to the next level with just a few clicks.
8
+
9
+ ## ✨ Features
10
+
11
+ - 🖼️ Automatically upload images from your notes to Strapi
12
+ - 🎨 Generate SEO-friendly alt text and captions for images using AI
13
+ - 📝 Create SEO-optimized article content based on your notes
14
+ - 🔧 Customize the JSON template for the article fields in Strapi
15
+ - ⚙️ Easy configuration for Strapi API URL, token, and content attribute name
16
+
17
+ ## 🛠️ Installation
18
+
19
+ ### For users:
20
+
21
+ 1. Download the latest release from the [releases page](https://github.com/CinquinAndy/notes-to-strapi-export-article-ai/releases/tag/)
22
+ 2. Download the `main.js` file & `manifest.json` from the latest release.
23
+ 3. Create a folder named `notes-to-strapi-export-article-ai` in your Obsidian plugins folder (usually located at `<vault>/.obsidian/plugins/`).
24
+ 4. Move the `main.js` file & `manifest.json` to your Obsidian plugins folder (usually located at `<vault>/.obsidian/plugins/`).
25
+ 5. Restart Obsidian
26
+ 6. Enable the plugin in Obsidian's settings under "Community plugins".
27
+ 7. Configure the necessary settings (see the Configuration section below).
28
+ 8. Enjoy!
29
+
30
+ ### For developers:
31
+
32
+ To install Strapi Exporter, follow these steps (coming soon to the Obsidian plugin marketplace):
33
+
34
+ 1. Clone this repository into your Obsidian plugins folder (usually located at `<vault>/.obsidian/plugins/`).
35
+ 2. Run `npm install` to install the dependencies
36
+ 3. Run `npm run build` to build the plugin
37
+ 4. Restart Obsidian
38
+ 5. Enable the plugin in Obsidian's settings under "Community plugins".
39
+ 6. Configure the necessary settings (see the Configuration section below).
40
+
41
+ ## ⚙️ Configuration
42
+
43
+ To get started with Strapi Exporter, you'll need to configure the following settings:
44
+
45
+ - **Strapi URL**: The URL of your Strapi instance (e.g., `https://your-strapi-url`).
46
+ ![img.png](img.png)
47
+ - **Strapi API Token**: Your Strapi API token for authentication. You can create an API token in your Strapi admin panel under "Settings" > "API Tokens".
48
+ ![img_1.png](img_1.png)
49
+ ![img_2.png](img_2.png)
50
+ - You need, at least, to have the following permissions:
51
+ - article: create
52
+ - upload: create
53
+ - (you can also add full permissions, but it's not really recommended for security reasons)
54
+ ![img_3.png](img_3.png)
55
+ - (the token in the screenshot is not valid, don't try to use it 😌)
56
+ - **OpenAI API Key**: Your OpenAI API key for using GPT-3 to generate SEO-friendly content. You can get your API key from the [OpenAI website](https://platform.openai.com/account/api-keys).
57
+ ![img_4.png](img_4.png)
58
+ - this key is needed to use the GPT-3 API, which is used to generate the content of the article
59
+ - (it need to access to "Model capabilities" with "write" permission)
60
+ ![img_5.png](img_5.png)
61
+ - (or with the "all" permission)
62
+ - **JSON Template**: The JSON template for the article fields in Strapi. Customize this according to your Strapi content type structure. You can find the JSON template in your Strapi API documentation (Swagger).
63
+ ![img_6.png](img_6.png)
64
+ - to get the JSON template, you can go to the documentation of your Strapi API, and copy the JSON template of the article creation
65
+ - it should look like this: ``https://{api_url}/documentation/v1.0.0``
66
+ - then, go to the article creation, and copy the JSON template
67
+ ![img_7.png](img_7.png)
68
+ ![img_8.png](img_8.png)
69
+ - it should look like this for example:
70
+ ```json
71
+ {
72
+ "data": {
73
+ "title": "string",
74
+ "seo_title": "string",
75
+ "seo_description": "string",
76
+ "slug": "string",
77
+ "excerpt": "string",
78
+ "links": [
79
+ {
80
+ "id": "number",
81
+ "label": "string",
82
+ "url": "string"
83
+ }
84
+ ],
85
+ "subtitle": "string",
86
+ "type": "string",
87
+ "rank": "number",
88
+ "tags": [
89
+ {
90
+ "id": "number",
91
+ "name": "string"
92
+ }
93
+ ],
94
+ "locale": "string"
95
+ }
96
+ }
97
+ ```
98
+ - then, you can copy this JSON template in the settings of the plugin
99
+ - and copy that, to describe each field in the other JSON description setting
100
+ - **JSON Template Description**: A description for each field in the JSON template to help GPT-3 understand the structure. Follow the same schema as the JSON template to provide descriptions for each field.
101
+ ```json
102
+ {
103
+ "data": {
104
+ "title": "<Title of the item, as a short string>",
105
+ "seo_title": "<SEO optimized title, as a short string>",
106
+ "seo_description": "<SEO optimized description, as a short string>",
107
+ "slug": "<URL-friendly string derived from the title>",
108
+ "excerpt": "<A short preview or snippet from the content>",
109
+ "links": [
110
+ {
111
+ "id": "<Unique identifier for the link, as a number>",
112
+ "label": "<Display text for the link, as a short string>",
113
+ "url": "<URL the link points to, as a string>"
114
+ }
115
+ ],
116
+ "subtitle": "<Subtitle or secondary title, as a short string>",
117
+ "type": "<Category or type of the item, as a short string>",
118
+ "rank": "<Numerical ranking or order priority, as a number>",
119
+ "tags": [
120
+ {
121
+ "id": "<Unique identifier for the tag, as a number>",
122
+ "name": "<Name of the tag, as a short string>"
123
+ }
124
+ ],
125
+ "locale": "<Locale or language code, as a short string>"
126
+ }
127
+ }
128
+ ```
129
+ ```
130
+ ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
131
+ ⚠️
132
+ ⚠️ **Important:** Remove the `content` (or the attribute name that correspond to the main content of the article)
133
+ ⚠️ field from the JSON template and specify it separately in the "Strapi Content Attribute Name" setting.
134
+ ⚠️ ( i do that to avoid changes on the main content by chat gpt )
135
+ ⚠️
136
+ ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
137
+ ```
138
+ - **Strapi Article Create URL**: The URL to create articles in Strapi (e.g., `https://your-strapi-url/api/articles`).
139
+ - to get the URL, you can go to the documentation of your Strapi API, and copy the URL of the article creation
140
+ - it should look like this: ``https://{api_url}/documentation/v1.0.0``
141
+ - then, go to the article creation, and copy the URL
142
+ - **Strapi Content Attribute Name**: The attribute name for the content field in Strapi (e.g., `content`).
143
+ - to get the attribute name, you can go to the documentation of your Strapi API, and copy the attribute name of the article creation, (it need to be the name of the attribute that contain the main content of the article, for me it's "content", but it can be different for you)
144
+ - **Additional Prompt** (optional): Additional prompt to provide context for GPT-3 when generating content. You can use this field to specify additional information or instructions for the AI model. Like your langage, the tone of the article, etc.
145
+
146
+ ## 🚀 Usage
147
+
148
+ 1. Open a Markdown file in Obsidian.
149
+ 2. Click on the plugin's ribbon icon to start the magic.
150
+ ![img_9.png](img_9.png)
151
+ 3. Sit back and relax while Strapi Exporter does the heavy lifting:
152
+ - 🖼️ Extracting and uploading images to Strapi
153
+ - 🎨 Generating SEO-friendly alt text and captions for images
154
+ - 📝 Creating SEO-optimized article content based on your notes
155
+ - 🌐 Publishing the article to Strapi with the generated content and images
156
+ 4. Enjoy your freshly exported article in Strapi!
157
+
158
+ ## 🤝 Contributing
159
+
160
+ We welcome contributions from the community! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request. Let's make Strapi Exporter even better together!
161
+
162
+ ## 📜 License
163
+
164
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
165
+
166
+ ---
167
+
168
+ 🌟 Elevate your content workflow with Strapi Exporter and unleash the full potential of your Obsidian notes! 🌟
169
+
170
+ ### Roadmap
171
+
172
+ - [ ] Make it available as a plugin in Obsidian
173
+ - [ ] Add tests
174
+ - [ ] Renovate
175
+ - [ ] ajouter l'étape de création du plugin (création du folder)
176
+ - [ ] ajouter l'étape de redémarrage d'obsidian
177
+ - [ ] ajouter l'étape de configuration du plugin
178
+ - [ ] ajouter l'étape de configuration du token d'access de strapi (accès etc)
179
+
@@ -0,0 +1,48 @@
1
+ import esbuild from "esbuild";
2
+ import process from "process";
3
+ import builtins from "builtin-modules";
4
+
5
+ const banner =
6
+ `/*
7
+ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
8
+ if you want to view the source, please visit the github repository of this plugin
9
+ */
10
+ `;
11
+
12
+ const prod = (process.argv[2] === "production");
13
+
14
+ const context = await esbuild.context({
15
+ banner: {
16
+ js: banner,
17
+ },
18
+ entryPoints: ["main.ts"],
19
+ bundle: true,
20
+ external: [
21
+ "obsidian",
22
+ "electron",
23
+ "@codemirror/autocomplete",
24
+ "@codemirror/collab",
25
+ "@codemirror/commands",
26
+ "@codemirror/language",
27
+ "@codemirror/lint",
28
+ "@codemirror/search",
29
+ "@codemirror/state",
30
+ "@codemirror/view",
31
+ "@lezer/common",
32
+ "@lezer/highlight",
33
+ "@lezer/lr",
34
+ ...builtins],
35
+ format: "cjs",
36
+ target: "es2018",
37
+ logLevel: "info",
38
+ sourcemap: prod ? false : "inline",
39
+ treeShaking: true,
40
+ outfile: "main.js",
41
+ });
42
+
43
+ if (prod) {
44
+ await context.rebuild();
45
+ process.exit(0);
46
+ } else {
47
+ await context.watch();
48
+ }
package/img.png ADDED
Binary file
package/img_1.png ADDED
Binary file
package/img_2.png ADDED
Binary file
package/img_3.png ADDED
Binary file
package/img_4.png ADDED
Binary file
package/img_5.png ADDED
Binary file
package/img_6.png ADDED
Binary file
package/img_7.png ADDED
Binary file
package/img_8.png ADDED
Binary file
package/img_9.png ADDED
Binary file
package/main.ts ADDED
@@ -0,0 +1,687 @@
1
+ import OpenAI from 'openai'
2
+ import {
3
+ App,
4
+ MarkdownView,
5
+ Notice,
6
+ Plugin,
7
+ PluginSettingTab,
8
+ Setting,
9
+ TFile,
10
+ } from 'obsidian'
11
+
12
+ /**
13
+ * The settings for the plugin
14
+ */
15
+ interface MyPluginSettings {
16
+ strapiUrl: string
17
+ strapiApiToken: string
18
+ openaiApiKey: string
19
+ jsonTemplate: string
20
+ jsonTemplateDescription: string
21
+ strapiArticleCreateUrl: string
22
+ strapiContentAttributeName: string
23
+ additionalPrompt: string
24
+ }
25
+
26
+ /**
27
+ * The default settings for the plugin
28
+ */
29
+ const DEFAULT_SETTINGS: MyPluginSettings = {
30
+ strapiUrl: '',
31
+ strapiApiToken: '',
32
+ openaiApiKey: '',
33
+ jsonTemplate: `{
34
+ "data": {
35
+ "title": "string",
36
+ "seo_title": "string",
37
+ "seo_description": "string",
38
+ "slug": "string",
39
+ "excerpt": "string",
40
+ "links": [
41
+ {
42
+ "id": "number",
43
+ "label": "string",
44
+ "url": "string"
45
+ }
46
+ ],
47
+ "subtitle": "string",
48
+ "type": "string",
49
+ "rank": "number",
50
+ "tags": [
51
+ {
52
+ "id": "number",
53
+ "name": "string"
54
+ }
55
+ ],
56
+ "locale": "string"
57
+ }
58
+ }`,
59
+ jsonTemplateDescription: `{
60
+ "data": {
61
+ "title": "Title of the item, as a short string",
62
+ "seo_title": "SEO optimized title, as a short string",
63
+ "seo_description": "SEO optimized description, as a short string",
64
+ "slug": "URL-friendly string derived from the title",
65
+ "excerpt": "A short preview or snippet from the content",
66
+ "links": "Array of related links with ID, label, and URL",
67
+ "subtitle": "Subtitle or secondary title, as a short string",
68
+ "type": "Category or type of the item, as a short string",
69
+ "rank": "Numerical ranking or order priority, as a number",
70
+ "tags": "Array of associated tags with ID and name",
71
+ "locale": "Locale or language code, as a short string"
72
+ }
73
+ }`,
74
+ strapiArticleCreateUrl: '',
75
+ strapiContentAttributeName: '',
76
+ additionalPrompt: '',
77
+ }
78
+
79
+ /**
80
+ * The main plugin class
81
+ */
82
+ export default class MyPlugin extends Plugin {
83
+ settings: MyPluginSettings
84
+
85
+ /**
86
+ * The main entry point for the plugin
87
+ */
88
+ async onload() {
89
+ await this.loadSettings()
90
+
91
+ /**
92
+ * Add a ribbon icon to the Markdown view (the little icon on the left side bar)
93
+ */
94
+ const ribbonIconEl = this.addRibbonIcon(
95
+ 'upload',
96
+ 'Upload images to Strapi and update links in Markdown content, then generate article content using OpenAI',
97
+ async (evt: MouseEvent) => {
98
+ const activeView = this.app.workspace.getActiveViewOfType(MarkdownView)
99
+ if (!activeView) {
100
+ new Notice('No active Markdown view')
101
+ return
102
+ }
103
+
104
+ /** ****************************************************************************
105
+ * Check if all the settings are configured
106
+ * *****************************************************************************
107
+ */
108
+ if (!this.settings.strapiUrl || !this.settings.strapiApiToken) {
109
+ new Notice(
110
+ 'Please configure Strapi URL and API token in the plugin settings'
111
+ )
112
+ return
113
+ }
114
+
115
+ if (!this.settings.openaiApiKey) {
116
+ new Notice('Please configure OpenAI API key in the plugin settings')
117
+ return
118
+ }
119
+
120
+ if (!this.settings.jsonTemplate) {
121
+ new Notice('Please configure JSON template in the plugin settings')
122
+ return
123
+ }
124
+
125
+ if (!this.settings.jsonTemplateDescription) {
126
+ new Notice(
127
+ 'Please configure JSON template description in the plugin settings'
128
+ )
129
+ return
130
+ }
131
+
132
+ if (!this.settings.strapiArticleCreateUrl) {
133
+ new Notice(
134
+ 'Please configure Strapi article create URL in the plugin settings'
135
+ )
136
+ return
137
+ }
138
+
139
+ if (!this.settings.strapiContentAttributeName) {
140
+ new Notice(
141
+ 'Please configure Strapi content attribute name in the plugin settings'
142
+ )
143
+ return
144
+ }
145
+
146
+ /** ****************************************************************************
147
+ * Process the Markdown content
148
+ * *****************************************************************************
149
+ */
150
+ new Notice('All settings are ok, processing Markdown content...')
151
+ const file = activeView.file
152
+ let content = ''
153
+ if (!file) {
154
+ new Notice('No file found in active view...')
155
+ return
156
+ }
157
+ /**
158
+ * Read the content of the file
159
+ */
160
+ content = await this.app.vault.read(file)
161
+
162
+ // check if the content has any images to process
163
+ const flag = this.hasUnexportedImages(content)
164
+ /**
165
+ * Initialize the OpenAI API
166
+ */
167
+ const openai = new OpenAI({
168
+ apiKey: this.settings.openaiApiKey,
169
+ dangerouslyAllowBrowser: true,
170
+ })
171
+
172
+ /**
173
+ * Process the images in the content, upload them to Strapi, and update the links,
174
+ * only if there are images in the content
175
+ * that are not already uploaded to Strapi
176
+ */
177
+ if (flag) {
178
+ /**
179
+ * Extract the image paths from the content
180
+ */
181
+ const imagePaths = this.extractImagePaths(content)
182
+
183
+ /**
184
+ * Get the image blobs from the image paths
185
+ */
186
+ const imageBlobs = await this.getImageBlobs(imagePaths)
187
+
188
+ /**
189
+ * Get the image descriptions using the OpenAI API
190
+ */
191
+ new Notice('Getting image descriptions...')
192
+ const imageDescriptions = await Promise.all(
193
+ imageBlobs.map(async imageBlob => {
194
+ const description = await this.getImageDescription(
195
+ imageBlob.blob,
196
+ openai
197
+ )
198
+ return {
199
+ blob: imageBlob.blob,
200
+ name: imageBlob.name,
201
+ path: imageBlob.path,
202
+ description,
203
+ }
204
+ })
205
+ )
206
+
207
+ /**
208
+ * Upload the images to Strapi
209
+ */
210
+ new Notice('Uploading images to Strapi...')
211
+ const uploadedImages =
212
+ await this.uploadImagesToStrapi(imageDescriptions)
213
+
214
+ /**
215
+ * Replace the image paths in the content with the uploaded image URLs
216
+ */
217
+ new Notice('Replacing image paths...')
218
+ content = this.replaceImagePaths(content, uploadedImages)
219
+ await this.app.vault.modify(file, content)
220
+ new Notice('Images uploaded and links updated successfully!')
221
+ } else {
222
+ new Notice(
223
+ 'No local images found in the content... Skip the image processing...'
224
+ )
225
+ }
226
+
227
+ /**
228
+ * Generate article content using OpenAI
229
+ */
230
+ new Notice('Generating article content...')
231
+ /**
232
+ * Parse the JSON template and description
233
+ */
234
+ const jsonTemplate = JSON.parse(this.settings.jsonTemplate)
235
+ const jsonTemplateDescription = JSON.parse(
236
+ this.settings.jsonTemplateDescription
237
+ )
238
+
239
+ /**
240
+ * If the content is not present, get it from the active view
241
+ */
242
+ content = await this.app.vault.read(file)
243
+
244
+ /**
245
+ * Prompt for generating the article content
246
+ */
247
+ const articlePrompt = `You are an SEO expert. Generate an article based on the following template and field descriptions:
248
+
249
+ Template:
250
+ ${JSON.stringify(jsonTemplate, null, 2)}
251
+
252
+ Field Descriptions:
253
+ ${JSON.stringify(jsonTemplateDescription, null, 2)}
254
+
255
+ The main content of the article should be based on the following text and all the keywords around the domain of the text:
256
+ ----- CONTENT -----
257
+ ${content.substring(0, 500)}
258
+ ----- END CONTENT -----
259
+
260
+ Please provide the generated article content as a JSON object following the given template structure.
261
+
262
+ ${this.settings.additionalPrompt ? `Additional Prompt: ${this.settings.additionalPrompt}` : ''}`
263
+
264
+ /**
265
+ * Generate the article content using OpenAI
266
+ */
267
+ const completion = await openai.chat.completions.create({
268
+ model: 'gpt-3.5-turbo-0125',
269
+ messages: [
270
+ {
271
+ role: 'user',
272
+ content: articlePrompt,
273
+ },
274
+ ],
275
+ max_tokens: 2000,
276
+ n: 1,
277
+ stop: null,
278
+ })
279
+
280
+ /**
281
+ * Parse the generated article content
282
+ */
283
+ let articleContent = JSON.parse(
284
+ completion.choices[0].message.content ?? '{}'
285
+ )
286
+ /**
287
+ * Add the content to the article content
288
+ */
289
+ articleContent = {
290
+ data: {
291
+ ...articleContent.data,
292
+ [this.settings.strapiContentAttributeName]: content,
293
+ },
294
+ }
295
+
296
+ new Notice('Article content generated successfully!')
297
+ try {
298
+ const response = await fetch(this.settings.strapiArticleCreateUrl, {
299
+ method: 'POST',
300
+ headers: {
301
+ 'Content-Type': 'application/json',
302
+ Authorization: `Bearer ${this.settings.strapiApiToken}`,
303
+ },
304
+ body: JSON.stringify(articleContent),
305
+ })
306
+
307
+ if (response.ok) {
308
+ new Notice('Article created successfully in Strapi!')
309
+ } else {
310
+ new Notice('Failed to create article in Strapi.')
311
+ }
312
+ } catch (error) {
313
+ new Notice('Error creating article in Strapi.')
314
+ }
315
+
316
+ new Notice(
317
+ 'Check your API content now, the article is created & uploaded ! 🎉'
318
+ )
319
+ }
320
+ )
321
+ ribbonIconEl.addClass('my-plugin-ribbon-class')
322
+
323
+ this.addSettingTab(new MyExportSettingTab(this.app, this))
324
+ }
325
+
326
+ onunload() {}
327
+
328
+ /**
329
+ * Load the settings for the plugin
330
+ */
331
+ async loadSettings() {
332
+ this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())
333
+ }
334
+
335
+ /**
336
+ * Save the settings for the plugin
337
+ */
338
+ async saveSettings() {
339
+ await this.saveData(this.settings)
340
+ }
341
+
342
+ /**
343
+ * Extract the image paths from the content
344
+ * @param content
345
+ */
346
+ extractImagePaths(content: string): string[] {
347
+ /**
348
+ * Extract the image paths from the content
349
+ */
350
+ const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
351
+ const imagePaths: string[] = []
352
+ let match
353
+
354
+ while ((match = imageRegex.exec(content)) !== null) {
355
+ imagePaths.push(match[1])
356
+ }
357
+
358
+ return imagePaths
359
+ }
360
+
361
+ /**
362
+ * Check if the content has any unexported images
363
+ * @param content
364
+ */
365
+ hasUnexportedImages(content: string): boolean {
366
+ const imageRegex = /!\[\[([^\[\]]*\.(png|jpe?g|gif|bmp|webp))\]\]/gi
367
+ return imageRegex.test(content)
368
+ }
369
+
370
+ /**
371
+ * Get the image blobs from the image paths
372
+ * @param imagePaths
373
+ */
374
+ async getImageBlobs(
375
+ imagePaths: string[]
376
+ ): Promise<{ path: string; blob: Blob; name: string }[]> {
377
+ // get all the files in the vault
378
+ const files = this.app.vault.getAllLoadedFiles()
379
+ // get the image files name from the vault
380
+ const fileNames = files.map(file => file.name)
381
+ // filter the image files, and get all the images files paths
382
+ const imageFiles = imagePaths.filter(path => fileNames.includes(path))
383
+ // get the image blobs, find it, and return the blob
384
+ return await Promise.all(
385
+ imageFiles.map(async path => {
386
+ const file = files.find(file => file.name === path)
387
+ if (file instanceof TFile) {
388
+ const blob = await this.app.vault.readBinary(file)
389
+ return {
390
+ name: path,
391
+ blob: new Blob([blob], { type: 'image/png' }),
392
+ path: file.path,
393
+ }
394
+ }
395
+ return {
396
+ name: '',
397
+ blob: new Blob(),
398
+ path: '',
399
+ }
400
+ })
401
+ )
402
+ }
403
+
404
+ /**
405
+ * Upload the images to Strapi
406
+ * @param imageBlobs
407
+ */
408
+ async uploadImagesToStrapi(
409
+ imageBlobs: {
410
+ path: string
411
+ blob: Blob
412
+ name: string
413
+ description: {
414
+ name: string
415
+ alternativeText: string
416
+ caption: string
417
+ }
418
+ }[]
419
+ ): Promise<{ [key: string]: { url: string; data: any } }> {
420
+ // upload the images to Strapi
421
+ const uploadedImages: {
422
+ [key: string]: { url: string; data: any }
423
+ } = {}
424
+
425
+ /**
426
+ * Upload the images to Strapi
427
+ */
428
+ for (const imageBlob of imageBlobs) {
429
+ const formData = new FormData()
430
+ /**
431
+ * Append the image blob and the image description to the form data
432
+ */
433
+ formData.append('files', imageBlob.blob, imageBlob.name)
434
+ formData.append(
435
+ 'fileInfo',
436
+ JSON.stringify({
437
+ name: imageBlob.description.name,
438
+ alternativeText: imageBlob.description.alternativeText,
439
+ caption: imageBlob.description.caption,
440
+ })
441
+ )
442
+
443
+ // upload the image to Strapi
444
+ try {
445
+ const response = await fetch(`${this.settings.strapiUrl}/api/upload`, {
446
+ method: 'POST',
447
+ headers: {
448
+ Authorization: `Bearer ${this.settings.strapiApiToken}`,
449
+ },
450
+ body: formData,
451
+ })
452
+
453
+ /**
454
+ * If the response is ok, add the uploaded image to the uploaded images object
455
+ */
456
+ if (response.ok) {
457
+ const data = await response.json()
458
+ uploadedImages[imageBlob.name] = {
459
+ url: data[0].url,
460
+ data: data[0],
461
+ }
462
+ } else {
463
+ new Notice(`Failed to upload image: ${imageBlob.name}`)
464
+ }
465
+ } catch (error) {
466
+ new Notice(`Error uploading image: ${imageBlob.name}`)
467
+ }
468
+ }
469
+
470
+ return uploadedImages
471
+ }
472
+
473
+ /**
474
+ * Replace the image paths in the content with the uploaded image URLs
475
+ * @param content
476
+ * @param uploadedImages
477
+ */
478
+ replaceImagePaths(
479
+ content: string,
480
+ uploadedImages: { [key: string]: { url: string; data: any } }
481
+ ): string {
482
+ /**
483
+ * Replace the image paths in the content with the uploaded image URLs
484
+ */
485
+ for (const [localPath, imageData] of Object.entries(uploadedImages)) {
486
+ const markdownImageRegex = new RegExp(`!\\[\\[${localPath}\\]\\]`, 'g')
487
+ content = content.replace(
488
+ markdownImageRegex,
489
+ `![${imageData.data.alternativeText}](${imageData.url})`
490
+ )
491
+ }
492
+ return content
493
+ }
494
+
495
+ /**
496
+ * Get the description of the image using OpenAI
497
+ * @param imageBlob
498
+ * @param openai
499
+ */
500
+ async getImageDescription(imageBlob: Blob, openai: OpenAI) {
501
+ // get the image description using the OpenAI API ( using gpt 4 vision preview model )
502
+ const response = await openai.chat.completions.create({
503
+ model: 'gpt-4-vision-preview',
504
+ messages: [
505
+ {
506
+ role: 'user',
507
+ // @ts-ignore
508
+ content: [
509
+ {
510
+ type: 'text',
511
+ text: `What's in this image? make it simple, i just want the context and an idea(think about alt text)`,
512
+ },
513
+ {
514
+ type: 'image_url',
515
+ // encode imageBlob as base64
516
+ image_url: `data:image/png;base64,${btoa(
517
+ new Uint8Array(await imageBlob.arrayBuffer()).reduce(
518
+ (data, byte) => data + String.fromCharCode(byte),
519
+ ''
520
+ )
521
+ )}`,
522
+ },
523
+ ],
524
+ },
525
+ ],
526
+ })
527
+
528
+ new Notice(response.choices[0].message.content ?? 'no response content...')
529
+ new Notice(
530
+ `prompt_tokens: ${response.usage?.prompt_tokens} // completion_tokens: ${response.usage?.completion_tokens} // total_tokens: ${response.usage?.total_tokens}`
531
+ )
532
+
533
+ // gpt-3.5-turbo-0125
534
+ // alt text, caption, and title for the image, based on the description of the image
535
+ const completion = await openai.chat.completions.create({
536
+ model: 'gpt-3.5-turbo-0125',
537
+ messages: [
538
+ {
539
+ role: 'user',
540
+ content: `You are an SEO expert and you are writing alt text, caption, and title for this image. The description of the image is: ${response.choices[0].message.content}.
541
+ Give me a title (name) for this image, an SEO-friendly alternative text, and a caption for this image.
542
+ Generate this information and respond with a JSON object using the following fields: name, alternativeText, caption.
543
+ Use this JSON template: {"name": "string", "alternativeText": "string", "caption": "string"}.`,
544
+ },
545
+ ],
546
+ max_tokens: 750,
547
+ n: 1,
548
+ stop: null,
549
+ })
550
+
551
+ new Notice(
552
+ completion.choices[0].message.content ?? 'no response content...'
553
+ )
554
+ new Notice(
555
+ `prompt_tokens: ${completion.usage?.prompt_tokens} // completion_tokens: ${completion.usage?.completion_tokens} // total_tokens: ${completion.usage?.total_tokens}`
556
+ )
557
+
558
+ return JSON.parse(completion.choices[0].message.content?.trim() || '{}')
559
+ }
560
+ }
561
+
562
+ /**
563
+ * The settings tab for the plugin
564
+ */
565
+ class MyExportSettingTab extends PluginSettingTab {
566
+ plugin: MyPlugin
567
+
568
+ constructor(app: App, plugin: MyPlugin) {
569
+ super(app, plugin)
570
+ this.plugin = plugin
571
+ }
572
+
573
+ display(): void {
574
+ const { containerEl } = this
575
+ containerEl.empty()
576
+
577
+ /** ****************************************************************************
578
+ * Add the settings for the plugin
579
+ * *****************************************************************************
580
+ */
581
+ new Setting(containerEl)
582
+ .setName('Strapi URL')
583
+ .setDesc('Enter your Strapi instance URL')
584
+ .addText(text =>
585
+ text
586
+ .setPlaceholder('https://your-strapi-url')
587
+ .setValue(this.plugin.settings.strapiUrl)
588
+ .onChange(async value => {
589
+ this.plugin.settings.strapiUrl = value
590
+ await this.plugin.saveSettings()
591
+ })
592
+ )
593
+
594
+ new Setting(containerEl)
595
+ .setName('Strapi API Token')
596
+ .setDesc('Enter your Strapi API token')
597
+ .addText(text =>
598
+ text
599
+ .setPlaceholder('Enter your token')
600
+ .setValue(this.plugin.settings.strapiApiToken)
601
+ .onChange(async value => {
602
+ this.plugin.settings.strapiApiToken = value
603
+ await this.plugin.saveSettings()
604
+ })
605
+ )
606
+
607
+ new Setting(containerEl)
608
+ .setName('OpenAI API Key')
609
+ .setDesc('Enter your OpenAI API key for GPT-3')
610
+ .addText(text =>
611
+ text
612
+ .setPlaceholder('Enter your OpenAI API key')
613
+ .setValue(this.plugin.settings.openaiApiKey)
614
+ .onChange(async value => {
615
+ this.plugin.settings.openaiApiKey = value
616
+ await this.plugin.saveSettings()
617
+ })
618
+ )
619
+
620
+ new Setting(containerEl)
621
+ .setName('JSON Template')
622
+ .setDesc('Enter the JSON template for the fields needed')
623
+ .addTextArea(text =>
624
+ text
625
+ .setPlaceholder('Enter your JSON template')
626
+ .setValue(this.plugin.settings.jsonTemplate)
627
+ .onChange(async value => {
628
+ this.plugin.settings.jsonTemplate = value
629
+ await this.plugin.saveSettings()
630
+ })
631
+ )
632
+
633
+ new Setting(containerEl)
634
+ .setName('JSON Template Description')
635
+ .setDesc('Enter the description for each field in the JSON template')
636
+ .addTextArea(text =>
637
+ text
638
+ .setPlaceholder('Enter the field descriptions')
639
+ .setValue(this.plugin.settings.jsonTemplateDescription)
640
+ .onChange(async value => {
641
+ this.plugin.settings.jsonTemplateDescription = value
642
+ await this.plugin.saveSettings()
643
+ })
644
+ )
645
+
646
+ new Setting(containerEl)
647
+ .setName('Strapi Article Create URL')
648
+ .setDesc('Enter the URL to create articles in Strapi')
649
+ .addText(text =>
650
+ text
651
+ .setPlaceholder('https://your-strapi-url/api/articles')
652
+ .setValue(this.plugin.settings.strapiArticleCreateUrl)
653
+ .onChange(async value => {
654
+ this.plugin.settings.strapiArticleCreateUrl = value
655
+ await this.plugin.saveSettings()
656
+ })
657
+ )
658
+
659
+ new Setting(containerEl)
660
+ .setName('Strapi Content Attribute Name')
661
+ .setDesc('Enter the attribute name for the content field in Strapi')
662
+ .addText(text =>
663
+ text
664
+ .setPlaceholder('content')
665
+ .setValue(this.plugin.settings.strapiContentAttributeName)
666
+ .onChange(async value => {
667
+ this.plugin.settings.strapiContentAttributeName = value
668
+ await this.plugin.saveSettings()
669
+ })
670
+ )
671
+
672
+ new Setting(containerEl)
673
+ .setName('Additional Prompt')
674
+ .setDesc(
675
+ 'Enter an optional additional prompt to customize the article content generation'
676
+ )
677
+ .addTextArea(text =>
678
+ text
679
+ .setPlaceholder('Enter your additional prompt here...')
680
+ .setValue(this.plugin.settings.additionalPrompt)
681
+ .onChange(async value => {
682
+ this.plugin.settings.additionalPrompt = value
683
+ await this.plugin.saveSettings()
684
+ })
685
+ )
686
+ }
687
+ }
package/manifest.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "notes-to-strapi-export-article-ai",
3
+ "name": "Strapi Exporter, Notes to Strapi article AI enhanced",
4
+ "version": "1.0.3",
5
+ "minAppVersion": "1.5.0",
6
+ "description": "Effortlessly export your notes to Strapi CMS with AI-powered image handling and SEO optimization. Replace all the images in your notes by uploaded images in Strapi, and add SEO metadata to uploaded images.",
7
+ "author": "Cinquin Andy",
8
+ "authorUrl": "https://andy.cinquin.com",
9
+ "isDesktopOnly": true,
10
+ "fundingUrl": "https://github.com/sponsors/CinquinAndy"
11
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "notes-to-strapi-export-article-ai",
3
+ "version": "1.0.3",
4
+ "description": "Effortlessly export your Obsidian notes to Strapi CMS with AI-powered image handling and SEO optimization. Replace all the images in your notes by uploaded images in Strapi, and add SEO metadata to uploaded images.",
5
+ "main": "main.js",
6
+ "scripts": {
7
+ "dev": "node esbuild.config.mjs",
8
+ "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9
+ "version": "node version-bump.mjs && git add manifest.json versions.json"
10
+ },
11
+ "keywords": [],
12
+ "author": "Cinquin Andy",
13
+ "devDependencies": {
14
+ "@types/node": "20.11.30",
15
+ "@typescript-eslint/eslint-plugin": "7.3.1",
16
+ "@typescript-eslint/parser": "7.3.1",
17
+ "builtin-modules": "3.3.0",
18
+ "esbuild": "0.20.2",
19
+ "obsidian": "latest",
20
+ "prettier": "3.2.5",
21
+ "tslib": "2.6.2",
22
+ "typescript": "5.4.3"
23
+ },
24
+ "dependencies": {
25
+ "openai": "^4.0.0"
26
+ }
27
+ }
@@ -0,0 +1,19 @@
1
+ module.exports = {
2
+ singleQuote: true,
3
+ semi: false,
4
+ trailingComma: 'es5',
5
+ tabWidth: 2,
6
+ useTabs: true,
7
+ printWidth: 80,
8
+ arrowParens: 'avoid',
9
+ bracketSpacing: true,
10
+ jsxSingleQuote: false,
11
+ proseWrap: 'preserve',
12
+ quoteProps: 'as-needed',
13
+ requirePragma: false,
14
+ insertPragma: false,
15
+ endOfLine: 'lf',
16
+ htmlWhitespaceSensitivity: 'css',
17
+ vueIndentScriptAndStyle: false,
18
+ embeddedLanguageFormatting: 'auto',
19
+ }
package/renovate.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:base"
5
+ ],
6
+ "packageRules": [
7
+ {
8
+ "matchUpdateTypes": [
9
+ "minor",
10
+ "patch"
11
+ ],
12
+ "matchCurrentVersion": "!/^0/",
13
+ "automerge": true,
14
+ "automergeType": "pr",
15
+ "automergeStrategy": "squash"
16
+ }
17
+ ]
18
+ }
package/styles.css ADDED
@@ -0,0 +1,8 @@
1
+ /*
2
+
3
+ This CSS file will be included with your plugin, and
4
+ available in the app when your plugin is enabled.
5
+
6
+ If your plugin does not need CSS, delete this file.
7
+
8
+ */
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "inlineSourceMap": true,
5
+ "inlineSources": true,
6
+ "module": "ESNext",
7
+ "target": "ES6",
8
+ "allowJs": true,
9
+ "noImplicitAny": false,
10
+ "moduleResolution": "node",
11
+ "importHelpers": true,
12
+ "isolatedModules": true,
13
+ "strictNullChecks": true,
14
+ "lib": [
15
+ "DOM",
16
+ "ES5",
17
+ "ES6",
18
+ "ES7"
19
+ ]
20
+ },
21
+ "include": [
22
+ "**/*.ts"
23
+ ]
24
+ }
@@ -0,0 +1,14 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+
3
+ const targetVersion = process.env.npm_package_version;
4
+
5
+ // read minAppVersion from manifest.json and bump version to target version
6
+ let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7
+ const { minAppVersion } = manifest;
8
+ manifest.version = targetVersion;
9
+ writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10
+
11
+ // update versions.json with target version and minAppVersion from manifest.json
12
+ let versions = JSON.parse(readFileSync("versions.json", "utf8"));
13
+ versions[targetVersion] = minAppVersion;
14
+ writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
package/versions.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "1.0.2": "1.0.2"
3
+ }