├── .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 | --------------------------------------------------------------------------------