├── .npmignore
├── .gitignore
├── example
├── .gitignore
├── public
│ ├── index.html
│ └── sitemap.xml
├── package.json
├── netlify.toml
└── package-lock.json
├── manifest.yml
├── package.json
├── helpers.js
├── README.md
└── index.js
/.npmignore:
--------------------------------------------------------------------------------
1 | example
2 | .netlify
3 | node_modules
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Local Netlify folder
2 | .netlify
3 | node_modules
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Local Netlify folder
2 | .netlify
3 | node_modules
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Netlify plugin submit sitemap example
6 |
7 |
8 | Netlify plugin submit sitemap example
9 |
10 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "node-fetch": "^3.2.3"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/example/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | http://example.com/categories/
7 |
8 |
9 |
10 | http://example.com/
11 |
12 |
13 |
14 | http://example.com/tags/
15 |
16 |
17 |
--------------------------------------------------------------------------------
/manifest.yml:
--------------------------------------------------------------------------------
1 | name: netlify-plugin-submit-sitemap
2 | inputs:
3 | - name: baseUrl
4 | description: The base url of your site
5 | default:
6 | - name: sitemapPath
7 | description: Path to the sitemap file
8 | default: "/sitemap.xml"
9 | - name: providers
10 | description: List of enabled providers
11 | default: ["google", "yandex"]
12 | - name: ignorePeriod
13 | description: Time in seconds to not submit the sitemap after successful submission
14 | default: 0
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "netlify-plugin-submit-sitemap",
3 | "version": "0.4.0",
4 | "description": "Automatically submit your sitemap to Google and Yandex!",
5 | "main": "index.js",
6 | "type": "module",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/cdeleeuwe/netlify-plugin-submit-sitemap.git"
10 | },
11 | "author": "Cassius de Leeuwe ",
12 | "license": "MIT",
13 | "bugs": {
14 | "url": "https://github.com/cdeleeuwe/netlify-plugin-submit-sitemap/issues"
15 | },
16 | "homepage": "https://github.com/cdeleeuwe/netlify-plugin-submit-sitemap#readme",
17 | "keywords": [
18 | "netlify",
19 | "netlify-plugin"
20 | ],
21 | "dependencies": {
22 | "node-fetch": "^3.2.3"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | publish = "public"
3 | ignore = "(exit 1)"
4 |
5 | [[plugins]]
6 | package = "../"
7 | [plugins.inputs]
8 |
9 | # The base url of your site (optional, default = main URL set in Netlify)
10 | baseUrl = "example.com"
11 |
12 | # Path to the sitemap URL (optional, default = /sitemap.xml)
13 | sitemapPath = "/sitemap.xml"
14 |
15 | # Time in seconds to not submit the sitemap after successful submission
16 | ignorePeriod = 0
17 |
18 | # Enabled providers to submit sitemap to (optional, default = 'google', 'yandex'). Possible providers are currently only 'google' and 'yandex'.
19 | providers = [
20 | "google",
21 | "bing",
22 | "yandex"
23 | ]
24 |
25 | [[plugins]]
26 | package = "@netlify/plugin-local-install-core"
27 |
--------------------------------------------------------------------------------
/helpers.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 |
3 | const LAST_SUBMIT_DATE_FILENAME = "./last_submit_date.txt";
4 |
5 | const getLastSubmitTimestamp = async (props) => {
6 | const { cache } = props.utils;
7 | await cache.restore(LAST_SUBMIT_DATE_FILENAME);
8 |
9 | if (!fs.existsSync(LAST_SUBMIT_DATE_FILENAME)) {
10 | return;
11 | }
12 |
13 | const date = fs.readFileSync(LAST_SUBMIT_DATE_FILENAME, "utf-8");
14 | return parseInt(`${date}`, 10);
15 | };
16 |
17 | export const setLastSubmitTimestamp = async (props) => {
18 | const { cache } = props.utils;
19 | const { ignorePeriod } = props.inputs;
20 |
21 | const date = new Date().getTime();
22 | const period = parseInt(`${ignorePeriod}`, 10);
23 | fs.writeFileSync(LAST_SUBMIT_DATE_FILENAME, `${date}`);
24 | return cache.save(LAST_SUBMIT_DATE_FILENAME, { ttl: period });
25 | };
26 |
27 | export const isInIgnorePeriod = async (props) => {
28 | const lastSubmitDate = await getLastSubmitTimestamp(props);
29 | if (!lastSubmitDate) return false;
30 |
31 | const { ignorePeriod } = props.inputs;
32 | const period = parseInt(`${ignorePeriod}`, 10) * 1000;
33 | const now = new Date().getTime();
34 | return now < lastSubmitDate + period;
35 | };
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Netlify Build Plugin: Automatically submit your sitemap after every production build
2 |
3 | Automatically submit your sitemap to **Google** and **Yandex** after every production build!
4 |
5 | This plugin will notify the search engines after every production build about your latest sitemap. The plugin can be used without any configuration if using the defaults.
6 |
7 | ## Usage
8 |
9 | You can install this plugin in the Netlify UI from this [direct in-app installation link](https://app.netlify.com/plugins/netlify-plugin-submit-sitemap/install) or from the [Plugins directory](https://app.netlify.com/plugins).
10 |
11 | To use file-based installation, add the following lines to your `netlify.toml` file:
12 |
13 | ```toml
14 | [build]
15 | publish = "public"
16 |
17 | [[plugins]]
18 | package = "netlify-plugin-submit-sitemap"
19 |
20 | [plugins.inputs]
21 |
22 | # The base url of your site (optional, default = main URL set in Netlify)
23 | baseUrl = "https://example.com"
24 |
25 | # Path to the sitemap URL (optional, default = /sitemap.xml)
26 | sitemapPath = "/sitemap.xml"
27 |
28 | # Time in seconds to not submit the sitemap after successful submission
29 | ignorePeriod = 0
30 |
31 | # Enabled providers to submit sitemap to (optional, default = 'google', 'yandex'). Possible providers are currently only 'google', 'yandex'.
32 | providers = [
33 | "google",
34 | "yandex"
35 | ]
36 | ```
37 |
38 | Note: The `[[plugins]]` line is required for each plugin, even if you have other plugins in your `netlify.toml` file already.
39 |
40 | To complete file-based installation, from your project's base directory, use npm, yarn, or any other Node.js package manager to add this plugin to `devDependencies` in `package.json`.
41 |
42 | ```
43 | npm install -D netlify-plugin-submit-sitemap
44 | ```
45 |
46 | ## Notes
47 |
48 | - **DuckDuckGo** is not a supported provider because it currently doesn't offer any manual method where you can enter your sitemap or webpage URLs for indexing; more information can be found [here](https://www.monsterinsights.com/submit-website-to-search-engines/)
49 |
50 | - **Bing** is not supported anymore, since it deprecated anonymous sitemap submission since May 2022
51 |
--------------------------------------------------------------------------------
/example/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "data-uri-to-buffer": {
8 | "version": "4.0.0",
9 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
10 | "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA=="
11 | },
12 | "fetch-blob": {
13 | "version": "3.1.5",
14 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz",
15 | "integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==",
16 | "requires": {
17 | "node-domexception": "^1.0.0",
18 | "web-streams-polyfill": "^3.0.3"
19 | }
20 | },
21 | "formdata-polyfill": {
22 | "version": "4.0.10",
23 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
24 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
25 | "requires": {
26 | "fetch-blob": "^3.1.2"
27 | }
28 | },
29 | "node-domexception": {
30 | "version": "1.0.0",
31 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
32 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
33 | },
34 | "node-fetch": {
35 | "version": "3.2.3",
36 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.3.tgz",
37 | "integrity": "sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==",
38 | "requires": {
39 | "data-uri-to-buffer": "^4.0.0",
40 | "fetch-blob": "^3.1.4",
41 | "formdata-polyfill": "^4.0.10"
42 | }
43 | },
44 | "web-streams-polyfill": {
45 | "version": "3.2.0",
46 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
47 | "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA=="
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import url from "url";
2 | import fetch from "node-fetch";
3 |
4 | import { isInIgnorePeriod, setLastSubmitTimestamp } from "./helpers.js";
5 | const { CONTEXT, URL } = process.env;
6 |
7 | const providerUrls = {
8 | google: (sitemapUrl) => `https://www.google.com/ping?sitemap=${sitemapUrl}`,
9 | yandex: (sitemapUrl) =>
10 | `https://webmaster.yandex.ru/ping?sitemap=${sitemapUrl}`,
11 | };
12 |
13 | // Default parameters (can be overriden with inputs)
14 | const defaults = {
15 | providers: Object.keys(providerUrls),
16 | baseUrl: URL,
17 | sitemapPath: "/sitemap.xml",
18 | };
19 |
20 | // Submit sitemap to a provider. Returns either a successful or failed submission, but no error is thrown
21 | const submitToProvider = async ({ provider, sitemapUrl }) => {
22 | // If the provider is Bing, skip it, because anonymous sitemap submission has been deprecated
23 | if (provider === "bing") {
24 | return {
25 | isWarning: true,
26 | message: `\u26A0 WARN! Sitemap not submitted to Bing, since this has been deprecated. See https://blogs.bing.com/webmaster/may-2022/Spring-cleaning-Removed-Bing-anonymous-sitemap-submission`,
27 | };
28 | }
29 |
30 | if (!providerUrls[provider]) {
31 | return {
32 | message: `Provider ${provider} not found!`,
33 | error: "Invalid provider",
34 | };
35 | }
36 |
37 | const providerUrl = providerUrls[provider](sitemapUrl);
38 | console.log(
39 | `Going to submit sitemap to ${provider} \n --> URL: ${providerUrl}`
40 | );
41 |
42 | try {
43 | await fetch(providerUrl);
44 | } catch (error) {
45 | return {
46 | message: `\u274c ERROR! was not able to submit sitemap to ${provider}`,
47 | error,
48 | };
49 | }
50 |
51 | return {
52 | message: `\u2713 DONE! Sitemap submitted succesfully to ${provider}`,
53 | };
54 | };
55 |
56 | // helpers
57 | const removeEmptyValues = (obj) => {
58 | return Object.keys(obj)
59 | .filter((key) => obj[key] != null)
60 | .reduce((prev, curr) => {
61 | prev[curr] = obj[curr];
62 | return prev;
63 | }, {});
64 | };
65 |
66 | // Make sure the url is prepended with 'https://'
67 | const prependScheme = (baseUrl) => {
68 | return baseUrl.match(/^[a-zA-Z]+:\/\//)
69 | ? baseUrl
70 | : (baseUrl = `https://${baseUrl}`);
71 | };
72 |
73 | export const onSuccess = async (props) => {
74 | const { utils, inputs, constants } = props;
75 | const { providers, baseUrl, sitemapPath } = {
76 | ...defaults,
77 | ...removeEmptyValues(inputs),
78 | };
79 |
80 | // Only run on production builds
81 | if (constants.IS_LOCAL || CONTEXT !== "production") {
82 | console.log(
83 | `Skip submitting sitemap to ${providers.join(
84 | ", "
85 | )}, because this isn't a production build`
86 | );
87 | return;
88 | }
89 |
90 | // Do not run if we are within the ignore period
91 | if (await isInIgnorePeriod(props)) {
92 | console.log(
93 | `Skip submitting sitemap, because it's within the ignore period`
94 | );
95 | return;
96 | }
97 |
98 | let sitemapUrl;
99 | const baseUrlWithScheme = prependScheme(baseUrl);
100 | try {
101 | sitemapUrl = new url.URL(sitemapPath, baseUrlWithScheme).href;
102 | } catch (error) {
103 | return utils.build.failPlugin(
104 | `Invalid sitemap URL! baseUrl: ${baseUrlWithScheme}, sitemapPath: ${sitemapPath}`,
105 | { error }
106 | );
107 | }
108 |
109 | // submit sitemap to all providers
110 | const submissions = await Promise.all(
111 | providers.map((provider) => submitToProvider({ provider, sitemapUrl }))
112 | );
113 |
114 | // For failed submissions, it might be better to use something like a utils.build.warn() as discussed here:
115 | // https://github.com/cdeleeuwe/netlify-plugin-submit-sitemap/issues/4
116 | // But till then, just console.log the errors and fail the plugin.
117 | // ---
118 | // For successful submissions, it's better to use utils.status.show(), but currently Netlify doesn't show
119 | // the status in the UI yet, so also console.log() it for now
120 | // See https://github.com/cdeleeuwe/netlify-plugin-submit-sitemap/issues/5
121 | submissions.forEach(({ error, isWarning, message }) => {
122 | if (error) {
123 | console.error("\x1b[31m", message, "\x1b[0m");
124 | } else if (isWarning) {
125 | console.log("\x1b[33m", message, "\x1b[0m");
126 | } else {
127 | console.log("\x1b[32m", message, "\x1b[0m");
128 | }
129 | });
130 |
131 | const messages = submissions
132 | .map((submission) => submission.message)
133 | .join("\n");
134 |
135 | const errors = submissions
136 | .map((submission) => submission.error)
137 | .filter((error) => error);
138 |
139 | // If there was at least 1 error, fail the plugin, but continue the build.
140 | if (errors.length > 0) {
141 | utils.build.failPlugin(`${errors.length} sitemap submission(s) failed`, {
142 | error: errors[0],
143 | });
144 | return;
145 | }
146 |
147 | await setLastSubmitTimestamp(props);
148 | utils.status.show({
149 | summary: "Sitemap submitted succesfully",
150 | text: messages,
151 | });
152 | };
153 |
--------------------------------------------------------------------------------