diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e05cc5c8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# This file is for unifying the coding style for different editors and IDEs. +# More information at https://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..240381ad --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all tests and documentation with "export-ignore". +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.php.cs export-ignore +/.travis.yml export-ignore +/phpunit.xml export-ignore +/tests export-ignore \ No newline at end of file diff --git a/.github/workflows/php-cs-fixer.yaml b/.github/workflows/php-cs-fixer.yaml new file mode 100644 index 00000000..f55d1fa8 --- /dev/null +++ b/.github/workflows/php-cs-fixer.yaml @@ -0,0 +1,23 @@ +name: Check & fix styling + +on: [push] + +jobs: + php-cs-fixer: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Run PHP CS Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php_cs.dist.php --allow-risky=yes + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Fix styling diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml new file mode 100644 index 00000000..44bab9c2 --- /dev/null +++ b/.github/workflows/run-tests.yaml @@ -0,0 +1,39 @@ +name: Tests + +on: [push, pull_request, workflow_dispatch] + +jobs: + php_tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [7.4, 8.0] + laravel: [8.*, 7.*] + dependency-version: [prefer-lowest, prefer-stable] + include: + - laravel: 8.* + testbench: 6.* + - laravel: 7.* + testbench: 5.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + + - name: Run Tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..cbdf8444 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +.idea +.env +.phpunit.result.cache +.php_cs.cache +.php-cs-fixer.cache +coverage +node_modules +tests/__fixtures__/content/*.yaml +tests/__fixtures__/users/*.yaml +vendor diff --git a/.php_cs.dist.php b/.php_cs.dist.php new file mode 100644 index 00000000..d612a548 --- /dev/null +++ b/.php_cs.dist.php @@ -0,0 +1,35 @@ +in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, + 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + 'single_trait_insert_per_statement' => true, + ]) + ->setFinder($finder); diff --git a/README.md b/README.md new file mode 100644 index 00000000..9e0c7516 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Advanced SEO +An advanced SEO addon for Statamic + +## Credits +Developed by [Michael Aerni](https://www.michaelaerni.ch) diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..55995196 --- /dev/null +++ b/composer.json @@ -0,0 +1,64 @@ +{ + "name": "aerni/advanced-seo", + "description": "An advanced SEO addon for Statamic.", + "keywords": [ + "statamic", + "seo" + ], + "homepage": "https://github.com/aerni/statamic-advanced-seo", + "license": "proprietary", + "authors": [ + { + "name": "Michael Aerni", + "email": "hello@michaelaerni.ch", + "homepage": "https://michaelaerni.ch", + "role": "Developer" + } + ], + "require": { + "php": "^7.4 | ^8.0", + "spatie/laravel-ray": "^1.24", + "statamic/cms": "^3.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "nunomaduro/collision": "^5.4", + "orchestra/testbench": "^6.17", + "phpunit/phpunit": "^9.5" + }, + "autoload": { + "psr-4": { + "Aerni\\AdvancedSeo\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Aerni\\AdvancedSeo\\Tests\\": "tests" + }, + "classmap": [ + "tests/TestCase.php" + ] + }, + "scripts": { + "test": "vendor/bin/phpunit", + "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "extra": { + "statamic": { + "name": "Advanced SEO", + "description": "An advanced SEO addon for Statamic." + }, + "laravel": { + "providers": [ + "Aerni\\AdvancedSeo\\ServiceProvider" + ] + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/advanced-seo.php b/config/advanced-seo.php new file mode 100644 index 00000000..ebf0f5b1 --- /dev/null +++ b/config/advanced-seo.php @@ -0,0 +1,120 @@ + true, + + /* + |-------------------------------------------------------------------------- + | Social Images + |-------------------------------------------------------------------------- + | + | Configure the options for your social images. + | + */ + + 'social_images' => [ + + /* + |-------------------------------------------------------------------------- + | Social Images Generator + |-------------------------------------------------------------------------- + | + | Do you want to enable the Social Images Generator? + | This requires Puppeteer and Browsershot. + | + */ + + 'generator' => false, + + /* + |-------------------------------------------------------------------------- + | Open Graph Images + |-------------------------------------------------------------------------- + | + | Do you want to enable the Open Graph Images settings? + | + */ + + 'open_graph' => true, + + /* + |-------------------------------------------------------------------------- + | Twitter Images + |-------------------------------------------------------------------------- + | + | Do you want to enable the Twitter Images settings? + | + */ + + 'twitter' => true, + + ], + + /* + |-------------------------------------------------------------------------- + | Trackers + |-------------------------------------------------------------------------- + | + | Configure the options for your trackers + | + */ + + 'trackers' => [ + + /* + |-------------------------------------------------------------------------- + | Site Verification + |-------------------------------------------------------------------------- + | + | Do you want to enable the Site Verification settings? + | + */ + + 'site_verification' => true, + + /* + |-------------------------------------------------------------------------- + | Fathom Analytics + |-------------------------------------------------------------------------- + | + | Do you want to enable the Fathom Analytics settings? + | + */ + + 'fathom' => true, + + /* + |-------------------------------------------------------------------------- + | Cloudflare Analytics + |-------------------------------------------------------------------------- + | + | Do you want to enable the Cloudflare Analytics settings? + | + */ + + 'cloudflare' => true, + + /* + |-------------------------------------------------------------------------- + | Google Tag Manager + |-------------------------------------------------------------------------- + | + | Do you want to enable the Google Tag Manager settings? + | + */ + + 'google_tag_manager' => true, + + ], + +]; diff --git a/package.json b/package.json new file mode 100644 index 00000000..3ec5f7e5 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "scripts": { + "development": "mix", + "watch": "mix watch", + "watch-poll": "mix watch -- --watch-options-poll=1000", + "hot": "mix watch --hot", + "production": "mix --production" + }, + "dependencies": { + "axios": "^0.21.1", + "tailwindcss": "^2.1.2", + "vue": "^2.6.12" + }, + "devDependencies": { + "laravel-mix": "^6.0.19", + "vue-loader": "^15.9.7", + "vue-template-compiler": "^2.6.12" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..2034c361 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + ./tests + + + + + ./src + + + diff --git a/resources/css/advanced-seo.css b/resources/css/advanced-seo.css new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/resources/css/advanced-seo.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/resources/js/advanced-seo.js b/resources/js/advanced-seo.js new file mode 100644 index 00000000..e69de29b diff --git a/resources/views/seo.antlers.html b/resources/views/seo.antlers.html new file mode 100644 index 00000000..e865e4d7 --- /dev/null +++ b/resources/views/seo.antlers.html @@ -0,0 +1,28 @@ +{{# + @name SEO + @desc The SEO partial rendered in the of your page. You don't need to use this file if you plan on using an addon for SEO. +#}} + +{{# Basic Meta Tags #}} +{{ partial:snippets/seo/basic }} + +{{# Favicons #}} +{{ partial:snippets/seo/favicons }} + +{{# Knowledge Graph #}} +{{ partial:snippets/seo/knowledge_graph }} + +{{# Open Graph #}} +{{ partial:snippets/seo/open_graph }} + +{{# Twitter #}} +{{ partial:snippets/seo/twitter }} + +{{# Canonical URL #}} +{{ partial:snippets/seo/canonical_url }} + +{{# Robots Indexing #}} +{{ partial:snippets/seo/indexing }} + +{{# Trackers #}} +{{ partial:snippets/seo/trackers }} diff --git a/resources/views/sitemap.antlers.html b/resources/views/sitemap.antlers.html new file mode 100644 index 00000000..71c8aed6 --- /dev/null +++ b/resources/views/sitemap.antlers.html @@ -0,0 +1,20 @@ +{{# + @name Sitemap + @desc The sitemap template that renders a SEO friendy sitemap.xml. +#}} + +{{ xml_header }} + + {{ seo:sitemap_collections }} + {{ collection from="{handle}" seo_noindex:isnt="true" as="results" }} + {{ results }} + + {{ permalink }} + {{ updated_at format="Y-m-d"}} + {{ sitemap_change_frequency ? sitemap_change_frequency : 'weekly' }} + {{ sitemap_priority ? sitemap_priority : '0.5' }} + + {{ /results }} + {{ /collection }} + {{ /seo:sitemap_collections }} + diff --git a/resources/views/snippets/_basic.antlers.html b/resources/views/snippets/_basic.antlers.html new file mode 100644 index 00000000..aa5ff7ec --- /dev/null +++ b/resources/views/snippets/_basic.antlers.html @@ -0,0 +1,12 @@ +{{# Meta Title #}} + + {{ yield:seo_title }} + {{ seo_title ?? title }} + {{ seo:title_separator ?? " | " }} + {{ seo:site_name ?? config:app:name }} + + +{{# Meta Description #}} +{{ if seo_description }} + +{{ /if }} diff --git a/resources/views/snippets/_canonical_url.antlers.html b/resources/views/snippets/_canonical_url.antlers.html new file mode 100644 index 00000000..ee53a342 --- /dev/null +++ b/resources/views/snippets/_canonical_url.antlers.html @@ -0,0 +1,16 @@ +{{# hreflang Links #}} +{{ if ! seo_noindex && seo_canonical_type === 'entry' && current_full_url === permalink }} + {{ locales }} + + {{ /locales }} +{{ /if }} + +{{ if ! seo_noindex }} + {{ if seo_canonical_type === 'current' }} + + {{ elseif seo_canonical_type === 'external' }} + + {{ elseif seo_canonical_type === 'entry' }} + + {{ /if }} +{{ /if }} diff --git a/resources/views/snippets/_favicons.antlers.html b/resources/views/snippets/_favicons.antlers.html new file mode 100644 index 00000000..2b01cec7 --- /dev/null +++ b/resources/views/snippets/_favicons.antlers.html @@ -0,0 +1,25 @@ +{{# + @name Favicons + @desc The favicons snippet for the `` to include Peak generated favicons. +#}} + +{{ if seo:generate_favicons }} + {{# The main favicon SVG #}} + + + {{# Single color Safari mask-icon #}} + + + {{# The 180x180px PNG favicon for iOS devices #}} + {{ if seo:favicon_ios_override }} + + {{ else }} + + {{ /if }} + + {{# The manifest.json that includes the 512x512px PNG favicon for Android devices #}} + + + {{# The background color for Android Chrome #}} + +{{ /if }} diff --git a/resources/views/snippets/_indexing.antlers.html b/resources/views/snippets/_indexing.antlers.html new file mode 100644 index 00000000..3be5d0f0 --- /dev/null +++ b/resources/views/snippets/_indexing.antlers.html @@ -0,0 +1,15 @@ +{{ if environment === "production" }} + + {{ if seo_noindex && seo_nofollow }} + + {{ elseif seo_nofollow }} + + {{ elseif seo_noindex }} + + {{ /if }} + +{{ else }} + + + +{{ /if }} diff --git a/resources/views/snippets/_knowledge_graph.antlers.html b/resources/views/snippets/_knowledge_graph.antlers.html new file mode 100644 index 00000000..b889e896 --- /dev/null +++ b/resources/views/snippets/_knowledge_graph.antlers.html @@ -0,0 +1,58 @@ +{{# Global JSON-LD Schema #}} +{{ if seo:json_ld_type === 'organization' }} + +{{ /if }} + +{{# Global JSON-LD Schema #}} +{{ if seo:json_ld_type === 'person' }} + +{{ /if }} + +{{# Global JSON-LD Schema #}} +{{ if seo:json_ld_type === 'custom' }} + +{{ /if }} + +{{# Per Entry JSON-LD Schema #}} +{{ if json_ld }} + +{{ /if }} + +{{# Global JSON-LD Breadcrumbs #}} +{{ if seo:breadcrumbs && segment_1 }} + +{{ /if }} diff --git a/resources/views/snippets/_open_graph.antlers.html b/resources/views/snippets/_open_graph.antlers.html new file mode 100644 index 00000000..0da90493 --- /dev/null +++ b/resources/views/snippets/_open_graph.antlers.html @@ -0,0 +1,12 @@ + + + + + +{{ if og_description or seo_description }} + +{{ /if }} + +{{ if og_image or seo:og_image }} + +{{ /if }} diff --git a/resources/views/snippets/_trackers.antlers.html b/resources/views/snippets/_trackers.antlers.html new file mode 100644 index 00000000..4f6f8796 --- /dev/null +++ b/resources/views/snippets/_trackers.antlers.html @@ -0,0 +1,41 @@ +{{ if environment === "production" }} + + {{# Google Site Verification #}} + {{ if seo:use_google_site_verification }} + + {{ /if }} + + {{# Fathom #}} + {{ if seo:use_fathom }} + + {{ /if }} + + {{# Cloudflare Web Analytics #}} + {{ if seo:use_cloudflare_web_analytics }} + + {{ /if }} + + {{# Google Tag Manager #}} + {{ if seo:use_google_tag_manager }} + + + {{# Yield this section in all your layouts after the opening #}} + {{ section:seo_body }} + + {{ /section:seo_body }} + {{ /if }} + +{{ /if }} diff --git a/resources/views/snippets/_twitter.antlers.html b/resources/views/snippets/_twitter.antlers.html new file mode 100644 index 00000000..77f23e7d --- /dev/null +++ b/resources/views/snippets/_twitter.antlers.html @@ -0,0 +1,26 @@ + + + +{{ if twitter_description or seo_description }} + +{{ /if }} + +{{ if seo:twitter_handle }} + +{{ /if }} + +{{ if twitter_image }} + {{ twitter_image }} + + {{ if alt }} + + {{ /if }} + {{ /twitter_image }} +{{ elseif seo:twitter_image }} + {{ seo:twitter_image }} + + {{ if alt }} + + {{ /if }} + {{ /seo:twitter_image }} +{{ /if }} diff --git a/resources/views/social_images/layout.antlers.html b/resources/views/social_images/layout.antlers.html new file mode 100644 index 00000000..924311f1 --- /dev/null +++ b/resources/views/social_images/layout.antlers.html @@ -0,0 +1,33 @@ +{{# + @name Social Images + @desc The template for generating Social Images. By default two sizes, one for the regular OG protocol and one for Twitter. +#}} + + + + + + + + + + + Social Images / {{ get_content:id }}{{ title }}{{ /get_content:id }} + + + + + + + + + + {{ partial:social_images/template selector="og" }} + {{ partial:social_images/template selector="twitter" }} + + + + + diff --git a/resources/views/social_images/template.antlers.html b/resources/views/social_images/template.antlers.html new file mode 100644 index 00000000..4a0aae00 --- /dev/null +++ b/resources/views/social_images/template.antlers.html @@ -0,0 +1,14 @@ +
+
+ {{ svg src="logo-primary.svg" a11y="Apotheker" class="fill-current" }} +
+
diff --git a/src/Blueprints/Sections/FaviconsSection.php b/src/Blueprints/Sections/FaviconsSection.php new file mode 100644 index 00000000..c55b7313 --- /dev/null +++ b/src/Blueprints/Sections/FaviconsSection.php @@ -0,0 +1,250 @@ +fields(); + + if (empty($fields)) { + return []; + } + + return [ + 'display' => 'Favicons', + 'fields' => $this->fields(), + ]; + } + + public function fields(): array + { + $fields = collect(); + + if (config('advanced-seo.favicons', true)) { + $fields->push($this->faviconsSection()); + } + + return $fields->flatten(1)->toArray(); + } + + protected function faviconsSection(): array + { + return [ + [ + 'handle' => 'section_favicons', + 'field' => [ + 'type' => 'section', + 'listable' => 'hidden', + 'display' => 'Favicons', + 'instructions' => 'Automatically generate favicons for different devices. This requires the [PHP Imagick Extension](https://github.com/Imagick/imagick).', + ], + ], + [ + 'handle' => 'generate_favicons', + 'field' => [ + 'display' => 'Generate Favicons', + 'type' => 'toggle', + 'icon' => 'toggle', + 'instructions' => 'Activate to generate favicons.', + 'listable' => 'hidden', + ], + ], + [ + 'handle' => 'favicon_svg', + 'field' => [ + 'mode' => 'list', + 'container' => 'seo', + 'restrict' => true, + 'allow_uploads' => true, + 'max_files' => 1, + 'type' => 'assets', + 'localizable' => false, + 'listable' => 'hidden', + 'folder' => 'favicons', + 'display' => 'Favicon (SVG)', + 'instructions' => 'Add your favicon as SVG file.', + 'width' => 50, + 'validate' => [ + 'required_if:generate_favicons,true', + 'image', + 'mimes:svg', + ], + 'if' => [ + 'generate_favicons' => 'equals true', + ], + ], + ], + [ + 'handle' => 'section_favicon_colors', + 'field' => [ + 'type' => 'section', + 'listable' => 'hidden', + 'display' => 'Favicon Colors', + 'instructions' => 'Configure your favicon colors.', + 'if' => [ + 'generate_favicons' => 'equals true', + ], + ], + ], + [ + 'handle' => 'favicon_safari_color', + 'field' => [ + 'theme' => 'nano', + 'lock_opacity' => true, + 'default_color_mode' => 'HEXA', + 'color_modes' => [ + 'hex', + ], + 'display' => 'Safari (mask-icon)', + 'type' => 'color', + 'icon' => 'color', + 'listable' => 'hidden', + 'instructions' => 'The color of your favicon in Safari.', + 'width' => 50, + 'validate' => [ + 'required_if:generate_favicons,true', + ], + 'if' => [ + 'generate_favicons' => 'equals true', + ], + ], + ], + [ + 'handle' => 'favicon_ios_color', + 'field' => [ + 'theme' => 'nano', + 'lock_opacity' => true, + 'default_color_mode' => 'HEXA', + 'color_modes' => [ + 'hex', + ], + 'display' => 'iOS (apple-touch-icon)', + 'type' => 'color', + 'icon' => 'color', + 'listable' => 'hidden', + 'instructions' => 'The background color of your favicon on iOS.', + 'width' => 50, + 'validate' => [ + 'required_if:generate_favicons,true', + ], + 'if' => [ + 'generate_favicons' => 'equals true', + ], + ], + ], + [ + 'handle' => 'favicon_android_chrome_color', + 'field' => [ + 'theme' => 'nano', + 'lock_opacity' => true, + 'default_color_mode' => 'HEXA', + 'color_modes' => [ + 'hex', + ], + 'display' => 'Android Chrome', + 'type' => 'color', + 'icon' => 'color', + 'listable' => 'hidden', + 'instructions' => 'The background color of your favicon on Android Chrome.', + 'width' => 50, + 'validate' => [ + 'required_if:generate_favicons,true', + ], + 'if' => [ + 'generate_favicons' => 'equals true', + ], + ], + ], + [ + 'handle' => 'section_favicon_overrides', + 'field' => [ + 'type' => 'section', + 'listable' => 'hidden', + 'display' => 'Favicon Overrides', + 'instructions' => 'You may override the automatically generated favicons with your own.', + 'if' => [ + 'generate_favicons' => 'equals true', + ], + ], + ], + [ + 'handle' => 'favicon_safari_override', + 'field' => [ + 'mode' => 'list', + 'container' => 'seo', + 'restrict' => true, + 'allow_uploads' => true, + 'max_files' => 1, + 'type' => 'assets', + 'localizable' => false, + 'listable' => 'hidden', + 'folder' => 'favicons', + 'display' => 'Safari (mask-icon)', + 'instructions' => 'A single color and as flattened as possible SVG. This will use the `Safari` color defined above.', + 'width' => 50, + 'if' => [ + 'generate_favicons' => 'equals true', + ], + 'validate' => [ + 'image', + 'mimes:svg', + ], + ], + ], + [ + 'handle' => 'favicon_ios_override', + 'field' => [ + 'mode' => 'list', + 'container' => 'seo', + 'restrict' => true, + 'allow_uploads' => true, + 'max_files' => 1, + 'type' => 'assets', + 'localizable' => false, + 'listable' => 'hidden', + 'folder' => 'favicons', + 'display' => 'iOS (apple-touch-icon)', + 'instructions' => 'A `180x180px` PNG for iOS devices.', + 'width' => 50, + 'validate' => [ + 'image', + 'mimes:png', + 'dimensions:width=180,height=180', + ], + 'if' => [ + 'generate_favicons' => 'equals true', + ], + ], + ], + [ + 'handle' => 'favicon_android_chrome_override', + 'field' => [ + 'mode' => 'list', + 'container' => 'seo', + 'restrict' => true, + 'allow_uploads' => true, + 'max_files' => 1, + 'type' => 'assets', + 'localizable' => false, + 'listable' => 'hidden', + 'folder' => 'favicons', + 'display' => 'Android Chrome', + 'instructions' => 'A `512x512px` PNG for Android devices.', + 'width' => 50, + 'validate' => [ + 'image', + 'mimes:png', + 'dimensions:width=512,height=512', + ], + 'if' => [ + 'generate_favicons' => 'equals true', + ], + ], + ], + ]; + } +} diff --git a/src/Blueprints/Sections/GeneralSection.php b/src/Blueprints/Sections/GeneralSection.php new file mode 100644 index 00000000..98f1822f --- /dev/null +++ b/src/Blueprints/Sections/GeneralSection.php @@ -0,0 +1,207 @@ +fields(); + + if (empty($fields)) { + return []; + } + + return [ + 'display' => 'General', + 'fields' => $this->fields(), + ]; + } + + public function fields(): array + { + $fields = collect(); + + $fields->push($this->generalSection()); + + return $fields->flatten(1)->toArray(); + } + + protected function generalSection(): array + { + return [ + [ + 'handle' => 'section_titles', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure how your site titles appear.', + 'display' => 'Titles', + ], + ], + [ + 'handle' => 'site_name', + 'field' => [ + 'input_type' => 'text', + 'type' => 'text', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Website Name', + 'instructions' => 'Set the name of the website.', + 'width' => 50, + ], + ], + [ + 'handle' => 'title_separator', + 'field' => [ + 'options' => [ + ' | ' => '|', + ' - ' => '-', + ' / ' => '/', + ' :: ' => '::', + ' > ' => '>', + ' ~ ' => '~', + ], + 'clearable' => false, + 'multiple' => false, + 'searchable' => true, + 'localizable' => true, + 'taggable' => false, + 'push_tags' => false, + 'cast_booleans' => false, + 'type' => 'select', + 'instructions' => 'Set the separator of the page title and site name.', + 'width' => 50, + 'listable' => 'hidden', + 'display' => 'Separator', + 'default' => '|', + ], + ], + [ + 'handle' => 'section_schema', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Add basic [JSON-LD](https://developers.google.com/search/docs/guides/intro-structured-data) information about this website.', + 'display' => 'Knowledge Graph', + ], + ], + [ + 'handle' => 'json_ld_type', + 'field' => [ + 'options' => [ + 'none' => 'None', + 'organization' => 'Organization', + 'person' => 'Person', + 'custom' => 'Custom', + ], + 'default' => 'none', + 'localizable' => true, + 'type' => 'button_group', + 'instructions' => 'The type of content this website represents.', + 'listable' => false, + 'display' => 'Type', + 'width' => 50, + ], + ], + [ + 'handle' => 'organization_name', + 'field' => [ + 'input_type' => 'text', + 'type' => 'text', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Organization Name', + 'instructions' => 'Set the name of the organization.', + 'width' => 50, + 'if' => [ + 'json_ld_type' => 'equals organization', + ], + 'validate' => [ + 'required_if:json_ld_type,organization', + ], + ], + ], + [ + 'handle' => 'organization_logo', + 'field' => [ + 'mode' => 'list', + 'container' => 'seo', + 'restrict' => false, + 'allow_uploads' => true, + 'max_files' => 1, + 'type' => 'assets', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Organization Logo', + 'instructions' => 'Add an optional logo with a minimum size of `112x112px`.', + 'validate' => [ + 'image', + ], + 'if' => [ + 'json_ld_type' => 'equals organization', + ], + ], + ], + [ + 'handle' => 'person_name', + 'field' => [ + 'listable' => 'hidden', + 'display' => 'Person Name', + 'instructions' => 'Set the name of the person.', + 'width' => 50, + 'input_type' => 'text', + 'type' => 'text', + 'localizable' => true, + 'if' => [ + 'json_ld_type' => 'equals person', + ], + 'validate' => [ + 'required_if:json_ld_type,person', + ], + ], + ], + [ + 'handle' => 'json_ld', + 'field' => [ + 'theme' => 'material', + 'mode' => 'javascript', + 'indent_type' => 'tabs', + 'indent_size' => 4, + 'key_map' => 'default', + 'line_numbers' => true, + 'line_wrapping' => true, + 'display' => 'JSON-LD Schema', + 'instructions' => 'Add custom [JSON-LD](https://developers.google.com/search/docs/guides/intro-structured-data) you want to include on each entry. This will be wrapped in the appropriate script tag.', + 'type' => 'code', + 'icon' => 'code', + 'listable' => 'hidden', + 'if' => [ + 'json_ld_type' => 'equals custom', + ], + 'validate' => [ + 'required_if:json_ld_type,custom', + ], + ], + ], + [ + 'handle' => 'section_breadcrumbs', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Add [breadcrumbs](https://developers.google.com/search/docs/data-types/breadcrumb) to your entries.', + 'display' => 'Breadcrumbs', + ], + ], + [ + 'handle' => 'breadcrumbs', + 'field' => [ + 'type' => 'toggle', + 'instructions' => 'Add breadcrumbs', + 'listable' => false, + 'localizable' => true, + 'display' => 'Breadcrumbs', + ], + ], + ]; + } +} diff --git a/src/Blueprints/Sections/SeoEntrySection.php b/src/Blueprints/Sections/SeoEntrySection.php new file mode 100644 index 00000000..4bcc50fe --- /dev/null +++ b/src/Blueprints/Sections/SeoEntrySection.php @@ -0,0 +1,369 @@ +fields(); + + if (empty($fields)) { + return []; + } + + return [ + 'display' => 'SEO', + 'fields' => $this->fields(), + ]; + } + + public function fields(): array + { + $fields = collect(); + + $fields->push($this->seoSection()); + + return $fields->flatten(1)->toArray(); + } + + protected function seoSection(): array + { + return [ + [ + 'handle' => 'section_seo_tags', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure the basic SEO Tags of this entry.', + 'display' => 'SEO Tags', + ], + ], + [ + 'handle' => 'seo_title', + 'field' => [ + 'input_type' => 'text', + 'type' => 'text', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Meta Title', + 'character_limit' => 60, + 'instructions' => 'Set the Meta Title of this entry. Defaults to the entry\'s `Title`.', + 'antlers' => false, + 'validate' => [ + 'max:60', + ], + ], + ], + [ + 'handle' => 'seo_description', + 'field' => [ + 'type' => 'textarea', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Meta Description', + 'character_limit' => 160, + 'instructions' => 'Set the Meta Description of this entry.', + 'validate' => [ + 'max:160', + ], + ], + ], + [ + 'handle' => 'section_og', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure the Open Graph settings of this entry.', + 'display' => 'Open Graph', + ], + ], + [ + 'handle' => 'og_title', + 'field' => [ + 'input_type' => 'text', + 'type' => 'text', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Open Graph Title', + 'instructions' => 'Set the Open Graph Title of this entry. Defaults to the entry\'s `Meta Title` or `Title`.', + 'character_limit' => 70, + 'antlers' => false, + 'validate' => [ + 'max:70', + ], + ], + ], + [ + 'handle' => 'og_description', + 'field' => [ + 'type' => 'textarea', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Open Graph Description', + 'character_limit' => '200', + 'instructions' => 'Set the Open Graph Description of this entry. Defaults to the entry\'s `Meta Description`.', + 'width' => 100, + 'validate' => [ + 'max:200', + ], + ], + ], + [ + 'handle' => 'og_image', + 'field' => [ + 'type' => 'assets', + 'mode' => 'list', + 'max_files' => 1, + 'allow_uploads' => true, + 'container' => 'seo', + 'restrict' => true, + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Open Graph Image', + 'folder' => 'social_images', + 'instructions' => 'Add an Open Graph Image for this entry. The recommended size is `1200x630px`.', + 'validate' => [ + 'image', + 'mimes:jpg,png', + ], + ], + ], + [ + 'handle' => 'section_twitter', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure the Twitter settings of this entry.', + 'display' => 'Twitter', + ], + ], + [ + 'handle' => 'twitter_title', + 'field' => [ + 'input_type' => 'text', + 'type' => 'text', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Twitter Title', + 'instructions' => 'Set the Twitter Title of this entry. Defaults to the entry\'s `Meta Title` or `Title`.', + 'character_limit' => 70, + 'antlers' => false, + 'validate' => [ + 'max:70', + ], + ], + ], + [ + 'handle' => 'twitter_description', + 'field' => [ + 'type' => 'textarea', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Twitter Description', + 'character_limit' => '200', + 'instructions' => 'Set the Twitter Description of this entry. Defaults to the entry\'s `Meta Description`.', + 'width' => 100, + 'validate' => [ + 'max:200', + ], + ], + ], + [ + 'handle' => 'twitter_image', + 'field' => [ + 'type' => 'assets', + 'mode' => 'list', + 'max_files' => 1, + 'allow_uploads' => true, + 'container' => 'seo', + 'restrict' => true, + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Twitter Image', + 'folder' => 'social_images', + 'instructions' => 'Add a Twitter Image for this entry with an aspect ratio of `2:1` and minimum size of `300x157px`.', + 'validate' => [ + 'image', + 'mimes:jpg,png', + 'dimensions:min_width=300,min_height=157', + ], + ], + ], + [ + 'handle' => 'section_canonical_url', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure the canonical URL settings for this entry.', + 'display' => 'Canonical URL', + ], + ], + [ + 'handle' => 'seo_canonical_type', + 'field' => [ + 'options' => [ + 'entry' => 'Current Entry', + 'current' => 'Current Domain', + 'external' => 'External Domain', + ], + 'display' => 'Canonical URL', + 'type' => 'button_group', + 'default' => 'entry', + 'icon' => 'button_group', + 'instructions' => 'Where should the canonical URL for this entry point to.', + 'listable' => 'hidden', + ], + ], + [ + 'handle' => 'seo_canonical_current', + 'field' => [ + 'type' => 'entries', + 'max_items' => 1, + 'mode' => 'select', + 'localizable' => true, + 'listable' => 'hidden', + 'display' => 'Canonical URL', + 'instructions' => 'If this is an entry with duplicate content, link to the entry with the original content.', + 'validate' => [ + 'required_if:seo_canonical_type,current', + ], + 'if' => [ + 'seo_canonical_type' => 'equals current', + ], + ], + ], + [ + 'handle' => 'seo_canonical_external', + 'field' => [ + 'input_type' => 'url', + 'display' => 'Canonical URL', + 'type' => 'text', + 'icon' => 'text', + 'listable' => 'hidden', + 'validate' => [ + 'required_if:seo_canonical_type,external', + ], + 'if' => [ + 'seo_canonical_type' => 'equals external', + ], + ], + ], + [ + 'handle' => 'section_indexing', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure the indexing settings for this entry.', + 'display' => 'Indexing', + ], + ], + [ + 'handle' => 'seo_noindex', + 'field' => [ + 'type' => 'toggle', + 'instructions' => 'Prevent this entry from being indexed by search engines.', + 'listable' => 'hidden', + 'width' => 50, + 'display' => 'Noindex', + ], + ], + [ + 'handle' => 'seo_nofollow', + 'field' => [ + 'type' => 'toggle', + 'instructions' => 'Prevent site crawlers from following links in this entry.', + 'listable' => 'hidden', + 'width' => 50, + 'display' => 'Nofollow', + ], + ], + [ + 'handle' => 'section_sitemap', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure the sitemap settings for this entry.', + 'display' => 'Sitemap', + ], + ], + [ + 'handle' => 'sitemap_priority', + 'field' => [ + 'options' => [ + '0.0' => '0.0', + '0.1' => '0.1', + '0.2' => '0.2', + '0.3' => '0.3', + '0.4' => '0.4', + '0.5' => '0.5', + '0.6' => '0.6', + '0.7' => '0.7', + '0.8' => '0.8', + '0.9' => '0.9', + '1.0' => '1.0', + ], + 'clearable' => false, + 'multiple' => false, + 'searchable' => true, + 'taggable' => false, + 'push_tags' => false, + 'cast_booleans' => false, + 'type' => 'select', + 'instructions' => 'Choose the priorty of this entry in the sitemap. `1.0` is the most important.', + 'width' => 50, + 'default' => '0.5', + 'listable' => 'hidden', + 'display' => 'Priority', + ], + ], + [ + 'handle' => 'sitemap_change_frequency', + 'field' => [ + 'options' => [ + 'always' => 'Always', + 'hourly' => 'Hourly', + 'daily' => 'Daily', + 'weekly' => 'Weekly', + 'monthly' => 'Monthly', + 'yearly' => 'Yearly', + 'never' => 'Never', + ], + 'clearable' => false, + 'multiple' => false, + 'searchable' => true, + 'taggable' => false, + 'push_tags' => false, + 'cast_booleans' => false, + 'type' => 'select', + 'instructions' => 'Choose the frequency in which search engines should crawl this entry.', + 'width' => 50, + 'default' => 'weekly', + 'listable' => 'hidden', + 'display' => 'Change Frequency', + ], + ], + [ + 'handle' => 'section_json_ld', + 'field' => [ + 'type' => 'section', + 'display' => 'JSON-ld Schema', + 'instructions' => 'Configure custom [JSON-LD](https://developers.google.com/search/docs/guides/intro-structured-data) for this entry.', + ], + ], + [ + 'handle' => 'json_ld', + 'field' => [ + 'theme' => 'material', + 'mode' => 'javascript', + 'indent_type' => 'tabs', + 'indent_size' => 4, + 'key_map' => 'default', + 'line_numbers' => true, + 'line_wrapping' => true, + 'display' => 'JSON-LD Schema', + 'instructions' => 'Add custom [JSON-LD](https://developers.google.com/search/docs/guides/intro-structured-data) for this entry. This will be wrapped in the appropriate script tag.', + 'type' => 'code', + 'icon' => 'code', + 'listable' => 'hidden', + ], + ], + ]; + } +} diff --git a/src/Blueprints/Sections/SitemapSection.php b/src/Blueprints/Sections/SitemapSection.php new file mode 100644 index 00000000..1f3740df --- /dev/null +++ b/src/Blueprints/Sections/SitemapSection.php @@ -0,0 +1,56 @@ +fields(); + + if (empty($fields)) { + return []; + } + + return [ + 'display' => 'Sitemap', + 'fields' => $this->fields(), + ]; + } + + public function fields(): array + { + $fields = collect(); + + $fields->push($this->sitemapSection()); + + return $fields->flatten(1)->toArray(); + } + + protected function sitemapSection(): array + { + return [ + [ + 'handle' => 'section_sitemap', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure your `sitemap.xml`', + 'display' => 'Sitemap', + ], + ], + [ + 'handle' => 'sitemap_collections', + 'field' => [ + 'mode' => 'select', + 'type' => 'collections', + 'instructions' => 'Select the collections you want to include in your sitemap.', + 'listable' => 'hidden', + 'display' => 'Collections', + 'width' => 50, + ], + ], + ]; + } +} diff --git a/src/Blueprints/Sections/SocialSection.php b/src/Blueprints/Sections/SocialSection.php new file mode 100644 index 00000000..af281660 --- /dev/null +++ b/src/Blueprints/Sections/SocialSection.php @@ -0,0 +1,165 @@ +fields(); + + if (empty($fields)) { + return []; + } + + return [ + 'display' => 'Social', + 'fields' => $this->fields(), + ]; + } + + public function fields(): array + { + $fields = collect(); + + if (config('advanced-seo.social_images.generator', false)) { + $fields->push($this->socialImagesGeneratorSection()); + } + + if (config('advanced-seo.social_images.open_graph', true)) { + $fields->push($this->openGraphSection()); + } + + if (config('advanced-seo.social_images.twitter', true)) { + $fields->push($this->twitterSection()); + } + + return $fields->flatten(1)->toArray(); + } + + protected function socialImagesGeneratorSection(): array + { + return [ + [ + 'handle' => 'section_social_images', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Automatically generate your social images.', + 'display' => 'Generate Social Images', + ], + ], + [ + 'handle' => 'generate_social_images', + 'field' => [ + 'display' => 'Generate Social Images', + 'type' => 'toggle', + 'icon' => 'toggle', + 'instructions' => 'Activate to generate your social images.', + 'listable' => 'hidden', + ], + ], + [ + 'handle' => 'social_images_collections', + 'field' => [ + 'mode' => 'select', + 'display' => 'Collections', + 'type' => 'collections', + 'icon' => 'collections', + 'instructions' => 'Select the collections you want to generate images for.', + 'listable' => 'hidden', + 'width' => 50, + 'if' => [ + 'generate_social_images' => 'equals true', + ], + 'validate' => [ + 'required_if:generate_social_images,true', + ], + ], + ], + ]; + } + + protected function openGraphSection(): array + { + return [ + [ + 'handle' => 'section_og', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure the default Open Graph settings.', + 'display' => 'Open Graph', + ], + ], + [ + 'handle' => 'og_image', + 'field' => [ + 'mode' => 'list', + 'container' => 'seo', + 'restrict' => true, + 'allow_uploads' => true, + 'max_files' => 1, + 'type' => 'assets', + 'localizable' => true, + 'listable' => 'hidden', + 'folder' => 'social_images', + 'display' => 'Open Graph Image', + 'instructions' => 'Add a default Open Graph Image. The recommended size is `1200x630px`.', + 'validate' => [ + 'image', + 'mimes:jpg,png', + ], + ], + ], + ]; + } + + protected function twitterSection(): array + { + return [ + [ + 'handle' => 'section_twitter', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Configure your default Twitter settings.', + 'display' => 'Twitter', + ], + ], + [ + 'handle' => 'twitter_handle', + 'field' => [ + 'listable' => 'hidden', + 'display' => 'Twitter Username', + 'input_type' => 'text', + 'type' => 'text', + 'instructions' => 'Add your Twitter username.', + 'prepend' => '@', + 'antlers' => false, + 'width' => 50, + ], + ], + [ + 'handle' => 'twitter_image', + 'field' => [ + 'mode' => 'list', + 'container' => 'seo', + 'restrict' => true, + 'allow_uploads' => true, + 'max_files' => 1, + 'type' => 'assets', + 'localizable' => true, + 'listable' => 'hidden', + 'folder' => 'social_images', + 'display' => 'Twitter Image', + 'instructions' => 'Add a default Twitter Image with an aspect ratio of `2:1` and minimum size of `300x157px`.', + 'validate' => [ + 'image', + 'mimes:jpg,png', + 'dimensions:min_width=300,min_height=157', + ], + ], + ], + ]; + } +} diff --git a/src/Blueprints/Sections/TrackersSection.php b/src/Blueprints/Sections/TrackersSection.php new file mode 100644 index 00000000..71d810f5 --- /dev/null +++ b/src/Blueprints/Sections/TrackersSection.php @@ -0,0 +1,237 @@ +fields(); + + if (empty($fields)) { + return []; + } + + return [ + 'display' => 'Trackers', + 'fields' => $this->fields(), + ]; + } + + public function fields(): array + { + $fields = collect(); + + if (config('advanced-seo.trackers.site_verification', true)) { + $fields->push($this->siteVerificationSection()); + } + + if (config('advanced-seo.trackers.fathom', true)) { + $fields->push($this->fathomSection()); + } + + if (config('advanced-seo.trackers.cloudflare', true)) { + $fields->push($this->cloudflareSection()); + } + + if (config('advanced-seo.trackers.google_tag_manager', true)) { + $fields->push($this->googleTagManagerSection()); + } + + return $fields->flatten(1)->toArray(); + } + + protected function siteVerificationSection(): array + { + return [ + [ + 'handle' => 'section_verification', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Verify your site ownership.', + 'display' => 'Site verifications', + ], + ], + [ + 'handle' => 'use_google_site_verification', + 'field' => [ + 'type' => 'toggle', + 'instructions' => 'Add the `google-site-verification` meta tag to your head.', + 'listable' => false, + 'display' => 'Google Site Verification', + ], + ], + [ + 'handle' => 'site_verification_code', + 'field' => [ + 'input_type' => 'text', + 'type' => 'text', + 'listable' => 'hidden', + 'width' => 50, + 'display' => 'Verification Code', + 'instructions' => 'Add your site verification code.', + 'validate' => [ + 'required_if:use_google_site_verification,true', + ], + 'if' => [ + 'use_google_site_verification' => 'equals true', + ], + ], + ], + ]; + } + + protected function fathomSection(): array + { + return [ + [ + 'handle' => 'section_fathom', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Use [Fathom](https://usefathom.com) as a privacy-friendly alternative to Google Analytics.', + 'display' => 'Fathom', + ], + ], + [ + 'handle' => 'use_fathom', + 'field' => [ + 'type' => 'toggle', + 'instructions' => 'Add the Fathom tracking script to your head.', + 'listable' => false, + 'display' => 'Fathom', + ], + ], + [ + 'handle' => 'fathom_id', + 'field' => [ + 'width' => 50, + 'display' => 'Site ID', + 'instructions' => 'Add your Site ID.', + 'input_type' => 'text', + 'type' => 'text', + 'listable' => 'hidden', + 'antlers' => true, + 'validate' => [ + 'required_if:use_fathom,true', + ], + 'if' => [ + 'use_fathom' => 'equals true', + ], + ], + ], + [ + 'handle' => 'fathom_domain', + 'field' => [ + 'width' => 50, + 'display' => 'Custom Domain', + 'instructions' => 'Add an optional Custom Domain.', + 'input_type' => 'text', + 'type' => 'text', + 'listable' => 'hidden', + 'antlers' => true, + 'if' => [ + 'use_fathom' => 'equals true', + ], + ], + ], + [ + 'handle' => 'fathom_spa', + 'field' => [ + 'display' => 'SPA Mode', + 'type' => 'toggle', + 'icon' => 'toggle', + 'instructions' => 'Activate if your site is a single page application.', + 'listable' => 'hidden', + 'validate' => [ + 'required_if:use_fathom,true', + ], + 'if' => [ + 'use_fathom' => 'equals true', + ], + ], + ], + ]; + } + + protected function cloudflareSection(): array + { + return [ + [ + 'handle' => 'section_cloudflare_web_analytics', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Use [Cloudflare Web Analytics](https://www.cloudflare.com/web-analytics) as a privacy-friendly alternative to Google Analytics.', + 'display' => 'Cloudflare Web Analytics', + ], + ], + [ + 'handle' => 'use_cloudflare_web_analytics', + 'field' => [ + 'type' => 'toggle', + 'instructions' => 'Add the Cloudflare tracking script to your head.', + 'listable' => false, + 'display' => 'Cloudflare Web Analytics', + ], + ], + [ + 'handle' => 'cloudflare_web_analytics', + 'field' => [ + 'width' => 50, + 'display' => 'Beacon Token', + 'instructions' => 'Add your Beacon Token.', + 'input_type' => 'text', + 'type' => 'text', + 'listable' => 'hidden', + 'validate' => [ + 'required_if:use_cloudflare_web_analytics,true', + ], + 'if' => [ + 'use_cloudflare_web_analytics' => 'equals true', + ], + ], + ], + ]; + } + + protected function googleTagManagerSection(): array + { + return [ + [ + 'handle' => 'section_google_tag_manager', + 'field' => [ + 'type' => 'section', + 'instructions' => 'Use [Google Tag Manager](https://marketingplatform.google.com/about/tag-manager) to track your users. You are `required by privacy law` to get your user\'s consent before loading any tracking scripts. You also need to inform them about what data you collect and what you intent to do with it.', + 'display' => 'Google Tag Manager', + ], + ], + [ + 'handle' => 'use_google_tag_manager', + 'field' => [ + 'type' => 'toggle', + 'instructions' => 'Add the Google Tag Manager tracking scripts.', + 'listable' => false, + 'display' => 'Google Tag Manager', + ], + ], + [ + 'handle' => 'google_tag_manager', + 'field' => [ + 'input_type' => 'text', + 'type' => 'text', + 'listable' => 'hidden', + 'width' => 50, + 'display' => 'Container ID', + 'instructions' => 'Add your Container ID.', + 'validate' => [ + 'required_if:use_google_tag_manager,true', + ], + 'if' => [ + 'use_google_tag_manager' => 'equals true', + ], + ], + ], + ]; + } +} diff --git a/src/Blueprints/SeoGlobalsBlueprint.php b/src/Blueprints/SeoGlobalsBlueprint.php new file mode 100644 index 00000000..11455929 --- /dev/null +++ b/src/Blueprints/SeoGlobalsBlueprint.php @@ -0,0 +1,31 @@ + $this->sections(), + ]; + } + + public function sections(): array + { + return array_filter([ + 'general' => GeneralSection::contents(), + 'favicons' => FaviconsSection::contents(), + 'social' => SocialSection::contents(), + 'trackers' => TrackersSection::contents(), + 'sitemap' => SitemapSection::contents(), + ]); + } +} diff --git a/src/Commands/SetupSeo.php b/src/Commands/SetupSeo.php new file mode 100644 index 00000000..aebac71b --- /dev/null +++ b/src/Commands/SetupSeo.php @@ -0,0 +1,66 @@ +setupGlobals(); + $this->setupAssetContainers(); + + $this->info("Advanced SEO is configured and ready to go!"); + } + + protected function setupGlobals(): void + { + $this->makeGlobal(SeoGlobals::handle(), SeoGlobals::title()); + } + + protected function setupAssetContainers(): void + { + $this->makeAssetContainer('seo', 'SEO'); + } + + protected function makeGlobal(string $handle, string $title): void + { + if (! GlobalSet::findByHandle($handle)) { + $global = GlobalSet::make($handle)->title($title); + + $global->addLocalization($global->makeLocalization(Site::default()->handle())); + + $global->save(); + + $this->line("[✓] Created SEO global in content/globals/$handle.yaml"); + } + } + + protected function makeAssetContainer(string $handle, string $title): void + { + if (! AssetContainer::findByHandle($handle)) { + AssetContainer::make($handle) + ->title($title) + ->disk('assets') + ->allowUploads(true) + ->allowDownloading(true) + ->allowMoving(true) + ->allowRenaming(true) + ->createFolders(true) + ->save(); + + $this->line("[✓] Created $title assets container in content/assets/$handle.yaml"); + } + } +} diff --git a/src/Contracts/Blueprint.php b/src/Contracts/Blueprint.php new file mode 100644 index 00000000..1a3f5f7c --- /dev/null +++ b/src/Contracts/Blueprint.php @@ -0,0 +1,8 @@ +handle; + } + + public function title(): string + { + return $this->title; + } + + public function blueprint(): array + { + return SeoGlobalsBlueprint::contents(); + } +} diff --git a/src/Listeners/AppendEntryBlueprint.php b/src/Listeners/AppendEntryBlueprint.php new file mode 100644 index 00000000..a6e1fe48 --- /dev/null +++ b/src/Listeners/AppendEntryBlueprint.php @@ -0,0 +1,17 @@ +blueprint->contents(); + $blueprint['sections']['seo'] = SeoEntrySection::contents(); + + $event->blueprint->setContents($blueprint); + } +} diff --git a/src/Listeners/AppendSeoGlobalsBlueprint.php b/src/Listeners/AppendSeoGlobalsBlueprint.php new file mode 100644 index 00000000..c871dac5 --- /dev/null +++ b/src/Listeners/AppendSeoGlobalsBlueprint.php @@ -0,0 +1,16 @@ +globals->id() === SeoGlobals::handle()) { + $event->blueprint->setContents(SeoGlobals::blueprint()); + } + } +} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php new file mode 100644 index 00000000..a00d1589 --- /dev/null +++ b/src/ServiceProvider.php @@ -0,0 +1,22 @@ + [ + 'Aerni\AdvancedSeo\Listeners\AppendEntryBlueprint', + ], + 'Statamic\Events\GlobalVariablesBlueprintFound' => [ + 'Aerni\AdvancedSeo\Listeners\AppendSeoGlobalsBlueprint', + ] + ]; +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..e8b655de --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +module.exports = { + mode: 'jit', + prefix: 'as-', + purge: [ + './resources/**/*.html', + './resources/**/*.php', + './resources/**/*.js', + './resources/**/*.vue', + './config/classify.php' + ] +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 00000000..928575d6 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,79 @@ + Statamic::class, + ]; + } + + /** + * Load Environment + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function getEnvironmentSetUp($app): void + { + parent::getEnvironmentSetUp($app); + + $app->make(Manifest::class)->manifest = [ + 'aerni/advanced-seo' => [ + 'id' => 'aerni/advanced-seo', + 'namespace' => 'Aerni\\AdvancedSeo\\', + ], + ]; + } + + /** + * Resolve the application configuration and set the Statamic configuration + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function resolveApplicationConfiguration($app): void + { + parent::resolveApplicationConfiguration($app); + + $configs = [ + 'assets', 'cp', 'forms', 'routes', 'sites', + 'stache', 'static_caching', 'system', 'users', + ]; + + foreach ($configs as $config) { + $app['config']->set("statamic.$config", require(__DIR__ . "/../vendor/statamic/cms/config/{$config}.php")); + } + + $app['config']->set('advanced-seo', require(__DIR__.'/../config/advanced-seo.php')); + } +} diff --git a/webpack.mix.js b/webpack.mix.js new file mode 100644 index 00000000..5d8427de --- /dev/null +++ b/webpack.mix.js @@ -0,0 +1,11 @@ +const mix = require('laravel-mix'); + +mix.setPublicPath('resources/dist') + .js('resources/js/advanced-seo.js', 'js').vue() + .postCss('resources/css/advanced-seo.css', 'css', [ + require('tailwindcss') + ]) + +if (mix.inProduction()) { + mix.version(); +}