├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── qa.sh ├── composer.json ├── config └── index-now.php ├── phpmd-ruleset.xml ├── phpunit.xml ├── pint.json └── src ├── Commands └── GenerateKeyCommand.php ├── Exceptions ├── KeyFileDirectoryMissing.php └── TooManyUrlsException.php ├── Facades └── IndexNow.php ├── IndexNow.php ├── IndexNowServiceProvider.php └── Jobs └── IndexNowSubmitJob.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-index-now` will be documented in this file. 4 | 5 | ## v2.0.0 - 2025-03-13 6 | 7 | ### Changes 8 | 9 | - Dropped Laravel 10 support 10 | - Added Laravel 11 support 11 | 12 | ## v1.3.1 - 2025-01-05 13 | 14 | Fixes php 8.4 implicit nullable type deprecation. 15 | 16 | Thanks @it-can 17 | 18 | ## 1.3 - 2024-03-17 19 | 20 | Laravel 11 support 21 | 22 | ## 1.2 23 | 24 | Laravel 10 support 25 | 26 | ## 1.1 - 2022-06-19 27 | 28 | Index submissions are only made on the configured production environment. 29 | 30 | ## 1.0 - 2022-06-16 31 | 32 | IndexNow API wrapper for Laravel 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) LaravelFreelancerNL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel IndexNow - Submit webpage updates to search engines 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/laravel-freelancer-nl/laravel-index-now.svg?style=flat)](https://packagist.org/packages/laravel-freelancer-nl/laravel-index-now) 4 | [![Code Quality](https://img.shields.io/github/workflow/status/LaravelFreelancerNL/laravel-index-now/quality-assurance?label=quality%20assurance)](https://github.com/LaravelFreelancerNL/laravel-index-now/actions?query=workflow%3Aquality-assurance+branch%3Anext) 5 | [![Tests](https://img.shields.io/github/workflow/status/LaravelFreelancerNL/laravel-index-now/run-tests?label=tests)](https://github.com/LaravelFreelancerNL/laravel-index-now/actions?query=workflow%3Arun-tests+branch%3Anext) 6 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/LaravelFreelancerNL/laravel-index-now/next)](https://scrutinizer-ci.com/g/LaravelFreelancerNL/laravel-index-now/?branch=next) 7 | [![License](https://img.shields.io/github/license/LaravelFreelancerNL/laravel-index-now)](https://github.com/LaravelFreelancerNL/laravel-index-now/blob/next/LICENSE.md) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/laravel-freelancer-nl/laravel-index-now.svg?style=flat)](https://packagist.org/packages/laravel-freelancer-nl/laravel-index-now) 9 | 10 | This packages provides a wrapper to use the IndexNow api in Laravel. This makes indexing new webpages in (some) search 11 | engines easy and fast. It makes for a nice little addition to sitemaps. 12 | 13 | [IndexNow](https://www.indexnow.org) is an API created by Microsoft Bing & Yandex to allow you to submit page changes 14 | to their search engines quickly. A submission to one search engine will automatically pass the submission on to the 15 | others. 16 | 17 | At the time of this writing Google is considering supporting the API 18 | and [has announced it will be testing it](https://www.searchenginejournal.com/google-will-be-testing-indexnow/426602/). 19 | 20 | ## Installation 21 | 22 | You can install the package via composer: 23 | 24 | ```bash 25 | composer require laravel-freelancer-nl/laravel-index-now 26 | ``` 27 | 28 | You can publish the config file with: 29 | ```bash 30 | php artisan vendor:publish --tag="index-now-config" 31 | ``` 32 | 33 | This is the contents of the published config file: 34 | 35 | ```php 36 | return [ 37 | 'host' => env('APP_URL', 'localhost'), 38 | 'key' => env('INDEXNOW_KEY', ''), 39 | 'key-location' => env('INDEXNOW_KEY_LOCATION', ''), 40 | 'log-failed-submits' => env('INDEXNOW_LOG_FAILED_SUBMITS', true), 41 | 'production-env' => env('INDEXNOW_PRODUCTION_ENV', 'production'), 42 | 'search-engine' => env('INDEXNOW_SEARCH_ENGINE', 'api.indexnow.org'), 43 | 'delay' => env('INDEXNOW_SUBMIT_DELAY', 600), 44 | ]; 45 | ``` 46 | - _host_: the domain for which you will submit pages to the search engine 47 | - _key_: the unique key for this domain (you will generate one in the next step) 48 | - _key-location_: the directory and/or prefix to the key file 49 | - _log-failed-submits_: disable logging of submit attempts in non-production environments 50 | - _production-env_: the name of the production environment; 51 | - _search-engine_: the domain of the specific search engine you wish to submit too. 52 | - _delay_: the delay in seconds for delayed page submissions. 53 | 54 | ### Generate a new key 55 | The IndexNow API matches the request key to a key file within the host domain. 56 | To generate the keyfile you use the following artisan command: 57 | 58 | ```bash 59 | php artisan index-now:generate-key 60 | ``` 61 | This will create a keyfile in the public_dir() of your project and output the key. 62 | Copy the displayed key and place it in your .env file. 63 | 64 | If you've set a key location in the config it will be prefixed to the file. 65 | 66 | Running this command multiple times will generate a new key and key file. 67 | 68 | ### Only submit pages for your production environment 69 | You probably don't want to submit pages in any environment other than production. 70 | 71 | By default, this package assumes that your production environment is called 'production' and will only submit when it 72 | matches the configured name in the package. 73 | 74 | In the case of an environment mismatch it will log a submission 'attempt' instead of actually submitting the page to 75 | the search engines. 76 | 77 | If you use an alternative name for your production environment you can set INDEXNOW_PRODUCTION_ENV in your .env 78 | to match. 79 | 80 | You can disable this by setting INDEXNOW_PRODUCTION_ENV to false. 81 | 82 | ## Usage 83 | You can submit one or more pages per request by calling the facade and passing the url(s) to the submit method. 84 | 85 | Note that the urls must be fully qualified without query parameters and without a fragment. 86 | 87 | ### Submit a single page 88 | ```php 89 | use LaravelFreelancerNL\LaravelIndexNow\Facades\IndexNow; 90 | 91 | IndexNow::submit('https://dejacht.nl/jagen'); 92 | ``` 93 | 94 | ### Submit a multiple pages 95 | To submit multiple pages at once just add an array of urls. 96 | 97 | ```php 98 | use LaravelFreelancerNL\LaravelIndexNow\Facades\IndexNow; 99 | 100 | IndexNow::submit([ 101 | 'https://dejacht.nl', 102 | 'https://dejacht.nl/fotoquiz/', 103 | 'https://dejacht.nl/jagen/', 104 | 'https://dejacht.nl/jachtvideos/', 105 | ]); 106 | ``` 107 | 108 | ### Prevent spam: delay page submissions 109 | You can delay page submissions by using the delaySubmission method. 110 | This dispatches the IndexNowSubmitJob with a delay in seconds as configured. The job is unique by payload, 111 | so multiple submissions of the same urls won't push a job to the queue before the current one is handled. 112 | 113 | ```php 114 | use LaravelFreelancerNL\LaravelIndexNow\Facades\IndexNow; 115 | 116 | IndexNow::delaySubmission('https://devechtschool.nl'); 117 | ``` 118 | 119 | You can override the configured default delay by adding your own. 120 | ```php 121 | use LaravelFreelancerNL\LaravelIndexNow\Facades\IndexNow; 122 | 123 | IndexNow::delaySubmission('https://devechtschool.nl', 100); 124 | ``` 125 | 126 | #### When to delay submits 127 | It isn't uncommon for people to make additional edits after creating or updating a page. On these actions it is a good idea to delay 128 | the submission. 129 | 130 | When deleting pages you can just submit the urls immediately. 131 | 132 | ## Testing 133 | 134 | ```bash 135 | composer test 136 | ``` 137 | 138 | ## Changelog 139 | 140 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 141 | 142 | ## Contributing 143 | 144 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 145 | 146 | ## Security Vulnerabilities 147 | 148 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 149 | 150 | ## Credits 151 | 152 | - [Bas](https://github.com/LaravelFreelancerNL) 153 | - [All Contributors](../../contributors) 154 | 155 | ## License 156 | 157 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 158 | -------------------------------------------------------------------------------- /bin/qa.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | printf "\nRun PHPMD\n" 3 | ./vendor/bin/phpmd src/ text phpmd-ruleset.xml 4 | 5 | printf "\nRun PHPStan\n" 6 | composer analyse 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-freelancer-nl/laravel-index-now", 3 | "description": "Alert search engines of content changes. ", 4 | "keywords": [ 5 | "indexnow", 6 | "laravel", 7 | "seo", 8 | "search engines" 9 | ], 10 | "homepage": "https://github.com/LaravelFreelancerNL/laravel-index-now", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Bas", 15 | "email": "info@laravel-freelancer.nl", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "guzzlehttp/guzzle": "^7.4", 22 | "nunomaduro/termwind": "^2.0", 23 | "pestphp/pest": "^3.0", 24 | "spatie/laravel-package-tools": "^1.9.2" 25 | }, 26 | "require-dev": { 27 | "laravel/framework": "^11.0 || ^12.0", 28 | "laravel/pint": "^1.13", 29 | "larastan/larastan": "^3.1", 30 | "orchestra/testbench": "^9.0 || ^10.0", 31 | "phpmd/phpmd": "^2.14", 32 | "phpstan/extension-installer": "^1.1", 33 | "phpstan/phpstan": "^1.8 || ^2.1", 34 | "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", 35 | "phpstan/phpstan-phpunit": "^1.0 || ^2.0", 36 | "spatie/laravel-ray": "^1.26", 37 | "timacdonald/log-fake": "^2.2" 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "LaravelFreelancerNL\\LaravelIndexNow\\": "src" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "LaravelFreelancerNL\\LaravelIndexNow\\Tests\\": "tests" 47 | } 48 | }, 49 | "scripts": { 50 | "analyse": "vendor/bin/phpstan analyse", 51 | "test": "vendor/bin/pest", 52 | "test-coverage": "vendor/bin/pest --coverage", 53 | "format": "vendor/bin/pint" 54 | }, 55 | "config": { 56 | "sort-packages": true, 57 | "allow-plugins": { 58 | "pestphp/pest-plugin": true, 59 | "phpstan/extension-installer": true 60 | } 61 | }, 62 | "extra": { 63 | "laravel": { 64 | "providers": [ 65 | "LaravelFreelancerNL\\LaravelIndexNow\\IndexNowServiceProvider" 66 | ], 67 | "aliases": { 68 | "IndexNow": "LaravelFreelancerNL\\laravel-index-now\\Facades\\IndexNow" 69 | } 70 | } 71 | }, 72 | "minimum-stability": "dev", 73 | "prefer-stable": true 74 | } 75 | -------------------------------------------------------------------------------- /config/index-now.php: -------------------------------------------------------------------------------- 1 | env('INDEXNOW_SUBMIT_DELAY', 600), 7 | 'host' => env('APP_URL', 'localhost'), 8 | 'key' => env('INDEXNOW_KEY', ''), 9 | 'key-location' => env('INDEXNOW_KEY_LOCATION', ''), 10 | 'log-failed-submits' => env('INDEXNOW_LOG_FAILED_SUBMITS', true), 11 | 'production-env' => env('INDEXNOW_PRODUCTION_ENV', 'production'), 12 | 'search-engine' => env('INDEXNOW_SEARCH_ENGINE', 'api.indexnow.org'), 13 | ]; 14 | -------------------------------------------------------------------------------- /phpmd-ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | Inspired by https://github.com/phpmd/phpmd/issues/137 9 | using http://phpmd.org/documentation/creating-a-ruleset.html 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | Detects when a field, formal or local variable is declared with a long name. 29 | 30 | 3 31 | 32 | 33 | 34 | 35 | 36 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "per" 3 | } -------------------------------------------------------------------------------- /src/Commands/GenerateKeyCommand.php: -------------------------------------------------------------------------------- 1 | info('The keyfile was generated. Please add the following key to your .env file:'); 21 | $this->newLine(); 22 | $this->info('INDEXNOW_KEY=' . $key); 23 | 24 | return self::SUCCESS; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Exceptions/KeyFileDirectoryMissing.php: -------------------------------------------------------------------------------- 1 | toString(); 28 | 29 | $filename = $prefix . $key . '.txt'; 30 | 31 | if (!file_exists(public_path(dirname($filename)))) { 32 | throw new KeyFileDirectoryMissing(); 33 | } 34 | 35 | File::put(public_path() . '/' . $filename, $key); 36 | 37 | return $key; 38 | } 39 | 40 | /** 41 | * @param string|string[] $url 42 | * 43 | * @throws Exception 44 | */ 45 | public function submit(string|array $url): Response|false 46 | { 47 | if (config('app.env') !== config('index-now.production-env')) { 48 | $this->logFailedAttempt($url); 49 | 50 | return false; 51 | } 52 | 53 | if (is_string($url)) { 54 | return $this->submitUrl($url); 55 | } 56 | 57 | return $this->submitUrls($url); 58 | } 59 | 60 | /** 61 | * @param string|string[] $url 62 | * 63 | * @throws Exception 64 | */ 65 | public function delaySubmission(string|array $url, ?int $delayInSeconds = null): PendingDispatch 66 | { 67 | $delayInSeconds ??= (int) config('index-now.delay'); 68 | 69 | return IndexNowSubmitJob::dispatch($url)->delay(now()->addSeconds($delayInSeconds)); 70 | } 71 | 72 | /** 73 | * @param string|string[] $url 74 | */ 75 | protected function logFailedAttempt(string|array $url): void 76 | { 77 | if (config('index-now.log-failed-submits') === false) { 78 | return; 79 | } 80 | 81 | Log::info( 82 | 'IndexNow: page submissions are only sent in production environments.', 83 | ['url' => $url], 84 | ); 85 | } 86 | 87 | /** 88 | * @throws Exception 89 | */ 90 | protected function submitUrl(string $url): Response 91 | { 92 | $targetUrl = $this->generateTargetUrl(); 93 | $queryData = $this->getQueryData(); 94 | $queryData['url'] = $url; 95 | 96 | return Http::get($targetUrl, $queryData); 97 | } 98 | 99 | /** 100 | * @param string[] $urls 101 | * 102 | * @throws TooManyUrlsException 103 | */ 104 | protected function submitUrls(array $urls): Response 105 | { 106 | $targetUrl = $this->generateTargetUrl(); 107 | $queryData = $this->getQueryData(); 108 | $queryData['host'] = config('index-now.host'); 109 | $queryData['urlList'] = $this->prepareUrls($urls); 110 | 111 | return Http::post($targetUrl, $queryData); 112 | } 113 | 114 | protected function generateTargetUrl(): string 115 | { 116 | $searchEngineDomain = config('index-now.search-engine'); 117 | 118 | return 'https://' . $searchEngineDomain . '/indexnow'; 119 | } 120 | 121 | /** 122 | * @return array 123 | */ 124 | protected function getQueryData(): array 125 | { 126 | $queryParameters = []; 127 | 128 | $keyLocation = config('index-now.key-location'); 129 | 130 | $queryParameters['key'] = config('index-now.key'); 131 | if (isset($keyLocation) && $keyLocation !== '') { 132 | $queryParameters['keyLocation'] = $keyLocation; 133 | } 134 | 135 | return $queryParameters; 136 | } 137 | 138 | /** 139 | * @param string[] $urls 140 | * @return string[] 141 | * 142 | * @throws TooManyUrlsException 143 | */ 144 | protected function prepareUrls(array $urls): array 145 | { 146 | if (count($urls) > 10000) { 147 | throw new TooManyUrlsException(); 148 | } 149 | 150 | foreach ($urls as $key => $url) { 151 | $urls[$key] = urlencode($url); 152 | } 153 | 154 | return $urls; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/IndexNowServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('index-now') 23 | ->hasConfigFile('index-now') 24 | ->hasCommand(GenerateKeyCommand::class); 25 | } 26 | 27 | /** 28 | * Register any application services. 29 | * 30 | * @return void 31 | * 32 | * @throws InvalidPackage 33 | */ 34 | public function register() 35 | { 36 | parent::register(); 37 | 38 | $this->app->bind( 39 | 'index-now', 40 | 'LaravelFreelancerNL\LaravelIndexNow\IndexNow', 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Jobs/IndexNowSubmitJob.php: -------------------------------------------------------------------------------- 1 | urls = $urls; 33 | } 34 | 35 | public function handle(): void 36 | { 37 | IndexNow::submit($this->urls); 38 | } 39 | 40 | /** 41 | * The unique ID of the job. 42 | * 43 | * @return string 44 | */ 45 | public function uniqueId() 46 | { 47 | return md5((string) json_encode($this->urls)); 48 | } 49 | } 50 | --------------------------------------------------------------------------------