├── .editorconfig
├── .gitignore
├── .php-cs-fixer.dist.php
├── LICENSE.md
├── README.md
├── composer.json
├── config
└── seo.php
├── database
├── factories
│ └── SeoFactory.php
└── migrations
│ └── 2023_01_19_171805_create_seo_table.php
├── phpunit.xml.dist
├── src
├── Casts
│ └── UrlCast.php
├── Commands
│ └── MoonShineCommand.php
├── Models
│ └── Seo.php
├── Providers
│ └── SeoServiceProvider.php
├── Rules
│ └── UrlRule.php
├── Seo.php
├── SeoManager.php
├── SeoMeta.php
└── helpers.php
├── stubs
├── moonshine_seo_resource_v1.stub
├── moonshine_seo_resource_v2.stub
└── moonshine_seo_resource_v3.stub
└── tests
├── SeoFeaturesTest.php
└── TestCase.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 4
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | vendor
3 | composer.lock
4 | node_modules
5 | .phpunit.result.cache
6 | .php-cs-fixer.cache
7 | app
8 | reports
9 | tools
10 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | in([
5 | __DIR__ . '/src',
6 | __DIR__ . '/tests',
7 | ])
8 | ->name('*.php')
9 | ->notName('*.blade.php')
10 | ->ignoreDotFiles(true)
11 | ->ignoreVCS(true);
12 |
13 | return (new PhpCsFixer\Config())
14 | ->setRules([
15 | '@PSR12' => true,
16 | 'array_syntax' => ['syntax' => 'short'],
17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'],
18 | 'no_unused_imports' => true,
19 | 'not_operator_with_successor_space' => true,
20 | 'trailing_comma_in_multiline' => true,
21 | 'phpdoc_scalar' => true,
22 | 'unary_operator_spaces' => true,
23 | 'binary_operator_spaces' => true,
24 | 'blank_line_before_statement' => [
25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
26 | ],
27 | 'phpdoc_single_line_var_spacing' => true,
28 | 'phpdoc_var_without_name' => true,
29 | 'method_argument_space' => [
30 | 'on_multiline' => 'ensure_fully_multiline',
31 | 'keep_multiple_spaces_after_comma' => true,
32 | ],
33 | 'single_trait_insert_per_statement' => true,
34 | ])
35 | ->setFinder($finder);
36 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Danil Shutsky
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ### Prolog
7 |
8 | Seo data is stored in the database in the `seo` table and is linked to pages based on the url, the url is unique for websites, therefore, the seo in this package is built from it
9 |
10 | - Easy to use
11 | - Not tied to entities
12 | - All data is cached relative to url and reset by events on the model
13 |
14 | ### Installation
15 |
16 | ```shell
17 | composer require lee-to/laravel-seo-by-url
18 | ```
19 | Publish config
20 |
21 | ```shell
22 | php artisan vendor:publish --provider="Leeto\Seo\Providers\SeoServiceProvider"
23 | ```
24 |
25 | ```shell
26 | php artisan migrate
27 | ```
28 |
29 | ### Are you a visual learner?
30 |
31 | We've recorded a [video](https://youtu.be/QjTsC1QF0co) on how to use this package. It's the best way to get started using media library
32 |
33 | ### MoonShine
34 |
35 | if you use the [MoonShine](https://moonshine-laravel.com), then publish the resource with this command
36 |
37 | ```shell
38 | php artisan seo:moonshine
39 | ```
40 |
41 | ### Get started
42 |
43 | For starters, you can choose the best usage approach for you:
44 |
45 | - Facade
46 | ```php
47 | use Leeto\Seo\Seo;
48 |
49 | // ...
50 |
51 | Seo::title('Hello world')
52 | ```
53 |
54 | - Helper
55 | ```php
56 | seo()->title('Hello world')
57 | ```
58 |
59 | - DI
60 | ```php
61 | use Leeto\Seo\SeoManager;
62 |
63 | // ...
64 |
65 | public function __invoke(SeoManager $seo)
66 | {
67 | //
68 | }
69 | ```
70 |
71 | * Ok I prefer to use the helper
72 |
73 | ### Blade directives
74 |
75 | #### Render meta tags
76 | title, descriptions, keywords, og
77 |
78 | ```html
79 |
80 |
81 |
82 |
83 | @seo
84 |
85 |
86 |
87 | ```
88 |
89 | #### Render seo text
90 |
91 | ```html
92 |
93 | @seoText('Default text')
94 |
95 | ```
96 |
97 | ### Set and save seo data
98 |
99 | - set
100 |
101 | ```php
102 | seo()->title('Im page title')
103 | ```
104 |
105 | - set and save in database
106 |
107 | ```php
108 | seo()->title('Im page title', true)
109 | ```
110 |
111 | - other tags
112 |
113 | ```php
114 | seo()->description('Im page description')
115 | seo()->keywords('Im page description')
116 | seo()->text('Im page description')
117 | seo()->og(['image' => 'link to image'])
118 | ```
119 |
120 | - get value
121 |
122 | ```php
123 | seo()->meta()->title()
124 | seo()->meta()->description()
125 | seo()->meta()->keywords()
126 | seo()->meta()->text()
127 | seo()->meta()->og()
128 | ```
129 |
130 | - get html tags
131 |
132 | ```php
133 | seo()->meta()->html()
134 | ```
135 |
136 | - save by model
137 |
138 | ```php
139 | use Leeto\Seo\Models\Seo;
140 |
141 | Seo::create([
142 | 'url' => '/',
143 | 'title' => 'Im title'
144 | ]);
145 | ```
146 |
147 | ### Default values
148 |
149 | Set in seo config `config/seo.php`
150 |
151 | ```php
152 | return [
153 | 'default' => [
154 | 'title' => 'Im title'
155 | ]
156 | ]);
157 | ```
158 |
159 | ### Inertia
160 |
161 | Use Shared Data
162 |
163 | ```php
164 | class HandleInertiaRequests extends Middleware
165 | {
166 | //
167 | public function share(Request $request)
168 | {
169 | return array_merge(parent::share($request), [
170 | // ...
171 |
172 | 'seo' => [
173 | 'title' => seo()->meta()->title(),
174 | 'description' => seo()->meta()->description(),
175 | 'keywords' => seo()->meta()->keywords(),
176 | 'og' => seo()->meta()->og(),
177 | 'text' => seo()->meta()->text(),
178 | ]
179 | ]);
180 | }
181 | //
182 | }
183 | ```
184 |
185 | ```js
186 | import { Head } from '@inertiajs/vue3'
187 |
188 |
189 | {{ $page.props.seo.title }}
190 |
191 |
192 | ```
193 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lee-to/laravel-seo-by-url",
3 | "description": "Easy seo for Laravel and MoonShine",
4 | "minimum-stability": "stable",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Danil Shutsky",
9 | "email": "info@cutcode.ru",
10 | "homepage": "https://github.com/lee-to"
11 | }
12 | ],
13 | "require": {
14 | "php": "^8.0|^8.1|^8.2"
15 | },
16 | "require-dev": {
17 | "fakerphp/faker": "^1.9.2",
18 | "phpunit/phpunit": "^9.5.8",
19 | "mockery/mockery": "^1.4.4",
20 | "orchestra/testbench": "^7.5"
21 | },
22 | "autoload": {
23 | "psr-4": {
24 | "Leeto\\Seo\\": "src/"
25 | },
26 | "files": [
27 | "src/helpers.php"
28 | ]
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "Leeto\\Seo\\Tests\\": "tests/",
33 | "Leeto\\Seo\\Database\\Factories\\": "database/factories/"
34 | }
35 | },
36 | "scripts": {
37 | "test": "vendor/bin/phpunit",
38 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
39 | },
40 | "extra": {
41 | "laravel": {
42 | "providers": [
43 | "Leeto\\Seo\\Providers\\SeoServiceProvider"
44 | ],
45 | "aliases": {
46 | "Seo": "Leeto\\Seo\\Seo"
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/config/seo.php:
--------------------------------------------------------------------------------
1 | [
5 | 'title' => env('APP_NAME', '')
6 | ]
7 | ];
8 |
--------------------------------------------------------------------------------
/database/factories/SeoFactory.php:
--------------------------------------------------------------------------------
1 |
12 | */
13 | class SeoFactory extends Factory
14 | {
15 | public function definition(): array
16 | {
17 | return [
18 | 'title' => ucfirst($this->faker->word()),
19 | 'description' => $this->faker->text(100),
20 | 'keywords' => $this->faker->text(100),
21 | 'text' => $this->faker->text(100),
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/database/migrations/2023_01_19_171805_create_seo_table.php:
--------------------------------------------------------------------------------
1 | id();
12 | $table->string('url')->unique();
13 | $table->string('title');
14 | $table->text('description')->nullable();
15 | $table->text('keywords')->nullable();
16 | $table->text('text')->nullable();
17 | $table->timestamps();
18 | });
19 | }
20 |
21 | public function down(): void
22 | {
23 | Schema::dropIfExists('seo');
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 | ./tests
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 | src
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/Casts/UrlCast.php:
--------------------------------------------------------------------------------
1 | choice(
16 | 'Choose MoonShine version (default v3)',
17 | [
18 | 1 => 'v1',
19 | 2 => 'v2',
20 | 3 => 'v3',
21 | ],
22 | 'v3'
23 | );
24 |
25 | $stub = "moonshine_seo_resource_{$version}.stub";
26 |
27 | if ($version === 'v3') {
28 | /** @var \MoonShine\Contracts\Core\DependencyInjection\ConfiguratorContract $config */
29 | $config = app(\MoonShine\Contracts\Core\DependencyInjection\ConfiguratorContract::class);
30 |
31 | $resource = $config->getDir() . '/Resources/SeoResource.php';
32 | $namespace = $config->getNamespace('\Resources');
33 | } else {
34 | $resource = \MoonShine\MoonShine::dir() . '/Resources/SeoResource.php';
35 | $namespace = \MoonShine\MoonShine::namespace('\Resources');
36 | }
37 |
38 | $contents = $this->laravel['files']->get(__DIR__ . "/../../stubs/{$stub}");
39 | $contents = str_replace('{namespace}', $namespace, $contents);
40 |
41 | $this->laravel['files']->put(
42 | $resource,
43 | $contents
44 | );
45 |
46 | $this->components->info('Now register resource in menu');
47 |
48 | return self::SUCCESS;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Models/Seo.php:
--------------------------------------------------------------------------------
1 | UrlCast::class,
34 | ];
35 |
36 | protected static function boot()
37 | {
38 | parent::boot();
39 |
40 | static::created(static fn (Seo $model) => $model->flushCache());
41 | static::updated(static fn (Seo $model) => $model->flushCache());
42 | static::deleting(static fn (Seo $model) => $model->flushCache());
43 | }
44 |
45 | public function flushCache(): void
46 | {
47 | seo()->flushCache(
48 | seo()->getCacheKey($this->url)
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Providers/SeoServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(
17 | __DIR__.'/../../config/seo.php', 'seo'
18 | );
19 | }
20 |
21 | public function boot(): void
22 | {
23 | $this->loadMigrationsFrom(__DIR__.'/../../database/migrations');
24 |
25 | $this->publishes([
26 | __DIR__.'/../../config/seo.php' => config_path('seo.php'),
27 | ]);
28 |
29 | $this->app->singleton(SeoManager::class);
30 |
31 | Blade::directive('seo', static function ($expression) {
32 | return '{!! seo() !!}';
33 | });
34 |
35 | Blade::directive('seoText', static function ($expression) {
36 | return '{!! seo()->meta()->text('.$expression.') !!}';
37 | });
38 |
39 | if ($this->app->runningInConsole()) {
40 | $this->commands([
41 | MoonShineCommand::class,
42 | ]);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Rules/UrlRule.php:
--------------------------------------------------------------------------------
1 | data = $this->cachedByUrl();
31 | }
32 |
33 | private function data(): ?Seo
34 | {
35 | return $this->data;
36 | }
37 |
38 | private function custom(): array
39 | {
40 | return $this->custom;
41 | }
42 |
43 | public function url(string $url = null): Stringable
44 | {
45 | return str($url ?? request()->getRequestUri())
46 | ->trim('/')
47 | ->prepend('/');
48 | }
49 |
50 | public function getCacheKey(string $url = null): string
51 | {
52 | $url = (string) $this->url($url);
53 | $parsedUrl = parse_url($url);
54 | $path = $parsedUrl["path"] ?? '/';
55 | $query = isset($parsedUrl["query"]) ? $this->normalizeQuery($parsedUrl["query"]) : '';
56 |
57 | return md5($path . '?' . $query);
58 | }
59 |
60 | protected function normalizeQuery(string $query): string
61 | {
62 | parse_str($query, $params);
63 | ksort($params);
64 | return http_build_query($params);
65 | }
66 |
67 | public function meta(): SeoMeta
68 | {
69 | if ($this->custom()) {
70 | $data = $this->data()
71 | ? array_merge(Arr::only($this->data()->toArray(), $this->data()->getFillable()), $this->custom())
72 | : $this->custom();
73 |
74 | return SeoMeta::fromArray($data);
75 | }
76 |
77 | return SeoMeta::fromModel($this->data())
78 | ->default(config('seo.default', []));
79 | }
80 |
81 | public function cachedByUrl(): Model|Seo|null
82 | {
83 | $data = cache()->rememberForever(
84 | $this->getCacheKey(),
85 | fn() => $this->byUrl() ?? false
86 | );
87 |
88 | return $data === false ? null : $data;
89 | }
90 |
91 | public function flushCache(string $key = null): void
92 | {
93 | cache()->forget($key ?? $this->getCacheKey());
94 |
95 | $this->data = $this->cachedByUrl();
96 | }
97 |
98 | public function byUrl(): Model|Seo|null
99 | {
100 | $url = (string)$this->url();
101 |
102 | $seo = Seo::query()->where("url", $url)->first();
103 |
104 | if ($seo) {
105 | return $seo;
106 | }
107 |
108 | $baseUrl = parse_url($url, PHP_URL_PATH);
109 | $seo = Seo::query()->where("url", $baseUrl)->first();
110 |
111 | if ($seo) {
112 | return $seo;
113 | }
114 |
115 | return null;
116 | }
117 |
118 | public function __toString(): string
119 | {
120 | return $this->meta()->html();
121 | }
122 |
123 | public function __call(string $name, array $arguments)
124 | {
125 | if (in_array($name, ['title', 'description', 'keywords', 'text', 'og'])) {
126 | $this->custom[$name] = $arguments[0] ?? '';
127 |
128 | if ($arguments[1] ?? false) {
129 | $data = Seo::query()->updateOrCreate(
130 | ['url' => (string) $this->url()],
131 | Arr::except($this->custom, ['og']),
132 | );
133 |
134 | $this->persisted[$data->url] = $data;
135 | }
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/SeoMeta.php:
--------------------------------------------------------------------------------
1 | model = $model ?? new Seo();
21 | $this->og = $og;
22 | }
23 |
24 | public static function fromModel(?Seo $model = null): SeoMeta
25 | {
26 | return new self($model);
27 | }
28 |
29 | public static function fromArray(array $data): SeoMeta
30 | {
31 | return new self(
32 | new Seo(collect($data)->except('og')->toArray()),
33 | $data['og'] ?? []
34 | );
35 | }
36 |
37 | public function default(array $default): self
38 | {
39 | $this->default = $default;
40 |
41 | return $this;
42 | }
43 |
44 | public function og(): array
45 | {
46 | return $this->og;
47 | }
48 |
49 | public function model(): Seo
50 | {
51 | return $this->model;
52 | }
53 |
54 | public function title(): ?string
55 | {
56 | return $this->model()->title ?? $this->default['title'] ?? null;
57 | }
58 |
59 | public function description(): ?string
60 | {
61 | return $this->model()->description ?? $this->default['description'] ?? null;
62 | }
63 |
64 | public function keywords(): ?string
65 | {
66 | return $this->model()->keywords ?? $this->default['keywords'] ?? null;
67 | }
68 |
69 | public function text(string $text = null): ?string
70 | {
71 | return $this->model()->text
72 | ? $this->model()->text
73 | : $text ?? $this->default['text'] ?? null;
74 | }
75 |
76 | public function html(): string
77 | {
78 | $html = str('');
79 |
80 | if ($this->title()) {
81 | $html = $html->append("{$this->title()}")->append(PHP_EOL);
82 | $html = $html->append($this->metaTag($this->title(), name: 'title'));
83 | }
84 |
85 | if ($this->description()) {
86 | $html = $html->append($this->metaTag($this->description(), name: 'description'));
87 | }
88 |
89 | if ($this->keywords()) {
90 | $html = $html->append($this->metaTag($this->keywords(), name: 'keywords'));
91 | }
92 |
93 | if ($this->og()) {
94 | foreach ($this->og() as $name => $content) {
95 | $html = $html->append($this->metaTag($content, property: "og:$name"));
96 | }
97 | }
98 |
99 | return $html->value();
100 | }
101 |
102 | private function metaTag(string $content, string $name = null, string $property = null): string
103 | {
104 | return "".PHP_EOL;
105 | }
106 |
107 | public function __toString(): string
108 | {
109 | return $this->html();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/helpers.php:
--------------------------------------------------------------------------------
1 | showOnExport()
37 | ->useOnImport()
38 | ->sortable(),
39 |
40 | Text::make('Url')
41 | ->required()
42 | ->showOnExport()
43 | ->useOnImport(),
44 |
45 | Text::make('Title')
46 | ->required()
47 | ->showOnExport()
48 | ->useOnImport(),
49 |
50 | Text::make('Description')
51 | ->showOnExport()
52 | ->useOnImport(),
53 |
54 | Text::make('Keywords')
55 | ->showOnExport()
56 | ->useOnImport(),
57 |
58 | TinyMce::make('Text')
59 | ->hideOnIndex()
60 | ->showOnExport()
61 | ->useOnImport()
62 | ])
63 | ];
64 | }
65 |
66 | public function rules(Model $item): array
67 | {
68 | return [
69 | 'title' => ['required', 'string', 'min:1'],
70 | 'url' => [
71 | 'required',
72 | 'string',
73 | new UrlRule,
74 | Rule::unique('seo')->ignoreModel($item)
75 | ]
76 | ];
77 | }
78 |
79 | public function search(): array
80 | {
81 | return ['id', 'url'];
82 | }
83 |
84 | public function filters(): array
85 | {
86 | return [
87 | TextFilter::make('Url')
88 | ];
89 | }
90 |
91 | public function itemActions(): array
92 | {
93 | return [
94 | ItemAction::make('Go to', function (Seo $item) {
95 | header('Location:'.$item->url);
96 | die();
97 | })->icon('clip')
98 | ];
99 | }
100 |
101 | public function actions(): array
102 | {
103 | return [
104 | FiltersAction::make('Filters'),
105 | ExportAction::make('Export'),
106 | ImportAction::make('Import')
107 | ];
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/stubs/moonshine_seo_resource_v2.stub:
--------------------------------------------------------------------------------
1 | showOnExport()
32 | ->useOnImport()
33 | ->sortable(),
34 |
35 | Text::make('Url')
36 | ->required()
37 | ->showOnExport()
38 | ->useOnImport(),
39 |
40 | Text::make('Title')
41 | ->required()
42 | ->showOnExport()
43 | ->useOnImport(),
44 |
45 | Text::make('Description')
46 | ->showOnExport()
47 | ->useOnImport(),
48 |
49 | Text::make('Keywords')
50 | ->showOnExport()
51 | ->useOnImport(),
52 |
53 | TinyMce::make('Text')
54 | ->hideOnIndex()
55 | ->showOnExport()
56 | ->useOnImport()
57 | ])
58 | ];
59 | }
60 |
61 | public function rules(Model $item): array
62 | {
63 | return [
64 | 'title' => ['required', 'string', 'min:1'],
65 | 'url' => [
66 | 'required',
67 | 'string',
68 | new UrlRule,
69 | Rule::unique('seo')->ignoreModel($item)
70 | ]
71 | ];
72 | }
73 |
74 | public function search(): array
75 | {
76 | return ['id', 'url'];
77 | }
78 |
79 | public function filters(): array
80 | {
81 | return [
82 | Text::make('Url')->required(),
83 | ];
84 | }
85 |
86 | public function indexButtons(): array
87 | {
88 | return [
89 | ActionButton::make('Go to', static fn (Seo $item) => $item->url)
90 | ->icon('heroicons.outline.paper-clip')
91 | ->blank()
92 | ];
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/stubs/moonshine_seo_resource_v3.stub:
--------------------------------------------------------------------------------
1 | required(),
45 | Text::make('Title')
46 | ->required(),
47 | Text::make('Description'),
48 | Text::make('Keywords'),
49 | TinyMce::make('Text'),
50 | ])
51 | ];
52 | }
53 |
54 | protected function indexFields(): iterable
55 | {
56 | return [
57 | ID::make()
58 | ->sortable(),
59 | Text::make('Url'),
60 | Text::make('Title'),
61 | Text::make('Description'),
62 | Text::make('Keywords'),
63 | ];
64 | }
65 |
66 | protected function detailFields(): iterable
67 | {
68 | return [
69 | ID::make(),
70 | Text::make('Url'),
71 | Text::make('Title'),
72 | Text::make('Description'),
73 | Text::make('Keywords'),
74 | TinyMce::make('Text')
75 | ];
76 | }
77 |
78 | protected function exportFields(): iterable
79 | {
80 | return [
81 | ID::make(),
82 | Text::make('Url'),
83 | Text::make('Title'),
84 | Text::make('Description'),
85 | Text::make('Keywords'),
86 | ];
87 | }
88 |
89 | protected function importFields(): iterable
90 | {
91 | return $this->exportFields();
92 | }
93 |
94 | protected function rules($item): array
95 | {
96 | return [
97 | 'title' => [
98 | 'required',
99 | 'string',
100 | 'min:3'
101 | ],
102 | 'url' => [
103 | 'required',
104 | 'string',
105 | new UrlRule,
106 | Rule::unique('seo')->ignoreModel($item)
107 | ]
108 | ];
109 | }
110 |
111 | public function search(): array
112 | {
113 | return ['id', 'url', 'title'];
114 | }
115 |
116 | public function filters(): array
117 | {
118 | return [
119 | Text::make('Url'),
120 | Text::make('Title'),
121 | ];
122 | }
123 |
124 | public function import(): ?Handler
125 | {
126 | return ImportHandler::make('Импорт')
127 | ->notifyUsers(fn() => [auth()->id()]);
128 | }
129 |
130 | public function export(): ?Handler
131 | {
132 | return ExportHandler::make('Экспорт')
133 | ->filename('seo_resource_export__' . date('Ymd-His'))
134 | ->dir('export_files')
135 | ->notifyUsers(fn() => [auth()->id()]);
136 | }
137 |
138 | public function indexButtons(): ListOf
139 | {
140 | return parent::indexButtons()->add(
141 | ActionButton::make('На страницу', static fn (Seo $item) => $item->url)
142 | ->icon('arrow-top-right-on-square')
143 | ->blank()
144 | );
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/tests/SeoFeaturesTest.php:
--------------------------------------------------------------------------------
1 | app->instance('request', Request::create('/test'));
22 |
23 | $seo = SeoModel::query()->create([
24 | 'url' => '/test',
25 | 'title' => 'Title',
26 | 'description' => 'Description',
27 | ]);
28 |
29 | $this->assertEquals($seo->title, seo()->meta()->title());
30 |
31 | $seo->update([
32 | 'title' => 'New title',
33 | ]);
34 |
35 | $this->assertEquals($seo->title, seo()->meta()->title());
36 |
37 | seo()->title('Custom title');
38 |
39 | $this->assertEquals('Custom title', seo()->meta()->title());
40 | $this->assertEquals('Description', seo()->meta()->description());
41 | }
42 |
43 | /**
44 | * @test
45 | * @return void
46 | */
47 | public function it_basic_usage_with_params(): void
48 | {
49 | $this->app->instance('request', Request::create('/test?page=2'));
50 |
51 | $seo = SeoModel::query()->create([
52 | 'url' => '/test?page=2',
53 | 'title' => 'Title',
54 | 'description' => 'Description params',
55 | ]);
56 |
57 | $this->assertEquals($seo->title, seo()->meta()->title());
58 |
59 | $seo->update([
60 | 'title' => 'New title params',
61 | ]);
62 |
63 | $this->assertEquals($seo->title, seo()->meta()->title());
64 |
65 | seo()->title('Custom title params');
66 |
67 | $this->assertEquals('Custom title params', seo()->meta()->title());
68 | $this->assertEquals('Description params', seo()->meta()->description());
69 | }
70 |
71 | /**
72 | * @test
73 | * @return void
74 | */
75 | public function it_success_url_generated(): void
76 | {
77 | $item = SeoModel::query()->create([
78 | 'url' => 'https://google.com/path',
79 | 'title' => 'Title',
80 | ]);
81 |
82 | $this->assertEquals('/path', $item->url);
83 |
84 | $item = SeoModel::query()->create([
85 | 'url' => 'https://google.com',
86 | 'title' => 'Title',
87 | ]);
88 |
89 | $this->assertEquals('/', $item->url);
90 | }
91 |
92 | /**
93 | * @test
94 | * @return void
95 | */
96 | public function it_empty_seo(): void
97 | {
98 | $this->app->instance('request', Request::create('/empty'));
99 |
100 | $this->assertEquals('', seo()->meta()->title());
101 | }
102 |
103 | /**
104 | * @test
105 | * @return void
106 | */
107 | public function it_html_value(): void
108 | {
109 | seo()->title('Custom title');
110 |
111 | $this->assertStringContainsString('Custom title', seo()->meta()->html());
112 | }
113 |
114 | /**
115 | * @test
116 | * @return void
117 | */
118 | public function it_html_og_value(): void
119 | {
120 | seo()->og(['image' => 'test']);
121 |
122 | $this->assertStringContainsString("", seo()->meta()->html());
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | artisan('cache:clear');
20 |
21 | $this->refreshApplication();
22 | $this->loadLaravelMigrations();
23 | $this->loadMigrationsFrom(realpath('./database/migrations'));
24 |
25 | Factory::guessFactoryNamesUsing(function ($factory) {
26 | $factoryBasename = class_basename($factory);
27 |
28 | return "Leeto\Seo\Database\Factories\\$factoryBasename".'Factory';
29 | });
30 | }
31 |
32 | protected function getPackageProviders($app): array
33 | {
34 | return [
35 | SeoServiceProvider::class,
36 | ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------