├── .gitignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ └── tests.yml ├── routes └── web.php ├── src ├── Http │ └── Controllers │ │ ├── SitemappableController.php.stub │ │ ├── Controller.php │ │ └── SitemappableController.php ├── Sitemappable.php ├── IsSitemappable.php ├── SitemappableServiceProvider.php └── ImportCommand.php ├── tests ├── TestModel.php ├── TestCase.php └── SitemappableTest.php ├── resources └── views │ └── sitemap.blade.php ├── phpunit.xml.dist ├── CHANGELOG.md ├── config └── sitemappable.php ├── database └── migrations │ └── create_sitemappable_table.php.stub ├── LICENSE.md ├── .phpunit.result.cache ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [vursion] 4 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 'array', 16 | ]; 17 | 18 | public function __construct(array $attributes = []) 19 | { 20 | parent::__construct($attributes); 21 | 22 | $this->table = config('sitemappable.db_table_name'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a Question 4 | url: https://github.com/vursion/laravel-sitemappable/discussions/new?category=q-a 5 | about: Ask the community for help 6 | - name: Feature Request 7 | url: https://github.com/vursion/laravel-sitemappable/discussions/new?category=ideas 8 | about: Share ideas for new features 9 | - name: Bug Report 10 | url: https://github.com/vursion/laravel-sitemappable/issues/new 11 | about: Report a reproducable bug 12 | -------------------------------------------------------------------------------- /tests/TestModel.php: -------------------------------------------------------------------------------- 1 | 'https://www.vursion.io/nl/testen/test-slug-in-het-nederlands', 20 | 'en' => 'https://www.vursion.io/en/tests/test-slug-in-english', 21 | ]; 22 | } 23 | 24 | public function shouldBeSitemappable() 25 | { 26 | return (! $this->draft && $this->published); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/views/sitemap.blade.php: -------------------------------------------------------------------------------- 1 | {!! '<' . '?xml version="1.0" encoding="UTF-8"?>' !!} 2 | 3 | @foreach ($sitemappables as $sitemappable) 4 | @foreach ($sitemappable->urls as $url) 5 | 6 | {{ $url }} 7 | @if (count(array_keys($sitemappable->urls)) > 1) 8 | @foreach ($sitemappable->urls as $lang => $url) 9 | 10 | @endforeach 11 | @endif 12 | @if ($sitemappable->updated_at) 13 | {{ $sitemappable->updated_at->toIso8601String() }} 14 | @endif 15 | 16 | @endforeach 17 | @endforeach 18 | 19 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-sitemappable` will be documented in this file 4 | 5 | ## 1.9.0 - 2025-02-24 6 | - Support Laravel 12 7 | 8 | ## 1.8.0 - 2024-11-25 9 | - Support PHP 8.4 10 | 11 | ## 1.7.0 - 2024-03-13 12 | - Support Laravel 11 13 | 14 | ## 1.6.0 - 2023-11-24 15 | - Support PHP 8.3 16 | 17 | ## 1.5.0 - 2023-02-15 18 | - Support Laravel 10 19 | 20 | ## 1.4.1 - 2022-12-09 21 | - Support PHP 8.2 (exclude Laravel 6 from PHP 8.2 tests) 22 | 23 | ## 1.4.0 - 2022-12-09 24 | - Support PHP 8.2 25 | 26 | ## 1.3.0 - 2022-02-10 27 | - Support Laravel 9 28 | 29 | ## 1.2.0 - 2021-12-22 30 | - Support PHP 8.1 31 | 32 | ## 1.1.0 - 2021-10-21 33 | - Follow Google guidelines for localized versions of pages. 34 | 35 | ## 1.0.0 - 2021-05-17 36 | - initial release. 37 | -------------------------------------------------------------------------------- /config/sitemappable.php: -------------------------------------------------------------------------------- 1 | 'sitemap', 10 | 11 | /* 12 | * The generated XML sitemap is cached to speed up performance. 13 | */ 14 | 'cache' => '60 minutes', 15 | 16 | /* 17 | * The batch import will loop through this directory and search for models 18 | * that use the IsSitemappable trait. 19 | */ 20 | 'model_directory' => 'app/Models', 21 | 22 | /* 23 | * If you're extending the controller, you'll need to specify the new location here. 24 | */ 25 | 'controller' => Vursion\LaravelSitemappable\Http\Controllers\SitemappableController::class, 26 | 27 | ]; 28 | -------------------------------------------------------------------------------- /database/migrations/create_sitemappable_table.php.stub: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 18 | $table->increments('id'); 19 | $table->morphs('entity'); 20 | $table->text('urls')->nullable(); 21 | $table->timestamps(); 22 | $table->softDeletes(); 23 | $table->index('entity_id'); 24 | $table->index('entity_type'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists(config('sitemappable.db_table_name')); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Controllers/SitemappableController.php: -------------------------------------------------------------------------------- 1 | otherRoutes())->map(function ($route) { 14 | return new Sitemappable([ 15 | 'urls' => $route, 16 | ]); 17 | }); 18 | 19 | $sitemappables = Sitemappable::get()->concat($otherRoutes)->filter(function ($sitemappable) { 20 | return (is_array($sitemappable->urls) && count($sitemappable->urls) > 0); 21 | }); 22 | 23 | return view('sitemappable::sitemap', compact('sitemappables'))->render(); 24 | }); 25 | 26 | return response(preg_replace('/>(\s)+<', $content), '200')->header('Content-Type', 'text/xml'); 27 | } 28 | 29 | protected function otherRoutes() 30 | { 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2023 vursion 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":1378:{a:2:{s:7:"defects";a:6:{s:79:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_can_be_instantiated";i:4;s:102:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_will_handle_a_should_be_sitemappable_model";i:4;s:107:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_will_discard_a_should_not_be_sitemappable_model";i:4;s:112:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_will_delete_a_no_longer_should_be_sitemappable_model";i:4;s:87:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_can_generate_an_xml_sitemap";i:4;s:93:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_can_batch_import_existing_records";i:3;}s:5:"times";a:6:{s:79:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_can_be_instantiated";d:0.366;s:102:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_will_handle_a_should_be_sitemappable_model";d:0.039;s:107:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_will_discard_a_should_not_be_sitemappable_model";d:0.011;s:112:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_will_delete_a_no_longer_should_be_sitemappable_model";d:0.013;s:87:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_can_generate_an_xml_sitemap";d:0.128;s:93:"Vursion\LaravelSitemappable\Tests\SitemappableTest::test_it_can_batch_import_existing_records";d:0.022;}}} -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | setUpDatabase(); 20 | $this->setUpTestModel(); 21 | 22 | config()->set('sitemappable.cache', '0 minutes'); 23 | config()->set('app.locale', 'nl'); 24 | 25 | $this->mock = $this->createPartialMock(ImportCommand::class, ['fetchCandidates']); 26 | 27 | $this->mock->method('fetchCandidates') 28 | ->willReturn(collect(['Vursion\LaravelSitemappable\Tests\TestModel'])); 29 | } 30 | 31 | protected function getPackageProviders($app) 32 | { 33 | return [SitemappableServiceProvider::class]; 34 | } 35 | 36 | protected function setUpDatabase() 37 | { 38 | include_once __DIR__ . '/../database/migrations/create_sitemappable_table.php.stub'; 39 | 40 | (new \CreateSitemappableTable())->up(); 41 | } 42 | 43 | protected function setUpTestModel() 44 | { 45 | Schema::create('test_models', function (Blueprint $table) { 46 | $table->increments('id'); 47 | $table->boolean('draft')->default(true); 48 | $table->boolean('published')->default(false); 49 | $table->timestamps(); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vursion/laravel-sitemappable", 3 | "description": "laravel-sitemappable", 4 | "keywords": [ 5 | "vursion", 6 | "laravel", 7 | "sitemap" 8 | ], 9 | "homepage": "https://github.com/vursion/laravel-sitemappable", 10 | "license": "MIT", 11 | "type": "library", 12 | "authors": [ 13 | { 14 | "name": "Jochen Sengier", 15 | "email": "support@vursion.io", 16 | "role": "Developer" 17 | } 18 | ], 19 | "require": { 20 | "php": "^7.1 || ^7.2 || ^7.3 || ^7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4" 21 | }, 22 | "require-dev": { 23 | "orchestra/testbench": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0", 24 | "phpunit/phpunit": "^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Vursion\\LaravelSitemappable\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Vursion\\LaravelSitemappable\\Tests\\": "tests" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "vendor/bin/phpunit" 38 | }, 39 | "config": { 40 | "sort-packages": true, 41 | "allow-plugins": { 42 | "kylekatarnls/update-helper": true 43 | } 44 | }, 45 | "extra": { 46 | "laravel": { 47 | "providers": [ 48 | "Vursion\\LaravelSitemappable\\SitemappableServiceProvider" 49 | ] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/IsSitemappable.php: -------------------------------------------------------------------------------- 1 | shouldBeSitemappable()) { 13 | static::addModel($model); 14 | } else { 15 | static::deleteModel($model, true); 16 | } 17 | }); 18 | 19 | static::deleted(function ($model) { 20 | static::deleteModel($model, true); 21 | }); 22 | } 23 | 24 | protected static function addModel($model) 25 | { 26 | $sitemap = Sitemappable::withTrashed()->firstOrCreate([ 27 | 'entity_id' => $model->id, 28 | 'entity_type' => get_class($model), 29 | ]); 30 | $sitemap->restore(); 31 | $sitemap->urls = $model->toSitemappableArray(); 32 | $sitemap->save(); 33 | } 34 | 35 | protected static function deleteModel($model, $forceDelete = false) 36 | { 37 | $sitemap = Sitemappable::where('entity_type', get_class($model)) 38 | ->where('entity_id', $model->id) 39 | ->withTrashed(); 40 | 41 | if ($sitemap) { 42 | if ($forceDelete) { 43 | $sitemap->forceDelete(); 44 | } else { 45 | $sitemap->delete(); 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Determine if the model should be sitemappable. 52 | * 53 | * @return bool 54 | */ 55 | public function shouldBeSitemappable() 56 | { 57 | return true; 58 | } 59 | 60 | /** 61 | * Returns an array with the (localized) URLs. 62 | * 63 | * @return array 64 | */ 65 | public function toSitemappableArray() 66 | { 67 | return []; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/SitemappableServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/../resources/views', 'sitemappable'); 15 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 16 | 17 | if ($this->app->runningInConsole()) { 18 | $this->publishes([ 19 | __DIR__ . '/../config/sitemappable.php' => config_path('sitemappable.php'), 20 | ], 'config'); 21 | 22 | $this->publishes([ 23 | __DIR__ . '/../database/migrations/create_sitemappable_table.php.stub' => $this->getMigrationFileName('create_sitemappable_table.php'), 24 | ], 'migrations'); 25 | 26 | $this->publishes([ 27 | __DIR__ . '/../src/Http/Controllers/SitemappableController.php.stub' => app_path('Http/Controllers/SitemappableController.php'), 28 | ], 'controllers'); 29 | } 30 | } 31 | 32 | public function register() 33 | { 34 | $this->mergeConfigFrom(__DIR__ . '/../config/sitemappable.php', 'sitemappable'); 35 | 36 | $this->commands([ 37 | ImportCommand::class, 38 | ]); 39 | } 40 | 41 | protected function getMigrationFileName($migrationFileName) 42 | { 43 | return Collection::make(database_path('migrations/*')) 44 | ->flatMap(function ($path) use ($migrationFileName) { 45 | return $this->app->make(Filesystem::class)->glob($path . '*_' . $migrationFileName); 46 | })->push(database_path('migrations/' . date('Y_m_d_His') . '_' . $migrationFileName))->first(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ImportCommand.php: -------------------------------------------------------------------------------- 1 | truncate(); 22 | 23 | $this->process(); 24 | 25 | $this->table(['Model', 'Result'], $this->results); 26 | } 27 | 28 | public function process() 29 | { 30 | $this->results = $this->fetchCandidates()->filter(function ($class) { 31 | return class_exists($class); 32 | })->map(function ($class) { 33 | $reflection = new ReflectionClass($class); 34 | 35 | if (! $reflection->isAbstract() && $reflection->isSubclassOf(\Illuminate\Database\Eloquent\Model::class)){ 36 | if (in_array('Vursion\LaravelSitemappable\IsSitemappable', $reflection->getTraitNames())) { 37 | return [ 38 | '' . $class . '', 39 | '' . $this->import($class) . '' 40 | ]; 41 | } 42 | 43 | return [ 44 | '' . $class . '', 45 | 'Sitemappable trait not found', 46 | ]; 47 | } 48 | })->filter()->toArray(); 49 | } 50 | 51 | protected function fetchCandidates() 52 | { 53 | return collect(File::allFiles(base_path(config('sitemappable.model_directory'))))->filter(function ($file) { 54 | return ($file->getExtension() === 'php'); 55 | })->map(function ($file) { 56 | return str_replace([base_path(), '/', '\app\\', '.php'], ['', '\\', 'App\\', ''], $file->getRealPath()); 57 | }); 58 | } 59 | 60 | protected function import($class) 61 | { 62 | $records = $class::get()->each(function ($model) use ($class) { 63 | if ($model->shouldBeSitemappable()) { 64 | Sitemappable::create([ 65 | 'entity_id' => $model->id, 66 | 'entity_type' => $class, 67 | 'urls' => $model->toSitemappableArray(), 68 | ]); 69 | } 70 | }); 71 | 72 | return trans_choice('{0} 0 records processed|{1} 1 record processed|[2,*] :records records processed', $records->count(), ['records' => $records->count()]); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/SitemappableTest.php: -------------------------------------------------------------------------------- 1 | app->getProvider(SitemappableServiceProvider::class)); 18 | } 19 | 20 | public function test_it_will_handle_a_should_be_sitemappable_model() 21 | { 22 | $testModel = TestModel::create([ 23 | 'draft' => false, 24 | 'published' => true, 25 | ]); 26 | 27 | $this->assertDatabaseHas('sitemap', [ 28 | 'entity_id' => $testModel->id, 29 | 'entity_type' => get_class($testModel), 30 | ]); 31 | 32 | $testModel->delete(); 33 | $this->assertDatabaseMissing($testModel->getTable(), ['id' => $testModel->id]); 34 | 35 | $this->assertDatabaseMissing('sitemap', [ 36 | 'entity_id' => $testModel->id, 37 | 'entity_type' => get_class($testModel), 38 | ]); 39 | } 40 | 41 | public function test_it_will_discard_a_should_not_be_sitemappable_model() 42 | { 43 | $testModel = TestModel::create([ 44 | 'draft' => false, 45 | 'published' => false, 46 | ]); 47 | 48 | $this->assertDatabaseMissing('sitemap', [ 49 | 'entity_id' => $testModel->id, 50 | 'entity_type' => get_class($testModel), 51 | ]); 52 | } 53 | 54 | public function test_it_will_delete_a_no_longer_should_be_sitemappable_model() 55 | { 56 | $testModel = TestModel::create([ 57 | 'draft' => false, 58 | 'published' => true, 59 | ]); 60 | 61 | $this->assertDatabaseHas('sitemap', [ 62 | 'entity_id' => $testModel->id, 63 | 'entity_type' => get_class($testModel), 64 | ]); 65 | 66 | $testModel->published = false; 67 | $testModel->save(); 68 | 69 | $this->assertDatabaseMissing('sitemap', [ 70 | 'entity_id' => $testModel->id, 71 | 'entity_type' => get_class($testModel), 72 | ]); 73 | } 74 | 75 | public function test_it_can_generate_an_xml_sitemap() 76 | { 77 | $testModel = TestModel::create([ 78 | 'draft' => false, 79 | 'published' => true, 80 | ]); 81 | 82 | $response = $this->get('sitemap.xml'); 83 | 84 | $response->assertStatus(200); 85 | $this->assertContains('text/xml', explode(';', $response->headers->get('Content-Type'))); 86 | 87 | $expected = preg_replace('/>(\s)+<', ' 88 | 89 | 90 | https://www.vursion.io/nl/testen/test-slug-in-het-nederlands 91 | 92 | 93 | ' . $testModel->updated_at->toIso8601String() . ' 94 | 95 | 96 | https://www.vursion.io/en/tests/test-slug-in-english 97 | 98 | 99 | ' . $testModel->updated_at->toIso8601String() . ' 100 | 101 | '); 102 | 103 | $this->assertXmlStringEqualsXmlString($expected, $response->getContent()); 104 | } 105 | 106 | public function test_it_can_batch_import_existing_records() 107 | { 108 | $testModel = TestModel::create([ 109 | 'draft' => false, 110 | 'published' => true, 111 | ]); 112 | 113 | $skipTestModel = TestModel::create([ 114 | 'draft' => false, 115 | 'published' => false, 116 | ]); 117 | 118 | DB::table(config('sitemappable.db_table_name'))->truncate(); 119 | 120 | $this->mock->process(); 121 | 122 | $this->assertDatabaseHas('sitemap', [ 123 | 'entity_id' => $testModel->id, 124 | 'entity_type' => get_class($testModel), 125 | ]); 126 | 127 | $this->assertDatabaseMissing('sitemap', [ 128 | 'entity_id' => $skipTestModel->id, 129 | 'entity_type' => get_class($skipTestModel), 130 | ]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 8 * * *' 8 | 9 | jobs: 10 | php-tests: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | php: [7.1, 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3, 8.4] 16 | laravel: [5.6.*, 5.7.*, 5.8.*, 6.*, 7.*, 8.*, 9.*, 10.*, 11.*, 12.*] 17 | version: [prefer-stable] 18 | os: [ubuntu-latest] 19 | include: 20 | - laravel: 5.6.* 21 | testbench: 3.6.* 22 | - laravel: 5.7.* 23 | testbench: 3.7.* 24 | - laravel: 5.8.* 25 | testbench: 3.8.* 26 | - laravel: 6.* 27 | testbench: 4.* 28 | - laravel: 7.* 29 | testbench: 5.* 30 | - laravel: 8.* 31 | testbench: 6.* 32 | - laravel: 9.* 33 | testbench: 7.* 34 | - laravel: 10.* 35 | testbench: 8.* 36 | - laravel: 11.* 37 | testbench: 9.* 38 | - laravel: 12.* 39 | testbench: 10.* 40 | exclude: 41 | - laravel: 5.6.* 42 | php: 8.0 43 | - laravel: 5.6.* 44 | php: 8.1 45 | - laravel: 5.6.* 46 | php: 8.2 47 | - laravel: 5.6.* 48 | php: 8.3 49 | - laravel: 5.6.* 50 | php: 8.4 51 | - laravel: 5.7.* 52 | php: 8.0 53 | - laravel: 5.7.* 54 | php: 8.1 55 | - laravel: 5.7.* 56 | php: 8.2 57 | - laravel: 5.7.* 58 | php: 8.3 59 | - laravel: 5.7.* 60 | php: 8.4 61 | - laravel: 5.8.* 62 | php: 8.0 63 | - laravel: 5.8.* 64 | php: 8.1 65 | - laravel: 5.8.* 66 | php: 8.2 67 | - laravel: 5.8.* 68 | php: 8.3 69 | - laravel: 5.8.* 70 | php: 8.4 71 | - laravel: 6.* 72 | php: 7.1 73 | - laravel: 6.* 74 | php: 8.1 75 | - laravel: 6.* 76 | php: 8.2 77 | - laravel: 6.* 78 | php: 8.3 79 | - laravel: 6.* 80 | php: 8.4 81 | - laravel: 7.* 82 | php: 7.1 83 | - laravel: 7.* 84 | php: 8.1 85 | - laravel: 7.* 86 | php: 8.2 87 | - laravel: 7.* 88 | php: 8.3 89 | - laravel: 7.* 90 | php: 8.4 91 | - laravel: 8.* 92 | php: 7.1 93 | - laravel: 8.* 94 | php: 7.2 95 | - laravel: 9.* 96 | php: 7.1 97 | - laravel: 9.* 98 | php: 7.2 99 | - laravel: 9.* 100 | php: 7.3 101 | - laravel: 9.* 102 | php: 7.4 103 | - laravel: 10.* 104 | php: 7.1 105 | - laravel: 10.* 106 | php: 7.2 107 | - laravel: 10.* 108 | php: 7.3 109 | - laravel: 10.* 110 | php: 7.4 111 | - laravel: 10.* 112 | php: 8.0 113 | - laravel: 11.* 114 | php: 7.1 115 | - laravel: 11.* 116 | php: 7.2 117 | - laravel: 11.* 118 | php: 7.3 119 | - laravel: 11.* 120 | php: 7.4 121 | - laravel: 11.* 122 | php: 8.0 123 | - laravel: 11.* 124 | php: 8.1 125 | - laravel: 12.* 126 | php: 7.1 127 | - laravel: 12.* 128 | php: 7.2 129 | - laravel: 12.* 130 | php: 7.3 131 | - laravel: 12.* 132 | php: 7.4 133 | - laravel: 12.* 134 | php: 8.0 135 | - laravel: 12.* 136 | php: 8.1 137 | 138 | name: PHP ${{ matrix.php }} - LARAVEL ${{ matrix.laravel }} - ${{ matrix.version }} - ${{ matrix.os }} 139 | 140 | steps: 141 | - name: Checkout code 142 | uses: actions/checkout@v2 143 | 144 | - name: Setup PHP 145 | uses: shivammathur/setup-php@v2 146 | with: 147 | php-version: ${{ matrix.php }} 148 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 149 | coverage: none 150 | tools: composer:v2 151 | 152 | - name: Install dependencies 153 | run: | 154 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 155 | composer update --${{ matrix.version }} --prefer-dist --no-interaction 156 | 157 | - name: Execute tests 158 | run: vendor/bin/phpunit 159 | 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Sitemappable 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/vursion/laravel-sitemappable.svg?style=flat-square)](https://packagist.org/packages/vursion/laravel-sitemappable) 4 | ![Tests](https://github.com/vursion/laravel-sitemappable/workflows/tests/badge.svg) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/vursion/laravel-sitemappable.svg?style=flat-square)](https://packagist.org/packages/vursion/laravel-sitemappable) 6 | 7 | ## Installation 8 | 9 | You can install the package via composer: 10 | 11 | ```bash 12 | composer require vursion/laravel-sitemappable 13 | ``` 14 | 15 | ***No need to register the service provider if you're using Laravel >= 5.5. 16 | The package will automatically register itself.*** 17 | Once the package is installed, you can register the service provider in `config/app.php` in the providers array: 18 | ```php 19 | 'providers' => [ 20 | ... 21 | Vursion\LaravelSitemappable\SitemappableServiceProvider::class 22 | ], 23 | ``` 24 | 25 | You need to publish the migration with: 26 | ```bash 27 | php artisan vendor:publish --provider="Vursion\LaravelSitemappable\SitemappableServiceProvider" --tag=migrations 28 | ``` 29 | 30 | You should publish the `config/sitemappable.php` config file with: 31 | ```bash 32 | php artisan vendor:publish --provider="Vursion\LaravelSitemappable\SitemappableServiceProvider" --tag=config 33 | ``` 34 | 35 | This is the content of the published config file: 36 | 37 | ```php 38 | return [ 39 | 40 | /* 41 | * This is the name of the table that will be created by the migration and 42 | * used by the Sitemappable model shipped with this package. 43 | */ 44 | 'db_table_name' => 'sitemap', 45 | 46 | /* 47 | * The generated XML sitemap is cached to speed up performance. 48 | */ 49 | 'cache' => '60 minutes', 50 | 51 | /* 52 | * The batch import will loop through this directory and search for models 53 | * that use the IsSitemappable trait. 54 | */ 55 | 'model_directory' => 'app/Models', 56 | 57 | /* 58 | * If you're extending the controller, you'll need to specify the new location here. 59 | */ 60 | 'controller' => Vursion\LaravelSitemappable\Http\Controllers\SitemappableController::class, 61 | 62 | ]; 63 | ``` 64 | 65 | ## Making a model sitemappable 66 | 67 | The required steps to make a model sitemappable are: 68 | - Add the `Vursion\LaravelSitemappable\IsSitemappable` trait. 69 | - Define a public method `toSitemappableArray` that returns an array with the (localized) URL(s). 70 | - Optionally define the conditions when a model should be sitemappable in a public method `shouldBeSitemappable`. 71 | 72 | Here's an example of a model: 73 | 74 | ```php 75 | use Illuminate\Database\Eloquent\Model; 76 | use Vursion\LaravelSitemappable\IsSitemappable; 77 | 78 | class YourModel extends Model 79 | { 80 | use IsSitemappable; 81 | 82 | public function toSitemappableArray() 83 | { 84 | return []; 85 | } 86 | 87 | public function shouldBeSitemappable() 88 | { 89 | return true; 90 | } 91 | } 92 | ``` 93 | 94 | ### toSitemappableArray 95 | 96 | You need to return an array with (localized) URL(s) of your model. 97 | 98 | ```php 99 | public function toSitemappableArray() 100 | { 101 | return [ 102 | 'nl' => 'https://www.vursion.io/nl/testen/test-slug-in-het-nederlands', 103 | 'en' => 'https://www.vursion.io/en/tests/test-slug-in-english', 104 | ]; 105 | } 106 | ``` 107 | 108 | This is an example of a model that uses [ARCANDEDEV\Localization](https://github.com/ARCANEDEV/Localization) 109 | for localized routes in combination with [spatie\laravel-translatable](https://github.com/spatie/laravel-translatable) 110 | for making Eloquent models translatable. 111 | 112 | ```php 113 | public function toSitemappableArray() 114 | { 115 | return collect(localization()->getSupportedLocalesKeys())->mapWithKeys(function ($key) { 116 | return [$key => localization()->getUrlFromRouteName($key, 'routes.your-route-name', ['slug' => $this->getTranslationWithoutFallback('slug', $key)])]; 117 | }); 118 | } 119 | ``` 120 | ### shouldBeSitemappable (conditionally sitemappable model instances) 121 | 122 | Sometimes you may need to only make a model sitemappable under certain conditions. 123 | For example, imagine you have a `App\Models\Posts\Post` model. 124 | You may only want to allow "non-draft" and "published" posts to be sitemappable. 125 | To accomplish this, you may define a `shouldBeSitemappable` method on your model: 126 | 127 | ```php 128 | public function shouldBeSitemappable() 129 | { 130 | return (! $this->draft && $this->published); 131 | } 132 | ``` 133 | 134 | ## Rebuild the sitemap from scratch 135 | 136 | If you are installing Laravel Sitemappable into an existing project, you may already have database records you need to import into your sitemap. 137 | Laravel Sitemappable provides a `sitemappable:import` Artisan command that you may use to import all of your existing records into your sitemap: 138 | 139 | ```bash 140 | php artisan sitemappable:import 141 | ``` 142 | 143 | ## Adding non-model associated routes 144 | 145 | It's very likely your project will have routes that are not associated with a model. 146 | You can add these URLs by extending the controller and returning them via the `otherRoutes` method. 147 | 148 | To publish the controller to `app/Http/Controllers/SitemappableController.php` run: 149 | 150 | ```bash 151 | php artisan vendor:publish --provider="Vursion\LaravelSitemappable\SitemappableServiceProvider" --tag=controllers 152 | ``` 153 | 154 | Don't forget to change the location of the controller in the `config/sitemappable.php` config file: 155 | 156 | ```php 157 | return [ 158 | 159 | ... 160 | 161 | /* 162 | * If you're extending the controller, you'll need to specify the new location here. 163 | */ 164 | 'controller' => App\Http\Controllers\SitemappableController::class, 165 | 166 | ... 167 | 168 | ]; 169 | ``` 170 | 171 | Just make sure you return an array of arrays with key/value pairs like the example below: 172 | 173 | ```php 174 | public function otherRoutes() 175 | { 176 | return [ 177 | [ 178 | 'nl' => 'https://www.vursion.io/nl/contacteer-ons', 179 | 'en' => 'https://www.vursion.io/en/contact-us', 180 | ], 181 | ... 182 | ]; 183 | } 184 | ``` 185 | 186 | ## Changelog 187 | 188 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 189 | 190 | ## Security 191 | 192 | If you discover any security related issues, please email jochen@celcius.be instead of using the issue tracker. 193 | 194 | ## Credits 195 | 196 | - [Jochen Sengier](https://github.com/celcius-jochen) 197 | 198 | ## License 199 | 200 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 201 | --------------------------------------------------------------------------------