From 4d2528ad4aced5c8163880ce7aa94e3e89d96238 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 1 Jul 2021 22:07:15 +0200 Subject: [PATCH 1/7] docs: Document URL Reader Concept Interface Using inside a plugin Writing a new one Signed-off-by: Himanshu Mishra --- docs/integrations/url-reader.md | 219 ++++++++++++++++++++++++++++++++ microsite/sidebars.json | 3 +- mkdocs.yml | 1 + 3 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 docs/integrations/url-reader.md diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md new file mode 100644 index 0000000000..e9d6caf17c --- /dev/null +++ b/docs/integrations/url-reader.md @@ -0,0 +1,219 @@ +--- +id: url-reader +title: URL Reader +sidebar_label: URL Reader +# prettier-ignore +description: URL Reader is a backend core API responsible for reading files from external locations. +--- + +## Concept + +Some of the core plugins of Backstage have to read files from an external +location. [Software Catalog](../features/software-catalog/index.md) has to read +the [`catalog-info.yaml`](../features/software-catalog/descriptor-format.md) +entity descriptor files to register and track an entity. +[Software Templates](../features/software-templates/index.md) have to download +the template skeleton files before creating a new component. +[TechDocs](../features/techdocs/README.md) has to download the markdown source +files before generating a documentation site. + +Since, the requirement for reading files is so essential for Backstage plugins, +the +[`@backstage/backend-common`](https://github.com/backstage/backstage/tree/master/packages/backend-common) +package provides a dedicated API for reading from such URL based remote +locations like GitHub, GitLab, Bitbucket, Google Cloud Storage, etc. This is +commonly referred to as "URL Reader". It takes care of making authenticated +requests to the remote host so that private files can be read securely. If users +have [GitHub App based authentication](../plugins/github-apps.md) set up, URL +Reader even refreshes the token, to avoid reaching the GitHub API rate limit. + +As a result, plugin authors do not have to worry about any of these problems +when trying to read files. + +## Interface + +When the Backstage backend starts, a new instance of URL Reader is created. You +can see this in the index file of your Backstage backend +i.e.`packages/backend/src/index.ts`. +[Example](https://github.com/backstage/backstage/blob/ebbe91dbe79038a61d35cf6ed2d96e0e0d5a15f3/packages/backend/src/index.ts#L57) + +```ts +// File: packages/backend/src/index.ts + +import { URLReaders } from '@backstage/backend-common'; + +function makeCreateEnv(config: Config) { + // .... + const reader = UrlReaders.default({ logger, config }); + // +} +``` + +This instance contains all +[the default URL Reader providers](https://github.com/backstage/backstage/blob/master/packages/backend-common/src/reading/UrlReaders.ts) +in the backend-common package including GitHub, Gitlab, Bitbucket, Azure, Google +GCS. As the need arises, more URL Readers are being written to support different +providers. + +The generic interface of a URL Reader instance looks like this. + +```ts +export type UrlReader = { + /* Used to read a single file and return its content. */ + read(url: string): Promise; + /* Used to read a file tree and download as a directory. */ + readTree(url: string, options?: ReadTreeOptions): Promise; + /* Used to search a file in a tree. */ + search(url: string, options?: SearchOptions): Promise; +}; +``` + +## Using inside a plugin + +The `reader` instance is available in the backend plugin environment and passed +on to all the backend plugins. You can see an +[example](https://github.com/backstage/backstage/blob/b0be185369ebaad22255b7cdf18535d1d4ffd0e7/packages/backend/src/plugins/techdocs.ts#L31). +When any of the methods on this instance is called with a URL, URL Reader +extracts the host for that URL (e.g. `github.com`, `ghe.mycompany.com`, etc.). +Using the +[`@backstage/integration`](https://github.com/backstage/backstage/tree/master/packages/integration) +package, it looks inside the +[`integrations:`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/app-config.yaml#L134-L158) +config of the `app-config.yaml` to find out how to work with the host based on +the configs provided like authentication token, API base URL, etc. + +Make sure your plugin-specific backend file at +`packages/backend/src/plugins/.ts` is forwarding the `reader` instance +passed on as the `PluginEnvironment` to the actual plugin's `createRouter` +function. See how this is done in +[Catalog](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend/src/plugins/catalog.ts#L25-L27) +and +[TechDocs](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend/src/plugins/techdocs.ts#L31-L36) +backend plugins. + +Once the reader instance is available inside the plugin, one of its methods can +directly be used with a URL. Some example usages - + +- [`read`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/plugins/catalog-backend/src/ingestion/processors/codeowners/read.ts#L24-L33) - + Catalog using the `read` method to read the CODEOWNERS file in a repository. +- [`readTree`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/techdocs-common/src/helpers.ts#L198-L220) - + TechDocs using the `readTree` method to download markdown files in order to + generate the documentation site. +- [`readTree`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/techdocs-common/src/stages/prepare/url.ts#L33-L54) - + TechDocs using `NotModifiedError` to maintain cache and speed up and limit the + number of requests. +- [`search`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts#L88-L108) - + Catalog using the `search` method to find files for a location URL containing + a glob pattern. + +## Writing a new URL Reader + +If the available URL Readers are not sufficient for your use case and you want +to add a new URL Reader for any other provider, you are most welcome to +contribute one! + +Feel free to use the +[GitHub URL Reader](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/GithubUrlReader.ts) +as a source of inspiration. + +### Add an integration + +The provider for your new URL Reader can also be called an "integration" in +Backstage. The `integrations:` section of your Backstage `app-config.yaml` +config file is supposed to be the place where a Backstage integrator defines the +host URL for the integration, authentication details and other integration +related configurations. + +The `@backstage/integration` package is where most of the integration specific +code lives, so that it is shareable across Backstage. Functions like "read the +integrations config and process it", "construct headers for authenticated +requests to the host" or "convert a plain file URL into its API URL for +downloading the file" would live in this package. + +### Create the URL Reader + +Create a new class which implements the +[`UrlReader` type](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/types.ts#L21-L28) +inside `@backstage/backend-common`. Create and export a static `factory` method +which reads the integration config and returns a map of host URLs the new reader +should be used for. See the +[GitHub URL Reader](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/GithubUrlReader.ts#L50-L63) +for example. + +### Implement the methods + +We want to make sure all URL Readers behave in the same way. Hence if possible, +all the methods of the `UrlReader` interface should be implemented. However it +is okay to start by implementing just one of them and create issues for the +remaining. + +#### read + +`read` method expects a user-friendly URL, something which can be copied from +the browser naturally when a person is browsing the provider in their browser. + +- ✅ Valid URL : + `https://github.com/backstage/backstage/blob/master/ADOPTERS.md` +- ❌ Not a valid URL : + `https://raw.githubusercontent.com/backstage/backstage/master/ADOPTERS.md` +- ❌ Not a valid URL : `https://github.com/backstage/backstage/ADOPTERS.md` + +Upon receiving the URL, `read` converts the user-friendly URL into an API URL +which can be used to request the provider's API. + +`read` then makes an authenticated request to the provider API and returns the +file's content. + +#### readTree + +`readTree` method also expects user-friendly URLs similar to `read` but the URL +should point to a tree (could be the root of a repository or even a +sub-directory). + +- ✅ Valid URL : `https://github.com/backstage/backstage` +- ✅ Valid URL : `https://github.com/backstage/backstage/blob/master` +- ✅ Valid URL : `https://github.com/backstage/backstage/blob/master/docs` + +Using the provider's API documentation, find out an API endpoint which can be +used to download either a zip or a tarball. You can download the entire tree +(e.g. a repository) and filter out in case the user is expecting only a +sub-tree. But some APIs are smart enough to accept a path and return only a +sub-tree in the downloaded archive. + +#### search + +`search` method expects a glob pattern of a URL and returns a list of files +matching the query. + +- ✅ Valid URL : + `https://github.com/backstage/backstage/blob/master/**/catalog-info.yaml` +- ✅ Valid URL : `https://github.com/backstage/backstage/blob/master/**/*.md` +- ✅ Valid URL : + `https://github.com/backstage/backstage/blob/master/*/package.json` +- ✅ Valid URL : `https://github.com/backstage/backstage/blob/master/READM` + +The core logic of `readTree` can be used here to extract all the files inside +the tree and return the files matching the pattern in the `url`. + +### Caching + +All of the methods above support an ETag based caching (except `read` which is +work in progress!). If the method is called without an `etag`, the response +contains an ETag of the resource (should ideally forward the ETag returned by +the provider). If the method is called with an `etag`, it first compares the +ETag and returns a `NotModifiedError` in case the resource has not been +modified. This approach is very similar to the actual +[ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and +[If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) +HTTP headers. + +### Debugging + +When debugging one of the URL Readers, you can straightforward use the +[`reader` instance created](https://github.com/backstage/backstage/blob/ebbe91dbe79038a61d35cf6ed2d96e0e0d5a15f3/packages/backend/src/index.ts#L57) +when the backend starts and call one of the methods with your debugging URL. +This will be run every time you restart the backend. Note that after any change +in the URL Reader code, you need to kill the backend and restart, since the +`reader` instance is memoized and does not update on hot module reloading. Also, +there are a lot of unit tests written for the URL Readers, which you can make +use of. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 0231d8f692..5530fc6f61 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -149,7 +149,8 @@ "type": "subcategory", "label": "LDAP", "ids": ["integrations/ldap/org"] - } + }, + "integrations/url-reader" ], "Plugins": [ "plugins/index", diff --git a/mkdocs.yml b/mkdocs.yml index c3d77e179f..47b7084146 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -98,6 +98,7 @@ nav: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: - Org Data: 'integrations/ldap/org.md' + - URL Reader: 'integrations/url-reader.md' - Plugins: - Intro to plugins: 'plugins/index.md' - Existing plugins: 'plugins/existing-plugins.md' From 31621bee6943d38daad97112781d81933a0ccf83 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 1 Jul 2021 22:07:43 +0200 Subject: [PATCH 2/7] backend-common: Update doc strings of URL Reader interface methods Signed-off-by: Himanshu Mishra --- packages/backend-common/src/reading/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index ecaa582612..8efc833ead 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -22,6 +22,7 @@ import { Config } from '@backstage/config'; * A generic interface for fetching plain data from URLs. */ export type UrlReader = { + /* Used to read a single file and return its content. */ read(url: string): Promise; /** @@ -32,8 +33,9 @@ export type UrlReader = { */ readUrl?(url: string, options?: ReadUrlOptions): Promise; + /* Used to read a file tree and download as a directory. */ readTree(url: string, options?: ReadTreeOptions): Promise; - + /* Used to search a file in a tree using a glob pattern. */ search(url: string, options?: SearchOptions): Promise; }; From ce338869136eb554c2560edba9fd584d21c17bcb Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 1 Jul 2021 22:12:45 +0200 Subject: [PATCH 3/7] docs: fix vale spelling errors Signed-off-by: Himanshu Mishra --- .github/styles/vocab.txt | 1 + docs/integrations/url-reader.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 846e050c49..736b654486 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -137,6 +137,7 @@ maintainership makefile md memcache +memoized microservice microservices microsite diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md index e9d6caf17c..23500a3dcb 100644 --- a/docs/integrations/url-reader.md +++ b/docs/integrations/url-reader.md @@ -51,7 +51,7 @@ function makeCreateEnv(config: Config) { This instance contains all [the default URL Reader providers](https://github.com/backstage/backstage/blob/master/packages/backend-common/src/reading/UrlReaders.ts) -in the backend-common package including GitHub, Gitlab, Bitbucket, Azure, Google +in the backend-common package including GitHub, GitLab, Bitbucket, Azure, Google GCS. As the need arises, more URL Readers are being written to support different providers. From 7edef795a391e546e6ad2ccfb0ce5c18321c65cc Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 2 Jul 2021 16:00:56 +0200 Subject: [PATCH 4/7] docs: Add a note about readUrl with read Signed-off-by: Himanshu Mishra --- docs/integrations/url-reader.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md index 23500a3dcb..83f6a743d9 100644 --- a/docs/integrations/url-reader.md +++ b/docs/integrations/url-reader.md @@ -149,6 +149,8 @@ remaining. #### read +NOTE: Use `readUrl` instead of `read`. + `read` method expects a user-friendly URL, something which can be copied from the browser naturally when a person is browsing the provider in their browser. @@ -164,6 +166,13 @@ which can be used to request the provider's API. `read` then makes an authenticated request to the provider API and returns the file's content. +#### readUrl + +`readUrl` is a new interface that allows complex response objects and is +intended to replace the `read` method. This new method is currently optional to +implement which allows for a soft migration to `readUrl` instead of `read` in +the future. + #### readTree `readTree` method also expects user-friendly URLs similar to `read` but the URL @@ -197,12 +206,11 @@ the tree and return the files matching the pattern in the `url`. ### Caching -All of the methods above support an ETag based caching (except `read` which is -work in progress!). If the method is called without an `etag`, the response -contains an ETag of the resource (should ideally forward the ETag returned by -the provider). If the method is called with an `etag`, it first compares the -ETag and returns a `NotModifiedError` in case the resource has not been -modified. This approach is very similar to the actual +All of the methods above support an ETag based caching. If the method is called +without an `etag`, the response contains an ETag of the resource (should ideally +forward the ETag returned by the provider). If the method is called with an +`etag`, it first compares the ETag and returns a `NotModifiedError` in case the +resource has not been modified. This approach is very similar to the actual [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) HTTP headers. From 9813f6c2c72a7c61ec9173385ae2f0137ee61f6e Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 5 Jul 2021 10:21:31 +0200 Subject: [PATCH 5/7] document how to add a new url reader (oss vs private) and update section title Signed-off-by: Himanshu Mishra --- docs/integrations/url-reader.md | 34 +++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md index 83f6a743d9..c37549cf8b 100644 --- a/docs/integrations/url-reader.md +++ b/docs/integrations/url-reader.md @@ -68,7 +68,7 @@ export type UrlReader = { }; ``` -## Using inside a plugin +## Using a URL Reader inside a plugin The `reader` instance is available in the backend plugin environment and passed on to all the backend plugins. You can see an @@ -116,7 +116,7 @@ Feel free to use the [GitHub URL Reader](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/GithubUrlReader.ts) as a source of inspiration. -### Add an integration +### 1. Add an integration The provider for your new URL Reader can also be called an "integration" in Backstage. The `integrations:` section of your Backstage `app-config.yaml` @@ -130,7 +130,7 @@ integrations config and process it", "construct headers for authenticated requests to the host" or "convert a plain file URL into its API URL for downloading the file" would live in this package. -### Create the URL Reader +### 2. Create the URL Reader Create a new class which implements the [`UrlReader` type](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/types.ts#L21-L28) @@ -140,7 +140,7 @@ should be used for. See the [GitHub URL Reader](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/GithubUrlReader.ts#L50-L63) for example. -### Implement the methods +### 3. Implement the methods We want to make sure all URL Readers behave in the same way. Hence if possible, all the methods of the `UrlReader` interface should be implemented. However it @@ -204,7 +204,29 @@ matching the query. The core logic of `readTree` can be used here to extract all the files inside the tree and return the files matching the pattern in the `url`. -### Caching +### 4. Add to available URL Readers + +There are two ways to make your new URL Reader available for use. + +You can choose to make it open source, by updating the +[`default` factory](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/UrlReaders.ts#L62-L81) +method of URL Readers. + +But for something internal which you don't want to make open source, you can +update your `packages/backend/src/index.ts` file and update how the `reader` +instance is created. + +```ts +// File: packages/backend/src/index.ts +const reader = UrlReaders.default({ + logger: root, + config, + // This is where your internal URL Readers would go. + factories: [myCustomReader.factory], +}); +``` + +### 5. Caching All of the methods above support an ETag based caching. If the method is called without an `etag`, the response contains an ETag of the resource (should ideally @@ -215,7 +237,7 @@ resource has not been modified. This approach is very similar to the actual [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) HTTP headers. -### Debugging +### 6. Debugging When debugging one of the URL Readers, you can straightforward use the [`reader` instance created](https://github.com/backstage/backstage/blob/ebbe91dbe79038a61d35cf6ed2d96e0e0d5a15f3/packages/backend/src/index.ts#L57) From 918e2be318282d21fb8bea5bb9ed476e6ce0fa28 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 5 Jul 2021 10:42:56 +0200 Subject: [PATCH 6/7] docs: Add an example code block for debugging new url reader Signed-off-by: Himanshu Mishra --- docs/integrations/url-reader.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md index c37549cf8b..a2108d5c71 100644 --- a/docs/integrations/url-reader.md +++ b/docs/integrations/url-reader.md @@ -242,6 +242,23 @@ HTTP headers. When debugging one of the URL Readers, you can straightforward use the [`reader` instance created](https://github.com/backstage/backstage/blob/ebbe91dbe79038a61d35cf6ed2d96e0e0d5a15f3/packages/backend/src/index.ts#L57) when the backend starts and call one of the methods with your debugging URL. + +```ts +// File: packages/backend/src/index.ts + +async function main() { + // ... + const createEnv = makeCreateEnv(config); + + const testReader = createEnv('test-url-reader').reader; + const response = await testReader.readUrl( + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + ); + console.log((await response.buffer()).toString()); + // ... +} +``` + This will be run every time you restart the backend. Note that after any change in the URL Reader code, you need to kill the backend and restart, since the `reader` instance is memoized and does not update on hot module reloading. Also, From 19b459d2ba2b7499cd597f9e51cbc75d43ab325c Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 5 Jul 2021 14:46:04 +0200 Subject: [PATCH 7/7] move url reader docs to plugins -> backend apis Signed-off-by: Himanshu Mishra --- docs/{integrations => plugins}/url-reader.md | 12 ++++++++++-- microsite/sidebars.json | 6 +++--- mkdocs.yml | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) rename docs/{integrations => plugins}/url-reader.md (96%) diff --git a/docs/integrations/url-reader.md b/docs/plugins/url-reader.md similarity index 96% rename from docs/integrations/url-reader.md rename to docs/plugins/url-reader.md index a2108d5c71..41aba98a66 100644 --- a/docs/integrations/url-reader.md +++ b/docs/plugins/url-reader.md @@ -24,8 +24,8 @@ package provides a dedicated API for reading from such URL based remote locations like GitHub, GitLab, Bitbucket, Google Cloud Storage, etc. This is commonly referred to as "URL Reader". It takes care of making authenticated requests to the remote host so that private files can be read securely. If users -have [GitHub App based authentication](../plugins/github-apps.md) set up, URL -Reader even refreshes the token, to avoid reaching the GitHub API rate limit. +have [GitHub App based authentication](github-apps.md) set up, URL Reader even +refreshes the token, to avoid reaching the GitHub API rate limit. As a result, plugin authors do not have to worry about any of these problems when trying to read files. @@ -61,6 +61,14 @@ The generic interface of a URL Reader instance looks like this. export type UrlReader = { /* Used to read a single file and return its content. */ read(url: string): Promise; + /** + * A replacement for the read method that supports options and complex responses. + * + * Use this whenever it is available, as the read method will be deprecated and + * eventually removed in the future. + */ + readUrl?(url: string, options?: ReadUrlOptions): Promise; + /* Used to read a file tree and download as a directory. */ readTree(url: string, options?: ReadTreeOptions): Promise; /* Used to search a file in a tree. */ diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 5530fc6f61..5999524073 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -149,8 +149,7 @@ "type": "subcategory", "label": "LDAP", "ids": ["integrations/ldap/org"] - }, - "integrations/url-reader" + } ], "Plugins": [ "plugins/index", @@ -167,7 +166,8 @@ "plugins/proxying", "plugins/backend-plugin", "plugins/call-existing-api", - "plugins/github-apps" + "plugins/github-apps", + "plugins/url-reader" ] }, { diff --git a/mkdocs.yml b/mkdocs.yml index 47b7084146..4d984e63e3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -98,7 +98,6 @@ nav: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: - Org Data: 'integrations/ldap/org.md' - - URL Reader: 'integrations/url-reader.md' - Plugins: - Intro to plugins: 'plugins/index.md' - Existing plugins: 'plugins/existing-plugins.md' @@ -113,6 +112,7 @@ nav: - Backend plugin: 'plugins/backend-plugin.md' - Call existing API: 'plugins/call-existing-api.md' - GitHub Apps for Backend Authentication: 'plugins/github-apps.md' + - URL Reader: 'plugins/url-reader.md' - Testing: - Testing with Jest: 'plugins/testing.md' - Publishing: