Renaming plugin.
This commit is contained in:
26
README.md
26
README.md
@@ -1,4 +1,4 @@
|
|||||||
# discourse-url-to-article
|
# discourse-bookmark-url
|
||||||
|
|
||||||
A Discourse plugin that detects when a URL is pasted into the **topic title** field and offers to scrape the page, extracting the article content (à la browser Reader Mode) and populating the **composer body** with a clean Markdown rendering.
|
A Discourse plugin that detects when a URL is pasted into the **topic title** field and offers to scrape the page, extracting the article content (à la browser Reader Mode) and populating the **composer body** with a clean Markdown rendering.
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ hooks:
|
|||||||
- exec:
|
- exec:
|
||||||
cd: $home/plugins
|
cd: $home/plugins
|
||||||
cmd:
|
cmd:
|
||||||
- git clone https://code.draft13.com/robert/discourse-url-to-article.git
|
- git clone https://code.draft13.com/robert/discourse-bookmark-url.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then rebuild: `./launcher rebuild app`
|
Then rebuild: `./launcher rebuild app`
|
||||||
@@ -36,12 +36,12 @@ Then rebuild: `./launcher rebuild app`
|
|||||||
|
|
||||||
| Setting | Default | Description |
|
| Setting | Default | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `url_to_article_enabled` | `true` | Enable/disable the plugin |
|
| `bookmark_url_enabled` | `true` | Enable/disable the plugin |
|
||||||
| `url_to_article_auto_populate` | `false` | Populate body automatically without button click |
|
| `bookmark_url_auto_populate` | `false` | Populate body automatically without button click |
|
||||||
| `url_to_article_max_content_length` | `50000` | Max chars extracted from a page |
|
| `bookmark_url_max_content_length` | `50000` | Max chars extracted from a page |
|
||||||
| `url_to_article_fetch_timeout` | `10` | Seconds before HTTP fetch times out |
|
| `bookmark_url_fetch_timeout` | `10` | Seconds before HTTP fetch times out |
|
||||||
| `url_to_article_allowed_domains` | *(blank = all)* | Comma-separated domain allowlist |
|
| `bookmark_url_allowed_domains` | *(blank = all)* | Comma-separated domain allowlist |
|
||||||
| `url_to_article_blocked_domains` | `localhost,127.0.0.1,…` | SSRF blocklist |
|
| `bookmark_url_blocked_domains` | `localhost,127.0.0.1,…` | SSRF blocklist |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,15 +49,15 @@ Then rebuild: `./launcher rebuild app`
|
|||||||
|
|
||||||
### Frontend (Ember.js)
|
### Frontend (Ember.js)
|
||||||
|
|
||||||
`initializers/url-to-article.js` hooks into the `composer-editor` component and observes the `composer.model.title` property via Ember's observer system. When the title matches a bare URL pattern:
|
`initializers/bookmark-url.js` hooks into the `composer-editor` component and observes the `composer.model.title` property via Ember's observer system. When the title matches a bare URL pattern:
|
||||||
|
|
||||||
1. A dismissible bar appears above the editor offering to import the article.
|
1. A dismissible bar appears above the editor offering to import the article.
|
||||||
2. On click (or automatically if `auto_populate` is on), it POSTs to `/url-to-article/extract`.
|
2. On click (or automatically if `auto_populate` is on), it POSTs to `/bookmark-url/extract`.
|
||||||
3. The response populates `composer.model.reply` (body) and optionally updates the title.
|
3. The response populates `composer.model.reply` (body) and optionally updates the title.
|
||||||
|
|
||||||
### Backend (Ruby)
|
### Backend (Ruby)
|
||||||
|
|
||||||
`ArticleExtractor` in `lib/url_to_article/article_extractor.rb`:
|
`ArticleExtractor` in `lib/bookmark_url/article_extractor.rb`:
|
||||||
|
|
||||||
1. **Fetches** the HTML via `Net::HTTP` with a browser-like User-Agent (follows one redirect).
|
1. **Fetches** the HTML via `Net::HTTP` with a browser-like User-Agent (follows one redirect).
|
||||||
2. **Extracts metadata** from Open Graph / Twitter Card / standard `<meta>` tags.
|
2. **Extracts metadata** from Open Graph / Twitter Card / standard `<meta>` tags.
|
||||||
@@ -67,7 +67,7 @@ Then rebuild: `./launcher rebuild app`
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Only authenticated users can call `/url-to-article/extract`.
|
- Only authenticated users can call `/bookmark-url/extract`.
|
||||||
- Only `http`/`https` schemes are allowed.
|
- Only `http`/`https` schemes are allowed.
|
||||||
- Configurable domain blocklist (loopback/private addresses blocked by default).
|
- Configurable domain blocklist (loopback/private addresses blocked by default).
|
||||||
- Optional allowlist to restrict to specific domains.
|
- Optional allowlist to restrict to specific domains.
|
||||||
@@ -97,7 +97,7 @@ Full article text in Markdown...
|
|||||||
|
|
||||||
### Custom extraction logic
|
### Custom extraction logic
|
||||||
|
|
||||||
Subclass or monkey-patch `UrlToArticle::ArticleExtractor` in a separate plugin to add site-specific selectors or post-processing.
|
Subclass or monkey-patch `BookmarkUrl::ArticleExtractor` in a separate plugin to add site-specific selectors or post-processing.
|
||||||
|
|
||||||
### Paywall / JS-rendered sites
|
### Paywall / JS-rendered sites
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module UrlToArticle
|
module BookmarkUrl
|
||||||
class ArticlesController < ::ApplicationController
|
class ArticlesController < ::ApplicationController
|
||||||
requires_login
|
requires_login
|
||||||
before_action :ensure_enabled!
|
before_action :ensure_enabled!
|
||||||
@@ -18,14 +18,14 @@ module UrlToArticle
|
|||||||
url: result.url,
|
url: result.url,
|
||||||
}
|
}
|
||||||
rescue => e
|
rescue => e
|
||||||
Rails.logger.warn("[url-to-article] Extraction failed for #{@url}: #{e.message}")
|
Rails.logger.warn("[bookmark-url] Extraction failed for #{@url}: #{e.message}")
|
||||||
render json: { error: "Could not extract article: #{e.message}" }, status: :unprocessable_entity
|
render json: { error: "Could not extract article: #{e.message}" }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def ensure_enabled!
|
def ensure_enabled!
|
||||||
raise Discourse::NotFound unless SiteSetting.url_to_article_enabled
|
raise Discourse::NotFound unless SiteSetting.bookmark_url_enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_url!
|
def validate_url!
|
||||||
@@ -42,7 +42,7 @@ module UrlToArticle
|
|||||||
end
|
end
|
||||||
|
|
||||||
# SSRF protection — block private/loopback addresses
|
# SSRF protection — block private/loopback addresses
|
||||||
blocked_domains = SiteSetting.url_to_article_blocked_domains
|
blocked_domains = SiteSetting.bookmark_url_blocked_domains
|
||||||
.split(",").map(&:strip).reject(&:empty?)
|
.split(",").map(&:strip).reject(&:empty?)
|
||||||
|
|
||||||
if blocked_domains.any? { |d| uri.host&.include?(d) }
|
if blocked_domains.any? { |d| uri.host&.include?(d) }
|
||||||
@@ -50,7 +50,7 @@ module UrlToArticle
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Optionally enforce an allowlist
|
# Optionally enforce an allowlist
|
||||||
allowed_domains = SiteSetting.url_to_article_allowed_domains
|
allowed_domains = SiteSetting.bookmark_url_allowed_domains
|
||||||
.split(",").map(&:strip).reject(&:empty?)
|
.split(",").map(&:strip).reject(&:empty?)
|
||||||
|
|
||||||
if allowed_domains.any? && !allowed_domains.any? { |d| uri.host&.end_with?(d) }
|
if allowed_domains.any? && !allowed_domains.any? { |d| uri.host&.end_with?(d) }
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ const STRINGS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default apiInitializer("1.8.0", (api) => {
|
export default apiInitializer("1.8.0", (api) => {
|
||||||
if (!api.container.lookup("site-settings:main").url_to_article_enabled) {
|
if (!api.container.lookup("site-settings:main").bookmark_url_enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
api.modifyClass("component:composer-editor", {
|
api.modifyClass("component:composer-editor", {
|
||||||
pluginId: "url-to-article",
|
pluginId: "bookmark-url",
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
@@ -75,30 +75,30 @@ export default apiInitializer("1.8.0", (api) => {
|
|||||||
this._hideArticleBar();
|
this._hideArticleBar();
|
||||||
|
|
||||||
const bar = document.createElement("div");
|
const bar = document.createElement("div");
|
||||||
bar.className = "url-to-article-bar";
|
bar.className = "bookmark-url-bar";
|
||||||
bar.innerHTML = `
|
bar.innerHTML = `
|
||||||
<span class="url-to-article-icon">📄</span>
|
<span class="bookmark-url-icon">📄</span>
|
||||||
<span class="url-to-article-label">${STRINGS.bar_label}</span>
|
<span class="bookmark-url-label">${STRINGS.bar_label}</span>
|
||||||
<button class="btn btn-small btn-primary url-to-article-btn">
|
<button class="btn btn-small btn-primary bookmark-url-btn">
|
||||||
${STRINGS.fetch_button}
|
${STRINGS.fetch_button}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-small btn-default url-to-article-onebox-btn">
|
<button class="btn btn-small btn-default bookmark-url-onebox-btn">
|
||||||
${STRINGS.onebox_button}
|
${STRINGS.onebox_button}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-small btn-flat url-to-article-dismiss"
|
<button class="btn btn-small btn-flat bookmark-url-dismiss"
|
||||||
aria-label="${STRINGS.dismiss}">✕</button>
|
aria-label="${STRINGS.dismiss}">✕</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
bar.querySelector(".url-to-article-btn").addEventListener("click", () => {
|
bar.querySelector(".bookmark-url-btn").addEventListener("click", () => {
|
||||||
this._fetchAndPopulate(url);
|
this._fetchAndPopulate(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
bar.querySelector(".url-to-article-onebox-btn").addEventListener("click", () => {
|
bar.querySelector(".bookmark-url-onebox-btn").addEventListener("click", () => {
|
||||||
this._hideArticleBar();
|
this._hideArticleBar();
|
||||||
this._commitUrlToModel();
|
this._commitUrlToModel();
|
||||||
});
|
});
|
||||||
|
|
||||||
bar.querySelector(".url-to-article-dismiss").addEventListener("click", () => {
|
bar.querySelector(".bookmark-url-dismiss").addEventListener("click", () => {
|
||||||
this._hideArticleBar();
|
this._hideArticleBar();
|
||||||
this._pendingUrl = null;
|
this._pendingUrl = null;
|
||||||
});
|
});
|
||||||
@@ -113,21 +113,21 @@ export default apiInitializer("1.8.0", (api) => {
|
|||||||
|
|
||||||
_hideArticleBar() {
|
_hideArticleBar() {
|
||||||
this.element
|
this.element
|
||||||
?.querySelectorAll(".url-to-article-bar")
|
?.querySelectorAll(".bookmark-url-bar")
|
||||||
.forEach((el) => el.remove());
|
.forEach((el) => el.remove());
|
||||||
},
|
},
|
||||||
|
|
||||||
_setStatus(message, type = "info") {
|
_setStatus(message, type = "info") {
|
||||||
const bar = this.element?.querySelector(".url-to-article-bar");
|
const bar = this.element?.querySelector(".bookmark-url-bar");
|
||||||
if (!bar) return;
|
if (!bar) return;
|
||||||
let status = bar.querySelector(".url-to-article-status");
|
let status = bar.querySelector(".bookmark-url-status");
|
||||||
if (!status) {
|
if (!status) {
|
||||||
status = document.createElement("span");
|
status = document.createElement("span");
|
||||||
status.className = "url-to-article-status";
|
status.className = "bookmark-url-status";
|
||||||
bar.appendChild(status);
|
bar.appendChild(status);
|
||||||
}
|
}
|
||||||
status.textContent = message;
|
status.textContent = message;
|
||||||
status.className = `url-to-article-status url-to-article-status--${type}`;
|
status.className = `bookmark-url-status bookmark-url-status--${type}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Release the URL into Ember's data-binding so Discourse handles it normally
|
// Release the URL into Ember's data-binding so Discourse handles it normally
|
||||||
@@ -143,8 +143,8 @@ export default apiInitializer("1.8.0", (api) => {
|
|||||||
// ---- Fetch & populate ---------------------------------------------
|
// ---- Fetch & populate ---------------------------------------------
|
||||||
|
|
||||||
async _fetchAndPopulate(url) {
|
async _fetchAndPopulate(url) {
|
||||||
const bar = this.element?.querySelector(".url-to-article-bar");
|
const bar = this.element?.querySelector(".bookmark-url-bar");
|
||||||
const btn = bar?.querySelector(".url-to-article-btn");
|
const btn = bar?.querySelector(".bookmark-url-btn");
|
||||||
|
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -153,7 +153,7 @@ export default apiInitializer("1.8.0", (api) => {
|
|||||||
this._setStatus(STRINGS.fetching, "info");
|
this._setStatus(STRINGS.fetching, "info");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await ajax("/url-to-article/extract", {
|
const data = await ajax("/bookmark-url/extract", {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: { url },
|
data: { url },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* URL-to-Article plugin styles */
|
/* Bookmark URL plugin styles */
|
||||||
|
|
||||||
.url-to-article-bar {
|
.bookmark-url-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -18,39 +18,39 @@
|
|||||||
order: -1;
|
order: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-to-article-icon {
|
.bookmark-url-icon {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-to-article-label {
|
.bookmark-url-label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 8rem;
|
min-width: 8rem;
|
||||||
color: var(--primary-medium);
|
color: var(--primary-medium);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-to-article-btn {
|
.bookmark-url-btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-to-article-dismiss {
|
.bookmark-url-dismiss {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 0.25rem 0.5rem !important;
|
padding: 0.25rem 0.5rem !important;
|
||||||
color: var(--primary-medium) !important;
|
color: var(--primary-medium) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url-to-article-status {
|
.bookmark-url-status {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: var(--font-down-1);
|
font-size: var(--font-down-1);
|
||||||
|
|
||||||
&.url-to-article-status--info {
|
&.bookmark-url-status--info {
|
||||||
color: var(--tertiary);
|
color: var(--tertiary);
|
||||||
}
|
}
|
||||||
&.url-to-article-status--success {
|
&.bookmark-url-status--success {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
&.url-to-article-status--error {
|
&.bookmark-url-status--error {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
en:
|
en:
|
||||||
url_to_article:
|
bookmark_url:
|
||||||
bar_label: "URL detected — import as article?"
|
bar_label: "URL detected — import as article?"
|
||||||
fetch_button: "Import Article"
|
fetch_button: "Import Article"
|
||||||
onebox_button: "Use Onebox"
|
onebox_button: "Use Onebox"
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
plugins:
|
plugins:
|
||||||
url_to_article_enabled:
|
bookmark_url_enabled:
|
||||||
default: true
|
default: true
|
||||||
client: true
|
client: true
|
||||||
type: bool
|
type: bool
|
||||||
|
|
||||||
url_to_article_auto_populate:
|
bookmark_url_auto_populate:
|
||||||
default: false
|
default: false
|
||||||
client: true
|
client: true
|
||||||
type: bool
|
type: bool
|
||||||
description: "Automatically populate the body when a URL is detected in the title (no button click needed)"
|
description: "Automatically populate the body when a URL is detected in the title (no button click needed)"
|
||||||
|
|
||||||
url_to_article_max_content_length:
|
bookmark_url_max_content_length:
|
||||||
default: 50000
|
default: 50000
|
||||||
type: integer
|
type: integer
|
||||||
description: "Maximum number of characters to extract from a page"
|
description: "Maximum number of characters to extract from a page"
|
||||||
|
|
||||||
url_to_article_fetch_timeout:
|
bookmark_url_fetch_timeout:
|
||||||
default: 10
|
default: 10
|
||||||
type: integer
|
type: integer
|
||||||
description: "Seconds to wait when fetching a URL"
|
description: "Seconds to wait when fetching a URL"
|
||||||
|
|
||||||
url_to_article_allowed_domains:
|
bookmark_url_allowed_domains:
|
||||||
default: ""
|
default: ""
|
||||||
type: string
|
type: string
|
||||||
description: "Comma-separated list of allowed domains. Leave blank to allow all."
|
description: "Comma-separated list of allowed domains. Leave blank to allow all."
|
||||||
|
|
||||||
url_to_article_blocked_domains:
|
bookmark_url_blocked_domains:
|
||||||
default: "localhost,127.0.0.1,0.0.0.0,::1"
|
default: "localhost,127.0.0.1,0.0.0.0,::1"
|
||||||
type: string
|
type: string
|
||||||
description: "Comma-separated list of blocked domains (SSRF protection)"
|
description: "Comma-separated list of blocked domains (SSRF protection)"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ require "net/http"
|
|||||||
require "uri"
|
require "uri"
|
||||||
require "timeout"
|
require "timeout"
|
||||||
|
|
||||||
module UrlToArticle
|
module BookmarkUrl
|
||||||
class ArticleExtractor
|
class ArticleExtractor
|
||||||
NOISE_SELECTORS = %w[
|
NOISE_SELECTORS = %w[
|
||||||
script style noscript iframe nav footer header
|
script style noscript iframe nav footer header
|
||||||
@@ -80,7 +80,7 @@ module UrlToArticle
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def fetch_html
|
def fetch_html
|
||||||
Timeout.timeout(SiteSetting.url_to_article_fetch_timeout) do
|
Timeout.timeout(SiteSetting.bookmark_url_fetch_timeout) do
|
||||||
response = do_get(@uri)
|
response = do_get(@uri)
|
||||||
|
|
||||||
if response.is_a?(Net::HTTPRedirection) && response["location"]
|
if response.is_a?(Net::HTTPRedirection) && response["location"]
|
||||||
@@ -100,10 +100,10 @@ module UrlToArticle
|
|||||||
http = Net::HTTP.new(uri.host, uri.port)
|
http = Net::HTTP.new(uri.host, uri.port)
|
||||||
http.use_ssl = uri.scheme == "https"
|
http.use_ssl = uri.scheme == "https"
|
||||||
http.open_timeout = 5
|
http.open_timeout = 5
|
||||||
http.read_timeout = SiteSetting.url_to_article_fetch_timeout
|
http.read_timeout = SiteSetting.bookmark_url_fetch_timeout
|
||||||
|
|
||||||
req = Net::HTTP::Get.new(uri.request_uri)
|
req = Net::HTTP::Get.new(uri.request_uri)
|
||||||
req["User-Agent"] = "Mozilla/5.0 (compatible; Discourse/url-to-article)"
|
req["User-Agent"] = "Mozilla/5.0 (compatible; Discourse/bookmark-url)"
|
||||||
req["Accept"] = "text/html,application/xhtml+xml"
|
req["Accept"] = "text/html,application/xhtml+xml"
|
||||||
req["Accept-Language"] = "en-US,en;q=0.9"
|
req["Accept-Language"] = "en-US,en;q=0.9"
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ module UrlToArticle
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def truncate(text)
|
def truncate(text)
|
||||||
max = SiteSetting.url_to_article_max_content_length
|
max = SiteSetting.bookmark_url_max_content_length
|
||||||
return text if text.length <= max
|
return text if text.length <= max
|
||||||
text[0...max] + "\n\n*[Content truncated — visit the original article for the full text.]*"
|
text[0...max] + "\n\n*[Content truncated — visit the original article for the full text.]*"
|
||||||
end
|
end
|
||||||
|
|||||||
20
plugin.rb
20
plugin.rb
@@ -1,32 +1,32 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# name: discourse-url-to-article
|
# name: discourse-bookmark-url
|
||||||
# about: Scrapes a URL pasted into the topic title and populates the composer body with the article content
|
# about: Scrapes a URL pasted into the topic title and populates the composer body with the article content
|
||||||
# version: 0.1.0
|
# version: 0.1.0
|
||||||
# authors: Robert Johnson
|
# authors: Robert Johnson
|
||||||
# url: https://code.draft13.com/robert/discourse-url-to-article
|
# url: https://code.draft13.com/robert/discourse-bookmark-url
|
||||||
|
|
||||||
enabled_site_setting :url_to_article_enabled
|
enabled_site_setting :bookmark_url_enabled
|
||||||
|
|
||||||
after_initialize do
|
after_initialize do
|
||||||
require_relative "lib/url_to_article/article_extractor"
|
require_relative "lib/bookmark_url/article_extractor"
|
||||||
|
|
||||||
module ::UrlToArticle
|
module ::BookmarkUrl
|
||||||
PLUGIN_NAME = "discourse-url-to-article"
|
PLUGIN_NAME = "discourse-bookmark-url"
|
||||||
|
|
||||||
class Engine < ::Rails::Engine
|
class Engine < ::Rails::Engine
|
||||||
engine_name PLUGIN_NAME
|
engine_name PLUGIN_NAME
|
||||||
isolate_namespace UrlToArticle
|
isolate_namespace BookmarkUrl
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require_relative "app/controllers/url_to_article/articles_controller"
|
require_relative "app/controllers/bookmark_url/articles_controller"
|
||||||
|
|
||||||
UrlToArticle::Engine.routes.draw do
|
BookmarkUrl::Engine.routes.draw do
|
||||||
post "/extract" => "articles#extract"
|
post "/extract" => "articles#extract"
|
||||||
end
|
end
|
||||||
|
|
||||||
Discourse::Application.routes.append do
|
Discourse::Application.routes.append do
|
||||||
mount UrlToArticle::Engine, at: "/url-to-article"
|
mount BookmarkUrl::Engine, at: "/bookmark-url"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user